@@ -35,6 +35,16 @@ import SwiftSyntax
35
35
/// 2. | let a = 123
36
36
/// Ignores `RuleName` and `OtherRuleName` for line 2.
37
37
///
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
+ ///
38
48
/// The rules themselves reference RuleMask to see if it is disabled for the line it is currently
39
49
/// examining.
40
50
@_spi ( Testing)
@@ -85,6 +95,29 @@ extension SourceRange {
85
95
}
86
96
}
87
97
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
+
88
121
/// A syntax visitor that finds `SourceRange`s of nodes that have rule status modifying comment
89
122
/// directives. The changes requested in each comment is parsed and collected into a map to support
90
123
/// status lookup per rule name.
@@ -106,18 +139,10 @@ fileprivate class RuleStatusCollectionVisitor: SyntaxVisitor {
106
139
/// Computes source locations and ranges for syntax nodes in a source file.
107
140
private let sourceLocationConverter : SourceLocationConverter
108
141
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.
115
143
private let ignoreRegex : NSRegularExpression
116
144
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.
121
146
private let ignoreFileRegex : NSRegularExpression
122
147
123
148
/// Stores the source ranges in which all rules are ignored.
@@ -127,8 +152,8 @@ fileprivate class RuleStatusCollectionVisitor: SyntaxVisitor {
127
152
var ruleMap : [ String : [ SourceRange ] ] = [ : ]
128
153
129
154
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: [ ] )
132
157
133
158
self . sourceLocationConverter = sourceLocationConverter
134
159
super. init ( viewMode: . sourceAccurate)
@@ -140,40 +165,28 @@ fileprivate class RuleStatusCollectionVisitor: SyntaxVisitor {
140
165
guard let firstToken = node. firstToken ( viewMode: . sourceAccurate) else {
141
166
return . visitChildren
142
167
}
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
-
156
168
let sourceRange = node. sourceRange (
157
169
converter: sourceLocationConverter,
158
170
afterLeadingTrivia: false ,
159
171
afterTrailingTrivia: true
160
172
)
161
- allRulesIgnoredRanges. append ( sourceRange)
162
- return . skipChildren
173
+ return appendRuleStatus ( from: firstToken, of: sourceRange, using: ignoreFileRegex)
163
174
}
164
175
165
176
override func visit( _ node: CodeBlockItemSyntax ) -> SyntaxVisitorContinueKind {
166
177
guard let firstToken = node. firstToken ( viewMode: . sourceAccurate) else {
167
178
return . visitChildren
168
179
}
169
- return appendRuleStatusDirectives ( from: firstToken, of: Syntax ( node) )
180
+ let sourceRange = node. sourceRange ( converter: sourceLocationConverter)
181
+ return appendRuleStatus ( from: firstToken, of: sourceRange, using: ignoreRegex)
170
182
}
171
183
172
184
override func visit( _ node: MemberBlockItemSyntax ) -> SyntaxVisitorContinueKind {
173
185
guard let firstToken = node. firstToken ( viewMode: . sourceAccurate) else {
174
186
return . visitChildren
175
187
}
176
- return appendRuleStatusDirectives ( from: firstToken, of: Syntax ( node) )
188
+ let sourceRange = node. sourceRange ( converter: sourceLocationConverter)
189
+ return appendRuleStatus ( from: firstToken, of: sourceRange, using: ignoreRegex)
177
190
}
178
191
179
192
// MARK: - Helper Methods
@@ -183,17 +196,19 @@ fileprivate class RuleStatusCollectionVisitor: SyntaxVisitor {
183
196
///
184
197
/// - Parameters:
185
198
/// - 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(
188
203
from token: TokenSyntax ,
189
- of node: Syntax
204
+ of sourceRange: SourceRange ,
205
+ using regex: NSRegularExpression
190
206
) -> SyntaxVisitorContinueKind {
191
207
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 {
197
212
case . all:
198
213
allRulesIgnoredRanges. append ( sourceRange)
199
214
@@ -210,9 +225,12 @@ fileprivate class RuleStatusCollectionVisitor: SyntaxVisitor {
210
225
211
226
/// Checks if a comment containing the given text matches a rule status directive. When it does
212
227
/// 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 ? {
214
232
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 {
216
234
return nil
217
235
}
218
236
guard match. numberOfRanges == 5 else { return . all }
0 commit comments