From 09e7d5761594fdb3d4881a1b4d7bbaf75e97c480 Mon Sep 17 00:00:00 2001 From: Natik Gadzhi Date: Fri, 1 Sep 2023 16:20:47 -0700 Subject: [PATCH 1/4] WIP: Make convenience initializers with CodeGeneration --- .../SyntaxSupport/AttributeNodes.swift | 8 ++ .../Sources/SyntaxSupport/Node.swift | 16 +++ .../LayoutNode+Extensions.swift | 110 ++++++++++++++---- .../swiftsyntax/SyntaxNodesFile.swift | 50 ++++++++ .../generated/syntaxNodes/SyntaxNodesAB.swift | 27 +++++ 5 files changed, 191 insertions(+), 20 deletions(-) diff --git a/CodeGeneration/Sources/SyntaxSupport/AttributeNodes.swift b/CodeGeneration/Sources/SyntaxSupport/AttributeNodes.swift index f8ada42f463..ddebf674af2 100644 --- a/CodeGeneration/Sources/SyntaxSupport/AttributeNodes.swift +++ b/CodeGeneration/Sources/SyntaxSupport/AttributeNodes.swift @@ -49,6 +49,14 @@ public let ATTRIBUTE_NODES: [Node] = [ nameForDiagnostics: "attribute", documentation: "An `@` attribute.", parserFunction: "parseAttribute", + rules: [ + NodeInitRule( + nonOptionalChildName: "arguments", + childDefaultValues: [ + "leftParen": .leftParen, + "rightParen": .rightParen + ]) + ], children: [ Child( name: "atSign", diff --git a/CodeGeneration/Sources/SyntaxSupport/Node.swift b/CodeGeneration/Sources/SyntaxSupport/Node.swift index 36bd7830db6..e339c839f1f 100644 --- a/CodeGeneration/Sources/SyntaxSupport/Node.swift +++ b/CodeGeneration/Sources/SyntaxSupport/Node.swift @@ -63,6 +63,9 @@ public class Node { return kind.varOrCaseName } + /// List of convenience initializer rules for this node. + public let rules: [NodeInitRule] + /// 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? { @@ -112,6 +115,7 @@ public class Node { documentation: String? = nil, parserFunction: TokenSyntax? = nil, traits: [String] = [], + rules: [NodeInitRule] = [], children: [Child] = [] ) { precondition(base != .syntaxCollection) @@ -124,6 +128,11 @@ public class Node { self.documentation = docCommentTrivia(from: documentation) self.parserFunction = parserFunction + + // FIXME: We should validate rules and check that all referenced children + // elements in fact exist on that node. + self.rules = rules + let childrenWithUnexpected: [Child] if children.isEmpty { childrenWithUnexpected = [ @@ -229,6 +238,7 @@ public class Node { isExperimental: Bool = false, nameForDiagnostics: String?, documentation: String? = nil, + rules: [NodeInitRule] = [], parserFunction: TokenSyntax? = nil, elementChoices: [SyntaxNodeKind] ) { @@ -239,6 +249,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) @@ -380,3 +391,8 @@ fileprivate extension Child { fileprivate extension Node { } + +public struct NodeInitRule { + public let nonOptionalChildName: String + public let childDefaultValues: [String: Token] +} diff --git a/CodeGeneration/Sources/generate-swift-syntax/LayoutNode+Extensions.swift b/CodeGeneration/Sources/generate-swift-syntax/LayoutNode+Extensions.swift index 495cbf89bf5..fea16e01f5a 100644 --- a/CodeGeneration/Sources/generate-swift-syntax/LayoutNode+Extensions.swift +++ b/CodeGeneration/Sources/generate-swift-syntax/LayoutNode+Extensions.swift @@ -16,52 +16,114 @@ import SyntaxSupport import Utils extension LayoutNode { - func generateInitializerDeclHeader(useDeprecatedChildName: Bool = false) -> SyntaxNodeString { + + func makeChildParamType(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 memberwise SyntaxNode initializer `SyntaxNodeString`. + /// + /// - parameters: + /// - rule: The ``NodeInitRule`` 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. + func generateInitializerDeclHeader(for rule: NodeInitRule? = 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)" + func childParameterName(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))?" + func ruleBasedChildIsOptional(for child: Child, with rule: NodeInitRule?) -> Bool? { + if let rule = rule { + if rule.nonOptionalChildName == child.name { + return false } else { - paramType = "\(paramType)?" + return child.isOptional } + } else { + return nil } + } - let parameterName: TokenSyntax + func ruleBasedChildDefaultValue(for child: Child, with rule: NodeInitRule?) -> InitializerClauseSyntax? { + if let rule, let defaultValue = rule.childDefaultValues[child.name] { + return InitializerClauseSyntax( + equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), + value: ExprSyntax(".\(defaultValue.spec.varOrCaseName)Token()") + ) + } else { + return nil + } + } - if useDeprecatedChildName, let deprecatedVarName = child.deprecatedVarName { - parameterName = deprecatedVarName + func ruleBasedShouldOverrideDefault(for child: Child, with rule: NodeInitRule?) -> Bool { + if let rule { + // If the rule provides a default for this child, override it and set the rule-based default. + if rule.childDefaultValues[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 } + } + + func createFunctionParameterSyntax(for child: Child, overrideOptional: Bool? = nil, shouldOverrideDefault: Bool = false, overrideDefaultValue: InitializerClauseSyntax? = nil) -> FunctionParameterSyntax { + + let parameterName = childParameterName(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: makeChildParamType(for: child, isOptional: overrideOptional ?? child.isOptional), + defaultValue: shouldOverrideDefault ? overrideDefaultValue : child.defaultInitialization ) } + // 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 + let params = FunctionParameterListSyntax { FunctionParameterSyntax("leadingTrivia: Trivia? = nil") - for child in children { - createFunctionParameterSyntax(for: child) + for child in childrenToIterate { + createFunctionParameterSyntax(for: child, + overrideOptional: ruleBasedChildIsOptional(for: child, with: rule), + shouldOverrideDefault: ruleBasedShouldOverrideDefault(for: child, with: rule), + overrideDefaultValue: ruleBasedChildDefaultValue(for: child, with: rule)) } FunctionParameterSyntax("trailingTrivia: Trivia? = nil") @@ -75,6 +137,14 @@ extension LayoutNode { """ } + func generateRuleBasedDefaultValuesDocComment(for rule: NodeInitRule) -> SwiftSyntax.Trivia { + var params = "" + for (childName, defaultValue) in rule.childDefaultValues { + params += " - `\(childName)`: `TokenSyntax.\(defaultValue.spec.varOrCaseName)Token()`\n" + } + return docCommentTrivia(from: params) + } + func generateInitializerDocComment() -> SwiftSyntax.Trivia { func generateParamDocComment(for child: Child) -> String? { if child.documentationAbstract.isEmpty { diff --git a/CodeGeneration/Sources/generate-swift-syntax/templates/swiftsyntax/SyntaxNodesFile.swift b/CodeGeneration/Sources/generate-swift-syntax/templates/swiftsyntax/SyntaxNodesFile.swift index 7cd8fc4ec43..4b0b32cced2 100644 --- a/CodeGeneration/Sources/generate-swift-syntax/templates/swiftsyntax/SyntaxNodesFile.swift +++ b/CodeGeneration/Sources/generate-swift-syntax/templates/swiftsyntax/SyntaxNodesFile.swift @@ -78,6 +78,55 @@ func syntaxNode(nodesStartingWith: [Character]) -> SourceFileSyntax { """ ) + for rule in node.rules { + try! InitializerDeclSyntax( + """ + /// A convenience initializer for ``\(node.kind.syntaxType)`` + /// that takes a non-optional value for `\(raw: rule.nonOptionalChildName)` parameter, + /// and adds the following default values: + \(node.generateRuleBasedDefaultValuesDocComment(for: rule)) + \(node.generateInitializerDeclHeader(for: rule)) + """ + ) { + // Convenience initializer just calls the full initializer + // with certain child parameters specified as optional types + // and providing the rule-based default value for the affected + // parameters. + FunctionCallExprSyntax( + calledExpression: ExprSyntax("self.init"), + leftParen: .leftParenToken(), + arguments: LabeledExprListSyntax { + LabeledExprSyntax( + label: TokenSyntax("leadingTrivia"), + colon: .colonToken(), + expression: ExprSyntax("leadingTrivia"), + trailingComma: .commaToken() + ) + + for child in node.nonUnexpectedChildren { + LabeledExprSyntax( + label: child.varOrCaseName, + colon: .colonToken(), + expression: rule.nonOptionalChildName == child.name ? ExprSyntax("\(child.varOrCaseName.backtickedIfNeeded) as \(node.makeChildParamType(for: child, isOptional: true))") : ExprSyntax("\(child.varOrCaseName.backtickedIfNeeded)"), + trailingComma: .commaToken() + ) + } + + LabeledExprSyntax( + label: TokenSyntax("trailingTrivia"), + colon: .colonToken(), + expression: ExprSyntax("trailingTrivia") + ) + }, + rightParen: .rightParenToken() + ) + } + } + + // The main member-wise initializer + // generateInitializerDocComment renders DocC comment + // generateInitializerDeclHeader renders the actual init declaration + // and lists out all it's parameters and their default values try! InitializerDeclSyntax( """ \(node.generateInitializerDocComment()) @@ -100,6 +149,7 @@ func syntaxNode(nodesStartingWith: [Character]) -> SourceFileSyntax { ) ) ) + let layoutList = ArrayExprSyntax { for child in node.children { ArrayElementSyntax( diff --git a/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesAB.swift b/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesAB.swift index e9e13445221..fd04fd0853c 100644 --- a/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesAB.swift +++ b/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesAB.swift @@ -2575,6 +2575,33 @@ public struct AttributeSyntax: SyntaxProtocol, SyntaxHashable, _LeafSyntaxNodePr self._syntaxNode = Syntax(data) } + /// A convenience initializer for ``AttributeSyntax`` + /// that takes a non-optional value for `arguments` parameter, + /// and adds the following default values: + /// - `rightParen`: `TokenSyntax.rightParenToken()` + /// - `leftParen`: `TokenSyntax.leftParenToken()` + /// + public init( + leadingTrivia: Trivia? = nil, + atSign: TokenSyntax = .atSignToken(), + attributeName: some TypeSyntaxProtocol, + leftParen: TokenSyntax? = .leftParenToken(), + arguments: Arguments, + rightParen: TokenSyntax? = .rightParenToken(), + trailingTrivia: Trivia? = nil + + ) { + self.init( + leadingTrivia: leadingTrivia, + atSign: atSign, + attributeName: attributeName, + leftParen: leftParen, + arguments: arguments as Arguments?, + rightParen: rightParen, + trailingTrivia: trailingTrivia + ) + } + /// - Parameters: /// - leadingTrivia: Trivia to be prepended to the leading trivia of the node’s first token. If the node is empty, there is no token to attach the trivia to and the parameter is ignored. /// - atSign: The `@` sign. From 17fbad3f77c33343c53b98a627bf8d0b88f93c20 Mon Sep 17 00:00:00 2001 From: Natik Gadzhi Date: Wed, 6 Sep 2023 17:55:20 -0700 Subject: [PATCH 2/4] Improved convenience init generator code --- .../LayoutNode+Extensions.swift | 88 ++++++++++++++----- .../swiftsyntax/SyntaxNodesFile.swift | 6 +- 2 files changed, 71 insertions(+), 23 deletions(-) diff --git a/CodeGeneration/Sources/generate-swift-syntax/LayoutNode+Extensions.swift b/CodeGeneration/Sources/generate-swift-syntax/LayoutNode+Extensions.swift index fea16e01f5a..773efc37a7f 100644 --- a/CodeGeneration/Sources/generate-swift-syntax/LayoutNode+Extensions.swift +++ b/CodeGeneration/Sources/generate-swift-syntax/LayoutNode+Extensions.swift @@ -17,7 +17,8 @@ import Utils extension LayoutNode { - func makeChildParamType(for child: Child, isOptional: Bool = false) -> TypeSyntax { + /// Returns Child parameter type as a ``TypeSyntax``. + func generateChildParameterType(for child: Child, isOptional: Bool = false) -> TypeSyntax { var paramType: TypeSyntax if !child.kind.isNodeChoicesEmpty { @@ -39,17 +40,21 @@ extension LayoutNode { return paramType } - /// Generates a memberwise SyntaxNode initializer `SyntaxNodeString`. + /// Generates a convenience memberwise SyntaxNode initializer based on a + /// given ``NodeInitRule``. /// /// - parameters: /// - rule: The ``NodeInitRule`` 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: NodeInitRule? = nil, useDeprecatedChildName: Bool = false) -> SyntaxNodeString { if children.isEmpty { return "public init()" } - func childParameterName(for child: Child) -> TokenSyntax { + /// Returns the child paramter name. + func generateChildParameterName(for child: Child) -> TokenSyntax { let parameterName: TokenSyntax if useDeprecatedChildName, let deprecatedVarName = child.deprecatedVarName { @@ -60,7 +65,14 @@ extension LayoutNode { return parameterName } - func ruleBasedChildIsOptional(for child: Child, with rule: NodeInitRule?) -> Bool? { + /// Returns whether a given child should be optional in the initializer, + /// based on a provided ``NodeInitRule``. + /// + /// 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: NodeInitRule?) -> Bool { if let rule = rule { if rule.nonOptionalChildName == child.name { return false @@ -68,21 +80,37 @@ extension LayoutNode { return child.isOptional } } else { - return nil + return child.isOptional } } + /// Returns a default value for a given child, based on a provided + /// ``NodeInitRule``. + /// + /// If the rule should not affect this child, the + /// `child.defualtInitialization` will be returned. func ruleBasedChildDefaultValue(for child: Child, with rule: NodeInitRule?) -> InitializerClauseSyntax? { - if let rule, let defaultValue = rule.childDefaultValues[child.name] { - return InitializerClauseSyntax( - equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), - value: ExprSyntax(".\(defaultValue.spec.varOrCaseName)Token()") - ) + if ruleBasedShouldOverrideDefault(for: child, with: rule) { + if let rule, let defaultValue = rule.childDefaultValues[child.name] { + return InitializerClauseSyntax( + equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), + value: ExprSyntax(".\(defaultValue.spec.varOrCaseName)Token()") + ) + } else { + return nil + } } else { - return nil + return child.defaultInitialization } + } + /// 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: NodeInitRule?) -> Bool { if let rule { // If the rule provides a default for this child, override it and set the rule-based default. @@ -97,33 +125,47 @@ extension LayoutNode { } } - func createFunctionParameterSyntax(for child: Child, overrideOptional: Bool? = nil, shouldOverrideDefault: Bool = false, overrideDefaultValue: InitializerClauseSyntax? = nil) -> FunctionParameterSyntax { - let parameterName = childParameterName(for: child) + /// 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: makeChildParamType(for: child, isOptional: overrideOptional ?? child.isOptional), - defaultValue: shouldOverrideDefault ? overrideDefaultValue : 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 + // 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 childrenToIterate { - createFunctionParameterSyntax(for: child, - overrideOptional: ruleBasedChildIsOptional(for: child, with: rule), - shouldOverrideDefault: ruleBasedShouldOverrideDefault(for: child, with: rule), - overrideDefaultValue: ruleBasedChildDefaultValue(for: child, with: rule)) + generateInitFunctionParameterSyntax( + for: child, + isOptional: ruleBasedChildIsOptional(for: child, with: rule), + defaultValue: ruleBasedChildDefaultValue(for: child, with: rule) + ) } FunctionParameterSyntax("trailingTrivia: Trivia? = nil") @@ -137,7 +179,10 @@ extension LayoutNode { """ } - func generateRuleBasedDefaultValuesDocComment(for rule: NodeInitRule) -> SwiftSyntax.Trivia { + /// 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: NodeInitRule) -> SwiftSyntax.Trivia { var params = "" for (childName, defaultValue) in rule.childDefaultValues { params += " - `\(childName)`: `TokenSyntax.\(defaultValue.spec.varOrCaseName)Token()`\n" @@ -145,6 +190,7 @@ extension LayoutNode { 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 { diff --git a/CodeGeneration/Sources/generate-swift-syntax/templates/swiftsyntax/SyntaxNodesFile.swift b/CodeGeneration/Sources/generate-swift-syntax/templates/swiftsyntax/SyntaxNodesFile.swift index 4b0b32cced2..530595e6390 100644 --- a/CodeGeneration/Sources/generate-swift-syntax/templates/swiftsyntax/SyntaxNodesFile.swift +++ b/CodeGeneration/Sources/generate-swift-syntax/templates/swiftsyntax/SyntaxNodesFile.swift @@ -84,7 +84,7 @@ func syntaxNode(nodesStartingWith: [Character]) -> SourceFileSyntax { /// A convenience initializer for ``\(node.kind.syntaxType)`` /// that takes a non-optional value for `\(raw: rule.nonOptionalChildName)` parameter, /// and adds the following default values: - \(node.generateRuleBasedDefaultValuesDocComment(for: rule)) + \(node.generateRuleBasedInitParamsDocComment(for: rule)) \(node.generateInitializerDeclHeader(for: rule)) """ ) { @@ -107,7 +107,9 @@ func syntaxNode(nodesStartingWith: [Character]) -> SourceFileSyntax { LabeledExprSyntax( label: child.varOrCaseName, colon: .colonToken(), - expression: rule.nonOptionalChildName == child.name ? ExprSyntax("\(child.varOrCaseName.backtickedIfNeeded) as \(node.makeChildParamType(for: child, isOptional: true))") : ExprSyntax("\(child.varOrCaseName.backtickedIfNeeded)"), + expression: rule.nonOptionalChildName == child.name ? + ExprSyntax("\(child.varOrCaseName.backtickedIfNeeded) as \(node.generateChildParameterType(for: child, isOptional: true))") : + ExprSyntax("\(child.varOrCaseName.backtickedIfNeeded)"), trailingComma: .commaToken() ) } From 8a30b2d7ed4ca1e158c5f6cff98a059b2976bd8e Mon Sep 17 00:00:00 2001 From: Natik Gadzhi Date: Wed, 6 Sep 2023 18:00:57 -0700 Subject: [PATCH 3/4] Moved NodeInitRule into a separate file --- .../Sources/SyntaxSupport/Node.swift | 12 +----- .../Sources/SyntaxSupport/NodeInitRule.swift | 37 +++++++++++++++++++ 2 files changed, 39 insertions(+), 10 deletions(-) create mode 100644 CodeGeneration/Sources/SyntaxSupport/NodeInitRule.swift diff --git a/CodeGeneration/Sources/SyntaxSupport/Node.swift b/CodeGeneration/Sources/SyntaxSupport/Node.swift index e339c839f1f..747bdf4ede0 100644 --- a/CodeGeneration/Sources/SyntaxSupport/Node.swift +++ b/CodeGeneration/Sources/SyntaxSupport/Node.swift @@ -63,7 +63,8 @@ public class Node { return kind.varOrCaseName } - /// List of convenience initializer rules for this node. + /// List of convenience initializer rules for this node. CodeGeneration will + /// generate a convenience initializer for each rule. public let rules: [NodeInitRule] /// If this is a layout node, return a view of the node that provides access @@ -127,10 +128,6 @@ public class Node { self.nameForDiagnostics = nameForDiagnostics self.documentation = docCommentTrivia(from: documentation) self.parserFunction = parserFunction - - - // FIXME: We should validate rules and check that all referenced children - // elements in fact exist on that node. self.rules = rules let childrenWithUnexpected: [Child] @@ -391,8 +388,3 @@ fileprivate extension Child { fileprivate extension Node { } - -public struct NodeInitRule { - public let nonOptionalChildName: String - public let childDefaultValues: [String: Token] -} diff --git a/CodeGeneration/Sources/SyntaxSupport/NodeInitRule.swift b/CodeGeneration/Sources/SyntaxSupport/NodeInitRule.swift new file mode 100644 index 00000000000..5a144caa932 --- /dev/null +++ b/CodeGeneration/Sources/SyntaxSupport/NodeInitRule.swift @@ -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 `childDefaultValues`. +/// +/// 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 NodeInitRule { + /// 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 childDefaultValues: [String: Token] +} From 6eacdbaf80bdf6f0a26a3dbf28f11222c64fbada Mon Sep 17 00:00:00 2001 From: Natik Gadzhi Date: Wed, 6 Sep 2023 23:03:01 -0700 Subject: [PATCH 4/4] More examples of autogenerating convenience inits --- .../SyntaxSupport/AttributeNodes.swift | 4 +- ...itRule.swift => ConvenienceInitRule.swift} | 6 +- .../Sources/SyntaxSupport/DeclNodes.swift | 8 +++ .../Sources/SyntaxSupport/ExprNodes.swift | 17 ++++++ .../Sources/SyntaxSupport/Node.swift | 6 +- .../LayoutNode+Extensions.swift | 24 ++++---- Sources/SwiftSyntax/Convenience.swift | 58 ------------------ .../generated/syntaxNodes/SyntaxNodesAB.swift | 2 +- .../generated/syntaxNodes/SyntaxNodesC.swift | 26 ++++++++ .../generated/syntaxNodes/SyntaxNodesEF.swift | 59 +++++++++++++++++++ 10 files changed, 131 insertions(+), 79 deletions(-) rename CodeGeneration/Sources/SyntaxSupport/{NodeInitRule.swift => ConvenienceInitRule.swift} (90%) diff --git a/CodeGeneration/Sources/SyntaxSupport/AttributeNodes.swift b/CodeGeneration/Sources/SyntaxSupport/AttributeNodes.swift index ddebf674af2..6c5af908494 100644 --- a/CodeGeneration/Sources/SyntaxSupport/AttributeNodes.swift +++ b/CodeGeneration/Sources/SyntaxSupport/AttributeNodes.swift @@ -50,9 +50,9 @@ public let ATTRIBUTE_NODES: [Node] = [ documentation: "An `@` attribute.", parserFunction: "parseAttribute", rules: [ - NodeInitRule( + ConvenienceInitRule( nonOptionalChildName: "arguments", - childDefaultValues: [ + defaults: [ "leftParen": .leftParen, "rightParen": .rightParen ]) diff --git a/CodeGeneration/Sources/SyntaxSupport/NodeInitRule.swift b/CodeGeneration/Sources/SyntaxSupport/ConvenienceInitRule.swift similarity index 90% rename from CodeGeneration/Sources/SyntaxSupport/NodeInitRule.swift rename to CodeGeneration/Sources/SyntaxSupport/ConvenienceInitRule.swift index 5a144caa932..7929e078d86 100644 --- a/CodeGeneration/Sources/SyntaxSupport/NodeInitRule.swift +++ b/CodeGeneration/Sources/SyntaxSupport/ConvenienceInitRule.swift @@ -20,18 +20,18 @@ import SwiftSyntax /// /// 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 `childDefaultValues`. +/// 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 NodeInitRule { +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 childDefaultValues: [String: Token] + public let defaults: [String: Token] } diff --git a/CodeGeneration/Sources/SyntaxSupport/DeclNodes.swift b/CodeGeneration/Sources/SyntaxSupport/DeclNodes.swift index 511ca84c3a3..ee8a7d7b3fb 100644 --- a/CodeGeneration/Sources/SyntaxSupport/DeclNodes.swift +++ b/CodeGeneration/Sources/SyntaxSupport/DeclNodes.swift @@ -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", diff --git a/CodeGeneration/Sources/SyntaxSupport/ExprNodes.swift b/CodeGeneration/Sources/SyntaxSupport/ExprNodes.swift index b79ced0bf07..dfe56e9296c 100644 --- a/CodeGeneration/Sources/SyntaxSupport/ExprNodes.swift +++ b/CodeGeneration/Sources/SyntaxSupport/ExprNodes.swift @@ -314,6 +314,14 @@ public let EXPR_NODES: [Node] = [ traits: [ "WithTrailingComma" ], + rules: [ + ConvenienceInitRule( + nonOptionalChildName: "name", + defaults: [ + "equal": .equal + ] + ) + ], children: [ Child( name: "specifier", @@ -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", diff --git a/CodeGeneration/Sources/SyntaxSupport/Node.swift b/CodeGeneration/Sources/SyntaxSupport/Node.swift index 747bdf4ede0..07ead68b2ba 100644 --- a/CodeGeneration/Sources/SyntaxSupport/Node.swift +++ b/CodeGeneration/Sources/SyntaxSupport/Node.swift @@ -65,7 +65,7 @@ public class Node { /// List of convenience initializer rules for this node. CodeGeneration will /// generate a convenience initializer for each rule. - public let rules: [NodeInitRule] + 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. @@ -116,7 +116,7 @@ public class Node { documentation: String? = nil, parserFunction: TokenSyntax? = nil, traits: [String] = [], - rules: [NodeInitRule] = [], + rules: [ConvenienceInitRule] = [], children: [Child] = [] ) { precondition(base != .syntaxCollection) @@ -235,7 +235,7 @@ public class Node { isExperimental: Bool = false, nameForDiagnostics: String?, documentation: String? = nil, - rules: [NodeInitRule] = [], + rules: [ConvenienceInitRule] = [], parserFunction: TokenSyntax? = nil, elementChoices: [SyntaxNodeKind] ) { diff --git a/CodeGeneration/Sources/generate-swift-syntax/LayoutNode+Extensions.swift b/CodeGeneration/Sources/generate-swift-syntax/LayoutNode+Extensions.swift index 773efc37a7f..d2c51f59762 100644 --- a/CodeGeneration/Sources/generate-swift-syntax/LayoutNode+Extensions.swift +++ b/CodeGeneration/Sources/generate-swift-syntax/LayoutNode+Extensions.swift @@ -41,14 +41,14 @@ extension LayoutNode { } /// Generates a convenience memberwise SyntaxNode initializer based on a - /// given ``NodeInitRule``. + /// given ``ConvenienceInitRule``. /// /// - parameters: - /// - rule: The ``NodeInitRule`` to use for generating the initializer. Applying a rule will make some children non-optional, and set default values for other children. + /// - 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: NodeInitRule? = nil, useDeprecatedChildName: Bool = false) -> SyntaxNodeString { + func generateInitializerDeclHeader(for rule: ConvenienceInitRule? = nil, useDeprecatedChildName: Bool = false) -> SyntaxNodeString { if children.isEmpty { return "public init()" } @@ -66,13 +66,13 @@ extension LayoutNode { } /// Returns whether a given child should be optional in the initializer, - /// based on a provided ``NodeInitRule``. + /// 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: NodeInitRule?) -> Bool { + func ruleBasedChildIsOptional(for child: Child, with rule: ConvenienceInitRule?) -> Bool { if let rule = rule { if rule.nonOptionalChildName == child.name { return false @@ -85,13 +85,13 @@ extension LayoutNode { } /// Returns a default value for a given child, based on a provided - /// ``NodeInitRule``. + /// ``ConvenienceInitRule``. /// /// If the rule should not affect this child, the /// `child.defualtInitialization` will be returned. - func ruleBasedChildDefaultValue(for child: Child, with rule: NodeInitRule?) -> InitializerClauseSyntax? { + func ruleBasedChildDefaultValue(for child: Child, with rule: ConvenienceInitRule?) -> InitializerClauseSyntax? { if ruleBasedShouldOverrideDefault(for: child, with: rule) { - if let rule, let defaultValue = rule.childDefaultValues[child.name] { + if let rule, let defaultValue = rule.defaults[child.name] { return InitializerClauseSyntax( equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), value: ExprSyntax(".\(defaultValue.spec.varOrCaseName)Token()") @@ -111,10 +111,10 @@ extension LayoutNode { /// 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: NodeInitRule?) -> Bool { + 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.childDefaultValues[child.name] != nil { + if rule.defaults[child.name] != nil { return true } @@ -182,9 +182,9 @@ 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: NodeInitRule) -> SwiftSyntax.Trivia { + func generateRuleBasedInitParamsDocComment(for rule: ConvenienceInitRule) -> SwiftSyntax.Trivia { var params = "" - for (childName, defaultValue) in rule.childDefaultValues { + for (childName, defaultValue) in rule.defaults { params += " - `\(childName)`: `TokenSyntax.\(defaultValue.spec.varOrCaseName)Token()`\n" } return docCommentTrivia(from: params) diff --git a/Sources/SwiftSyntax/Convenience.swift b/Sources/SwiftSyntax/Convenience.swift index 6521bd0e569..177915f49fe 100644 --- a/Sources/SwiftSyntax/Convenience.swift +++ b/Sources/SwiftSyntax/Convenience.swift @@ -10,64 +10,6 @@ // //===----------------------------------------------------------------------===// -extension ClosureCaptureSyntax { - - /// Creates a ``ClosureCaptureSyntax`` with a `name`, and automatically adds an `equal` token to it since the name is non-optional. - /// - /// - SeeAlso: ``ClosureCaptureSyntax/init(leadingTrivia:_:specifier:_:name:_:equal:_:expression:_:trailingComma:_:trailingTrivia:)``. - /// - public init( - leadingTrivia: Trivia? = nil, - specifier: ClosureCaptureSpecifierSyntax? = nil, - name: TokenSyntax, - equal: TokenSyntax = TokenSyntax.equalToken(), - expression: some ExprSyntaxProtocol, - trailingComma: TokenSyntax? = nil, - trailingTrivia: Trivia? = nil - ) { - self.init( - leadingTrivia: leadingTrivia, - specifier: specifier, - name: name as TokenSyntax?, - equal: equal, - expression: expression, - trailingComma: trailingComma, - trailingTrivia: trailingTrivia - ) - } -} - -extension EnumCaseParameterSyntax { - - /// Creates an ``EnumCaseParameterSyntax`` with a `firstName`, and automatically adds a `colon` to it. - /// - /// - SeeAlso: For more information on the arguments, see ``EnumCaseParameterSyntax/init(leadingTrivia:_:modifiers:_:firstName:_:secondName:_:colon:_:type:_:defaultArgument:_:trailingComma:_:trailingTrivia:)`` - /// - public init( - leadingTrivia: Trivia? = nil, - modifiers: DeclModifierListSyntax = [], - firstName: TokenSyntax, - secondName: TokenSyntax? = nil, - colon: TokenSyntax = TokenSyntax.colonToken(), - type: some TypeSyntaxProtocol, - defaultValue: InitializerClauseSyntax? = nil, - trailingComma: TokenSyntax? = nil, - trailingTrivia: Trivia? = nil - ) { - self.init( - leadingTrivia: leadingTrivia, - modifiers: modifiers, - firstName: firstName as TokenSyntax?, - secondName: secondName, - colon: colon, - type: type, - defaultValue: defaultValue, - trailingComma: trailingComma, - trailingTrivia: trailingTrivia - ) - } -} - extension MemberAccessExprSyntax { /// Creates a new ``MemberAccessExprSyntax`` where the accessed member is represented by /// an identifier without specifying argument labels. diff --git a/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesAB.swift b/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesAB.swift index fd04fd0853c..8d1ccb1b660 100644 --- a/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesAB.swift +++ b/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesAB.swift @@ -2578,8 +2578,8 @@ public struct AttributeSyntax: SyntaxProtocol, SyntaxHashable, _LeafSyntaxNodePr /// A convenience initializer for ``AttributeSyntax`` /// that takes a non-optional value for `arguments` parameter, /// and adds the following default values: - /// - `rightParen`: `TokenSyntax.rightParenToken()` /// - `leftParen`: `TokenSyntax.leftParenToken()` + /// - `rightParen`: `TokenSyntax.rightParenToken()` /// public init( leadingTrivia: Trivia? = nil, diff --git a/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesC.swift b/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesC.swift index 388feb8c1f7..b2a1af0786d 100644 --- a/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesC.swift +++ b/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesC.swift @@ -1585,6 +1585,32 @@ public struct ClosureCaptureSyntax: SyntaxProtocol, SyntaxHashable, _LeafSyntaxN self._syntaxNode = Syntax(data) } + /// A convenience initializer for ``ClosureCaptureSyntax`` + /// that takes a non-optional value for `name` parameter, + /// and adds the following default values: + /// - `equal`: `TokenSyntax.equalToken()` + /// + public init( + leadingTrivia: Trivia? = nil, + specifier: ClosureCaptureSpecifierSyntax? = nil, + name: TokenSyntax, + equal: TokenSyntax? = .equalToken(), + expression: some ExprSyntaxProtocol, + trailingComma: TokenSyntax? = nil, + trailingTrivia: Trivia? = nil + + ) { + self.init( + leadingTrivia: leadingTrivia, + specifier: specifier, + name: name as TokenSyntax?, + equal: equal, + expression: expression, + trailingComma: trailingComma, + trailingTrivia: trailingTrivia + ) + } + /// - Parameters: /// - leadingTrivia: Trivia to be prepended to the leading trivia of the node’s first token. If the node is empty, there is no token to attach the trivia to and the parameter is ignored. /// - trailingTrivia: Trivia to be appended to the trailing trivia of the node’s last token. If the node is empty, there is no token to attach the trivia to and the parameter is ignored. diff --git a/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesEF.swift b/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesEF.swift index cd0e5a5c089..90d6a36e8ce 100644 --- a/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesEF.swift +++ b/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesEF.swift @@ -982,6 +982,36 @@ public struct EnumCaseParameterSyntax: SyntaxProtocol, SyntaxHashable, _LeafSynt self._syntaxNode = Syntax(data) } + /// A convenience initializer for ``EnumCaseParameterSyntax`` + /// that takes a non-optional value for `firstName` parameter, + /// and adds the following default values: + /// - `colon`: `TokenSyntax.colonToken()` + /// + public init( + leadingTrivia: Trivia? = nil, + modifiers: DeclModifierListSyntax = [], + firstName: TokenSyntax, + secondName: TokenSyntax? = nil, + colon: TokenSyntax? = .colonToken(), + type: some TypeSyntaxProtocol, + defaultValue: InitializerClauseSyntax? = nil, + trailingComma: TokenSyntax? = nil, + trailingTrivia: Trivia? = nil + + ) { + self.init( + leadingTrivia: leadingTrivia, + modifiers: modifiers, + firstName: firstName as TokenSyntax?, + secondName: secondName, + colon: colon, + type: type, + defaultValue: defaultValue, + trailingComma: trailingComma, + trailingTrivia: trailingTrivia + ) + } + /// - Parameters: /// - leadingTrivia: Trivia to be prepended to the leading trivia of the node’s first token. If the node is empty, there is no token to attach the trivia to and the parameter is ignored. /// - colon: If the parameter has a label, the colon separating the label from the type. @@ -3128,6 +3158,35 @@ public struct FunctionCallExprSyntax: ExprSyntaxProtocol, SyntaxHashable, _LeafE self._syntaxNode = Syntax(data) } + /// A convenience initializer for ``FunctionCallExprSyntax`` + /// that takes a non-optional value for `arguments` parameter, + /// and adds the following default values: + /// - `rightParen`: `TokenSyntax.rightParenToken()` + /// - `leftParen`: `TokenSyntax.leftParenToken()` + /// + public init( + leadingTrivia: Trivia? = nil, + calledExpression: some ExprSyntaxProtocol, + leftParen: TokenSyntax? = .leftParenToken(), + arguments: LabeledExprListSyntax, + rightParen: TokenSyntax? = .rightParenToken(), + trailingClosure: ClosureExprSyntax? = nil, + additionalTrailingClosures: MultipleTrailingClosureElementListSyntax = [], + trailingTrivia: Trivia? = nil + + ) { + self.init( + leadingTrivia: leadingTrivia, + calledExpression: calledExpression, + leftParen: leftParen, + arguments: arguments as LabeledExprListSyntax?, + rightParen: rightParen, + trailingClosure: trailingClosure, + additionalTrailingClosures: additionalTrailingClosures, + trailingTrivia: trailingTrivia + ) + } + /// - Parameters: /// - leadingTrivia: Trivia to be prepended to the leading trivia of the node’s first token. If the node is empty, there is no token to attach the trivia to and the parameter is ignored. /// - trailingTrivia: Trivia to be appended to the trailing trivia of the node’s last token. If the node is empty, there is no token to attach the trivia to and the parameter is ignored.