From 614d941dda73d792d9d52abfc71aa1016c2adfab Mon Sep 17 00:00:00 2001 From: sjh9714 <163989462+sjh9714@users.noreply.github.com> Date: Wed, 22 Jul 2026 07:38:48 +0900 Subject: [PATCH] Fix closure_end_indentation corrupting CRLF files The rule computed expected indentation by indexing SourceKitten's file.lines with a line number produced by SwiftSyntax's location converter. The two disagree on CRLF line endings: SwiftSyntax counts \r\n as one newline while StringView splits it into two, inserting a phantom empty line after every real line. On CRLF files the anchor line lookup then hit the wrong (often empty) line, so swiftlint --fix moved closing braces to column 1. Read the anchor line from locationConverter.sourceLines instead, which uses the same line numbering that produced the line number. --- CHANGELOG.md | 5 ++ .../Style/ClosureEndIndentationRule.swift | 7 ++- .../ClosureEndIndentationRuleTests.swift | 53 +++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 Tests/BuiltInRulesTests/ClosureEndIndentationRuleTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 8778f7025b..7ee726732d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,11 @@ [#6798](https://github.com/realm/SwiftLint/issues/6798) [#6799](https://github.com/realm/SwiftLint/issues/6799) +* Fix `closure_end_indentation` moving closing braces to column 1 in files + with CRLF line endings when running `swiftlint --fix`. + [sjh9714](https://github.com/sjh9714) + [#6598](https://github.com/realm/SwiftLint/issues/6598) + ## 0.65.0: Fresh Folded Fixtures ### Breaking diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRule.swift index 9ff0d0ab5a..9422aadab9 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRule.swift @@ -195,10 +195,13 @@ private extension ClosureEndIndentationRule { /// Calculates the column of the first non-whitespace character on a given line. private func getFirstNonWhitespaceColumn(onLine lineNumber: Int) -> Int { - guard lineNumber > 0, lineNumber <= file.lines.count else { + // Use the lines seen by `locationConverter`, which produced `lineNumber`. + // `file.lines` counts CRLF line endings differently, corrupting files. + let sourceLines = locationConverter.sourceLines + guard lineNumber > 0, lineNumber <= sourceLines.count else { return 1 // Should not happen } - let lineContent = file.lines[lineNumber - 1].content + let lineContent = sourceLines[lineNumber - 1] if let firstCharIndex = lineContent.firstIndex(where: { !$0.isWhitespace }) { return lineContent.distance(from: lineContent.startIndex, to: firstCharIndex) + 1 diff --git a/Tests/BuiltInRulesTests/ClosureEndIndentationRuleTests.swift b/Tests/BuiltInRulesTests/ClosureEndIndentationRuleTests.swift new file mode 100644 index 0000000000..5f4e2a7c0d --- /dev/null +++ b/Tests/BuiltInRulesTests/ClosureEndIndentationRuleTests.swift @@ -0,0 +1,53 @@ +import TestHelpers +import Testing + +@testable import SwiftLintBuiltInRules + +@Suite(.rulesRegistered) +struct ClosureEndIndentationRuleTests { + @Test + func crlfLineEndings() { + let correctlyIndented = [ + "struct TestView: View {", + " var body: some View {", + " HStack {", + " VStack {", + " Text(\"Hello\")", + " Spacer()", + " }", + " }", + " }", + "}", + "", + ].joined(separator: "\r\n") + let misindented = [ + "struct S {", + " func f() {", + " run {", + " print(\"hi\")", + " ↓}", + " }", + "}", + "", + ].joined(separator: "\r\n") + let corrected = [ + "struct S {", + " func f() {", + " run {", + " print(\"hi\")", + " }", + " }", + "}", + "", + ].joined(separator: "\r\n") + + // testWrappingInString is disabled because the test helper's string + // wrapping escapes only "\n", leaving bare "\r" bytes in the literal. + let description = ClosureEndIndentationRule.description + .with(nonTriggeringExamples: [Example(code: correctlyIndented)]) + .with(triggeringExamples: [Example(code: misindented, testWrappingInString: false)]) + .with(corrections: [Example(code: misindented): Example(code: corrected)]) + + verifyRule(description) + } +}