@@ -35,6 +35,16 @@ import SwiftSyntax
3535/// 2. | let a = 123
3636/// Ignores `RuleName` and `OtherRuleName` for line 2.
3737///
38+ /// 1. | // swift-format-ignore-file: RuleName
39+ /// 2. | let a = 123
40+ /// 3. | class Foo { }
41+ /// Ignores `RuleName` for the entire file (lines 2-3).
42+ ///
43+ /// 1. | // swift-format-ignore-file: RuleName, OtherRuleName
44+ /// 2. | let a = 123
45+ /// 3. | class Foo { }
46+ /// Ignores `RuleName` and `OtherRuleName` for the entire file (lines 2-3).
47+ ///
3848/// The rules themselves reference RuleMask to see if it is disabled for the line it is currently
3949/// examining.
4050@_spi ( Testing)
@@ -85,6 +95,29 @@ extension SourceRange {
8595 }
8696}
8797
98+ /// Represents the kind of ignore directive encountered in the source.
99+ enum IgnoreDirective : CustomStringConvertible {
100+ /// A node-level directive that disables rules for the following node and its children.
101+ case node
102+ /// A file-level directive that disables rules for the entire file.
103+ case file
104+
105+ var description : String {
106+ switch self {
107+ case . node:
108+ return " swift-format-ignore "
109+ case . file:
110+ return " swift-format-ignore-file "
111+ }
112+ }
113+
114+ /// Regex pattern to match an ignore comment. This pattern supports 0 or more comma delimited rule
115+ /// names. The rule name(s), when present, are in capture group #3.
116+ fileprivate var pattern : String {
117+ return #"^\s*\/\/\s*"# + description + #"((:\s+(([A-z0-9]+[,\s]*)+))?$|\s+$)"#
118+ }
119+ }
120+
88121/// A syntax visitor that finds `SourceRange`s of nodes that have rule status modifying comment
89122/// directives. The changes requested in each comment is parsed and collected into a map to support
90123/// status lookup per rule name.
@@ -106,18 +139,10 @@ fileprivate class RuleStatusCollectionVisitor: SyntaxVisitor {
106139 /// Computes source locations and ranges for syntax nodes in a source file.
107140 private let sourceLocationConverter : SourceLocationConverter
108141
109- /// Regex pattern to match an ignore comment. This pattern supports 0 or more comma delimited rule
110- /// names. The rule name(s), when present, are in capture group #3.
111- private let ignorePattern =
112- #"^\s*\/\/\s*swift-format-ignore((:\s+(([A-z0-9]+[,\s]*)+))?$|\s+$)"#
113-
114- /// Rule ignore regex object.
142+ /// Cached regex object for ignoring rules at the node.
115143 private let ignoreRegex : NSRegularExpression
116144
117- /// Regex pattern to match an ignore comment that applies to an entire file.
118- private let ignoreFilePattern = #"^\s*\/\/\s*swift-format-ignore-file$"#
119-
120- /// Rule ignore regex object.
145+ /// Cached regex object for ignoring rules at the file.
121146 private let ignoreFileRegex : NSRegularExpression
122147
123148 /// Stores the source ranges in which all rules are ignored.
@@ -127,8 +152,8 @@ fileprivate class RuleStatusCollectionVisitor: SyntaxVisitor {
127152 var ruleMap : [ String : [ SourceRange ] ] = [ : ]
128153
129154 init ( sourceLocationConverter: SourceLocationConverter ) {
130- ignoreRegex = try ! NSRegularExpression ( pattern: ignorePattern , options: [ ] )
131- ignoreFileRegex = try ! NSRegularExpression ( pattern: ignoreFilePattern , options: [ ] )
155+ ignoreRegex = try ! NSRegularExpression ( pattern: IgnoreDirective . node . pattern , options: [ ] )
156+ ignoreFileRegex = try ! NSRegularExpression ( pattern: IgnoreDirective . file . pattern , options: [ ] )
132157
133158 self . sourceLocationConverter = sourceLocationConverter
134159 super. init ( viewMode: . sourceAccurate)
@@ -140,40 +165,28 @@ fileprivate class RuleStatusCollectionVisitor: SyntaxVisitor {
140165 guard let firstToken = node. firstToken ( viewMode: . sourceAccurate) else {
141166 return . visitChildren
142167 }
143- let comments = loneLineComments ( in: firstToken. leadingTrivia, isFirstToken: true )
144- var foundIgnoreFileComment = false
145- for comment in comments {
146- let range = NSRange ( comment. startIndex..< comment. endIndex, in: comment)
147- if ignoreFileRegex. firstMatch ( in: comment, options: [ ] , range: range) != nil {
148- foundIgnoreFileComment = true
149- break
150- }
151- }
152- guard foundIgnoreFileComment else {
153- return . visitChildren
154- }
155-
156168 let sourceRange = node. sourceRange (
157169 converter: sourceLocationConverter,
158170 afterLeadingTrivia: false ,
159171 afterTrailingTrivia: true
160172 )
161- allRulesIgnoredRanges. append ( sourceRange)
162- return . skipChildren
173+ return appendRuleStatus ( from: firstToken, of: sourceRange, using: ignoreFileRegex)
163174 }
164175
165176 override func visit( _ node: CodeBlockItemSyntax ) -> SyntaxVisitorContinueKind {
166177 guard let firstToken = node. firstToken ( viewMode: . sourceAccurate) else {
167178 return . visitChildren
168179 }
169- return appendRuleStatusDirectives ( from: firstToken, of: Syntax ( node) )
180+ let sourceRange = node. sourceRange ( converter: sourceLocationConverter)
181+ return appendRuleStatus ( from: firstToken, of: sourceRange, using: ignoreRegex)
170182 }
171183
172184 override func visit( _ node: MemberBlockItemSyntax ) -> SyntaxVisitorContinueKind {
173185 guard let firstToken = node. firstToken ( viewMode: . sourceAccurate) else {
174186 return . visitChildren
175187 }
176- return appendRuleStatusDirectives ( from: firstToken, of: Syntax ( node) )
188+ let sourceRange = node. sourceRange ( converter: sourceLocationConverter)
189+ return appendRuleStatus ( from: firstToken, of: sourceRange, using: ignoreRegex)
177190 }
178191
179192 // MARK: - Helper Methods
@@ -183,17 +196,19 @@ fileprivate class RuleStatusCollectionVisitor: SyntaxVisitor {
183196 ///
184197 /// - Parameters:
185198 /// - token: A token that may have comments that modify the status of rules.
186- /// - node: The node to which the token belongs.
187- private func appendRuleStatusDirectives(
199+ /// - sourceRange: The range covering the node to which `token` belongs. If an ignore directive
200+ /// is found among the comments, this entire range is used to ignore the specified rules.
201+ /// - regex: The regular expression used to detect ignore directives.
202+ private func appendRuleStatus(
188203 from token: TokenSyntax ,
189- of node: Syntax
204+ of sourceRange: SourceRange ,
205+ using regex: NSRegularExpression
190206 ) -> SyntaxVisitorContinueKind {
191207 let isFirstInFile = token. previousToken ( viewMode: . sourceAccurate) == nil
192- let matches = loneLineComments ( in: token. leadingTrivia, isFirstToken: isFirstInFile)
193- . compactMap ( ruleStatusDirectiveMatch)
194- let sourceRange = node. sourceRange ( converter: sourceLocationConverter)
195- for match in matches {
196- switch match {
208+ let comments = loneLineComments ( in: token. leadingTrivia, isFirstToken: isFirstInFile)
209+ for comment in comments {
210+ guard let matchResult = ruleStatusDirectiveMatch ( in: comment, using: regex) else { continue }
211+ switch matchResult {
197212 case . all:
198213 allRulesIgnoredRanges. append ( sourceRange)
199214
@@ -210,9 +225,12 @@ fileprivate class RuleStatusCollectionVisitor: SyntaxVisitor {
210225
211226 /// Checks if a comment containing the given text matches a rule status directive. When it does
212227 /// match, its contents (e.g. list of rule names) are returned.
213- private func ruleStatusDirectiveMatch( in text: String ) -> RuleStatusDirectiveMatch ? {
228+ private func ruleStatusDirectiveMatch(
229+ in text: String ,
230+ using regex: NSRegularExpression
231+ ) -> RuleStatusDirectiveMatch ? {
214232 let textRange = NSRange ( text. startIndex..< text. endIndex, in: text)
215- guard let match = ignoreRegex . firstMatch ( in: text, options: [ ] , range: textRange) else {
233+ guard let match = regex . firstMatch ( in: text, options: [ ] , range: textRange) else {
216234 return nil
217235 }
218236 guard match. numberOfRanges == 5 else { return . all }
0 commit comments