Skip to content

Commit 5c2b398

Browse files
authored
[Commands] Update AddTarget command to add auxiliary files for new … (#9043)
…targets ### Motivation: The command currently relies on the refactoring action to supply it with auxiliary files to add, but we'd like refactoring actions to only be responsible for manifest updates and everything else should be done either by a command itself or trigger additional commands in case the refactoring is happening via something like sourcekit-lsp. ### Modifications: - Add `addAuxiliaryFiles` and `createAuxiliaryFile` (previously part of `PackageEdit.applyEdits`) methods to `AddTarget` command to add new primary and other files to the target directory once the manifest has been updated. ### Result: This helps us to remove `ManifestEditRefactoringProvider` (all of the manifest refactoring actions are going to conform to `EditRefactoringProvider` protocol directly) and `PackageEdit` types from swift-syntax and adjust all of the package manifest refactoring actions to produce `[SourceEdit]` instead.
1 parent f042205 commit 5c2b398

File tree

3 files changed

+331
-78
lines changed

3 files changed

+331
-78
lines changed

Sources/Commands/PackageCommands/AddTarget.swift

Lines changed: 252 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,17 @@ import PackageModel
1919
import SwiftParser
2020
@_spi(PackageRefactor) import SwiftRefactor
2121
import SwiftSyntax
22-
import TSCBasic
23-
import TSCUtility
22+
import SwiftSyntaxBuilder
2423
import Workspace
2524

26-
extension AddPackageTarget.TestHarness: @retroactive ExpressibleByArgument { }
25+
import struct TSCBasic.ByteString
26+
import struct TSCBasic.StringError
27+
28+
extension AddPackageTarget.TestHarness: @retroactive ExpressibleByArgument {}
29+
30+
/// The array of auxiliary files that can be added by a package editing
31+
/// operation.
32+
private typealias AuxiliaryFiles = [(RelativePath, SourceFileSyntax)]
2733

2834
extension SwiftPackageCommand {
2935
struct AddTarget: AsyncSwiftCommand {
@@ -36,7 +42,8 @@ extension SwiftPackageCommand {
3642
}
3743

3844
package static let configuration = CommandConfiguration(
39-
abstract: "Add a new target to the manifest.")
45+
abstract: "Add a new target to the manifest."
46+
)
4047

4148
@OptionGroup(visibility: .hidden)
4249
var globalOptions: GlobalOptions
@@ -62,7 +69,9 @@ extension SwiftPackageCommand {
6269
@Option(help: "The checksum for a remote binary target.")
6370
var checksum: String?
6471

65-
@Option(help: "The testing library to use when generating test targets, which can be one of 'xctest', 'swift-testing', or 'none'.")
72+
@Option(
73+
help: "The testing library to use when generating test targets, which can be one of 'xctest', 'swift-testing', or 'none'."
74+
)
6675
var testingLibrary: AddPackageTarget.TestHarness = .default
6776

6877
func run(_ swiftCommandState: SwiftCommandState) async throws {
@@ -92,46 +101,55 @@ extension SwiftPackageCommand {
92101
}
93102

94103
// Move sources into their own folder if they're directly in `./Sources`.
95-
try await moveSingleTargetSources(
104+
try await self.moveSingleTargetSources(
96105
workspace: workspace,
97106
packagePath: packagePath,
98-
verbose: !globalOptions.logging.quiet,
107+
verbose: !self.globalOptions.logging.quiet,
99108
observabilityScope: swiftCommandState.observabilityScope
100109
)
101110

102111
// Map the target type.
103112
let type: PackageTarget.TargetKind = switch self.type {
104-
case .library: .library
105-
case .executable: .executable
106-
case .test: .test
107-
case .macro: .macro
113+
case .library: .library
114+
case .executable: .executable
115+
case .test: .test
116+
case .macro: .macro
108117
}
109118

110119
// Map dependencies
111120
let dependencies: [PackageTarget.Dependency] = self.dependencies.map {
112121
.byName(name: $0)
113122
}
114123

124+
let target = PackageTarget(
125+
name: name,
126+
type: type,
127+
dependencies: dependencies,
128+
path: path,
129+
url: url,
130+
checksum: checksum
131+
)
115132
let editResult = try AddPackageTarget.manifestRefactor(
116133
syntax: manifestSyntax,
117134
in: .init(
118-
target: .init(
119-
name: name,
120-
type: type,
121-
dependencies: dependencies,
122-
path: path,
123-
url: url,
124-
checksum: checksum
125-
),
126-
testHarness: testingLibrary
135+
target: target,
136+
testHarness: self.testingLibrary
127137
)
128138
)
129139

130140
try editResult.applyEdits(
131141
to: fileSystem,
132142
manifest: manifestSyntax,
133143
manifestPath: manifestPath,
134-
verbose: !globalOptions.logging.quiet
144+
verbose: !self.globalOptions.logging.quiet
145+
)
146+
147+
// Once edits are applied, it's time to create new files for the target.
148+
try self.addAuxiliaryFiles(
149+
target: target,
150+
testHarness: self.testingLibrary,
151+
fileSystem: fileSystem,
152+
rootPath: manifestPath.parentDirectory
135153
)
136154
}
137155

@@ -140,7 +158,7 @@ extension SwiftPackageCommand {
140158
// the target before adding a new target.
141159
private func moveSingleTargetSources(
142160
workspace: Workspace,
143-
packagePath: Basics.AbsolutePath,
161+
packagePath: AbsolutePath,
144162
verbose: Bool = false,
145163
observabilityScope: ObservabilityScope
146164
) async throws {
@@ -185,6 +203,218 @@ extension SwiftPackageCommand {
185203
}
186204
}
187205
}
206+
207+
private func createAuxiliaryFile(
208+
fileSystem: any FileSystem,
209+
rootPath: AbsolutePath,
210+
filePath: RelativePath,
211+
contents: SourceFileSyntax,
212+
verbose: Bool = false
213+
) throws {
214+
// If the file already exists, skip it.
215+
let filePath = rootPath.appending(filePath)
216+
if fileSystem.exists(filePath) {
217+
if verbose {
218+
print("Skipping \(filePath.relative(to: rootPath)) because it already exists.")
219+
}
220+
221+
return
222+
}
223+
224+
// If the directory does not exist yet, create it.
225+
let fileDir = filePath.parentDirectory
226+
if !fileSystem.exists(fileDir) {
227+
if verbose {
228+
print("Creating directory \(fileDir.relative(to: rootPath))...", terminator: "")
229+
}
230+
231+
try fileSystem.createDirectory(fileDir, recursive: true)
232+
233+
if verbose {
234+
print(" done.")
235+
}
236+
}
237+
238+
// Write the file.
239+
if verbose {
240+
print("Writing \(filePath.relative(to: rootPath))...", terminator: "")
241+
}
242+
243+
try fileSystem.writeFileContents(
244+
filePath,
245+
string: contents.description
246+
)
247+
248+
if verbose {
249+
print(" done.")
250+
}
251+
}
252+
253+
private func addAuxiliaryFiles(
254+
target: PackageTarget,
255+
testHarness: AddPackageTarget.TestHarness,
256+
fileSystem: any FileSystem,
257+
rootPath: AbsolutePath
258+
) throws {
259+
let outerDirectory: String? = switch target.type {
260+
case .binary, .plugin, .system: nil
261+
case .executable, .library, .macro: "Sources"
262+
case .test: "Tests"
263+
}
264+
265+
guard let outerDirectory else {
266+
return
267+
}
268+
269+
let targetDir = try RelativePath(validating: outerDirectory).appending(component: target.name)
270+
let sourceFilePath = targetDir.appending(component: "\(target.name).swift")
271+
272+
// Introduce imports for each of the dependencies that were specified.
273+
var importModuleNames = target.dependencies.map {
274+
switch $0 {
275+
case .byName(let name),
276+
.target(let name),
277+
.product(let name, package: _):
278+
name
279+
}
280+
}
281+
282+
// Add appropriate test module dependencies.
283+
if target.type == .test {
284+
switch testHarness {
285+
case .none:
286+
break
287+
288+
case .xctest:
289+
importModuleNames.append("XCTest")
290+
291+
case .swiftTesting:
292+
importModuleNames.append("Testing")
293+
}
294+
}
295+
296+
let importDecls = importModuleNames.lazy.sorted().map { name in
297+
DeclSyntax("import \(raw: name)\n")
298+
}
299+
300+
let imports = CodeBlockItemListSyntax {
301+
for importDecl in importDecls {
302+
importDecl
303+
}
304+
}
305+
306+
var files: AuxiliaryFiles = []
307+
switch target.type {
308+
case .binary, .plugin, .system:
309+
break
310+
311+
case .macro:
312+
files.addSourceFile(
313+
path: sourceFilePath,
314+
sourceCode: """
315+
\(imports)
316+
struct \(raw: target.sanitizedName): Macro {
317+
/// TODO: Implement one or more of the protocols that inherit
318+
/// from Macro. The appropriate macro protocol is determined
319+
/// by the "macro" declaration that \(raw: target.sanitizedName) implements.
320+
/// Examples include:
321+
/// @freestanding(expression) macro --> ExpressionMacro
322+
/// @attached(member) macro --> MemberMacro
323+
}
324+
"""
325+
)
326+
327+
// Add a file that introduces the main entrypoint and provided macros
328+
// for a macro target.
329+
files.addSourceFile(
330+
path: targetDir.appending(component: "ProvidedMacros.swift"),
331+
sourceCode: """
332+
import SwiftCompilerPlugin
333+
334+
@main
335+
struct \(raw: target.sanitizedName)Macros: CompilerPlugin {
336+
let providingMacros: [Macro.Type] = [
337+
\(raw: target.sanitizedName).self,
338+
]
339+
}
340+
"""
341+
)
342+
343+
case .test:
344+
let sourceCode: SourceFileSyntax = switch testHarness {
345+
case .none:
346+
"""
347+
\(imports)
348+
// Test code here
349+
"""
350+
351+
case .xctest:
352+
"""
353+
\(imports)
354+
class \(raw: target.sanitizedName)Tests: XCTestCase {
355+
func test\(raw: target.sanitizedName)() {
356+
XCTAssertEqual(42, 17 + 25)
357+
}
358+
}
359+
"""
360+
361+
case .swiftTesting:
362+
"""
363+
\(imports)
364+
@Suite
365+
struct \(raw: target.sanitizedName)Tests {
366+
@Test("\(raw: target.sanitizedName) tests")
367+
func example() {
368+
#expect(42 == 17 + 25)
369+
}
370+
}
371+
"""
372+
}
373+
374+
files.addSourceFile(path: sourceFilePath, sourceCode: sourceCode)
375+
376+
case .library:
377+
files.addSourceFile(
378+
path: sourceFilePath,
379+
sourceCode: """
380+
\(imports)
381+
"""
382+
)
383+
384+
case .executable:
385+
files.addSourceFile(
386+
path: sourceFilePath,
387+
sourceCode: """
388+
\(imports)
389+
@main
390+
struct \(raw: target.sanitizedName)Main {
391+
static func main() {
392+
print("Hello, world")
393+
}
394+
}
395+
"""
396+
)
397+
}
398+
399+
for (file, sourceCode) in files {
400+
try self.createAuxiliaryFile(
401+
fileSystem: fileSystem,
402+
rootPath: rootPath,
403+
filePath: file,
404+
contents: sourceCode,
405+
verbose: !self.globalOptions.logging.quiet
406+
)
407+
}
408+
}
188409
}
189410
}
190411

412+
extension AuxiliaryFiles {
413+
/// Add a source file to the list of auxiliary files.
414+
fileprivate mutating func addSourceFile(
415+
path: RelativePath,
416+
sourceCode: SourceFileSyntax
417+
) {
418+
self.append((path, sourceCode))
419+
}
420+
}

Sources/Commands/Utilities/RefactoringSupport.swift

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -42,46 +42,5 @@ package extension PackageEdit {
4242
if verbose {
4343
print(" done.")
4444
}
45-
46-
// Write all of the auxiliary files.
47-
for (auxiliaryFileRelPath, auxiliaryFileSyntax) in auxiliaryFiles {
48-
// If the file already exists, skip it.
49-
let filePath = try rootPath.appending(RelativePath(validating: auxiliaryFileRelPath))
50-
if filesystem.exists(filePath) {
51-
if verbose {
52-
print("Skipping \(filePath.relative(to: rootPath)) because it already exists.")
53-
}
54-
55-
continue
56-
}
57-
58-
// If the directory does not exist yet, create it.
59-
let fileDir = filePath.parentDirectory
60-
if !filesystem.exists(fileDir) {
61-
if verbose {
62-
print("Creating directory \(fileDir.relative(to: rootPath))...", terminator: "")
63-
}
64-
65-
try filesystem.createDirectory(fileDir, recursive: true)
66-
67-
if verbose {
68-
print(" done.")
69-
}
70-
}
71-
72-
// Write the file.
73-
if verbose {
74-
print("Writing \(filePath.relative(to: rootPath))...", terminator: "")
75-
}
76-
77-
try filesystem.writeFileContents(
78-
filePath,
79-
string: auxiliaryFileSyntax.description
80-
)
81-
82-
if verbose {
83-
print(" done.")
84-
}
85-
}
8645
}
8746
}

0 commit comments

Comments
 (0)