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

Fix superfluous disable command for custom rules #5546

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,21 @@ struct TypesafeArrayInitRule: AnalyzerRule {
func f<Seq: Sequence>(s: Seq) -> [Seq.Element] {
s.↓map({ $0 })
}
"""),
""", testDisableCommand: false),
Example("""
func f(array: [Int]) -> [Int] {
array.↓map { $0 }
}
"""),
""", testDisableCommand: false),
Example("""
let myInts = [1, 2, 3].↓map { return $0 }
"""),
""", testDisableCommand: false),
Example("""
struct Generator: Sequence, IteratorProtocol {
func next() -> Int? { nil }
}
let array = Generator().↓map { i in i }
"""),
""", testDisableCommand: false),
],
requiresFileOnDisk: true
)
Expand Down
78 changes: 51 additions & 27 deletions Source/SwiftLintCore/Models/Linter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,47 +16,61 @@ private struct LintResult {
}

private extension Rule {
static func superfluousDisableCommandViolations(regions: [Region],
superfluousDisableCommandRule: SuperfluousDisableCommandRule?,
allViolations: [StyleViolation]) -> [StyleViolation] {
func superfluousDisableCommandViolations(regions: [Region],
superfluousDisableCommandRule: SuperfluousDisableCommandRule?,
allViolations: [StyleViolation]) -> [StyleViolation] {
guard regions.isNotEmpty, let superfluousDisableCommandRule else {
return []
}

let regionsDisablingCurrentRule = regions.filter { region in
return region.isRuleDisabled(self.init())
}
let regionsDisablingSuperfluousDisableRule = regions.filter { region in
return region.isRuleDisabled(superfluousDisableCommandRule)
}

return regionsDisablingCurrentRule.compactMap { region -> StyleViolation? in
let isSuperfluousRuleDisabled = regionsDisablingSuperfluousDisableRule.contains {
$0.contains(region.start)
let regionsWithIdentifiers: [(String, [Region])] = {
if let customRules = self as? CustomRules {
return customRules.configuration.customRuleConfigurations.map { configuration in
let regionsDisablingCurrentRule = regions.filter { region in
return region.isRuleDisabled(customRuleIdentifier: configuration.identifier)
}
return (configuration.identifier, regionsDisablingCurrentRule)
}
}

guard !isSuperfluousRuleDisabled else {
return nil
let regionsDisablingCurrentRule = regions.filter { region in
return region.isRuleDisabled(self)
}
return [(Self.description.identifier, regionsDisablingCurrentRule)]
}()

return regionsWithIdentifiers.flatMap { ruleIdentifier, regionsDisablingCurrentRule in
regionsDisablingCurrentRule.compactMap { region -> StyleViolation? in
let isSuperfluousRuleDisabled = regionsDisablingSuperfluousDisableRule.contains {
$0.contains(region.start)
}

guard !isSuperfluousRuleDisabled else {
return nil
}

let noViolationsInDisabledRegion = !allViolations.contains { violation in
return region.contains(violation.location) && violation.ruleIdentifier == ruleIdentifier
}
guard noViolationsInDisabledRegion else {
return nil
}

let noViolationsInDisabledRegion = !allViolations.contains { violation in
return region.contains(violation.location)
}
guard noViolationsInDisabledRegion else {
return nil
return StyleViolation(
ruleDescription: type(of: superfluousDisableCommandRule).description,
severity: superfluousDisableCommandRule.configuration.severity,
location: region.start,
reason: superfluousDisableCommandRule.reason(forRuleIdentifier: ruleIdentifier)
)
}

return StyleViolation(
ruleDescription: type(of: superfluousDisableCommandRule).description,
severity: superfluousDisableCommandRule.configuration.severity,
location: region.start,
reason: superfluousDisableCommandRule.reason(for: self)
)
}
}

// As we need the configuration to get custom identifiers.
// swiftlint:disable:next function_parameter_count
// swiftlint:disable:next function_parameter_count function_body_length
func lint(file: SwiftLintFile, regions: [Region], benchmark: Bool,
storage: RuleStorage,
configuration: Configuration,
Expand Down Expand Up @@ -92,16 +106,26 @@ private extension Rule {

let (disabledViolationsAndRegions, enabledViolationsAndRegions) = violations.map { violation in
return (violation, regions.first { $0.contains(violation.location) })
}.partitioned { _, region in
}.partitioned { violation, region in
if self is CustomRules {
return !(region?.isRuleDisabled(customRuleIdentifier: violation.ruleIdentifier) ?? true)
}
return region?.isRuleEnabled(self) ?? true
}

let customRulesIDs: [String] = {
guard let customRules = self as? CustomRules else {
return []
}
return customRules.configuration.customRuleConfigurations.map(\.identifier)
}()
let ruleIDs = Self.description.allIdentifiers +
customRulesIDs +
(superfluousDisableCommandRule.map({ type(of: $0) })?.description.allIdentifiers ?? []) +
[RuleIdentifier.all.stringRepresentation]
let ruleIdentifiers = Set(ruleIDs.map { RuleIdentifier($0) })

let superfluousDisableCommandViolations = Self.superfluousDisableCommandViolations(
let superfluousDisableCommandViolations = superfluousDisableCommandViolations(
regions: regions.count > 1 ? file.regions(restrictingRuleIdentifiers: ruleIdentifiers) : regions,
superfluousDisableCommandRule: superfluousDisableCommandRule,
allViolations: violations
Expand Down
5 changes: 5 additions & 0 deletions Source/SwiftLintCore/Models/Region.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ public struct Region: Equatable {
///
/// - returns: True if the specified rule is disabled in this region.
public func isRuleDisabled(_ rule: some Rule) -> Bool {
if rule is CustomRules {
let customRulesConfiguration = rule.configuration as? CustomRulesConfiguration
let identifiers = customRulesConfiguration?.customRuleConfigurations.map { $0.identifier } ?? []
return areRulesDisabled(ruleIDs: identifiers)
}
return areRulesDisabled(ruleIDs: type(of: rule).description.allIdentifiers)
}

Expand Down
13 changes: 4 additions & 9 deletions Source/SwiftLintCore/Rules/CustomRules.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,19 +79,14 @@ struct CustomRules: Rule, CacheDescriptionProvider {
severity: configuration.severity,
location: Location(file: file, characterOffset: $0.location),
reason: configuration.message)
}).filter { violation in
guard let region = file.regions().first(where: { $0.contains(violation.location) }) else {
return true
}

return !region.isRuleDisabled(customRuleIdentifier: configuration.identifier)
}
})
}
}
}

private extension Region {
extension Region {
func isRuleDisabled(customRuleIdentifier: String) -> Bool {
return disabledRuleIdentifiers.contains(RuleIdentifier(customRuleIdentifier))
disabledRuleIdentifiers.contains(.all) ||
disabledRuleIdentifiers.contains(RuleIdentifier(customRuleIdentifier))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ package struct SuperfluousDisableCommandRule: SourceKitFreeRule {
return []
}

func reason(for rule: (some Rule).Type) -> String {
func reason(forRuleIdentifier ruleIdentifier: String) -> String {
"""
SwiftLint rule '\(rule.description.identifier)' did not trigger a violation in the disabled region; \
SwiftLint rule '\(ruleIdentifier)' did not trigger a violation in the disabled region; \
remove the disable command
"""
}
Expand Down
107 changes: 103 additions & 4 deletions Tests/SwiftLintFrameworkTests/CustomRulesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,17 @@ final class CustomRulesTests: SwiftLintTestCase {
)
}

func testLocalDisableCustomRule() {
let (_, customRules) = getCustomRules()
let file = SwiftLintFile(contents: "//swiftlint:disable custom \n// file with a pattern")
XCTAssertEqual(customRules.validate(file: file), [])
func testLocalDisableCustomRule() throws {
let customRules: [String: Any] = [
"custom": [
"regex": "pattern",
"match_kinds": "comment",
],
]
let example = Example("//swiftlint:disable custom \n// file with a pattern")
let violations = try violations(forExample: example, customRules: customRules)

XCTAssertTrue(violations.isEmpty)
}

func testLocalDisableCustomRuleWithMultipleRules() {
Expand Down Expand Up @@ -195,6 +202,86 @@ final class CustomRulesTests: SwiftLintTestCase {
XCTAssertEqual(violations[0].location.character, 6)
}

func testSuperfluousDisableCommandWithCustomRules() throws {
let customRules: [String: Any] = [
"custom1": [
"regex": "pattern",
"match_kinds": "comment",
],
]

let example = Example("// swiftlint:disable custom1\n")
let violations = try violations(forExample: example, customRules: customRules)

XCTAssertEqual(violations.count, 1)
XCTAssertTrue(violations.allSatisfy { $0.ruleIdentifier == "superfluous_disable_command" })
XCTAssertTrue(violations.contains { violation in
violation.description.contains("SwiftLint rule 'custom1' did not trigger a violation")
})
}

func testSuperfluousDisableCommandWithMultipleCustomRules() throws {
let customRules: [String: Any] = [
"custom1": [
"regex": "pattern",
"match_kinds": "comment",
],
"custom2": [
"regex": "10",
"match_kinds": "number",
],
"custom3": [
"regex": "100",
"match_kinds": "number",
],
]

let example = Example(
"""
// swiftlint:disable custom1 custom3
return 10
"""
)

let violations = try violations(forExample: example, customRules: customRules)

XCTAssertEqual(violations.count, 3)
XCTAssertEqual(violations.filter { $0.ruleIdentifier == "superfluous_disable_command" }.count, 2)
XCTAssertEqual(violations.filter { $0.ruleIdentifier == "custom2" }.count, 1)
XCTAssertTrue(violations.contains { violation in
violation.description.contains("SwiftLint rule 'custom1' did not trigger a violation")
})
XCTAssertTrue(violations.contains { violation in
violation.description.contains("SwiftLint rule 'custom3' did not trigger a violation")
})
}

func testSuperfluousDisableCommandDoesNotViolate() throws {
let customRules: [String: Any] = [
"dont_print": [
"regex": "print\\("
],
]
let example = Example("""
// swiftlint:disable:next dont_print
print("Hello, world")
""")
XCTAssertTrue(try violations(forExample: example, customRules: customRules).isEmpty)
}

func testDisableAll() throws {
let customRules: [String: Any] = [
"dont_print": [
"regex": "print\\("
],
]
let example = Example("""
// swiftlint:disable:next all
print("Hello, world")
""")
XCTAssertTrue(try violations(forExample: example, customRules: customRules).isEmpty)
}

private func getCustomRules(_ extraConfig: [String: Any] = [:]) -> (Configuration, CustomRules) {
var config: [String: Any] = [
"regex": "pattern",
Expand Down Expand Up @@ -253,4 +340,16 @@ final class CustomRulesTests: SwiftLintTestCase {
private func getTestTextFile() -> SwiftLintFile {
return SwiftLintFile(path: "\(testResourcesPath)/test.txt")!
}

private func violations(forExample example: Example, customRules: [String: Any]) throws -> [StyleViolation] {
let configDict: [String: Any] = [
"only_rules": ["custom_rules", "superfluous_disable_command"],
"custom_rules": customRules,
]
let configuration = try SwiftLintCore.Configuration(dict: configDict)
return SwiftLintTestHelpers.violations(
example.skipWrappingInCommentTest(),
config: configuration
)
}
}