diff --git a/CHANGELOG.md b/CHANGELOG.md index 50835855b4..f31aa0509e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -332,6 +332,10 @@ content mode. [DemiDevv](https://github.com/DemiDevv) [#5713](https://github.com/realm/SwiftLint/issues/5713) +* Add autocorrection support to `indentation_width` rule to automatically fix + indentation violations using the `--fix` option. + [nadeemnali](https://github.com/nadeemnali) + [#6497](https://github.com/realm/SwiftLint/issues/6497) * Support access level modifiers on imports in `unused_imports` rule. [SimplyDanny](https://github.com/SimplyDanny) @@ -345,6 +349,10 @@ * Improve the opt-in `pattern_matching_keywords` rule by extending support beyond `switch case` and refining nested pattern handling. [GandaLF2006](https://github.com/GandaLF2006) +* Add autocorrection support to `indentation_width` rule to automatically fix + indentation violations using the `--fix` option. + [nadeemnali](https://github.com/nadeemnali) + [#6497](https://github.com/realm/SwiftLint/issues/6497) ### Bug Fixes diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift index d40271fa2e..13881c040a 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift @@ -4,7 +4,7 @@ import SwiftLintCore import SwiftSyntax @DisabledWithoutSourceKit -struct IndentationWidthRule: OptInRule { +struct IndentationWidthRule: OptInRule, CorrectableRule { // MARK: - Subtypes private enum Indentation: Equatable { case tabs(Int) @@ -55,31 +55,6 @@ struct IndentationWidthRule: OptInRule { "firstLine\n\tsecondLine\n\t\tthirdLine\n\n\t\tfourthLine", "firstLine\n\tsecondLine\n\t\tthirdLine\n\t//test\n\t\tfourthLine", "firstLine\n secondLine\n thirdLine\nfourthLine", - """ - guard let x = foo(), - let y = bar() else { - return - } - """, - """ - if let x = foo(), - let y = bar() { - doSomething() - } - """, - """ - while let x = foo(), - let y = bar() { - doSomething() - } - """, - """ - if let x = foo(), - let y = bar(), - let z = baz() { - doSomething() - } - """, ]), triggeringExamples: #examples([ "↓ firstLine".asExample(testMultiByteOffsets: false, testDisableCommand: false), @@ -280,6 +255,83 @@ struct IndentationWidthRule: OptInRule { ) // Allow unindent if it stays in the grid ) } + + // MARK: - Methods: Correction + func correct(file: SwiftLintFile) -> Int { + var corrections = 0 + var previousLineIndentations: [Indentation] = [] + var correctedLines = file.lines.map(\.content) + + let conditionContinuationInfo = multilineConditionInfo(in: file) + + for line in file.lines { + let indentationCharacterCount = line.content.countOfLeadingCharacters(in: CharacterSet(charactersIn: " \t")) + if shouldSkipLine(line: line, indentationCharacterCount: indentationCharacterCount, in: file) { continue } + + // Skip multiline condition continuation lines (they have specific alignment, don't auto-correct) + if conditionContinuationInfo[line.index] != nil { continue } + + let prefix = IndentationPrefix(line: line, length: indentationCharacterCount) + let (indentation, mixedViolation) = parseIndentation(line: line, prefix: prefix, file: file) + + // Skip lines with mixed tabs/spaces + if mixedViolation != nil { continue } + + // Catch indented first line + guard previousLineIndentations.isNotEmpty else { + previousLineIndentations = [indentation] + if indentation != .spaces(0) { + correctedLines[line.index] = String(line.content.dropFirst(indentationCharacterCount)) + corrections += 1 + } + continue + } + + // Check if indentation is valid + if validate(indentation: indentation, comparingTo: previousLineIndentations[0]) { + previousLineIndentations = [indentation] + continue + } + + // Indentation is wrong, fix it + let lastValidIndentation = previousLineIndentations[0] + let correctIndentLevel = lastValidIndentation.spacesEquivalent( + indentationWidth: configuration.indentationWidth) + let shouldUseTabs = prefix.tabCount > 0 + let correctIndent = generateIndentation(spaceCount: correctIndentLevel, usesTabs: shouldUseTabs) + let lineContent = String(line.content.dropFirst(indentationCharacterCount)) + correctedLines[line.index] = correctIndent + lineContent + + let correctedIndentation: Indentation = shouldUseTabs + ? .tabs(correctIndent.filter { $0 == "\t" }.count) + : .spaces(correctIndent.filter { $0 == " " }.count) + previousLineIndentations = [correctedIndentation] + + corrections += 1 + } + + if corrections > 0 { + let correctedContent = correctedLines.joined(separator: "\n") + file.write(correctedContent) + } + + return corrections + } + + /// Generates an indentation string based on the number of spaces and whether tabs should be used. + /// + /// - parameter spaceCount: The number of space-equivalents needed. + /// - parameter usesTabs: Whether the indentation should use tabs. + /// + /// - returns: The generated indentation string. + private func generateIndentation(spaceCount: Int, usesTabs: Bool) -> String { + if usesTabs { + let tabCount = spaceCount / configuration.indentationWidth + let remainingSpaces = spaceCount % configuration.indentationWidth + return String(repeating: "\t", count: tabCount) + String(repeating: " ", count: remainingSpaces) + } + return String(repeating: " ", count: spaceCount) + } } private final class MultilineConditionLineVisitor: SyntaxVisitor { diff --git a/Tests/IntegrationTests/Resources/default_rule_configurations.yml b/Tests/IntegrationTests/Resources/default_rule_configurations.yml index cc2f59c8b2..975a2c4cc8 100644 --- a/Tests/IntegrationTests/Resources/default_rule_configurations.yml +++ b/Tests/IntegrationTests/Resources/default_rule_configurations.yml @@ -549,7 +549,7 @@ indentation_width: include_multiline_conditions: false meta: opt-in: true - correctable: false + correctable: true invalid_swiftlint_command: severity: warning meta: