From 5d8d83ef2fbc54011bd32a3c7e822ea38059da9d Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Tue, 29 Jul 2025 15:51:25 +0100 Subject: [PATCH] Fix off-by-one errors in format range --- Sources/SwiftFormat.swift | 11 +++--- Tests/FormatterTests.swift | 70 +++++++++++++++++++++++++++++++++--- Tests/SwiftFormatTests.swift | 29 +-------------- 3 files changed, 74 insertions(+), 36 deletions(-) diff --git a/Sources/SwiftFormat.swift b/Sources/SwiftFormat.swift index 23ecc394..cb9dfe6d 100644 --- a/Sources/SwiftFormat.swift +++ b/Sources/SwiftFormat.swift @@ -431,8 +431,9 @@ public func tokenRange(forLineRange lineRange: ClosedRange, in tokens: [Tok let startOffset = SourceOffset(line: lineRange.lowerBound, column: 0) let endOffset = SourceOffset(line: lineRange.upperBound + 1, column: 0) // NOTE: tab width is not relevant for line-based offsets - return tokenIndex(for: startOffset, in: tokens, tabWidth: 1) - ..< tokenIndex(for: endOffset, in: tokens, tabWidth: 1) + let tokenStart = max(0, tokenIndex(for: startOffset, in: tokens, tabWidth: 1) - 1) + let tokenEnd = max(tokenStart, tokenIndex(for: endOffset, in: tokens, tabWidth: 1) - 1) + return tokenStart ..< tokenEnd } /// Get new offset for an original offset (before formatting) @@ -499,13 +500,14 @@ public func applyRules( to originalTokens: [Token], with options: FormatOptions, trackChanges: Bool, - range: Range?, + range originalRange: Range?, maxIterations: Int = 10 ) throws -> (tokens: [Token], changes: [Formatter.Change]) { precondition(maxIterations > 1) let originalRules = originalRules.sorted() var tokens = originalTokens + var range = originalRange // Ensure rule names have been set if originalRules.first?.name == FormatRule.unnamedRule { @@ -637,8 +639,9 @@ public func applyRules( return (tokens, changes) } - // Update tokens + // Update tokens and range tokens = newTokens + range = formatter.range // Remove rules that should only be run once if iteration == 0 { diff --git a/Tests/FormatterTests.swift b/Tests/FormatterTests.swift index 0f1bf9d3..dd1a51f7 100644 --- a/Tests/FormatterTests.swift +++ b/Tests/FormatterTests.swift @@ -519,7 +519,7 @@ class FormatterTests: XCTestCase { ]) } - // MARK: range + // MARK: Format range func testCodeOutsideRangeNotFormatted() throws { let input = tokenize(""" @@ -529,9 +529,14 @@ class FormatterTests: XCTestCase { } """) for range in [0 ..< 2, 5 ..< 7, 14 ..< 16, 17 ..< 19] { - XCTAssertEqual(try format(input, - rules: FormatRules.all, - range: range).tokens, input) + XCTAssertEqual(try sourceCode( + for: format( + input, + rules: FormatRules.all, + range: range + ).tokens + ), + sourceCode(for: input), "range \(range)") } let output1 = tokenize(""" func foo () { @@ -556,6 +561,63 @@ class FormatterTests: XCTestCase { ).tokens), output2) } + // MARK: format line range + + func testFormattingRange() { + let input = """ + let badlySpaced1:Int = 5 + let badlySpaced2:Int=5 + let badlySpaced3 : Int = 5 + """ + let output = """ + let badlySpaced1:Int = 5 + let badlySpaced2: Int = 5 + let badlySpaced3 : Int = 5 + """ + XCTAssertEqual(try format(input, lineRange: 2 ... 2).output, output) + } + + func testFormattingRange2() { + let input = """ + enum ImagesToShow { + case none + case mentioned + case all + } + """ + let output = """ + enum ImagesToShow + { + case none + case mentioned + case all + } + """ + let options = FormatOptions(allmanBraces: true) + XCTAssertEqual(try format(input, options: options, lineRange: 1 ... 2).output, output) + } + + func testFormattingRangeNoCrash() { + let input = """ + func foo() { + if bar { + print( "foo") + } + } + """ + let output = """ + func foo() { + if bar { + print("foo") + } + } + """ + let inputTokens = tokenize(input), outputTokens = tokenize(output) + XCTAssertEqual(tokenRange(forLineRange: 3 ... 4, in: inputTokens), 14 ..< 26) + XCTAssertEqual(tokenRange(forLineRange: 3 ... 4, in: outputTokens), 14 ..< 25) + XCTAssertEqual(try format(input, lineRange: 3 ... 4).output, output) + } + // MARK: endOfScope func testEndOfScopeInSwitch() throws { diff --git a/Tests/SwiftFormatTests.swift b/Tests/SwiftFormatTests.swift index 0603f648..d030e7bf 100644 --- a/Tests/SwiftFormatTests.swift +++ b/Tests/SwiftFormatTests.swift @@ -155,33 +155,6 @@ class SwiftFormatTests: XCTestCase { XCTAssertEqual(try format(input, rules: [], options: options).output, input) } - // MARK: format line range - - func testFormattingRange() { - let input = """ - let badlySpaced1:Int = 5 - let badlySpaced2:Int=5 - let badlySpaced3 : Int = 5 - """ - let output = """ - let badlySpaced1:Int = 5 - let badlySpaced2: Int = 5 - let badlySpaced3 : Int = 5 - """ - XCTAssertEqual(try format(input, lineRange: 2 ... 2).output, output) - } - - func testFormattingRangeNoCrash() { - let input = """ - func foo() { - if bar { - print( "foo") - } - } - """ - XCTAssertNoThrow(try format(input, lineRange: 3 ... 4)) - } - // MARK: conflict markers func testFormattingFailsForConflict() { @@ -264,7 +237,7 @@ class SwiftFormatTests: XCTestCase { func testTokenRange() { let tokens = tokenize("// a comment\n let foo = 5\n") - XCTAssertEqual(tokenRange(forLineRange: 1 ... 1, in: tokens), 0 ..< 4) + XCTAssertEqual(tokenRange(forLineRange: 1 ... 1, in: tokens), 0 ..< 3) } // MARK: newOffset