Skip to content

Introduce tokenSpecSetType and syntaxChoicesType for child nodes in CodeGeneration #2011

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 11, 2023
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
29 changes: 29 additions & 0 deletions CodeGeneration/Sources/SyntaxSupport/Child.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ public enum ChildKind {
}
}

public var isToken: Bool {
if case .token = self {
return true
} else {
return false
}
}

public var isNodeChoicesEmpty: Bool {
if case .nodeChoices(let nodeChoices) = self {
return nodeChoices.isEmpty
Expand Down Expand Up @@ -103,6 +111,22 @@ public class Child {
return .identifier(lowercaseFirstWord(name: name))
}

/// If this child has node choices, the type that the nested `SyntaxChildChoices` type should get.
///
/// For any other kind of child nodes, accessing this property crashes.
public var syntaxChoicesType: TypeSyntax {
precondition(kind.isNodeChoices, "Cannot get `syntaxChoicesType` for node that doesn’t have nodeChoices")
return "\(raw: name.withFirstCharacterUppercased)"
}

/// If this child only has tokens, the type that the generated `TokenSpecSet` should get.
///
/// For any other kind of child nodes, accessing this property crashes.
public var tokenSpecSetType: TypeSyntax {
precondition(kind.isToken, "Cannot get `tokenSpecSetType` for node that isn’t a token")
return "\(raw: name.withFirstCharacterUppercased)Options"
}

/// The deprecated name of this child that's suitable to be used for variable or enum case names.
public var deprecatedVarName: TokenSyntax? {
guard let deprecatedName = deprecatedName else {
Expand All @@ -111,6 +135,11 @@ public class Child {
return .identifier(lowercaseFirstWord(name: deprecatedName))
}

/// Determines if this child has a deprecated name
public var hasDeprecatedName: Bool {
return deprecatedName != nil
}

/// If the child ends with "token" in the kind, it's considered a token node.
/// Grab the existing reference to that token from the global list.
public var tokenKind: Token? {
Expand Down
22 changes: 12 additions & 10 deletions CodeGeneration/Sources/SyntaxSupport/Node.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,18 +114,20 @@ public class Node {
// any two defined children
childrenWithUnexpected =
children.enumerated().flatMap { (i, child) -> [Child] in
let childName = child.name.withFirstCharacterUppercased

let unexpectedName: String
let unexpectedDeprecatedName: String?

if i == 0 {
unexpectedName = "UnexpectedBefore\(child.name)"
unexpectedDeprecatedName = child.deprecatedName.map { "UnexpectedBefore\($0)" }
unexpectedName = "UnexpectedBefore\(childName)"
unexpectedDeprecatedName = child.deprecatedName.map { "UnexpectedBefore\($0.withFirstCharacterUppercased)" }
} else {
unexpectedName = "UnexpectedBetween\(children[i - 1].name)And\(child.name)"
if let deprecatedName = children[i - 1].deprecatedName {
unexpectedDeprecatedName = "UnexpectedBetween\(deprecatedName)And\(child.deprecatedName ?? child.name)"
} else if let deprecatedName = child.deprecatedName {
unexpectedDeprecatedName = "UnexpectedBetween\(children[i - 1].name)And\(deprecatedName)"
unexpectedName = "UnexpectedBetween\(children[i - 1].name.withFirstCharacterUppercased)And\(childName)"
if let deprecatedName = children[i - 1].deprecatedName?.withFirstCharacterUppercased {
unexpectedDeprecatedName = "UnexpectedBetween\(deprecatedName)And\(child.deprecatedName?.withFirstCharacterUppercased ?? childName)"
} else if let deprecatedName = child.deprecatedName?.withFirstCharacterUppercased {
unexpectedDeprecatedName = "UnexpectedBetween\(children[i - 1].name.withFirstCharacterUppercased)And\(deprecatedName)"
} else {
unexpectedDeprecatedName = nil
}
Expand All @@ -139,9 +141,9 @@ public class Node {
return [unexpectedBefore, child]
} + [
Child(
name: "UnexpectedAfter\(children.last!.name)",
deprecatedName: children.last!.deprecatedName.map { "UnexpectedAfter\($0)" },
kind: .collection(kind: .unexpectedNodes, collectionElementName: "UnexpectedAfter\(children.last!.name)"),
name: "UnexpectedAfter\(children.last!.name.withFirstCharacterUppercased)",
deprecatedName: children.last!.deprecatedName.map { "UnexpectedAfter\($0.withFirstCharacterUppercased)" },
kind: .collection(kind: .unexpectedNodes, collectionElementName: "UnexpectedAfter\(children.last!.name.withFirstCharacterUppercased)"),
isOptional: true
)
]
Expand Down
20 changes: 10 additions & 10 deletions CodeGeneration/Sources/Utils/SyntaxBuildableChild.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public enum SyntaxOrTokenNodeKind: Hashable {
public extension Child {
/// The type of this child, represented by a ``SyntaxBuildableType``, which can
/// be used to create the corresponding `Buildable` and `ExpressibleAs` types.
var type: SyntaxBuildableType {
var buildableType: SyntaxBuildableType {
let buildableKind: SyntaxOrTokenNodeKind
switch kind {
case .node(kind: let kind):
Expand All @@ -44,29 +44,29 @@ public extension Child {
)
}

var parameterBaseType: String {
var parameterBaseType: TypeSyntax {
switch kind {
case .nodeChoices:
return self.name
return self.syntaxChoicesType
default:
return type.parameterBaseType
return buildableType.parameterBaseType
}
}

var parameterType: TypeSyntax {
return self.type.optionalWrapped(type: IdentifierTypeSyntax(name: .identifier(parameterBaseType)))
return self.buildableType.optionalWrapped(type: parameterBaseType)
}

var defaultValue: ExprSyntax? {
if isOptional || isUnexpectedNodes {
if type.isBaseType && kind.isNodeChoicesEmpty {
return ExprSyntax("\(type.buildable).none")
if buildableType.isBaseType && kind.isNodeChoicesEmpty {
return ExprSyntax("\(buildableType.buildable).none")
} else {
return ExprSyntax("nil")
}
}
guard let token = token, isToken else {
return type.defaultValue
return buildableType.defaultValue
}
if token.text != nil {
return ExprSyntax(".\(token.varOrCaseName)Token()")
Expand Down Expand Up @@ -128,7 +128,7 @@ public extension Child {
}

var preconditionChoices: [ExprSyntax] = []
if type.isOptional {
if buildableType.isOptional {
preconditionChoices.append(
ExprSyntax(
SequenceExprSyntax {
Expand All @@ -143,7 +143,7 @@ public extension Child {
preconditionChoices.append(
ExprSyntax(
SequenceExprSyntax {
MemberAccessExprSyntax(base: type.forceUnwrappedIfNeeded(expr: DeclReferenceExprSyntax(baseName: .identifier(varName))), name: "text")
MemberAccessExprSyntax(base: buildableType.forceUnwrappedIfNeeded(expr: DeclReferenceExprSyntax(baseName: .identifier(varName))), name: "text")
BinaryOperatorExprSyntax(text: "==")
StringLiteralExprSyntax(content: textChoice)
}
Expand Down
12 changes: 6 additions & 6 deletions CodeGeneration/Sources/Utils/SyntaxBuildableType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public struct SyntaxBuildableType: Hashable {
/// - For token: ``TokenSyntax`` (tokens don't have a dedicated type in SwiftSyntaxBuilder)
/// If the type is optional, the type is wrapped in an `OptionalType`.
public var buildable: TypeSyntax {
optionalWrapped(type: IdentifierTypeSyntax(name: .identifier(syntaxBaseName)))
optionalWrapped(type: syntaxBaseName)
}

/// Whether parameters of this type should be initializable by a result builder.
Expand Down Expand Up @@ -120,10 +120,10 @@ public struct SyntaxBuildableType: Hashable {

/// The corresponding `*Syntax` type defined in the `SwiftSyntax` module,
/// without any question marks attached.
public var syntaxBaseName: String {
public var syntaxBaseName: TypeSyntax {
switch kind {
case .node(kind: let kind):
return "\(kind.syntaxType)"
return kind.syntaxType
case .token:
return "TokenSyntax"
}
Expand All @@ -133,12 +133,12 @@ public struct SyntaxBuildableType: Hashable {
/// which will eventually get built from `SwiftSyntaxBuilder`. If the type
/// is optional, this terminates with a `?`.
public var syntax: TypeSyntax {
return optionalWrapped(type: IdentifierTypeSyntax(name: .identifier(syntaxBaseName)))
return optionalWrapped(type: syntaxBaseName)
}

/// The type that is used for parameters in SwiftSyntaxBuilder that take this
/// type of syntax node.
public var parameterBaseType: String {
public var parameterBaseType: TypeSyntax {
if isBaseType {
return "\(syntaxBaseName)Protocol"
} else {
Expand All @@ -147,7 +147,7 @@ public struct SyntaxBuildableType: Hashable {
}

public var parameterType: TypeSyntax {
return optionalWrapped(type: IdentifierTypeSyntax(name: .identifier(parameterBaseType)))
return optionalWrapped(type: parameterBaseType)
}

/// Assuming that this is a collection type, the non-optional type of the result builder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ extension LayoutNode {
func createFunctionParameterSyntax(for child: Child) -> FunctionParameterSyntax {
var paramType: TypeSyntax
if !child.kind.isNodeChoicesEmpty {
paramType = "\(raw: child.name)"
paramType = "\(child.syntaxChoicesType)"
} else if child.hasBaseType {
paramType = "some \(raw: child.syntaxNodeKind.protocolType)"
} else {
Expand Down Expand Up @@ -119,16 +119,16 @@ extension LayoutNode {
childName = child.varOrCaseName
}

if child.type.isBuilderInitializable {
if child.buildableType.isBuilderInitializable {
// Allow initializing certain syntax collections with result builders
shouldCreateInitializer = true
let builderInitializableType = child.type.builderInitializableType
if child.type.builderInitializableType != child.type {
let param = Node.from(type: child.type).layoutNode!.singleNonDefaultedChild
let builderInitializableType = child.buildableType.builderInitializableType
if child.buildableType.builderInitializableType != child.buildableType {
let param = Node.from(type: child.buildableType).layoutNode!.singleNonDefaultedChild
if child.isOptional {
produceExpr = ExprSyntax("\(childName)Builder().map { \(raw: child.type.syntaxBaseName)(\(param.varOrCaseName): $0) }")
produceExpr = ExprSyntax("\(childName)Builder().map { \(raw: child.buildableType.syntaxBaseName)(\(param.varOrCaseName): $0) }")
} else {
produceExpr = ExprSyntax("\(raw: child.type.syntaxBaseName)(\(param.varOrCaseName): \(childName)Builder())")
produceExpr = ExprSyntax("\(raw: child.buildableType.syntaxBaseName)(\(param.varOrCaseName): \(childName)Builder())")
}
} else {
produceExpr = ExprSyntax("\(childName)Builder()")
Expand Down Expand Up @@ -195,8 +195,8 @@ fileprivate func convertFromSyntaxProtocolToSyntaxType(child: Child, useDeprecat
childName = child.varOrCaseName
}

if child.type.isBaseType && !child.kind.isNodeChoices {
return ExprSyntax("\(raw: child.type.syntaxBaseName)(fromProtocol: \(childName.backtickedIfNeeded))")
if child.buildableType.isBaseType && !child.kind.isNodeChoices {
return ExprSyntax("\(raw: child.buildableType.syntaxBaseName)(fromProtocol: \(childName.backtickedIfNeeded))")
}
return ExprSyntax("\(raw: childName.backtickedIfNeeded)")
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ import SyntaxSupport

extension Array where Element == Child {
var hasDeprecatedChild: Bool {
return self.contains(where: { $0.deprecatedName != nil })
return self.contains(where: { $0.hasDeprecatedName })
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ let parserTokenSpecSetFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
try EnumDeclSyntax(
"""
@_spi(Diagnostics)
public enum \(raw: child.name)Options: TokenSpecSet
public enum \(child.tokenSpecSetType): TokenSpecSet
"""
) {
for choice in choices {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ import SyntaxSupport
import Utils

fileprivate extension Node {
var childrenChoicesEnums: [(name: TokenSyntax, choices: [(caseName: TokenSyntax, kind: SyntaxNodeKind)])] {
var childrenChoicesEnums: [(name: TypeSyntax, choices: [(caseName: TokenSyntax, kind: SyntaxNodeKind)])] {
let node = self
if let node = node.layoutNode {
return node.children.compactMap { child -> (name: TokenSyntax, choices: [(caseName: TokenSyntax, kind: SyntaxNodeKind)])? in
return node.children.compactMap { child -> (name: TypeSyntax, choices: [(caseName: TokenSyntax, kind: SyntaxNodeKind)])? in
switch child.kind {
case .nodeChoices(let choices):
return (.identifier(child.name), choices.map { ($0.varOrCaseName, $0.syntaxNodeKind) })
return (child.syntaxChoicesType, choices.map { ($0.varOrCaseName, $0.syntaxNodeKind) })
default:
return nil
}
Expand All @@ -31,7 +31,7 @@ fileprivate extension Node {
let choices = node.elementChoices.map { choice -> (TokenSyntax, SyntaxNodeKind) in
(SYNTAX_NODE_MAP[choice]!.varOrCaseName, SYNTAX_NODE_MAP[choice]!.kind)
}
return [(.identifier("Element"), choices)]
return [("Element", choices)]
} else {
return []
}
Expand Down Expand Up @@ -238,7 +238,7 @@ let rawSyntaxNodesFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
}

for (index, child) in node.children.enumerated() {
try VariableDeclSyntax("public var \(child.varOrCaseName.backtickedIfNeeded): Raw\(raw: child.type.buildable)") {
try VariableDeclSyntax("public var \(child.varOrCaseName.backtickedIfNeeded): Raw\(raw: child.buildableType.buildable)") {
let iuoMark = child.isOptional ? "" : "!"

if child.syntaxNodeKind == .syntax {
Expand All @@ -257,11 +257,11 @@ fileprivate extension Child {
var rawParameterType: TypeSyntax {
let paramType: TypeSyntax
if case ChildKind.nodeChoices = kind {
paramType = "\(raw: name)"
paramType = syntaxChoicesType
} else {
paramType = syntaxNodeKind.rawType
}

return type.optionalWrapped(type: paramType)
return buildableType.optionalWrapped(type: paramType)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ let rawSyntaxValidationFile = try! SourceFileSyntax(leadingTrivia: copyrightHead
let verifiedChoices = ArrayExprSyntax {
ArrayElementSyntax(
leadingTrivia: .newline,
expression: ExprSyntax("verify(layout[\(raw: index)], as: Raw\(raw: child.type.buildable).self)")
expression: ExprSyntax("verify(layout[\(raw: index)], as: Raw\(raw: child.buildableType.buildable).self)")
)
}

Expand All @@ -220,10 +220,12 @@ let rawSyntaxValidationFile = try! SourceFileSyntax(leadingTrivia: copyrightHead
}
}
}
let verifyCall = ExprSyntax("verify(layout[\(raw: index)], as: Raw\(raw: child.type.buildable).self, tokenChoices: \(choices))")
let verifyCall = ExprSyntax(
"verify(layout[\(raw: index)], as: Raw\(raw: child.buildableType.buildable).self, tokenChoices: \(choices))"
)
ExprSyntax("assertNoError(kind, \(raw: index), \(verifyCall))")
default:
ExprSyntax("assertNoError(kind, \(raw: index), verify(layout[\(raw: index)], as: Raw\(raw: child.type.buildable).self))")
ExprSyntax("assertNoError(kind, \(raw: index), verify(layout[\(raw: index)], as: Raw\(raw: child.buildableType.buildable).self))")
}
}
} else if let node = node.collectionNode {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ let renamedChildrenCompatibilityFile = try! SourceFileSyntax(leadingTrivia: copy
try ExtensionDeclSyntax("extension \(raw: layoutNode.type.syntaxBaseName)") {
for child in layoutNode.children {
if let deprecatedVarName = child.deprecatedVarName {
let childType: TypeSyntax = child.kind.isNodeChoicesEmpty ? child.syntaxNodeKind.syntaxType : "\(raw: child.name)"
let type = child.isOptional ? TypeSyntax("\(raw: childType)?") : TypeSyntax("\(raw: childType)")
let childType: TypeSyntax = child.kind.isNodeChoicesEmpty ? child.syntaxNodeKind.syntaxType : child.syntaxChoicesType
let type = child.isOptional ? TypeSyntax("\(childType)?") : childType

DeclSyntax(
"""
Expand Down Expand Up @@ -56,7 +56,7 @@ let renamedChildrenCompatibilityFile = try! SourceFileSyntax(leadingTrivia: copy
}

let deprecatedNames = layoutNode.children
.filter { !$0.isUnexpectedNodes && $0.deprecatedName != nil }
.filter { !$0.isUnexpectedNodes && $0.hasDeprecatedName }
.map { $0.varOrCaseName.description }
.joined(separator: ", ")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func syntaxNode(emitKind: SyntaxNodeKind) -> SourceFileSyntax {
for child in node.children {
ArrayElementSyntax(
expression: MemberAccessExprSyntax(
base: child.type.optionalChained(expr: ExprSyntax("\(child.varOrCaseName.backtickedIfNeeded)")),
base: child.buildableType.optionalChained(expr: ExprSyntax("\(child.varOrCaseName.backtickedIfNeeded)")),
period: .periodToken(),
name: "raw"
)
Expand Down Expand Up @@ -159,7 +159,7 @@ func syntaxNode(emitKind: SyntaxNodeKind) -> SourceFileSyntax {
// Children properties
// ===================

let childType: TypeSyntax = child.kind.isNodeChoicesEmpty ? child.syntaxNodeKind.syntaxType : "\(raw: child.name)"
let childType: TypeSyntax = child.kind.isNodeChoicesEmpty ? child.syntaxNodeKind.syntaxType : child.syntaxChoicesType
let type = child.isOptional ? TypeSyntax("\(raw: childType)?") : TypeSyntax("\(raw: childType)")

try! VariableDeclSyntax(
Expand Down Expand Up @@ -244,7 +244,7 @@ private func generateSyntaxChildChoices(for child: Child) throws -> EnumDeclSynt
return nil
}

return try! EnumDeclSyntax("public enum \(raw: child.name): SyntaxChildChoices, SyntaxHashable") {
return try! EnumDeclSyntax("public enum \(child.syntaxChoicesType): SyntaxChildChoices, SyntaxHashable") {
for choice in choices {
DeclSyntax("case `\(choice.varOrCaseName)`(\(raw: choice.syntaxNodeKind.syntaxType))")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ let renamedChildrenBuilderCompatibilityFile = try! SourceFileSyntax(leadingTrivi
for layoutNode in SYNTAX_NODES.compactMap(\.layoutNode).filter({ $0.children.hasDeprecatedChild }) {
if let convenienceInit = try layoutNode.createConvenienceBuilderInitializer(useDeprecatedChildName: true) {
let deprecatedNames = layoutNode.children
.filter { !$0.isUnexpectedNodes && $0.deprecatedName != nil }
.filter { !$0.isUnexpectedNodes && $0.hasDeprecatedName }
.compactMap { $0.varOrCaseName.description }
.joined(separator: ", ")

Expand Down
Loading