mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-10-02 11:03:32 +02:00
1450 lines
36 KiB
Swift
1450 lines
36 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2014 - 2023 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 BuildServerProtocol
|
|
@_spi(SourceKitLSP) import LanguageServerProtocol
|
|
import SKLogging
|
|
import SKTestSupport
|
|
import SwiftExtensions
|
|
import ToolchainRegistry
|
|
import XCTest
|
|
|
|
final class RenameTests: SourceKitLSPTestCase {
|
|
func testRenameVariableBaseName() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
let 1️⃣foo2️⃣ = 1
|
|
print(3️⃣foo4️⃣)
|
|
""",
|
|
newName: "bar",
|
|
expectedPrepareRenamePlaceholder: "foo",
|
|
expected: """
|
|
let bar = 1
|
|
print(bar)
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testRenameFunctionBaseName() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
func 1️⃣foo() {}
|
|
2️⃣foo()
|
|
_ = 3️⃣foo
|
|
""",
|
|
newName: "bar",
|
|
expectedPrepareRenamePlaceholder: "foo",
|
|
expected: """
|
|
func bar() {}
|
|
bar()
|
|
_ = bar
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testRenameFunctionParameter() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
func 1️⃣foo(5️⃣x: Int) {}
|
|
2️⃣foo(6️⃣x: 1)
|
|
_ = 3️⃣foo(7️⃣x:)
|
|
_ = 4️⃣foo
|
|
""",
|
|
newName: "bar(y:)",
|
|
expectedPrepareRenamePlaceholder: "foo(x:)",
|
|
expected: """
|
|
func bar(y: Int) {}
|
|
bar(y: 1)
|
|
_ = bar(y:)
|
|
_ = bar
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testRenameFromFunctionParameter() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
func foo(1️⃣x: Int) {}
|
|
foo(2️⃣x: 1)
|
|
_ = foo(3️⃣x:)
|
|
_ = foo
|
|
""",
|
|
newName: "bar(y:)",
|
|
expectedPrepareRenamePlaceholder: "foo(x:)",
|
|
expected: """
|
|
func bar(y: Int) {}
|
|
bar(y: 1)
|
|
_ = bar(y:)
|
|
_ = bar
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testRenameFromFunctionParameterOnSeparateLine() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
func foo(
|
|
1️⃣x: Int
|
|
) {}
|
|
foo(x: 1)
|
|
""",
|
|
newName: "bar(y:)",
|
|
expectedPrepareRenamePlaceholder: "foo(x:)",
|
|
expected: """
|
|
func bar(
|
|
y: Int
|
|
) {}
|
|
bar(y: 1)
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testSecondParameterNameIfMatches() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
func 1️⃣foo(4️⃣x y: Int) {}
|
|
2️⃣foo(5️⃣x: 1)
|
|
_ = 3️⃣foo(6️⃣x:)
|
|
""",
|
|
newName: "foo(y:)",
|
|
expectedPrepareRenamePlaceholder: "foo(x:)",
|
|
expected: """
|
|
func foo(y: Int) {}
|
|
foo(y: 1)
|
|
_ = foo(y:)
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testIntroduceLabel() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
func 1️⃣foo(4️⃣_ y: Int) {}
|
|
2️⃣foo(1)
|
|
_ = 3️⃣foo(5️⃣_:)
|
|
""",
|
|
newName: "foo(y:)",
|
|
expectedPrepareRenamePlaceholder: "foo(_:)",
|
|
expected: """
|
|
func foo(y: Int) {}
|
|
foo(y: 1)
|
|
_ = foo(y:)
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testRemoveLabel() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
func 1️⃣foo(4️⃣x: Int) {}
|
|
2️⃣foo(5️⃣x: 1)
|
|
_ = 3️⃣foo(6️⃣x:)
|
|
""",
|
|
newName: "foo(_:)",
|
|
expectedPrepareRenamePlaceholder: "foo(x:)",
|
|
expected: """
|
|
func foo(_ x: Int) {}
|
|
foo(1)
|
|
_ = foo(_:)
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testRemoveLabelWithExistingInternalName() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
func 1️⃣foo(4️⃣x a: Int) {}
|
|
2️⃣foo(5️⃣x: 1)
|
|
_ = 3️⃣foo(6️⃣x:)
|
|
""",
|
|
newName: "foo(_:)",
|
|
expectedPrepareRenamePlaceholder: "foo(x:)",
|
|
expected: """
|
|
func foo(_ a: Int) {}
|
|
foo(1)
|
|
_ = foo(_:)
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testRenameSubscript() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
struct Foo {
|
|
1️⃣subscript(3️⃣x x: Int) -> Int { x }
|
|
}
|
|
Foo()2️⃣[4️⃣x: 1]
|
|
""",
|
|
newName: "subscript(y:)",
|
|
expectedPrepareRenamePlaceholder: "subscript(x:)",
|
|
expected: """
|
|
struct Foo {
|
|
subscript(y x: Int) -> Int { x }
|
|
}
|
|
Foo()[y: 1]
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testRemoveExternalLabelFromSubscript() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
struct Foo {
|
|
1️⃣subscript(3️⃣x x: Int) -> Int { x }
|
|
}
|
|
Foo()2️⃣[4️⃣x: 1]
|
|
""",
|
|
newName: "subscript(_:)",
|
|
expectedPrepareRenamePlaceholder: "subscript(x:)",
|
|
expected: """
|
|
struct Foo {
|
|
subscript(_ x: Int) -> Int { x }
|
|
}
|
|
Foo()[1]
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testIntroduceExternalLabelFromSubscript() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
struct Foo {
|
|
1️⃣subscript(3️⃣x: Int) -> Int { x }
|
|
}
|
|
Foo()2️⃣[1]
|
|
""",
|
|
newName: "subscript(x:)",
|
|
expectedPrepareRenamePlaceholder: "subscript(_:)",
|
|
expected: """
|
|
struct Foo {
|
|
subscript(x x: Int) -> Int { x }
|
|
}
|
|
Foo()[x: 1]
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testIgnoreRenameSubscriptBaseName() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
struct Foo {
|
|
1️⃣subscript(3️⃣x: Int) -> Int { x }
|
|
}
|
|
Foo()2️⃣[1]
|
|
""",
|
|
newName: "arrayAccess(x:)",
|
|
expectedPrepareRenamePlaceholder: "subscript(_:)",
|
|
expected: """
|
|
struct Foo {
|
|
subscript(x x: Int) -> Int { x }
|
|
}
|
|
Foo()[x: 1]
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testRenameInitializerLabels() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
struct Foo {
|
|
1️⃣init(4️⃣x: Int) {}
|
|
}
|
|
Foo(x: 1)
|
|
Foo.2️⃣init(5️⃣x: 1)
|
|
_ = Foo.3️⃣init(6️⃣x:)
|
|
""",
|
|
newName: "init(y:)",
|
|
expectedPrepareRenamePlaceholder: "init(x:)",
|
|
expected: """
|
|
struct Foo {
|
|
init(y: Int) {}
|
|
}
|
|
Foo(y: 1)
|
|
Foo.init(y: 1)
|
|
_ = Foo.init(y:)
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testIgnoreRenameOfInitBaseName() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
struct Foo {
|
|
1️⃣init(4️⃣x: Int) {}
|
|
}
|
|
Foo(5️⃣x: 1)
|
|
Foo.2️⃣init(6️⃣x: 1)
|
|
_ = Foo.3️⃣init(7️⃣x:)
|
|
""",
|
|
newName: "create(y:)",
|
|
expectedPrepareRenamePlaceholder: "init(x:)",
|
|
expected: """
|
|
struct Foo {
|
|
init(y: Int) {}
|
|
}
|
|
Foo(y: 1)
|
|
Foo.init(y: 1)
|
|
_ = Foo.init(y:)
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testRenameMultipleParameters() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
func 1️⃣foo(4️⃣a: Int, 5️⃣b: Int) {}
|
|
2️⃣foo(6️⃣a: 1, 7️⃣b: 1)
|
|
_ = 3️⃣foo(8️⃣a:9️⃣b:)
|
|
""",
|
|
newName: "foo(x:y:)",
|
|
expectedPrepareRenamePlaceholder: "foo(a:b:)",
|
|
expected: """
|
|
func foo(x: Int, y: Int) {}
|
|
foo(x: 1, y: 1)
|
|
_ = foo(x:y:)
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testDontRenameParametersOmittedFromNewName() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
func 1️⃣foo(4️⃣a: Int, 5️⃣b: Int) {}
|
|
2️⃣foo(6️⃣a: 1, 7️⃣b: 1)
|
|
_ = 3️⃣foo(8️⃣a:9️⃣b:)
|
|
""",
|
|
newName: "foo(x:)",
|
|
expectedPrepareRenamePlaceholder: "foo(a:b:)",
|
|
expected: """
|
|
func foo(x: Int, b: Int) {}
|
|
foo(x: 1, b: 1)
|
|
_ = foo(x:b:)
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testIgnoreAdditionalParametersInNewName() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
func 1️⃣foo(4️⃣a: Int) {}
|
|
2️⃣foo(5️⃣a: 1)
|
|
_ = 3️⃣foo(6️⃣a:)
|
|
""",
|
|
newName: "foo(x:y:)",
|
|
expectedPrepareRenamePlaceholder: "foo(a:)",
|
|
expected: """
|
|
func foo(x: Int) {}
|
|
foo(x: 1)
|
|
_ = foo(x:)
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testOnlySpecifyBaseNameWhenRenamingFunction() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
func 1️⃣foo(4️⃣a: Int) {}
|
|
2️⃣foo(5️⃣a: 1)
|
|
_ = 3️⃣foo(6️⃣a:)
|
|
""",
|
|
newName: "bar",
|
|
expectedPrepareRenamePlaceholder: "foo(a:)",
|
|
expected: """
|
|
func bar(a: Int) {}
|
|
bar(a: 1)
|
|
_ = bar(a:)
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testIgnoreParametersInNewNameWhenRenamingVariable() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
let 1️⃣foo = 1
|
|
_ = 2️⃣foo
|
|
""",
|
|
newName: "bar(x:y:)",
|
|
expectedPrepareRenamePlaceholder: "foo",
|
|
expected: """
|
|
let bar = 1
|
|
_ = bar
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testNewNameDoesntContainClosingParenthesis() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
func 1️⃣foo(3️⃣a: Int) {}
|
|
2️⃣foo(4️⃣a: 1)
|
|
""",
|
|
newName: "bar(x:",
|
|
expectedPrepareRenamePlaceholder: "foo(a:)",
|
|
expected: """
|
|
func bar(x: Int) {}
|
|
bar(x: 1)
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testNewNameContainsTextAfterParenthesis() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
func 1️⃣foo(3️⃣a: Int) {}
|
|
2️⃣foo(4️⃣a: 1)
|
|
""",
|
|
newName: "bar(x:)other:",
|
|
expectedPrepareRenamePlaceholder: "foo(a:)",
|
|
expected: """
|
|
func bar(x: Int) {}
|
|
bar(x: 1)
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testSpacesInNewParameterNames() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
func 1️⃣foo(4️⃣a: Int) {}
|
|
2️⃣foo(5️⃣a: 1)
|
|
_ = 3️⃣foo(6️⃣a:)
|
|
""",
|
|
newName: "bar ( x : )",
|
|
expectedPrepareRenamePlaceholder: "foo(a:)",
|
|
expected: """
|
|
func bar ( x : Int) {}
|
|
bar ( x : 1)
|
|
_ = bar ( x :)
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testRenameOperator() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
struct Foo {}
|
|
func 1️⃣+(x: Foo, y: Foo) {}
|
|
Foo() 2️⃣+ Foo()
|
|
""",
|
|
newName: "-",
|
|
expectedPrepareRenamePlaceholder: "+(_:_:)",
|
|
expected: """
|
|
struct Foo {}
|
|
func -(x: Foo, y: Foo) {}
|
|
Foo() - Foo()
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testRenameParameterToEmptyName() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
func 1️⃣foo(3️⃣x: Int) {
|
|
print(x)
|
|
}
|
|
2️⃣foo(4️⃣x: 1)
|
|
""",
|
|
newName: "bar(:)",
|
|
expectedPrepareRenamePlaceholder: "foo(x:)",
|
|
expected: """
|
|
func bar(_ x: Int) {
|
|
print(x)
|
|
}
|
|
bar(1)
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testRenameInsidePoundSelector() async throws {
|
|
try SkipUnless.platformIsDarwin("#selector in test case doesn't compile without Objective-C runtime.")
|
|
try await assertSingleFileRename(
|
|
"""
|
|
import Foundation
|
|
class Foo: NSObject {
|
|
@objc public func 1️⃣bar(x: Int) {}
|
|
}
|
|
_ = #selector(Foo.2️⃣bar(x:))
|
|
""",
|
|
newName: "foo(y:)",
|
|
expectedPrepareRenamePlaceholder: "bar(x:)",
|
|
expected: """
|
|
import Foundation
|
|
class Foo: NSObject {
|
|
@objc public func foo(y: Int) {}
|
|
}
|
|
_ = #selector(Foo.foo(y:))
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testCrossFileSwiftRename() async throws {
|
|
try await assertMultiFileRename(
|
|
files: [
|
|
"a.swift": """
|
|
func 1️⃣foo() {}
|
|
""",
|
|
"b.swift": """
|
|
func test() {
|
|
2️⃣foo()
|
|
}
|
|
""",
|
|
],
|
|
newName: "bar",
|
|
expectedPrepareRenamePlaceholder: "foo",
|
|
expected: [
|
|
"a.swift": """
|
|
func bar() {}
|
|
""",
|
|
"b.swift": """
|
|
func test() {
|
|
bar()
|
|
}
|
|
""",
|
|
]
|
|
)
|
|
}
|
|
|
|
func testSwiftCrossModuleRename() async throws {
|
|
try await assertMultiFileRename(
|
|
files: [
|
|
"LibA/LibA.swift": """
|
|
public func 1️⃣foo(2️⃣argLabel: Int) {}
|
|
""",
|
|
"LibB/LibB.swift": """
|
|
import LibA
|
|
public func test() {
|
|
3️⃣foo(4️⃣argLabel: 1)
|
|
}
|
|
""",
|
|
],
|
|
newName: "bar(new:)",
|
|
expectedPrepareRenamePlaceholder: "foo(argLabel:)",
|
|
expected: [
|
|
"LibA/LibA.swift": """
|
|
public func bar(new: Int) {}
|
|
""",
|
|
"LibB/LibB.swift": """
|
|
import LibA
|
|
public func test() {
|
|
bar(new: 1)
|
|
}
|
|
""",
|
|
],
|
|
manifest: """
|
|
let package = Package(
|
|
name: "MyLibrary",
|
|
targets: [
|
|
.target(name: "LibA"),
|
|
.target(name: "LibB", dependencies: ["LibA"]),
|
|
]
|
|
)
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testTryIndexLocationsDontMatchInMemoryLocations() async throws {
|
|
try await assertMultiFileRename(
|
|
files: [
|
|
"a.swift": """
|
|
func 1️⃣foo() {}
|
|
""",
|
|
"b.swift": """
|
|
0️⃣func test() {
|
|
foo()
|
|
}
|
|
""",
|
|
],
|
|
newName: "bar",
|
|
expectedPrepareRenamePlaceholder: "foo",
|
|
expected: [
|
|
"a.swift": """
|
|
func bar() {}
|
|
""",
|
|
"b.swift": """
|
|
func test() {
|
|
foo()
|
|
}
|
|
""",
|
|
],
|
|
manifest: """
|
|
let package = Package(
|
|
name: "MyLibrary",
|
|
targets: [
|
|
.target(
|
|
name: "MyLibrary",
|
|
swiftSettings: [.unsafeFlags(["-Xfrontend", "-disable-objc-attr-requires-foundation-module"])]
|
|
)
|
|
]
|
|
)
|
|
""",
|
|
preRenameActions: { ws in
|
|
let (bUri, bPositions) = try ws.openDocument("b.swift")
|
|
ws.testClient.send(
|
|
DidChangeTextDocumentNotification(
|
|
textDocument: VersionedTextDocumentIdentifier(bUri, version: 1),
|
|
contentChanges: [TextDocumentContentChangeEvent(range: Range(bPositions["0️⃣"]), text: "\n")]
|
|
)
|
|
)
|
|
}
|
|
)
|
|
}
|
|
|
|
func testTryIndexLocationsDontMatchInMemoryLocationsByLineColumnButNotOffset() async throws {
|
|
try await assertMultiFileRename(
|
|
files: [
|
|
"a.swift": """
|
|
func 1️⃣foo() {}
|
|
""",
|
|
"b.swift": """
|
|
0️⃣func test() {
|
|
foo()
|
|
}
|
|
""",
|
|
],
|
|
newName: "bar",
|
|
expectedPrepareRenamePlaceholder: "foo",
|
|
expected: [
|
|
"a.swift": """
|
|
func bar() {}
|
|
""",
|
|
"b.swift": """
|
|
func test() {
|
|
bar()
|
|
}
|
|
""",
|
|
],
|
|
manifest: """
|
|
let package = Package(
|
|
name: "MyLibrary",
|
|
targets: [
|
|
.target(
|
|
name: "MyLibrary",
|
|
swiftSettings: [.unsafeFlags(["-Xfrontend", "-disable-objc-attr-requires-foundation-module"])]
|
|
)
|
|
]
|
|
)
|
|
""",
|
|
preRenameActions: { ws in
|
|
let (bUri, bPositions) = try ws.openDocument("b.swift")
|
|
ws.testClient.send(
|
|
DidChangeTextDocumentNotification(
|
|
textDocument: VersionedTextDocumentIdentifier(bUri, version: 1),
|
|
contentChanges: [
|
|
TextDocumentContentChangeEvent(range: Range(bPositions["0️⃣"]), text: "/* this is just a comment */")
|
|
]
|
|
)
|
|
)
|
|
}
|
|
)
|
|
}
|
|
|
|
func testPrepeareRenameOnDefinition() async throws {
|
|
let testClient = try await TestSourceKitLSPClient()
|
|
let uri = DocumentURI(for: .swift)
|
|
let positions = testClient.openDocument(
|
|
"""
|
|
func 1️⃣foo2️⃣(3️⃣a: Int) {}
|
|
""",
|
|
uri: uri
|
|
)
|
|
let response = try await testClient.send(
|
|
PrepareRenameRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
|
|
)
|
|
let range = try XCTUnwrap(response?.range)
|
|
let placeholder = try XCTUnwrap(response?.placeholder)
|
|
XCTAssertEqual(range, positions["1️⃣"]..<positions["2️⃣"])
|
|
XCTAssertEqual(placeholder, "foo(a:)")
|
|
}
|
|
|
|
func testPrepeareRenameOnReference() async throws {
|
|
let testClient = try await TestSourceKitLSPClient()
|
|
let uri = DocumentURI(for: .swift)
|
|
let positions = testClient.openDocument(
|
|
"""
|
|
func foo(a: Int, b: Int = 1) {}
|
|
1️⃣foo2️⃣(a: 1)
|
|
""",
|
|
uri: uri
|
|
)
|
|
let response = try await testClient.send(
|
|
PrepareRenameRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
|
|
)
|
|
let range = try XCTUnwrap(response?.range)
|
|
let placeholder = try XCTUnwrap(response?.placeholder)
|
|
XCTAssertEqual(range, positions["1️⃣"]..<positions["2️⃣"])
|
|
XCTAssertEqual(placeholder, "foo(a:b:)")
|
|
}
|
|
|
|
func testGlobalRenameC() async throws {
|
|
try await assertMultiFileRename(
|
|
files: [
|
|
"Sources/MyLibrary/include/lib.h": """
|
|
void 1️⃣do2️⃣Stuff();
|
|
""",
|
|
"lib.c": """
|
|
#include "lib.h"
|
|
|
|
void 3️⃣doStuff() {
|
|
4️⃣doStuff();
|
|
}
|
|
""",
|
|
],
|
|
headerFileLanguage: .c,
|
|
newName: "doRecursiveStuff",
|
|
expectedPrepareRenamePlaceholder: "doStuff",
|
|
expected: [
|
|
"Sources/MyLibrary/include/lib.h": """
|
|
void doRecursiveStuff();
|
|
""",
|
|
"lib.c": """
|
|
#include "lib.h"
|
|
|
|
void doRecursiveStuff() {
|
|
doRecursiveStuff();
|
|
}
|
|
""",
|
|
]
|
|
)
|
|
}
|
|
|
|
func testGlobalRenameObjC() async throws {
|
|
try await assertMultiFileRename(
|
|
files: [
|
|
"Sources/MyLibrary/include/lib.h": """
|
|
@interface Foo
|
|
- (int)1️⃣perform2️⃣Action:(int)action 3️⃣wi4️⃣th:(int)value;
|
|
@end
|
|
""",
|
|
"lib.m": """
|
|
#include "lib.h"
|
|
|
|
@implementation Foo
|
|
- (int)5️⃣performAction:(int)action 6️⃣with:(int)value {
|
|
return [self 7️⃣performAction:action 8️⃣with:value];
|
|
}
|
|
@end
|
|
""",
|
|
],
|
|
headerFileLanguage: .objective_c,
|
|
newName: "performNewAction:by:",
|
|
expectedPrepareRenamePlaceholder: "performAction:with:",
|
|
expected: [
|
|
"Sources/MyLibrary/include/lib.h": """
|
|
@interface Foo
|
|
- (int)performNewAction:(int)action by:(int)value;
|
|
@end
|
|
""",
|
|
"lib.m": """
|
|
#include "lib.h"
|
|
|
|
@implementation Foo
|
|
- (int)performNewAction:(int)action by:(int)value {
|
|
return [self performNewAction:action by:value];
|
|
}
|
|
@end
|
|
""",
|
|
]
|
|
)
|
|
}
|
|
|
|
func testRenameParameterReferencesInsideBody() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
func 1️⃣foo(x: Int) {
|
|
print(x)
|
|
}
|
|
func test() {
|
|
2️⃣foo(x: 1)
|
|
}
|
|
""",
|
|
newName: "bar(y:)",
|
|
expectedPrepareRenamePlaceholder: "foo(x:)",
|
|
expected: """
|
|
func bar(y: Int) {
|
|
print(y)
|
|
}
|
|
func test() {
|
|
bar(y: 1)
|
|
}
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testDontRenameParameterReferencesInsideBodyIfTheyUseSecondName() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
func 1️⃣foo(x a: Int) {
|
|
print(a)
|
|
}
|
|
func test() {
|
|
2️⃣foo(x: 1)
|
|
}
|
|
""",
|
|
newName: "bar(y:)",
|
|
expectedPrepareRenamePlaceholder: "foo(x:)",
|
|
expected: """
|
|
func bar(y a: Int) {
|
|
print(a)
|
|
}
|
|
func test() {
|
|
bar(y: 1)
|
|
}
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testRenameParameterReferencesInsideBodyAcrossFiles() async throws {
|
|
try await assertMultiFileRename(
|
|
files: [
|
|
"a.swift": """
|
|
func foo(x: Int) {
|
|
print(x)
|
|
}
|
|
""",
|
|
"b.swift": """
|
|
func test() {
|
|
1️⃣foo(x: 1)
|
|
}
|
|
""",
|
|
],
|
|
newName: "bar(y:)",
|
|
expectedPrepareRenamePlaceholder: "foo(x:)",
|
|
expected: [
|
|
"a.swift": """
|
|
func bar(y: Int) {
|
|
print(y)
|
|
}
|
|
""",
|
|
"b.swift": """
|
|
func test() {
|
|
bar(y: 1)
|
|
}
|
|
""",
|
|
]
|
|
)
|
|
}
|
|
|
|
func testRenameParameterInsideFunction() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
func test(myParam: Int) {
|
|
print(1️⃣myParam)
|
|
}
|
|
""",
|
|
newName: "other",
|
|
expectedPrepareRenamePlaceholder: "myParam",
|
|
expected: """
|
|
func test(myParam other: Int) {
|
|
print(other)
|
|
}
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testRenameParameterSecondNameInsideFunction() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
func test(myExternalName myParam: Int) {
|
|
print(1️⃣myParam)
|
|
}
|
|
""",
|
|
newName: "other",
|
|
expectedPrepareRenamePlaceholder: "myParam",
|
|
expected: """
|
|
func test(myExternalName other: Int) {
|
|
print(other)
|
|
}
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testRenameClassHierarchy() async throws {
|
|
try await assertMultiFileRename(
|
|
files: [
|
|
"Lib.swift": """
|
|
class Base {
|
|
func 1️⃣foo() {}
|
|
}
|
|
|
|
class Inherited: Base {
|
|
override func 2️⃣foo() {}
|
|
}
|
|
|
|
class OtherInherited: Base {
|
|
override func 3️⃣foo() {}
|
|
}
|
|
|
|
func calls() {
|
|
Base().4️⃣foo()
|
|
Inherited().5️⃣foo()
|
|
OtherInherited().6️⃣foo()
|
|
}
|
|
"""
|
|
],
|
|
newName: "bar",
|
|
expectedPrepareRenamePlaceholder: "foo",
|
|
expected: [
|
|
"Lib.swift": """
|
|
class Base {
|
|
func bar() {}
|
|
}
|
|
|
|
class Inherited: Base {
|
|
override func bar() {}
|
|
}
|
|
|
|
class OtherInherited: Base {
|
|
override func bar() {}
|
|
}
|
|
|
|
func calls() {
|
|
Base().bar()
|
|
Inherited().bar()
|
|
OtherInherited().bar()
|
|
}
|
|
"""
|
|
]
|
|
)
|
|
}
|
|
|
|
func testRenameProtocolRequirement() async throws {
|
|
try await assertMultiFileRename(
|
|
files: [
|
|
"Lib.swift": """
|
|
protocol MyProtocol {
|
|
func 1️⃣foo()
|
|
}
|
|
|
|
extension MyProtocol {
|
|
func 2️⃣foo() {}
|
|
}
|
|
|
|
struct Foo: MyProtocol {
|
|
func 3️⃣foo() {}
|
|
}
|
|
|
|
func calls() {
|
|
Foo().4️⃣foo()
|
|
}
|
|
func protocolCall(proto: MyProtocol) {
|
|
proto.5️⃣foo()
|
|
}
|
|
"""
|
|
],
|
|
newName: "bar",
|
|
expectedPrepareRenamePlaceholder: "foo",
|
|
expected: [
|
|
"Lib.swift": """
|
|
protocol MyProtocol {
|
|
func bar()
|
|
}
|
|
|
|
extension MyProtocol {
|
|
func bar() {}
|
|
}
|
|
|
|
struct Foo: MyProtocol {
|
|
func bar() {}
|
|
}
|
|
|
|
func calls() {
|
|
Foo().bar()
|
|
}
|
|
func protocolCall(proto: MyProtocol) {
|
|
proto.bar()
|
|
}
|
|
"""
|
|
]
|
|
)
|
|
}
|
|
|
|
func testRenameProtocolsConnectedByClassHierarchy() async throws {
|
|
try await assertMultiFileRename(
|
|
files: [
|
|
"Lib.swift": """
|
|
protocol MyProtocol {
|
|
func 1️⃣foo()
|
|
}
|
|
|
|
protocol MyOtherProtocol {
|
|
func 2️⃣foo()
|
|
}
|
|
|
|
class Base: MyProtocol {
|
|
func 3️⃣foo() {}
|
|
}
|
|
|
|
class Inherited: Base, MyOtherProtocol {}
|
|
"""
|
|
],
|
|
newName: "bar",
|
|
expectedPrepareRenamePlaceholder: "foo",
|
|
expected: [
|
|
"Lib.swift": """
|
|
protocol MyProtocol {
|
|
func bar()
|
|
}
|
|
|
|
protocol MyOtherProtocol {
|
|
func bar()
|
|
}
|
|
|
|
class Base: MyProtocol {
|
|
func bar() {}
|
|
}
|
|
|
|
class Inherited: Base, MyOtherProtocol {}
|
|
"""
|
|
]
|
|
)
|
|
}
|
|
|
|
func testRenameClassHierarchyWithoutIndexData() async throws {
|
|
// Without index data we don't know overriding + overridden decls.
|
|
// Check that we at least fall back to renaming the direct references to `Inherited.foo`.
|
|
try await assertSingleFileRename(
|
|
"""
|
|
class Base {
|
|
func foo() {}
|
|
}
|
|
|
|
class Inherited: Base {
|
|
override func 2️⃣foo() {}
|
|
}
|
|
|
|
class OtherInherited: Base {
|
|
override func foo() {}
|
|
}
|
|
|
|
func calls() {
|
|
Base().foo()
|
|
Inherited().5️⃣foo()
|
|
OtherInherited().foo()
|
|
}
|
|
""",
|
|
newName: "bar",
|
|
expectedPrepareRenamePlaceholder: "foo",
|
|
expected: """
|
|
class Base {
|
|
func foo() {}
|
|
}
|
|
|
|
class Inherited: Base {
|
|
override func bar() {}
|
|
}
|
|
|
|
class OtherInherited: Base {
|
|
override func foo() {}
|
|
}
|
|
|
|
func calls() {
|
|
Base().foo()
|
|
Inherited().bar()
|
|
OtherInherited().foo()
|
|
}
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testRenameClassHierarchyWithoutIndexCpp() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
class Base {
|
|
virtual void 1️⃣foo();
|
|
};
|
|
|
|
class Inherited: Base {
|
|
void 2️⃣foo() override {}
|
|
};
|
|
|
|
class OtherInherited: Base {
|
|
void 3️⃣foo() override {}
|
|
};
|
|
""",
|
|
language: .cpp,
|
|
newName: "bar",
|
|
expectedPrepareRenamePlaceholder: "foo",
|
|
expected: """
|
|
class Base {
|
|
virtual void bar();
|
|
};
|
|
|
|
class Inherited: Base {
|
|
void bar() override {}
|
|
};
|
|
|
|
class OtherInherited: Base {
|
|
void bar() override {}
|
|
};
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testRenameAfterFileMove() async throws {
|
|
let project = try await SwiftPMTestProject(
|
|
files: [
|
|
"definition.swift": """
|
|
func 1️⃣foo2️⃣() {}
|
|
""",
|
|
"caller.swift": """
|
|
func test() {
|
|
3️⃣foo4️⃣()
|
|
}
|
|
""",
|
|
],
|
|
enableBackgroundIndexing: true
|
|
)
|
|
|
|
let definitionUri = try project.uri(for: "definition.swift")
|
|
let (callerUri, callerPositions) = try project.openDocument("caller.swift")
|
|
|
|
// Validate that we get correct rename results before moving the definition file.
|
|
let resultBeforeFileMove = try await project.testClient.send(
|
|
RenameRequest(textDocument: TextDocumentIdentifier(callerUri), position: callerPositions["3️⃣"], newName: "bar")
|
|
)
|
|
XCTAssertEqual(
|
|
resultBeforeFileMove,
|
|
WorkspaceEdit(
|
|
changes: [
|
|
definitionUri: [
|
|
TextEdit(
|
|
range: try project.position(
|
|
of: "1️⃣",
|
|
in: "definition.swift"
|
|
)..<project.position(of: "2️⃣", in: "definition.swift"),
|
|
newText: "bar"
|
|
)
|
|
],
|
|
callerUri: [TextEdit(range: callerPositions["3️⃣"]..<callerPositions["4️⃣"], newText: "bar")],
|
|
]
|
|
)
|
|
)
|
|
|
|
let movedDefinitionUri =
|
|
DocumentURI(
|
|
definitionUri.fileURL!
|
|
.deletingLastPathComponent()
|
|
.appending(component: "movedDefinition.swift")
|
|
)
|
|
|
|
try FileManager.default.moveItem(at: XCTUnwrap(definitionUri.fileURL), to: XCTUnwrap(movedDefinitionUri.fileURL))
|
|
|
|
project.testClient.send(
|
|
DidChangeWatchedFilesNotification(changes: [
|
|
FileEvent(uri: definitionUri, type: .deleted), FileEvent(uri: movedDefinitionUri, type: .created),
|
|
])
|
|
)
|
|
|
|
try await project.testClient.send(SynchronizeRequest(index: true))
|
|
|
|
let resultAfterFileMove = try await project.testClient.send(
|
|
RenameRequest(textDocument: TextDocumentIdentifier(callerUri), position: callerPositions["3️⃣"], newName: "bar")
|
|
)
|
|
XCTAssertEqual(
|
|
resultAfterFileMove,
|
|
WorkspaceEdit(
|
|
changes: [
|
|
movedDefinitionUri: [
|
|
TextEdit(
|
|
range: try project.position(
|
|
of: "1️⃣",
|
|
in: "definition.swift"
|
|
)..<project.position(of: "2️⃣", in: "definition.swift"),
|
|
newText: "bar"
|
|
)
|
|
],
|
|
callerUri: [TextEdit(range: callerPositions["3️⃣"]..<callerPositions["4️⃣"], newText: "bar")],
|
|
]
|
|
)
|
|
)
|
|
}
|
|
|
|
func testRenameEnumCaseWithUnlabeledAssociatedValue() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
enum MyEnum {
|
|
case 1️⃣myCase(String)
|
|
}
|
|
""",
|
|
newName: "newName",
|
|
expectedPrepareRenamePlaceholder: "myCase(_:)",
|
|
expected: """
|
|
enum MyEnum {
|
|
case newName(String)
|
|
}
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testAddLabelToEnumCase() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
enum MyEnum {
|
|
case 1️⃣myCase(String)
|
|
}
|
|
""",
|
|
newName: "newName(newLabel:)",
|
|
expectedPrepareRenamePlaceholder: "myCase(_:)",
|
|
expected: """
|
|
enum MyEnum {
|
|
case newName(newLabel: String)
|
|
}
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testRemoveLabelFromEnumCase() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
enum MyEnum {
|
|
case myCase(label: String)
|
|
}
|
|
|
|
func test() {
|
|
_ = MyEnum.2️⃣myCase(label: "abc")
|
|
}
|
|
""",
|
|
newName: "newName(_:)",
|
|
expectedPrepareRenamePlaceholder: "myCase(label:)",
|
|
expected: """
|
|
enum MyEnum {
|
|
case newName(_ label: String)
|
|
}
|
|
|
|
func test() {
|
|
_ = MyEnum.newName("abc")
|
|
}
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testRenameEnumCaseWithUnderscoreLabel() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
enum MyEnum {
|
|
case myCase(_ label: String)
|
|
}
|
|
|
|
func test() {
|
|
_ = MyEnum.2️⃣myCase("abc")
|
|
}
|
|
""",
|
|
newName: "newName(_:)",
|
|
expectedPrepareRenamePlaceholder: "myCase(_:)",
|
|
expected: """
|
|
enum MyEnum {
|
|
case newName(_ label: String)
|
|
}
|
|
|
|
func test() {
|
|
_ = MyEnum.newName("abc")
|
|
}
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testRenameEnumCaseWithUnderscoreToLabelMatchingInternalName() async throws {
|
|
try await assertSingleFileRename(
|
|
"""
|
|
enum MyEnum {
|
|
case myCase(_ label: String)
|
|
}
|
|
|
|
func test() {
|
|
_ = MyEnum.2️⃣myCase("abc")
|
|
}
|
|
""",
|
|
newName: "newName(label:)",
|
|
expectedPrepareRenamePlaceholder: "myCase(_:)",
|
|
expected: """
|
|
enum MyEnum {
|
|
case newName(label: String)
|
|
}
|
|
|
|
func test() {
|
|
_ = MyEnum.newName(label: "abc")
|
|
}
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testRenameEnumCaseWithUnderscoreToLabelNotMatchingInternalName() async throws {
|
|
// Note that the renamed code doesn't compile because enum cases can't have an external and internal parameter name.
|
|
// But it's probably the best thing we can do because we don't want to erase the user-specified internal name, which
|
|
// they didn't request to rename. Also, this is a pretty niche case and having a special case for it is probably not
|
|
// worth it.
|
|
try await assertSingleFileRename(
|
|
"""
|
|
enum MyEnum {
|
|
case myCase(_ label: String)
|
|
}
|
|
|
|
func test() {
|
|
_ = MyEnum.2️⃣myCase("abc")
|
|
}
|
|
""",
|
|
newName: "newName(publicLabel:)",
|
|
expectedPrepareRenamePlaceholder: "myCase(_:)",
|
|
expected: """
|
|
enum MyEnum {
|
|
case newName(publicLabel label: String)
|
|
}
|
|
|
|
func test() {
|
|
_ = MyEnum.newName(publicLabel: "abc")
|
|
}
|
|
"""
|
|
)
|
|
}
|
|
|
|
func testRenameDoesNotReportEditsIfNoActualChangeIsMade() async throws {
|
|
let project = try await SwiftPMTestProject(
|
|
files: [
|
|
"FileA.swift": """
|
|
func 1️⃣foo(x: Int) {}
|
|
""",
|
|
"FileB.swift": """
|
|
func test() {
|
|
foo(x: 1)
|
|
}
|
|
""",
|
|
],
|
|
enableBackgroundIndexing: true
|
|
)
|
|
let (uri, positions) = try project.openDocument("FileA.swift")
|
|
let result = try await project.testClient.send(
|
|
RenameRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"], newName: "foo(x:)")
|
|
)
|
|
XCTAssertEqual(result?.changes, [:])
|
|
}
|
|
|
|
func testRenameDedupesMultiUnitIndexEntries() async throws {
|
|
final class BuildServer: CustomBuildServer {
|
|
let inProgressRequestsTracker = CustomBuildServerInProgressRequestTracker()
|
|
|
|
private let projectRoot: URL
|
|
|
|
private let libATarget = BuildTargetIdentifier(uri: try! URI(string: "build://targetA"))
|
|
private let libBTarget = BuildTargetIdentifier(uri: try! URI(string: "build://targetB"))
|
|
|
|
private var sources: [SourcesItem] {
|
|
return [
|
|
SourcesItem(
|
|
target: libATarget,
|
|
sources: [
|
|
sourceItem(
|
|
for: projectRoot.appending(component: "Shared.swift"),
|
|
outputPath: fakeOutputPath(for: "Shared.swift", in: "LibA")
|
|
),
|
|
sourceItem(
|
|
for: projectRoot.appending(component: "LibA.swift"),
|
|
outputPath: fakeOutputPath(for: "LibA.swift", in: "LibA")
|
|
),
|
|
]
|
|
),
|
|
SourcesItem(
|
|
target: libBTarget,
|
|
sources: [
|
|
sourceItem(
|
|
for: projectRoot.appending(component: "Shared.swift"),
|
|
outputPath: fakeOutputPath(for: "Shared.swift", in: "LibB")
|
|
)
|
|
]
|
|
),
|
|
]
|
|
}
|
|
|
|
init(projectRoot: URL, connectionToSourceKitLSP: any LanguageServerProtocol.Connection) {
|
|
self.projectRoot = projectRoot
|
|
}
|
|
|
|
func initializeBuildRequest(_ request: InitializeBuildRequest) async throws -> InitializeBuildResponse {
|
|
return try initializationResponseSupportingBackgroundIndexing(
|
|
projectRoot: projectRoot,
|
|
outputPathsProvider: true
|
|
)
|
|
}
|
|
|
|
func workspaceBuildTargetsRequest(
|
|
_ request: WorkspaceBuildTargetsRequest
|
|
) async throws -> WorkspaceBuildTargetsResponse {
|
|
WorkspaceBuildTargetsResponse(targets: [
|
|
BuildTarget(id: libATarget, languageIds: [.swift], dependencies: []),
|
|
BuildTarget(id: libBTarget, languageIds: [.swift], dependencies: []),
|
|
])
|
|
}
|
|
|
|
func buildTargetSourcesRequest(_ request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse {
|
|
return BuildTargetSourcesResponse(items: sources.filter { request.targets.contains($0.target) })
|
|
}
|
|
|
|
func textDocumentSourceKitOptionsRequest(
|
|
_ request: TextDocumentSourceKitOptionsRequest
|
|
) async throws -> TextDocumentSourceKitOptionsResponse? {
|
|
let targetSources = try XCTUnwrap(sources.first(where: { $0.target == request.target })?.sources)
|
|
let sourceInfo = try XCTUnwrap(targetSources.first(where: { $0.uri == request.textDocument.uri }))
|
|
var arguments = targetSources.map(\.uri.pseudoPath)
|
|
arguments += [
|
|
"-module-name", "Shared",
|
|
"-index-unit-output-path", try XCTUnwrap(sourceInfo.sourceKitData?.outputPath),
|
|
]
|
|
if request.target == libBTarget {
|
|
arguments.append("-DLIB_B")
|
|
}
|
|
if let defaultSDKPath {
|
|
arguments += ["-sdk", defaultSDKPath]
|
|
}
|
|
return TextDocumentSourceKitOptionsResponse(compilerArguments: arguments)
|
|
}
|
|
}
|
|
|
|
let project = try await CustomBuildServerTestProject(
|
|
files: [
|
|
"Shared.swift": """
|
|
func 1️⃣foo2️⃣() {}
|
|
#if LIB_B
|
|
func extra() {}
|
|
#endif
|
|
""",
|
|
"LibA.swift": """
|
|
func bar() {
|
|
3️⃣foo4️⃣()
|
|
}
|
|
""",
|
|
],
|
|
buildServer: BuildServer.self,
|
|
enableBackgroundIndexing: true
|
|
)
|
|
|
|
let newName = "baz"
|
|
let (libAUri, libAPositions) = try project.openDocument("LibA.swift")
|
|
let result = try await project.testClient.send(
|
|
RenameRequest(
|
|
textDocument: TextDocumentIdentifier(libAUri),
|
|
position: libAPositions["3️⃣"],
|
|
newName: newName
|
|
)
|
|
)
|
|
XCTAssertEqual(
|
|
result,
|
|
WorkspaceEdit(changes: [
|
|
try project.uri(for: "Shared.swift"): [
|
|
TextEdit(range: try project.range(from: "1️⃣", to: "2️⃣", in: "Shared.swift"), newText: newName)
|
|
],
|
|
libAUri: [
|
|
TextEdit(range: libAPositions["3️⃣"]..<libAPositions["4️⃣"], newText: newName)
|
|
],
|
|
])
|
|
)
|
|
}
|
|
}
|