Skip to content

Commit f431821

Browse files
committed
Add 'as'/'is' enum case accessor code actions
Replaces the single accessor refactoring with two: "Generate 'as' accessors for enum cases" adds an `as<Case>` property for each case with associated values, and "Generate 'is' accessors for enum cases" adds an `is<Case>` Boolean for every case. Resolves #2522.
1 parent d65afbf commit f431821

4 files changed

Lines changed: 572 additions & 0 deletions

File tree

Sources/SwiftSyntaxCodeActions/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ add_library(SwiftSyntaxCodeActions STATIC
66
ConvertIntegerLiteral.swift
77
ConvertJSONToCodableStruct.swift
88
ConvertStringConcatenationToStringInterpolation.swift
9+
GenerateEnumCaseAccessors.swift
910
IndentationRemover.swift
1011
PackageManifestEdits.swift
1112
SyntaxCodeActionProvider.swift
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2026 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftBasicFormat
14+
import SwiftExtensions
15+
import SwiftRefactor
16+
import SwiftSyntax
17+
18+
/// Syntactic code action that adds an `as<Case>` computed property for every
19+
/// enum case with associated values, returning the value(s) as an optional.
20+
struct GenerateEnumCaseAsAccessors: SyntaxRefactoringCodeActionProvider {
21+
static let title: String = "Generate 'as' accessors for enum cases"
22+
23+
static func nodeToRefactor(in scope: SyntaxCodeActionScope) -> EnumDeclSyntax? {
24+
return scope.innermostNodeContainingRange?.findParentOfSelf(
25+
ofType: EnumDeclSyntax.self,
26+
stoppingIf: { $0.is(CodeBlockSyntax.self) }
27+
)
28+
}
29+
30+
static func textRefactor(syntax: EnumDeclSyntax, in context: Void) throws -> [SourceEdit] {
31+
let indentation = accessorIndentation(for: syntax)
32+
33+
var accessors: [String] = []
34+
for element in enumCaseElements(of: syntax) {
35+
guard let parameters = element.parameterClause?.parameters, !parameters.isEmpty else {
36+
continue
37+
}
38+
accessors.append(
39+
makeAsAccessor(
40+
name: "as\(capitalizedCaseName(of: element))",
41+
casePattern: element.name.text,
42+
parameters: parameters,
43+
baseIndentation: indentation.base,
44+
indentationStep: indentation.step
45+
)
46+
)
47+
}
48+
49+
return try sourceEdits(insertingAccessors: accessors, into: syntax)
50+
}
51+
52+
private static func makeAsAccessor(
53+
name: String,
54+
casePattern: String,
55+
parameters: EnumCaseParameterListSyntax,
56+
baseIndentation: Trivia,
57+
indentationStep: Trivia
58+
) -> String {
59+
let memberIndentation = (baseIndentation + indentationStep).description
60+
let bodyIndentation = (baseIndentation + indentationStep + indentationStep).description
61+
let bindings = parameters.enumerated().map { index, parameter -> String in
62+
if let label = parameter.firstName, label.text != "_" {
63+
return label.text
64+
}
65+
return parameters.count == 1 ? "value" : "value\(index + 1)"
66+
}
67+
let returnType: String
68+
if let parameter = parameters.only {
69+
let type = parameter.type.trimmedDescription
70+
returnType = needsParenthesesBeforeOptional(parameter.type) ? "(\(type))?" : "\(type)?"
71+
} else {
72+
let elements = parameters.map { parameter -> String in
73+
if let label = parameter.firstName, label.text != "_" {
74+
return "\(label.text): \(parameter.type.trimmedDescription)"
75+
}
76+
return parameter.type.trimmedDescription
77+
}
78+
returnType = "(\(elements.joined(separator: ", ")))?"
79+
}
80+
let boundValues = bindings.joined(separator: ", ")
81+
let returnValue = bindings.only ?? "(\(boundValues))"
82+
return """
83+
\(memberIndentation)var \(name): \(returnType) {
84+
\(bodyIndentation)if case let .\(casePattern)(\(boundValues)) = self { return \(returnValue) }
85+
\(bodyIndentation)return nil
86+
\(memberIndentation)}
87+
"""
88+
}
89+
}
90+
91+
/// Syntactic code action that adds an `is<Case>` Boolean computed property for
92+
/// every enum case.
93+
struct GenerateEnumCaseIsAccessors: SyntaxRefactoringCodeActionProvider {
94+
static let title: String = "Generate 'is' accessors for enum cases"
95+
96+
static func nodeToRefactor(in scope: SyntaxCodeActionScope) -> EnumDeclSyntax? {
97+
return scope.innermostNodeContainingRange?.findParentOfSelf(
98+
ofType: EnumDeclSyntax.self,
99+
stoppingIf: { $0.is(CodeBlockSyntax.self) }
100+
)
101+
}
102+
103+
static func textRefactor(syntax: EnumDeclSyntax, in context: Void) throws -> [SourceEdit] {
104+
let indentation = accessorIndentation(for: syntax)
105+
106+
let accessors = enumCaseElements(of: syntax).map { element in
107+
makeIsAccessor(
108+
name: "is\(capitalizedCaseName(of: element))",
109+
casePattern: element.name.text,
110+
baseIndentation: indentation.base,
111+
indentationStep: indentation.step
112+
)
113+
}
114+
115+
return try sourceEdits(insertingAccessors: accessors, into: syntax)
116+
}
117+
118+
private static func makeIsAccessor(
119+
name: String,
120+
casePattern: String,
121+
baseIndentation: Trivia,
122+
indentationStep: Trivia
123+
) -> String {
124+
let memberIndentation = (baseIndentation + indentationStep).description
125+
let bodyIndentation = (baseIndentation + indentationStep + indentationStep).description
126+
return """
127+
\(memberIndentation)var \(name): Bool {
128+
\(bodyIndentation)if case .\(casePattern) = self { return true }
129+
\(bodyIndentation)return false
130+
\(memberIndentation)}
131+
"""
132+
}
133+
}
134+
135+
// MARK: - Shared helpers
136+
137+
private func enumCaseElements(of enumDecl: EnumDeclSyntax) -> [EnumCaseElementSyntax] {
138+
enumDecl.memberBlock.members.flatMap { member -> [EnumCaseElementSyntax] in
139+
guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else { return [] }
140+
return Array(caseDecl.elements)
141+
}
142+
}
143+
144+
/// The `as`/`is` accessor name for a case, with the leading character upper-cased.
145+
private func capitalizedCaseName(of element: EnumCaseElementSyntax) -> String {
146+
let caseName = element.name.identifier?.name ?? element.name.text
147+
return caseName.prefix(1).uppercased() + caseName.dropFirst()
148+
}
149+
150+
/// Whether `type` must be parenthesized to be made optional. Types that the postfix `?` already
151+
/// binds to are returned as-is; anything else (function, composition, `any`, attributed, … types)
152+
/// is parenthesized so e.g. `(Int) -> Void` becomes `((Int) -> Void)?` rather than `(Int) -> Void?`.
153+
private func needsParenthesesBeforeOptional(_ type: TypeSyntax) -> Bool {
154+
switch type.as(TypeSyntaxEnum.self) {
155+
case .identifierType, .memberType, .arrayType, .dictionaryType, .optionalType,
156+
.implicitlyUnwrappedOptionalType, .tupleType, .metatypeType:
157+
return false
158+
default:
159+
return true
160+
}
161+
}
162+
163+
/// The enclosing enum's indentation and the file's inferred indentation step.
164+
private func accessorIndentation(for enumDecl: EnumDeclSyntax) -> (base: Trivia, step: Trivia) {
165+
let base = enumDecl.firstToken(viewMode: .sourceAccurate)?.indentationOfLine ?? []
166+
let step = BasicFormat.inferIndentation(of: enumDecl.root) ?? .spaces(4)
167+
return (base: base, step: step)
168+
}
169+
170+
private func sourceEdits(insertingAccessors accessors: [String], into enumDecl: EnumDeclSyntax) throws -> [SourceEdit] {
171+
guard !accessors.isEmpty else {
172+
throw RefactoringNotApplicableError("No accessors to generate")
173+
}
174+
// Insert before the closing brace's leading trivia so the brace keeps its own
175+
// newline and indentation (correct for both top-level and nested enums).
176+
let insertion = "\n\n" + accessors.joined(separator: "\n\n")
177+
let position = enumDecl.memberBlock.rightBrace.position
178+
return [SourceEdit(range: position..<position, replacement: insertion)]
179+
}

Sources/SwiftSyntaxCodeActions/SyntaxCodeActions.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ package let allSyntaxCodeActions: [any SyntaxCodeActionProvider.Type] = {
2727
ConvertStringConcatenationToStringInterpolation.self,
2828
ConvertZeroParameterFunctionToComputedProperty.self,
2929
FormatRawStringLiteral.self,
30+
GenerateEnumCaseAsAccessors.self,
31+
GenerateEnumCaseIsAccessors.self,
3032
MigrateToNewIfLetSyntax.self,
3133
OpaqueParameterToGeneric.self,
3234
RemoveRedundantParentheses.self,

0 commit comments

Comments
 (0)