From 63695134a8f4d176e0d7dacf39afea1bef04a8ba Mon Sep 17 00:00:00 2001 From: theamodhshetty Date: Fri, 6 Mar 2026 13:11:35 +0530 Subject: [PATCH 1/3] fix(indentation_width): catch under-indented closing braces --- CHANGELOG.md | 5 ++ .../Rules/Style/IndentationWidthRule.swift | 53 +++++++++++++++++-- .../IndentationWidthRuleTests.swift | 36 +++++++++++++ 3 files changed, 91 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 612e577b77..026c6671fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,11 @@ ### Bug Fixes +* Ensure that `indentation_width` flags under-indented closing braces while + still avoiding duplicate warnings on the next outer closing brace. + [theamodhshetty](https://github.com/theamodhshetty) + [#6498](https://github.com/realm/SwiftLint/issues/6498) + * 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/IndentationWidthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift index 2c8f304de7..1406876163 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift @@ -45,6 +45,8 @@ struct IndentationWidthRule: OptInRule { func validate(file: SwiftLintFile) -> [StyleViolation] { // swiftlint:disable:this function_body_length var violations: [StyleViolation] = [] var previousLineIndentations: [Indentation] = [] + var previousLineStartedWithClosingDelimiter = false + var previousLineWasInvalid = false for line in file.lines { if ignoreCompilerDirective(line: line, in: file) { continue } @@ -85,6 +87,8 @@ struct IndentationWidthRule: OptInRule { // Catch indented first line guard previousLineIndentations.isNotEmpty else { previousLineIndentations = [indentation] + previousLineStartedWithClosingDelimiter = startsWithClosingDelimiter(in: line.content) + previousLineWasInvalid = false if indentation != .spaces(0) { // There's an indentation although this is the first line! @@ -101,12 +105,23 @@ struct IndentationWidthRule: OptInRule { continue } - let linesValidationResult = previousLineIndentations.map { - validate(indentation: indentation, comparingTo: $0) + let startsWithClosingDelimiter = startsWithClosingDelimiter(in: line.content) + let linesValidationResult = if startsWithClosingDelimiter { + [validateClosingDelimiterLine( + indentation: indentation, + previousIndentation: previousLineIndentations.last!, + previousLineStartedWithClosingDelimiter: previousLineStartedWithClosingDelimiter, + previousLineWasInvalid: previousLineWasInvalid + )] + } else { + previousLineIndentations.map { + validate(indentation: indentation, comparingTo: $0) + } } // Catch wrong indentation or wrong unindentation - if !linesValidationResult.contains(true) { + let isValidLine = linesValidationResult.contains(true) + if !isValidLine { let isIndentation = previousLineIndentations.last.map { indentation.spacesEquivalent(indentationWidth: configuration.indentationWidth) >= $0.spacesEquivalent(indentationWidth: configuration.indentationWidth) @@ -136,6 +151,9 @@ struct IndentationWidthRule: OptInRule { // This mechanism avoids duplicate warnings. previousLineIndentations.append(indentation) } + + previousLineStartedWithClosingDelimiter = startsWithClosingDelimiter + previousLineWasInvalid = !isValidLine } return violations @@ -202,4 +220,33 @@ struct IndentationWidthRule: OptInRule { ) // Allow unindent if it stays in the grid ) } + + private func validateClosingDelimiterLine( + indentation: Indentation, + previousIndentation: Indentation, + previousLineStartedWithClosingDelimiter: Bool, + previousLineWasInvalid: Bool + ) -> Bool { + let currentSpaceEquivalent = indentation.spacesEquivalent(indentationWidth: configuration.indentationWidth) + let previousSpaceEquivalent = previousIndentation.spacesEquivalent( + indentationWidth: configuration.indentationWidth + ) + + return ( + currentSpaceEquivalent == previousSpaceEquivalent - configuration.indentationWidth || + ( + previousLineStartedWithClosingDelimiter && + previousLineWasInvalid && + currentSpaceEquivalent == previousSpaceEquivalent + ) + ) + } + + private func startsWithClosingDelimiter(in lineContent: String) -> Bool { + guard let firstCharacter = lineContent.trimmingCharacters(in: .whitespacesAndNewlines).first else { + return false + } + + return firstCharacter == "}" || firstCharacter == "]" || firstCharacter == ")" + } } diff --git a/Tests/BuiltInRulesTests/IndentationWidthRuleTests.swift b/Tests/BuiltInRulesTests/IndentationWidthRuleTests.swift index 936dd77bfc..8f2b9da31f 100644 --- a/Tests/BuiltInRulesTests/IndentationWidthRuleTests.swift +++ b/Tests/BuiltInRulesTests/IndentationWidthRuleTests.swift @@ -89,6 +89,42 @@ final class IndentationWidthRuleTests: SwiftLintTestCase { assertNoViolation(in: "firstLine\n\tsecondLine\n\t\tthirdLine\n\t\t\tfourthLine\nfifthLine") } + func testClosingBraceIndentation() { + assert1Violation(in: """ + import SwiftUI + + struct TestView: View { + var body: some View { + VStack { + Text("Hello") + } + .onTapGesture { + if true { + print("inside if") + } + } + } + } + """) + + assertNoViolation(in: """ + import SwiftUI + + struct TestView: View { + var body: some View { + VStack { + Text("Hello") + } + .onTapGesture { + if true { + print("inside if") + } + } + } + } + """) + } + /// It's okay to have empty lines between iff the following indentations obey the rules. func testEmptyLinesBetween() { assertNoViolation(in: "firstLine\n\tsecondLine\n\n\tfourthLine") From 6915a6c8d301aff056273b162da8f04724ed9170 Mon Sep 17 00:00:00 2001 From: theamodhshetty Date: Fri, 6 Mar 2026 14:05:46 +0530 Subject: [PATCH 2/3] refactor(indentation_width): extract closing delimiter validation --- .../Rules/Style/IndentationWidthRule.swift | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift index 1406876163..a79c7d9b44 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift @@ -106,18 +106,13 @@ struct IndentationWidthRule: OptInRule { } let startsWithClosingDelimiter = startsWithClosingDelimiter(in: line.content) - let linesValidationResult = if startsWithClosingDelimiter { - [validateClosingDelimiterLine( - indentation: indentation, - previousIndentation: previousLineIndentations.last!, - previousLineStartedWithClosingDelimiter: previousLineStartedWithClosingDelimiter, - previousLineWasInvalid: previousLineWasInvalid - )] - } else { - previousLineIndentations.map { - validate(indentation: indentation, comparingTo: $0) - } - } + let linesValidationResult = validationResults( + for: indentation, + previousLineIndentations: previousLineIndentations, + startsWithClosingDelimiter: startsWithClosingDelimiter, + previousLineStartedWithClosingDelimiter: previousLineStartedWithClosingDelimiter, + previousLineWasInvalid: previousLineWasInvalid + ) // Catch wrong indentation or wrong unindentation let isValidLine = linesValidationResult.contains(true) @@ -242,6 +237,29 @@ struct IndentationWidthRule: OptInRule { ) } + private func validationResults( + for indentation: Indentation, + previousLineIndentations: [Indentation], + startsWithClosingDelimiter: Bool, + previousLineStartedWithClosingDelimiter: Bool, + previousLineWasInvalid: Bool + ) -> [Bool] { + if startsWithClosingDelimiter { + return [ + validateClosingDelimiterLine( + indentation: indentation, + previousIndentation: previousLineIndentations.last!, + previousLineStartedWithClosingDelimiter: previousLineStartedWithClosingDelimiter, + previousLineWasInvalid: previousLineWasInvalid + ), + ] + } + + return previousLineIndentations.map { + validate(indentation: indentation, comparingTo: $0) + } + } + private func startsWithClosingDelimiter(in lineContent: String) -> Bool { guard let firstCharacter = lineContent.trimmingCharacters(in: .whitespacesAndNewlines).first else { return false From d1f37d53452b86e5ecfadf841541e8af914f008c Mon Sep 17 00:00:00 2001 From: theamodhshetty Date: Fri, 6 Mar 2026 18:45:24 +0530 Subject: [PATCH 3/3] refactor(indentation_width): drop force unwrap in helper --- .../Rules/Style/IndentationWidthRule.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift index a79c7d9b44..eb55aebdb5 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift @@ -244,11 +244,13 @@ struct IndentationWidthRule: OptInRule { previousLineStartedWithClosingDelimiter: Bool, previousLineWasInvalid: Bool ) -> [Bool] { - if startsWithClosingDelimiter { + if + startsWithClosingDelimiter, + let previousIndentation = previousLineIndentations.last { return [ validateClosingDelimiterLine( indentation: indentation, - previousIndentation: previousLineIndentations.last!, + previousIndentation: previousIndentation, previousLineStartedWithClosingDelimiter: previousLineStartedWithClosingDelimiter, previousLineWasInvalid: previousLineWasInvalid ),