Skip to content

Add Expand Derived Conformance refactoring - #2780

Open
Paul-Passeron wants to merge 5 commits into
swiftlang:mainfrom
Paul-Passeron:derconf-clean
Open

Paul-Passeron wants to merge 5 commits into
swiftlang:mainfrom
Paul-Passeron:derconf-clean

Conversation

@Paul-Passeron

@Paul-Passeron Paul-Passeron commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

Following the work in, and depending on swiftlang/swift#91811.

Adds a code action to expand conformances that the compiler derives through macros (the experimental DeriveConformancesViaMacros feature).
The expansion is shown the same way as Expand Macro and reuses a lot of the same machinery.

  • Adds ExpandDerivedConformanceCommand, which wraps sourcekitd's source.refactoring.kind.expand.derived_conformance action. It is offered only when sourcekitd reports that action, so projects that don't use the feature shouldn't see it.
  • Makes MacroExpansionManager work with any expansion refactoring, not just macros, so that workspace/getReferenceDocument can resolve derived-conformance buffers.
  • Adds testExpandDerivedConformance. It is skipped on toolchains that don't support the refactoring.

@Paul-Passeron

Copy link
Copy Markdown
Member Author

@swift-ci please test

@hamishknight
hamishknight self-requested a review September 28, 2026 13:21
@Paul-Passeron
Paul-Passeron marked this pull request as ready for review September 28, 2026 17:06
@Paul-Passeron

Copy link
Copy Markdown
Member Author

@swift-ci please test windows

@hamishknight hamishknight left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you!

try await SkipUnless.sourcekitdSupportsExpandDerivedConformance()

let testClient = try await TestSourceKitLSPClient(
options: SourceKitLSPOptions.deriveConformancesViaMacros(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think it's also worth adding a test case that makes sure we don't get the refactoring action when we don't pass the experimental feature

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done !


/// The sourcekitd identifier of the refactoring action.
package var actionString = "source.refactoring.kind.expand.macro"
package static let actionString = "source.refactoring.kind.expand.macro"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note that this change drops it from the JSON encoding, any concerns with that @rintaro @ahoppen?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it's safe.

Comment on lines +49 to +55
/// A cursor position is only ever applicable to a single one of them. Expanding a
/// macro requires the cursor to be on the macro, and expanding a derived conformance
/// requires it to be on a protocol in the inheritance clause.
private static let expansionActionUIDs = [
ExpandMacroCommand.actionString,
ExpandDerivedConformanceCommand.actionString,
]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm, in principle I think we could track whether a MacroExpansionReferenceDocumentURLData is for a regular macro expansion or derived conformance since it's produced by expandMacro. What if we tracked the action string there? Any thoughts @rintaro @ahoppen?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Embedding the information in MacroExpansionReferenceDocumentURLData sounds like a reasonable idea. It's not worth to make a separate ReferenceDocumentURL case.

Comment on lines +1076 to +1078
if action.actionString == ExpandDerivedConformanceCommand.actionString {
canExpandDerivedConformance = true
return nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I wonder if we should also consider exposing an "inline" refactoring for cases where you want to explicitly have the witnesses written?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

My concern with this is Codable since the type witnesses aren't synthesised by macro, they are missing after inlining and you end up with invalid code. I remember the issue being that type witnesses could be requested multiple times which didn't play well with the macro expansions but I could be wrong.

Copy link
Copy Markdown
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 that's a reason not to provide the refactoring, we really should just include CodingKeys in the expansion even if it isn't strictly a witness

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I added the inline refactoring

)
} catch {
logger.debug("Failed to run \(actionUID) for \(macroExpansionURLData.bufferName): \(error.forLogging)")
continue

@rintaro rintaro Sep 30, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(Not relevant anymore if we embed the UID information in the URL, but)

Mild concern with this possibly hides the real error like sourcekitd errors, or cancellation.
At least we should rethrow CancellationError. For other errors, I'd rethrow the last real error caught.

}
}

extension SourceKitLSPOptions {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don’t think that SkipUnless is the right file for this extension. I’d move it to a separate file.

package static func deriveConformancesViaMacros(enabled: Bool = true) async throws -> SourceKitLSPOptions {
let toolchain = try await unwrap(ToolchainRegistry.forTesting.default)
var options = try await SourceKitLSPOptions.testDefault()
var flags = ["-plugin-path", try toolchain.path.appending(components: "lib", "swift", "host", "plugins").filePath]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Instead of setting this on tests, should these arguments be included in the fallback arguments we use inside SourceKit-LSP?

If we do this, we might not need this extension at all. SourceKitLSPOptions.testDefault could just gain a parameter of the experimental features to enable.

)
)
guard case .commands(let commands) = response else {
XCTFail("Expected commands, got \(String(describing: response))")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
XCTFail("Expected commands, got \(String(describing: response))")
XCTFail("Expected commands, got \(response)")

let expansionURI = try XCTUnwrap(peekDocumentsRequestURIs.value?.only)
let expansion = try await testClient.send(GetReferenceDocumentRequest(uri: expansionURI))
XCTAssert(
expansion.content.contains("static func __derived_struct_equals(_ lhs: Self, _ rhs: Self) -> Swift::Bool"),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

High-level question: Is this generated function really something we want to surface to users? If we allow them to inline it, they may start modifying it instead of implementing a custom func ==, which would probably be the preferred solution.


XCTAssertEqual(innerMacroExpansion.content, #"(1 + 2, "1 + 2")"#)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just anchoring this conversation at a random place.

I suppose this would also be able to generate explicit Codable conformances, right? If so, should we suppress the Add Explicit Coddle Implementation refactoring currently offered by sourcekitd? Or is this already happening on the sourcekitd side?

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