import Foundation import SwiftSyntax @SwiftSyntaxRule struct TodoRule: Rule { var configuration = TodoConfiguration() static let description = RuleDescription( identifier: "todo", name: "Todo", description: "TODOs and FIXMEs should be resolved.", kind: .lint, nonTriggeringExamples: [ Example("// notaTODO:"), Example("// notaFIXME:") ], triggeringExamples: [ Example("// ↓TODO:"), Example("// ↓FIXME:"), Example("// ↓TODO(note)"), Example("// ↓FIXME(note)"), Example("/* ↓FIXME: */"), Example("/* ↓TODO: */"), Example("/** ↓FIXME: */"), Example("/** ↓TODO: */") ].skipWrappingInCommentTests() ) } private extension TodoRule { final class Visitor: ViolationsSyntaxVisitor { override func visitPost(_ node: TokenSyntax) { let leadingViolations = node.leadingTrivia.violations(offset: node.position, for: configuration.only) let trailingViolations = node.trailingTrivia.violations(offset: node.endPositionBeforeTrailingTrivia, for: configuration.only) violations.append(contentsOf: leadingViolations + trailingViolations) } } } private extension Trivia { func violations(offset: AbsolutePosition, for todoKeywords: [TodoConfiguration.TodoKeyword]) -> [ReasonedRuleViolation] { var position = offset var violations = [ReasonedRuleViolation]() for piece in self { violations.append(contentsOf: piece.violations(offset: position, for: todoKeywords)) position += piece.sourceLength } return violations } } private extension TriviaPiece { func violations(offset: AbsolutePosition, for todoKeywords: [TodoConfiguration.TodoKeyword]) -> [ReasonedRuleViolation] { switch self { case .blockComment(let comment), .lineComment(let comment), .docBlockComment(let comment), .docLineComment(let comment): // Construct a regex string considering only keywords. let searchKeywords = todoKeywords.map(\.rawValue).joined(separator: "|") let matches = regex(#"\b((?:\#(searchKeywords))(?::|\b))"#) .matches(in: comment, range: comment.bridge().fullNSRange) return matches.reduce(into: []) { violations, match in guard let annotationRange = Range(match.range(at: 1), in: comment) else { return } let maxLengthOfMessage = 30 // customizing the reason message to be specific to fixme or todo let kind = comment[annotationRange].hasPrefix("FIXME") ? "FIXMEs" : "TODOs" let message = comment[annotationRange.upperBound...] .trimmingCharacters(in: .whitespaces) .truncated(maxLength: maxLengthOfMessage) .prefix { $0 != "\n" } let reason: String if message.isEmpty { reason = "\(kind) should be resolved" } else { reason = "\(kind) should be resolved (\(message))" } let violation = ReasonedRuleViolation( position: offset.advanced(by: comment[.. String { if utf16.count > maxLength { let end = index(startIndex, offsetBy: maxLength, limitedBy: endIndex) ?? endIndex return self[..