1
- import Foundation
2
- import SourceKittenFramework
1
+ import SwiftLintCore
2
+ import SwiftSyntax
3
3
4
- struct VerticalWhitespaceClosingBracesRule : CorrectableRule , OptInRule {
4
+ @SwiftSyntaxRule ( correctable: true , optIn: true )
5
+ struct VerticalWhitespaceClosingBracesRule : Rule {
5
6
var configuration = VerticalWhitespaceClosingBracesConfiguration ( )
6
7
7
8
static let description = RuleDescription (
@@ -14,52 +15,304 @@ struct VerticalWhitespaceClosingBracesRule: CorrectableRule, OptInRule {
14
15
triggeringExamples: Array ( VerticalWhitespaceClosingBracesRuleExamples . violatingToValidExamples. keys. sorted ( ) ) ,
15
16
corrections: VerticalWhitespaceClosingBracesRuleExamples . violatingToValidExamples. removingViolationMarkers ( )
16
17
)
18
+ }
19
+
20
+ private struct TriviaAnalysis {
21
+ var consecutiveNewlines = 0
22
+ var violationStartPosition : AbsolutePosition ?
23
+ var violationEndPosition : AbsolutePosition ?
24
+ }
17
25
18
- private let pattern = " ((?: \\ n[ \\ t]*)+)( \\ n[ \\ t]*[)} \\ ]]) "
19
- private let trivialLinePattern = " ((?: \\ n[ \\ t]*)+)( \\ n[ \\ t)} \\ ]]*$) "
26
+ private struct CorrectionState {
27
+ var result = [ TriviaPiece] ( )
28
+ var consecutiveNewlines = 0
29
+ var pendingWhitespace = [ TriviaPiece] ( )
30
+ var correctionCount = 0
31
+ var hasViolation = false
32
+ }
20
33
21
- func validate( file: SwiftLintFile ) -> [ StyleViolation ] {
22
- let pattern = configuration. onlyEnforceBeforeTrivialLines ? self . trivialLinePattern : self . pattern
34
+ private struct NewlineProcessingContext {
35
+ let currentPosition : AbsolutePosition
36
+ let consecutiveNewlines : Int
37
+ var violationStartPosition : AbsolutePosition ?
38
+ var violationEndPosition : AbsolutePosition ?
39
+ }
23
40
24
- let patternRegex : NSRegularExpression = regex ( pattern)
41
+ private func isTokenLineTrivialHelper(
42
+ for token: TokenSyntax ,
43
+ file: SwiftLintFile ,
44
+ locationConverter: SourceLocationConverter
45
+ ) -> Bool {
46
+ let lineColumn = locationConverter. location ( for: token. positionAfterSkippingLeadingTrivia)
47
+ let line = lineColumn. line
25
48
26
- return file. violatingRanges ( for: pattern) . map { violationRange in
27
- let substring = file. contents. substring ( from: violationRange. location, length: violationRange. length)
28
- let matchResult = patternRegex. firstMatch ( in: substring, options: [ ] , range: substring. fullNSRange) !
29
- let violatingSubrange = matchResult. range ( at: 1 )
30
- let characterOffset = violationRange. location + violatingSubrange. location + 1
49
+ guard let lineContent = file. lines. first ( where: { $0. index == line } ) ? . content else {
50
+ return false
51
+ }
31
52
32
- return StyleViolation (
33
- ruleDescription: Self . description,
34
- severity: configuration. severityConfiguration. severity,
35
- location: Location ( file: file, characterOffset: characterOffset)
53
+ let trimmedLine = lineContent. trimmingCharacters ( in: . whitespaces)
54
+ let closingBraces : Set < Character > = [ " ] " , " } " , " ) " ]
55
+ return !trimmedLine. isEmpty && trimmedLine. allSatisfy { closingBraces. contains ( $0) }
56
+ }
57
+
58
+ private extension VerticalWhitespaceClosingBracesRule {
59
+ final class Visitor : ViolationsSyntaxVisitor < VerticalWhitespaceClosingBracesConfiguration > {
60
+ override func visitPost( _ node: TokenSyntax ) {
61
+ guard node. isClosingBrace else {
62
+ return
63
+ }
64
+
65
+ let triviaAnalysis = analyzeTriviaForViolations (
66
+ trivia: node. leadingTrivia,
67
+ token: node,
68
+ position: node. position
36
69
)
70
+
71
+ if let violation = triviaAnalysis {
72
+ violations. append (
73
+ ReasonedRuleViolation (
74
+ position: violation. position,
75
+ correction: . init(
76
+ start: violation. position,
77
+ end: violation. endPosition,
78
+ replacement: " "
79
+ )
80
+ )
81
+ )
82
+ }
83
+ }
84
+
85
+ private func analyzeTriviaForViolations(
86
+ trivia: Trivia ,
87
+ token: TokenSyntax ,
88
+ position: AbsolutePosition
89
+ ) -> ( position: AbsolutePosition , endPosition: AbsolutePosition ) ? {
90
+ let analysis = analyzeTrivia ( trivia: trivia, startPosition: position)
91
+
92
+ guard let startPos = analysis. violationStartPosition,
93
+ let endPos = analysis. violationEndPosition,
94
+ analysis. consecutiveNewlines >= 2 else {
95
+ return nil
96
+ }
97
+
98
+ if configuration. onlyEnforceBeforeTrivialLines &&
99
+ !isTokenLineTrivialHelper( for: token, file: file, locationConverter: locationConverter) {
100
+ return nil
101
+ }
102
+
103
+ return ( position: startPos, endPosition: endPos)
104
+ }
105
+
106
+ private func analyzeTrivia(
107
+ trivia: Trivia ,
108
+ startPosition: AbsolutePosition
109
+ ) -> TriviaAnalysis {
110
+ var result = TriviaAnalysis ( )
111
+ var currentPosition = startPosition
112
+
113
+ for piece in trivia {
114
+ let ( newlines, positionAdvance) = processTriviaPiece (
115
+ piece: piece,
116
+ currentPosition: currentPosition,
117
+ consecutiveNewlines: result. consecutiveNewlines,
118
+ violationStartPosition: & result. violationStartPosition,
119
+ violationEndPosition: & result. violationEndPosition
120
+ )
121
+ result. consecutiveNewlines = newlines
122
+ currentPosition = currentPosition. advanced ( by: positionAdvance)
123
+ }
124
+
125
+ return result
126
+ }
127
+
128
+ private func processTriviaPiece(
129
+ piece: TriviaPiece ,
130
+ currentPosition: AbsolutePosition ,
131
+ consecutiveNewlines: Int ,
132
+ violationStartPosition: inout AbsolutePosition ? ,
133
+ violationEndPosition: inout AbsolutePosition ?
134
+ ) -> ( newlines: Int , positionAdvance: Int ) {
135
+ switch piece {
136
+ case . newlines( let count) , . carriageReturns( let count) :
137
+ var context = NewlineProcessingContext (
138
+ currentPosition: currentPosition,
139
+ consecutiveNewlines: consecutiveNewlines,
140
+ violationStartPosition: violationStartPosition,
141
+ violationEndPosition: violationEndPosition
142
+ )
143
+ let result = processNewlines (
144
+ count: count,
145
+ bytesPerNewline: 1 ,
146
+ context: & context
147
+ )
148
+ violationStartPosition = context. violationStartPosition
149
+ violationEndPosition = context. violationEndPosition
150
+ return result
151
+ case . carriageReturnLineFeeds( let count) :
152
+ var context = NewlineProcessingContext (
153
+ currentPosition: currentPosition,
154
+ consecutiveNewlines: consecutiveNewlines,
155
+ violationStartPosition: violationStartPosition,
156
+ violationEndPosition: violationEndPosition
157
+ )
158
+ let result = processNewlines (
159
+ count: count,
160
+ bytesPerNewline: 2 ,
161
+ context: & context
162
+ )
163
+ violationStartPosition = context. violationStartPosition
164
+ violationEndPosition = context. violationEndPosition
165
+ return result
166
+ case . spaces, . tabs:
167
+ return ( consecutiveNewlines, piece. sourceLength. utf8Length)
168
+ default :
169
+ // Any other trivia breaks the sequence
170
+ violationStartPosition = nil
171
+ violationEndPosition = nil
172
+ return ( 0 , piece. sourceLength. utf8Length)
173
+ }
37
174
}
38
- }
39
175
40
- func correct( file: SwiftLintFile ) -> Int {
41
- let pattern = configuration. onlyEnforceBeforeTrivialLines ? self . trivialLinePattern : self . pattern
42
- let violatingRanges = file. ruleEnabled ( violatingRanges: file. violatingRanges ( for: pattern) , for: self )
43
- guard violatingRanges. isNotEmpty else {
44
- return 0
176
+ private func processNewlines(
177
+ count: Int ,
178
+ bytesPerNewline: Int ,
179
+ context: inout NewlineProcessingContext
180
+ ) -> ( newlines: Int , positionAdvance: Int ) {
181
+ var newConsecutiveNewlines = context. consecutiveNewlines
182
+ var totalAdvance = 0
183
+
184
+ for _ in 0 ..< count {
185
+ newConsecutiveNewlines += 1
186
+ // violationStartPosition marks the beginning of the first newline
187
+ // that constitutes an empty line (i.e., the second in a sequence of \n\n).
188
+ if newConsecutiveNewlines == 2 && context. violationStartPosition == nil {
189
+ context. violationStartPosition = context. currentPosition. advanced ( by: totalAdvance)
190
+ }
191
+ // violationEndPosition tracks the end of the last newline in any sequence of >= 2 newlines.
192
+ if newConsecutiveNewlines >= 2 {
193
+ context. violationEndPosition = context. currentPosition. advanced ( by: totalAdvance + bytesPerNewline)
194
+ }
195
+ totalAdvance += bytesPerNewline
196
+ }
197
+
198
+ return ( newConsecutiveNewlines, totalAdvance)
45
199
}
46
- let patternRegex = regex ( pattern)
47
- var fileContents = file. contents
48
- for violationRange in violatingRanges. reversed ( ) {
49
- fileContents = patternRegex. stringByReplacingMatches (
50
- in: fileContents,
51
- options: [ ] ,
52
- range: violationRange,
53
- withTemplate: " $2 "
200
+ }
201
+
202
+ final class Rewriter : ViolationsSyntaxRewriter < VerticalWhitespaceClosingBracesConfiguration > {
203
+ override func visit( _ token: TokenSyntax ) -> TokenSyntax {
204
+ guard token. isClosingBrace else {
205
+ return super. visit ( token)
206
+ }
207
+
208
+ let correctedTrivia = correctTrivia (
209
+ trivia: token. leadingTrivia,
210
+ token: token
54
211
)
212
+
213
+ if correctedTrivia. hasCorrections {
214
+ numberOfCorrections += correctedTrivia. correctionCount
215
+ return super. visit ( token. with ( \. leadingTrivia, correctedTrivia. trivia) )
216
+ }
217
+
218
+ return super. visit ( token)
219
+ }
220
+
221
+ private func correctTrivia(
222
+ trivia: Trivia ,
223
+ token: TokenSyntax
224
+ ) -> ( trivia: Trivia , hasCorrections: Bool , correctionCount: Int ) {
225
+ // First check if we should apply corrections
226
+ if configuration. onlyEnforceBeforeTrivialLines &&
227
+ !isTokenLineTrivialHelper( for: token, file: file, locationConverter: locationConverter) {
228
+ return ( trivia: trivia, hasCorrections: false , correctionCount: 0 )
229
+ }
230
+
231
+ var state = CorrectionState ( )
232
+
233
+ for piece in trivia {
234
+ processPieceForCorrection ( piece: piece, state: & state)
235
+ }
236
+
237
+ // Add any remaining whitespace
238
+ state. result. append ( contentsOf: state. pendingWhitespace)
239
+
240
+ return ( trivia: Trivia ( pieces: state. result) ,
241
+ hasCorrections: state. correctionCount > 0 ,
242
+ correctionCount: state. correctionCount)
243
+ }
244
+
245
+ private func processPieceForCorrection( piece: TriviaPiece , state: inout CorrectionState ) {
246
+ switch piece {
247
+ case . newlines( let count) , . carriageReturns( let count) :
248
+ let newlineCreator = piece. isNewline ? TriviaPiece . newlines : TriviaPiece . carriageReturns
249
+ processNewlinesForCorrection (
250
+ count: count,
251
+ newlineCreator: { newlineCreator ( $0) } ,
252
+ state: & state
253
+ )
254
+ case . carriageReturnLineFeeds( let count) :
255
+ processNewlinesForCorrection (
256
+ count: count,
257
+ newlineCreator: { TriviaPiece . carriageReturnLineFeeds ( $0) } ,
258
+ state: & state
259
+ )
260
+ case . spaces, . tabs:
261
+ // Only keep whitespace if we haven't seen a violation yet
262
+ if !state. hasViolation {
263
+ state. pendingWhitespace. append ( piece)
264
+ }
265
+ default :
266
+ // Other trivia breaks the sequence
267
+ state. consecutiveNewlines = 0
268
+ state. hasViolation = false
269
+ state. result. append ( contentsOf: state. pendingWhitespace)
270
+ state. result. append ( piece)
271
+ state. pendingWhitespace. removeAll ( )
272
+ }
273
+ }
274
+
275
+ private func processNewlinesForCorrection(
276
+ count: Int ,
277
+ newlineCreator: ( Int ) -> TriviaPiece ,
278
+ state: inout CorrectionState
279
+ ) {
280
+ for _ in 0 ..< count {
281
+ state. consecutiveNewlines += 1
282
+ if state. consecutiveNewlines == 1 {
283
+ // First newline - always keep it with any preceding whitespace
284
+ state. result. append ( contentsOf: state. pendingWhitespace)
285
+ state. result. append ( newlineCreator ( 1 ) )
286
+ state. pendingWhitespace. removeAll ( )
287
+ } else {
288
+ // Additional newlines - these form empty lines and should be removed
289
+ state. hasViolation = true
290
+ state. correctionCount += 1
291
+ state. pendingWhitespace. removeAll ( )
292
+ }
293
+ }
55
294
}
56
- file. write ( fileContents)
57
- return violatingRanges. count
58
295
}
59
296
}
60
297
61
- private extension SwiftLintFile {
62
- func violatingRanges( for pattern: String ) -> [ NSRange ] {
63
- match ( pattern: pattern, excludingSyntaxKinds: SyntaxKind . commentAndStringKinds)
298
+ private extension TokenSyntax {
299
+ var isClosingBrace : Bool {
300
+ switch tokenKind {
301
+ case . rightBrace, . rightParen, . rightSquare:
302
+ return true
303
+ default :
304
+ return false
305
+ }
306
+ }
307
+ }
308
+
309
+ private extension TriviaPiece {
310
+ var isNewline : Bool {
311
+ switch self {
312
+ case . newlines, . carriageReturns, . carriageReturnLineFeeds:
313
+ return true
314
+ default :
315
+ return false
316
+ }
64
317
}
65
318
}
0 commit comments