//===----------------------------------------------------------------------===// // // This source file is part of the Swift.org open source project // // Copyright (c) 2014 - 2026 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 // //===----------------------------------------------------------------------===// @_spi(SourceKitLSP) import LanguageServerProtocol import SKLogging import SKTestSupport import SourceKitLSP import SwiftExtensions @_spi(Testing) import SwiftLanguageService import SwiftParser import SwiftSyntax import SwiftSyntaxBuilder import XCTest private typealias CodeActionCapabilities = TextDocumentClientCapabilities.CodeAction private typealias CodeActionLiteralSupport = CodeActionCapabilities.CodeActionLiteralSupport private typealias CodeActionKindCapabilities = CodeActionLiteralSupport.CodeActionKindValueSet private let clientCapabilitiesWithCodeActionSupport: ClientCapabilities = { var documentCapabilities = TextDocumentClientCapabilities() var codeActionCapabilities = CodeActionCapabilities() let codeActionKinds = CodeActionKindCapabilities(valueSet: [.refactor, .quickFix]) let codeActionLiteralSupport = CodeActionLiteralSupport(codeActionKind: codeActionKinds) codeActionCapabilities.codeActionLiteralSupport = codeActionLiteralSupport documentCapabilities.codeAction = codeActionCapabilities documentCapabilities.completion = .init(completionItem: .init(snippetSupport: true)) return ClientCapabilities(workspace: nil, textDocument: documentCapabilities) }() final class CodeActionTests: SourceKitLSPTestCase { func testCodeActionResponseLegacySupport() throws { let command = Command(title: "Title", command: "Command", arguments: [1, "text", 2.2, nil]) let codeAction = CodeAction(title: "1") let codeAction2 = CodeAction(title: "2", command: command) var capabilities: TextDocumentClientCapabilities.CodeAction var capabilityJson: String var data: Data var response: CodeActionRequestResponse capabilityJson = """ { "dynamicRegistration": true, "codeActionLiteralSupport" : { "codeActionKind": { "valueSet": [] } } } """ data = capabilityJson.data(using: .utf8)! capabilities = try JSONDecoder().decode( TextDocumentClientCapabilities.CodeAction.self, from: data ) response = .init(codeActions: [codeAction, codeAction2], clientCapabilities: capabilities) let actions = try JSONDecoder().decode([CodeAction].self, from: JSONEncoder().encode(response)) XCTAssertEqual(actions, [codeAction, codeAction2]) capabilityJson = """ { "dynamicRegistration": true } """ data = capabilityJson.data(using: .utf8)! capabilities = try JSONDecoder().decode( TextDocumentClientCapabilities.CodeAction.self, from: data ) response = .init(codeActions: [codeAction, codeAction2], clientCapabilities: capabilities) let commands = try JSONDecoder().decode([Command].self, from: JSONEncoder().encode(response)) XCTAssertEqual(commands, [command]) } func testCodeActionResponseIgnoresSupportedKinds() throws { // The client guarantees that unsupported kinds will be handled, and in // practice some clients use `"codeActionKind":{"valueSet":[]}`, since // they support all kinds anyway. So to avoid filtering all actions, we // ignore the supported kinds. let unspecifiedAction = CodeAction(title: "Unspecified") let refactorAction = CodeAction(title: "Refactor", kind: .refactor) let quickfixAction = CodeAction(title: "Quickfix", kind: .quickFix) let actions = [unspecifiedAction, refactorAction, quickfixAction] var capabilities: TextDocumentClientCapabilities.CodeAction var capabilityJson: String var data: Data var response: CodeActionRequestResponse capabilityJson = """ { "dynamicRegistration": true, "codeActionLiteralSupport" : { "codeActionKind": { "valueSet": ["refactor"] } } } """ data = capabilityJson.data(using: .utf8)! capabilities = try JSONDecoder().decode( TextDocumentClientCapabilities.CodeAction.self, from: data ) response = .init(codeActions: actions, clientCapabilities: capabilities) XCTAssertEqual(response, .codeActions([unspecifiedAction, refactorAction, quickfixAction])) capabilityJson = """ { "dynamicRegistration": true, "codeActionLiteralSupport" : { "codeActionKind": { "valueSet": [] } } } """ data = capabilityJson.data(using: .utf8)! capabilities = try JSONDecoder().decode( TextDocumentClientCapabilities.CodeAction.self, from: data ) response = .init(codeActions: actions, clientCapabilities: capabilities) XCTAssertEqual(response, .codeActions([unspecifiedAction, refactorAction, quickfixAction])) } func testCodeActionResponseCommandMetadataInjection() throws { let url = URL(fileURLWithPath: "/a.swift") let textDocument = TextDocumentIdentifier(url) let expectedMetadata: LSPAny = try { let metadata = SourceKitLSPCommandMetadata(textDocument: textDocument) let data = try JSONEncoder().encode(metadata) return try JSONDecoder().decode(LSPAny.self, from: data) }() XCTAssertEqual(expectedMetadata, .dictionary(["sourcekitlsp_textDocument": ["uri": "file:///a.swift"]])) let command = Command(title: "Title", command: "Command", arguments: [1, "text", 2.2, nil]) let codeAction = CodeAction(title: "1") let codeAction2 = CodeAction(title: "2", command: command) let request = CodeActionRequest( range: Position(line: 0, utf16index: 0).. String { var a = "hello" 1️⃣ return a } """, uri: uri ) let request = CodeActionRequest( range: positions["1️⃣"].. String { var a = "1️⃣" return a } """, uri: uri ) let testPosition = positions["1️⃣"] let request = CodeActionRequest( range: Range(testPosition), context: .init(), textDocument: TextDocumentIdentifier(uri) ) let result = try await testClient.send(request) let expectedCommandArgs: LSPAny = [ "actionString": "source.refactoring.kind.localize.string", "positionRange": [ "start": [ "character": .int(testPosition.utf16index), "line": .int(testPosition.line), ], "end": [ "character": .int(testPosition.utf16index), "line": .int(testPosition.line), ], ], "title": "Localize String", "textDocument": ["uri": .string(uri.stringValue)], ] let metadataArguments: LSPAny = ["sourcekitlsp_textDocument": ["uri": .string(uri.stringValue)]] let expectedCommand = Command( title: "Localize String", command: "semantic.refactor.command", arguments: [expectedCommandArgs] + [metadataArguments] ) let expectedCodeAction = CodeAction( title: "Localize String", kind: .refactor, command: expectedCommand ) assertContains(result?.codeActions ?? [], expectedCodeAction) } func testJSONCodableCodeActionResult() async throws { let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport) let uri = DocumentURI(for: .swift) let positions = testClient.openDocument( """ 1️⃣{ "name": "Produce", "shelves": [ { "name": "Discount Produce", "product": { "name": "Banana", "points": 200, "description": "A banana that's perfectly ripe." } } ] } """, uri: uri ) let testPosition = positions["1️⃣"] let request = CodeActionRequest( range: Range(testPosition), context: .init(), textDocument: TextDocumentIdentifier(uri) ) let result = try await testClient.send(request) // Make sure we get a JSON conversion action. let codableAction = result?.codeActions?.first { action in return action.title == "Create Codable structs from JSON" } XCTAssertNotNil(codableAction) } func testSemanticRefactorRangeCodeActionResult() async throws { let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport) let uri = DocumentURI(for: .swift) let positions = testClient.openDocument( """ func foo() -> String { 1️⃣var a = "hello" return a2️⃣ } """, uri: uri ) let startPosition = positions["1️⃣"] let endPosition = positions["2️⃣"] let request = CodeActionRequest( range: startPosition.. ApplyEditResponse in defer { editReceived.fulfill() } guard let change = request.edit.changes?[uri]?.only else { XCTFail("Expected exactly one edit") return ApplyEditResponse(applied: false, failureReason: "Expected exactly one edit") } XCTAssertEqual( change.newText.trimmingTrailingWhitespace(), """ func foo() { } """ ) return ApplyEditResponse(applied: true, failureReason: nil) } _ = try await testClient.send(ExecuteCommandRequest(command: command.command, arguments: command.arguments)) try await fulfillmentOfOrThrow(editReceived) } func testAddDocumentationCodeActionResult() async throws { let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport) let uri = DocumentURI(for: .swift) let positions = testClient.openDocument( """ 2️⃣func refacto1️⃣r(syntax: DeclSyntax, in context: Void) -> DeclSyntax? { }3️⃣ """, uri: uri ) let testPosition = positions["1️⃣"] let request = CodeActionRequest( range: Range(testPosition), context: .init(), textDocument: TextDocumentIdentifier(uri) ) let result = try await testClient.send(request) // Make sure we get an add-documentation action. let addDocAction = result?.codeActions?.first { action in return action.title == "Add documentation" } XCTAssertNotNil(addDocAction) } func testCodeActionForFixItsProducedBySwiftSyntax() async throws { let project = try await MultiFileTestProject(files: [ "test.swift": "protocol 1️⃣Multi2️⃣ 3️⃣ident 4️⃣{}", "compile_commands.json": "[]", ]) let (uri, positions) = try project.openDocument("test.swift") let report = try await project.testClient.send( DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri)) ) XCTAssertEqual(report.fullReport?.items.count, 1) let codeActions = try XCTUnwrap(report.fullReport?.items.first?.codeActions) let expectedCodeActions = [ CodeAction( title: "Join the identifiers together", kind: .quickFix, edit: WorkspaceEdit( changes: [ uri: [ TextEdit(range: positions["1️⃣"]..(_ input: T1) {}" ) ] ] ) ) ] } } func testOpaqueParameterToGenericIsNotShownFromTheBody() async throws { try await assertCodeActions( ##""" func someFunction(_ input: some Value) 1️⃣{ 2️⃣print("x") }3️⃣ """##, exhaustive: false ) { uri, positions in [] } } func testConvertJSONToCodable() async throws { try await assertCodeActions( ##""" 1️⃣{ 2️⃣"id": 3️⃣1, "values": 4️⃣["foo", "bar"] }5️⃣ """##, ranges: [("1️⃣", "5️⃣")], exhaustive: false ) { uri, positions in [ CodeAction( title: "Create Codable structs from JSON", kind: .refactorInline, diagnostics: nil, edit: WorkspaceEdit( changes: [ uri: [ TextEdit( range: positions["1️⃣"]..= b)" ) } func testApplyDeMorganLawPrecedencePreservation() throws { try assertDeMorganTransform( input: "!(a && b || c)", expected: "((!a || !b) && !c)" ) } func testApplyDeMorganLawBitwise() throws { try assertDeMorganTransform( input: "~(a | b)", expected: "(~a & ~b)" ) } func testApplyDeMorganLawPropositionsToNegation() throws { try assertDeMorganTransform( input: "!a || !b", expected: "!(a && b)" ) } func testApplyDeMorganLawNestedNegation() throws { try assertDeMorganTransform( input: "!(!(a && b) || c)", expected: "((a && b) && !c)" ) } func testApplyDeMorganLawOrToAnd() throws { try assertDeMorganTransform( input: "!((a || b) && c)", expected: "((!a && !b) || !c)" ) } func testApplyDeMorganLawTernaryPropagation() throws { try assertDeMorganTransform( input: "!(a ? !b : c)", expected: "(a ? b : !c)" ) } func testApplyDeMorganLawWithIsExpression() throws { try assertDeMorganTransform( input: "!a || !(s is String)", expected: "!(a && (s is String))" ) } func testApplyDeMorganLawReducedBoolean() throws { try assertDeMorganTransform( input: "((((((a !== !(b || c)))) && !d)))", expected: "!((((((a === !(b || c)))) || d)))" ) } func testApplyDeMorganLawReducedBooleanNonNested() throws { try assertDeMorganTransform( input: "!a || ((!((b)))) || s is String || c != d", expected: "!(a && ((((b)))) && !(s is String) && c == d)" ) } func testApplyDeMorganLawSpreadBitwise() throws { try assertDeMorganTransform( input: "~((b | ((c)) | d | e & ~f | (~g & h)))", expected: "((~b & ~((c)) & ~d & (~e | f) & (g | ~h)))" ) } func testApplyDeMorganLawTernaryExpansion() throws { try assertDeMorganTransform( input: "!((a ? b : !c) || (!d ? !e : f) && (g ? h : i))", expected: "((a ? !b : c) && ((!d ? e : !f) || !(g ? h : i)))" ) } func testApplyDeMorganLawTernaryNoPropagation() throws { // Negating the ternary (b ? !c : !d) adds complexity (2 negations) vs wrapping (1 negation). // So we expect the ternary to be wrapped in parens and negated. try assertDeMorganTransform( input: "!(a && (b ? c : d))", expected: "(!a || !(b ? c : d))" ) } func testApplyDeMorganLawBooleanLiteral() throws { // !true -> false try assertDeMorganTransform( input: "!true", expected: "false" ) // !(a && false) -> !a || true try assertDeMorganTransform( input: "!(a && false)", expected: "(!a || true)" ) } func testApplyDeMorganLawTrivia() throws { // /*c1*/!(/*c2*/a /*c3*/&& /*c4*/b/*c5*/)/*c6*/ // Note: Comments attached to the removed '!' (/*c1*/) are dropped. // Comment between '!' and '(' is illegal in swift. try assertDeMorganTransform( input: "/*c1*/!(/*c2*/a /*c3*/&& /*c4*/b/*c5*/)/*c6*/", expected: "(/*c2*/!a /*c3*/|| /*c4*/!b/*c5*/)/*c6*/" ) } func testApplyDeMorganLawAdvancedTrivia() throws { // Multiline preservation try assertDeMorganTransform( input: """ !( a && b ) """, expected: """ ( !a || !b ) """ ) // Line comments try assertDeMorganTransform( input: """ !(a && b // check ) """, expected: """ (!a || !b // check ) """ ) // Comments attached to inner operators try assertDeMorganTransform( input: "!(a /*op*/ && b)", expected: "(!a /*op*/ || !b)" ) } func testApplyDeMorganLawTryNegationPropagation() throws { try assertDeMorganTransform( input: "true && try a", expected: "!(false || try !a)" ) } func testApplyDeMorganLawAwaitNegationPropagation() throws { try assertDeMorganTransform( input: "true && await a", expected: "!(false || await !a)" ) } func testApplyDeMorganLawTryOptionalDoesNotPropagate() throws { try assertDeMorganTransform( input: "true && try? a", expected: "!(false || !(try? a))" ) } func testApplyDeMorganLawTryWithTrivia() throws { try assertDeMorganTransform( input: "true && try /* comment */ a", expected: "!(false || try /* comment */ !a)" ) } func testApplyDeMorganLawTryFusionCheck() throws { try assertDeMorganTransform( input: "true && try(a)", expected: "!(false || try !(a))" ) } func testApplyDeMorganLawForcedUnwrapAsAtomic() throws { try assertDeMorganTransform( input: "a! && false", expected: "!(!a! || true)" ) } func testApplyDeMorganLawTriviaPreservation() throws { try assertDeMorganTransform( input: "true /*a*/&& false", expected: "!(false /*a*/|| true)" ) } func testApplyDeMorganLawPreservesIndentationForPropositions() throws { try assertDeMorganTransform( input: " !a || !b", expected: " !(a && b)" ) } func testApplyDeMorganLawPreservesTriviaForPropositions() throws { try assertDeMorganTransform( input: "/* c */ !a || !b", expected: "/* c */ !(a && b)" ) } func testApplyDeMorganLawNestedActionAvailability() async throws { try await assertCodeActions( """ let x = 1️⃣!2️⃣(3️⃣!(4️⃣a && b) 5️⃣|| c6️⃣)7️⃣ """, markers: ["4️⃣"], exhaustive: false ) { uri, positions in [ CodeAction( title: "Apply De Morgan's law, converting '!(a && b) ' to '(!a || !b) '", kind: .refactorInline, edit: WorkspaceEdit( changes: [ uri: [ TextEdit( range: positions["3️⃣"].. ApplyEditResponse in XCTAssertEqual( request.edit.changes, [ uri: [ TextEdit(range: positions["1️⃣"].. String2️⃣ { return "" }3️⃣ """, uri: uri ) let request = CodeActionRequest( range: positions["1️⃣"].. String 1️⃣{ 2️⃣return "" }3️⃣ """##, exhaustive: false ) { uri, positions in [] } } func testConvertZeroParameterFunctionToComputedPropertyNotOfferedForImplicitVoid() async throws { try await assertCodeActions( """ 1️⃣func test()2️⃣ { }3️⃣ """, ranges: [("1️⃣", "2️⃣")], exhaustive: false ) { _, _ in [] } } func testConvertZeroParameterFunctionToComputedPropertyNotOfferedForExplicitVoid() async throws { try await assertCodeActions( """ 1️⃣func test() -> Void2️⃣ { }3️⃣ """, ranges: [("1️⃣", "2️⃣")], exhaustive: false ) { _, _ in [] } } func testConvertZeroParameterFunctionToComputedPropertyNotOfferedForEmptyTuple() async throws { try await assertCodeActions( """ 1️⃣func test() -> ()2️⃣ { }3️⃣ """, ranges: [("1️⃣", "2️⃣")], exhaustive: false ) { _, _ in [] } } func testConvertComputedPropertyToZeroParameterFunction() async throws { let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport) let uri = DocumentURI(for: .swift) let positions = testClient.openDocument( """ 1️⃣var someFunction: String2️⃣ { return "" }3️⃣ """, uri: uri ) let request = CodeActionRequest( range: positions["1️⃣"].. String { return "" } """ ) ] ] ) ) XCTAssertTrue(codeActions.contains(expectedCodeAction)) } func testConvertComputedPropertyToZeroParameterFunctionIsNotShownFromTheBody() async throws { try await assertCodeActions( ##""" var someFunction: String 1️⃣{ 2️⃣return "" }3️⃣ """##, exhaustive: false ) { uri, positions in [] } } /// Retrieves the code action at a set of markers and asserts that it matches a list of expected code actions. /// /// - Parameters: /// - markedText: The source file input to get the code actions for. /// - markers: The list of markers to retrieve code actions at. If `nil` code actions will be retrieved for all /// markers in `markedText` /// - ranges: If specified, code actions are also requested for selection ranges between these markers. /// - exhaustive: Whether `expected` is expected to be a subset of the returned code actions or whether it is /// expected to exhaustively match all code actions. /// - expected: A closure that returns the list of expected code actions, given the URI of the test document and the /// marker positions within. private func assertCodeActions( _ markedText: String, markers: [String]? = nil, ranges: [(String, String)] = [], exhaustive: Bool = true, expected: (_ uri: DocumentURI, _ positions: DocumentPositions) -> [CodeAction], testName: String = #function, file: StaticString = #filePath, line: UInt = #line ) async throws { let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport) let uri = DocumentURI(for: .swift, testName: testName) let positions = testClient.openDocument(markedText, uri: uri) var ranges = ranges if let markers { ranges += markers.map { ($0, $0) } } else { ranges += extractMarkers(markedText).markers.map(\.key).map { ($0, $0) } } for (startMarker, endMarker) in ranges { let result = try await testClient.send( CodeActionRequest( range: positions[startMarker]..