Add new `invalid_swiftlint_command` rule (#4546)

This commit is contained in:
Martin Redington 2023-03-04 12:53:27 +00:00 committed by GitHub
parent d6e3bbb64d
commit 1c3c62e422
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 101 additions and 18 deletions

View File

@ -89,6 +89,11 @@
* Catch more valid `no_magic_numbers` violations. * Catch more valid `no_magic_numbers` violations.
[JP Simard](https://github.com/jpsim) [JP Simard](https://github.com/jpsim)
* Add `invalid_swiftlint_command` rule that validates
`// swiftlint:enable` and `disable` commands.
[Martin Redington](https://github.com/mildm8nnered)
[#4546](https://github.com/realm/SwiftLint/pull/4546)
* Improve `identifier_name` documentation. * Improve `identifier_name` documentation.
[Martin Redington](https://github.com/mildm8nnered) [Martin Redington](https://github.com/mildm8nnered)
[#4767](https://github.com/realm/SwiftLint/issues/4767) [#4767](https://github.com/realm/SwiftLint/issues/4767)

View File

@ -165,7 +165,8 @@ extension SwiftLintFile {
internal var locationConverter: SourceLocationConverter { locationConverterCache.get(self) } internal var locationConverter: SourceLocationConverter { locationConverterCache.get(self) }
internal var commands: [Command] { commandsCache.get(self) } internal var commands: [Command] { commandsCache.get(self).filter { $0.isValid } }
internal var invalidCommands: [Command] { commandsCache.get(self).filter { !$0.isValid } }
internal var syntaxTokensByLines: [[SwiftLintSyntaxToken]] { internal var syntaxTokensByLines: [[SwiftLintSyntaxToken]] {
guard let syntaxTokensByLines = syntaxTokensByLinesCache.get(self) else { guard let syntaxTokensByLines = syntaxTokensByLinesCache.get(self) else {

View File

@ -31,6 +31,9 @@ extension SwiftLintFile {
case .enable: case .enable:
disabledRules.subtract(command.ruleIdentifiers) disabledRules.subtract(command.ruleIdentifiers)
case .invalid:
break
} }
let start = Location(file: path, line: command.line, character: command.character) let start = Location(file: path, line: command.line, character: command.character)

View File

@ -8,6 +8,8 @@ public struct Command: Equatable {
case enable case enable
/// The rule(s) associated with this command should be disabled by the SwiftLint engine. /// The rule(s) associated with this command should be disabled by the SwiftLint engine.
case disable case disable
/// The action string was invalid.
case invalid
/// - returns: The inverse action that can cancel out the current action, restoring the SwifttLint engine's /// - returns: The inverse action that can cancel out the current action, restoring the SwifttLint engine's
/// state prior to the current action. /// state prior to the current action.
@ -15,6 +17,7 @@ public struct Command: Equatable {
switch self { switch self {
case .enable: return .disable case .enable: return .disable
case .disable: return .enable case .disable: return .enable
case .invalid: return .invalid
} }
} }
} }
@ -27,6 +30,8 @@ public struct Command: Equatable {
case this case this
/// The command should only apply to the line following its definition. /// The command should only apply to the line following its definition.
case next case next
/// The modifier string was invalid.
case invalid
} }
/// Text after this delimiter is not considered part of the rule. /// Text after this delimiter is not considered part of the rule.
@ -36,6 +41,10 @@ public struct Command: Equatable {
/// swiftlint:disable:next force_try - Explanation here /// swiftlint:disable:next force_try - Explanation here
private static let commentDelimiter = " - " private static let commentDelimiter = " - "
var isValid: Bool {
action != .invalid && modifier != .invalid && !ruleIdentifiers.isEmpty
}
internal let action: Action internal let action: Action
internal let ruleIdentifiers: Set<RuleIdentifier> internal let ruleIdentifiers: Set<RuleIdentifier>
internal let line: Int internal let line: Int
@ -53,7 +62,7 @@ public struct Command: Equatable {
/// defined. /// defined.
/// - parameter modifier: This command's modifier, if any. /// - parameter modifier: This command's modifier, if any.
/// - parameter trailingComment: The comment following this command's `-` delimiter, if any. /// - parameter trailingComment: The comment following this command's `-` delimiter, if any.
public init(action: Action, ruleIdentifiers: Set<RuleIdentifier>, line: Int = 0, public init(action: Action, ruleIdentifiers: Set<RuleIdentifier> = [], line: Int = 0,
character: Int? = nil, modifier: Modifier? = nil, trailingComment: String? = nil) { character: Int? = nil, modifier: Modifier? = nil, trailingComment: String? = nil) {
self.action = action self.action = action
self.ruleIdentifiers = ruleIdentifiers self.ruleIdentifiers = ruleIdentifiers
@ -69,24 +78,24 @@ public struct Command: Equatable {
/// - parameter line: The line in the source file where this command is defined. /// - parameter line: The line in the source file where this command is defined.
/// - parameter character: The character offset within the line in the source file where this command is /// - parameter character: The character offset within the line in the source file where this command is
/// defined. /// defined.
public init?(actionString: String, line: Int, character: Int) { public init(actionString: String, line: Int, character: Int) {
let scanner = Scanner(string: actionString) let scanner = Scanner(string: actionString)
_ = scanner.scanString("swiftlint:") _ = scanner.scanString("swiftlint:")
// (enable|disable)(:previous|:this|:next) // (enable|disable)(:previous|:this|:next)
guard let actionAndModifierString = scanner.scanUpToString(" ") else { guard let actionAndModifierString = scanner.scanUpToString(" ") else {
return nil self.init(action: .invalid, line: line, character: character)
return
} }
let actionAndModifierScanner = Scanner(string: actionAndModifierString) let actionAndModifierScanner = Scanner(string: actionAndModifierString)
guard let actionString = actionAndModifierScanner.scanUpToString(":"), guard let actionString = actionAndModifierScanner.scanUpToString(":"),
let action = Action(rawValue: actionString) let action = Action(rawValue: actionString)
else { else {
return nil self.init(action: .invalid, line: line, character: character)
return
} }
self.action = action
self.line = line
self.character = character
let rawRuleTexts = scanner.scanUpToString(Self.commentDelimiter) ?? "" let rawRuleTexts = scanner.scanUpToString(Self.commentDelimiter) ?? ""
var trailingComment: String?
if scanner.isAtEnd { if scanner.isAtEnd {
trailingComment = nil trailingComment = nil
} else { } else {
@ -103,19 +112,26 @@ public struct Command: Equatable {
return component.isNotEmpty && component != "*/" return component.isNotEmpty && component != "*/"
} }
ruleIdentifiers = Set(ruleTexts.map(RuleIdentifier.init(_:))) let ruleIdentifiers = Set(ruleTexts.map(RuleIdentifier.init(_:)))
// Modifier // Modifier
let hasModifier = actionAndModifierScanner.scanString(":") != nil let hasModifier = actionAndModifierScanner.scanString(":") != nil
let modifier: Modifier?
if hasModifier { if hasModifier {
modifier = Modifier( let modifierString = String(actionAndModifierScanner.string[actionAndModifierScanner.currentIndex...])
rawValue: String( modifier = Modifier(rawValue: modifierString) ?? .invalid
actionAndModifierScanner.string[actionAndModifierScanner.currentIndex...]
)
)
} else { } else {
modifier = nil modifier = nil
} }
self.init(
action: action,
ruleIdentifiers: ruleIdentifiers,
line: line,
character: character,
modifier: modifier,
trailingComment: trailingComment
)
} }
/// Expands the current command into its fully descriptive form without any modifiers. /// Expands the current command into its fully descriptive form without any modifiers.
@ -142,6 +158,8 @@ public struct Command: Equatable {
Self(action: action, ruleIdentifiers: ruleIdentifiers, line: line + 1), Self(action: action, ruleIdentifiers: ruleIdentifiers, line: line + 1),
Self(action: action.inverse(), ruleIdentifiers: ruleIdentifiers, line: line + 1, character: Int.max) Self(action: action.inverse(), ruleIdentifiers: ruleIdentifiers, line: line + 1, character: Int.max)
] ]
case .invalid:
return []
} }
} }
} }

View File

@ -89,6 +89,7 @@ let builtInRules: [Rule.Type] = [
InclusiveLanguageRule.self, InclusiveLanguageRule.self,
IndentationWidthRule.self, IndentationWidthRule.self,
InertDeferRule.self, InertDeferRule.self,
InvalidSwiftLintCommandRule.self,
IsDisjointRule.self, IsDisjointRule.self,
JoinedDefaultParameterRule.self, JoinedDefaultParameterRule.self,
LargeTupleRule.self, LargeTupleRule.self,

View File

@ -0,0 +1,49 @@
import Foundation
import SwiftSyntax
struct InvalidSwiftLintCommandRule: ConfigurationProviderRule {
var configuration = SeverityConfiguration(.warning)
init() {}
static let description = RuleDescription(
identifier: "invalid_swiftlint_command",
name: "Invalid SwiftLint Command",
description: "swiftlint command does not have a valid action or modifier",
kind: .lint,
nonTriggeringExamples: [
Example("// swiftlint:disable unused_import"),
Example("// swiftlint:enable unused_import"),
Example("// swiftlint:disable:next unused_import"),
Example("// swiftlint:disable:previous unused_import"),
Example("// swiftlint:disable:this unused_import")
],
triggeringExamples: [
Example("// swiftlint:"),
Example("// swiftlint: "),
Example("// swiftlint::"),
Example("// swiftlint:: "),
Example("// swiftlint:disable"),
Example("// swiftlint:dissable unused_import"),
Example("// swiftlint:enaaaable unused_import"),
Example("// swiftlint:disable:nxt unused_import"),
Example("// swiftlint:enable:prevus unused_import"),
Example("// swiftlint:enable:ths unused_import"),
Example("// swiftlint:enable"),
Example("// swiftlint:enable:"),
Example("// swiftlint:enable: "),
Example("// swiftlint:disable: unused_import")
].skipWrappingInCommentTests()
)
func validate(file: SwiftLintFile) -> [StyleViolation] {
file.invalidCommands.map {
let location = Location(file: file.path, line: $0.line, character: $0.character)
return StyleViolation(
ruleDescription: Self.description,
severity: configuration.severity,
location: location
)
}
}
}

View File

@ -36,9 +36,9 @@ private extension Trivia {
case let actionString = String(comment[lower...]), case let actionString = String(comment[lower...]),
case let end = locationConverter.location(for: offset + triviaOffset), case let end = locationConverter.location(for: offset + triviaOffset),
let line = end.line, let line = end.line,
let column = end.column, let column = end.column
let command = Command(actionString: actionString, line: line, character: column)
{ {
let command = Command(actionString: actionString, line: line, character: column)
results.append(command) results.append(command)
} }
default: default:

View File

@ -517,6 +517,12 @@ class InertDeferRuleGeneratedTests: XCTestCase {
} }
} }
class InvalidSwiftLintCommandRuleGeneratedTests: XCTestCase {
func testWithDefaultConfiguration() {
verifyRule(InvalidSwiftLintCommandRule.description)
}
}
class IsDisjointRuleGeneratedTests: XCTestCase { class IsDisjointRuleGeneratedTests: XCTestCase {
func testWithDefaultConfiguration() { func testWithDefaultConfiguration() {
verifyRule(IsDisjointRule.description) verifyRule(IsDisjointRule.description)