Skip to content

Commit 38e4257

Browse files
committed
Add refactoring action for adding a test target to a package manifest
Leverage the newly-introduced package manifest editing tools in SwiftPM to create a package editing refactoring operation. This operation can be triggered from the a target in the manifest itself, e.g., .target(name: "MyLib") and will add a test target to the package manifest that depends on this target, i.e., .testTarget( name: "MyLibTests", dependencies: [ "MyLib" ] ) It will also create a new source file `Tests/MyLibTests/MyLibTests.swift` that that imports both MyLib and XCTest, and contains an XCTestCase subclass with one test to get you started.
1 parent 6dafe87 commit 38e4257

File tree

3 files changed

+168
-0
lines changed

3 files changed

+168
-0
lines changed

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ let package = Package(
309309
.product(name: "SwiftRefactor", package: "swift-syntax"),
310310
.product(name: "SwiftSyntax", package: "swift-syntax"),
311311
.product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"),
312+
.product(name: "SwiftPM-auto", package: "swift-package-manager"),
312313
],
313314
exclude: ["CMakeLists.txt"]
314315
),
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import LanguageServerProtocol
14+
import PackageModel
15+
import PackageModelSyntax
16+
import SwiftRefactor
17+
import SwiftSyntax
18+
19+
/// Syntactic code action provider to provide refactoring actions that
20+
/// edit a package manifest.
21+
struct PackageManifestEdits: SyntaxCodeActionProvider {
22+
static func codeActions(in scope: SyntaxCodeActionScope) -> [CodeAction] {
23+
guard
24+
let token = scope.firstToken,
25+
let call = token.findEnclosingCall()
26+
else {
27+
return []
28+
}
29+
30+
var actions = [CodeAction]()
31+
32+
// If there's a target name, offer to create a test target derived from it.
33+
if let targetName = call.findStringArgument(label: "name") {
34+
do {
35+
let target = try TargetDescription(
36+
name: "\(targetName)Tests",
37+
dependencies: [ .byName(name: targetName, condition: nil) ],
38+
type: .test
39+
)
40+
41+
let edits = try AddTarget.addTarget(target, to: scope.file)
42+
actions.append(
43+
CodeAction(
44+
title: "Add test target for this target",
45+
kind: .refactor,
46+
edit: edits.asWorkspaceEdit(snapshot: scope.snapshot)
47+
)
48+
)
49+
} catch {
50+
// nothing to do
51+
}
52+
}
53+
54+
return actions
55+
}
56+
}
57+
58+
fileprivate extension PackageEditResult {
59+
/// Translate package manifest edits into a workspace edit. This can
60+
/// involve both modifications to the manifest file as well as the creation
61+
/// of new files.
62+
func asWorkspaceEdit(snapshot: DocumentSnapshot) -> WorkspaceEdit {
63+
// The edits to perform on the manifest itself.
64+
let manifestTextEdits = manifestEdits.map { edit in
65+
TextEdit(
66+
range: snapshot.range(of: edit.range),
67+
newText: edit.replacement
68+
)
69+
}
70+
71+
// If we couldn't figure out the manifest directory, or there are no
72+
// files to add, the only changes are the manifest edits. We're done
73+
// here.
74+
let manifestDirectoryURI = snapshot.uri.fileURL?
75+
.deletingLastPathComponent()
76+
guard let manifestDirectoryURI, !auxiliaryFiles.isEmpty else {
77+
return WorkspaceEdit(
78+
changes: [snapshot.uri: manifestTextEdits]
79+
)
80+
}
81+
82+
// Use the more full-featured documentChanges, which takes precedence
83+
// over the individual changes to documents.
84+
var documentChanges: [WorkspaceEditDocumentChange] = []
85+
86+
// Put the manifest changes into the array.
87+
documentChanges.append(
88+
.textDocumentEdit(
89+
.init(
90+
textDocument: .init(snapshot.uri, version: nil),
91+
edits: manifestTextEdits.map { .textEdit($0) }
92+
)
93+
)
94+
)
95+
96+
// Create an populate all of the auxiliary files.
97+
for (relativePath, contents) in auxiliaryFiles {
98+
guard let uri = URL(
99+
string: relativePath.pathString,
100+
relativeTo: manifestDirectoryURI
101+
) else {
102+
continue
103+
}
104+
105+
let documentURI = DocumentURI(uri)
106+
let createFile = CreateFile(
107+
uri: documentURI
108+
)
109+
110+
let zeroPosition = Position(line: 0, utf16index: 0)
111+
let edit = TextEdit(
112+
range: zeroPosition..<zeroPosition,
113+
newText: contents.description
114+
)
115+
116+
documentChanges.append(.createFile(createFile))
117+
documentChanges.append(
118+
.textDocumentEdit(
119+
.init(
120+
textDocument: .init(documentURI, version: nil),
121+
edits: [ .textEdit(edit) ]
122+
)
123+
)
124+
)
125+
}
126+
127+
return WorkspaceEdit(
128+
changes: [ snapshot.uri: manifestTextEdits ],
129+
documentChanges: documentChanges
130+
)
131+
}
132+
}
133+
134+
fileprivate extension SyntaxProtocol {
135+
// Find an enclosing call syntax expression.
136+
func findEnclosingCall() -> FunctionCallExprSyntax? {
137+
var current = Syntax(self)
138+
while true {
139+
if let call = current.as(FunctionCallExprSyntax.self) {
140+
return call
141+
}
142+
143+
if let parent = current.parent {
144+
current = parent
145+
continue
146+
}
147+
148+
return nil
149+
}
150+
}
151+
}
152+
153+
fileprivate extension FunctionCallExprSyntax {
154+
/// Find an argument with the given label that has a string literal as
155+
/// its argument.
156+
func findStringArgument(label: String) -> String? {
157+
for arg in arguments {
158+
if arg.label?.text == label {
159+
return arg.expression.as(StringLiteralExprSyntax.self)?
160+
.representedLiteralValue
161+
}
162+
}
163+
164+
return nil
165+
}
166+
}

Sources/SourceKitLSP/Swift/CodeActions/SyntaxCodeActions.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@ let allSyntaxCodeActions: [SyntaxCodeActionProvider.Type] = [
2020
FormatRawStringLiteral.self,
2121
MigrateToNewIfLetSyntax.self,
2222
OpaqueParameterToGeneric.self,
23+
PackageManifestEdits.self,
2324
RemoveSeparatorsFromIntegerLiteral.self,
2425
]

0 commit comments

Comments
 (0)