-
Notifications
You must be signed in to change notification settings - Fork 378
Add 'Generate enum associated value accessors' code action #2680
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ayush-that
wants to merge
4
commits into
swiftlang:main
Choose a base branch
from
ayush-that:feature/generate-enum-associated-value-accessors
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+649
−0
Open
Changes from 3 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
4e6aeb2
Add 'as'/'is' enum case accessor code actions
ayush-that 9b130a6
Document enum case accessor actions and rename their existing-names h…
ayush-that a1a4a11
Merge remote-tracking branch 'upstream/main' into feature/generate-en…
ayush-that c58835e
Import ToolsProtocolsSwiftExtensions for its only helper
ayush-that File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
199 changes: 199 additions & 0 deletions
199
Sources/SwiftSyntaxCodeActions/GenerateEnumCaseAccessors.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| /// 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)] | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you also document these new refactoring actions in https://github.com/swiftlang/sourcekit-lsp/blob/main/Documentation/Refactoring%20Actions.md?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.