From 87bdbd586efc4ce76a07700d83e41c4f7ce080ed Mon Sep 17 00:00:00 2001 From: Nadeem Ali Date: Mon, 20 Apr 2026 20:50:04 +0100 Subject: [PATCH 1/4] Add autocorrection support to indentation_width rule --- CHANGELOG.md | 4 + .../Rules/Style/IndentationWidthRule.swift | 99 ++++++++++++++++++- 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50835855b4..6eb8e9d306 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -345,6 +345,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..a5966fe98f 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) @@ -280,6 +280,103 @@ 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) + + for (lineIndex, line) in file.lines.enumerated() { + corrections += correctLine( + at: lineIndex, + line: line, + file: file, + in: &correctedLines, + trackingIndentations: &previousLineIndentations + ) + } + + if corrections > 0 { + let correctedContent = correctedLines.joined(separator: "\n") + file.write(correctedContent) + } + + return corrections + } + + private func correctLine( + at lineIndex: Int, + line: Line, + file: SwiftLintFile, + in correctedLines: inout [String], + trackingIndentations previousLineIndentations: inout [Indentation] + ) -> Int { + if ignoreCompilerDirective(line: line, in: file) { return 0 } + let indentationCharacterCount = line.content.countOfLeadingCharacters(in: CharacterSet(charactersIn: " \t")) + if line.content.count == indentationCharacterCount { return 0 } + if ignoreComment(line: line, in: file) || ignoreMultilineStrings(line: line, in: file) { return 0 } + + let prefix = String(line.content.prefix(indentationCharacterCount)) + let tabCount = prefix.filter { $0 == "\t" }.count + let spaceCount = prefix.filter { $0 == " " }.count + + if tabCount != 0, spaceCount != 0 { return 0 } + + let indentation: Indentation = tabCount != 0 ? .tabs(tabCount) : .spaces(spaceCount) + + guard previousLineIndentations.isNotEmpty else { + previousLineIndentations = [indentation] + if indentation != .spaces(0) { + correctedLines[lineIndex] = String(line.content.dropFirst(indentationCharacterCount)) + return 1 + } + return 0 + } + + let linesValidationResult = previousLineIndentations.map { + validate(indentation: indentation, comparingTo: $0) + } + + if linesValidationResult.contains(true) { + if linesValidationResult.first == true { + previousLineIndentations = [indentation] + } else { + previousLineIndentations.append(indentation) + } + return 0 + } + + guard let lastValidIndentation = previousLineIndentations.first else { return 0 } + + let correctIndentLevel = lastValidIndentation.spacesEquivalent(indentationWidth: configuration.indentationWidth) + let shouldUseTabs = tabCount > 0 + let correctIndent = generateIndentation(spaceCount: correctIndentLevel, usesTabs: shouldUseTabs) + let lineContent = String(line.content.dropFirst(indentationCharacterCount)) + correctedLines[lineIndex] = correctIndent + lineContent + + let correctedIndentation: Indentation = shouldUseTabs + ? .tabs(correctIndent.filter { $0 == "\t" }.count) + : .spaces(correctIndent.filter { $0 == " " }.count) + previousLineIndentations = [correctedIndentation] + + return 1 + } + + /// 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 { From d1af9222b90a3e9f97aea17dbdb4ef420efc4088 Mon Sep 17 00:00:00 2001 From: Nadeem Ali Date: Mon, 20 Apr 2026 20:50:04 +0100 Subject: [PATCH 2/4] Add autocorrection support to indentation_width rule --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6eb8e9d306..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) From 5d2ad11d2a03b9fd7858374e8648651eb60bb694 Mon Sep 17 00:00:00 2001 From: Nadeem Ali Date: Fri, 25 Sep 2026 18:59:52 +0100 Subject: [PATCH 3/4] Update Failing test cases --- .../Rules/Style/IndentationWidthRule.swift | 131 ++++++------------ 1 file changed, 43 insertions(+), 88 deletions(-) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift index a5966fe98f..13881c040a 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift @@ -55,31 +55,6 @@ struct IndentationWidthRule: OptInRule, CorrectableRule { "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), @@ -287,80 +262,60 @@ struct IndentationWidthRule: OptInRule, CorrectableRule { var previousLineIndentations: [Indentation] = [] var correctedLines = file.lines.map(\.content) - for (lineIndex, line) in file.lines.enumerated() { - corrections += correctLine( - at: lineIndex, - line: line, - file: file, - in: &correctedLines, - trackingIndentations: &previousLineIndentations - ) - } + let conditionContinuationInfo = multilineConditionInfo(in: file) - if corrections > 0 { - let correctedContent = correctedLines.joined(separator: "\n") - file.write(correctedContent) - } + for line in file.lines { + let indentationCharacterCount = line.content.countOfLeadingCharacters(in: CharacterSet(charactersIn: " \t")) + if shouldSkipLine(line: line, indentationCharacterCount: indentationCharacterCount, in: file) { continue } - return corrections - } + // Skip multiline condition continuation lines (they have specific alignment, don't auto-correct) + if conditionContinuationInfo[line.index] != nil { continue } - private func correctLine( - at lineIndex: Int, - line: Line, - file: SwiftLintFile, - in correctedLines: inout [String], - trackingIndentations previousLineIndentations: inout [Indentation] - ) -> Int { - if ignoreCompilerDirective(line: line, in: file) { return 0 } - let indentationCharacterCount = line.content.countOfLeadingCharacters(in: CharacterSet(charactersIn: " \t")) - if line.content.count == indentationCharacterCount { return 0 } - if ignoreComment(line: line, in: file) || ignoreMultilineStrings(line: line, in: file) { return 0 } - - let prefix = String(line.content.prefix(indentationCharacterCount)) - let tabCount = prefix.filter { $0 == "\t" }.count - let spaceCount = prefix.filter { $0 == " " }.count - - if tabCount != 0, spaceCount != 0 { return 0 } - - let indentation: Indentation = tabCount != 0 ? .tabs(tabCount) : .spaces(spaceCount) - - guard previousLineIndentations.isNotEmpty else { - previousLineIndentations = [indentation] - if indentation != .spaces(0) { - correctedLines[lineIndex] = String(line.content.dropFirst(indentationCharacterCount)) - return 1 - } - return 0 - } + let prefix = IndentationPrefix(line: line, length: indentationCharacterCount) + let (indentation, mixedViolation) = parseIndentation(line: line, prefix: prefix, file: file) - let linesValidationResult = previousLineIndentations.map { - validate(indentation: indentation, comparingTo: $0) - } + // Skip lines with mixed tabs/spaces + if mixedViolation != nil { continue } - if linesValidationResult.contains(true) { - if linesValidationResult.first == true { + // Catch indented first line + guard previousLineIndentations.isNotEmpty else { previousLineIndentations = [indentation] - } else { - previousLineIndentations.append(indentation) + if indentation != .spaces(0) { + correctedLines[line.index] = String(line.content.dropFirst(indentationCharacterCount)) + corrections += 1 + } + continue } - return 0 - } - guard let lastValidIndentation = previousLineIndentations.first else { return 0 } + // Check if indentation is valid + if validate(indentation: indentation, comparingTo: previousLineIndentations[0]) { + previousLineIndentations = [indentation] + continue + } - let correctIndentLevel = lastValidIndentation.spacesEquivalent(indentationWidth: configuration.indentationWidth) - let shouldUseTabs = tabCount > 0 - let correctIndent = generateIndentation(spaceCount: correctIndentLevel, usesTabs: shouldUseTabs) - let lineContent = String(line.content.dropFirst(indentationCharacterCount)) - correctedLines[lineIndex] = correctIndent + lineContent + // 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 + } - let correctedIndentation: Indentation = shouldUseTabs - ? .tabs(correctIndent.filter { $0 == "\t" }.count) - : .spaces(correctIndent.filter { $0 == " " }.count) - previousLineIndentations = [correctedIndentation] + if corrections > 0 { + let correctedContent = correctedLines.joined(separator: "\n") + file.write(correctedContent) + } - return 1 + return corrections } /// Generates an indentation string based on the number of spaces and whether tabs should be used. From 10874002885db398fd971665b1e457cb98ad5c10 Mon Sep 17 00:00:00 2001 From: Nadeem Ali Date: Fri, 25 Sep 2026 19:14:38 +0100 Subject: [PATCH 4/4] Updated default rule --- .../IntegrationTests/Resources/default_rule_configurations.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: