Files
sourcekit-lsp/Tests/SourceKitLSPTests/CodeActionTests.swift
T
Ayush Thakur 28e69b6c25 Add 'Add Explicit Raw Values' code action
Adds a syntactic refactoring action that converts implicit raw values to
explicit ones for enums whose first inherited type is an integer type
(any width up to Int128/UInt128) or String.

Resolves #2516.

Before:

    enum Status: Int {
        case active
        case inactive
        case pending = 10
        case archived
    }

After:

    enum Status: Int {
        case active = 0
        case inactive = 1
        case pending = 10
        case archived = 11
    }

The action is exposed through `SyntaxRefactoringCodeActionProvider` and
performs all transformations as a list of `SourceEdit`s, leaving the
rest of the enum (including trailing trivia and member ordering)
untouched.

### Motivation:

For enums with implicit raw values, making the values explicit is a
common refactoring step before serialising the values or relying on
their numeric identity. Doing the renumbering by hand is error-prone
once any cases already have explicit values, because the implicit
continuation rule has to be reapplied for every gap.

### Modifications:

- New `Sources/SwiftSyntaxCodeActions/AddExplicitEnumRawValues.swift`
  conforming to `EditRefactoringProvider` and
  `SyntaxRefactoringCodeActionProvider`. Handles negative integer
  literals, hex/binary/octal literals with underscore separators, and
  backticked case identifiers in String enums.
- Registered in `Sources/SwiftSyntaxCodeActions/SyntaxCodeActions.swift`
  and `Sources/SwiftSyntaxCodeActions/CMakeLists.txt`.
- Twelve LSP-level tests in `Tests/SourceKitLSPTests/CodeActionTests.swift`
  covering positive cases (Int, String, negative continuation, hex
  continuation, backtick stripping) and negative cases (no raw value
  type, raw value type not first in inheritance clause, all cases
  already explicit, unsupported raw value expression, #if directives,
  freestanding macro expansions, associated value cases).

### Result:

When the cursor is on an enum declaration whose raw value type is
supported and at least one case is missing an explicit raw value,
SourceKit-LSP offers the "Add Explicit Raw Values" code action and
inserts the missing raw values in place. The action is suppressed in
situations where the implicit numbering cannot be computed safely
(unsupported raw value expression, #if blocks, freestanding macros,
associated value cases).
2026-05-26 20:07:03 +05:30

2466 lines
70 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//===----------------------------------------------------------------------===//
//
// 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 SwiftSyntaxCodeActions
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 expectedCommandMetadata: LSPAny = try {
let metadata = SourceKitLSPCommandMetadata(textDocument: textDocument)
let data = try JSONEncoder().encode(metadata)
return try JSONDecoder().decode(LSPAny.self, from: data)
}()
XCTAssertEqual(expectedCommandMetadata, ["sourcekitlsp_textDocument": ["uri": "file:///a.swift"]])
let expectedResolveMetadata: LSPAny = try {
let metadata = CodeActionResolveMetadata(textDocument: textDocument)
let data = try JSONEncoder().encode(metadata)
return try JSONDecoder().decode(LSPAny.self, from: data)
}()
XCTAssertEqual(expectedResolveMetadata, ["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)..<Position(line: 1, utf16index: 1),
context: .init(diagnostics: [], only: nil),
textDocument: textDocument
)
var response = request.injectMetadata(toResponse: .commands([command]))
XCTAssertEqual(
response,
.commands([
Command(
title: command.title,
command: command.command,
arguments: command.arguments! + [expectedCommandMetadata]
)
])
)
response = request.injectMetadata(toResponse: .codeActions([codeAction, codeAction2]))
var expectedCodeAction = codeAction
expectedCodeAction.data = expectedResolveMetadata
XCTAssertEqual(
response,
.codeActions([
expectedCodeAction,
CodeAction(
title: codeAction2.title,
command: Command(
title: command.title,
command: command.command,
arguments: command.arguments! + [expectedCommandMetadata]
)
),
])
)
response = request.injectMetadata(toResponse: nil)
XCTAssertNil(response)
}
func testCodeActionResolveMetadataPreservesUnderlyingData() throws {
let url = URL(fileURLWithPath: "/a.swift")
let textDocument = TextDocumentIdentifier(url)
let originalData: LSPAny = ["custom": "value"]
var codeAction = CodeAction(title: "With data")
codeAction.data = originalData
let request = CodeActionRequest(
range: Position(line: 0, utf16index: 0)..<Position(line: 1, utf16index: 1),
context: .init(diagnostics: [], only: nil),
textDocument: textDocument
)
let response = request.injectMetadata(toResponse: .codeActions([codeAction]))
guard case .codeActions(let actions) = response, let injected = actions.first,
case .dictionary(let dict) = injected.data,
let metadata = CodeActionResolveMetadata(fromLSPDictionary: dict)
else {
return XCTFail("Expected resolve metadata in data")
}
XCTAssertEqual(metadata.textDocument, textDocument)
XCTAssertEqual(metadata.underlyingData, originalData)
}
func testCommandEncoding() throws {
let dictionary: LSPAny = ["1": [nil, 2], "2": "text", "3": ["4": [1, 2]]]
let array: LSPAny = [1, [2, "string"], dictionary]
let arguments: LSPAny = [1, 2.2, "text", nil, array, dictionary]
let command = Command(title: "Command", command: "command.id", arguments: [arguments, arguments])
let decoded = try JSONDecoder().decode(Command.self, from: JSONEncoder().encode(command))
XCTAssertEqual(decoded, command)
}
func testEmptyCodeActionResult() async throws {
let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport)
let uri = DocumentURI(for: .swift)
let positions = testClient.openDocument(
"""
func foo() -> String {
var a = "hello"
1️⃣ return a
}
""",
uri: uri
)
let request = CodeActionRequest(
range: positions["1️⃣"]..<positions["1️⃣"],
context: .init(),
textDocument: TextDocumentIdentifier(uri)
)
let result = try await testClient.send(request)
XCTAssertEqual(result, .codeActions([]))
}
func testSemanticRefactorLocalRenameResult() async throws {
let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport)
let uri = DocumentURI(for: .swift)
let positions = testClient.openDocument(
"""
func localRename() {
var 1️⃣local = 1
_ = local
}
""",
uri: uri
)
let request = CodeActionRequest(
range: Range(positions["1️⃣"]),
context: .init(),
textDocument: TextDocumentIdentifier(uri)
)
let result = try await testClient.send(request)
XCTAssertEqual(result?.codeActions?.map(\.title), ["Add documentation"])
}
func testSemanticRefactorLocationCodeActionResult() async throws {
let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport)
let uri = DocumentURI(for: .swift)
let positions = testClient.openDocument(
"""
func foo() -> 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..<endPosition,
context: .init(),
textDocument: TextDocumentIdentifier(uri)
)
let result = try await testClient.send(request)
let expectedCommandArgs: LSPAny = [
"actionString": "source.refactoring.kind.extract.function",
"positionRange": [
"start": [
"character": .int(startPosition.utf16index),
"line": .int(startPosition.line),
],
"end": [
"character": .int(endPosition.utf16index),
"line": .int(endPosition.line),
],
],
"title": "Extract Method",
"textDocument": ["uri": .string(uri.stringValue)],
]
let metadataArguments: LSPAny = ["sourcekitlsp_textDocument": ["uri": .string(uri.stringValue)]]
let expectedCommand = Command(
title: "Extract Method",
command: "semantic.refactor.command",
arguments: [expectedCommandArgs] + [metadataArguments]
)
let expectedCodeAction = CodeAction(
title: "Extract Method",
kind: .refactorExtract,
command: expectedCommand
)
var resultActions = try XCTUnwrap(result?.codeActions)
// Filter out "Add documentation"; we test it elsewhere
if let addDocIndex = resultActions.firstIndex(where: {
$0.title == "Add documentation"
}
) {
resultActions.remove(at: addDocIndex)
} else {
XCTFail("Missing 'Add documentation'.")
return
}
XCTAssertEqual(resultActions, [expectedCodeAction])
}
func testCodeActionsRemovePlaceholders() async throws {
let testClient = try await TestSourceKitLSPClient(
capabilities: clientCapabilitiesWithCodeActionSupport,
usePullDiagnostics: false
)
let uri = DocumentURI(for: .swift)
let positions = testClient.openDocument(
"""
protocol MyProto {
func foo()
}
struct 1️⃣MyStruct: MyProto {
}
""",
uri: uri
)
let diags = try await testClient.nextDiagnosticsNotification()
XCTAssertEqual(diags.uri, uri)
XCTAssertEqual(diags.diagnostics.count, 1)
let diagPosition = try XCTUnwrap(diags.diagnostics.only?.range.lowerBound)
let quickFixActionResult = try await testClient.send(
CodeActionRequest(
range: Range(diagPosition),
context: .init(diagnostics: diags.diagnostics),
textDocument: TextDocumentIdentifier(uri)
)
)
// Check that the Fix-It action contains snippets
guard let quickFixAction = quickFixActionResult?.codeActions?.filter({ $0.kind == .quickFix }).only else {
return XCTFail("Expected exactly one quick fix action")
}
guard let change = quickFixAction.edit?.changes?[uri]?.only else {
return XCTFail("Expected exactly one change")
}
XCTAssertEqual(
change.newText.trimmingTrailingWhitespace(),
"""
func foo() {
}
"""
)
// Check that the refactor action contains snippets
let refactorActionResult = try await testClient.send(
CodeActionRequest(
range: Range(positions["1️⃣"]),
context: .init(diagnostics: diags.diagnostics),
textDocument: TextDocumentIdentifier(uri)
)
)
guard let refactorAction = refactorActionResult?.codeActions?.filter({ $0.kind == .refactor }).only else {
return XCTFail("Expected exactly one refactor action")
}
guard let command = refactorAction.command else {
return XCTFail("Expected the refactor action to have a command")
}
let editReceived = self.expectation(description: "Received ApplyEdit request")
testClient.handleSingleRequest { (request: ApplyEditRequest) -> 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️⃣"]..<positions["2️⃣"], newText: "Multiident"),
TextEdit(range: positions["3️⃣"]..<positions["4️⃣"], newText: ""),
]
]
)
),
CodeAction(
title: "Join the identifiers together with camel-case",
kind: .quickFix,
edit: WorkspaceEdit(
changes: [
uri: [
TextEdit(range: positions["1️⃣"]..<positions["2️⃣"], newText: "MultiIdent"),
TextEdit(range: positions["3️⃣"]..<positions["4️⃣"], newText: ""),
]
]
)
),
]
XCTAssertEqual(expectedCodeActions, codeActions)
}
func testPackageManifestEditingCodeActionResult() async throws {
let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport)
let uri = DocumentURI(for: .swift)
let positions = testClient.openDocument(
"""
// swift-tools-version: 5.5
let package = Package(
name: "packages",
targets: [
.tar1️⃣get(name: "MyLib"),
]
)
""",
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 codeActions = try XCTUnwrap(result?.codeActions)
// Make sure we get the expected package manifest editing actions.
let addTestAction = codeActions.first { action in
return action.title == "Add test target (Swift Testing)"
}
XCTAssertNotNil(addTestAction)
XCTAssertTrue(
codeActions.contains { action in
action.title == "Add library target"
}
)
guard let addTestChanges = addTestAction?.edit?.changes else {
XCTFail("Didn't have changes in the 'Add test target (Swift Testing)' action")
return
}
guard let manifestEdits = addTestChanges[uri] else {
XCTFail("Didn't have edits")
return
}
XCTAssertTrue(
manifestEdits.contains { edit in
edit.newText.contains("testTarget")
}
)
XCTAssertTrue(
codeActions.contains { action in
return action.title == "Add product to export this target"
}
)
}
func testPackageManifestEditingCodeActionNoTestResult() async throws {
let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport)
let uri = DocumentURI(for: .swift)
let positions = testClient.openDocument(
"""
// swift-tools-version: 5.5
let package = Package(
name: "packages",
targets: [
.testTar1️⃣get(name: "MyLib"),
]
)
""",
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 codeActions = try XCTUnwrap(result?.codeActions)
// Make sure we get the expected package manifest editing actions.
XCTAssertTrue(
!codeActions.contains { action in
return action.title == "Add test target"
}
)
XCTAssertTrue(
!codeActions.contains { action in
return action.title == "Add product to export this target"
}
)
}
func testConvertIntegerLiteral() async throws {
try await assertCodeActions(
"""
let x = 1️⃣12️⃣63️⃣
""",
ranges: [("1️⃣", "2️⃣"), ("1️⃣", "3️⃣")],
ignoringCodeActions: ["Convert to computed property"]
) { uri, positions in
[
CodeAction(
title: "Convert 16 to 0b10000",
kind: .refactorInline,
diagnostics: nil,
edit: WorkspaceEdit(
changes: [uri: [TextEdit(range: positions["1️⃣"]..<positions["3️⃣"], newText: "0b10000")]]
)
),
CodeAction(
title: "Convert 16 to 0o20",
kind: .refactorInline,
diagnostics: nil,
edit: WorkspaceEdit(
changes: [uri: [TextEdit(range: positions["1️⃣"]..<positions["3️⃣"], newText: "0o20")]]
)
),
CodeAction(
title: "Convert 16 to 0x10",
kind: .refactorInline,
diagnostics: nil,
edit: WorkspaceEdit(
changes: [uri: [TextEdit(range: positions["1️⃣"]..<positions["3️⃣"], newText: "0x10")]]
)
),
]
}
}
func testAddSeparatorsToIntegerLiteral() async throws {
try await assertCodeActions(
"""
let x = 1️⃣10002️⃣
""",
markers: ["1️⃣"],
ignoringCodeActions: ["Convert to computed property"]
) { uri, positions in
[
CodeAction(
title: "Add digit separators",
kind: .refactorInline,
diagnostics: nil,
edit: WorkspaceEdit(
changes: [uri: [TextEdit(range: positions["1️⃣"]..<positions["2️⃣"], newText: "1_000")]]
)
),
CodeAction(
title: "Convert 1000 to 0b1111101000",
kind: .refactorInline,
diagnostics: nil,
edit: WorkspaceEdit(
changes: [uri: [TextEdit(range: positions["1️⃣"]..<positions["2️⃣"], newText: "0b1111101000")]]
)
),
CodeAction(
title: "Convert 1000 to 0o1750",
kind: .refactorInline,
diagnostics: nil,
edit: WorkspaceEdit(
changes: [uri: [TextEdit(range: positions["1️⃣"]..<positions["2️⃣"], newText: "0o1750")]]
)
),
CodeAction(
title: "Convert 1000 to 0x3e8",
kind: .refactorInline,
diagnostics: nil,
edit: WorkspaceEdit(
changes: [uri: [TextEdit(range: positions["1️⃣"]..<positions["2️⃣"], newText: "0x3e8")]]
)
),
]
}
}
func testAddExplicitEnumRawValuesInt() async throws {
try await assertCodeActions(
"""
1️⃣enum Status: Int {
case active2️⃣
case inactive3️⃣
case pending = 10
case archived4️⃣
}
""",
markers: ["1️⃣"],
exhaustive: false
) { uri, positions in
[
CodeAction(
title: "Add Explicit Raw Values",
kind: .refactorInline,
diagnostics: nil,
edit: WorkspaceEdit(
changes: [
uri: [
TextEdit(range: positions["2️⃣"]..<positions["2️⃣"], newText: " = 0"),
TextEdit(range: positions["3️⃣"]..<positions["3️⃣"], newText: " = 1"),
TextEdit(range: positions["4️⃣"]..<positions["4️⃣"], newText: " = 11"),
]
]
)
)
]
}
}
func testAddExplicitEnumRawValuesString() async throws {
try await assertCodeActions(
"""
1️⃣enum Direction: String {
case north2️⃣
case south = "down"
case east3️⃣
}
""",
markers: ["1️⃣"],
exhaustive: false
) { uri, positions in
[
CodeAction(
title: "Add Explicit Raw Values",
kind: .refactorInline,
diagnostics: nil,
edit: WorkspaceEdit(
changes: [
uri: [
TextEdit(range: positions["2️⃣"]..<positions["2️⃣"], newText: " = \"north\""),
TextEdit(range: positions["3️⃣"]..<positions["3️⃣"], newText: " = \"east\""),
]
]
)
)
]
}
}
func testAddExplicitEnumRawValuesContinuesAfterNegativeLiteral() async throws {
try await assertCodeActions(
"""
1️⃣enum Offset: Int {
case low = -5
case medium2️⃣
case high3️⃣
}
""",
markers: ["1️⃣"],
exhaustive: false
) { uri, positions in
[
CodeAction(
title: "Add Explicit Raw Values",
kind: .refactorInline,
diagnostics: nil,
edit: WorkspaceEdit(
changes: [
uri: [
TextEdit(range: positions["2️⃣"]..<positions["2️⃣"], newText: " = -4"),
TextEdit(range: positions["3️⃣"]..<positions["3️⃣"], newText: " = -3"),
]
]
)
)
]
}
}
func testAddExplicitEnumRawValuesContinuesAfterHexLiteral() async throws {
try await assertCodeActions(
"""
1️⃣enum Mask: Int {
case bit0 = 0x10
case bit12️⃣
}
""",
markers: ["1️⃣"],
exhaustive: false
) { uri, positions in
[
CodeAction(
title: "Add Explicit Raw Values",
kind: .refactorInline,
diagnostics: nil,
edit: WorkspaceEdit(
changes: [
uri: [
TextEdit(range: positions["2️⃣"]..<positions["2️⃣"], newText: " = 17")
]
]
)
)
]
}
}
func testAddExplicitEnumRawValuesNotOfferedWhenAllCasesExplicit() async throws {
try await assertNoCodeAction(
titled: "Add Explicit Raw Values",
in: """
1️⃣enum Status: Int {
case active = 0
case inactive = 1
}
""",
atMarker: "1️⃣"
)
}
func testAddExplicitEnumRawValuesNotOfferedWithoutRawValueType() async throws {
try await assertNoCodeAction(
titled: "Add Explicit Raw Values",
in: """
1️⃣enum Direction {
case north
case south
}
""",
atMarker: "1️⃣"
)
}
func testAddExplicitEnumRawValuesNotOfferedWithUnsupportedRawValue() async throws {
try await assertNoCodeAction(
titled: "Add Explicit Raw Values",
in: """
let base = 10
1️⃣enum Tier: Int {
case low = base
case high
}
""",
atMarker: "1️⃣"
)
}
func testAddExplicitEnumRawValuesStripsBackticksInImplicitStringRawValue() async throws {
try await assertCodeActions(
"""
1️⃣enum Keyword: String {
case `default`2️⃣
case ifBranch = "if"
case `class`3️⃣
}
""",
markers: ["1️⃣"],
exhaustive: false
) { uri, positions in
[
CodeAction(
title: "Add Explicit Raw Values",
kind: .refactorInline,
diagnostics: nil,
edit: WorkspaceEdit(
changes: [
uri: [
TextEdit(range: positions["2️⃣"]..<positions["2️⃣"], newText: " = \"default\""),
TextEdit(range: positions["3️⃣"]..<positions["3️⃣"], newText: " = \"class\""),
]
]
)
)
]
}
}
func testAddExplicitEnumRawValuesNotOfferedWhenRawValueTypeIsNotFirst() async throws {
// Per Swift's grammar, only the first inherited type may specify a raw
// value. `: CaseIterable, Int` is invalid Swift, so the code action must
// not be offered.
try await assertNoCodeAction(
titled: "Add Explicit Raw Values",
in: """
1️⃣enum Status: CaseIterable, Int {
case active
case inactive
}
""",
atMarker: "1️⃣"
)
}
func testAddExplicitEnumRawValuesNotOfferedWithIfConfigMembers() async throws {
// `#if` blocks change which cases are active in a given configuration, so
// the implicit raw value counter cannot be computed purely syntactically.
try await assertNoCodeAction(
titled: "Add Explicit Raw Values",
in: """
1️⃣enum Mode: Int {
case a
#if DEBUG
case b
#endif
case c
}
""",
atMarker: "1️⃣"
)
}
func testAddExplicitEnumRawValuesNotOfferedWithFreestandingMacro() async throws {
// Freestanding declaration macros can expand to enum cases, so the counter
// cannot be computed without expanding the macro.
try await assertNoCodeAction(
titled: "Add Explicit Raw Values",
in: """
1️⃣enum Mode: Int {
case a
#generatedCases()
case b
}
""",
atMarker: "1️⃣"
)
}
func testAddExplicitEnumRawValuesNotOfferedWithAssociatedValues() async throws {
// Cases with associated values cannot have raw values. Mixing them inside
// a raw-value enum is invalid Swift; the refactor should bail rather than
// produce broken output like `case payload = 0(String)`.
try await assertNoCodeAction(
titled: "Add Explicit Raw Values",
in: """
1️⃣enum Event: Int {
case empty
case payload(String)
}
""",
atMarker: "1️⃣"
)
}
func testAddExplicitEnumRawValuesUInt() async throws {
try await assertCodeActions(
"""
1️⃣enum Mask: UInt {
case bit02️⃣
case bit13️⃣
case bit2 = 4
case bit34️⃣
}
""",
markers: ["1️⃣"],
exhaustive: false
) { uri, positions in
[
CodeAction(
title: "Add Explicit Raw Values",
kind: .refactorInline,
diagnostics: nil,
edit: WorkspaceEdit(
changes: [
uri: [
TextEdit(range: positions["2️⃣"]..<positions["2️⃣"], newText: " = 0"),
TextEdit(range: positions["3️⃣"]..<positions["3️⃣"], newText: " = 1"),
TextEdit(range: positions["4️⃣"]..<positions["4️⃣"], newText: " = 5"),
]
]
)
)
]
}
}
func testAddExplicitEnumRawValuesNotOfferedForUnsignedEnumWithNegativeRawValue() async throws {
// `case = -1` is invalid Swift for an unsigned-integer raw type, so the
// refactor must not insert further values based on it.
try await assertNoCodeAction(
titled: "Add Explicit Raw Values",
in: """
1️⃣enum Mask: UInt {
case a = -1
case b
}
""",
atMarker: "1️⃣"
)
}
func testAddExplicitEnumRawValuesNotOfferedWhenEnumHasAttribute() async throws {
// Attached attribute macros can introduce members at expansion time, so
// the refactor bails whenever the enum has any attributes.
try await assertNoCodeAction(
titled: "Add Explicit Raw Values",
in: """
@available(*, deprecated)
1️⃣enum Status: Int {
case active
case inactive
}
""",
atMarker: "1️⃣"
)
}
func testFormatRawStringLiteral() async throws {
try await assertCodeActions(
"""
let x = 1️⃣#"Hello 2️⃣world"#3️⃣
""",
ranges: [("1️⃣", "3️⃣")],
exhaustive: false
) { uri, positions in
[
CodeAction(
title: "Convert string literal to minimal number of \'#\'s",
kind: .refactorInline,
diagnostics: nil,
edit: WorkspaceEdit(
changes: [uri: [TextEdit(range: positions["1️⃣"]..<positions["3️⃣"], newText: #""Hello world""#)]]
)
)
]
}
}
func testFormatRawStringLiteralFromInterpolation() async throws {
try await assertCodeActions(
##"""
let x = 1️⃣#"Hello 2️⃣\#(name)"#3️⃣
"""##,
ranges: [("1️⃣", "3️⃣")],
exhaustive: false
) { uri, positions in
[
CodeAction(
title: "Convert string literal to minimal number of \'#\'s",
kind: .refactorInline,
diagnostics: nil,
edit: WorkspaceEdit(
changes: [
uri: [
TextEdit(
range: positions["1️⃣"]..<positions["3️⃣"],
newText: ##"""
##"Hello \#(name)"##
"""##
)
]
]
)
)
]
}
}
func testFormatRawStringLiteralDoesNotShowUpWhenInvokedFromInsideInterpolationSegment() async throws {
try await assertCodeActions(
##"""
let x = #"Hello \#(n1ame)"#
"""##,
ignoringCodeActions: ["Convert to computed property"]
) { uri, positions in
[]
}
}
func testMigrateIfLetSyntax() async throws {
try await assertCodeActions(
##"""
1️⃣if 2️⃣let 3️⃣foo = 4️⃣foo {}5️⃣
"""##,
markers: ["1️⃣", "2️⃣", "3️⃣", "4️⃣"],
ranges: [("1️⃣", "4️⃣"), ("1️⃣", "5️⃣")]
) { uri, positions in
[
CodeAction(
title: "Migrate to shorthand 'if let' syntax",
kind: .refactorInline,
diagnostics: nil,
edit: WorkspaceEdit(
changes: [
uri: [
TextEdit(
range: positions["1️⃣"]..<positions["5️⃣"],
newText: "if let foo {}"
)
]
]
)
)
]
}
}
func testMigrateIfLetSyntaxDoesNotShowUpWhenInvokedFromInsideTheBody() async throws {
try await assertCodeActions(
##"""
if let foo = foo 1️⃣{
2️⃣print(foo)
3️⃣}4️⃣
"""##
) { uri, positions in
[]
}
}
func testOpaqueParameterToGeneric() async throws {
try await assertCodeActions(
##"""
1️⃣func 2️⃣someFunction(_ 3️⃣input: some4️⃣ Value) {}5️⃣
"""##,
markers: ["1️⃣", "2️⃣", "3️⃣", "4️⃣"],
ranges: [("1️⃣", "2️⃣"), ("1️⃣", "5️⃣")],
exhaustive: false
) { uri, positions in
[
CodeAction(
title: "Expand 'some' parameters to generic parameters",
kind: .refactorInline,
diagnostics: nil,
edit: WorkspaceEdit(
changes: [
uri: [
TextEdit(
range: positions["1️⃣"]..<positions["5️⃣"],
newText: "func someFunction<T1: Value>(_ input: T1) {}"
)
]
]
)
)
]
}
}
func testOpaqueParameterToGenericIsNotShownFromTheBody() async throws {
try await assertCodeActions(
##"""
func someFunction(_ input: some Value) 1️⃣{
2️⃣print("x")
}3️⃣
"""##,
ignoringCodeActions: ["Add documentation"]
) { 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️⃣"]..<positions["5️⃣"],
newText: """
struct JSONValue: Codable {
var id: Double
var values: [String]
}
"""
)
]
]
)
)
]
}
}
func testAddDocumentationRefactorNotAtStartOfFile() async throws {
try await assertCodeActions(
"""
struct Foo {
1️⃣func 2️⃣refactor(3️⃣syntax: 4️⃣Decl5️⃣Syntax)6️⃣ { }7️⃣
}
""",
ranges: [("1️⃣", "2️⃣"), ("1️⃣", "6️⃣"), ("1️⃣", "7️⃣")],
exhaustive: false
) { uri, positions in
[
CodeAction(
title: "Add documentation",
kind: .refactorInline,
diagnostics: nil,
edit: WorkspaceEdit(
changes: [
uri: [
TextEdit(
range: Range(positions["1️⃣"]),
newText: """
/// A description
/// - Parameter syntax:
\("")
"""
)
]
]
)
)
]
}
}
func testAddDocumentationRefactorAtStartOfFile() async throws {
try await assertCodeActions(
"""
1️⃣func 2️⃣refactor(3️⃣syntax: 4️⃣Decl5️⃣Syntax)6️⃣ { }7️⃣
""",
ranges: [("1️⃣", "2️⃣"), ("1️⃣", "6️⃣"), ("1️⃣", "7️⃣")],
exhaustive: false
) { uri, positions in
[
CodeAction(
title: "Add documentation",
kind: .refactorInline,
diagnostics: nil,
edit: WorkspaceEdit(
changes: [
uri: [
TextEdit(
range: Range(positions["1️⃣"]),
newText: """
/// A description
/// - Parameter syntax:
\("")
"""
)
]
]
)
)
]
}
}
func testAddDocumentationDoesNotShowUpIfItIsNotOnItsOwnLine() async throws {
try await assertCodeActions(
"""
var x = 1; var 1️⃣y = 2
""",
ignoringCodeActions: ["Convert to computed property"]
) { uri, positions in
[]
}
}
func testConvertStringConcatenationToStringInterpolation() async throws {
try await assertCodeActions(
#"""
0️⃣
1️⃣/*leading*/ #"["# + 2️⃣key + ": \(3️⃣d) " + 4️⃣value + ##"]"## /*trailing*/5️⃣
"""#,
markers: ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣"],
ranges: [("1️⃣", "2️⃣"), ("3️⃣", "4️⃣"), ("1️⃣", "5️⃣")],
exhaustive: false
) { uri, positions in
[
CodeAction(
title: "Convert String Concatenation to String Interpolation",
kind: .refactorInline,
edit: WorkspaceEdit(
changes: [
uri: [
TextEdit(
range: positions["0️⃣"]..<positions["5️⃣"],
newText: ###"""
/*leading*/ ##"[\##(key): \##(d) \##(value)]"## /*trailing*/
"""###
)
]
]
)
)
]
}
}
func testConvertStringConcatenationToStringInterpolationWithInterspersingAndMultilineComments() async throws {
try await assertCodeActions(
"""
1️⃣"hello" + /*self.leading1*/ /**self.leading2*/ self //self.trailing1
///concat.leading1
2️⃣+/*concat.trailing1
line 1
line 2
line 3
*/ value3️⃣
""",
exhaustive: false
) { uri, positions in
[
CodeAction(
title: "Convert String Concatenation to String Interpolation",
kind: .refactorInline,
edit: WorkspaceEdit(
changes: [
uri: [
TextEdit(
range: positions["1️⃣"]..<positions["3️⃣"],
newText: #"""
"hello\(/*self.leading1*/ /**self.leading2*/ self /*self.trailing1*/ /**concat.leading1*/)\(/*concat.trailing1 line 1 line 2 line 3 */ value)"
"""#
)
]
]
)
)
]
}
}
func testConvertStringConcatenationToStringInterpolationNotShowUpMissingExpr() async throws {
try await assertCodeActions(
###"""
1️⃣"Hello" + 2️⃣
"""###,
ranges: [("1️⃣", "2️⃣")]
) { uri, positions in
[]
}
}
func testConvertStringConcatenationToStringInterpolationNotShowUpOnlyOneStringLiteral() async throws {
try await assertCodeActions(
###"""
1️⃣"[\(2️⃣key): \(3️⃣d) 4️⃣\(value)]"5️⃣
"""###,
ranges: [("1️⃣", "2️⃣"), ("3️⃣", "4️⃣"), ("1️⃣", "5️⃣")]
) { uri, positions in
[]
}
}
func testConvertStringConcatenationToStringInterpolationMultilineStringLiteral() async throws {
try await assertCodeActions(
###"""
1️⃣"""
I
am
a
""" + value +
"""
multi-line
string
literal
"""2️⃣
"""###,
exhaustive: false
) { uri, positions in
[
CodeAction(
title: "Convert String Concatenation to String Interpolation",
kind: .refactorInline,
edit: WorkspaceEdit(
changes: [
uri: [
TextEdit(
range: positions["1️⃣"]..<positions["2️⃣"],
newText: #"""
"""
I
am
a\(value)multi-line
string
literal
"""
"""#
)
]
]
)
)
]
}
}
func testConvertStringConcatenationToStringInterpolationDifferentIndentation() async throws {
try await assertCodeActions(
###"""
1️⃣"""
a
b
""" + """
c
d
"""2️⃣
"""###,
exhaustive: false
) { uri, positions in
[
CodeAction(
title: "Convert String Concatenation to String Interpolation",
kind: .refactorInline,
edit: WorkspaceEdit(
changes: [
uri: [
TextEdit(
range: positions["1️⃣"]..<positions["2️⃣"],
newText: #"""
"""
a
bc
d
"""
"""#
)
]
]
)
)
]
}
}
func testConvertStringConcatenationMixedMultilineAndSingleLine() async throws {
// multiline + single-line + multiline
try await assertCodeActions(
###"""
1️⃣"""
a
b
""" + "c" + """
d
"""2️⃣
"""###,
exhaustive: false
) { uri, positions in
[
CodeAction(
title: "Convert String Concatenation to String Interpolation",
kind: .refactorInline,
edit: WorkspaceEdit(
changes: [
uri: [
TextEdit(
range: positions["1️⃣"]..<positions["2️⃣"],
newText: #"""
"""
a
bcd
"""
"""#
)
]
]
)
)
]
}
}
func testConvertStringConcatenationMultilinePlusSingleLine() async throws {
// multiline + single-line ending
try await assertCodeActions(
###"""
1️⃣"""
a
b
""" + "c"2️⃣
"""###,
exhaustive: false
) { uri, positions in
[
CodeAction(
title: "Convert String Concatenation to String Interpolation",
kind: .refactorInline,
edit: WorkspaceEdit(
changes: [
uri: [
TextEdit(
range: positions["1️⃣"]..<positions["2️⃣"],
newText: #"""
"""
a
bc
"""
"""#
)
]
]
)
)
]
}
}
func testApplyDeMorganLawNegatedAnd() async throws {
try await assertCodeActions(
"""
let x = 1️⃣!(a && b)2️⃣
""",
ranges: [("1️⃣", "2️⃣")],
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["1️⃣"]..<positions["2️⃣"],
newText: "(!a || !b)"
)
]
]
)
)
]
}
}
func testApplyDeMorganLawNegatedOr() throws {
try assertDeMorganTransform(
input: "!(a || b)",
expected: "(!a && !b)"
)
}
func testApplyDeMorganLawDoubleNegation() throws {
try assertDeMorganTransform(
input: "!(!a && !b)",
expected: "(a || b)"
)
}
func testApplyDeMorganLawComparisonFlip() throws {
try assertDeMorganTransform(
input: "!(a < b)",
expected: "(a >= 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️⃣"]..<positions["5️⃣"],
newText: "(!a || !b) "
)
]
]
)
),
CodeAction(
title: "Apply De Morgan's law, converting '!(a && b) || c' to '!((a && b) && !c)'",
kind: .refactorInline,
edit: WorkspaceEdit(
changes: [
uri: [
TextEdit(
range: positions["3️⃣"]..<positions["6️⃣"],
newText: "!((a && b) && !c)"
)
]
]
)
),
CodeAction(
title: "Apply De Morgan's law, converting '(!(a && b) || c)' to '!((a && b) && !c)'",
kind: .refactorInline,
edit: WorkspaceEdit(
changes: [
uri: [
TextEdit(
range: positions["2️⃣"]..<positions["7️⃣"],
newText: "!((a && b) && !c)"
)
]
]
)
),
CodeAction(
title: "Apply De Morgan's law, converting '!(!(a && b) || c)' to '((a && b) && !c)'",
kind: .refactorInline,
edit: WorkspaceEdit(
changes: [
uri: [
TextEdit(
range: positions["1️⃣"]..<positions["7️⃣"],
newText: "((a && b) && !c)"
)
]
]
)
),
]
}
}
func testRemoveUnusedImports() async throws {
let project = try await SwiftPMTestProject(
files: [
"LibA/LibA.swift": "",
"LibB/LibB.swift": "",
"Test/Test.swift": """
// Some file header
// over multiple lines
1️⃣import LibA // LibA implements A
2️⃣import Foundation3️⃣
// LibB implements B
import LibB4️⃣
#warning("Removing imports should work despite warning")
5️⃣func test(x: Date) {}
""",
],
manifest: """
let package = Package(
name: "MyLibrary",
targets: [
.target(name: "LibA"),
.target(name: "LibB"),
.target(
name: "Test",
dependencies: ["LibA", "LibB"],
swiftSettings: [.enableUpcomingFeature("MemberImportVisibility")]
)
]
)
""",
capabilities: clientCapabilitiesWithCodeActionSupport,
enableBackgroundIndexing: true
)
let (uri, positions) = try project.openDocument("Test.swift")
let functionResult = try await project.testClient.send(
CodeActionRequest(
range: Range(positions["5️⃣"]),
context: CodeActionContext(),
textDocument: TextDocumentIdentifier(uri)
)
)
XCTAssertFalse(
try XCTUnwrap(functionResult?.codeActions).contains(where: {
$0.command?.command == RemoveUnusedImportsCommand.identifier
})
)
let importResult = try await project.testClient.send(
CodeActionRequest(
range: Range(positions["1️⃣"]),
context: CodeActionContext(),
textDocument: TextDocumentIdentifier(uri)
)
)
let removeUnusedImportsCommand = try XCTUnwrap(
importResult?.codeActions?.first(where: {
$0.command?.command == "remove.unused.imports.command"
})?.command
)
project.testClient.handleSingleRequest { (request: ApplyEditRequest) -> ApplyEditResponse in
XCTAssertEqual(
request.edit.changes,
[
uri: [
TextEdit(range: positions["1️⃣"]..<positions["2️⃣"], newText: ""),
TextEdit(range: positions["3️⃣"]..<positions["4️⃣"], newText: ""),
]
]
)
return ApplyEditResponse(applied: true, failureReason: nil)
}
_ = try await project.testClient.send(
ExecuteCommandRequest(
command: removeUnusedImportsCommand.command,
arguments: removeUnusedImportsCommand.arguments
)
)
}
func testRemoveUnusedImportsNotAvailableIfSourceFileHasError() async throws {
let project = try await SwiftPMTestProject(
files: [
"Test.swift": """
1️⃣import Foundation
#error("Some error")
"""
],
manifest: """
let package = Package(
name: "MyLibrary",
targets: [
.target(name: "Test", swiftSettings: [.enableUpcomingFeature("MemberImportVisibility")]c)
]
)
""",
capabilities: clientCapabilitiesWithCodeActionSupport,
enableBackgroundIndexing: true
)
let (uri, positions) = try project.openDocument("Test.swift")
let result = try await project.testClient.send(
CodeActionRequest(
range: Range(positions["1️⃣"]),
context: CodeActionContext(),
textDocument: TextDocumentIdentifier(uri)
)
)
XCTAssertFalse(
try XCTUnwrap(result?.codeActions).contains(where: {
$0.command?.command == RemoveUnusedImportsCommand.identifier
})
)
}
func testConvertFunctionZeroParameterToComputedProperty() async throws {
let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport)
let uri = DocumentURI(for: .swift)
let positions = testClient.openDocument(
"""
1️⃣func someFunction() -> String2️⃣ { return "" }3️⃣
""",
uri: uri
)
let request = CodeActionRequest(
range: positions["1️⃣"]..<positions["2️⃣"],
context: .init(),
textDocument: TextDocumentIdentifier(uri)
)
let result = try await testClient.send(request)
guard case .codeActions(let codeActions) = result else {
XCTFail("Expected code actions")
return
}
let expectedCodeAction = CodeAction(
title: "Convert to computed property",
kind: .refactorInline,
diagnostics: nil,
edit: WorkspaceEdit(
changes: [
uri: [
TextEdit(
range: positions["1️⃣"]..<positions["3️⃣"],
newText: """
var someFunction: String { return "" }
"""
)
]
]
)
)
XCTAssertTrue(codeActions.contains(expectedCodeAction))
}
func testConvertZeroParameterFunctionToComputedPropertyIsNotShownFromTheBody() async throws {
try await assertCodeActions(
##"""
func someFunction() -> String 1️⃣{
2️⃣return ""
}3️⃣
"""##,
ignoringCodeActions: ["Add documentation"]
) { uri, positions in
[]
}
}
func testConvertZeroParameterFunctionToComputedPropertyNotOfferedForImplicitVoid() async throws {
try await assertCodeActions(
"""
1️⃣func test()2️⃣ { }3️⃣
""",
ranges: [("1️⃣", "2️⃣")],
ignoringCodeActions: ["Add documentation"]
) { _, _ in
[]
}
}
func testConvertZeroParameterFunctionToComputedPropertyNotOfferedForExplicitVoid() async throws {
try await assertCodeActions(
"""
1️⃣func test() -> Void2️⃣ { }3️⃣
""",
ranges: [("1️⃣", "2️⃣")],
ignoringCodeActions: ["Add documentation"]
) { _, _ in
[]
}
}
func testConvertZeroParameterFunctionToComputedPropertyNotOfferedForEmptyTuple() async throws {
try await assertCodeActions(
"""
1️⃣func test() -> ()2️⃣ { }3️⃣
""",
ranges: [("1️⃣", "2️⃣")],
ignoringCodeActions: ["Add documentation"]
) { _, _ 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️⃣"]..<positions["2️⃣"],
context: .init(),
textDocument: TextDocumentIdentifier(uri)
)
let result = try await testClient.send(request)
guard case .codeActions(let codeActions) = result else {
XCTFail("Expected code actions")
return
}
let expectedCodeAction = CodeAction(
title: "Convert to zero parameter function",
kind: .refactorInline,
diagnostics: nil,
edit: WorkspaceEdit(
changes: [
uri: [
TextEdit(
range: positions["1️⃣"]..<positions["3️⃣"],
newText: """
func someFunction() -> String { return "" }
"""
)
]
]
)
)
XCTAssertTrue(codeActions.contains(expectedCodeAction))
}
func testConvertComputedPropertyToZeroParameterFunctionIsNotShownFromTheBody() async throws {
try await assertCodeActions(
##"""
var someFunction: String {
1️⃣return ""
}
"""##
) { uri, positions in
[]
}
}
func testConvertComputeToStoredProperty() async throws {
try await assertCodeActions(
##"""
1️⃣var 2️⃣foo: String { "abc" }3️⃣
"""##,
markers: ["2️⃣"],
ignoringCodeActions: ["Add documentation", "Convert to zero parameter function"]
) { uri, positions in
[
CodeAction(
title: "Convert to stored property",
kind: .refactorInline,
edit: WorkspaceEdit(
changes: [
uri: [
TextEdit(
range: positions["1️⃣"]..<positions["3️⃣"],
newText: """
let foo: String = "abc"
"""
)
]
]
)
)
]
}
}
func testConvertLineCommentToDocComment() async throws {
try await assertCodeActions(
"""
1️⃣// A comment
func foo() {}2️⃣
""",
markers: ["1️⃣"],
exhaustive: false
) { uri, positions in
[
CodeAction(
title: "Convert Comment to Doc Comment",
kind: .refactorInline,
edit: WorkspaceEdit(
changes: [
uri: [
TextEdit(
range: positions["1️⃣"]..<positions["2️⃣"],
newText: """
/// A comment
func foo() {}
"""
)
]
]
)
)
]
}
}
func testConvertBlockCommentToDocComment() async throws {
try await assertCodeActions(
"""
1️⃣/* A block comment */
func foo() {}2️⃣
""",
markers: ["1️⃣"],
exhaustive: false
) { uri, positions in
[
CodeAction(
title: "Convert Comment to Doc Comment",
kind: .refactorInline,
edit: WorkspaceEdit(
changes: [
uri: [
TextEdit(
range: positions["1️⃣"]..<positions["2️⃣"],
newText: """
/** A block comment */
func foo() {}
"""
)
]
]
)
)
]
}
}
func testConvertMultipleLineCommentsToDocComments() async throws {
try await assertCodeActions(
"""
1️⃣// First line
// Second line
func foo() {}2️⃣
""",
markers: ["1️⃣"],
exhaustive: false
) { uri, positions in
[
CodeAction(
title: "Convert Comment to Doc Comment",
kind: .refactorInline,
edit: WorkspaceEdit(
changes: [
uri: [
TextEdit(
range: positions["1️⃣"]..<positions["2️⃣"],
newText: """
/// First line
/// Second line
func foo() {}
"""
)
]
]
)
)
]
}
}
func testConvertCommentToDocCommentNotShownOnFunctionBody() async throws {
try await assertCodeActions(
"""
// A comment
func foo() {
1️⃣print("hello")
}
"""
) { 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.
/// - ignoringCodeActions: Ignore code actions with this title for exhaustiveness checking
/// - 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)] = [],
ignoringCodeActions: [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]..<positions[endMarker],
context: .init(),
textDocument: TextDocumentIdentifier(uri)
)
)
let codeActions = try XCTUnwrap(result?.codeActions, file: file, line: line).filter {
!ignoringCodeActions.contains($0.title)
}
let expected = expected(uri, positions)
if exhaustive {
XCTAssertEqual(
codeActions,
expected,
"Found unexpected code actions at range \(startMarker)-\(endMarker)",
file: file,
line: line
)
} else {
if expected.isEmpty {
XCTFail(
"exhaustive: false is incompatible with an empty list of expected code actions",
file: file,
line: line
)
}
XCTAssert(
Set(codeActions).isSuperset(of: expected),
"""
Code actions did not contain expected at range \(startMarker)-\(endMarker):
\(codeActions)
""",
file: file,
line: line
)
}
}
}
/// Assert that no code action with the given title is offered at the given marker.
private func assertNoCodeAction(
titled title: String,
in markedText: String,
atMarker marker: String,
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)
let position = positions[marker]
let result = try await testClient.send(
CodeActionRequest(
range: position..<position,
context: .init(),
textDocument: TextDocumentIdentifier(uri)
)
)
let codeActions = result?.codeActions ?? []
XCTAssertFalse(
codeActions.contains(where: { $0.title == title }),
"did not expect code action titled '\(title)' at marker \(marker), got: \(codeActions.map(\.title))",
file: file,
line: line
)
}
}
private extension CodeActionRequestResponse {
var codeActions: [CodeAction]? {
guard case .codeActions(let actions) = self else {
return nil
}
return actions
}
}
/// Tests De Morgan transformation directly without LSP overhead.
private func assertDeMorganTransform(
input: String,
expected: String,
file: StaticString = #filePath,
line: UInt = #line
) throws {
let expr = ExprSyntax("\(raw: input)")
let transformer = DeMorganTransformer()
guard let result = transformer.computeComplement(of: expr) else {
XCTFail("Failed to compute De Morgan complement", file: file, line: line)
return
}
XCTAssertEqual(result.description, expected, file: file, line: line)
}