diff --git a/CHANGELOG.md b/CHANGELOG.md index c5dd4bc83..7ae52d1bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,8 +19,11 @@ [kimdv](https://github.com/kimdv) * Extend `xct_specific_matcher` rule to check for boolean asserts on (un)equal - comparisons. + comparisons. The rule can be configured with the matchers that should trigger + rule violations. By default, all matchers trigger, but that can be limited to + just `one-argument-asserts` or `two-argument-asserts`. [SimplyDanny](https://github.com/SimplyDanny) + [JP Simard](https://github.com/jpsim) [#3726](https://github.com/realm/SwiftLint/issues/3726) * Trigger `prefer_self_in_static_references` rule on more type references. diff --git a/Source/SwiftLintFramework/Rules/Idiomatic/XCTSpecificMatcherRule.swift b/Source/SwiftLintFramework/Rules/Idiomatic/XCTSpecificMatcherRule.swift index 16514c14b..950819d4a 100644 --- a/Source/SwiftLintFramework/Rules/Idiomatic/XCTSpecificMatcherRule.swift +++ b/Source/SwiftLintFramework/Rules/Idiomatic/XCTSpecificMatcherRule.swift @@ -2,7 +2,7 @@ import SwiftOperators import SwiftSyntax struct XCTSpecificMatcherRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule { - var configuration = SeverityConfiguration(.warning) + var configuration = XCTSpecificMatcherRuleConfiguration() init() {} @@ -16,14 +16,28 @@ struct XCTSpecificMatcherRule: SwiftSyntaxRule, OptInRule, ConfigurationProvider ) func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor { - Visitor(viewMode: .sourceAccurate) + Visitor(configuration: configuration) } } private extension XCTSpecificMatcherRule { final class Visitor: ViolationsSyntaxVisitor { + let configuration: XCTSpecificMatcherRuleConfiguration + + init(configuration: XCTSpecificMatcherRuleConfiguration) { + self.configuration = configuration + super.init(viewMode: .sourceAccurate) + } + override func visitPost(_ node: FunctionCallExprSyntax) { - if let suggestion = TwoArgsXCTAssert.violations(in: node) ?? OneArgXCTAssert.violations(in: node) { + if configuration.matchers.contains(.twoArgumentAsserts), + let suggestion = TwoArgsXCTAssert.violations(in: node) { + violations.append(ReasonedRuleViolation( + position: node.positionAfterSkippingLeadingTrivia, + reason: "Prefer the specific matcher '\(suggestion)' instead" + )) + } else if configuration.matchers.contains(.oneArgumentAsserts), + let suggestion = OneArgXCTAssert.violations(in: node) { violations.append(ReasonedRuleViolation( position: node.positionAfterSkippingLeadingTrivia, reason: "Prefer the specific matcher '\(suggestion)' instead" diff --git a/Source/SwiftLintFramework/Rules/Idiomatic/XCTSpecificMatcherRuleExamples.swift b/Source/SwiftLintFramework/Rules/Idiomatic/XCTSpecificMatcherRuleExamples.swift index d57548ea5..a471ff43c 100644 --- a/Source/SwiftLintFramework/Rules/Idiomatic/XCTSpecificMatcherRuleExamples.swift +++ b/Source/SwiftLintFramework/Rules/Idiomatic/XCTSpecificMatcherRuleExamples.swift @@ -50,7 +50,13 @@ internal struct XCTSpecificMatcherRuleExamples { Example("XCTAssertEqual(foo?.bar, toto())"), Example("XCTAssertEqual(foo?.bar, .toto(.zoo))"), Example("XCTAssertEqual(toto(), foo?.bar)"), - Example("XCTAssertEqual(.toto(.zoo), foo?.bar)") + Example("XCTAssertEqual(.toto(.zoo), foo?.bar)"), + + // Configurations Disabled + Example("XCTAssertEqual(foo, true)", + configuration: ["matchers": ["one-argument-asserts"]]), + Example("XCTAssert(foo == bar)", + configuration: ["matchers": ["two-argument-asserts"]]) ] static let triggeringExamples = [ diff --git a/Source/SwiftLintFramework/Rules/RuleConfigurations/XCTSpecificMatcherRuleConfiguration.swift b/Source/SwiftLintFramework/Rules/RuleConfigurations/XCTSpecificMatcherRuleConfiguration.swift new file mode 100644 index 000000000..8e618be68 --- /dev/null +++ b/Source/SwiftLintFramework/Rules/RuleConfigurations/XCTSpecificMatcherRuleConfiguration.swift @@ -0,0 +1,35 @@ +struct XCTSpecificMatcherRuleConfiguration: SeverityBasedRuleConfiguration, Equatable { + private(set) var severityConfiguration = SeverityConfiguration(.warning) + private(set) var matchers = Set(Matcher.allCases) + + enum Matcher: String, Hashable, CaseIterable { + case oneArgumentAsserts = "one-argument-asserts" + case twoArgumentAsserts = "two-argument-asserts" + } + + private enum ConfigurationKey: String { + case severity + case matchers + } + + var consoleDescription: String { + return [ + "severity: \(severityConfiguration.consoleDescription)", + "\(ConfigurationKey.matchers): \(matchers.map(\.rawValue).sorted().joined(separator: ", "))" + ].joined(separator: ", ") + } + + mutating func apply(configuration: Any) throws { + guard let configuration = configuration as? [String: Any] else { + throw ConfigurationError.unknownConfiguration + } + + if let severityString = configuration[ConfigurationKey.severity.rawValue] as? String { + try severityConfiguration.apply(configuration: severityString) + } + + if let matchers = configuration[ConfigurationKey.matchers.rawValue] as? [String] { + self.matchers = Set(matchers.compactMap(Matcher.init(rawValue:))) + } + } +}