//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 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 CompletionScoring
import Csourcekitd
import LanguageServerProtocol
import SKTestSupport
import SourceKitD
import SwiftExtensions
import ToolchainRegistry
import XCTest
final class SwiftSourceKitPluginTests: XCTestCase {
/// Returns a path to a file name that is unique to this test execution.
///
/// The file does not actually exist on disk.
private func scratchFilePath(testName: String = #function, fileName: String = "a.swift") -> String {
#if os(Windows)
return "C:\\\(testScratchName(testName: testName))\\\(fileName)"
#else
return "/\(testScratchName(testName: testName))/\(fileName)"
#endif
}
func getSourceKitD() async throws -> SourceKitD {
guard let sourcekitd = await ToolchainRegistry.forTesting.default?.sourcekitd else {
struct NoSourceKitdFound: Error, CustomStringConvertible {
var description: String = "Could not find SourceKitD"
}
throw NoSourceKitdFound()
}
return try await SourceKitD.getOrCreate(
dylibPath: sourcekitd,
pluginPaths: try sourceKitPluginPaths
)
}
func testBasicCompletion() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath()
let positions = try await sourcekitd.openDocument(
path,
contents: """
struct S {
func test() {
self.1️⃣ 2️⃣
}
}
""",
compilerArguments: [path]
)
await assertThrowsError(
try await sourcekitd.completeUpdate(path: path, position: positions["1️⃣"], filter: ""),
expectedMessage: #/no matching session/#
)
await assertThrowsError(
try await sourcekitd.completeClose(path: path, position: positions["1️⃣"]),
expectedMessage: #/no matching session/#
)
func checkTestMethod(result: CompletionResultSet, file: StaticString = #filePath, line: UInt = #line) {
guard let test = result.items.first(where: { $0.name == "test()" }) else {
XCTFail("did not find test(); got \(result.items)", file: file, line: line)
return
}
XCTAssertEqual(test.kind, sourcekitd.values.declMethodInstance, file: file, line: line)
XCTAssertEqual(test.description, "test()", file: file, line: line)
XCTAssertEqual(test.sourcetext, "test()", file: file, line: line)
XCTAssertEqual(test.typename, "Void", file: file, line: line)
XCTAssertEqual(test.priorityBucket, 9, file: file, line: line)
XCTAssertFalse(test.semanticScore.isNaN, file: file, line: line)
XCTAssertEqual(test.isSystem, false, file: file, line: line)
XCTAssertEqual(test.numBytesToErase, 0, file: file, line: line)
XCTAssertEqual(test.hasDiagnostic, false, file: file, line: line)
}
func checkTestMethodAnnotated(result: CompletionResultSet, file: StaticString = #filePath, line: UInt = #line) {
guard let test = result.items.first(where: { $0.name == "test()" }) else {
XCTFail("did not find test(); got \(result.items)", file: file, line: line)
return
}
XCTAssertEqual(test.kind, sourcekitd.values.declMethodInstance, file: file, line: line)
XCTAssertEqual(test.description, "test()", file: file, line: line)
XCTAssertEqual(test.sourcetext, "test()", file: file, line: line)
XCTAssertEqual(test.typename, "Void", file: file, line: line)
XCTAssertEqual(test.priorityBucket, 9, file: file, line: line)
XCTAssertFalse(test.semanticScore.isNaN, file: file, line: line)
XCTAssertEqual(test.isSystem, false, file: file, line: line)
XCTAssertEqual(test.numBytesToErase, 0, file: file, line: line)
XCTAssertEqual(test.hasDiagnostic, false, file: file, line: line)
}
var unfilteredResultCount: Int? = nil
let result1 = try await sourcekitd.completeOpen(path: path, position: positions["1️⃣"], filter: "")
XCTAssertEqual(result1.items.count, result1.unfilteredResultCount)
checkTestMethod(result: result1)
unfilteredResultCount = result1.unfilteredResultCount
let result2 = try await sourcekitd.completeUpdate(path: path, position: positions["1️⃣"], filter: "")
XCTAssertEqual(result2.items.count, result2.unfilteredResultCount)
XCTAssertEqual(result2.unfilteredResultCount, unfilteredResultCount)
checkTestMethod(result: result2)
let result3 = try await sourcekitd.completeUpdate(path: path, position: positions["1️⃣"], filter: "test")
XCTAssertEqual(result3.unfilteredResultCount, unfilteredResultCount)
XCTAssertEqual(result3.items.count, 1)
checkTestMethod(result: result3)
let result4 = try await sourcekitd.completeUpdate(path: path, position: positions["1️⃣"], filter: "testify")
XCTAssertEqual(result4.unfilteredResultCount, unfilteredResultCount)
XCTAssertEqual(result4.items.count, 0)
// Update on different location
await assertThrowsError(
try await sourcekitd.completeUpdate(path: path, position: positions["2️⃣"], filter: ""),
expectedMessage: #/no matching session/#
)
await assertThrowsError(
try await sourcekitd.completeClose(path: path, position: positions["2️⃣"]),
expectedMessage: #/no matching session/#
)
// Update on different location
await assertThrowsError(
try await sourcekitd.completeUpdate(path: "/other.swift", position: positions["1️⃣"], filter: ""),
expectedMessage: #/no matching session/#
)
// Annotated
let result5 = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: "",
flags: [.annotate]
)
XCTAssertEqual(result5.items.count, result5.unfilteredResultCount)
checkTestMethodAnnotated(result: result5)
let result6 = try await sourcekitd.completeUpdate(
path: path,
position: positions["1️⃣"],
filter: "test",
flags: [.annotate]
)
XCTAssertEqual(result6.items.count, 1)
checkTestMethodAnnotated(result: result6)
try await sourcekitd.completeClose(path: path, position: positions["1️⃣"])
await assertThrowsError(
try await sourcekitd.completeUpdate(path: path, position: positions["1️⃣"], filter: ""),
expectedMessage: #/no matching session/#
)
}
func testEmptyName() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath()
let positions = try await sourcekitd.openDocument(
path,
contents: """
func test1(closure: () -> Void) {
closure(1️⃣
}
func noArg() -> String {}
func noArg() -> Int {}
func test2() {
noArg(2️⃣
}
""",
compilerArguments: [path]
)
let result1 = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: "",
flags: [.annotate]
)
XCTAssertEqual(result1.items.count, 1)
XCTAssertEqual(result1.items[0].name, "")
let result2 = try await sourcekitd.completeOpen(
path: path,
position: positions["2️⃣"],
filter: "",
flags: [.annotate],
maxResults: 1
)
XCTAssertEqual(result2.items.count, 1)
XCTAssertEqual(result2.items[0].name, "")
let doc = try await sourcekitd.completeDocumentation(id: result2.items[0].id)
XCTAssertEqual(doc.docBrief, nil)
}
func testMultipleFiles() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let pathA = scratchFilePath(fileName: "a.swift")
let pathB = scratchFilePath(fileName: "b.swift")
let positionsA = try await sourcekitd.openDocument(
pathA,
contents: """
struct A {
func aaa(b: B) {
b.1️⃣
}
}
""",
compilerArguments: [pathA, pathB]
)
let positionsB = try await sourcekitd.openDocument(
pathB,
contents: """
struct B {
func bbb(a: A) {
a.2️⃣
}
}
""",
compilerArguments: [pathA, pathB]
)
func checkResult(name: String, result: CompletionResultSet, file: StaticString = #filePath, line: UInt = #line) {
guard let test = result.items.first(where: { $0.name == name }) else {
XCTFail("did not find \(name); got \(result.items)", file: file, line: line)
return
}
XCTAssertEqual(test.kind, sourcekitd.values.declMethodInstance, file: file, line: line)
}
let result1 = try await sourcekitd.completeOpen(
path: pathA,
position: positionsA["1️⃣"],
filter: ""
)
checkResult(name: "bbb(a:)", result: result1)
let result2 = try await sourcekitd.completeUpdate(
path: pathA,
position: positionsA["1️⃣"],
filter: "b"
)
checkResult(name: "bbb(a:)", result: result2)
let result3 = try await sourcekitd.completeOpen(
path: pathB,
position: positionsB["2️⃣"],
filter: ""
)
checkResult(name: "aaa(b:)", result: result3)
let result4 = try await sourcekitd.completeUpdate(
path: pathB,
position: positionsB["2️⃣"],
filter: "a"
)
checkResult(name: "aaa(b:)", result: result4)
}
func testCancellation() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath()
let positions = try await sourcekitd.openDocument(
path,
contents: """
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() }
class Foo {
func slow(x: Invalid1, y: Invalid2) {
let x: C = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 1️⃣
}
struct Foo {
let fooMember: String
}
func fast(a: Foo) {
a.2️⃣
}
}
""",
compilerArguments: [path]
)
let slowCompletionRequestSent = self.expectation(description: "slow completion result sent")
let slowCompletionResultReceived = self.expectation(description: "slow completion")
try await sourcekitd.withRequestHandlingHook {
let slowCompletionTask = Task {
await assertThrowsError(try await sourcekitd.completeOpen(path: path, position: positions["1️⃣"], filter: "")) {
XCTAssert($0 is CancellationError, "Expected completion to be cancelled, failed with \($0)")
}
slowCompletionResultReceived.fulfill()
}
// Wait for the slow completion request to actually be sent to sourcekitd. Otherwise, we might hit a cancellation
// check somewhere during request sending and we aren't actually sending the completion request to sourcekitd.
try await fulfillmentOfOrThrow(slowCompletionRequestSent)
slowCompletionTask.cancel()
try await fulfillmentOfOrThrow(slowCompletionResultReceived, timeout: 30)
} hook: { request in
// Check that we aren't matching against a request sent by something else that has handle to the same sourcekitd.
assertContains(request.description.replacing(#"\\"#, with: #"\"#), path)
slowCompletionRequestSent.fulfill()
}
let fastCompletionStarted = Date()
let result = try await sourcekitd.completeOpen(
path: path,
position: positions["2️⃣"],
filter: ""
)
XCTAssert(result.items.count > 0)
XCTAssertLessThan(Date().timeIntervalSince(fastCompletionStarted), 30)
}
func testEdits() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath()
let positions = try await sourcekitd.openDocument(
path,
contents: """
struct S {
func test() {
let solf = 1
solo.1️⃣
}
func magic_method_of_greatness() {}
}
""",
compilerArguments: [path]
)
let result1 = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: "magic_method_of_greatness",
flags: []
)
XCTAssertEqual(result1.unfilteredResultCount, 0)
XCTAssertEqual(result1.items.count, 0)
let sOffset = """
struct S {
func test() {
let solo = 1
s
""".count - 1
try await sourcekitd.editDocument(path, fromOffset: sOffset + 1, length: 1, newContents: "e")
try await sourcekitd.editDocument(path, fromOffset: sOffset + 3, length: 1, newContents: "f")
let result2 = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: "magic_method_of_greatness",
flags: []
)
XCTAssertGreaterThan(result2.unfilteredResultCount, 1)
XCTAssertEqual(result2.items.count, 1)
try await sourcekitd.editDocument(path, fromOffset: sOffset, length: 3, newContents: "")
try await sourcekitd.editDocument(path, fromOffset: sOffset, length: 0, newContents: "sel")
let result3 = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: "magic_method_of_greatness",
flags: []
)
XCTAssertGreaterThan(result3.unfilteredResultCount, 1)
XCTAssertEqual(result3.items.count, 1)
}
func testDocumentation() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath()
let positions = try await sourcekitd.openDocument(
path,
contents: """
protocol P {
/// Protocol P foo1
func foo1()
}
struct S: P {
func foo1() {}
/// Struct S foo2
func foo2() {}
func foo3() {}
func test() {
self.1️⃣
}
}
""",
compilerArguments: [path]
)
let result = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: "foo"
)
XCTAssertGreaterThan(result.unfilteredResultCount, 3)
let sym1 = try unwrap(result.items.first(where: { $0.name == "foo1()" }), "did not find foo1; got \(result.items)")
let sym2 = try unwrap(result.items.first(where: { $0.name == "foo2()" }), "did not find foo2; got \(result.items)")
let sym3 = try unwrap(result.items.first(where: { $0.name == "foo3()" }), "did not find foo3; got \(result.items)")
let sym1Doc = try await sourcekitd.completeDocumentation(id: sym1.id)
XCTAssertEqual(sym1Doc.docBrief, "Protocol P foo1")
XCTAssertEqual(sym1Doc.associatedUSRs, ["s:1a1SV4foo1yyF", "s:1a1PP4foo1yyF"])
let sym2Doc = try await sourcekitd.completeDocumentation(id: sym2.id)
XCTAssertEqual(sym2Doc.docBrief, "Struct S foo2")
XCTAssertEqual(sym2Doc.associatedUSRs, ["s:1a1SV4foo2yyF"])
let sym3Doc = try await sourcekitd.completeDocumentation(id: sym3.id)
XCTAssertNil(sym3Doc.docBrief)
XCTAssertEqual(sym3Doc.associatedUSRs, ["s:1a1SV4foo3yyF"])
}
func testNumBytesToErase() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath()
let positions = try await sourcekitd.openDocument(
path,
contents: """
struct S { var myVar: Int }
func test(s: S?) {
s.1️⃣
}
""",
compilerArguments: [path]
)
let result = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: ""
)
XCTAssertEqual(result.items.count, result.unfilteredResultCount)
let myVar = try unwrap(result.items.first(where: { $0.name == "myVar" }), "did not find myVar; got \(result.items)")
XCTAssertEqual(myVar.isSystem, false)
XCTAssertEqual(myVar.numBytesToErase, 1)
let unwrapped = try unwrap(
result.items.first(where: { $0.name == "unsafelyUnwrapped" }),
"did not find myVar; got \(result.items)"
)
XCTAssertEqual(unwrapped.isSystem, true)
XCTAssertEqual(unwrapped.numBytesToErase, 0)
}
func testObjectLiterals() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath()
let positions = try await sourcekitd.openDocument(
path,
contents: """
func test() {
}1️⃣
""",
compilerArguments: [path]
)
let result = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: ""
)
XCTAssertFalse(
result.items.contains(where: {
$0.description.hasPrefix("#colorLiteral") || $0.description.hasPrefix("#imageLiteral")
})
)
}
func testAddInitsToTopLevel() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath()
let positions = try await sourcekitd.openDocument(
path,
contents: """
func test() {
1️⃣}
struct MyStruct {
init(arg1: Int) {}
init(arg2: String) {}
}
""",
compilerArguments: [path]
)
// With 'addInitsToTopLevel'
let result1 = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: "MyStr",
flags: [.addInitsToTopLevel]
)
XCTAssert(result1.items.filter({ $0.description.hasPrefix("MyStruct") }).count == 3)
let typeResult = try unwrap(
result1.items.first { $0.description == "MyStruct" && $0.kind == sourcekitd.values.declStruct }
)
XCTAssertNotNil(typeResult.groupID)
XCTAssert(
result1.items.contains(where: {
$0.description.hasPrefix("MyStruct(arg1:") && $0.kind == sourcekitd.values.declConstructor
&& $0.groupID == typeResult.groupID
})
)
XCTAssert(
result1.items.contains(where: {
$0.description.hasPrefix("MyStruct(arg2:") && $0.kind == sourcekitd.values.declConstructor
&& $0.groupID == typeResult.groupID
})
)
XCTAssertLessThan(
try unwrap(result1.items.firstIndex(where: { $0.description == "MyStruct" })),
try unwrap(result1.items.firstIndex(where: { $0.description.hasPrefix("MyStruct(") })),
"Type names must precede the initializer calls"
)
// Without 'addInitsToTopLevel'
let result2 = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: "MyStr",
flags: []
)
XCTAssert(result2.items.filter({ $0.description.hasPrefix("MyStruct") }).count == 1)
XCTAssertFalse(result2.items.contains(where: { $0.description.hasPrefix("MyStruct(") }))
}
func testMembersGroupID() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath()
let positions = try await sourcekitd.openDocument(
path,
contents: """
struct Animal {
var name: String
var species: String
func name(changedTo: String) { }
func name(updatedTo: String) { }
func otherFunction() { }
}
func test() {
let animal = Animal(name: "", species: "")
animal.1️⃣
}
""",
compilerArguments: [path]
)
let result = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: ""
)
guard result.items.count == 6 else {
XCTFail("Expected 6 completion results; received \(result)")
return
}
// Properties don't have a groupID.
XCTAssertEqual(result.items[0].name, "name")
XCTAssertNil(result.items[0].groupID)
XCTAssertEqual(result.items[1].name, "species")
XCTAssertNil(result.items[1].groupID)
XCTAssertEqual(result.items[2].name, "name(changedTo:)")
XCTAssertEqual(result.items[3].name, "name(updatedTo:)")
XCTAssertEqual(result.items[2].groupID, result.items[3].groupID)
XCTAssertEqual(result.items[4].name, "otherFunction()")
XCTAssertNotNil(result.items[4].groupID)
}
func testAddCallWithNoDefaultArgs() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath()
let positions = try await sourcekitd.openDocument(
path,
contents: """
struct Defaults {
func noDefault(a: Int) { }
func singleDefault(a: Int = 0) { }
}
func defaults(def: Defaults) {
def.1️⃣
}
""",
compilerArguments: [path]
)
// With 'addCallWithNoDefaultArgs'
let result1 = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: "",
flags: [.addCallWithNoDefaultArgs]
)
guard result1.items.count == 4 else {
XCTFail("Expected 4 results; received \(result1)")
return
}
XCTAssertEqual(result1.items[0].description, "noDefault(a: Int)")
XCTAssertEqual(result1.items[1].description, "singleDefault()")
XCTAssertEqual(result1.items[2].description, "singleDefault(a: Int)")
XCTAssertEqual(result1.items[3].description, "self")
// Without 'addCallWithNoDefaultArgs'
let result2 = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: ""
)
guard result2.items.count == 3 else {
XCTFail("Expected 3 results; received \(result2)")
return
}
XCTAssertEqual(result2.items[0].description, "noDefault(a: Int)")
XCTAssertEqual(result2.items[1].description, "singleDefault(a: Int)")
XCTAssertEqual(result2.items[2].description, "self")
}
func testTextMatchScore() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath()
let positions = try await sourcekitd.openDocument(
path,
contents: """
struct S {
func goodMatchOne() {}
func goodMatchNotOneButTwo() {}
func test() {
self.1️⃣
}
}
""",
compilerArguments: [path]
)
let result1 = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: "gmo"
)
XCTAssertGreaterThan(result1.unfilteredResultCount, result1.items.count)
guard result1.items.count >= 2 else {
XCTFail("Expected at least 2 results; received \(result1)")
return
}
XCTAssertEqual(result1.items[0].description, "goodMatchOne()")
XCTAssertEqual(result1.items[1].description, "goodMatchNotOneButTwo()")
XCTAssertGreaterThan(result1.items[0].textMatchScore, result1.items[1].textMatchScore)
let result1Score = result1.items[0].textMatchScore
let result2 = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: "gmo",
useXPC: true
)
guard result2.items.count >= 2 else {
XCTFail("Expected at least 2 results; received \(result2)")
return
}
XCTAssertEqual(result2.items[0].description, "goodMatchOne()")
XCTAssertEqual(result2.items[1].description, "goodMatchNotOneButTwo()")
XCTAssertGreaterThan(result2.items[0].textMatchScore, result2.items[1].textMatchScore)
XCTAssertEqual(result2.items[0].textMatchScore, result1Score)
}
func testSemanticScore() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath()
let positions = try await sourcekitd.openDocument(
path,
contents: """
struct S {
func goodMatchAsync() async {}
@available(*, deprecated)
func goodMatchDeprecated() {}
func goodMatchType() {}
func test() {
let goodMatchLocal = 1
1️⃣
}
}
""",
compilerArguments: [path]
)
let result = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: "goodMatch"
)
guard result.items.count >= 4 else {
XCTFail("Expected at least 4 results; received \(result)")
return
}
XCTAssertEqual(result.items[0].description, "goodMatchLocal")
XCTAssertEqual(result.items[1].description, "goodMatchAsync() async")
XCTAssertEqual(result.items[2].description, "goodMatchType()")
XCTAssertEqual(result.items[3].description, "goodMatchDeprecated()")
XCTAssertGreaterThan(result.items[0].semanticScore, result.items[1].semanticScore)
// Note: async and deprecated get the same penalty currently, but we don't want to be too specific in this test.
XCTAssertEqual(result.items[1].semanticScore, result.items[2].semanticScore)
XCTAssertGreaterThan(result.items[1].semanticScore, result.items[3].semanticScore)
XCTAssertFalse(result.items[1].hasDiagnostic)
XCTAssertFalse(result.items[2].hasDiagnostic)
XCTAssertTrue(result.items[3].hasDiagnostic)
}
func testSemanticScoreInit() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath()
let positions = try await sourcekitd.openDocument(
path,
contents: """
enum E {
case good
init(_ param: Int, param2: String) { self = .good }
func test() {
let _: E = .1️⃣
}
func test2() {
let local = 1
E(2️⃣)
}
}
""",
compilerArguments: [path]
)
let result1 = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: ""
)
guard result1.items.count >= 2 else {
XCTFail("Expected at least 2 results; received \(result1)")
return
}
XCTAssertEqual(result1.items[0].description, "good")
XCTAssertEqual(result1.items[1].description, "init(param: Int, param2: String)")
XCTAssertGreaterThan(result1.items[0].semanticScore, result1.items[1].semanticScore)
let result2 = try await sourcekitd.completeOpen(
path: path,
position: positions["2️⃣"],
filter: ""
)
guard result2.items.count >= 2 else {
XCTFail("Expected at least 2 results; received \(result2)")
return
}
XCTAssertEqual(result2.items[0].description, "(param: Int, param2: String)")
XCTAssertEqual(result2.items[1].description, "local")
XCTAssertGreaterThan(result2.items[0].semanticScore, result2.items[1].semanticScore)
}
func testSemanticScoreComponents() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath()
let positions = try await sourcekitd.openDocument(
path,
contents: """
struct Animal {
var name = "Test"
func breed() { }
}
let animal = Animal()
animal.1️⃣
""",
compilerArguments: [path]
)
let result = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: "",
flags: [.includeSemanticComponents]
)
XCTAssertEqual(result.items.count, 3)
for item in result.items {
let data = Data(base64Encoded: try unwrap(item.semanticScoreComponents))!
let bytes = [UInt8](data)
XCTAssertFalse(bytes.isEmpty)
let classification = try SemanticClassification(byteRepresentation: bytes)
XCTAssertEqual(classification.score, item.semanticScore)
}
}
func testMemberAccessTypes() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath(fileName: "AnimalKit.swift")
let positions = try await sourcekitd.openDocument(
path,
contents: """
class Animal { }
class Dog: Animal {
var name = "Test"
func breed() { }
}
let dog = Dog()
dog1️⃣.
""",
compilerArguments: [path]
)
let result1 = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: ""
)
XCTAssertEqual(result1.memberAccessTypes, ["AnimalKit.Dog", "AnimalKit.Animal"])
guard result1.items.count == 5 else {
XCTFail("Expected 5 result. Received \(result1)")
return
}
XCTAssertEqual(result1.items[0].module, "AnimalKit")
XCTAssertEqual(result1.items[0].name, "name")
XCTAssertEqual(result1.items[1].module, "AnimalKit")
XCTAssertEqual(result1.items[1].name, "breed()")
let result2 = try await sourcekitd.completeUpdate(
path: path,
position: positions["1️⃣"],
filter: "name"
)
XCTAssertEqual(result2.memberAccessTypes, ["AnimalKit.Dog", "AnimalKit.Animal"])
guard result2.items.count == 1 else {
XCTFail("Expected 1 result. Received \(result2)")
return
}
XCTAssertEqual(result2.items[0].module, "AnimalKit")
XCTAssertEqual(result2.items[0].name, "name")
}
func testTypeModule() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath(fileName: "AnimalKit.swift")
let positions = try await sourcekitd.openDocument(
path,
contents: """
class Animal { }
class Dog: Animal {
var name = "Test"
func breed() { }
}
AnimalKit1️⃣.
""",
compilerArguments: [path]
)
let result = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: ""
)
XCTAssertEqual(result.memberAccessTypes, [])
XCTAssertEqual(result.items.count, 2)
// Note: the order of `Animal` and `Dog` isn't stable.
for item in result.items {
XCTAssertEqual(item.module, "AnimalKit")
}
}
func testKeyword() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath()
let positions = try await sourcekitd.openDocument(
path,
contents: """
1️⃣
""",
compilerArguments: [path]
)
let result = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: "extensio"
)
XCTAssertEqual(result.memberAccessTypes, [])
guard result.items.count == 1 else {
XCTFail("Expected 1 result. Received \(result)")
return
}
XCTAssertEqual(result.items[0].name, "extension")
XCTAssertNil(result.items[0].module)
}
func testSemanticScoreComponentsAsExtraUpdate() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath()
let positions = try await sourcekitd.openDocument(
path,
contents: """
struct S {
func test() {
self.1️⃣
}
}
""",
compilerArguments: [path]
)
// Open without `includeSemanticComponents`.
let result1 = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: ""
)
XCTAssertEqual(result1.items.count, 2)
for item in result1.items {
XCTAssertNil(item.semanticScoreComponents)
}
// Update without `includeSemanticComponents`.
let result2 = try await sourcekitd.completeUpdate(
path: path,
position: positions["1️⃣"],
filter: "t"
)
XCTAssertEqual(result2.items.count, 1)
for item in result2.items {
XCTAssertNil(item.semanticScoreComponents)
}
// Now, do the same update _with_ `includeSemanticComponents`.
let result3 = try await sourcekitd.completeUpdate(
path: path,
position: positions["1️⃣"],
filter: "t",
flags: [.includeSemanticComponents]
)
XCTAssertEqual(result3.items.count, 1)
for item in result3.items {
// Assert we get `semanticScoreComponents`,
// when `update` is called with different options than `open`.
XCTAssertNotNil(item.semanticScoreComponents)
}
// Same update _without_ `includeSemanticComponents`.
let result4 = try await sourcekitd.completeUpdate(
path: path,
position: positions["1️⃣"],
filter: "t"
)
XCTAssertEqual(result4.items.count, 1)
for item in result4.items {
// Response no longer contains the `semanticScoreComponents`.
XCTAssertNil(item.semanticScoreComponents)
}
}
// rdar://104381080 (NSImage(imageLiteralResourceName:) was my top completion — this seems odd)
func testPopularityForTypeFromSubmodule() async throws {
#if !os(macOS)
try XCTSkipIf(true, "AppKit is only defined on macOS")
#endif
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath()
let positions = try await sourcekitd.openDocument(
path,
contents: """
import AppKit
func test() {
1️⃣
}
""",
compilerArguments: [path]
)
let popularityIndex = """
{
"AppKit": {
"scores": [
0.6
],
"values": [
"NSImage"
]
}
}
"""
try await withTestScratchDir { scratchDir in
let popularityIndexPath = scratchDir.appending(component: "popularityIndex.json")
try popularityIndex.write(to: popularityIndexPath, atomically: true, encoding: .utf8)
try await sourcekitd.setPopularityIndex(
scopedPopularityDataPath: try popularityIndexPath.filePath,
popularModules: [],
notoriousModules: []
)
let result = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: "",
flags: [.addInitsToTopLevel]
)
// `NSImage` is defined in `AppKit.NSImage` (a submodule).
// The popularity index is keyed only on the base module (e.g. `AppKit`).
// This asserts we correctly see `NSImage` as popular.
XCTAssertEqual(result.items.first?.description, "NSImage")
}
}
func testPopularity() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath()
let positions = try await sourcekitd.openDocument(
path,
contents: """
struct S {
func test() {
self.1️⃣
}
let popular: Int = 0
let other1: Int = 0
let unpopular: Int = 0
let other2: Int = 0
let recent1: Int = 0
let recent2: Int = 0
let other3: Int = 0
}
""",
compilerArguments: [path]
)
// Reset the scoped popularity data path if it was set by previous requests
try await sourcekitd.setPopularityIndex(
scopedPopularityDataPath: "/invalid",
popularModules: [],
notoriousModules: []
)
try await sourcekitd.setPopularAPI(popular: ["popular"], unpopular: ["unpopular"])
let result1 = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: "",
recentCompletions: ["recent1"]
)
guard result1.items.count >= 6 else {
XCTFail("Expected at least 6 results. Received \(result1)")
return
}
XCTAssertEqual(result1.items[0].description, "popular")
XCTAssertEqual(result1.items[1].description, "recent1")
XCTAssertEqual(result1.items[2].description, "other1")
XCTAssertEqual(result1.items.last?.description, "unpopular")
let result2 = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: "",
recentCompletions: ["recent2", "recent1"]
)
guard result2.items.count >= 6 else {
XCTFail("Expected at least 6 results. Received \(result2)")
return
}
XCTAssertEqual(result2.items[0].description, "popular")
XCTAssertEqual(result2.items[1].description, "recent2")
XCTAssertEqual(result2.items[2].description, "recent1")
XCTAssertEqual(result2.items[3].description, "other1")
XCTAssertEqual(result2.items.last?.description, "unpopular")
try await sourcekitd.setPopularAPI(popular: [], unpopular: [])
let result3 = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: "",
recentCompletions: ["recent2", "recent1"]
)
guard result3.items.count >= 6 else {
XCTFail("Expected at least 6 results. Received \(result3)")
return
}
XCTAssertEqual(result3.items[0].description, "recent2")
XCTAssertEqual(result3.items[1].description, "recent1")
// Results 2 - 6 share the same score
XCTAssertEqual(result3.items[2].description, "popular")
XCTAssertEqual(result3.items[3].description, "other1")
XCTAssertEqual(result3.items[4].description, "unpopular")
XCTAssertEqual(result3.items[5].description, "other2")
XCTAssertEqual(result3.items[6].description, "other3")
XCTAssertEqual(result3.items[2].semanticScore, result3.items[3].semanticScore)
XCTAssertEqual(result3.items[2].semanticScore, result3.items[4].semanticScore)
XCTAssertEqual(result3.items[2].semanticScore, result3.items[5].semanticScore)
XCTAssertEqual(result3.items[2].semanticScore, result3.items[6].semanticScore)
}
func testScopedPopularity() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath()
let positions = try await sourcekitd.openDocument(
path,
contents: """
struct Strinz: Encodable {
var aProp: String = ""
var value: Int = 12
}
func test(arg: Strinz) {
arg.1️⃣
}
func testGlobal() {
2️⃣
}
""",
compilerArguments: [path, "-module-name", "MyMod"]
)
let popularityIndex = """
{
"Swift.Encodable": {
"values": [
"encode"
],
"scores": [
1.0
]
}
}
"""
try await withTestScratchDir { scratchDir in
let popularityIndexPath = scratchDir.appending(component: "popularityIndex.json")
try popularityIndex.write(to: popularityIndexPath, atomically: true, encoding: .utf8)
let result1 = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: ""
)
let scoreWithoutPopularity = try XCTUnwrap(result1.items.first(where: { $0.name == "encode(to:)" })).semanticScore
try await sourcekitd.setPopularityIndex(
scopedPopularityDataPath: try popularityIndexPath.filePath,
popularModules: [],
notoriousModules: ["MyMod"]
)
let result2 = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: ""
)
let scoreWithPopularity = try XCTUnwrap(result2.items.first(where: { $0.name == "encode(to:)" })).semanticScore
XCTAssert(scoreWithoutPopularity < scoreWithPopularity)
// Ensure 'notoriousModules' lowers the score.
let result3 = try await sourcekitd.completeOpen(
path: path,
position: positions["2️⃣"],
filter: "Strin",
recentCompletions: []
)
let string = try XCTUnwrap(result3.items.first(where: { $0.name == "String" }))
let strinz = try XCTUnwrap(result3.items.first(where: { $0.name == "Strinz" }))
XCTAssert(string.textMatchScore == strinz.textMatchScore)
XCTAssert(string.semanticScore > strinz.semanticScore)
}
}
func testModulePopularity() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath()
let positions = try await sourcekitd.openDocument(
path,
contents: """
struct S {
func test() {
self.1️⃣
}
let foo: Int = 0
}
""",
compilerArguments: [path]
)
try await sourcekitd.setPopularityTable(
PopularityTable(moduleSymbolReferenceTables: [], recentCompletions: [], popularModules: [], notoriousModules: [])
)
let result1 = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: "",
recentCompletions: []
)
let noPopularModulesScore = try unwrap(result1.items.first(where: { $0.description == "foo" })?.semanticScore)
try await sourcekitd.setPopularityTable(
PopularityTable(
moduleSymbolReferenceTables: [],
recentCompletions: [],
popularModules: ["a"],
notoriousModules: []
)
)
let result2 = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: "",
recentCompletions: []
)
let moduleIsPopularScore = try unwrap(result2.items.first(where: { $0.description == "foo" })?.semanticScore)
try await sourcekitd.setPopularityTable(
PopularityTable(
moduleSymbolReferenceTables: [],
recentCompletions: [],
popularModules: [],
notoriousModules: ["a"]
)
)
let result3 = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: "",
recentCompletions: []
)
let moduleIsUnpopularScore = try unwrap(result3.items.first(where: { $0.description == "foo" })?.semanticScore)
XCTAssertLessThan(moduleIsUnpopularScore, noPopularModulesScore)
XCTAssertLessThan(noPopularModulesScore, moduleIsPopularScore)
}
func testFlair() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath()
let positions = try await sourcekitd.openDocument(
path,
contents: """
struct S {
func foo(x: Int, y: Int) {}
func foo(_ arg: String) {}
func test(localArg: String) {
self.foo(1️⃣)
}
}
""",
compilerArguments: [path]
)
let result = try await sourcekitd.completeOpen(
path: path,
position: positions["1️⃣"],
filter: ""
)
guard result.items.count >= 3 else {
XCTFail("Expected at least 3 results. Received \(result)")
return
}
XCTAssertTrue(Set(result.items[0...1].map(\.description)) == ["(arg: String)", "(x: Int, y: Int)"])
XCTAssertTrue(result.items[2...].contains(where: { $0.description == "localArg" }))
}
func testPluginFilterAndSortPerfAllMatch() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath()
let (position, recent) = try await sourcekitd.perfTestSetup(path: path)
let initResult = try await sourcekitd.completeOpen(
path: path,
position: position,
filter: "",
recentCompletions: recent
)
XCTAssertEqual(initResult.items.count, 200)
self.measureMetrics([.wallClockTime], automaticallyStartMeasuring: false) {
assertNoThrow {
self.startMeasuring()
let result = try runAsync {
return try await sourcekitd.completeUpdate(
path: path,
position: position,
filter: ""
)
}
self.stopMeasuring()
XCTAssertEqual(result.items.count, 200)
try runAsync {
// Use a non-matching search to ensure we aren't caching the results.
let resetResult = try await sourcekitd.completeUpdate(
path: path,
position: position,
filter: "sadfasdfasd"
)
XCTAssertEqual(resetResult.items.count, 0)
}
}
}
}
func testPluginFilterAndSortPerfFiltered() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath()
let (position, recent) = try await sourcekitd.perfTestSetup(path: path)
let initResult = try await sourcekitd.completeOpen(
path: path,
position: position,
filter: "",
recentCompletions: recent
)
XCTAssertGreaterThanOrEqual(initResult.unfilteredResultCount, initResult.items.count)
self.measureMetrics([.wallClockTime], automaticallyStartMeasuring: false) {
assertNoThrow {
self.startMeasuring()
let result = try runAsync {
try await sourcekitd.completeUpdate(
path: path,
position: position,
filter: "mMethS"
)
}
self.stopMeasuring()
XCTAssertEqual(result.items.count, 200)
try runAsync {
// Use a non-matching search to ensure we aren't caching the results.
let resetResult = try await sourcekitd.completeUpdate(
path: path,
position: position,
filter: "sadfasdfasd"
)
XCTAssertEqual(resetResult.items.count, 0)
}
}
}
}
func testCrossModuleCompletion() async throws {
let project = try await PluginSwiftPMTestProject(files: [
"Sources/LibA/LibA.swift": """
public struct LibA {
public init() {}
public func method() {}
}
""",
"Sources/LibB/LibB.swift": """
import LibA
func test(lib: LibA) {
lib.1️⃣method()
}
""",
"Package.swift": """
// swift-tools-version: 5.7
import PackageDescription
let package = Package(
name: "MyLibrary",
targets: [
.target(name: "LibA"),
.target(name: "LibB", dependencies: ["LibA"]),
]
)
""",
])
// Open document in sourcekitd
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let libBPath = try project.uri(for: "LibB.swift").pseudoPath
try await sourcekitd.openDocument(
libBPath,
contents: project.contents(of: "LibB.swift"),
compilerArguments: project.compilerArguments(for: "LibB.swift")
)
// Invoke first code completion
let result = try await sourcekitd.completeOpen(
path: libBPath,
position: project.position(of: "1️⃣", in: "LibB.swift"),
filter: "met"
)
XCTAssertEqual(1, result.items.count)
XCTAssertEqual(result.items.first?.name, "method()")
// Modify LibA.swift to contain another memeber on the `LibA` struct
let modifiedLibA = """
\(try project.contents(of: "LibA.swift"))
extension LibA {
public var meta: Int { 0 }
}
"""
try modifiedLibA.write(to: project.uri(for: "LibA.swift").fileURL!, atomically: true, encoding: .utf8)
try await SwiftPMTestProject.build(at: project.scratchDirectory)
// Tell sourcekitd that dependencies have been updated and run completion again.
try await sourcekitd.dependencyUpdated()
let result2 = try await sourcekitd.completeOpen(
path: libBPath,
position: project.position(of: "1️⃣", in: "LibB.swift"),
filter: "met"
)
XCTAssertEqual(Set(result2.items.map(\.name)), ["meta", "method()"])
}
func testCompletionImportDepth() async throws {
let project = try await PluginSwiftPMTestProject(files: [
"Sources/Main/Main.swift": """
import Depth1Module
struct Depth0Struct {}
func test() {
1️⃣
return
}
""",
"Sources/Depth1Module/Depth1.swift": """
@_exported import Depth2Module
public struct Depth1Struct {}
""",
"Sources/Depth2Module/Depth2.swift": """
public struct Depth2Struct {}
""",
"Package.swift": """
// swift-tools-version: 5.7
import PackageDescription
let package = Package(
name: "MyLibrary",
targets: [
.executableTarget(name: "Main", dependencies: ["Depth1Module"]),
.target(name: "Depth1Module", dependencies: ["Depth2Module"]),
.target(name: "Depth2Module"),
]
)
""",
])
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let mainPath = try project.uri(for: "Main.swift").pseudoPath
try await sourcekitd.openDocument(
mainPath,
contents: project.contents(of: "Main.swift"),
compilerArguments: project.compilerArguments(for: "Main.swift")
)
let result = try await sourcekitd.completeOpen(
path: mainPath,
position: project.position(of: "1️⃣", in: "Main.swift"),
filter: "depth"
)
let depth0struct = try unwrap(result.items.first(where: { $0.name == "Depth0Struct" }))
let depth1struct = try unwrap(result.items.first(where: { $0.name == "Depth1Struct" }))
let depth2struct = try unwrap(result.items.first(where: { $0.name == "Depth2Struct" }))
let depth1module = try unwrap(result.items.first(where: { $0.name == "Depth1Module" }))
let depth2module = try unwrap(result.items.first(where: { $0.name == "Depth2Module" }))
XCTAssertGreaterThan(depth0struct.semanticScore, depth1struct.semanticScore)
XCTAssertGreaterThan(depth1struct.semanticScore, depth2struct.semanticScore)
// Since "module" entry doesn't have "import depth", we only checks that modules are de-prioritized.
XCTAssertGreaterThan(depth2struct.semanticScore, depth1module.semanticScore)
XCTAssertGreaterThan(depth2struct.semanticScore, depth2module.semanticScore)
}
func testCompletionDiagnostics() async throws {
#if !os(macOS)
try XCTSkipIf(true, "Soft deprecation is only defined for macOS in this test case")
#endif
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath()
let positions = try await sourcekitd.openDocument(
path,
contents: """
import 1️⃣Swift
struct S: P {
@available(*, deprecated)
func deprecatedF() {}
func test() {
self.2️⃣
}
var theVariable: Int {
3️⃣
}
@available(macOS, deprecated: 100000.0)
func softDeprecatedF() {}
}
""",
compilerArguments: [path]
)
let result1 = try await sourcekitd.completeOpen(path: path, position: positions["1️⃣"], filter: "Swift")
let swiftResult = try unwrap(result1.items.filter({ $0.description == "Swift" }).first)
XCTAssertEqual(swiftResult.description, "Swift")
XCTAssertEqual(swiftResult.hasDiagnostic, true)
let diag1 = try unwrap(try await sourcekitd.completeDiagnostic(id: swiftResult.id))
XCTAssertEqual(diag1.severity, sourcekitd.values.diagWarning)
XCTAssertEqual(diag1.description, "module 'Swift' is already imported")
let result2 = try await sourcekitd.completeOpen(path: path, position: positions["2️⃣"], filter: "deprecatedF")
guard result2.items.count >= 2 else {
XCTFail("Expected at least 2 results. Received \(result2)")
return
}
XCTAssertEqual(result2.items[0].description, "deprecatedF()")
XCTAssertEqual(result2.items[0].hasDiagnostic, true)
let diag2_0 = try unwrap(try await sourcekitd.completeDiagnostic(id: result2.items[0].id))
XCTAssertEqual(diag2_0.severity, sourcekitd.values.diagWarning)
XCTAssertEqual(diag2_0.description, "'deprecatedF()' is deprecated")
XCTAssertEqual(result2.items[1].description, "softDeprecatedF()")
XCTAssertEqual(result2.items[1].hasDiagnostic, true)
let diag2_1 = try unwrap(try await sourcekitd.completeDiagnostic(id: result2.items[1].id))
XCTAssertEqual(diag2_1.severity, sourcekitd.values.diagWarning)
XCTAssertEqual(diag2_1.description, "'softDeprecatedF()' will be deprecated in a future version of macOS")
let result4 = try await sourcekitd.completeOpen(path: path, position: positions["3️⃣"], filter: "theVariable")
guard result4.items.count >= 1 else {
XCTFail("Expected at least 1 results. Received \(result4)")
return
}
XCTAssertEqual(result4.items[0].description, "theVariable")
XCTAssertEqual(result4.items[0].hasDiagnostic, true)
let diag4_0 = try unwrap(try await sourcekitd.completeDiagnostic(id: result4.items[0].id))
XCTAssertEqual(diag4_0.severity, sourcekitd.values.diagWarning)
XCTAssertEqual(diag4_0.description, "attempting to access 'theVariable' within its own getter")
}
func testActorKind() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath()
let positions = try await sourcekitd.openDocument(
path,
contents: """
actor MyActor {
}
func test() {
1️⃣}
""",
compilerArguments: [path]
)
let result = try await sourcekitd.completeOpen(path: path, position: positions["1️⃣"], filter: "My")
let actorItem = try unwrap(result.items.first { item in item.description == "MyActor" })
XCTAssertEqual(actorItem.kind, sourcekitd.values.declActor)
}
func testMacroKind() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath()
let positions = try await sourcekitd.openDocument(
path,
contents: """
@attached(conformance) public macro MyConformance() = #externalMacro(module: "MyMacros", type: "MyConformanceMacro")
@freestanding(expression) public macro MyExpression(_: Any) -> String = #externalMacro(module: "MyMacros", type: "MyExpressionMacro")
func testAttached() {
@1️⃣
}
func testFreestanding() {
_ = #2️⃣
}
""",
compilerArguments: [path]
)
let result1 = try await sourcekitd.completeOpen(path: path, position: positions["1️⃣"], filter: "My")
let macroItem1 = try unwrap(result1.items.first { item in item.name == "MyConformance" })
XCTAssertEqual(macroItem1.kind, sourcekitd.values.declMacro)
let result2 = try await sourcekitd.completeOpen(path: path, position: positions["2️⃣"], filter: "My")
let macroItem2 = try unwrap(result2.items.first { item in item.name.starts(with: "MyExpression") })
XCTAssertEqual(macroItem2.kind, sourcekitd.values.declMacro)
}
func testMaxResults() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let sourcekitd = try await getSourceKitD()
let path = scratchFilePath()
var sourceText = "//dummy\n";
for i in 0..<200 {
/// Create at least 200 (default maxResults) items
sourceText += """
func foo\(i)() {}
"""
}
try await sourcekitd.openDocument(path, contents: sourceText, compilerArguments: [path])
let position = Position(line: 200, utf16index: 0)
let result1 = try await sourcekitd.completeOpen(path: path, position: position, filter: "f", maxResults: 3)
XCTAssertEqual(result1.items.count, 3)
let result2 = try await sourcekitd.completeUpdate(path: path, position: position, filter: "fo", maxResults: 5)
XCTAssertEqual(result2.items.count, 5)
let result3 = try await sourcekitd.completeUpdate(path: path, position: position, filter: "f")
XCTAssertEqual(result3.items.count, 200)
}
}
// MARK: - Structured result types
fileprivate struct CompletionResultSet: Sendable {
var unfilteredResultCount: Int
var memberAccessTypes: [String]
var items: [CompletionResult]
init(_ dict: SKDResponseDictionary) throws {
let keys = dict.sourcekitd.keys
guard let unfilteredResultCount: Int = dict[keys.unfilteredResultCount],
let memberAccessTypes = dict[keys.memberAccessTypes]?.asStringArray,
let results: SKDResponseArray = dict[keys.results]
else {
throw TestError(
"expected {key.results: , key.unfiltered_result_count: }; got \(dict)"
)
}
self.unfilteredResultCount = unfilteredResultCount
self.memberAccessTypes = memberAccessTypes
self.items =
try results
.map { try CompletionResult($0) }
.sorted(by: { $0.semanticScore > $1.semanticScore })
XCTAssertGreaterThanOrEqual(
self.unfilteredResultCount,
self.items.count,
"unfiltered_result_count must be greater than or equal to the count of results"
)
}
}
fileprivate struct CompletionResult: Equatable, Sendable {
nonisolated(unsafe) var kind: sourcekitd_api_uid_t
var id: Int
var name: String
var description: String
var sourcetext: String
var module: String?
var typename: String
var textMatchScore: Double
var semanticScore: Double
var semanticScoreComponents: String?
var priorityBucket: Int
var isSystem: Bool
var numBytesToErase: Int
var hasDiagnostic: Bool
var groupID: Int?
init(_ dict: SKDResponseDictionary) throws {
let keys = dict.sourcekitd.keys
guard let kind: sourcekitd_api_uid_t = dict[keys.kind],
let id: Int = dict[keys.identifier],
let name: String = dict[keys.name],
let description: String = dict[keys.description],
let sourcetext: String = dict[keys.sourceText],
let typename: String = dict[keys.typeName],
let textMatchScore: Double = dict[keys.textMatchScore],
let semanticScore: Double = dict[keys.semanticScore],
let priorityBucket: Int = dict[keys.priorityBucket],
let isSystem: Bool = dict[keys.isSystem],
let hasDiagnostic: Bool = dict[keys.hasDiagnostic]
else {
throw TestError("Failed to decode CompletionResult. Received \(dict)")
}
self.kind = kind
self.id = id
self.name = name
self.description = description
self.sourcetext = sourcetext
self.module = dict[keys.moduleName]
self.typename = typename
self.textMatchScore = textMatchScore
self.semanticScore = semanticScore
self.semanticScoreComponents = dict[keys.semanticScoreComponents]
self.priorityBucket = priorityBucket
self.isSystem = isSystem
self.numBytesToErase = dict[keys.numBytesToErase] ?? 0
self.hasDiagnostic = hasDiagnostic
self.groupID = dict[keys.groupId]
assert(self.groupID != 0)
}
}
fileprivate struct CompletionDocumentation {
var docBrief: String? = nil
var associatedUSRs: [String] = []
init(_ dict: SKDResponseDictionary) {
let keys = dict.sourcekitd.keys
self.docBrief = dict[keys.docBrief]
self.associatedUSRs = dict[keys.associatedUSRs]?.asStringArray ?? []
}
}
fileprivate struct CompletionDiagnostic {
var severity: sourcekitd_api_uid_t
var description: String
init?(_ dict: SKDResponseDictionary) {
let keys = dict.sourcekitd.keys
guard
let severity: sourcekitd_api_uid_t = dict[keys.severity],
let description: String = dict[keys.description]
else {
return nil
}
self.severity = severity
self.description = description
}
}
fileprivate struct TestError: Error {
let error: String
init(_ message: String) {
self.error = message
}
}
// MARK: - sourcekitd convenience functions
struct CompletionRequestFlags: OptionSet {
let rawValue: Int
static let annotate: Self = .init(rawValue: 1 << 0)
static let addInitsToTopLevel: Self = .init(rawValue: 1 << 1)
static let addCallWithNoDefaultArgs: Self = .init(rawValue: 1 << 2)
static let includeSemanticComponents: Self = .init(rawValue: 1 << 3)
}
fileprivate extension SourceKitD {
@discardableResult
nonisolated func openDocument(
_ name: String,
contents markedSource: String,
compilerArguments: [String]? = nil
) async throws -> DocumentPositions {
let (markers, textWithoutMarkers) = extractMarkers(markedSource)
var compilerArguments = compilerArguments ?? [name]
if let defaultSDKPath {
compilerArguments += ["-sdk", defaultSDKPath]
}
let req = dictionary([
keys.name: name,
keys.sourceText: textWithoutMarkers,
keys.syntacticOnly: 1,
keys.compilerArgs: compilerArguments as [SKDRequestValue],
])
_ = try await send(\.editorOpen, req)
return DocumentPositions(markers: markers, textWithoutMarkers: textWithoutMarkers)
}
nonisolated func editDocument(_ name: String, fromOffset offset: Int, length: Int, newContents: String) async throws {
let req = dictionary([
keys.name: name,
keys.offset: offset,
keys.length: length,
keys.sourceText: newContents,
keys.syntacticOnly: 1,
])
_ = try await send(\.editorReplaceText, req)
}
nonisolated func closeDocument(_ name: String) async throws {
let req = dictionary([
keys.name: name
])
_ = try await send(\.editorClose, req)
}
nonisolated func completeImpl(
requestUID: KeyPath & Sendable,
path: String,
position: Position,
filter: String,
recentCompletions: [String]? = nil,
flags: CompletionRequestFlags = [],
useXPC: Bool = false,
maxResults: Int? = nil,
compilerArguments: [String]? = nil
) async throws -> CompletionResultSet {
let options = dictionary([
keys.useNewAPI: 1,
keys.annotatedDescription: flags.contains(.annotate) ? 1 : 0,
keys.addInitsToTopLevel: flags.contains(.addInitsToTopLevel) ? 1 : 0,
keys.addCallWithNoDefaultArgs: flags.contains(.addCallWithNoDefaultArgs) ? 1 : 0,
keys.includeSemanticComponents: flags.contains(.includeSemanticComponents) ? 1 : 0,
keys.filterText: filter,
keys.recentCompletions: recentCompletions as [SKDRequestValue]?,
keys.maxResults: maxResults,
])
let req = dictionary([
keys.line: position.line + 1,
// Technically sourcekitd needs a UTF-8 index but we can assume there are no Unicode characters in the tests
keys.column: position.utf16index + 1,
keys.sourceFile: path,
keys.codeCompleteOptions: options,
keys.compilerArgs: compilerArguments as [SKDRequestValue]?,
])
let res = try await send(requestUID, req)
return try CompletionResultSet(res)
}
nonisolated func completeOpen(
path: String,
position: Position,
filter: String,
recentCompletions: [String]? = nil,
flags: CompletionRequestFlags = [],
useXPC: Bool = false,
maxResults: Int? = nil,
compilerArguments: [String]? = nil
) async throws -> CompletionResultSet {
return try await completeImpl(
requestUID: \.codeCompleteOpen,
path: path,
position: position,
filter: filter,
recentCompletions: recentCompletions,
flags: flags,
useXPC: useXPC,
maxResults: maxResults,
compilerArguments: compilerArguments
)
}
nonisolated func completeUpdate(
path: String,
position: Position,
filter: String,
flags: CompletionRequestFlags = [],
useXPC: Bool = false,
maxResults: Int? = nil
) async throws -> CompletionResultSet {
return try await completeImpl(
requestUID: \.codeCompleteUpdate,
path: path,
position: position,
filter: filter,
recentCompletions: nil,
flags: flags,
useXPC: useXPC,
maxResults: maxResults,
compilerArguments: nil
)
}
nonisolated func completeClose(path: String, position: Position) async throws {
let req = dictionary([
keys.line: position.line + 1,
// Technically sourcekitd needs a UTF-8 index but we can assume there are no Unicode characters in the tests
keys.column: position.utf16index + 1,
keys.sourceFile: path,
keys.codeCompleteOptions: dictionary([keys.useNewAPI: 1]),
])
_ = try await send(\.codeCompleteClose, req)
}
nonisolated func completeDocumentation(id: Int) async throws -> CompletionDocumentation {
let resp = try await send(\.codeCompleteDocumentation, dictionary([keys.identifier: id]))
return CompletionDocumentation(resp)
}
nonisolated func completeDiagnostic(id: Int) async throws -> CompletionDiagnostic? {
let resp = try await send(\.codeCompleteDiagnostic, dictionary([keys.identifier: id]))
return CompletionDiagnostic(resp)
}
nonisolated func dependencyUpdated() async throws {
_ = try await send(\.dependencyUpdated, dictionary([:]))
}
nonisolated func setPopularAPI(popular: [String], unpopular: [String]) async throws {
let req = dictionary([
keys.codeCompleteOptions: dictionary([keys.useNewAPI: 1]),
keys.popular: popular as [SKDRequestValue],
keys.unpopular: unpopular as [SKDRequestValue],
])
let resp = try await send(\.codeCompleteSetPopularAPI, req)
XCTAssertEqual(resp[keys.useNewAPI], 1)
}
nonisolated func setPopularityIndex(
scopedPopularityDataPath: String,
popularModules: [String],
notoriousModules: [String]
) async throws {
let req = dictionary([
keys.codeCompleteOptions: dictionary([keys.useNewAPI: 1]),
keys.scopedPopularityTablePath: scopedPopularityDataPath,
keys.popularModules: popularModules as [SKDRequestValue],
keys.notoriousModules: notoriousModules as [SKDRequestValue],
])
let resp = try await send(\.codeCompleteSetPopularAPI, req)
XCTAssertEqual(resp[keys.useNewAPI], 1)
}
func setPopularityTable(_ popularityTable: PopularityTable) async throws {
let symbolPopularity = popularityTable.symbolPopularity.map { key, value in
dictionary([
keys.popularityKey: key,
keys.popularityValueIntBillion: Int(value.scoreComponent * 1_000_000_000),
])
}
let modulePopularity = popularityTable.modulePopularity.map { key, value in
dictionary([
keys.popularityKey: key,
keys.popularityValueIntBillion: Int(value.scoreComponent * 1_000_000_000),
])
}
let req = dictionary([
keys.codeCompleteOptions: dictionary([keys.useNewAPI: 1]),
keys.symbolPopularity: symbolPopularity as [SKDRequestValue],
keys.modulePopularity: modulePopularity as [SKDRequestValue],
])
let resp = try await send(\.codeCompleteSetPopularAPI, req)
XCTAssertEqual(resp[keys.useNewAPI], 1)
}
func perfTestSetup(path: String) async throws -> (Position, recent: [String]) {
var content = """
struct S {
func test() {
self.1️⃣
}
"""
#if DEBUG
let numMethods = 1_000
#else
let numMethods = 100_000
#endif
var popular: [String] = []
var unpopular: [String] = []
for i in 0..(_ body: @escaping @Sendable () async throws -> T) throws -> T {
var result: Result!
let expectation = XCTestExpectation(description: "")
Task {
do {
result = .success(try await body())
} catch {
result = .failure(error)
}
expectation.fulfill()
}
let started = XCTWaiter.wait(for: [expectation], timeout: defaultTimeout)
if started != .completed {
throw ExpectationNotFulfilledError()
}
return try result.get()
}