Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add only configuration option to todo rule #5233

Merged
merged 11 commits into from Sep 26, 2023
5 changes: 4 additions & 1 deletion CHANGELOG.md
Expand Up @@ -12,7 +12,10 @@

#### Enhancements

* None.
* Add `only` configuration option to `todo` rule which allows to specify
whether the rule shall trigger on `TODO`s, `FIXME`s or both.
[gibachan](https://github.com/gibachan)
[#5233](https://github.com/realm/SwiftLint/pull/5233)

#### Bug Fixes

Expand Down
30 changes: 22 additions & 8 deletions Source/SwiftLintBuiltInRules/Rules/Lint/TodoRule.swift
Expand Up @@ -2,7 +2,7 @@ import Foundation
import SwiftSyntax

struct TodoRule: SwiftSyntaxRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
var configuration = TodoConfiguration()

static let description = RuleDescription(
identifier: "todo",
Expand All @@ -26,41 +26,55 @@ struct TodoRule: SwiftSyntaxRule, ConfigurationProviderRule {
)

func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
Visitor(todoKeywords: configuration.only)
}
}

private extension TodoRule {
final class Visitor: ViolationsSyntaxVisitor {
private let todoKeywords: [TodoConfiguration.TodoKeyword]

init(todoKeywords: [TodoConfiguration.TodoKeyword]) {
self.todoKeywords = todoKeywords
super.init(viewMode: .sourceAccurate)
}

override func visitPost(_ node: TokenSyntax) {
let leadingViolations = node.leadingTrivia.violations(offset: node.position)
let trailingViolations = node.trailingTrivia.violations(offset: node.endPositionBeforeTrailingTrivia)
let leadingViolations = node.leadingTrivia.violations(offset: node.position,
for: todoKeywords)
let trailingViolations = node.trailingTrivia.violations(offset: node.endPositionBeforeTrailingTrivia,
for: todoKeywords)
violations.append(contentsOf: leadingViolations + trailingViolations)
}
}
}

private extension Trivia {
func violations(offset: AbsolutePosition) -> [ReasonedRuleViolation] {
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))
violations.append(contentsOf: piece.violations(offset: position, for: todoKeywords))
position += piece.sourceLength
}
return violations
}
}

private extension TriviaPiece {
func violations(offset: AbsolutePosition) -> [ReasonedRuleViolation] {
func violations(offset: AbsolutePosition,
for todoKeywords: [TodoConfiguration.TodoKeyword]) -> [ReasonedRuleViolation] {
switch self {
case
.blockComment(let comment),
.lineComment(let comment),
.docBlockComment(let comment),
.docLineComment(let comment):
let matches = regex(#"\b((?:TODO|FIXME)(?::|\b))"#)

// 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 {
Expand Down
@@ -0,0 +1,31 @@
import SwiftLintCore

struct TodoConfiguration: SeverityBasedRuleConfiguration, Equatable {
typealias Parent = TodoRule

enum TodoKeyword: String, CaseIterable, AcceptableByConfigurationElement {
case todo = "TODO"
case fixme = "FIXME"

func asOption() -> OptionType { .symbol(rawValue) }
}

@ConfigurationElement(key: "severity")
private(set) var severityConfiguration = SeverityConfiguration<Parent>(.warning)
@ConfigurationElement(key: "only")
private(set) var only = TodoKeyword.allCases

mutating func apply(configuration: Any) throws {
guard let configuration = configuration as? [String: Any] else {
throw Issue.unknownConfiguration(ruleID: Parent.identifier)
}

if let severityString = configuration[$severityConfiguration] as? String {
try severityConfiguration.apply(configuration: severityString)
}

if let onlyStrings = configuration[$only] as? [String] {
self.only = onlyStrings.compactMap { TodoKeyword(rawValue: $0) }
}
}
}
24 changes: 22 additions & 2 deletions Tests/SwiftLintFrameworkTests/TodoRuleTests.swift
Expand Up @@ -20,8 +20,28 @@ class TodoRuleTests: SwiftLintTestCase {
XCTAssertEqual(violations.first!.reason, "FIXMEs should be resolved (Implement)")
}

private func violations(_ example: Example) -> [StyleViolation] {
let config = makeConfig(nil, TodoRule.description.identifier)!
func testOnlyFixMe() {
let example = Example("""
fatalError() // TODO: Implement todo
fatalError() // FIXME: Implement fixme
""")
let violations = self.violations(example, config: ["only": ["FIXME"]])
XCTAssertEqual(violations.count, 1)
XCTAssertEqual(violations.first!.reason, "FIXMEs should be resolved (Implement fixme)")
}

func testOnlyTodo() {
let example = Example("""
fatalError() // TODO: Implement todo
fatalError() // FIXME: Implement fixme
""")
let violations = self.violations(example, config: ["only": ["TODO"]])
XCTAssertEqual(violations.count, 1)
XCTAssertEqual(violations.first!.reason, "TODOs should be resolved (Implement todo)")
}

private func violations(_ example: Example, config: Any? = nil) -> [StyleViolation] {
let config = makeConfig(config, TodoRule.description.identifier)!
return SwiftLintFrameworkTests.violations(example, config: config)
}
}