Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Documentation/Refactoring Actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ The specific refactorings available depend on what code is selected or where the
| **Convert To Computed Property** | Select a variable declaration with an initializer |
| **Expand 'some' parameters to generic parameters** | Cursor on a function declaration using `some` opaque parameter types |
| **Add Explicit Raw Values** | Cursor on an enum with an integer or `String` raw value type where some cases lack explicit raw values |
| **Generate 'as' accessors for enum cases** | Cursor inside an enum that has a case with associated values |
| **Generate 'is' accessors for enum cases** | Cursor inside an enum that has a case |

### Source Organization

Expand Down
1 change: 1 addition & 0 deletions Sources/SwiftSyntaxCodeActions/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ add_library(SwiftSyntaxCodeActions STATIC
ConvertIntegerLiteral.swift
ConvertJSONToCodableStruct.swift
ConvertStringConcatenationToStringInterpolation.swift
GenerateEnumCaseAccessors.swift
IndentationRemover.swift
PackageManifestEdits.swift
SyntaxCodeActionProvider.swift
Expand Down
199 changes: 199 additions & 0 deletions Sources/SwiftSyntaxCodeActions/GenerateEnumCaseAccessors.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2026 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import SwiftBasicFormat
import SwiftExtensions
import SwiftRefactor
import SwiftSyntax

/// Syntactic code action that adds an `as<Case>` computed property for every
/// enum case with associated values, returning the value(s) as an optional.
struct GenerateEnumCaseAsAccessors: SyntaxRefactoringCodeActionProvider {
static let title: String = "Generate 'as' accessors for enum cases"

static func nodeToRefactor(in scope: SyntaxCodeActionScope) -> EnumDeclSyntax? {
return scope.innermostNodeContainingRange?.findParentOfSelf(
ofType: EnumDeclSyntax.self,
stoppingIf: { $0.is(CodeBlockSyntax.self) }
)
}

static func textRefactor(syntax: EnumDeclSyntax, in context: Void) throws -> [SourceEdit] {
let indentation = accessorIndentation(for: syntax)
let existingNames = existingVariableNames(of: syntax)

var accessors: [String] = []
for element in enumCaseElements(of: syntax) {
guard let parameters = element.parameterClause?.parameters, !parameters.isEmpty else {
continue
}
let name = "as\(capitalizedCaseName(of: element))"
guard !existingNames.contains(name) else { continue }
accessors.append(
makeAsAccessor(
name: name,
casePattern: element.name.text,
parameters: parameters,
baseIndentation: indentation.base,
indentationStep: indentation.step
)
)
}

return try sourceEdits(insertingAccessors: accessors, into: syntax)
}

private static func makeAsAccessor(
name: String,
casePattern: String,
parameters: EnumCaseParameterListSyntax,
baseIndentation: Trivia,
indentationStep: Trivia
) -> String {
let memberIndentation = (baseIndentation + indentationStep).description
let bodyIndentation = (baseIndentation + indentationStep + indentationStep).description
let bindings = parameters.enumerated().map { index, parameter -> String in
if let label = parameter.firstName, label.text != "_" {
return label.text
}
return parameters.count == 1 ? "value" : "value\(index + 1)"
}
let returnType: String
if let parameter = parameters.only {
let type = parameter.type.trimmedDescription
returnType = needsParenthesesBeforeOptional(parameter.type) ? "(\(type))?" : "\(type)?"
} else {
let elements = parameters.map { parameter -> String in
if let label = parameter.firstName, label.text != "_" {
return "\(label.text): \(parameter.type.trimmedDescription)"
}
return parameter.type.trimmedDescription
}
returnType = "(\(elements.joined(separator: ", ")))?"
}
let boundValues = bindings.joined(separator: ", ")
let returnValue = bindings.only ?? "(\(boundValues))"
return """
\(memberIndentation)var \(name): \(returnType) {
\(bodyIndentation)if case let .\(casePattern)(\(boundValues)) = self { return \(returnValue) }
\(bodyIndentation)return nil
\(memberIndentation)}
"""
}
}

/// Syntactic code action that adds an `is<Case>` Boolean computed property for

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.

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.

/// every enum case.
struct GenerateEnumCaseIsAccessors: SyntaxRefactoringCodeActionProvider {
static let title: String = "Generate 'is' accessors for enum cases"

static func nodeToRefactor(in scope: SyntaxCodeActionScope) -> EnumDeclSyntax? {
return scope.innermostNodeContainingRange?.findParentOfSelf(
ofType: EnumDeclSyntax.self,
stoppingIf: { $0.is(CodeBlockSyntax.self) }
)
}

static func textRefactor(syntax: EnumDeclSyntax, in context: Void) throws -> [SourceEdit] {
let indentation = accessorIndentation(for: syntax)
let existingNames = existingVariableNames(of: syntax)

let accessors = enumCaseElements(of: syntax).compactMap { element -> String? in
let name = "is\(capitalizedCaseName(of: element))"
guard !existingNames.contains(name) else { return nil }
return makeIsAccessor(
name: name,
casePattern: element.name.text,
baseIndentation: indentation.base,
indentationStep: indentation.step
)
}

return try sourceEdits(insertingAccessors: accessors, into: syntax)
}

private static func makeIsAccessor(
name: String,
casePattern: String,
baseIndentation: Trivia,
indentationStep: Trivia
) -> String {
let memberIndentation = (baseIndentation + indentationStep).description
let bodyIndentation = (baseIndentation + indentationStep + indentationStep).description
return """
\(memberIndentation)var \(name): Bool {
\(bodyIndentation)if case .\(casePattern) = self { return true }
\(bodyIndentation)return false
\(memberIndentation)}
"""
}
}

// MARK: - Shared helpers

private func enumCaseElements(of enumDecl: EnumDeclSyntax) -> [EnumCaseElementSyntax] {
enumDecl.memberBlock.members.flatMap { member -> [EnumCaseElementSyntax] in
guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else { return [] }
return Array(caseDecl.elements)
}
}

/// The names of the enum's existing properties, so an accessor whose name is already taken isn't generated again.
private func existingVariableNames(of enumDecl: EnumDeclSyntax) -> Set<String> {
var names: Set<String> = []
for member in enumDecl.memberBlock.members {
guard let variable = member.decl.as(VariableDeclSyntax.self) else { continue }
for binding in variable.bindings {
if let pattern = binding.pattern.as(IdentifierPatternSyntax.self) {
names.insert(pattern.identifier.identifier?.name ?? pattern.identifier.text)
}
}
}
return names
}

/// The `as`/`is` accessor name for a case, with the leading character upper-cased.
private func capitalizedCaseName(of element: EnumCaseElementSyntax) -> String {
let caseName = element.name.identifier?.name ?? element.name.text
return caseName.prefix(1).uppercased() + caseName.dropFirst()
}

/// Whether `type` must be parenthesized to be made optional. Types that the postfix `?` already
/// binds to are returned as-is; anything else (function, composition, `any`, attributed, … types)
/// is parenthesized so e.g. `(Int) -> Void` becomes `((Int) -> Void)?` rather than `(Int) -> Void?`.
private func needsParenthesesBeforeOptional(_ type: TypeSyntax) -> Bool {
switch type.as(TypeSyntaxEnum.self) {
case .identifierType, .memberType, .arrayType, .dictionaryType, .optionalType,
.implicitlyUnwrappedOptionalType, .tupleType, .metatypeType:
return false
default:
return true
}
}

/// The enclosing enum's indentation and the file's inferred indentation step.
private func accessorIndentation(for enumDecl: EnumDeclSyntax) -> (base: Trivia, step: Trivia) {
let base = enumDecl.firstToken(viewMode: .sourceAccurate)?.indentationOfLine ?? []
let step = BasicFormat.inferIndentation(of: enumDecl.root) ?? .spaces(4)
return (base: base, step: step)
}

private func sourceEdits(insertingAccessors accessors: [String], into enumDecl: EnumDeclSyntax) throws -> [SourceEdit] {
guard !accessors.isEmpty else {
throw RefactoringNotApplicableError("No accessors to generate")
}
// Insert before the closing brace's leading trivia so the brace keeps its own
// newline and indentation (correct for both top-level and nested enums).
let insertion = "\n\n" + accessors.joined(separator: "\n\n")
let position = enumDecl.memberBlock.rightBrace.position
return [SourceEdit(range: position..<position, replacement: insertion)]
}
2 changes: 2 additions & 0 deletions Sources/SwiftSyntaxCodeActions/SyntaxCodeActions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ package let allSyntaxCodeActions: [any SyntaxCodeActionProvider.Type] = {
ConvertStringConcatenationToStringInterpolation.self,
ConvertZeroParameterFunctionToComputedProperty.self,
FormatRawStringLiteral.self,
GenerateEnumCaseAsAccessors.self,
GenerateEnumCaseIsAccessors.self,
MigrateToNewIfLetSyntax.self,
OpaqueParameterToGeneric.self,
RemoveRedundantParentheses.self,
Expand Down
Loading