1- import Foundation
2- import SourceKittenFramework
1+ import SwiftLintCore
2+ import SwiftSyntax
33
4- struct VerticalWhitespaceClosingBracesRule : CorrectableRule , OptInRule {
4+ @SwiftSyntaxRule ( correctable: true , optIn: true )
5+ struct VerticalWhitespaceClosingBracesRule : Rule {
56 var configuration = VerticalWhitespaceClosingBracesConfiguration ( )
67
78 static let description = RuleDescription (
@@ -14,52 +15,304 @@ struct VerticalWhitespaceClosingBracesRule: CorrectableRule, OptInRule {
1415 triggeringExamples: Array ( VerticalWhitespaceClosingBracesRuleExamples . violatingToValidExamples. keys. sorted ( ) ) ,
1516 corrections: VerticalWhitespaceClosingBracesRuleExamples . violatingToValidExamples. removingViolationMarkers ( )
1617 )
18+ }
19+
20+ private struct TriviaAnalysis {
21+ var consecutiveNewlines = 0
22+ var violationStartPosition : AbsolutePosition ?
23+ var violationEndPosition : AbsolutePosition ?
24+ }
1725
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+ }
2033
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+ }
2340
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
2548
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+ }
3152
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
3669 )
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+ }
37174 }
38- }
39175
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)
45199 }
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
54211 )
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+ }
55294 }
56- file. write ( fileContents)
57- return violatingRanges. count
58295 }
59296}
60297
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+ }
64317 }
65318}
0 commit comments