Skip to content

Swift package manifest refactoring actions #2904

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
Aug 8, 2025

Conversation

DougGregor
Copy link
Member

@DougGregor DougGregor commented Dec 1, 2024

Port the package manifest editing refactoring actions over to SwiftRefactor. These refactoring operations come from the swift-package-manager repository, and are being moved here for several reasons:

  1. They are shared by SwiftPM and SourceKit-LSP already, and are likely to also get used in the compiler, so swift-syntax is a good place for them to be to avoid code duplication.
  2. The SwiftRefactor module is where we are collecting various refactoring.
  3. Putting them here completely separates them from the semantic package model, so it's clear that we are just dealing with syntactic transforms.

This ports all of the package manifest refactoring actions over from swift-package-manager (add package dependency, add target, add product, add target dependency) and introduces a new one that will be used by swift-package-manager in the future, "add plugin usage". The code itself is little changed from the SwiftPM version, although the ManifestEditRefactoringProvider protocol that unifies them all is new.

Note that we end up bringing over syntactic versions of various SwiftPM types like TargetDescription, but in a string-backed, simplified form that is purely syntactic. These are needed to community with the refactoring actions, and can easily be constructed by clients.

@DougGregor
Copy link
Member Author

@swift-ci please test

1 similar comment
@DougGregor
Copy link
Member Author

@swift-ci please test

@DougGregor
Copy link
Member Author

@swift-ci please test Windows

@DougGregor DougGregor force-pushed the package-manifest-refactor branch from f371a2c to 4061163 Compare December 1, 2024 16:57
@DougGregor
Copy link
Member Author

@swift-ci please test

2 similar comments
@DougGregor
Copy link
Member Author

@swift-ci please test

@DougGregor
Copy link
Member Author

@swift-ci please test

@DougGregor
Copy link
Member Author

@swift-ci please test Windows

@DougGregor
Copy link
Member Author

@swift-ci please test

1 similar comment
@DougGregor
Copy link
Member Author

@swift-ci please test

@DougGregor
Copy link
Member Author

@swift-ci please test Linux

@DougGregor
Copy link
Member Author

@swift-ci please test macOS

Copy link
Member

@ahoppen ahoppen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for moving the code, Doug.

Start porting the package manifest editing operations from the Swift
Package Manager package over here, so that all of the syntactic
refactorings are together in one common place. These refactorings are
needed by a number of tools, including SwiftPM, SourceKit-LSP, and
(soon) the Swift compiler itself, which can all depend on
swift-syntax.

The implementation here stubs out the various types used to describe
package syntax, using simple string-backed types in place of some of
the semantic types that are part of SwiftPM itself, such as
SemanticVersion or SourceControlURL. I've also introduced the notion
of a ManifestEditRefactoringProvider to generalize over all of the
package manifest editing operations.

This commit ports over the "Add Package Dependency" command and its
tests.
…Refactor

This manifest editing action introduces a new dependency to an existing target
…for a target

This allows one to add a plugin usage to a given target, given the target
name and description of how the plugin should be used.
This removes a conflict with the SwiftPM version.
@xedin xedin force-pushed the package-manifest-refactor branch from ede1bea to 6af939f Compare August 5, 2025 07:20
xedin added 11 commits August 7, 2025 11:38
…sError`

Removes `XCTAssertThrows` in favor of XCTest standard way of
testing throwing functions.
…ents

When inserting a new element to an array or new argument to a call
let's only copy the indentation of the previous element instead of
whole leading trivia that includes comments.
…String`

All of these types are simple placeholders already with one exception
`RelativePath` which used to differentiate path separators on Windows
and other platforms, which is not strictly necessary any longer.
…ring`

This removes final stub type that could simply be a string and instead
of hard-coding a version of swift-syntax library for new dependencies,
let's use a placeholder.
…ft settings

Allows to add `enable{Upcoming, Experimental}Feature`, `swiftLanguageMode`,
`strictMemorySafety` as well as custom ones.
…always accept a string

This matches `PackageDescription` which explicitly states that
all source control dependencies expect a URL.
@xedin xedin force-pushed the package-manifest-refactor branch from a0d3cfe to b896109 Compare August 7, 2025 20:28
@xedin xedin requested review from bnbarham and ahoppen August 7, 2025 20:32
@xedin
Copy link
Contributor

xedin commented Aug 7, 2025

@swift-ci please test

public protocol ManifestEditRefactoringProvider: EditRefactoringProvider
where Self.Input == SourceFileSyntax {

static func manifestRefactor(syntax: SourceFileSyntax, in context: Context) throws -> PackageEdit
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I'm planning to follow-up to this PR and remove this, I think all we need to do is change result type of textRefactor to not only produce edits but also auxiliary files.

/// Add a package dependency to a package manifest's source code.
public struct AddPackageDependency: ManifestEditRefactoringProvider {
public struct Context {
public var dependency: PackageDependency
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm tempted to say this should be optional and we could add placeholder if not given?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd think that the whole point of the refactoring is to actually add a dependency that's why it's non-optional at the moment, I'm not sure how we'd placeholder that efficiently because different dependency kinds have different arguments...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how we'd placeholder that efficiently

We'd just pick a reasonable default, ie. SourceControl and from:

.package(url: <#url#>, from: <#version#>)

(though those placeholders could also be typed)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe that is the way to go here but instead of having it optional we can make it a defaulted parameter on initializer in both cases.

/// Add a target to a manifest's source code.
public struct AddPackageTarget: ManifestEditRefactoringProvider {
public struct Context {
public let target: PackageTarget
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as adding a package dependency, I could see it being useful to add a placeholder target. Although maybe I'm wrong and you could just use completion for that. Thoughts?

Copy link
Member

@ahoppen ahoppen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this from a swift-syntax standpoint, not from a SwiftPM correctness point of view.

A couple high-level comments:

  1. This PR is quite large and I think we might need at least one more round of reviews. Would it be possible to split it up, eg. by adding only one refactoring action in this one.
  2. I am sceptical about adding SwiftPM-specific types to swift-syntax (or generally the amount of public API surface that this PR adds). Should we consider making these types SPI instead?
  3. For the TODO/FIXMEs in the codebase, could you file issues if you’re not planning to address them in this PR? In my experience they just get lost somehow and some of them probably make for good first issues.
  4. Should these refactorings live in a separte module (eg. SwiftPackageRefactor)? I think that would be a good separation of general Swift language refactorings and refactorings that have knowledge about SwiftPM package manifests. Ideally, we could consider moving that module to a separate repo at some point in the future. I’m not really a fan of adding SwiftPM-specific types to swift-syntax. I think in a perfect world, these types would live in some repository that depends on swift-syntax and that both SourceKit-LSP and SwiftPM can depend on. Would it make sense to mark these types as SPI for now?


import SwiftSyntax

public protocol ManifestEditRefactoringProvider: EditRefactoringProvider
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be a public protocol? I think it would be better of EditRefactoringProvider would gain the ability to modify auxiliary files. At that point a package refactoring wouldn’t be anything special, it would just be a normal refactoring.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comment about this - #2904 (comment)

static func manifestRefactor(syntax: SourceFileSyntax, in context: Context) throws -> PackageEdit
}

extension EditRefactoringProvider where Self: ManifestEditRefactoringProvider {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn’t this be simplified to the following? Or doesn’t that work?

Suggested change
extension EditRefactoringProvider where Self: ManifestEditRefactoringProvider {
extension ManifestEditRefactoringProvider {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ You are right, it doesn't require Self: could be placed directly.

case registry(Registry)

public struct FileSystem: Sendable {
public let nameForTargetDependencyResolutionOnly: String?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That name is quite a mouthful. If this is the best name, could you add a comment that describes what it does? Maybe an example would be the best way to convey what both of these members do – also for SourceControl and Registry.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly seems like this could just be name, I don't think the "for target dependency resolution" part really matters

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this completely since we don't really need that for refactorings.

}

extension PackageDependency: ManifestSyntaxRepresentable {
func asSyntax() -> ExprSyntax {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this make more sense as a computed property?


extension PackageDependency.SourceControl: ManifestSyntaxRepresentable {
func asSyntax() -> ExprSyntax {
// TODO: Not handling identity, nameForTargetDependencyResolutionOnly,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you file an issue for this if you’re not planning to address it in this PR. My experience is that these TODOs will just be forgotten. And this would probably also make for a nice good first issue.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should handle name now and remove the TODO

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ This TODO was outdated and I removed it, nameFor* is SwiftPM concept that shouldn't be involved here and source control could be identified by its location.


fileprivate extension SyntaxProtocol {
/// Replace the given child with a new child node.
func replacingChild(_ childNode: Syntax, with newChildNode: Syntax) -> Self {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we ever replace nodes with nodes of a different type? If so, this would fail the cast(Self.self) if self == childNode. If we don’t need to replace nodes of different types, this could be

Suggested change
func replacingChild(_ childNode: Syntax, with newChildNode: Syntax) -> Self {
func replacingChild<T: SyntaxProtocol>(_ childNode: T, with newChildNode: T) -> Self {


// Format the element appropriately for the context.
let indentation = Trivia(
pieces: arg.leadingTrivia.filter(\.isSpaceOrTab)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be using indentaitonOfLine or something like that to cover if arg is not on a new line?

}

let importDecls = importModuleNames.lazy.sorted().map { name in
DeclSyntax("import \(raw: name)").with(\.trailingTrivia, .newline)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can’t this just be

Suggested change
DeclSyntax("import \(raw: name)").with(\.trailingTrivia, .newline)
DeclSyntax("import \(raw: name)\n")

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let sourceFileText: SourceFileSyntax
switch target.type {
case .binary, .plugin, .system:
fatalError("should have exited above")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is above? I don’t see the check where this should have exited.

}

fileprivate extension String {
func localizedFirstWordCapitalized() -> String { prefix(1).uppercased() + dropFirst() }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What’s the localize part of this function name?

@xedin xedin force-pushed the package-manifest-refactor branch from f72f9d9 to 22bbcde Compare August 8, 2025 17:45
xedin added 2 commits August 8, 2025 10:48
…porting types as SPI

Avoid increasing public API surface of the swift-syntax for now
until the factorings are fully evaluated.
@xedin xedin force-pushed the package-manifest-refactor branch from 22bbcde to 2f6937d Compare August 8, 2025 17:48
@xedin
Copy link
Contributor

xedin commented Aug 8, 2025

We have discussed this and decided to SPI new refactorings for now.

@xedin
Copy link
Contributor

xedin commented Aug 8, 2025

@swift-ci please test

@xedin
Copy link
Contributor

xedin commented Aug 8, 2025

@swift-ci please test Windows platform

Copy link
Member

@ahoppen ahoppen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good with me, assuming all declarations are at most SPI (ie. no public declarations).

@xedin xedin merged commit c4002f2 into swiftlang:main Aug 8, 2025
30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants