@@ -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