mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-10-01 16:56:43 +02:00
fixes: https://github.com/swiftlang/sourcekit-lsp/issues/1569 mostly works but not sure what to do with many edge cases and has a todo for switch statements, also this will probably have conflicts with https://github.com/swiftlang/sourcekit-lsp/pull/2406 marking as draft till that merges and i can resolve the conflicts. https://github.com/user-attachments/assets/a6d07f9d-6f09-4330-8cd0-2d24bd6973fb --------- Signed-off-by: Karan <karanlokchandani@protonmail.com>
641 lines
17 KiB
Swift
641 lines
17 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// 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
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import LanguageServerProtocol
|
|
import SKTestSupport
|
|
import SKUtilities
|
|
import SourceKitLSP
|
|
@_spi(Testing) import SwiftLanguageService
|
|
import SwiftParser
|
|
import SwiftSyntax
|
|
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()
|
|
codeActionCapabilities.codeActionLiteralSupport = .init(
|
|
codeActionKind: .init(valueSet: [.refactorInline])
|
|
)
|
|
documentCapabilities.codeAction = codeActionCapabilities
|
|
documentCapabilities.completion = .init(completionItem: .init(snippetSupport: true))
|
|
return ClientCapabilities(workspace: nil, textDocument: documentCapabilities)
|
|
}()
|
|
|
|
final class IfGuardConversionTests: SourceKitLSPTestCase {
|
|
private func validateCodeAction(
|
|
input: String,
|
|
expectedOutput: String?,
|
|
title: String,
|
|
file: StaticString = #filePath,
|
|
line: UInt = #line
|
|
) async throws {
|
|
let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport)
|
|
let uri = DocumentURI(for: .swift)
|
|
let positions = testClient.openDocument(input, uri: uri)
|
|
|
|
// Determine range
|
|
let range: Range<Position>
|
|
if input.contains("1️⃣") && input.contains("2️⃣") {
|
|
range = positions["1️⃣"]..<positions["2️⃣"]
|
|
} else if input.contains("1️⃣") {
|
|
let pos = positions["1️⃣"]
|
|
range = pos..<pos
|
|
} else {
|
|
XCTFail("Missing marker 1️⃣ in input", file: file, line: line)
|
|
return
|
|
}
|
|
|
|
let request = CodeActionRequest(
|
|
range: range,
|
|
context: .init(),
|
|
textDocument: TextDocumentIdentifier(uri)
|
|
)
|
|
let result = try await testClient.send(request)
|
|
|
|
guard case .codeActions(let codeActions) = result else {
|
|
XCTFail("Expected code actions response")
|
|
return
|
|
}
|
|
|
|
let action = codeActions.first(where: { $0.title == title })
|
|
if let expectedOutput {
|
|
guard let action else {
|
|
let available = codeActions.map(\.title)
|
|
XCTFail("Action '\(title)' not found. Available: \(available)", file: file, line: line)
|
|
return
|
|
}
|
|
|
|
guard let edit = action.edit else {
|
|
XCTFail("Action '\(title)' has no edit", file: file, line: line)
|
|
return
|
|
}
|
|
|
|
let changes = edit.changes?[uri] ?? []
|
|
|
|
let cleanInput = extractMarkers(input).textWithoutMarkers
|
|
|
|
let resultingText = apply(edits: changes, to: cleanInput)
|
|
|
|
XCTAssertEqual(resultingText, expectedOutput, file: file, line: line)
|
|
} else {
|
|
XCTAssertNil(action, "Expected action '\(title)' to be not offered", file: file, line: line)
|
|
}
|
|
}
|
|
|
|
private func context(indent: Int) -> String {
|
|
let spaces = String(repeating: " ", count: indent)
|
|
return """
|
|
\(spaces)print(1)
|
|
\(spaces)print(2)
|
|
\(spaces)print(3)
|
|
"""
|
|
}
|
|
|
|
func testConvertIfLetToGuard() async throws {
|
|
try await validateCodeAction(
|
|
input: """
|
|
func test() -> Int? {
|
|
1️⃣if let value = optional {
|
|
print(value)
|
|
return value
|
|
}
|
|
return nil
|
|
}
|
|
""",
|
|
expectedOutput: """
|
|
func test() -> Int? {
|
|
guard let value = optional else {
|
|
return nil
|
|
}
|
|
print(value)
|
|
return value
|
|
}
|
|
""",
|
|
title: "Convert to guard"
|
|
)
|
|
}
|
|
|
|
func testConvertIfLetToGuardNotShownWithoutEarlyExit() async throws {
|
|
try await validateCodeAction(
|
|
input: """
|
|
func test() {
|
|
1️⃣if let value = optional {
|
|
print(value)
|
|
}
|
|
return
|
|
}
|
|
""",
|
|
expectedOutput: nil,
|
|
title: "Convert to guard"
|
|
)
|
|
}
|
|
|
|
func testConvertIfLetToGuardNotShownWhenPartOfExpression() async throws {
|
|
try await validateCodeAction(
|
|
input: """
|
|
func test() -> Int? {
|
|
if let a = optional {
|
|
let x = 1️⃣if let b = optional { b } else { nil }
|
|
return a
|
|
}
|
|
return nil
|
|
}
|
|
""",
|
|
expectedOutput: nil,
|
|
title: "Convert to guard"
|
|
)
|
|
}
|
|
|
|
func testConvertIfLetToGuardShownWithReturn() async throws {
|
|
try await validateCodeAction(
|
|
input: """
|
|
func test() -> Int? {
|
|
1️⃣if let value = optional {
|
|
\(context(indent: 4))
|
|
return value
|
|
}
|
|
return nil
|
|
}
|
|
""",
|
|
expectedOutput: """
|
|
func test() -> Int? {
|
|
guard let value = optional else {
|
|
return nil
|
|
}
|
|
\(context(indent: 2))
|
|
return value
|
|
}
|
|
""",
|
|
title: "Convert to guard"
|
|
)
|
|
}
|
|
|
|
func testConvertIfLetToGuardShownWithThrow() async throws {
|
|
try await validateCodeAction(
|
|
input: """
|
|
func test() throws -> Int {
|
|
1️⃣if let value = optional {
|
|
\(context(indent: 4))
|
|
throw MyError()
|
|
}
|
|
return 0
|
|
}
|
|
""",
|
|
expectedOutput: """
|
|
func test() throws -> Int {
|
|
guard let value = optional else {
|
|
return 0
|
|
}
|
|
\(context(indent: 2))
|
|
throw MyError()
|
|
}
|
|
""",
|
|
title: "Convert to guard"
|
|
)
|
|
}
|
|
|
|
func testConvertIfLetToGuardNotShownWithBreakAndFallthrough() async throws {
|
|
try await validateCodeAction(
|
|
input: """
|
|
func test() {
|
|
while true {
|
|
1️⃣if let value = optional {
|
|
\(context(indent: 6))
|
|
break
|
|
}
|
|
print("loop")
|
|
}
|
|
}
|
|
""",
|
|
expectedOutput: nil,
|
|
title: "Convert to guard"
|
|
)
|
|
}
|
|
|
|
func testConvertIfLetToGuardNotShownWithContinueAndFallthrough() async throws {
|
|
try await validateCodeAction(
|
|
input: """
|
|
func test() {
|
|
while true {
|
|
1️⃣if let value = optional {
|
|
\(context(indent: 6))
|
|
continue
|
|
}
|
|
print("loop")
|
|
}
|
|
}
|
|
""",
|
|
expectedOutput: nil,
|
|
title: "Convert to guard"
|
|
)
|
|
}
|
|
|
|
func testConvertIfLetToGuardNotShownWithFatalError() async throws {
|
|
try await validateCodeAction(
|
|
input: """
|
|
func test() -> Int {
|
|
1️⃣if let value = optional {
|
|
fatalError("unreachable")
|
|
}
|
|
return 0
|
|
}
|
|
""",
|
|
expectedOutput: nil,
|
|
title: "Convert to guard"
|
|
)
|
|
}
|
|
|
|
func testConvertIfLetToGuardShownWithIfElseBothExiting() async throws {
|
|
// Note: Nested if-else as last statement should be detected as exiting
|
|
// when both branches guarantee exit.
|
|
try await validateCodeAction(
|
|
input: """
|
|
func test() -> Int? {
|
|
1️⃣if let value = optional {
|
|
if value > 0 {
|
|
return value
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
""",
|
|
expectedOutput: """
|
|
func test() -> Int? {
|
|
guard let value = optional else {
|
|
return nil
|
|
}
|
|
if value > 0 {
|
|
return value
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
""",
|
|
title: "Convert to guard"
|
|
)
|
|
}
|
|
|
|
func testConvertIfLetToGuardNotShownWithElse() async throws {
|
|
try await validateCodeAction(
|
|
input: """
|
|
func test() {
|
|
1️⃣if let value = optional {
|
|
print(value)
|
|
} else {
|
|
print("none")
|
|
}
|
|
return
|
|
}
|
|
""",
|
|
expectedOutput: nil,
|
|
title: "Convert to guard"
|
|
)
|
|
}
|
|
|
|
func testConvertIfLetToGuardNotShownWithDefer() async throws {
|
|
try await validateCodeAction(
|
|
input: """
|
|
func test() -> Int? {
|
|
1️⃣if let value = optional {
|
|
defer { cleanup(value) }
|
|
return value
|
|
}
|
|
return nil
|
|
}
|
|
""",
|
|
expectedOutput: nil,
|
|
title: "Convert to guard"
|
|
)
|
|
}
|
|
|
|
func testConvertIfLetToGuardNotShownWithCasePattern() async throws {
|
|
try await validateCodeAction(
|
|
input: """
|
|
func test() -> Int? {
|
|
1️⃣if case let .some(value) = optional {
|
|
return value
|
|
}
|
|
return nil
|
|
}
|
|
""",
|
|
expectedOutput: nil,
|
|
title: "Convert to guard"
|
|
)
|
|
}
|
|
|
|
func testConvertIfLetToGuardNotShownWithSwitchExit() async throws {
|
|
// Switch statements are conservatively treated as not guaranteeing exit
|
|
// even if all cases return, because checking exhaustiveness is complex.
|
|
try await validateCodeAction(
|
|
input: """
|
|
func test() -> Int? {
|
|
1️⃣if let value = optional {
|
|
switch value {
|
|
case 0: return nil
|
|
default: return value
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
""",
|
|
expectedOutput: nil,
|
|
title: "Convert to guard"
|
|
)
|
|
}
|
|
|
|
func testConvertIfLetToGuardWithComments() async throws {
|
|
// Note: Comments inside the if body have their leading trivia replaced
|
|
// during the transformation, so inline comments are preserved but
|
|
// leading comments on the first statement may be adjusted.
|
|
try await validateCodeAction(
|
|
input: """
|
|
func test() -> Int? {
|
|
// Check if we have a value
|
|
1️⃣if let value = optional /* unwrap */ {
|
|
print(value) // Use the value
|
|
return value // return it
|
|
}
|
|
return nil // fallback
|
|
}
|
|
""",
|
|
expectedOutput: """
|
|
func test() -> Int? {
|
|
// Check if we have a value
|
|
guard let value = optional /* unwrap */ else {
|
|
return nil // fallback
|
|
}
|
|
print(value) // Use the value
|
|
return value // return it
|
|
}
|
|
""",
|
|
title: "Convert to guard"
|
|
)
|
|
}
|
|
|
|
func testConvertIfLetToGuardMultipleConditions() async throws {
|
|
try await validateCodeAction(
|
|
input: """
|
|
func test() -> Int? {
|
|
1️⃣if let a = optA, let b = optB, a > 0 {
|
|
\(context(indent: 4))
|
|
return a + b
|
|
}
|
|
return nil
|
|
}
|
|
""",
|
|
expectedOutput: """
|
|
func test() -> Int? {
|
|
guard let a = optA, let b = optB, a > 0 else {
|
|
return nil
|
|
}
|
|
\(context(indent: 2))
|
|
return a + b
|
|
}
|
|
""",
|
|
title: "Convert to guard"
|
|
)
|
|
}
|
|
|
|
func testConvertIfLetToGuardPreserves4SpaceIndent() async throws {
|
|
// BasicFormat.inferIndentation requires at least 3 lines of code to infer indentation.
|
|
try await validateCodeAction(
|
|
input: """
|
|
func test() -> Int? {
|
|
1️⃣if let value = optional {
|
|
\(context(indent: 8))
|
|
return value
|
|
}
|
|
return nil
|
|
}
|
|
""",
|
|
expectedOutput: """
|
|
func test() -> Int? {
|
|
guard let value = optional else {
|
|
return nil
|
|
}
|
|
\(context(indent: 4))
|
|
return value
|
|
}
|
|
""",
|
|
title: "Convert to guard"
|
|
)
|
|
}
|
|
|
|
func testConvertIfLetToGuardSelectsInnermostCandidate() async throws {
|
|
// When cursor is on inner if-let, only the inner one should be converted
|
|
try await validateCodeAction(
|
|
input: """
|
|
func test() -> Int? {
|
|
if let outer = optA {
|
|
1️⃣if let inner = optB {
|
|
\(context(indent: 6))
|
|
return inner
|
|
}
|
|
return outer
|
|
}
|
|
return nil
|
|
}
|
|
""",
|
|
expectedOutput: """
|
|
func test() -> Int? {
|
|
if let outer = optA {
|
|
guard let inner = optB else {
|
|
return outer
|
|
}
|
|
\(context(indent: 4))
|
|
return inner
|
|
}
|
|
return nil
|
|
}
|
|
""",
|
|
title: "Convert to guard"
|
|
)
|
|
}
|
|
|
|
func testConvertIfLetToGuardSelectsOuterWhenCursorOnOuter() async throws {
|
|
// When cursor is on outer if-let, it should be converted even if it contains
|
|
// another if-let that is not convertible (e.g. doesn't guarantee exit).
|
|
try await validateCodeAction(
|
|
input: """
|
|
func test() -> Int? {
|
|
1️⃣if let outer = optA {
|
|
if let inner = optB {
|
|
print(inner)
|
|
}
|
|
return outer
|
|
}
|
|
return nil
|
|
}
|
|
""",
|
|
expectedOutput: """
|
|
func test() -> Int? {
|
|
guard let outer = optA else {
|
|
return nil
|
|
}
|
|
if let inner = optB {
|
|
print(inner)
|
|
}
|
|
return outer
|
|
}
|
|
""",
|
|
title: "Convert to guard"
|
|
)
|
|
}
|
|
|
|
func testConvertIfLetToGuardWithCRLF() async throws {
|
|
// Test that CRLF line endings in input are handled correctly.
|
|
try await validateCodeAction(
|
|
input: """
|
|
func test() -> Int? {\r\n\
|
|
1️⃣if let value = optional {\r\n\
|
|
print(value)\r\n\
|
|
return value\r\n\
|
|
}\r\n\
|
|
return nil\r\n\
|
|
}
|
|
""",
|
|
expectedOutput: """
|
|
func test() -> Int? {\r\n\
|
|
guard let value = optional else {\r\n\
|
|
return nil\n\
|
|
}\n\
|
|
print(value)\r\n\
|
|
return value\r\n\
|
|
}
|
|
""",
|
|
title: "Convert to guard"
|
|
)
|
|
}
|
|
|
|
func testConvertIfLetToGuardWithBodyComments() async throws {
|
|
try await validateCodeAction(
|
|
input: """
|
|
func test() -> Int? {
|
|
1️⃣if let value = optional {
|
|
// A comment before the first statement
|
|
print(value)
|
|
/* A block comment
|
|
spanning multiple lines */
|
|
return value
|
|
}
|
|
return nil
|
|
}
|
|
""",
|
|
expectedOutput: """
|
|
func test() -> Int? {
|
|
guard let value = optional else {
|
|
return nil
|
|
}
|
|
// A comment before the first statement
|
|
print(value)
|
|
/* A block comment
|
|
spanning multiple lines */
|
|
return value
|
|
}
|
|
""",
|
|
title: "Convert to guard"
|
|
)
|
|
}
|
|
|
|
func testConvertIfLetToGuardWithMultiLineStatement() async throws {
|
|
try await validateCodeAction(
|
|
input: """
|
|
func test() -> Int? {
|
|
1️⃣if let value = optional {
|
|
print(
|
|
value,
|
|
"is here"
|
|
)
|
|
return value
|
|
}
|
|
return nil
|
|
}
|
|
""",
|
|
expectedOutput: """
|
|
func test() -> Int? {
|
|
guard let value = optional else {
|
|
return nil
|
|
}
|
|
print(
|
|
value,
|
|
"is here"
|
|
)
|
|
return value
|
|
}
|
|
""",
|
|
title: "Convert to guard"
|
|
)
|
|
}
|
|
|
|
func testConvertIfLetToGuardWithFirstStatementLineComment() async throws {
|
|
try await validateCodeAction(
|
|
input: """
|
|
func test() -> Int? {
|
|
1️⃣if let value = optional {
|
|
// This comment is attached to 'return'
|
|
return value
|
|
}
|
|
return nil
|
|
}
|
|
""",
|
|
expectedOutput: """
|
|
func test() -> Int? {
|
|
guard let value = optional else {
|
|
return nil
|
|
}
|
|
// This comment is attached to 'return'
|
|
return value
|
|
}
|
|
""",
|
|
title: "Convert to guard"
|
|
)
|
|
}
|
|
|
|
func testConvertIfLetToGuardWithMultiLineString() async throws {
|
|
try await validateCodeAction(
|
|
input: #"""
|
|
func test() -> String? {
|
|
1️⃣if let value = optional {
|
|
print("""
|
|
Hello
|
|
\(value)
|
|
World
|
|
""")
|
|
return value
|
|
}
|
|
return nil
|
|
}
|
|
"""#,
|
|
expectedOutput: #"""
|
|
func test() -> String? {
|
|
guard let value = optional else {
|
|
return nil
|
|
}
|
|
print("""
|
|
Hello
|
|
\(value)
|
|
World
|
|
""")
|
|
return value
|
|
}
|
|
"""#,
|
|
title: "Convert to guard"
|
|
)
|
|
}
|
|
}
|