diff --git a/CHANGELOG.md b/CHANGELOG.md index f6455ddce4..44a6b535c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,11 @@ ### Bug Fixes +* Detect and autocorrect missing whitespace before `else` in `guard` + statements for the `statement_position` rule. + [theamodhshetty](https://github.com/theamodhshetty) + [#6153](https://github.com/realm/SwiftLint/issues/6153) + * Add an `ignore_attributes` option to `implicit_optional_initialization` so wrappers/attributes that require explicit `= nil` can be excluded from style checks for both `style: always` and `style: never`. diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/StatementPositionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/StatementPositionRule.swift index 768a018743..4e438b1183 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/StatementPositionRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/StatementPositionRule.swift @@ -14,6 +14,7 @@ struct StatementPositionRule: CorrectableRule { Example("} else if {"), Example("} else {"), Example("} catch {"), + Example("guard foo() else { return }"), Example("\"}else{\""), Example("struct A { let catchphrase: Int }\nlet a = A(\n catchphrase: 0\n)"), Example("struct A { let `catch`: Int }\nlet a = A(\n `catch`: 0\n)"), @@ -23,11 +24,13 @@ struct StatementPositionRule: CorrectableRule { Example("↓} else {"), Example("↓}\ncatch {"), Example("↓}\n\t catch {"), + Example("guard foo()↓else { return }"), ], corrections: [ Example("↓}\n else {"): Example("} else {"), Example("↓}\n else if {"): Example("} else if {"), Example("↓}\n catch {"): Example("} catch {"), + Example("guard foo()↓else { return }"): Example("guard foo() else { return }"), ] ) @@ -87,34 +90,75 @@ private extension StatementPositionRule { // followed by 'else' or 'catch' literals static let defaultPattern = "\\}(?:[\\s\\n\\r]{2,}|[\\n\\t\\r]+)?\\b(else|catch)\\b" + // match a guard statement where `else` is glued to the condition without whitespace + static let defaultGuardPattern = "(\\bguard\\b[^\\n]*\\S)(else\\b)" + + static let defaultGuardRegex = regex(defaultGuardPattern) + func defaultValidate(file: SwiftLintFile) -> [StyleViolation] { - defaultViolationRanges(in: file, matching: Self.defaultPattern).compactMap { range in + defaultViolationRanges(in: file).compactMap { range in StyleViolation(ruleDescription: Self.description, severity: configuration.severity, location: Location(file: file, characterOffset: range.location)) } } - func defaultViolationRanges(in file: SwiftLintFile, matching pattern: String) -> [NSRange] { - file.match(pattern: pattern).filter { _, syntaxKinds in + func defaultViolationRanges(in file: SwiftLintFile) -> [NSRange] { + defaultBraceViolationRanges(in: file) + defaultGuardViolationRanges(in: file) + } + + func defaultBraceViolationRanges(in file: SwiftLintFile) -> [NSRange] { + file.match(pattern: Self.defaultPattern).filter { _, syntaxKinds in syntaxKinds.starts(with: [.keyword]) }.compactMap(\.0) } + func defaultGuardViolationRanges(in file: SwiftLintFile) -> [NSRange] { + defaultGuardMatches(in: file).map { $0.range(at: 2) } + } + + func defaultGuardCorrectionRanges(in file: SwiftLintFile) -> [NSRange] { + defaultGuardMatches(in: file).map(\.range) + } + + func defaultGuardMatches(in file: SwiftLintFile) -> [NSTextCheckingResult] { + let contents = file.stringView + let syntaxMap = file.syntaxMap + + return Self.defaultGuardRegex.matches(in: file).filter { match in + guard let elseRange = contents.NSRangeToByteRange( + start: match.range(at: 2).location, + length: match.range(at: 2).length + ) else { + return false + } + + return syntaxMap.kinds(inByteRange: elseRange) == [.keyword] + } + } + func defaultCorrect(file: SwiftLintFile) -> Int { - let violations = defaultViolationRanges(in: file, matching: Self.defaultPattern) - let matches = file.ruleEnabled(violatingRanges: violations, for: self) - if matches.isEmpty { + let braceViolations = defaultBraceViolationRanges(in: file) + let guardViolations = defaultGuardCorrectionRanges(in: file) + let enabledBraceViolations = file.ruleEnabled(violatingRanges: braceViolations, for: self) + let enabledGuardViolations = file.ruleEnabled(violatingRanges: guardViolations, for: self) + if enabledBraceViolations.isEmpty, enabledGuardViolations.isEmpty { return 0 } - let regularExpression = regex(Self.defaultPattern) + var contents = file.contents - for range in matches.reversed() { - contents = regularExpression.stringByReplacingMatches(in: contents, options: [], range: range, - withTemplate: "} $1") + let braceRegex = regex(Self.defaultPattern) + for range in enabledBraceViolations.reversed() { + contents = braceRegex.stringByReplacingMatches(in: contents, options: [], range: range, + withTemplate: "} $1") + } + + for range in enabledGuardViolations.reversed() { + contents = Self.defaultGuardRegex.stringByReplacingMatches(in: contents, options: [], range: range, + withTemplate: "$1 $2") } file.write(contents) - return matches.count + return enabledBraceViolations.count + enabledGuardViolations.count } }