Skip to content

Add new features to ConvertStoredPropertyToComputed#3229

Merged
ahoppen merged 1 commit intoswiftlang:mainfrom
myaumura:update-computed-properties-codeaction
Jan 19, 2026
Merged

Add new features to ConvertStoredPropertyToComputed#3229
ahoppen merged 1 commit intoswiftlang:mainfrom
myaumura:update-computed-properties-codeaction

Conversation

@myaumura
Copy link
Contributor

@myaumura myaumura commented Jan 7, 2026

As part of swiftlang/sourcekit-lsp#2424.
Add new functionality to the existing code action analogous to  ConvertToComputedProperty.cpp  and tryComputedPropertyFixIts.

  • Support removing lazy attribute if present.
  • Support removing the = sign if variable like var foo: Int = { return 0 }().

I'm happy to make changes if needed

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 your contribution, @myaumura.

@myaumura
Copy link
Contributor Author

myaumura commented Jan 7, 2026

@ahoppen, Thanks for your review!

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.

Looking at this again, I noticed that the = { … }() should already be handled.

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.

Sorry, looks like the trivia handling is going to be a little more complex than I initially thought. But nothing you won’t be able to do 😉

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I’m not sure it should be in this module


/// Removes attributes from a syntax tree while maintaining their surrounding trivia.
@_spi(Testing)
public class NodeRemover: SyntaxRewriter {
Copy link
Member

Choose a reason for hiding this comment

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

Let’s change this to the package access level. I think we didn’t use to have it when this was written.

//
//===----------------------------------------------------------------------===//

import Foundation
Copy link
Member

Choose a reason for hiding this comment

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

swift-syntax must not depend on Foundation because it’s lower in the stack than Foundation. Could you remove the Foundation dependency?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, my miss

return AttributeListSyntax(filteredAttributes)
}

public override func visit(_ node: DeclModifierListSyntax) -> DeclModifierListSyntax {
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, I was hoping that we could extend NodeRemover to be able to remove any arbitrary node. This doesn’t seem to be the case right now and I don’t want to have a semi-general type that promises to remove arbitrary nodes but isn’t actually able to do so. Maybe the better solution in that case is to extract all the common functionality (like the trivia convenience accessors) to the SwiftSyntax module and write another dedicated rewriter to remove decl modifiers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I’ll try to work on this idea

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I’d like to clarify — is the current purpose to extract the base logic into a NodeRemover class and have AttributeRemover and DeclModifierRemover inherit from it, with only strictly typed  visit  methods remaining in them?

Copy link
Member

Choose a reason for hiding this comment

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

No, I’m not proposing a type hierarchy. Essentially just create two separate classes and whatever you can factor out, factor out. I was mostly talking about at least sharing Trivia.trimmingPrefix etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hope I got it right this time, could you please check and sorry for the previous misunderstanding  

var filteredModifiers: [DeclModifierListSyntax.Element] = []

for modifier in node {
if self.predicate(Syntax(modifier)) {
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 read easier as a guard self.predicate(Syntax(modifier)) else {? At least it saves an indentation level.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done


for modifier in node {
if self.predicate(Syntax(modifier)) {
let trailingTrivia = modifier.trailingTrivia.trimmingPrefix(while: \.isSpaceOrTab)
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 need to trim the spaces and tabs here. Shouldn’t that be handled already when merging triviaToAttachToNextToken into existing trivia? Or am I missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When lazy is first before the comment, removing lazy leaves the space before the comment – it doesn't merge with trivia after comment.

case handled in test testRefactoringStoredPropertyWithModifiersAndComment

Copy link
Member

Choose a reason for hiding this comment

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

Oh, I see. That makes sense. Thanks for the explanation. Can you add that as a comment for future reference?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added comment

Comment on lines 102 to 107
if triviaToAttachToNextToken.isEmpty,
let previousToken = modifier.previousToken(viewMode: .sourceAccurate),
previousToken.trailingTrivia.isEmpty
{
triviaToAttachToNextToken = modifier.trailingTrivia
}
Copy link
Member

Choose a reason for hiding this comment

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

Why is this check necessary? Could you maybe add a doc comment that explains it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed this code

Comment on lines 135 to 136
defer { triviaToAttachToNextToken = Trivia() }
return syntaxNode.with(\.leadingTrivia, triviaToAttachToNextToken + syntaxNode.leadingTrivia)
Copy link
Member

Choose a reason for hiding this comment

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

I think we should get a little better performance if we don’t modify the node if triviaToAttachToNextToken is empty (that way we don’t need to allocate a new node), ie. just do an early exist if triviaToAttachToNextToken is empty.

Comment on lines 58 to 59
guard let keyword = $0.as(DeclModifierSyntax.self) else { return false }
return keyword.name.tokenKind == lazyKeyword.name.tokenKind
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn’t we be able to use the identity on Syntax to find the token to remove, ie. do something like the following

Suggested change
guard let keyword = $0.as(DeclModifierSyntax.self) else { return false }
return keyword.name.tokenKind == lazyKeyword.name.tokenKind
$0.id == lazyKeyword.id

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.

Nice. Just a few small comments, otherwise LGTM.


return DeclModifierListSyntax(filteredModifiers)
}

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 also need a visit function for Token that calls prependAndClearAccumulatedTrivia to transfer trivia to the token after the last modifier, eg. in lazy /* comment */ var test: Int?

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 add a test case for this so we don’t regress it in the future?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@ahoppen
Copy link
Member

ahoppen commented Jan 17, 2026

Just #3229 (comment) left, after that this is good to go 🚀

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.

Nice. I just realized that we didn’t report this refactoring action in SourceKit-LSP so far, fixed that in swiftlang/sourcekit-lsp#2458 because I was touching related code anyway.

@ahoppen
Copy link
Member

ahoppen commented Jan 18, 2026

@swift-ci Please test

1 similar comment
@ahoppen
Copy link
Member

ahoppen commented Jan 18, 2026

@swift-ci Please test

@ahoppen
Copy link
Member

ahoppen commented Jan 18, 2026

@swift-ci Please test Windows

@ahoppen
Copy link
Member

ahoppen commented Jan 18, 2026

@myaumura We can ignore the Bazel build failure, it is because of #3232 (comment).

@myaumura
Copy link
Contributor Author

@ahoppen, Thank you for the thorough review and detailed explanations—they really helped clarify the issues!

Should I squash commits?

@ahoppen
Copy link
Member

ahoppen commented Jan 18, 2026

Yes, squashing would be appreciated, it makes for a cleaner git history.

And it appears that the CMake build doesn’t support the package access level yet either (not just the Bazel build). Could you use @_spi(RawSyntax) public in the meantime instead?

@myaumura
Copy link
Contributor Author

Sure, doing it now!

@ahoppen
Copy link
Member

ahoppen commented Jan 18, 2026

@swift-ci Please test

@myaumura myaumura force-pushed the update-computed-properties-codeaction branch from ce91e33 to 4b2774d Compare January 18, 2026 21:23
@ahoppen
Copy link
Member

ahoppen commented Jan 18, 2026

Also FWIW, adding support for the package access modifier in #3239

@ahoppen
Copy link
Member

ahoppen commented Jan 18, 2026

@swift-ci Please test

@ahoppen
Copy link
Member

ahoppen commented Jan 18, 2026

@swift-ci Please test Windows

@ahoppen ahoppen merged commit 65b02a9 into swiftlang:main Jan 19, 2026
37 checks passed
@ahoppen
Copy link
Member

ahoppen commented Jan 19, 2026

Congrats to your first contribution to Swift, @myaumura 🚀

@myaumura myaumura deleted the update-computed-properties-codeaction branch January 19, 2026 09:22
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.

2 participants