Add Generate enum associated value accessors code action#2544
Add Generate enum associated value accessors code action#2544william-laverty wants to merge 1 commit into
Conversation
Adds a syntax-based code action that generates computed properties for
enums with associated values:
- `asX: T?` — extracts the associated value via pattern matching
- `isX: Bool` — checks if the value matches a given case
Handles single and multiple associated values (tuples). Skips cases
that already have corresponding accessors.
Example:
```swift
enum Value {
case text(String)
case number(Int)
}
```
Generates `asText`, `isText`, `asNumber`, `isNumber`.
Resolves #2522
| // Collect all cases with associated values. | ||
| let casesWithAssociatedValues = enumDecl.memberBlock.members.compactMap { member -> EnumCaseElementSyntax? in | ||
| guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self), | ||
| let element = caseDecl.elements.first, |
There was a problem hiding this comment.
Could you run swift-format on your PR as described in CONTRIBUTING.md
| // Collect all cases with associated values. | ||
| let casesWithAssociatedValues = enumDecl.memberBlock.members.compactMap { member -> EnumCaseElementSyntax? in | ||
| guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self), | ||
| let element = caseDecl.elements.first, |
There was a problem hiding this comment.
We have .only for this
| let element = caseDecl.elements.first, | |
| let element = caseDecl.elements.only, |
| /// } | ||
| /// ``` | ||
| /// Generates `asText`, `isText`, `asNumber`, `isNumber` computed properties. | ||
| struct GenerateEnumAssociatedValueAccessors: SyntaxCodeActionProvider { |
There was a problem hiding this comment.
Would it be possible to implement this as a SyntaxRefactoringCodeActionProvider?
|
|
||
| guard let enumDecl = node.findParentOfSelf( | ||
| ofType: EnumDeclSyntax.self, | ||
| stoppingIf: { _ in false } |
There was a problem hiding this comment.
We should stop at a CodeBlockSyntax same as in other code actions so we don’t offer this while the cursor is inside a function implementation. We should probably also stop at DeclSyntax so we don’t offer this while you’re in a nested type.
| } | ||
|
|
||
| if casesWithAssociatedValues.isEmpty { | ||
| return [] |
There was a problem hiding this comment.
You should throw a RefactoringNotApplicableError if the refactoring action doesn’t apply because that will hide it in SourceKit-LSP.
| } else { | ||
| let tupleTypes = params.map { $0.type.trimmedDescription } | ||
| let returnType = "(\(tupleTypes.joined(separator: ", ")))" | ||
| let bindingVars = (0..<params.count).map { "v\($0)" } |
There was a problem hiding this comment.
We should attempt to pick more descriptive variable names here. If the associated value has a label, we should pick that. Otherwise, I’d use value2 instead of v2.
| accessors.append( | ||
| """ | ||
| var \(asName): \(returnType)? { | ||
| if case let .\(caseName)(\(bindingPattern)) = self { return (\(bindingPattern)) } |
There was a problem hiding this comment.
We should infer the indentation of the source file (see how other code actions do this) instead of hardcoding it to 4 spaces.
| uri: [ | ||
| TextEdit( | ||
| range: positions["2️⃣"]..<positions["2️⃣"], | ||
| newText: "\n var asText: String? {\n if case let .text(v) = self { return v }\n return nil\n }\n\n var isText: Bool {\n if case .text = self { return true }\n return false\n }\n\n var asNumber: Int? {\n if case let .number(v) = self { return v }\n return nil\n }\n\n var isNumber: Bool {\n if case .number = self { return true }\n return false\n }\n" |
There was a problem hiding this comment.
I think this would be a lot more readable as a multi-line string literal.
| 2️⃣} | ||
| """##, | ||
| ranges: [("1️⃣", "2️⃣")], | ||
| exhaustive: false |
There was a problem hiding this comment.
You need to pass exhaustive: true if you want to check that a code action doesn’t exist.
| } | ||
|
|
||
| if casesWithAssociatedValues.isEmpty { | ||
| return [] |
There was a problem hiding this comment.
Is there any reason why we shouldn’t generate the is accessors for cases without associated values?
Description
Adds a syntax-based code action that generates computed properties for enums with associated values.
For each case with associated values, generates:
asX: T?— extracts the associated value via pattern matching, ornilfor other casesisX: Bool— returnstrueif the value matches the caseBefore:
After:
Handles single and multiple associated values (tuples). Skips generation for accessors that already exist. Inspired by rust-analyzer's
generate_enum_as_method.Tests
Resolves #2522