11import Foundation
2- import SourceKittenFramework
2+ import SwiftSyntax
33
4- struct ExpiringTodoRule : OptInRule {
4+ @SwiftSyntaxRule ( optIn: true )
5+ struct ExpiringTodoRule : Rule {
56 enum ExpiryViolationLevel {
67 case approachingExpiry
78 case expired
@@ -10,11 +11,11 @@ struct ExpiringTodoRule: OptInRule {
1011 var reason : String {
1112 switch self {
1213 case . approachingExpiry:
13- return " TODO/FIXME is approaching its expiry and should be resolved soon "
14+ " TODO/FIXME is approaching its expiry and should be resolved soon "
1415 case . expired:
15- return " TODO/FIXME has expired and must be resolved "
16+ " TODO/FIXME has expired and must be resolved "
1617 case . badFormatting:
17- return " Expiring TODO/FIXME is incorrectly formatted "
18+ " Expiring TODO/FIXME is incorrectly formatted "
1819 }
1920 }
2021 }
@@ -45,73 +46,142 @@ struct ExpiringTodoRule: OptInRule {
4546 )
4647
4748 var configuration = ExpiringTodoConfiguration ( )
49+ }
4850
49- func validate( file: SwiftLintFile ) -> [ StyleViolation ] {
50- let regex = #"""
51- \b(?:TODO|FIXME)(?::|\b)(?:(?!\b(?:TODO|FIXME)(?::|\b)).)*?\#
52- \ \#( configuration. dateDelimiters. opening) \#
53- (\d{1,4}\ \#( configuration. dateSeparator) \d{1,4}\ \#( configuration. dateSeparator) \d{1,4})\#
54- \ \#( configuration. dateDelimiters. closing)
55- """#
56-
57- return file. matchesAndSyntaxKinds ( matching: regex) . compactMap { checkingResult, syntaxKinds in
58- guard
59- syntaxKinds. allSatisfy ( \. isCommentLike) ,
60- checkingResult. numberOfRanges > 1 ,
61- case let range = checkingResult. range ( at: 1 ) ,
62- let violationLevel = violationLevel ( for: expiryDate ( file: file, range: range) ) ,
63- let severity = severity ( for: violationLevel) else {
64- return nil
51+ private extension ExpiringTodoRule {
52+ final class Visitor : ViolationsSyntaxVisitor < ConfigurationType > {
53+ private lazy var regex : NSRegularExpression = {
54+ let pattern = #"""
55+ \b(?:TODO|FIXME)(?::|\b)(?:(?!\b(?:TODO|FIXME)(?::|\b)).)*?\#
56+ \ \#( configuration. dateDelimiters. opening) \#
57+ (\d{1,4}\ \#( configuration. dateSeparator) \d{1,4}\ \#( configuration. dateSeparator) \d{1,4})\#
58+ \ \#( configuration. dateDelimiters. closing)
59+ """#
60+ return SwiftLintCore . regex ( pattern)
61+ } ( )
62+
63+ override func visit( _ node: SourceFileSyntax ) -> SyntaxVisitorContinueKind {
64+ // Process each comment individually
65+ for token in node. tokens ( viewMode: . sourceAccurate) {
66+ processTrivia (
67+ token. leadingTrivia,
68+ baseOffset: token. position. utf8Offset
69+ )
70+ processTrivia (
71+ token. trailingTrivia,
72+ baseOffset: token. endPositionBeforeTrailingTrivia. utf8Offset
73+ )
6574 }
6675
67- return StyleViolation (
68- ruleDescription: Self . description,
69- severity: severity,
70- location: Location ( file: file, characterOffset: range. location) ,
71- reason: violationLevel. reason
72- )
76+ return . skipChildren
7377 }
74- }
7578
76- private func expiryDate( file: SwiftLintFile , range: NSRange ) -> Date ? {
77- let expiryDateString = file. contents. bridge ( )
78- . substring ( with: range)
79- . trimmingCharacters ( in: . whitespacesAndNewlines)
79+ private func processTrivia( _ trivia: Trivia , baseOffset: Int ) {
80+ var triviaOffset = baseOffset
8081
81- let formatter = DateFormatter ( )
82- formatter. calendar = . current
83- formatter. dateFormat = configuration. dateFormat
82+ for (index, piece) in trivia. enumerated ( ) {
83+ defer { triviaOffset += piece. sourceLength. utf8Length }
8484
85- return formatter. date ( from: expiryDateString)
86- }
85+ guard let commentText = piece. commentText else { continue }
86+
87+ // Handle multiline comments by checking consecutive line comments
88+ if piece. isLineComment {
89+ var combinedText = commentText
90+ let currentOffset = triviaOffset
91+
92+ // Look ahead for consecutive line comments
93+ let remainingTrivia = trivia. dropFirst ( index + 1 )
8794
88- private func severity( for violationLevel: ExpiryViolationLevel ) -> ViolationSeverity ? {
89- switch violationLevel {
90- case . approachingExpiry:
91- return configuration. approachingExpirySeverity. severity
92- case . expired:
93- return configuration. expiredSeverity. severity
94- case . badFormatting:
95- return configuration. badFormattingSeverity. severity
95+ for nextPiece in remainingTrivia {
96+ if case . lineComment( let nextText) = nextPiece {
97+ // Check if it's a continuation (starts with //)
98+ if nextText. hasPrefix ( " // " ) {
99+ combinedText += " \n " + nextText
100+ } else {
101+ break
102+ }
103+ } else if !nextPiece. isWhitespace {
104+ break
105+ }
106+ }
107+
108+ processComment ( combinedText, offset: currentOffset)
109+ } else {
110+ processComment ( commentText, offset: triviaOffset)
111+ }
112+ }
113+ }
114+
115+ private func processComment( _ commentText: String , offset: Int ) {
116+ let matches = regex. matches ( in: commentText, options: [ ] , range: commentText. fullNSRange)
117+ let nsStringComment = commentText. bridge ( )
118+
119+ for match in matches {
120+ guard match. numberOfRanges > 1 else { continue }
121+
122+ // Get the date capture group (second capture group, index 1)
123+ let dateRange = match. range ( at: 1 )
124+ guard dateRange. location != NSNotFound else { continue }
125+
126+ let matchOffset = offset + dateRange. location
127+ let matchPosition = AbsolutePosition ( utf8Offset: matchOffset)
128+
129+ let dateString = nsStringComment. substring ( with: dateRange)
130+ . trimmingCharacters ( in: . whitespacesAndNewlines)
131+
132+ if let violationLevel = getViolationLevel ( for: parseDate ( dateString: dateString) ) ,
133+ let severity = getSeverity ( for: violationLevel) {
134+ let violation = ReasonedRuleViolation (
135+ position: matchPosition,
136+ reason: violationLevel. reason,
137+ severity: severity
138+ )
139+ violations. append ( violation)
140+ }
141+ }
96142 }
97- }
98143
99- private func violationLevel( for expiryDate: Date ? ) -> ExpiryViolationLevel ? {
100- guard let expiryDate else {
101- return . badFormatting
144+ private func parseDate( dateString: String ) -> Date ? {
145+ let formatter = DateFormatter ( )
146+ formatter. calendar = . current
147+ formatter. dateFormat = configuration. dateFormat
148+ return formatter. date ( from: dateString)
102149 }
103- guard expiryDate. isAfterToday else {
104- return . expired
150+
151+ private func getSeverity( for violationLevel: ExpiryViolationLevel ) -> ViolationSeverity ? {
152+ switch violationLevel {
153+ case . approachingExpiry:
154+ configuration. approachingExpirySeverity. severity
155+ case . expired:
156+ configuration. expiredSeverity. severity
157+ case . badFormatting:
158+ configuration. badFormattingSeverity. severity
159+ }
105160 }
106- guard let approachingDate = Calendar . current. date (
107- byAdding: . day,
108- value: - configuration. approachingExpiryThreshold,
109- to: expiryDate) else {
161+
162+ private func getViolationLevel( for expiryDate: Date ? ) -> ExpiryViolationLevel ? {
163+ guard let expiryDate else {
164+ return . badFormatting
165+ }
166+
167+ guard expiryDate. isAfterToday else {
168+ return . expired
169+ }
170+
171+ let approachingDate = Calendar . current. date (
172+ byAdding: . day,
173+ value: - configuration. approachingExpiryThreshold,
174+ to: expiryDate
175+ )
176+
177+ guard let approachingDate else {
110178 return nil
179+ }
180+
181+ return approachingDate. isAfterToday ?
182+ nil :
183+ . approachingExpiry
111184 }
112- return approachingDate. isAfterToday ?
113- nil :
114- . approachingExpiry
115185 }
116186}
117187
@@ -121,9 +191,23 @@ private extension Date {
121191 }
122192}
123193
124- private extension SyntaxKind {
125- /// Returns if the syntax kind is comment-like.
126- var isCommentLike : Bool {
127- Self . commentKinds. contains ( self )
128- }
194+ private extension TriviaPiece {
195+ var isLineComment : Bool {
196+ switch self {
197+ case . lineComment, . docLineComment:
198+ true
199+ default :
200+ false
201+ }
202+ }
203+
204+ var commentText : String ? {
205+ switch self {
206+ case . lineComment( let text) , . blockComment( let text) ,
207+ . docLineComment( let text) , . docBlockComment( let text) :
208+ text
209+ default :
210+ nil
211+ }
212+ }
129213}
0 commit comments