mirror of
https://github.com/realm/SwiftLint.git
synced 2026-05-07 20:12:49 +00:00
112 lines
4.0 KiB
Swift
112 lines
4.0 KiB
Swift
import Foundation
|
|
import SourceKittenFramework
|
|
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<ConfigurationType> {
|
|
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[..<annotationRange.lowerBound].utf8.count),
|
|
reason: reason
|
|
)
|
|
violations.append(violation)
|
|
}
|
|
default:
|
|
return []
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension String {
|
|
func truncated(maxLength: Int) -> String {
|
|
if utf16.count > maxLength {
|
|
let end = index(startIndex, offsetBy: maxLength, limitedBy: endIndex) ?? endIndex
|
|
return self[..<end] + "..."
|
|
}
|
|
return self
|
|
}
|
|
}
|