diff --git a/CHANGELOG.md b/CHANGELOG.md index 02357fd76..0fa9acb57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,8 @@ * Add GitHub Actions Logging reporter (`github-actions-logging`). [Norio Nomura](https://github.com/norio-nomura) +* Make `control_statement` rule substitution correctable. + [MaxHaertwig](https://github.com/maxhaertwig) #### Bug Fixes diff --git a/Rules.md b/Rules.md index 3d820d72c..0910f2c6f 100644 --- a/Rules.md +++ b/Rules.md @@ -2953,7 +2953,7 @@ resourceString.range(of: rule.regex, options: .regularExpression) != nil Identifier | Enabled by default | Supports autocorrection | Kind | Analyzer | Minimum Swift Compiler Version --- | --- | --- | --- | --- | --- -`control_statement` | Enabled | No | style | No | 3.0.0 +`control_statement` | Enabled | Yes | style | No | 3.0.0 `if`, `for`, `guard`, `switch`, `while`, and `catch` statements shouldn't unnecessarily wrap their conditionals or arguments in parentheses. diff --git a/Source/SwiftLintFramework/Rules/Style/ControlStatementRule.swift b/Source/SwiftLintFramework/Rules/Style/ControlStatementRule.swift index 8846c02bd..c754dbe50 100644 --- a/Source/SwiftLintFramework/Rules/Style/ControlStatementRule.swift +++ b/Source/SwiftLintFramework/Rules/Style/ControlStatementRule.swift @@ -1,6 +1,6 @@ import SourceKittenFramework -public struct ControlStatementRule: ConfigurationProviderRule, AutomaticTestableRule { +public struct ControlStatementRule: ConfigurationProviderRule, AutomaticTestableRule, SubstitutionCorrectableRule { public var configuration = SeverityConfiguration(.warning) public init() {} @@ -53,10 +53,39 @@ public struct ControlStatementRule: ConfigurationProviderRule, AutomaticTestable "↓switch (foo) {\n", "do {\n} ↓catch(let error as NSError) {\n}", "↓if (max(a, b) < c) {\n" + ], + corrections: [ + "↓if (condition) {\n": "if condition {\n", + "↓if(condition) {\n": "if condition {\n", + "↓if (condition == endIndex) {\n": "if condition == endIndex {\n", + "↓if ((a || b) && (c || d)) {\n": "if (a || b) && (c || d) {\n", + "↓if ((min...max).contains(value)) {\n": "if (min...max).contains(value) {\n", + "↓for (item in collection) {\n": "for item in collection {\n", + "↓for (var index = 0; index < 42; index++) {\n": "for var index = 0; index < 42; index++ {\n", + "↓for(item in collection) {\n": "for item in collection {\n", + "↓for(var index = 0; index < 42; index++) {\n": "for var index = 0; index < 42; index++ {\n", + "↓guard (condition) else {\n": "guard condition else {\n", + "↓while (condition) {\n": "while condition {\n", + "↓while(condition) {\n": "while condition {\n", + "} ↓while (condition) {\n": "} while condition {\n", + "} ↓while(condition) {\n": "} while condition {\n", + "do { ; } ↓while(condition) {\n": "do { ; } while condition {\n", + "do { ; } ↓while (condition) {\n": "do { ; } while condition {\n", + "↓switch (foo) {\n": "switch foo {\n", + "do {\n} ↓catch(let error as NSError) {\n}": "do {\n} catch let error as NSError {\n}", + "↓if (max(a, b) < c) {\n": "if max(a, b) < c {\n", ] ) public func validate(file: SwiftLintFile) -> [StyleViolation] { + return violationRanges(in: file).map { match -> StyleViolation in + return StyleViolation(ruleDescription: type(of: self).description, + severity: configuration.severity, + location: Location(file: file, characterOffset: match.location)) + } + } + + public func violationRanges(in file: SwiftLintFile) -> [NSRange] { let statements = ["if", "for", "guard", "switch", "while", "catch"] let statementPatterns: [String] = statements.map { statement -> String in let isGuard = statement == "guard" @@ -65,13 +94,14 @@ public struct ControlStatementRule: ConfigurationProviderRule, AutomaticTestable let clausePattern = isSwitch ? "[^,{]*" : "[^{]*" return "\(statement)\\s*\\(\(clausePattern)\\)\\s*\(elsePattern)\\{" } - return statementPatterns.flatMap { pattern -> [StyleViolation] in + return statementPatterns.flatMap { pattern -> [NSRange] in return file.match(pattern: pattern) .filter { match, syntaxKinds -> Bool in let matchString = file.contents.substring(from: match.location, length: match.length) return !isFalsePositive(matchString, syntaxKind: syntaxKinds.first) } - .filter { match, _ -> Bool in + .map { $0.0 } + .filter { match -> Bool in let contents = file.contents.bridge() guard let byteOffset = contents.NSRangeToByteRange(start: match.location, length: 1)?.location, let outerKind = file.structureDictionary.kinds(forByteOffset: byteOffset).last else { @@ -80,15 +110,25 @@ public struct ControlStatementRule: ConfigurationProviderRule, AutomaticTestable return SwiftExpressionKind(rawValue: outerKind.kind) != .call } - .map { match, _ -> StyleViolation in - return StyleViolation(ruleDescription: type(of: self).description, - severity: configuration.severity, - location: Location(file: file, characterOffset: match.location)) - } } } - fileprivate func isFalsePositive(_ content: String, syntaxKind: SyntaxKind?) -> Bool { + public func substitution(for violationRange: NSRange, in file: SwiftLintFile) -> (NSRange, String) { + var violationString = file.contents.bridge().substring(with: violationRange) + if violationString.contains("(") && violationString.contains(")") { + if let openingIndex = violationString.firstIndex(of: "(") { + let replacement = violationString[violationString.index(before: openingIndex)] == " " ? "" : " " + violationString.replaceSubrange(openingIndex...openingIndex, with: replacement) + } + if let closingIndex = violationString.lastIndex(of: ")" as Character) { + let replacement = violationString[violationString.index(after: closingIndex)] == " " ? "" : " " + violationString.replaceSubrange(closingIndex...closingIndex, with: replacement) + } + } + return (violationRange, violationString) + } + + private func isFalsePositive(_ content: String, syntaxKind: SyntaxKind?) -> Bool { if syntaxKind != .keyword { return true }