From 49c81a51955353fe2bb4c26706c2fd2ed71efe23 Mon Sep 17 00:00:00 2001 From: Hamish Knight Date: Wed, 11 Jun 2025 20:09:54 +0100 Subject: [PATCH 1/2] NFC: Remove a couple of unused functions --- Sources/SKUtilities/LineTable.swift | 24 ------------------- .../CodeCompletion/Connection.swift | 17 ------------- 2 files changed, 41 deletions(-) diff --git a/Sources/SKUtilities/LineTable.swift b/Sources/SKUtilities/LineTable.swift index 2d9abd05d..25387883a 100644 --- a/Sources/SKUtilities/LineTable.swift +++ b/Sources/SKUtilities/LineTable.swift @@ -115,30 +115,6 @@ extension LineTable { self.replace(fromLine: fromLine, utf16Offset: fromOff, toLine: toLine, utf16Offset: toOff, with: replacement) } - /// Replace the line table's `content` in the given range and update the line data. - /// - /// - parameter fromLine: Starting line number (zero-based). - /// - parameter fromOff: Starting UTF-8 column offset (zero-based). - /// - parameter toLine: Ending line number (zero-based). - /// - parameter toOff: Ending UTF-8 column offset (zero-based). - /// - parameter replacement: The new text for the given range. - @inlinable - mutating package func replace( - fromLine: Int, - utf8Offset fromOff: Int, - toLine: Int, - utf8Offset toOff: Int, - with replacement: String - ) { - let start = content.utf8.index(impl[fromLine], offsetBy: fromOff) - let end = content.utf8.index(impl[toLine], offsetBy: toOff) - - var newText = self.content - newText.replaceSubrange(start.. Date: Wed, 11 Jun 2025 20:09:54 +0100 Subject: [PATCH 2/2] Avoid crashing on invalid range in `editDocument` Rename `LineTable.replace(utf8Offset:length:with)` to `tryReplace` and bail if the provided range is out of bounds of the buffer. This ensures we match the behavior of SourceKit when handling an `editor.replacetext` request. rdar://90385969 --- Sources/SKUtilities/LineTable.swift | 38 ++++++++-- .../CodeCompletion/Connection.swift | 7 +- .../SwiftSourceKitPluginTests.swift | 74 +++++++++++++++++++ 3 files changed, 110 insertions(+), 9 deletions(-) diff --git a/Sources/SKUtilities/LineTable.swift b/Sources/SKUtilities/LineTable.swift index 25387883a..bd6ea4a39 100644 --- a/Sources/SKUtilities/LineTable.swift +++ b/Sources/SKUtilities/LineTable.swift @@ -115,21 +115,45 @@ extension LineTable { self.replace(fromLine: fromLine, utf16Offset: fromOff, toLine: toLine, utf16Offset: toOff, with: replacement) } + private struct OutOfBoundsError: Error, CustomLogStringConvertible { + var utf8Range: (lower: Int, upper: Int) + var utf8Bounds: (lower: Int, upper: Int) + + var description: String { + """ + \(utf8Range.lower)..<\(utf8Range.upper) is out of bounds \ + \(utf8Bounds.lower)..<\(utf8Bounds.upper) + """ + } + + var redactedDescription: String { + description + } + } + /// Replace the line table's `content` in the given range and update the line data. + /// If the given range is out-of-bounds, throws an error. /// - /// - parameter fromLine: Starting line number (zero-based). - /// - parameter fromOff: Starting UTF-8 column offset (zero-based). - /// - parameter toLine: Ending line number (zero-based). - /// - parameter toOff: Ending UTF-8 column offset (zero-based). + /// - parameter utf8Offset: Starting UTF-8 offset (zero-based). + /// - parameter length: UTF-8 length. /// - parameter replacement: The new text for the given range. @inlinable mutating package func replace( utf8Offset fromOff: Int, length: Int, with replacement: String - ) { - let start = content.utf8.index(content.startIndex, offsetBy: fromOff) - let end = content.utf8.index(content.startIndex, offsetBy: fromOff + length) + ) throws { + let utf8 = self.content.utf8 + guard + fromOff >= 0, length >= 0, + let start = utf8.index(utf8.startIndex, offsetBy: fromOff, limitedBy: utf8.endIndex), + let end = utf8.index(start, offsetBy: length, limitedBy: utf8.endIndex) + else { + throw OutOfBoundsError( + utf8Range: (lower: fromOff, upper: fromOff + length), + utf8Bounds: (lower: 0, upper: utf8.count) + ) + } var newText = self.content newText.replaceSubrange(start.. Int {} + } + + """ + var fullText = typeWithMethod + + try await sourcekitd.editDocument(path, fromOffset: 0, length: 0, newContents: typeWithMethod) + + let completion = """ + S. + """ + fullText += completion + + try await sourcekitd.editDocument(path, fromOffset: typeWithMethod.utf8.count, length: 0, newContents: completion) + + func testCompletion(file: StaticString = #filePath, line: UInt = #line) async throws { + let result = try await sourcekitd.completeOpen( + path: path, + position: Position(line: 3, utf16index: 2), + filter: "foo", + flags: [] + ) + XCTAssertGreaterThan(result.unfilteredResultCount, 1, file: file, line: line) + XCTAssertEqual(result.items.count, 1, file: file, line: line) + } + try await testCompletion() + + // Bogus edits are ignored (negative offsets crash SourceKit itself so we don't test them here). + await assertThrowsError( + try await sourcekitd.editDocument(path, fromOffset: 0, length: 99999, newContents: "") + ) + await assertThrowsError( + try await sourcekitd.editDocument(path, fromOffset: 99999, length: 1, newContents: "") + ) + await assertThrowsError( + try await sourcekitd.editDocument(path, fromOffset: 99999, length: 0, newContents: "unrelated") + ) + // SourceKit doesn't throw an error for a no-op edit. + try await sourcekitd.editDocument(path, fromOffset: 99999, length: 0, newContents: "") + + try await sourcekitd.editDocument(path, fromOffset: 0, length: 0, newContents: "") + try await sourcekitd.editDocument(path, fromOffset: fullText.utf8.count, length: 0, newContents: "") + + try await testCompletion() + + let badCompletion = """ + X. + """ + fullText = fullText.dropLast(2) + badCompletion + + try await sourcekitd.editDocument(path, fromOffset: fullText.utf8.count - 2, length: 2, newContents: badCompletion) + + let result = try await sourcekitd.completeOpen( + path: path, + position: Position(line: 3, utf16index: 2), + filter: "foo", + flags: [] + ) + XCTAssertEqual(result.unfilteredResultCount, 0) + XCTAssertEqual(result.items.count, 0) + } + func testDocumentation() async throws { try await SkipUnless.sourcekitdSupportsPlugin() let sourcekitd = try await getSourceKitD()