Skip to content

Add Generate enum associated value accessors code action#2544

Closed
william-laverty wants to merge 1 commit into
swiftlang:mainfrom
william-laverty:generate-enum-associated-value-accessors
Closed

Add Generate enum associated value accessors code action#2544
william-laverty wants to merge 1 commit into
swiftlang:mainfrom
william-laverty:generate-enum-associated-value-accessors

Conversation

@william-laverty

Copy link
Copy Markdown

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, or nil for other cases
  • isX: Bool — returns true if the value matches the case

Before:

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

After:

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

    var asText: String? {
        if case let .text(v) = self { return v }
        return nil
    }

    var isText: Bool {
        if case .text = self { return true }
        return false
    }

    var asNumber: Int? {
        if case let .number(v) = self { return v }
        return nil
    }

    var isNumber: Bool {
        if case .number = self { return true }
        return false
    }
}

Handles single and multiple associated values (tuples). Skips generation for accessors that already exist. Inspired by rust-analyzer's generate_enum_as_method.

Tests

  • Test for enum with two cases having associated values
  • Test that no action is offered for enums without associated values

Resolves #2522

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,

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 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,

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 have .only for this

Suggested change
let element = caseDecl.elements.first,
let element = caseDecl.elements.only,

/// }
/// ```
/// Generates `asText`, `isText`, `asNumber`, `isNumber` computed properties.
struct GenerateEnumAssociatedValueAccessors: SyntaxCodeActionProvider {

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.

Would it be possible to implement this as a SyntaxRefactoringCodeActionProvider?


guard let enumDecl = node.findParentOfSelf(
ofType: EnumDeclSyntax.self,
stoppingIf: { _ in false }

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 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 []

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 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)" }

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 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)) }

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 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"

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 would be a lot more readable as a multi-line string literal.

2️⃣}
"""##,
ranges: [("1️⃣", "2️⃣")],
exhaustive: false

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 need to pass exhaustive: true if you want to check that a code action doesn’t exist.

}

if casesWithAssociatedValues.isEmpty {
return []

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.

Is there any reason why we shouldn’t generate the is accessors for cases without associated values?

@william-laverty william-laverty closed this by deleting the head repository Mar 10, 2026
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.

Add Generate enum associated value accessors code action

2 participants