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..eb55aebdb5 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,18 @@ struct IndentationWidthRule: OptInRule { continue } - let linesValidationResult = previousLineIndentations.map { - validate(indentation: indentation, comparingTo: $0) - } + let startsWithClosingDelimiter = startsWithClosingDelimiter(in: line.content) + let linesValidationResult = validationResults( + for: indentation, + previousLineIndentations: previousLineIndentations, + startsWithClosingDelimiter: startsWithClosingDelimiter, + previousLineStartedWithClosingDelimiter: previousLineStartedWithClosingDelimiter, + previousLineWasInvalid: previousLineWasInvalid + ) // 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 +146,9 @@ struct IndentationWidthRule: OptInRule { // This mechanism avoids duplicate warnings. previousLineIndentations.append(indentation) } + + previousLineStartedWithClosingDelimiter = startsWithClosingDelimiter + previousLineWasInvalid = !isValidLine } return violations @@ -202,4 +215,58 @@ 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 validationResults( + for indentation: Indentation, + previousLineIndentations: [Indentation], + startsWithClosingDelimiter: Bool, + previousLineStartedWithClosingDelimiter: Bool, + previousLineWasInvalid: Bool + ) -> [Bool] { + if + startsWithClosingDelimiter, + let previousIndentation = previousLineIndentations.last { + return [ + validateClosingDelimiterLine( + indentation: indentation, + previousIndentation: previousIndentation, + 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 + } + + 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")