mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-02 18:23:24 +01:00
1489 lines
44 KiB
Swift
1489 lines
44 KiB
Swift
//===----------------------------------------------------------------------===//
|
||
//
|
||
// This source file is part of the Swift.org open source project
|
||
//
|
||
// Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors
|
||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||
//
|
||
// See https://swift.org/LICENSE.txt for license information
|
||
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
||
//
|
||
//===----------------------------------------------------------------------===//
|
||
|
||
import LanguageServerProtocol
|
||
import SKLogging
|
||
import SKOptions
|
||
import SKTestSupport
|
||
import SourceKitD
|
||
@_spi(Testing) import SourceKitLSP
|
||
import SwiftExtensions
|
||
import SwiftParser
|
||
import SwiftSyntax
|
||
import XCTest
|
||
|
||
final class LocalSwiftTests: XCTestCase {
|
||
private let quickFixCapabilities = ClientCapabilities(
|
||
textDocument: TextDocumentClientCapabilities(
|
||
codeAction: .init(
|
||
codeActionLiteralSupport: .init(
|
||
codeActionKind: .init(valueSet: [.quickFix])
|
||
)
|
||
)
|
||
)
|
||
)
|
||
|
||
// MARK: - Tests
|
||
|
||
func testEditing() async throws {
|
||
let testClient = try await TestSourceKitLSPClient(usePullDiagnostics: false)
|
||
let uri = DocumentURI(for: .swift)
|
||
|
||
let documentManager = await testClient.server.documentManager
|
||
|
||
testClient.openDocument("func", uri: uri, version: 12)
|
||
|
||
let openDiags = try await testClient.nextDiagnosticsNotification()
|
||
XCTAssertEqual(openDiags.diagnostics.count, 1)
|
||
XCTAssertEqual(
|
||
openDiags.diagnostics.first?.range.lowerBound,
|
||
Position(line: 0, utf16index: 4)
|
||
)
|
||
XCTAssertEqual("func", try documentManager.latestSnapshot(uri).text)
|
||
|
||
testClient.send(
|
||
DidChangeTextDocumentNotification(
|
||
textDocument: .init(uri, version: 13),
|
||
contentChanges: [
|
||
.init(range: Range(Position(line: 0, utf16index: 4)), text: " foo() {}\n")
|
||
]
|
||
)
|
||
)
|
||
let edit1Diags = try await testClient.nextDiagnosticsNotification()
|
||
XCTAssertEqual(edit1Diags.diagnostics.count, 0)
|
||
XCTAssertEqual("func foo() {}\n", try documentManager.latestSnapshot(uri).text)
|
||
|
||
testClient.send(
|
||
DidChangeTextDocumentNotification(
|
||
textDocument: .init(uri, version: 14),
|
||
contentChanges: [
|
||
.init(range: Range(Position(line: 1, utf16index: 0)), text: "bar()")
|
||
]
|
||
)
|
||
)
|
||
let edit2Diags = try await testClient.nextDiagnosticsNotification()
|
||
// 1 = semantic update finished already
|
||
XCTAssertEqual(edit2Diags.diagnostics.count, 1)
|
||
XCTAssertEqual(
|
||
edit2Diags.diagnostics.first?.range.lowerBound,
|
||
Position(line: 1, utf16index: 0)
|
||
)
|
||
XCTAssertEqual(
|
||
"""
|
||
func foo() {}
|
||
bar()
|
||
""",
|
||
try documentManager.latestSnapshot(uri).text
|
||
)
|
||
|
||
testClient.send(
|
||
DidChangeTextDocumentNotification(
|
||
textDocument: .init(uri, version: 15),
|
||
contentChanges: [
|
||
.init(range: Position(line: 1, utf16index: 0)..<Position(line: 1, utf16index: 3), text: "foo")
|
||
]
|
||
)
|
||
)
|
||
|
||
let edit3Diags = try await testClient.nextDiagnosticsNotification()
|
||
XCTAssertEqual(edit3Diags.diagnostics.count, 0)
|
||
XCTAssertEqual(
|
||
"""
|
||
func foo() {}
|
||
foo()
|
||
""",
|
||
try documentManager.latestSnapshot(uri).text
|
||
)
|
||
|
||
testClient.send(
|
||
DidChangeTextDocumentNotification(
|
||
textDocument: .init(uri, version: 16),
|
||
contentChanges: [
|
||
.init(range: Position(line: 1, utf16index: 0)..<Position(line: 1, utf16index: 3), text: "fooTypo")
|
||
]
|
||
)
|
||
)
|
||
let edit4Diags = try await testClient.nextDiagnosticsNotification()
|
||
// 0 = only syntactic
|
||
XCTAssertEqual(edit4Diags.diagnostics.count, 1)
|
||
XCTAssertEqual(
|
||
edit4Diags.diagnostics.first?.range.lowerBound,
|
||
Position(line: 1, utf16index: 0)
|
||
)
|
||
XCTAssertEqual(
|
||
"""
|
||
func foo() {}
|
||
fooTypo()
|
||
""",
|
||
try documentManager.latestSnapshot(uri).text
|
||
)
|
||
|
||
testClient.send(
|
||
DidChangeTextDocumentNotification(
|
||
textDocument: .init(uri, version: 17),
|
||
contentChanges: [
|
||
.init(
|
||
range: nil,
|
||
text: """
|
||
func bar() {}
|
||
foo()
|
||
"""
|
||
)
|
||
]
|
||
)
|
||
)
|
||
|
||
let edit5Diags = try await testClient.nextDiagnosticsNotification()
|
||
XCTAssertEqual(edit5Diags.diagnostics.count, 1)
|
||
XCTAssertEqual(
|
||
edit5Diags.diagnostics.first?.range.lowerBound,
|
||
Position(line: 1, utf16index: 0)
|
||
)
|
||
XCTAssertEqual(
|
||
"""
|
||
func bar() {}
|
||
foo()
|
||
""",
|
||
try documentManager.latestSnapshot(uri).text
|
||
)
|
||
}
|
||
|
||
func testEditingNonURL() async throws {
|
||
let testClient = try await TestSourceKitLSPClient(usePullDiagnostics: false)
|
||
let uri = try DocumentURI(string: "urn:uuid:A1B08909-E791-469E-BF0F-F5790977E051")
|
||
|
||
let documentManager = await testClient.server.documentManager
|
||
|
||
testClient.openDocument("func", uri: uri, language: .swift)
|
||
|
||
let openDiags = try await testClient.nextDiagnosticsNotification()
|
||
XCTAssertEqual(openDiags.diagnostics.count, 1)
|
||
XCTAssertEqual(
|
||
openDiags.diagnostics.first?.range.lowerBound,
|
||
Position(line: 0, utf16index: 4)
|
||
)
|
||
try XCTAssertEqual("func", documentManager.latestSnapshot(uri).text)
|
||
|
||
testClient.send(
|
||
DidChangeTextDocumentNotification(
|
||
textDocument: .init(uri, version: 13),
|
||
contentChanges: [
|
||
.init(range: Range(Position(line: 0, utf16index: 4)), text: " foo() {}\n")
|
||
]
|
||
)
|
||
)
|
||
|
||
let edit1Diags = try await testClient.nextDiagnosticsNotification()
|
||
XCTAssertEqual(edit1Diags.diagnostics.count, 0)
|
||
try XCTAssertEqual("func foo() {}\n", documentManager.latestSnapshot(uri).text)
|
||
|
||
testClient.send(
|
||
DidChangeTextDocumentNotification(
|
||
textDocument: .init(uri, version: 14),
|
||
contentChanges: [
|
||
.init(range: Range(Position(line: 1, utf16index: 0)), text: "bar()")
|
||
]
|
||
)
|
||
)
|
||
|
||
let edit2Diags = try await testClient.nextDiagnosticsNotification()
|
||
XCTAssertEqual(edit2Diags.diagnostics.count, 1)
|
||
XCTAssertEqual(
|
||
edit2Diags.diagnostics.first?.range.lowerBound,
|
||
Position(line: 1, utf16index: 0)
|
||
)
|
||
XCTAssertEqual(
|
||
"""
|
||
func foo() {}
|
||
bar()
|
||
""",
|
||
try documentManager.latestSnapshot(uri).text
|
||
)
|
||
|
||
testClient.send(
|
||
DidChangeTextDocumentNotification(
|
||
textDocument: .init(uri, version: 15),
|
||
contentChanges: [
|
||
.init(range: Position(line: 1, utf16index: 0)..<Position(line: 1, utf16index: 3), text: "foo")
|
||
]
|
||
)
|
||
)
|
||
|
||
let edit3Diags = try await testClient.nextDiagnosticsNotification()
|
||
XCTAssertEqual(edit3Diags.diagnostics.count, 0)
|
||
XCTAssertEqual(
|
||
"""
|
||
func foo() {}
|
||
foo()
|
||
""",
|
||
try documentManager.latestSnapshot(uri).text
|
||
)
|
||
|
||
testClient.send(
|
||
DidChangeTextDocumentNotification(
|
||
textDocument: .init(uri, version: 16),
|
||
contentChanges: [
|
||
.init(range: Position(line: 1, utf16index: 0)..<Position(line: 1, utf16index: 3), text: "fooTypo")
|
||
]
|
||
)
|
||
)
|
||
let edit4Diags = try await testClient.nextDiagnosticsNotification()
|
||
XCTAssertEqual(edit4Diags.diagnostics.count, 1)
|
||
XCTAssertEqual(
|
||
edit4Diags.diagnostics.first?.range.lowerBound,
|
||
Position(line: 1, utf16index: 0)
|
||
)
|
||
XCTAssertEqual(
|
||
"""
|
||
func foo() {}
|
||
fooTypo()
|
||
""",
|
||
try documentManager.latestSnapshot(uri).text
|
||
)
|
||
|
||
testClient.send(
|
||
DidChangeTextDocumentNotification(
|
||
textDocument: .init(uri, version: 17),
|
||
contentChanges: [
|
||
.init(
|
||
range: nil,
|
||
text: """
|
||
func bar() {}
|
||
foo()
|
||
"""
|
||
)
|
||
]
|
||
)
|
||
)
|
||
|
||
let edit5Diags = try await testClient.nextDiagnosticsNotification()
|
||
XCTAssertEqual(edit5Diags.diagnostics.count, 1)
|
||
XCTAssertEqual(
|
||
edit5Diags.diagnostics.first?.range.lowerBound,
|
||
Position(line: 1, utf16index: 0)
|
||
)
|
||
XCTAssertEqual(
|
||
"""
|
||
func bar() {}
|
||
foo()
|
||
""",
|
||
try documentManager.latestSnapshot(uri).text
|
||
)
|
||
|
||
}
|
||
|
||
func testExcludedDocumentSchemeDiagnostics() async throws {
|
||
let testClient = try await TestSourceKitLSPClient(usePullDiagnostics: false)
|
||
let includedURL = URL(fileURLWithPath: "/a.swift")
|
||
let includedURI = DocumentURI(includedURL)
|
||
|
||
let excludedURI = try DocumentURI(string: "git:/a.swift")
|
||
|
||
// Open the excluded URI first so our later notification handlers can confirm
|
||
// that no diagnostics were emitted for this excluded URI.
|
||
testClient.openDocument("func", uri: excludedURI, language: .swift)
|
||
|
||
testClient.openDocument("func", uri: includedURI, language: .swift)
|
||
let diags = try await testClient.nextDiagnosticsNotification()
|
||
XCTAssertEqual(diags.uri, includedURI)
|
||
}
|
||
|
||
func testCrossFileDiagnostics() async throws {
|
||
let testClient = try await TestSourceKitLSPClient(usePullDiagnostics: false)
|
||
let urlA = URL(fileURLWithPath: "/\(UUID())/a.swift")
|
||
let urlB = URL(fileURLWithPath: "/\(UUID())/b.swift")
|
||
let uriA = DocumentURI(urlA)
|
||
let uriB = DocumentURI(urlB)
|
||
|
||
testClient.openDocument("foo()", uri: uriA)
|
||
|
||
let openADiags = try await testClient.nextDiagnosticsNotification()
|
||
XCTAssertEqual(openADiags.diagnostics.count, 1)
|
||
XCTAssertEqual(
|
||
openADiags.diagnostics.first?.range.lowerBound,
|
||
Position(line: 0, utf16index: 0)
|
||
)
|
||
|
||
testClient.openDocument("bar()", uri: uriB)
|
||
|
||
let openBDiags = try await testClient.nextDiagnosticsNotification()
|
||
XCTAssertEqual(openBDiags.diagnostics.count, 1)
|
||
XCTAssertEqual(
|
||
openBDiags.diagnostics.first?.range.lowerBound,
|
||
Position(line: 0, utf16index: 0)
|
||
)
|
||
|
||
testClient.send(
|
||
DidChangeTextDocumentNotification(
|
||
textDocument: .init(uriA, version: 13),
|
||
contentChanges: [
|
||
.init(range: nil, text: "foo()\n")
|
||
]
|
||
)
|
||
)
|
||
|
||
let editADiags = try await testClient.nextDiagnosticsNotification()
|
||
XCTAssertEqual(editADiags.diagnostics.count, 1)
|
||
}
|
||
|
||
func testDiagnosticsReopen() async throws {
|
||
let testClient = try await TestSourceKitLSPClient(usePullDiagnostics: false)
|
||
let urlA = URL(fileURLWithPath: "/\(UUID())/a.swift")
|
||
let uriA = DocumentURI(urlA)
|
||
|
||
testClient.openDocument("foo()", uri: uriA)
|
||
|
||
let open1Diags = try await testClient.nextDiagnosticsNotification()
|
||
XCTAssertEqual(open1Diags.diagnostics.count, 1)
|
||
XCTAssertEqual(
|
||
open1Diags.diagnostics.first?.range.lowerBound,
|
||
Position(line: 0, utf16index: 0)
|
||
)
|
||
|
||
testClient.send(DidCloseTextDocumentNotification(textDocument: .init(urlA)))
|
||
|
||
testClient.openDocument("var", uri: uriA)
|
||
|
||
let open2Diags = try await testClient.nextDiagnosticsNotification()
|
||
XCTAssertEqual(open2Diags.diagnostics.count, 1)
|
||
XCTAssertEqual(
|
||
open2Diags.diagnostics.first?.range.lowerBound,
|
||
Position(line: 0, utf16index: 3)
|
||
)
|
||
}
|
||
|
||
func testEducationalNotesAreUsedAsDiagnosticCodes() async throws {
|
||
let testClient = try await TestSourceKitLSPClient(
|
||
capabilities: ClientCapabilities(
|
||
textDocument: TextDocumentClientCapabilities(
|
||
publishDiagnostics: .init(codeDescriptionSupport: true)
|
||
)
|
||
),
|
||
usePullDiagnostics: false
|
||
)
|
||
let url = URL(fileURLWithPath: "/\(UUID())/a.swift")
|
||
let uri = DocumentURI(url)
|
||
|
||
testClient.openDocument("@propertyWrapper struct Bar {}", uri: uri)
|
||
|
||
let diags = try await testClient.nextDiagnosticsNotification()
|
||
XCTAssertEqual(diags.diagnostics.count, 1)
|
||
let diag = diags.diagnostics.first!
|
||
XCTAssertEqual(diag.code, .string("property-wrapper-requirements"))
|
||
let filename = try XCTUnwrap(diag.codeDescription?.href.fileURL?.lastPathComponent)
|
||
XCTAssert(filename.starts(with: "property-wrapper-requirements"))
|
||
}
|
||
|
||
func testFixitsAreIncludedInPublishDiagnostics() async throws {
|
||
let testClient = try await TestSourceKitLSPClient(usePullDiagnostics: false)
|
||
let url = URL(fileURLWithPath: "/\(UUID())/a.swift")
|
||
let uri = DocumentURI(url)
|
||
|
||
testClient.openDocument(
|
||
"""
|
||
func foo() {
|
||
let a = 2
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
|
||
let diags = try await testClient.nextDiagnosticsNotification()
|
||
XCTAssertEqual(diags.diagnostics.count, 1)
|
||
let diag = diags.diagnostics.first!
|
||
XCTAssertNotNil(diag.codeActions)
|
||
XCTAssertEqual(diag.codeActions!.count, 1)
|
||
let fixit = diag.codeActions!.first!
|
||
|
||
// Expected Fix-it: Replace `let a` with `_` because it's never used
|
||
let expectedTextEdit = TextEdit(
|
||
range: Position(line: 1, utf16index: 2)..<Position(line: 1, utf16index: 7),
|
||
newText: "_"
|
||
)
|
||
XCTAssertEqual(
|
||
fixit,
|
||
CodeAction(
|
||
title: "Replace 'let a' with '_'",
|
||
kind: .quickFix,
|
||
diagnostics: nil,
|
||
edit: WorkspaceEdit(changes: [uri: [expectedTextEdit]], documentChanges: nil),
|
||
command: nil
|
||
)
|
||
)
|
||
}
|
||
|
||
func testFixitsAreIncludedInPublishDiagnosticsNotifications() async throws {
|
||
let testClient = try await TestSourceKitLSPClient(usePullDiagnostics: false)
|
||
let url = URL(fileURLWithPath: "/\(UUID())/a.swift")
|
||
let uri = DocumentURI(url)
|
||
|
||
testClient.openDocument(
|
||
"""
|
||
func foo(a: Int?) {
|
||
_ = a.bigEndian
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
|
||
let diags = try await testClient.nextDiagnosticsNotification()
|
||
XCTAssertEqual(diags.diagnostics.count, 1)
|
||
let diag = diags.diagnostics.first!
|
||
XCTAssertEqual(diag.relatedInformation?.count, 2)
|
||
if let note1 = diag.relatedInformation?.first(where: { $0.message.contains("'?'") }) {
|
||
XCTAssertEqual(note1.codeActions?.count, 1)
|
||
if let fixit = note1.codeActions?.first {
|
||
// Expected Fix-it: Replace `let a` with `_` because it's never used
|
||
let expectedTextEdit = TextEdit(
|
||
range: Position(line: 1, utf16index: 7)..<Position(line: 1, utf16index: 7),
|
||
newText: "?"
|
||
)
|
||
XCTAssertEqual(
|
||
fixit,
|
||
CodeAction(
|
||
title: "Chain the optional using '?' to access member 'bigEndian' only for non-'nil' base values",
|
||
kind: .quickFix,
|
||
diagnostics: nil,
|
||
edit: WorkspaceEdit(changes: [uri: [expectedTextEdit]], documentChanges: nil),
|
||
command: nil
|
||
)
|
||
)
|
||
}
|
||
} else {
|
||
XCTFail("missing '?' note")
|
||
}
|
||
if let note2 = diag.relatedInformation?.first(where: { $0.message.contains("'!'") }) {
|
||
XCTAssertEqual(note2.codeActions?.count, 1)
|
||
if let fixit = note2.codeActions?.first {
|
||
// Expected Fix-it: Replace `let a` with `_` because it's never used
|
||
let expectedTextEdit = TextEdit(
|
||
range: Position(line: 1, utf16index: 7)..<Position(line: 1, utf16index: 7),
|
||
newText: "!"
|
||
)
|
||
XCTAssertEqual(
|
||
fixit,
|
||
CodeAction(
|
||
title: "Force-unwrap using '!' to abort execution if the optional value contains 'nil'",
|
||
kind: .quickFix,
|
||
diagnostics: nil,
|
||
edit: WorkspaceEdit(changes: [uri: [expectedTextEdit]], documentChanges: nil),
|
||
command: nil
|
||
)
|
||
)
|
||
}
|
||
} else {
|
||
XCTFail("missing '!' note")
|
||
}
|
||
}
|
||
|
||
func testFixitInsert() async throws {
|
||
let testClient = try await TestSourceKitLSPClient(usePullDiagnostics: false)
|
||
let url = URL(fileURLWithPath: "/\(UUID())/a.swift")
|
||
let uri = DocumentURI(url)
|
||
|
||
testClient.openDocument(
|
||
"""
|
||
func foo() {
|
||
print("")print("")
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
|
||
let diags = try await testClient.nextDiagnosticsNotification()
|
||
XCTAssertEqual(diags.diagnostics.count, 1)
|
||
let diag = diags.diagnostics.first!
|
||
XCTAssertNotNil(diag.codeActions)
|
||
XCTAssertEqual(diag.codeActions!.count, 1)
|
||
let fixit = diag.codeActions!.first!
|
||
|
||
// Expected Fix-it: Insert `;`
|
||
let expectedTextEdit = TextEdit(
|
||
range: Position(line: 1, utf16index: 11)..<Position(line: 1, utf16index: 11),
|
||
newText: ";"
|
||
)
|
||
XCTAssertEqual(
|
||
fixit,
|
||
CodeAction(
|
||
title: "Insert ';'",
|
||
kind: .quickFix,
|
||
diagnostics: nil,
|
||
edit: WorkspaceEdit(changes: [uri: [expectedTextEdit]], documentChanges: nil),
|
||
command: nil
|
||
)
|
||
)
|
||
}
|
||
|
||
func testFixitTitle() {
|
||
XCTAssertEqual("Insert ';'", CodeAction.fixitTitle(replace: "", with: ";"))
|
||
XCTAssertEqual("Replace 'let a' with '_'", CodeAction.fixitTitle(replace: "let a", with: "_"))
|
||
XCTAssertEqual("Remove 'foo ='", CodeAction.fixitTitle(replace: "foo =", with: ""))
|
||
}
|
||
|
||
func testFixitsAreReturnedFromCodeActions() async throws {
|
||
let testClient = try await TestSourceKitLSPClient(capabilities: quickFixCapabilities, usePullDiagnostics: false)
|
||
let url = URL(fileURLWithPath: "/\(UUID())/a.swift")
|
||
let uri = DocumentURI(url)
|
||
|
||
testClient.openDocument(
|
||
"""
|
||
func foo() {
|
||
let a = 2
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
|
||
let diags = try await testClient.nextDiagnosticsNotification()
|
||
XCTAssertEqual(diags.diagnostics.count, 1)
|
||
let diagnostic = diags.diagnostics.first
|
||
|
||
let request = CodeActionRequest(
|
||
range: Position(line: 1, utf16index: 0)..<Position(line: 1, utf16index: 11),
|
||
context: CodeActionContext(
|
||
diagnostics: [try XCTUnwrap(diagnostic, "expected diagnostic to be available")],
|
||
only: nil
|
||
),
|
||
textDocument: TextDocumentIdentifier(uri)
|
||
)
|
||
let response = try await testClient.send(request)
|
||
|
||
XCTAssertNotNil(response)
|
||
guard case .codeActions(let codeActions) = response else {
|
||
XCTFail("Expected code actions as response")
|
||
return
|
||
}
|
||
let quickFixes = codeActions.filter { $0.kind == .quickFix }
|
||
XCTAssertEqual(quickFixes.count, 1)
|
||
let fixit = quickFixes.first!
|
||
|
||
// Diagnostic returned by code actions cannot be recursive
|
||
var expectedDiagnostic = try XCTUnwrap(diagnostic, "expected diagnostic to be available")
|
||
expectedDiagnostic.codeActions = nil
|
||
|
||
// Expected Fix-it: Replace `let a` with `_` because it's never used
|
||
let expectedTextEdit = TextEdit(
|
||
range: Position(line: 1, utf16index: 2)..<Position(line: 1, utf16index: 7),
|
||
newText: "_"
|
||
)
|
||
XCTAssertEqual(
|
||
fixit,
|
||
CodeAction(
|
||
title: "Replace 'let a' with '_'",
|
||
kind: .quickFix,
|
||
diagnostics: [expectedDiagnostic],
|
||
edit: WorkspaceEdit(changes: [uri: [expectedTextEdit]], documentChanges: nil),
|
||
command: nil
|
||
)
|
||
)
|
||
}
|
||
|
||
func testFixitsAreReturnedFromCodeActionsNotifications() async throws {
|
||
let testClient = try await TestSourceKitLSPClient(capabilities: quickFixCapabilities, usePullDiagnostics: false)
|
||
let url = URL(fileURLWithPath: "/\(UUID())/a.swift")
|
||
let uri = DocumentURI(url)
|
||
|
||
testClient.openDocument(
|
||
"""
|
||
func foo(a: Int?) {
|
||
_ = a.bigEndian
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
|
||
let diags = try await testClient.nextDiagnosticsNotification()
|
||
XCTAssertEqual(diags.diagnostics.count, 1)
|
||
let diagnostic = diags.diagnostics.first
|
||
|
||
let request = CodeActionRequest(
|
||
range: Position(line: 1, utf16index: 0)..<Position(line: 1, utf16index: 11),
|
||
context: CodeActionContext(
|
||
diagnostics: [try XCTUnwrap(diagnostic, "expected diagnostic to be available")],
|
||
only: nil
|
||
),
|
||
textDocument: TextDocumentIdentifier(uri)
|
||
)
|
||
let response = try await testClient.send(request)
|
||
|
||
XCTAssertNotNil(response)
|
||
guard case .codeActions(let codeActions) = response else {
|
||
XCTFail("Expected code actions as response")
|
||
return
|
||
}
|
||
let quickFixes = codeActions.filter { $0.kind == .quickFix }
|
||
XCTAssertEqual(quickFixes.count, 2)
|
||
|
||
var expectedTextEdit = TextEdit(
|
||
range: Position(line: 1, utf16index: 7)..<Position(line: 1, utf16index: 7),
|
||
newText: "_"
|
||
)
|
||
|
||
for fixit in quickFixes {
|
||
if fixit.title.contains("!") {
|
||
XCTAssert(fixit.title.starts(with: "Force-unwrap using '!'"))
|
||
expectedTextEdit.newText = "!"
|
||
XCTAssertEqual(fixit.edit, WorkspaceEdit(changes: [uri: [expectedTextEdit]], documentChanges: nil))
|
||
} else {
|
||
XCTAssert(fixit.title.starts(with: "Chain the optional using '?'"))
|
||
expectedTextEdit.newText = "?"
|
||
XCTAssertEqual(fixit.edit, WorkspaceEdit(changes: [uri: [expectedTextEdit]], documentChanges: nil))
|
||
}
|
||
XCTAssertEqual(fixit.kind, .quickFix)
|
||
XCTAssertEqual(fixit.diagnostics?.count, 1)
|
||
XCTAssertEqual(fixit.diagnostics?.first?.severity, .error)
|
||
XCTAssertEqual(fixit.diagnostics?.first?.range, Range(Position(line: 1, utf16index: 6)))
|
||
XCTAssert(fixit.diagnostics?.first?.message.starts(with: "Value of optional type") == true)
|
||
}
|
||
}
|
||
|
||
func testMultiEditFixitCodeActionPrimary() async throws {
|
||
let project = try await IndexedSingleSwiftFileTestProject(
|
||
"""
|
||
func foo(a: Int, b: Int) {}
|
||
func test() {
|
||
foo(1️⃣1, 2️⃣2)
|
||
}
|
||
""",
|
||
capabilities: quickFixCapabilities,
|
||
allowBuildFailure: true
|
||
)
|
||
let diags = try await project.testClient.send(
|
||
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(project.fileURI))
|
||
)
|
||
let diagnostic = try XCTUnwrap(diags.fullReport?.items.only)
|
||
|
||
let request = CodeActionRequest(
|
||
range: diagnostic.range,
|
||
context: CodeActionContext(
|
||
diagnostics: [try XCTUnwrap(diagnostic, "expected diagnostic to be available")],
|
||
only: nil
|
||
),
|
||
textDocument: TextDocumentIdentifier(project.fileURI)
|
||
)
|
||
let response = try await project.testClient.send(request)
|
||
|
||
guard case .codeActions(let codeActions) = response else {
|
||
XCTFail("Expected code actions as response")
|
||
return
|
||
}
|
||
let quickFixes = codeActions.filter { $0.kind == .quickFix }
|
||
let fixit = try XCTUnwrap(quickFixes.only)
|
||
|
||
XCTAssertEqual(fixit.title, "Insert 'a: '...")
|
||
XCTAssertEqual(fixit.diagnostics?.count, 1)
|
||
XCTAssertEqual(
|
||
fixit.edit?.changes?[project.fileURI],
|
||
[
|
||
TextEdit(range: Range(project.positions["1️⃣"]), newText: "a: "),
|
||
TextEdit(range: Range(project.positions["2️⃣"]), newText: "b: "),
|
||
]
|
||
)
|
||
}
|
||
|
||
func testMuliEditFixitCodeActionNotifications() async throws {
|
||
let testClient = try await TestSourceKitLSPClient(capabilities: quickFixCapabilities, usePullDiagnostics: false)
|
||
let url = URL(fileURLWithPath: "/\(UUID())/a.swift")
|
||
let uri = DocumentURI(url)
|
||
|
||
testClient.openDocument(
|
||
"""
|
||
@available(*, deprecated, renamed: "new(_:hotness:)")
|
||
func old(and: Int, busted: Int) {}
|
||
func test() {
|
||
old(and: 1, busted: 2)
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
|
||
let diags = try await testClient.nextDiagnosticsNotification()
|
||
XCTAssertEqual(diags.diagnostics.count, 1)
|
||
let diagnostic = diags.diagnostics.first!
|
||
|
||
let request = CodeActionRequest(
|
||
range: Position(line: 3, utf16index: 2)..<Position(line: 3, utf16index: 2),
|
||
context: CodeActionContext(
|
||
diagnostics: [try XCTUnwrap(diagnostic, "expected diagnostic to be available")],
|
||
only: nil
|
||
),
|
||
textDocument: TextDocumentIdentifier(uri)
|
||
)
|
||
let response = try await testClient.send(request)
|
||
|
||
XCTAssertNotNil(response)
|
||
guard case .codeActions(let codeActions) = response else {
|
||
XCTFail("Expected code actions as response")
|
||
return
|
||
}
|
||
let quickFixes = codeActions.filter { $0.kind == .quickFix }
|
||
XCTAssertEqual(quickFixes.count, 1)
|
||
guard let fixit = quickFixes.first else { return }
|
||
|
||
XCTAssertEqual(fixit.title, "Use 'new(_:hotness:)' instead")
|
||
XCTAssertEqual(fixit.diagnostics?.count, 1)
|
||
assertContains(try XCTUnwrap(fixit.diagnostics?.first?.message), "is deprecated")
|
||
XCTAssertEqual(
|
||
fixit.edit?.changes?[uri],
|
||
[
|
||
TextEdit(range: Position(line: 3, utf16index: 2)..<Position(line: 3, utf16index: 5), newText: "new"),
|
||
TextEdit(range: Position(line: 3, utf16index: 6)..<Position(line: 3, utf16index: 11), newText: ""),
|
||
TextEdit(range: Position(line: 3, utf16index: 14)..<Position(line: 3, utf16index: 20), newText: "hotness"),
|
||
]
|
||
)
|
||
}
|
||
|
||
func testXMLToMarkdownDeclaration() {
|
||
XCTAssertEqual(
|
||
try xmlDocumentationToMarkdown(
|
||
"""
|
||
<Declaration>func foo(_ bar: <Type usr="fake">Baz</Type>)</Declaration>
|
||
"""
|
||
),
|
||
"""
|
||
```swift
|
||
func foo(_ bar: Baz)
|
||
```
|
||
|
||
"""
|
||
)
|
||
XCTAssertEqual(
|
||
try xmlDocumentationToMarkdown(
|
||
"""
|
||
<Declaration>func foo() -> <Type>Bar</Type></Declaration>
|
||
"""
|
||
),
|
||
"""
|
||
```swift
|
||
func foo() -> Bar
|
||
```
|
||
|
||
"""
|
||
)
|
||
XCTAssertEqual(
|
||
try xmlDocumentationToMarkdown(
|
||
"""
|
||
<Link href="https://example.com">My Link</Link>
|
||
"""
|
||
),
|
||
"""
|
||
[My Link](https://example.com)
|
||
"""
|
||
)
|
||
XCTAssertEqual(
|
||
try xmlDocumentationToMarkdown(
|
||
"""
|
||
<Link>My Invalid Link</Link>
|
||
"""
|
||
),
|
||
"""
|
||
My Invalid Link
|
||
"""
|
||
)
|
||
XCTAssertEqual(
|
||
try xmlDocumentationToMarkdown(
|
||
"""
|
||
<Declaration>func replacingOccurrences<Target, Replacement>(of target: Target, with replacement: Replacement, options: <Type usr="s:SS">String</Type>.<Type usr="s:SS10FoundationE14CompareOptionsa">CompareOptions</Type> = default, range searchRange: <Type usr="s:Sn">Range</Type><<Type usr="s:SS">String</Type>.<Type usr="s:SS5IndexV">Index</Type>>? = default) -> <Type usr="s:SS">String</Type> where Target : <Type usr="s:Sy">StringProtocol</Type>, Replacement : <Type usr="s:Sy">StringProtocol</Type></Declaration>
|
||
"""
|
||
),
|
||
"""
|
||
```swift
|
||
func replacingOccurrences<Target, Replacement>(of target: Target, with replacement: Replacement, options: String.CompareOptions = default, range searchRange: Range<String.Index>? = default) -> String where Target : StringProtocol, Replacement : StringProtocol
|
||
```
|
||
|
||
"""
|
||
)
|
||
}
|
||
|
||
func testXMLToMarkdownComment() {
|
||
XCTAssertEqual(
|
||
try xmlDocumentationToMarkdown(
|
||
"""
|
||
<Class><Declaration>var foo</Declaration></Class>
|
||
"""
|
||
),
|
||
"""
|
||
```swift
|
||
var foo
|
||
```
|
||
|
||
"""
|
||
)
|
||
|
||
XCTAssertEqual(
|
||
try xmlDocumentationToMarkdown(
|
||
"""
|
||
<Class><Name>foo</Name><Declaration>var foo</Declaration></Class>
|
||
"""
|
||
),
|
||
"""
|
||
```swift
|
||
var foo
|
||
```
|
||
|
||
"""
|
||
)
|
||
XCTAssertEqual(
|
||
try xmlDocumentationToMarkdown(
|
||
"""
|
||
<Class><USR>asdf</USR><Declaration>var foo</Declaration><Name>foo</Name></Class>
|
||
"""
|
||
),
|
||
"""
|
||
```swift
|
||
var foo
|
||
```
|
||
|
||
"""
|
||
)
|
||
|
||
XCTAssertEqual(
|
||
try xmlDocumentationToMarkdown(
|
||
"""
|
||
<Class><Abstract>FOO</Abstract></Class>
|
||
"""
|
||
),
|
||
"""
|
||
FOO
|
||
"""
|
||
)
|
||
XCTAssertEqual(
|
||
try xmlDocumentationToMarkdown(
|
||
"""
|
||
<Class><Abstract>FOO</Abstract><Declaration>var foo</Declaration></Class>
|
||
"""
|
||
),
|
||
"""
|
||
FOO
|
||
|
||
```swift
|
||
var foo
|
||
```
|
||
|
||
"""
|
||
)
|
||
|
||
XCTAssertEqual(
|
||
try xmlDocumentationToMarkdown(
|
||
"""
|
||
<Class><Abstract>FOO</Abstract><Discussion>BAR</Discussion></Class>
|
||
"""
|
||
),
|
||
"""
|
||
FOO
|
||
|
||
### Discussion
|
||
|
||
BAR
|
||
"""
|
||
)
|
||
|
||
XCTAssertEqual(
|
||
try xmlDocumentationToMarkdown(
|
||
"""
|
||
<Class><Para>A</Para><Para>B</Para><Para>C</Para></Class>
|
||
"""
|
||
),
|
||
"""
|
||
A
|
||
|
||
B
|
||
|
||
C
|
||
"""
|
||
)
|
||
|
||
XCTAssertEqual(
|
||
try xmlDocumentationToMarkdown(
|
||
"""
|
||
<CodeListing>a</CodeListing>
|
||
"""
|
||
),
|
||
"""
|
||
```
|
||
a
|
||
```
|
||
|
||
|
||
"""
|
||
)
|
||
|
||
XCTAssertEqual(
|
||
try xmlDocumentationToMarkdown(
|
||
"""
|
||
<CodeListing><zCodeLineNumbered>a</zCodeLineNumbered></CodeListing>
|
||
"""
|
||
),
|
||
"""
|
||
```
|
||
1.\ta
|
||
```
|
||
|
||
|
||
"""
|
||
)
|
||
XCTAssertEqual(
|
||
try xmlDocumentationToMarkdown(
|
||
"""
|
||
<CodeListing><zCodeLineNumbered>a</zCodeLineNumbered><zCodeLineNumbered>b</zCodeLineNumbered></CodeListing>
|
||
"""
|
||
),
|
||
"""
|
||
```
|
||
1.\ta
|
||
2.\tb
|
||
```
|
||
|
||
|
||
"""
|
||
)
|
||
XCTAssertEqual(
|
||
try xmlDocumentationToMarkdown(
|
||
"""
|
||
<Class><CodeListing><zCodeLineNumbered>a</zCodeLineNumbered><zCodeLineNumbered>b</zCodeLineNumbered></CodeListing><CodeListing><zCodeLineNumbered>c</zCodeLineNumbered><zCodeLineNumbered>d</zCodeLineNumbered></CodeListing></Class>
|
||
"""
|
||
),
|
||
"""
|
||
```
|
||
1.\ta
|
||
2.\tb
|
||
```
|
||
|
||
```
|
||
1.\tc
|
||
2.\td
|
||
```
|
||
|
||
|
||
"""
|
||
)
|
||
|
||
XCTAssertEqual(
|
||
try xmlDocumentationToMarkdown(
|
||
"""
|
||
<Para>a b c <codeVoice>d e f</codeVoice> g h i</Para>
|
||
"""
|
||
),
|
||
"""
|
||
a b c `d e f` g h i
|
||
"""
|
||
)
|
||
|
||
XCTAssertEqual(
|
||
try xmlDocumentationToMarkdown(
|
||
"""
|
||
<Para>a b c <emphasis>d e f</emphasis> g h i</Para>
|
||
"""
|
||
),
|
||
"""
|
||
a b c *d e f* g h i
|
||
"""
|
||
)
|
||
|
||
XCTAssertEqual(
|
||
try xmlDocumentationToMarkdown(
|
||
"""
|
||
<Para>a b c <bold>d e f</bold> g h i</Para>
|
||
"""
|
||
),
|
||
"""
|
||
a b c **d e f** g h i
|
||
"""
|
||
)
|
||
|
||
XCTAssertEqual(
|
||
try xmlDocumentationToMarkdown(
|
||
"""
|
||
<Para>a b c<h1>d e f</h1>g h i</Para>
|
||
"""
|
||
),
|
||
"""
|
||
a b c
|
||
|
||
# d e f
|
||
|
||
g h i
|
||
"""
|
||
)
|
||
|
||
XCTAssertEqual(
|
||
try xmlDocumentationToMarkdown(
|
||
"""
|
||
<Para>a b c<h3>d e f</h3>g h i</Para>
|
||
"""
|
||
),
|
||
"""
|
||
a b c
|
||
|
||
### d e f
|
||
|
||
g h i
|
||
"""
|
||
)
|
||
|
||
XCTAssertEqual(
|
||
try xmlDocumentationToMarkdown(
|
||
"<Class>" + "<Name>String</Name>" + "<USR>s:SS</USR>" + "<Declaration>struct String</Declaration>"
|
||
+ "<CommentParts>" + "<Abstract>" + "<Para>A Unicode s</Para>" + "</Abstract>" + "<Discussion>"
|
||
+ "<Para>A string is a series of characters, such as <codeVoice>"Swift"</codeVoice>, that forms a collection. "
|
||
+ "The <codeVoice>String</codeVoice> type bridges with the Objective-C class <codeVoice>NSString</codeVoice> and offers"
|
||
+ "</Para>" + "<Para>You can create new strings A <emphasis>string literal</emphasis> i" + "</Para>"
|
||
+ "<CodeListing language=\"swift\">"
|
||
+ "<zCodeLineNumbered><![CDATA[let greeting = \"Welcome!\"]]></zCodeLineNumbered>"
|
||
+ "<zCodeLineNumbered></zCodeLineNumbered>" + "</CodeListing>" + "<Para>...</Para>"
|
||
+ "<CodeListing language=\"swift\">"
|
||
+ "<zCodeLineNumbered><![CDATA[let greeting = \"Welcome!\"]]></zCodeLineNumbered>"
|
||
+ "<zCodeLineNumbered></zCodeLineNumbered>" + "</CodeListing>" + "</Discussion>" + "</CommentParts>"
|
||
+ "</Class>"
|
||
),
|
||
"""
|
||
```swift
|
||
struct String
|
||
```
|
||
A Unicode s
|
||
|
||
### Discussion
|
||
|
||
A string is a series of characters, such as `"Swift"`, that forms a collection. The `String` type bridges with the Objective-C class `NSString` and offers
|
||
|
||
You can create new strings A *string literal* i
|
||
|
||
```swift
|
||
1.\tlet greeting = "Welcome!"
|
||
2.\t
|
||
```
|
||
|
||
...
|
||
|
||
```swift
|
||
1.\tlet greeting = "Welcome!"
|
||
2.\t
|
||
```
|
||
|
||
|
||
"""
|
||
)
|
||
|
||
XCTAssertEqual(
|
||
try xmlDocumentationToMarkdown(
|
||
"<Function file=\"DocumentManager.swift\" line=\"92\" column=\"15\">" + "<CommentParts>"
|
||
+ "<Abstract><Para>Applies the given edits to the document.</Para></Abstract>" + "<Parameters>"
|
||
+ "<Parameter>" + "<Name>editCallback</Name>" + "<Direction isExplicit=\"0\">in</Direction>"
|
||
+ "<Discussion><Para>Optional closure to call for each edit.</Para></Discussion>" + "</Parameter>"
|
||
+ "<Parameter>" + "<Name>before</Name>" + "<Direction isExplicit=\"0\">in</Direction>"
|
||
+ "<Discussion><Para>The document contents <emphasis>before</emphasis> the edit is applied.</Para></Discussion>"
|
||
+ "</Parameter>" + "</Parameters>" + "</CommentParts>" + "</Function>"
|
||
),
|
||
"""
|
||
Applies the given edits to the document.
|
||
|
||
- Parameters:
|
||
- editCallback: Optional closure to call for each edit.
|
||
- before: The document contents *before* the edit is applied.
|
||
"""
|
||
)
|
||
|
||
XCTAssertEqual(
|
||
try xmlDocumentationToMarkdown(
|
||
"""
|
||
<ResultDiscussion><Para>The contents of the file after all the edits are applied.</Para></ResultDiscussion>
|
||
"""
|
||
),
|
||
"""
|
||
### Returns
|
||
|
||
The contents of the file after all the edits are applied.
|
||
"""
|
||
)
|
||
|
||
XCTAssertEqual(
|
||
try xmlDocumentationToMarkdown(
|
||
"""
|
||
<ThrowsDiscussion><Para>Error.missingDocument if the document is not open.</Para></ThrowsDiscussion>
|
||
"""
|
||
),
|
||
"""
|
||
### Throws
|
||
|
||
Error.missingDocument if the document is not open.
|
||
"""
|
||
)
|
||
|
||
XCTAssertEqual(
|
||
try xmlDocumentationToMarkdown(
|
||
"<Class>" + "<Name>S</Name>" + "<USR>s:1a1SV</USR>" + "<Declaration>struct S</Declaration>" + "<CommentParts>"
|
||
+ "<Discussion>" + #"<CodeListing language="swift">"# + "<zCodeLineNumbered>" + "<![CDATA[let S = 12456]]>"
|
||
+ "</zCodeLineNumbered>" + "<zCodeLineNumbered></zCodeLineNumbered>" + "</CodeListing>" + "<rawHTML>"
|
||
+ "<![CDATA[<h2>]]>" + "</rawHTML>Title<rawHTML>" + "<![CDATA[</h2>]]>" + "</rawHTML>"
|
||
+ "<Para>Details.</Para>" + "</Discussion>" + "</CommentParts>" + "</Class>"
|
||
),
|
||
"""
|
||
```swift
|
||
struct S
|
||
```
|
||
### Discussion
|
||
|
||
```swift
|
||
1.\tlet S = 12456
|
||
2.\t
|
||
```
|
||
|
||
<h2>Title</h2>
|
||
|
||
Details.
|
||
"""
|
||
)
|
||
}
|
||
|
||
func testSymbolInfo() async throws {
|
||
let testClient = try await TestSourceKitLSPClient()
|
||
let url = URL(fileURLWithPath: "/\(UUID())/a.swift")
|
||
let uri = DocumentURI(url)
|
||
|
||
testClient.openDocument(
|
||
"""
|
||
import Foundation
|
||
struct S {
|
||
func foo() {
|
||
var local = 1
|
||
}
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
|
||
do {
|
||
let resp = try await testClient.send(
|
||
SymbolInfoRequest(
|
||
textDocument: TextDocumentIdentifier(url),
|
||
position: Position(line: 0, utf16index: 7)
|
||
)
|
||
)
|
||
|
||
XCTAssertEqual(resp.count, 1)
|
||
if let sym = resp.first {
|
||
XCTAssertEqual(sym.name, "Foundation")
|
||
XCTAssertNil(sym.containerName)
|
||
XCTAssertEqual(sym.usr, nil)
|
||
XCTAssertEqual(sym.kind, .module)
|
||
XCTAssertEqual(sym.bestLocalDeclaration?.uri, nil)
|
||
XCTAssertEqual(sym.bestLocalDeclaration?.range.lowerBound.line, nil)
|
||
XCTAssertEqual(sym.bestLocalDeclaration?.range.lowerBound.utf16index, nil)
|
||
}
|
||
}
|
||
|
||
do {
|
||
let resp = try await testClient.send(
|
||
SymbolInfoRequest(
|
||
textDocument: TextDocumentIdentifier(url),
|
||
position: Position(line: 1, utf16index: 7)
|
||
)
|
||
)
|
||
|
||
XCTAssertEqual(resp.count, 1)
|
||
if let sym = resp.first {
|
||
XCTAssertEqual(sym.name, "S")
|
||
XCTAssertNil(sym.containerName)
|
||
XCTAssertEqual(sym.usr, "s:1a1SV")
|
||
XCTAssertEqual(sym.bestLocalDeclaration?.uri, uri)
|
||
XCTAssertEqual(sym.bestLocalDeclaration?.range.lowerBound.line, 1)
|
||
XCTAssertEqual(sym.bestLocalDeclaration?.range.lowerBound.utf16index, 7)
|
||
}
|
||
}
|
||
|
||
do {
|
||
let resp = try await testClient.send(
|
||
SymbolInfoRequest(
|
||
textDocument: TextDocumentIdentifier(url),
|
||
position: Position(line: 2, utf16index: 7)
|
||
)
|
||
)
|
||
|
||
XCTAssertEqual(resp.count, 1)
|
||
if let sym = resp.first {
|
||
XCTAssertEqual(sym.name, "foo()")
|
||
XCTAssertNil(sym.containerName)
|
||
XCTAssertEqual(sym.usr, "s:1a1SV3fooyyF")
|
||
XCTAssertEqual(sym.bestLocalDeclaration?.uri, uri)
|
||
XCTAssertEqual(sym.bestLocalDeclaration?.range.lowerBound.line, 2)
|
||
XCTAssertEqual(sym.bestLocalDeclaration?.range.lowerBound.utf16index, 7)
|
||
}
|
||
}
|
||
|
||
do {
|
||
let resp = try await testClient.send(
|
||
SymbolInfoRequest(
|
||
textDocument: TextDocumentIdentifier(url),
|
||
position: Position(line: 3, utf16index: 8)
|
||
)
|
||
)
|
||
|
||
XCTAssertEqual(resp.count, 1)
|
||
if let sym = resp.first {
|
||
XCTAssertEqual(sym.name, "local")
|
||
XCTAssertNil(sym.containerName)
|
||
XCTAssertEqual(sym.usr, "s:1a1SV3fooyyF5localL_Sivp")
|
||
XCTAssertEqual(sym.bestLocalDeclaration?.uri, uri)
|
||
XCTAssertEqual(sym.bestLocalDeclaration?.range.lowerBound.line, 3)
|
||
XCTAssertEqual(sym.bestLocalDeclaration?.range.lowerBound.utf16index, 8)
|
||
}
|
||
}
|
||
|
||
do {
|
||
let resp = try await testClient.send(
|
||
SymbolInfoRequest(
|
||
textDocument: TextDocumentIdentifier(url),
|
||
position: Position(line: 3, utf16index: 0)
|
||
)
|
||
)
|
||
|
||
XCTAssertEqual(resp.count, 0)
|
||
}
|
||
}
|
||
|
||
func testDocumentSymbolHighlight() async throws {
|
||
let testClient = try await TestSourceKitLSPClient()
|
||
let url = URL(fileURLWithPath: "/\(UUID())/a.swift")
|
||
let uri = DocumentURI(url)
|
||
|
||
testClient.openDocument(
|
||
"""
|
||
func test() {
|
||
let a = 1
|
||
let b = 2
|
||
let ccc = 3
|
||
_ = b
|
||
_ = ccc + ccc
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
|
||
do {
|
||
let resp = try await testClient.send(
|
||
DocumentHighlightRequest(
|
||
textDocument: TextDocumentIdentifier(url),
|
||
position: Position(line: 0, utf16index: 0)
|
||
)
|
||
)
|
||
XCTAssertEqual(resp?.count, 0)
|
||
}
|
||
|
||
do {
|
||
let resp = try await testClient.send(
|
||
DocumentHighlightRequest(
|
||
textDocument: TextDocumentIdentifier(url),
|
||
position: Position(line: 1, utf16index: 6)
|
||
)
|
||
)
|
||
XCTAssertEqual(resp?.count, 1)
|
||
if let highlight = resp?.first {
|
||
XCTAssertEqual(highlight.range.lowerBound.line, 1)
|
||
XCTAssertEqual(highlight.range.lowerBound.utf16index, 6)
|
||
XCTAssertEqual(highlight.range.upperBound.line, 1)
|
||
XCTAssertEqual(highlight.range.upperBound.utf16index, 7)
|
||
}
|
||
}
|
||
|
||
do {
|
||
let resp = try await testClient.send(
|
||
DocumentHighlightRequest(
|
||
textDocument: TextDocumentIdentifier(url),
|
||
position: Position(line: 2, utf16index: 6)
|
||
)
|
||
)
|
||
XCTAssertEqual(resp?.count, 2)
|
||
if let highlight = resp?.first {
|
||
XCTAssertEqual(highlight.range.lowerBound.line, 2)
|
||
XCTAssertEqual(highlight.range.lowerBound.utf16index, 6)
|
||
XCTAssertEqual(highlight.range.upperBound.line, 2)
|
||
XCTAssertEqual(highlight.range.upperBound.utf16index, 7)
|
||
}
|
||
if let highlight = resp?.dropFirst().first {
|
||
XCTAssertEqual(highlight.range.lowerBound.line, 4)
|
||
XCTAssertEqual(highlight.range.lowerBound.utf16index, 6)
|
||
XCTAssertEqual(highlight.range.upperBound.line, 4)
|
||
XCTAssertEqual(highlight.range.upperBound.utf16index, 7)
|
||
}
|
||
}
|
||
|
||
do {
|
||
let resp = try await testClient.send(
|
||
DocumentHighlightRequest(
|
||
textDocument: TextDocumentIdentifier(url),
|
||
position: Position(line: 3, utf16index: 6)
|
||
)
|
||
)
|
||
XCTAssertEqual(resp?.count, 3)
|
||
if let highlight = resp?.first {
|
||
XCTAssertEqual(highlight.range.lowerBound.line, 3)
|
||
XCTAssertEqual(highlight.range.lowerBound.utf16index, 6)
|
||
XCTAssertEqual(highlight.range.upperBound.line, 3)
|
||
XCTAssertEqual(highlight.range.upperBound.utf16index, 9)
|
||
}
|
||
if let highlight = resp?.dropFirst().first {
|
||
XCTAssertEqual(highlight.range.lowerBound.line, 5)
|
||
XCTAssertEqual(highlight.range.lowerBound.utf16index, 6)
|
||
XCTAssertEqual(highlight.range.upperBound.line, 5)
|
||
XCTAssertEqual(highlight.range.upperBound.utf16index, 9)
|
||
}
|
||
if let highlight = resp?.dropFirst(2).first {
|
||
XCTAssertEqual(highlight.range.lowerBound.line, 5)
|
||
XCTAssertEqual(highlight.range.lowerBound.utf16index, 12)
|
||
XCTAssertEqual(highlight.range.upperBound.line, 5)
|
||
XCTAssertEqual(highlight.range.upperBound.utf16index, 15)
|
||
}
|
||
}
|
||
}
|
||
|
||
func testIncrementalParse() async throws {
|
||
let testClient = try await TestSourceKitLSPClient()
|
||
let url = URL(fileURLWithPath: "/\(UUID())/a.swift")
|
||
let uri = DocumentURI(url)
|
||
|
||
let reusedNodeCallback = self.expectation(description: "reused node callback called")
|
||
let reusedNodes = ThreadSafeBox<[Syntax]>(initialValue: [])
|
||
let swiftLanguageService =
|
||
await testClient.server.languageService(for: uri, .swift, in: testClient.server.workspaceForDocument(uri: uri)!)
|
||
as! SwiftLanguageService
|
||
await swiftLanguageService.setReusedNodeCallback {
|
||
reusedNodes.value.append($0)
|
||
reusedNodeCallback.fulfill()
|
||
}
|
||
|
||
testClient.openDocument(
|
||
"""
|
||
func foo() {
|
||
}
|
||
class bar {
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
|
||
// Send a request that triggers a syntax tree to be built.
|
||
_ = try await testClient.send(FoldingRangeRequest(textDocument: .init(uri)))
|
||
|
||
testClient.send(
|
||
DidChangeTextDocumentNotification(
|
||
textDocument: .init(uri, version: 1),
|
||
contentChanges: [
|
||
.init(range: Range(Position(line: 2, utf16index: 7)), text: "a")
|
||
]
|
||
)
|
||
)
|
||
try await fulfillmentOfOrThrow(reusedNodeCallback)
|
||
|
||
XCTAssertEqual(reusedNodes.value.count, 1)
|
||
let firstNode = try XCTUnwrap(reusedNodes.value.first)
|
||
XCTAssertEqual(
|
||
firstNode.description,
|
||
"""
|
||
func foo() {
|
||
}
|
||
"""
|
||
)
|
||
XCTAssertEqual(firstNode.kind, .codeBlockItem)
|
||
}
|
||
|
||
func testDebouncePublishDiagnosticsNotification() async throws {
|
||
try SkipUnless.longTestsEnabled()
|
||
|
||
let options = SourceKitLSPOptions(swiftPublishDiagnosticsDebounceDuration: 1 /* second */)
|
||
let testClient = try await TestSourceKitLSPClient(options: options, usePullDiagnostics: false)
|
||
|
||
let uri = DocumentURI(URL(fileURLWithPath: "/\(UUID())/a.swift"))
|
||
testClient.openDocument("foo", uri: uri)
|
||
|
||
let edit = TextDocumentContentChangeEvent(
|
||
range: Position(line: 0, utf16index: 0)..<Position(line: 0, utf16index: 3),
|
||
text: "bar"
|
||
)
|
||
testClient.send(
|
||
DidChangeTextDocumentNotification(
|
||
textDocument: VersionedTextDocumentIdentifier(uri, version: 0),
|
||
contentChanges: [edit]
|
||
)
|
||
)
|
||
|
||
let diagnostic = try await testClient.nextDiagnosticsNotification()
|
||
let diag = try XCTUnwrap(diagnostic.diagnostics.first)
|
||
XCTAssertEqual(diag.message, "Cannot find 'bar' in scope")
|
||
|
||
// Ensure that we don't get a second `PublishDiagnosticsNotification`
|
||
await assertThrowsError(try await testClient.nextDiagnosticsNotification(timeout: .seconds(2)))
|
||
}
|
||
|
||
func testSourceKitdTimeout() async throws {
|
||
try SkipUnless.longTestsEnabled()
|
||
|
||
var options = try await SourceKitLSPOptions.testDefault()
|
||
// This is how long we wait until implicitly cancelling the first diagnostics request.
|
||
// It needs to be long enough so we can compute diagnostics for the second requests.
|
||
// 1s is not sufficient on Windows for that.
|
||
options.sourcekitdRequestTimeout = 3 /* seconds */
|
||
let testClient = try await TestSourceKitLSPClient(options: options)
|
||
let uri = DocumentURI(for: .swift)
|
||
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
struct A: ExpressibleByIntegerLiteral { init(integerLiteral value: Int) {} }
|
||
struct B: ExpressibleByIntegerLiteral { init(integerLiteral value: Int) {} }
|
||
struct C: ExpressibleByIntegerLiteral { init(integerLiteral value: Int) {} }
|
||
|
||
func + (lhs: A, rhs: B) -> A { fatalError() }
|
||
func + (lhs: B, rhs: C) -> A { fatalError() }
|
||
func + (lhs: C, rhs: A) -> A { fatalError() }
|
||
|
||
func + (lhs: B, rhs: A) -> B { fatalError() }
|
||
func + (lhs: C, rhs: B) -> B { fatalError() }
|
||
func + (lhs: A, rhs: C) -> B { fatalError() }
|
||
|
||
func + (lhs: C, rhs: B) -> C { fatalError() }
|
||
func + (lhs: B, rhs: C) -> C { fatalError() }
|
||
func + (lhs: A, rhs: A) -> C { fatalError() }
|
||
|
||
1️⃣class Foo {
|
||
func slow(x: Invalid1, y: Invalid2) {
|
||
let x: C = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10
|
||
}
|
||
}2️⃣
|
||
""",
|
||
uri: uri
|
||
)
|
||
|
||
let responseBeforeEdit = try await testClient.send(
|
||
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
|
||
)
|
||
/// The diagnostic request times out, which causes us to return empty diagnostics.
|
||
XCTAssertEqual(responseBeforeEdit.fullReport?.items, [])
|
||
|
||
// Now check that sourcekitd is not blocked.
|
||
// Replacing the file and sending another diagnostic request should return proper diagnostics.
|
||
testClient.send(
|
||
DidChangeTextDocumentNotification(
|
||
textDocument: VersionedTextDocumentIdentifier(uri, version: 2),
|
||
contentChanges: [
|
||
TextDocumentContentChangeEvent(range: positions["1️⃣"]..<positions["2️⃣"], text: "let x: String = 1")
|
||
]
|
||
)
|
||
)
|
||
let responseAfterEdit = try await testClient.send(
|
||
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
|
||
)
|
||
XCTAssertEqual(
|
||
responseAfterEdit.fullReport?.items.map(\.message),
|
||
["Cannot convert value of type 'Int' to specified type 'String'"]
|
||
)
|
||
}
|
||
}
|