11import Foundation
2- import SourceKittenFramework
2+ import SwiftSyntax
33
4+ @SwiftSyntaxRule ( explicitRewriter: true )
45struct MarkRule : CorrectableRule {
56var configuration = SeverityConfiguration < Self > ( . warning)
67
@@ -9,239 +10,130 @@ struct MarkRule: CorrectableRule {
910 name: " Mark " ,
1011 description: " MARK comment should be in valid format. e.g. '// MARK: ...' or '// MARK: - ...' " ,
1112 kind: . lint,
12- nonTriggeringExamples: [
13- Example ( " // MARK: good " ) ,
14- Example ( " // MARK: - good " ) ,
15- Example ( " // MARK: - " ) ,
16- Example ( " // BOOKMARK " ) ,
17- Example ( " //BOOKMARK " ) ,
18- Example ( " // BOOKMARKS " ) ,
19- issue1749Example
20- ] ,
21- triggeringExamples: [
22- Example ( " ↓//MARK: bad " ) ,
23- Example ( " ↓// MARK:bad " ) ,
24- Example ( " ↓//MARK:bad " ) ,
25- Example ( " ↓// MARK: bad " ) ,
26- Example ( " ↓// MARK: bad " ) ,
27- Example ( " ↓// MARK: -bad " ) ,
28- Example ( " ↓// MARK:- bad " ) ,
29- Example ( " ↓// MARK:-bad " ) ,
30- Example ( " ↓//MARK: - bad " ) ,
31- Example ( " ↓//MARK:- bad " ) ,
32- Example ( " ↓//MARK: -bad " ) ,
33- Example ( " ↓//MARK:-bad " ) ,
34- Example ( " ↓//Mark: bad " ) ,
35- Example ( " ↓// Mark: bad " ) ,
36- Example ( " ↓// MARK bad " ) ,
37- Example ( " ↓//MARK bad " ) ,
38- Example ( " ↓// MARK - bad " ) ,
39- Example ( " ↓//MARK : bad " ) ,
40- Example ( " ↓// MARKL: " ) ,
41- Example ( " ↓// MARKR " ) ,
42- Example ( " ↓// MARKK - " ) ,
43- Example ( " ↓/// MARK: " ) ,
44- Example ( " ↓/// MARK bad " ) ,
45- issue1029Example
46- ] ,
47- corrections: [
48- Example ( " ↓//MARK: comment " ) : Example ( " // MARK: comment " ) ,
49- Example ( " ↓// MARK: comment " ) : Example ( " // MARK: comment " ) ,
50- Example ( " ↓// MARK:comment " ) : Example ( " // MARK: comment " ) ,
51- Example ( " ↓// MARK: comment " ) : Example ( " // MARK: comment " ) ,
52- Example ( " ↓//MARK: - comment " ) : Example ( " // MARK: - comment " ) ,
53- Example ( " ↓// MARK:- comment " ) : Example ( " // MARK: - comment " ) ,
54- Example ( " ↓// MARK: -comment " ) : Example ( " // MARK: - comment " ) ,
55- Example ( " ↓// MARK: - comment " ) : Example ( " // MARK: - comment " ) ,
56- Example ( " ↓// Mark: comment " ) : Example ( " // MARK: comment " ) ,
57- Example ( " ↓// Mark: - comment " ) : Example ( " // MARK: - comment " ) ,
58- Example ( " ↓// MARK - comment " ) : Example ( " // MARK: - comment " ) ,
59- Example ( " ↓// MARK : comment " ) : Example ( " // MARK: comment " ) ,
60- Example ( " ↓// MARKL: " ) : Example ( " // MARK: " ) ,
61- Example ( " ↓// MARKL: - " ) : Example ( " // MARK: - " ) ,
62- Example ( " ↓// MARKK " ) : Example ( " // MARK: " ) ,
63- Example ( " ↓// MARKK - " ) : Example ( " // MARK: - " ) ,
64- Example ( " ↓/// MARK: " ) : Example ( " // MARK: " ) ,
65- Example ( " ↓/// MARK comment " ) : Example ( " // MARK: comment " ) ,
66- issue1029Example: issue1029Correction,
67- issue1749Example: issue1749Correction
68- ]
13+ nonTriggeringExamples: MarkRuleExamples . nonTriggeringExamples,
14+ triggeringExamples: MarkRuleExamples . triggeringExamples,
15+ corrections: MarkRuleExamples . corrections
6916)
17+ }
7018
71- private let spaceStartPattern = " (?: \( nonSpaceOrTwoOrMoreSpace) \( mark) ) "
72-
73- private let endNonSpacePattern = " (?: \( mark) \( nonSpace) ) "
74- private let endTwoOrMoreSpacePattern = " (?: \( mark) \( twoOrMoreSpace) ) "
75-
76- private let invalidEndSpacesPattern = " (?: \( mark) \( nonSpaceOrTwoOrMoreSpace) ) "
77-
78- private let twoOrMoreSpacesAfterHyphenPattern = " (?: \( mark) - \( twoOrMoreSpace) ) "
79- private let nonSpaceOrNewlineAfterHyphenPattern = " (?: \( mark) -[^ \n ]) "
80-
81- private let invalidSpacesAfterHyphenPattern = " (?: \( mark) - \( nonSpaceOrTwoOrMoreSpaceOrNewline) ) "
82-
83- private let invalidLowercasePattern = " (?:// ?[Mm]ark:) "
84-
85- private let missingColonPattern = " (?:// ?MARK[^:]) "
86- // The below patterns more specifically describe some of the above pattern's failure cases for correction.
87- private let oneOrMoreSpacesBeforeColonPattern = " (?:// ?MARK +:) "
88- private let nonWhitespaceBeforeColonPattern = " (?:// ?MARK \\ S+:) "
89- private let nonWhitespaceNorColonBeforeSpacesPattern = " (?:// ?MARK[^ \\ s:]* +) "
90- private let threeSlashesInsteadOfTwo = " /// MARK:? "
91-
92- private var pattern : String {
93- return [
94- spaceStartPattern,
95- invalidEndSpacesPattern,
96- invalidSpacesAfterHyphenPattern,
97- invalidLowercasePattern,
98- missingColonPattern,
99- threeSlashesInsteadOfTwo
100- ] . joined ( separator: " | " )
19+ private extension MarkRule {
20+ final class Visitor : ViolationsSyntaxVisitor < ConfigurationType > {
21+ override func visitPost( _ node: TokenSyntax ) {
22+ for result in node. violationResults ( ) {
23+ violations. append ( result. position)
24+ }
25+ }
10126}
10227
103- func validate( file: SwiftLintFile ) -> [ StyleViolation ] {
104- return violationRanges ( in: file, matching: pattern) . map {
105- StyleViolation ( ruleDescription: Self . description,
106- severity: configuration. severity,
107- location: Location ( file: file, characterOffset: $0. location) )
28+ final class Rewriter : ViolationsSyntaxRewriter {
29+ override func visit( _ token: TokenSyntax ) -> TokenSyntax {
30+ var pieces = token. leadingTrivia. pieces
31+ for result in token. violationResults ( ) {
32+ // caution: `correctionPositions` records the positions before the mutations.
33+ // https://github.com/realm/SwiftLint/pull/4297
34+ correctionPositions. append ( result. position)
35+ result. correct ( & pieces)
36+ }
37+ return super. visit ( token. with ( \. leadingTrivia, Trivia ( pieces: pieces) ) )
10838}
10939}
40+ }
11041
111- func correct( file: SwiftLintFile ) -> [ Correction ] {
112- var result = [ Correction] ( )
42+ private struct ViolationResult {
43+ let position : AbsolutePosition
44+ let correct : ( inout [ TriviaPiece ] ) -> Void
45+ }
11346
114- result. append ( contentsOf: correct ( file: file,
115- pattern: spaceStartPattern,
116- replaceString: " // MARK: " ) )
47+ private extension TokenSyntax {
48+ private enum Mark {
49+ static func lint( in text: String ) -> [ ( ) -> String ] {
50+ let range = NSRange ( text. startIndex..< text. endIndex, in: text)
51+ return regex ( badPattern) . matches ( in: text, options: [ ] , range: range) . compactMap { matchin
52+ isIgnoredCases ( text, range: range) ? nil : {
53+ var corrected = replace ( text, range: match. range ( at: 2 ) , to: " - " )
54+ corrected= replace ( corrected, range: match. range ( at: 1 ) , to: " // MARK: " )
55+ if !text. hasSuffix ( " " ) , corrected. hasSuffix ( " " ) {
56+ corrected. removeLast ( )
57+ }
58+ return corrected
59+ }
60+ }
61+ }
11762
118- result. append ( contentsOf: correct ( file: file,
119- pattern: endNonSpacePattern,
120- replaceString: " // MARK: " ,
121- keepLastChar: true ) )
63+ private static func isIgnoredCases( _ text: String , range: NSRange ) -> Bool {
64+ regex ( goodPattern) . firstMatch ( in: text, range: range) != nil
65+ }
12266
123- result. append ( contentsOf: correct ( file: file,
124- pattern: endTwoOrMoreSpacePattern,
125- replaceString: " // MARK: " ) )
67+ private static let goodPattern = [
68+ " ^// MARK: \( oneOrMoreHyphen) \( anyText) $ " ,
69+ " ^// MARK: \( oneOrMoreHyphen) ?$ " ,
70+ " ^// MARK: \( nonSpaceOrHyphen) + ? \( anyText) ?$ " ,
71+ " ^// MARK:$ " ,
12672
127- result . append ( contentsOf : correct ( file : file ,
128- pattern : twoOrMoreSpacesAfterHyphenPattern ,
129- replaceString : " // MARK: - " ) )
73+ // comment start with `Mark ...` is ignored
74+ " ^ \( twoOrThreeSlashes ) +[Mm]ark[^:] "
75+ ] . map ( nonCapturingGroup ) . joined ( separator : " | " )
13076
131- result. append ( contentsOf: correct ( file: file,
132- pattern: nonSpaceOrNewlineAfterHyphenPattern,
133- replaceString: " // MARK: - " ,
134- keepLastChar: true ) )
77+ private static let badPattern = capturingGroup ( [
78+ " MARK[^ \\ s:] " ,
79+ " [Mm]ark " ,
80+ " MARK "
81+ ] . map ( basePattern) . joined ( separator: " | " ) ) + capturingGroup( hyphenOrEmpty)
13582
136- result. append ( contentsOf: correct ( file: file,
137- pattern: oneOrMoreSpacesBeforeColonPattern,
138- replaceString: " // MARK: " ,
139- keepLastChar: false ) )
83+ private static let anySpace = " * "
84+ private static let nonSpaceOrTwoOrMoreSpace = " (?: {2,})? "
14085
141- result. append ( contentsOf: correct ( file: file,
142- pattern: nonWhitespaceBeforeColonPattern,
143- replaceString: " // MARK: " ,
144- keepLastChar: false ) )
86+ private static let anyText = " (?: \\ S.*) "
14587
146- result. append ( contentsOf: correct ( file: file,
147- pattern: nonWhitespaceNorColonBeforeSpacesPattern,
148- replaceString: " // MARK: " ,
149- keepLastChar: false ) )
88+ private static let oneOrMoreHyphen = " -+ "
89+ private static let nonSpaceOrHyphen = " [^ -] "
15090
151- result . append ( contentsOf : correct ( file : file ,
152- pattern : invalidLowercasePattern ,
153- replaceString : " // MARK: " ) )
91+ private static let twoOrThreeSlashes = " ///? "
92+ private static let colonOrEmpty = " :? "
93+ private static let hyphenOrEmpty = " -? * "
15494
155- result . append ( contentsOf : correct ( file : file ,
156- pattern : threeSlashesInsteadOfTwo ,
157- replaceString : " // MARK: " ) )
95+ private static func nonCapturingGroup ( _ pattern : String ) -> String {
96+ " (?: \( pattern ) ) "
97+ }
15898
159- return result. unique
160- }
99+ private static func capturingGroup( _ pattern: String ) -> String {
100+ " ( \( pattern) ) "
101+ }
161102
162- private func correct( file: SwiftLintFile ,
163- pattern: String ,
164- replaceString: String ,
165- keepLastChar: Bool = false ) -> [ Correction ] {
166- let violations = violationRanges ( in: file, matching: pattern)
167- let matches = file. ruleEnabled ( violatingRanges: violations, for: self )
168- if matches. isEmpty{ return [ ] }
169-
170- var nsstring = file. contents. bridge ( )
171- let description = Self . description
172- var corrections = [ Correction] ( )
173- for var rangein matches. reversed ( ) {
174- if keepLastChar{
175- range. length-= 1
103+ private static func basePattern( _ pattern: String ) -> String {
104+ nonCapturingGroup ( " \( twoOrThreeSlashes) \( anySpace) \( pattern) \( anySpace) \( colonOrEmpty) \( anySpace) " )
105+ }
106+
107+ private static func replace( _ target: String , range nsrange: NSRange , to replaceString: String ) -> String {
108+ guard nsrange. length> 0 , let range= Range ( nsrange, in: target) else {
109+ return target
176110}
177- let location = Location ( file: file, characterOffset: range. location)
178- nsstring= nsstring. replacingCharacters ( in: range, with: replaceString) . bridge ( )
179- corrections. append ( Correction ( ruleDescription: description, location: location) )
111+ return target. replacingCharacters ( in: range, with: replaceString)
180112}
181- file. write ( nsstring. bridge ( ) )
182- return corrections
183113}
184114
185- private func violationRanges( in file: SwiftLintFile , matching pattern: String ) -> [ NSRange ] {
186- return file. rangesAndTokens ( matching: pattern) . filter { matchRange, syntaxTokensin
187- guard
188- let syntaxToken= syntaxTokens. first,
189- let syntaxKind= syntaxToken. kind,
190- SyntaxKind . commentKinds. contains ( syntaxKind) ,
191- caselet tokenLocation= Location ( file: file, byteOffset: syntaxToken. offset) ,
192- caselet matchLocation= Location ( file: file, characterOffset: matchRange. location) ,
193- // Skip MARKs that are part of a multiline comment
194- tokenLocation. line== matchLocation. line
195- else {
196- return false
115+ func violationResults( ) -> [ ViolationResult ] {
116+ var utf8Offset = 0
117+ var results : [ ViolationResult ] = [ ]
118+
119+ for index in leadingTrivia. pieces. indices{
120+ let piece = leadingTrivia. pieces [ index]
121+ defer { utf8Offset+= piece. sourceLength. utf8Length}
122+
123+ switch piece{
124+ case . lineComment( let comment) , . docLineComment( let comment) :
125+ for correct in Mark . lint ( in: comment) {
126+ let position = position. advanced ( by: utf8Offset)
127+ results. append ( ViolationResult ( position: position) { piecesin
128+ pieces [ index] = . lineComment( correct ( ) )
129+ } )
130+ }
131+
132+ default :
133+ break
197134}
198- return true
199- } . compactMap { range, syntaxTokensin
200- let byteRange = ByteRange ( location: syntaxTokens [ 0 ] . offset, length: 0 )
201- let identifierRange = file. stringView. byteRangeToNSRange ( byteRange)
202- return identifierRange. map { NSUnionRange ( $0, range) }
203135}
204- }
205- }
206136
207- private let issue1029Example = Example ( """
208- ↓//MARK:- Top-Level bad mark
209- ↓//MARK:- Another bad mark
210- struct MarkTest {}
211- ↓// MARK:- Bad mark
212- extension MarkTest {}
213- """ )
214-
215- private let issue1029Correction = Example ( """
216- // MARK: - Top-Level bad mark
217- // MARK: - Another bad mark
218- struct MarkTest {}
219- // MARK: - Bad mark
220- extension MarkTest {}
221- """ )
222-
223- // https://github.com/realm/SwiftLint/issues/1749
224- // https://github.com/realm/SwiftLint/issues/3841
225- private let issue1749Example = Example (
226- """
227- /*
228- func test1() {
229- }
230- //MARK: mark
231- func test2() {
137+ return results
232138}
233- */
234- """
235- )
236-
237- // This example should not trigger changes
238- private let issue1749Correction = issue1749Example
239-
240- // These need to be at the bottom of the file to work around https://bugs.swift.org/browse/SR-10486
241-
242- private let nonSpace = " [^ ] "
243- private let twoOrMoreSpace = " {2,} "
244- private let mark = " MARK: "
245- private let nonSpaceOrTwoOrMoreSpace = " (?: \( nonSpace) | \( twoOrMoreSpace) ) "
246-
247- private let nonSpaceOrTwoOrMoreSpaceOrNewline = " (?:[^ \n ]| \( twoOrMoreSpace) ) "
139+ }