Files
sourcekit-lsp/Tests/SourceKitLSPTests/IfGuardConversionTests.swift
Karan Lokchandani 55d75954e5 Add if let to guard transform (#2420)
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>
2026-01-13 22:46:54 +01:00

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"
)
}
}