Skip to content

[CodeGeneration] Improve identifier backticking #2813

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

Merged
merged 1 commit into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
15 changes: 8 additions & 7 deletions CodeGeneration/Sources/SyntaxSupport/Child.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import SwiftSyntax

/// The kind of token a node can contain. Either a token of a specific kind or a
/// keyword with the given text.
public enum TokenChoice: Equatable {
public enum TokenChoice: Equatable, IdentifierConvertible {
case keyword(Keyword)
case token(Token)

Expand All @@ -25,12 +25,13 @@ public enum TokenChoice: Equatable {
}
}

public var varOrCaseName: TokenSyntax {
/// The name of this token choice as an identifier.
public var identifier: TokenSyntax {
switch self {
case .keyword(let keyword):
return keyword.spec.varOrCaseName
return keyword.spec.identifier
case .token(let token):
return token.spec.varOrCaseName
return token.spec.identifier
}
}
}
Expand Down Expand Up @@ -79,7 +80,7 @@ public enum ChildKind {

/// A child of a node, that may be declared optional or a token with a
/// restricted subset of acceptable kinds or texts.
public class Child {
public class Child: IdentifierConvertible {
/// The name of the child.
///
/// The first character of the name is always uppercase.
Expand Down Expand Up @@ -145,8 +146,8 @@ public class Child {
}
}

/// A name of this child that's suitable to be used for variable or enum case names.
public var varOrCaseName: TokenSyntax {
/// A name of this child as an identifier.
public var identifier: TokenSyntax {
return .identifier(lowercaseFirstWord(name: name))
}

Expand Down
4 changes: 2 additions & 2 deletions CodeGeneration/Sources/SyntaxSupport/GrammarGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ struct GrammarGenerator {
if let tokenText = tokenSpec.text {
return "`\(tokenText)`"
} else {
return "`<\(tokenSpec.varOrCaseName)>`"
return "`<\(tokenSpec.identifier)>`"
}
}
}
Expand Down Expand Up @@ -62,7 +62,7 @@ struct GrammarGenerator {
return
children
.filter { !$0.isUnexpectedNodes }
.map { " - `\($0.varOrCaseName)`: \(generator.grammar(for: $0))" }
.map { " - `\($0.identifier)`: \(generator.grammar(for: $0))" }
.joined(separator: "\n")
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 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 SwiftSyntax

/// Instances of a conforming type should provide an identifier to be used in code generation.
public protocol IdentifierConvertible {
/// The name of the instance as an identifer.
var identifier: TokenSyntax {
get
}
}

public extension IdentifierConvertible {
/// ``identifier`` escaped as a base name suitable for call sites.
var baseCallName: TokenSyntax {
identifier.declNameOrVarCallName
}

/// ``identifier`` escaped as an enum case name suitable for call sites.
var enumCaseCallName: TokenSyntax {
memberCallName
}

/// ``identifier`` escaped as a member name suitable for call sites.
var memberCallName: TokenSyntax {
identifier.nonVarCallNameOrLabelDeclName
}

/// ``identifier`` escaped as an enum case name suitable for declaration sites.
var enumCaseDeclName: TokenSyntax {
identifier.declNameOrVarCallName
}

/// ``identifier`` escaped as a function name suitable for declaration sites.
var funcDeclName: TokenSyntax {
identifier.declNameOrVarCallName
}

/// ``identifier`` escaped as an argument label name suitable for declaration sites.
var labelDeclName: TokenSyntax {
identifier.nonVarCallNameOrLabelDeclName
}

/// ``identifier`` escaped as a variable name suitable for declaration sites.
var varDeclName: TokenSyntax {
identifier.declNameOrVarCallName
}
}
12 changes: 4 additions & 8 deletions CodeGeneration/Sources/SyntaxSupport/KeywordSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import SwiftSyntax

public struct KeywordSpec {
public struct KeywordSpec: IdentifierConvertible {
/// The name of the keyword.
public let name: String

Expand All @@ -29,13 +29,9 @@ public struct KeywordSpec {
/// API generated should be marked as SPI
public var isExperimental: Bool { experimentalFeature != nil }

/// The name of this keyword that's suitable to be used for variable or enum case names.
public var varOrCaseName: TokenSyntax {
if name == "init" {
return "`init`"
} else {
return TokenSyntax.identifier(name)
}
/// The name of this keyword as an identifier.
public var identifier: TokenSyntax {
TokenSyntax.identifier(name)
}

/// The attributes that should be printed on any API for the generated keyword.
Expand Down
11 changes: 5 additions & 6 deletions CodeGeneration/Sources/SyntaxSupport/Node.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import SwiftSyntax
/// but fixed types.
/// - Collection nodes contains an arbitrary number of children but all those
/// children are of the same type.
public class Node {
public class Node: IdentifierConvertible {
fileprivate enum Data {
case layout(children: [Child], traits: [String])
case collection(choices: [SyntaxNodeKind])
Expand Down Expand Up @@ -61,10 +61,9 @@ public class Node {
/// API generated should be SPI.
public var isExperimental: Bool { experimentalFeature != nil }

/// A name for this node that is suitable to be used as a variables or enum
/// case's name.
public var varOrCaseName: TokenSyntax {
return kind.varOrCaseName
/// A name for this node as an identifier.
public var identifier: TokenSyntax {
return kind.identifier
}

/// If this is a layout node, return a view of the node that provides access
Expand Down Expand Up @@ -229,7 +228,7 @@ public class Node {
let list =
childIn
.map {
if let childName = $0.child?.varOrCaseName {
if let childName = $0.child?.identifier {
// This will repeat the syntax type before and after the dot, which is
// a little unfortunate, but it's the only way I found to get docc to
// generate a fully-qualified type + member.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,6 @@ extension StringProtocol {
}
return prefix(1).uppercased() + dropFirst()
}
public var backtickedIfNeeded: String {
if Keyword.allCases.map(\.spec).contains(where: {
$0.name == self && ($0.isLexerClassified || $0.name == "Type" || $0.name == "Protocol")
}) {
return "`\(self)`"
} else {
return String(self)
}
}
}

extension String {
Expand Down
7 changes: 3 additions & 4 deletions CodeGeneration/Sources/SyntaxSupport/SyntaxNodeKind.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import SwiftSyntaxBuilder
///
/// Using the cases of this enum, children of syntax nodes can refer the syntax
/// node that defines their layout.
public enum SyntaxNodeKind: String, CaseIterable {
public enum SyntaxNodeKind: String, CaseIterable, IdentifierConvertible {
// Please keep this list sorted alphabetically

case _canImportExpr
Expand Down Expand Up @@ -337,9 +337,8 @@ public enum SyntaxNodeKind: String, CaseIterable {
}
}

/// A name for this node that is suitable to be used as a variables or enum
/// case's name.
public var varOrCaseName: TokenSyntax {
/// A name for this node as an identifier.
public var identifier: TokenSyntax {
return .identifier(rawValue)
}

Expand Down
8 changes: 4 additions & 4 deletions CodeGeneration/Sources/SyntaxSupport/TokenSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@
import SwiftSyntax

/// Represents the specification for a Token in the TokenSyntax file.
public struct TokenSpec {
public struct TokenSpec: IdentifierConvertible {
public enum Kind {
case punctuation
/// The `keyword` TokenKind that contains the actual keyword as an associated value
case keyword
case other
}

/// The name of the token, suitable for use in variable or enum case names.
public let varOrCaseName: TokenSyntax
/// The name of the token as an identifier.
public let identifier: TokenSyntax

/// The experimental feature the token is part of, or `nil` if this isn't
/// for an experimental feature.
Expand Down Expand Up @@ -66,7 +66,7 @@ public struct TokenSpec {
text: String? = nil,
kind: Kind
) {
self.varOrCaseName = .identifier(name)
self.identifier = .identifier(name)
self.experimentalFeature = experimentalFeature
self.nameForDiagnostics = nameForDiagnostics
self.text = text
Expand Down
23 changes: 19 additions & 4 deletions CodeGeneration/Sources/SyntaxSupport/Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,29 @@ extension Collection {
}
}

private extension Keyword {
static let backticksNeeded: Set<String> = Set(
Keyword.allCases.map(\.spec).filter {
$0.isLexerClassified || $0.name == "Type" || $0.name == "Protocol"
}.map(\.name)
)
}

extension TokenSyntax {
public var backtickedIfNeeded: TokenSyntax {
if Keyword.allCases.map(\.spec).contains(where: {
$0.name == self.description && ($0.isLexerClassified || $0.name == "Type" || $0.name == "Protocol")
}) {
public var declNameOrVarCallName: Self {
if Keyword.backticksNeeded.contains(self.description) {
return "`\(self)`"
} else {
return self
}
}

public var nonVarCallNameOrLabelDeclName: Self {
switch self.tokenKind {
case .keyword(.`init`), .identifier("init"):
return "`init`"
default:
return self
}
}
}
4 changes: 2 additions & 2 deletions CodeGeneration/Sources/Utils/SyntaxBuildableChild.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,12 @@ extension Child {
return buildableType.defaultValue
}
if token.text != nil {
return ExprSyntax(".\(token.varOrCaseName)Token()")
return ExprSyntax(".\(token.identifier)Token()")
}
if case .token(let choices, _, _) = kind,
case .keyword(let keyword) = choices.only
{
return ExprSyntax(".\(token.varOrCaseName)(.\(keyword.spec.varOrCaseName))")
return ExprSyntax(".\(token.memberCallName)(.\(keyword.spec.memberCallName))")
}
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion CodeGeneration/Sources/Utils/SyntaxBuildableType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public struct SyntaxBuildableType: Hashable {
return ExprSyntax(NilLiteralExprSyntax())
} else if let token = token {
if token.text != nil {
return ExprSyntax(".\(token.varOrCaseName)Token()")
return ExprSyntax(".\(token.identifier)Token()")
}
}
return nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ extension LayoutNode {
if useDeprecatedChildName, let deprecatedVarName = child.deprecatedVarName {
parameterName = deprecatedVarName
} else {
parameterName = child.varOrCaseName
parameterName = child.labelDeclName
}

return FunctionParameterSyntax(
Expand Down Expand Up @@ -80,7 +80,7 @@ extension LayoutNode {
if child.documentationAbstract.isEmpty {
return nil
}
return " - \(child.varOrCaseName): \(child.documentationAbstract)"
return " - \(child.identifier): \(child.documentationAbstract)"
}

let formattedParams = """
Expand Down Expand Up @@ -116,7 +116,7 @@ extension LayoutNode {
if useDeprecatedChildName, let deprecatedVarName = child.deprecatedVarName {
childName = deprecatedVarName
} else {
childName = child.varOrCaseName
childName = child.identifier
}

if child.buildableType.isBuilderInitializable {
Expand All @@ -127,11 +127,11 @@ extension LayoutNode {
let param = Node.from(type: child.buildableType).layoutNode!.singleNonDefaultedChild
if child.isOptional {
produceExpr = ExprSyntax(
"\(childName)Builder().map { \(child.buildableType.syntaxBaseName)(\(param.varOrCaseName): $0) }"
"\(childName)Builder().map { \(child.buildableType.syntaxBaseName)(\(param.labelDeclName): $0) }"
)
} else {
produceExpr = ExprSyntax(
"\(child.buildableType.syntaxBaseName)(\(param.varOrCaseName): \(childName)Builder())"
"\(child.buildableType.syntaxBaseName)(\(param.labelDeclName): \(childName)Builder())"
)
}
} else {
Expand All @@ -149,7 +149,7 @@ extension LayoutNode {
)
normalParameters.append(
FunctionParameterSyntax(
firstName: childName,
firstName: childName.nonVarCallNameOrLabelDeclName,
colon: .colonToken(),
type: child.parameterType,
defaultValue: child.defaultInitialization
Expand All @@ -158,7 +158,7 @@ extension LayoutNode {
}
delegatedInitArgs.append(
LabeledExprSyntax(
label: child.isUnexpectedNodes ? nil : child.varOrCaseName,
label: child.isUnexpectedNodes ? nil : child.labelDeclName,
colon: child.isUnexpectedNodes ? nil : .colonToken(),
expression: produceExpr
)
Expand Down Expand Up @@ -202,11 +202,11 @@ fileprivate func convertFromSyntaxProtocolToSyntaxType(
if useDeprecatedChildName, let deprecatedVarName = child.deprecatedVarName {
childName = deprecatedVarName
} else {
childName = child.varOrCaseName
childName = child.identifier
}

if child.buildableType.isBaseType && !child.kind.isNodeChoices {
return ExprSyntax("\(child.buildableType.syntaxBaseName)(fromProtocol: \(childName.backtickedIfNeeded))")
return ExprSyntax("\(child.buildableType.syntaxBaseName)(fromProtocol: \(childName.declNameOrVarCallName))")
}
return ExprSyntax("\(childName.backtickedIfNeeded)")
return ExprSyntax("\(childName.declNameOrVarCallName)")
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ let isLexerClassifiedFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
try! SwitchExprSyntax("switch self") {
for keyword in Keyword.allCases {
if keyword.spec.isLexerClassified {
SwitchCaseSyntax("case .\(keyword.spec.varOrCaseName): return true")
SwitchCaseSyntax("case .\(keyword.spec.enumCaseCallName): return true")
}
}
SwitchCaseSyntax("default: return false")
Expand Down
Loading