Skip to content

Commit

Permalink
Add only configuration option to todo rule (realm#5233)
Browse files Browse the repository at this point in the history
  • Loading branch information
gibachan authored and KS1019 committed Oct 2, 2023
1 parent d65595f commit e3ab04b
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 11 deletions.
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)
}
}

0 comments on commit e3ab04b

Please sign in to comment.