Skip to content

Add 'Generate enum associated value accessors' code action#2680

Open
ayush-that wants to merge 1 commit into
swiftlang:mainfrom
ayush-that:feature/generate-enum-associated-value-accessors
Open

Add 'Generate enum associated value accessors' code action#2680
ayush-that wants to merge 1 commit into
swiftlang:mainfrom
ayush-that:feature/generate-enum-associated-value-accessors

Conversation

@ayush-that

@ayush-that ayush-that commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Implements #2522.

Adds a syntactic code action on an enum that generates, per case, an is<Case> Boolean property and (for cases with associated values) an as<Case> property returning the value(s) as an optional.

For example, on:

enum Value {
    case text(String)
    case number(Int)
}

it generates asText / isText / asNumber / isNumber.

Question (from #2544): should is<Case> be generated for every case(current behavior), or only for cases with associated values?

Testing

Adds testGenerateEnumAssociatedValueAccessors.

Copilot AI review requested due to automatic review settings June 1, 2026 08:03

Copilot AI 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.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a new SwiftSyntax-based refactoring code action to generate as<Case>/is<Case> accessors for enum cases, and wires it into the code action registry with a corresponding test.

Changes:

  • Register the new GenerateEnumAssociatedValueAccessors provider in the global syntax code action list.
  • Implement the enum accessor generation refactoring provider.
  • Add a code action test asserting the expected workspace edit.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
Tests/SourceKitLSPTests/CodeActionTests.swift Adds a test validating the new “Generate enum associated value accessors” code action and its edit.
Sources/SwiftSyntaxCodeActions/SyntaxCodeActions.swift Registers the new code action provider in the aggregated provider list.
Sources/SwiftSyntaxCodeActions/GenerateEnumAssociatedValueAccessors.swift Implements the refactoring provider that inserts asX/isX computed properties for enum cases.
Sources/SwiftSyntaxCodeActions/CMakeLists.txt Includes the new Swift source file in the CMake build.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +2024 to +2026
markers: ["1️⃣"],
exhaustive: false
) { uri, positions in
Comment on lines +107 to +139
var accessors: [String] = []
for element in caseElements {
let caseName = element.name.identifier?.name ?? element.name.text
let capitalized = caseName.prefix(1).uppercased() + caseName.dropFirst()
let casePattern = element.name.text

if let parameters = element.parameterClause?.parameters, !parameters.isEmpty {
let asName = "as\(capitalized)"
if usedNames.insert(asName).inserted {
accessors.append(
makeAsAccessor(
name: asName,
casePattern: casePattern,
parameters: Array(parameters),
memberIndentation: memberIndentation,
bodyIndentation: bodyIndentation
)
)
}
}

let isName = "is\(capitalized)"
if usedNames.insert(isName).inserted {
accessors.append(
makeIsAccessor(
name: isName,
casePattern: casePattern,
memberIndentation: memberIndentation,
bodyIndentation: bodyIndentation
)
)
}
}
///
/// After:
///
/// ```swift

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 we should have two separate refactorings to generate the as and is names.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

okie. split into two.

Comment on lines +65 to +71
// Attached macros, `#if` blocks, and freestanding macro expansions can
// introduce cases or members that are not visible to a purely syntactic
// walk, which could produce incomplete accessors or collide with members
// added by macro expansion. Bail conservatively in those cases.
if !syntax.attributes.isEmpty {
throw RefactoringNotApplicableError("Enum has attributes that may be attached macros")
}

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 we need this bail-out. If we fail to generate one is accessor, that’s not a correctness issue and if it collides with a member generated by the macro, the user will get a compilation issue, which they can also decide how to fix, we shouldn’t try to make assumptions on their behalf.

Similarly, we can generate accessors if the enum contains #if since we also won’t produce a correctness issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

removed.

Comment on lines +94 to +97
// Infer indentation from the source rather than hardcoding it: members are
// indented one step deeper than the enum, and property bodies one step
// deeper than the members. Deriving the step from the existing members
// honors whatever width (or tabs) the file already uses.

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 this comment provides much value

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

removed.

Comment on lines +100 to +101
syntax.memberBlock.members.first?.firstToken(viewMode: .sourceAccurate)?.indentationOfLine.description
?? (enumIndentation + " ")

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.

We shouldn’t assume 4 space indentation. Use BasicFormat.inferIndentation to determine the file’s indentation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done.


if let parameters = element.parameterClause?.parameters, !parameters.isEmpty {
let asName = "as\(capitalized)"
if usedNames.insert(asName).inserted {

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.

We shouldn’t generate the same name twice, so I think it’s sufficient to check usedNames.contains(asName).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done.

@ayush-that ayush-that force-pushed the feature/generate-enum-associated-value-accessors branch from f77ad8a to ff61ae1 Compare June 5, 2026 10:21
@ayush-that ayush-that requested a review from ahoppen June 5, 2026 10:27
Comment on lines +30 to +31
guard !usedNames.contains(name) else { continue }
usedNames.insert(name)

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.

You should be able to combine this in one call as follows.

Suggested change
guard !usedNames.contains(name) else { continue }
usedNames.insert(name)
guard !usedNames.insert(name).inserted else { continue }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this code is gone now, along with the rest of the name tracking.

continue
}
let name = "as\(capitalizedCaseName(of: element))"
guard !usedNames.contains(name) else { continue }

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.

Why do we need to check for duplicate names here? Do you have an example?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

no. i removed the whole name tracking, including the skip for existing members. duplicate names now end up as compiler errors.

]
}
}

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.

Could you add a test case for the following source code?

enum Foo {
    case value(int: Int)
    case value(string: String)
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done. i've added testGenerateEnumCaseAsAccessorsForOverloadedCaseNames.

}
let types = parameters.map { $0.type.trimmedDescription }
let returnType: String
if parameters.count == 1 {

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.

If you write it as follows, you don’t need the [0] access below.

Suggested change
if parameters.count == 1 {
if let parameter = parameters.only {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done.

name: String,
casePattern: String,
parameters: [EnumCaseParameterSyntax],
indentation: (member: String, body: String)

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 passing the member and body indentation here, it would be cleaner to pass a base indentation and the indentation step so that we can compute the effective indentation in here. Otherwise you need to understand how indentation.member and indentation.body are used to correctly set it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done.

}
}

extension GenerateEnumCaseAsAccessors: SyntaxRefactoringCodeActionProvider {

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 having extensions of the same type in the same file, I think it’s clearer if we just throw all members into on struct declaration.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done.

newText: """


var asPoint: (Int, Int)? {

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 this should also contain the tuple labels, ie. this should be

Suggested change
var asPoint: (Int, Int)? {
var asPoint: (x: Int, y: Int)? {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done.

newText: """


var isFoo: 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.

I would rather generate duplicate cases here and have a compiler error that the user needs to fix instead of implicitly assuming that isFoo should refer to the underscore version of foo.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

i agree. i've removed the name tracking, both isFoo accessors get generated and the foo/Foo test checks the duplicate output.

@ayush-that ayush-that force-pushed the feature/generate-enum-associated-value-accessors branch from ff61ae1 to f431821 Compare June 12, 2026 18:16
@ayush-that

Copy link
Copy Markdown
Contributor Author
  1. the name tracking is gone completely
  2. is accessors are generated for every case, as accessors for cases with associated values
  3. duplicate names end up as compiler errors
  4. tuple return types keep their labels
  5. added overloaded test case

@ayush-that ayush-that requested a review from ahoppen June 13, 2026 06:38

@ahoppen ahoppen left a comment

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.

Thanks for the update. Now, if an accessor already exists, we will create a second copy of it, right? I don’t think we should do that. Also: If all accessors already exist, we shouldn’t suggest the refactoring action.

@ayush-that ayush-that force-pushed the feature/generate-enum-associated-value-accessors branch from f431821 to 4e6aeb2 Compare June 20, 2026 07:38
@ayush-that

Copy link
Copy Markdown
Contributor Author

You're right 😓. It duplicated, so I've added back the skip for accessor names that already exist, and when all of them already exist the action isn't offered anymore.

@ayush-that ayush-that requested a review from ahoppen June 20, 2026 07:44
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.

3 participants