Skip to content
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

WIP: Make convenience initializers with CodeGeneration #2142

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions CodeGeneration/Sources/SyntaxSupport/AttributeNodes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ public let ATTRIBUTE_NODES: [Node] = [
nameForDiagnostics: "attribute",
documentation: "An `@` attribute.",
parserFunction: "parseAttribute",
rules: [
ConvenienceInitRule(
nonOptionalChildName: "arguments",
defaults: [
"leftParen": .leftParen,
"rightParen": .rightParen
])
],
children: [
Child(
name: "atSign",
Expand Down
37 changes: 37 additions & 0 deletions CodeGeneration/Sources/SyntaxSupport/ConvenienceInitRule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2023 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 Foundation
import SwiftSyntax

/// A rule that describes convenienve initialization rules for a ``Node``.
///
/// When generating syntax nodes, SwiftSyntax will make additional
/// convenience initializer for each rule that a Node has.
///
/// The convenience initializer will take a non-optional parameter
/// `nonOptionalChildName`, and when a non-optional value is passed, it'll call
/// the full memberwise initializer with the provided `defaults`.
///
/// For example, when initializing an `EnumCaseParameterSyntax`, the convenience
/// initializer will take a non-optional `firstName` parameter, and when it's
/// passed, it'll call the full memberwise initializer with
/// `colon = .colonToken()`.
public struct ConvenienceInitRule {
/// The name of the parameter that is required to be present for
/// this conveniece initializer rule to apply.
public let nonOptionalChildName: String

/// A dicrionary of parameter names to their respective default values
/// to apply when the `nonOptionalChildName` is passed as concrete value.
public let defaults: [String: Token]
}
8 changes: 8 additions & 0 deletions CodeGeneration/Sources/SyntaxSupport/DeclNodes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,14 @@ public let DECL_NODES: [Node] = [
nameForDiagnostics: "parameter",
parserFunction: "parseEnumCaseParameter",
traits: ["WithTrailingComma", "WithModifiers"],
rules: [
ConvenienceInitRule(
nonOptionalChildName: "firstName",
defaults: [
"colon": .colon
]
)
],
children: [
Child(
name: "modifiers",
Expand Down
17 changes: 17 additions & 0 deletions CodeGeneration/Sources/SyntaxSupport/ExprNodes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,14 @@ public let EXPR_NODES: [Node] = [
traits: [
"WithTrailingComma"
],
rules: [
ConvenienceInitRule(
nonOptionalChildName: "name",
defaults: [
"equal": .equal
]
)
],
children: [
Child(
name: "specifier",
Expand Down Expand Up @@ -794,6 +802,15 @@ public let EXPR_NODES: [Node] = [
kind: .functionCallExpr,
base: .expr,
nameForDiagnostics: "function call",
rules: [
ConvenienceInitRule(
nonOptionalChildName: "arguments",
defaults: [
"leftParen": .leftParen,
"rightParen": .rightParen
]
)
],
children: [
Child(
name: "calledExpression",
Expand Down
8 changes: 8 additions & 0 deletions CodeGeneration/Sources/SyntaxSupport/Node.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ public class Node {
return kind.varOrCaseName
}

/// List of convenience initializer rules for this node. CodeGeneration will
/// generate a convenience initializer for each rule.
public let rules: [ConvenienceInitRule]

/// If this is a layout node, return a view of the node that provides access
/// to the layout-node specific properties.
public var layoutNode: LayoutNode? {
Expand Down Expand Up @@ -112,6 +116,7 @@ public class Node {
documentation: String? = nil,
parserFunction: TokenSyntax? = nil,
traits: [String] = [],
rules: [ConvenienceInitRule] = [],
children: [Child] = []
) {
precondition(base != .syntaxCollection)
Expand All @@ -123,6 +128,7 @@ public class Node {
self.nameForDiagnostics = nameForDiagnostics
self.documentation = docCommentTrivia(from: documentation)
self.parserFunction = parserFunction
self.rules = rules

let childrenWithUnexpected: [Child]
if children.isEmpty {
Expand Down Expand Up @@ -229,6 +235,7 @@ public class Node {
isExperimental: Bool = false,
nameForDiagnostics: String?,
documentation: String? = nil,
rules: [ConvenienceInitRule] = [],
parserFunction: TokenSyntax? = nil,
elementChoices: [SyntaxNodeKind]
) {
Expand All @@ -239,6 +246,7 @@ public class Node {
self.nameForDiagnostics = nameForDiagnostics
self.documentation = docCommentTrivia(from: documentation)
self.parserFunction = parserFunction
self.rules = rules

assert(!elementChoices.isEmpty)
self.data = .collection(choices: elementChoices)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,52 +16,156 @@ import SyntaxSupport
import Utils

extension LayoutNode {
func generateInitializerDeclHeader(useDeprecatedChildName: Bool = false) -> SyntaxNodeString {

/// Returns Child parameter type as a ``TypeSyntax``.
func generateChildParameterType(for child: Child, isOptional: Bool = false) -> TypeSyntax {
var paramType: TypeSyntax

if !child.kind.isNodeChoicesEmpty {
paramType = "\(child.syntaxChoicesType)"
} else if child.hasBaseType {
paramType = "some \(child.syntaxNodeKind.protocolType)"
} else {
paramType = child.syntaxNodeKind.syntaxType
}

if isOptional {
if paramType.is(SomeOrAnyTypeSyntax.self) {
paramType = "(\(paramType))?"
} else {
paramType = "\(paramType)?"
}
}

return paramType
}

/// Generates a convenience memberwise SyntaxNode initializer based on a
/// given ``ConvenienceInitRule``.
///
/// - parameters:
/// - rule: The ``ConvenienceInitRule`` to use for generating the initializer. Applying a rule will make some children non-optional, and set default values for other children.
/// - useDeprecatedChildName: Whether to use the deprecated child name for the initializer parameter.
/// - returns:
/// - ``SyntaxNodeString``: The generated initializer.
func generateInitializerDeclHeader(for rule: ConvenienceInitRule? = nil, useDeprecatedChildName: Bool = false) -> SyntaxNodeString {
if children.isEmpty {
return "public init()"
}

func createFunctionParameterSyntax(for child: Child) -> FunctionParameterSyntax {
var paramType: TypeSyntax
if !child.kind.isNodeChoicesEmpty {
paramType = "\(child.syntaxChoicesType)"
} else if child.hasBaseType {
paramType = "some \(child.syntaxNodeKind.protocolType)"
/// Returns the child paramter name.
func generateChildParameterName(for child: Child) -> TokenSyntax {
let parameterName: TokenSyntax

if useDeprecatedChildName, let deprecatedVarName = child.deprecatedVarName {
parameterName = deprecatedVarName
} else {
paramType = child.syntaxNodeKind.syntaxType
parameterName = child.varOrCaseName
}
return parameterName
}

if child.isOptional {
if paramType.is(SomeOrAnyTypeSyntax.self) {
paramType = "(\(paramType))?"
/// Returns whether a given child should be optional in the initializer,
/// based on a provided ``ConvenienceInitRule``.
///
/// If the rule is `nil`, this func will return `nil` as well, which means
/// that you should fall back to whether child is optional in the ``Node``
/// definition.
///
func ruleBasedChildIsOptional(for child: Child, with rule: ConvenienceInitRule?) -> Bool {
if let rule = rule {
if rule.nonOptionalChildName == child.name {
return false
} else {
paramType = "\(paramType)?"
return child.isOptional
}
} else {
return child.isOptional
}
}

let parameterName: TokenSyntax
/// Returns a default value for a given child, based on a provided
/// ``ConvenienceInitRule``.
///
/// If the rule should not affect this child, the
/// `child.defualtInitialization` will be returned.
func ruleBasedChildDefaultValue(for child: Child, with rule: ConvenienceInitRule?) -> InitializerClauseSyntax? {
if ruleBasedShouldOverrideDefault(for: child, with: rule) {
if let rule, let defaultValue = rule.defaults[child.name] {
return InitializerClauseSyntax(
equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space),
value: ExprSyntax(".\(defaultValue.spec.varOrCaseName)Token()")
)
} else {
return nil
}
} else {
return child.defaultInitialization
}

if useDeprecatedChildName, let deprecatedVarName = child.deprecatedVarName {
parameterName = deprecatedVarName
}

/// Should the convenience initializer override the default value of a given
/// child?
///
/// Returns `true` if there is a default value in the rule, or if the rule
/// requires this parameter to be non-optional.
/// If the rule is `nil`, it will return false.
func ruleBasedShouldOverrideDefault(for child: Child, with rule: ConvenienceInitRule?) -> Bool {
if let rule {
// If the rule provides a default for this child, override it and set the rule-based default.
if rule.defaults[child.name] != nil {
return true
}

// For the non-optional rule-based parameter, strip the default value (override, but there will be no default)
return rule.nonOptionalChildName == child.name
} else {
parameterName = child.varOrCaseName
return false
}
}


/// Generates a ``FunctionParameterSyntax`` for a given ``Child`` of this node.
///
/// - parameters:
/// - child: The ``Child`` to generate the parameter for.
/// - isOptional: Is the parameter optional?
///
func generateInitFunctionParameterSyntax(
for child: Child,
isOptional: Bool,
defaultValue: InitializerClauseSyntax? = nil
) -> FunctionParameterSyntax {
let parameterName = generateChildParameterName(for: child)

return FunctionParameterSyntax(
leadingTrivia: .newline,
firstName: child.isUnexpectedNodes ? .wildcardToken(trailingTrivia: .space) : parameterName,
secondName: child.isUnexpectedNodes ? parameterName : nil,
colon: .colonToken(),
type: paramType,
defaultValue: child.defaultInitialization
type: generateChildParameterType(for: child, isOptional: isOptional),
defaultValue: defaultValue
)
}

// Iterate over all children including unexpected, or only over expected children of the Node.
//
// For convenience initializers, we don't need unexpected tokens in the arguments list
// because convenience initializers are meant to be used bo developers manually
// hence there should be no unexpected tokens.
let childrenToIterate = rule != nil ? nonUnexpectedChildren : children

// Iterate over the selected children, and make FunctionParameterSyntax for each of them.
let params = FunctionParameterListSyntax {
FunctionParameterSyntax("leadingTrivia: Trivia? = nil")

for child in children {
createFunctionParameterSyntax(for: child)
for child in childrenToIterate {
generateInitFunctionParameterSyntax(
for: child,
isOptional: ruleBasedChildIsOptional(for: child, with: rule),
defaultValue: ruleBasedChildDefaultValue(for: child, with: rule)
)
}

FunctionParameterSyntax("trailingTrivia: Trivia? = nil")
Expand All @@ -75,6 +179,18 @@ extension LayoutNode {
"""
}

/// Returns a DccC comment for the parameters that get a default value,
/// with their corresponding default values, for a rule-based convenience initializer
/// for a node.
func generateRuleBasedInitParamsDocComment(for rule: ConvenienceInitRule) -> SwiftSyntax.Trivia {
var params = ""
for (childName, defaultValue) in rule.defaults {
params += " - `\(childName)`: `TokenSyntax.\(defaultValue.spec.varOrCaseName)Token()`\n"
}
return docCommentTrivia(from: params)
}

/// Returns a DocC comment for the full memberwise initializer for this node.
func generateInitializerDocComment() -> SwiftSyntax.Trivia {
func generateParamDocComment(for child: Child) -> String? {
if child.documentationAbstract.isEmpty {
Expand Down