Skip to content

Allow node children to override whether a token needs a leading/trailing space #1298

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
Feb 6, 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
6 changes: 4 additions & 2 deletions CodeGeneration/Sources/SyntaxSupport/Child.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ public enum ChildKind {
/// The child is a collection of `kind`.
case collection(kind: String, collectionElementName: String)
/// The child is a token that matches one of the given `choices`.
case token(choices: [TokenChoice])
/// If `requiresLeadingSpace` or `requiresTrailingSpace` is not `nil`, it
/// overrides the default leading/trailing space behavior of the token.
case token(choices: [TokenChoice], requiresLeadingSpace: Bool? = nil, requiresTrailingSpace: Bool? = nil)

public var isNodeChoices: Bool {
if case .nodeChoices = self {
Expand Down Expand Up @@ -67,7 +69,7 @@ public class Child {
return "syntax"
case .collection(kind: let kind, collectionElementName: _):
return kind
case .token(choices: let choices):
case .token(choices: let choices, requiresLeadingSpace: _, requiresTrailingSpace: _):
if choices.count == 1 {
switch choices.first! {
case .keyword: return "KeywordToken"
Expand Down
2 changes: 1 addition & 1 deletion CodeGeneration/Sources/SyntaxSupport/KeywordSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ public let KEYWORDS: [KeywordSpec] = [
KeywordSpec("in", isLexerClassified: true, requiresLeadingSpace: true, requiresTrailingSpace: true),
KeywordSpec("indirect"),
KeywordSpec("infix"),
KeywordSpec("init", isLexerClassified: true, requiresTrailingSpace: true),
KeywordSpec("init", isLexerClassified: true),
KeywordSpec("inline"),
KeywordSpec("inout", isLexerClassified: true, requiresTrailingSpace: true),
KeywordSpec("internal", isLexerClassified: true, requiresTrailingSpace: true),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ public let ATTRIBUTE_NODES: [Node] = [
kind: "Syntax",
children: [
Child(name: "ForLabel",
kind: .token(choices: [.keyword(text: "for")])),
kind: .token(choices: [.keyword(text: "for")], requiresTrailingSpace: false)),
Child(name: "Colon",
kind: .token(choices: [.token(tokenKind: "ColonToken")])),
Child(name: "Declname",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public let AVAILABILITY_NODES: [Node] = [
Child(name: "Entry",
kind: .nodeChoices(choices: [
Child(name: "Token",
kind: .token(choices: [.token(tokenKind: "BinaryOperatorToken"), .token(tokenKind: "IdentifierToken")])),
kind: .token(choices: [.token(tokenKind: "BinaryOperatorToken"), .token(tokenKind: "IdentifierToken")], requiresLeadingSpace: false, requiresTrailingSpace: false)),
Child(name: "AvailabilityVersionRestriction",
kind: .node(kind: "AvailabilityVersionRestriction")),
Child(name: "AvailabilityLabeledArgument",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public let EXPR_NODES: [Node] = [
Child(name: "TryKeyword",
kind: .token(choices: [.keyword(text: "try")])),
Child(name: "QuestionOrExclamationMark",
kind: .token(choices: [.token(tokenKind: "PostfixQuestionMarkToken"), .token(tokenKind: "ExclamationMarkToken")]),
kind: .token(choices: [.token(tokenKind: "PostfixQuestionMarkToken"), .token(tokenKind: "ExclamationMarkToken")], requiresTrailingSpace: true),
isOptional: true),
Child(name: "Expression",
kind: .node(kind: "Expr"))
Expand Down Expand Up @@ -284,7 +284,7 @@ public let EXPR_NODES: [Node] = [
Child(name: "Content",
kind: .nodeChoices(choices: [
Child(name: "Colon",
kind: .token(choices: [.token(tokenKind: "ColonToken")])),
kind: .token(choices: [.token(tokenKind: "ColonToken")], requiresTrailingSpace: false)),
Child(name: "Elements",
kind: .node(kind: "DictionaryElementList"))
]),
Expand Down
8 changes: 7 additions & 1 deletion CodeGeneration/Sources/SyntaxSupport/gyb_helpers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,13 @@ def make_swift_child(child, spaces):
mapped_choices = [f'.token(tokenKind: "{choice.name}Token")' for (choice, text) in child.token_choices if text is None]
mapped_choices += [f'.keyword(text: "{text}")' for (choice, text) in child.token_choices if text is not None]
joined_choices = ', '.join(mapped_choices)
kind = f'.token(choices: [{joined_choices}])'
token_arguments = [f'choices: [{joined_choices}]']
if child.requires_leading_space is not None:
token_arguments += ['requiresLeadingSpace: ' + ('true' if child.requires_leading_space else 'false')]
if child.requires_trailing_space is not None:
token_arguments += ['requiresTrailingSpace: ' + ('true' if child.requires_trailing_space else 'false')]
arguments = ', '.join(token_arguments)
kind = f'.token({arguments})'
elif child.collection_element_name:
kind = f'.collection(kind: "{child.syntax_kind}", collectionElementName: "{child.collection_element_name}")'
elif child.node_choices:
Expand Down
4 changes: 2 additions & 2 deletions CodeGeneration/Sources/Utils/SyntaxBuildableChild.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public extension Child {

var defaultInitialization: ExprSyntax? {
switch kind {
case .token(choices: let choices):
case .token(choices: let choices, requiresLeadingSpace: _, requiresTrailingSpace: _):
if choices.count == 1, case .keyword(text: let text) = choices.first {
var textChoice = text
if textChoice == "init" {
Expand All @@ -66,7 +66,7 @@ public extension Child {
/// `assert` statement that verifies the variable with name var_name and of type
/// `TokenSyntax` contains one of the supported text options. Otherwise return `nil`.
func generateAssertStmtTextChoices(varName: String) -> FunctionCallExprSyntax? {
guard case .token(choices: let choices) = kind else {
guard case .token(choices: let choices, requiresLeadingSpace: _, requiresTrailingSpace: _) = kind else {
return nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,40 @@ import SwiftSyntaxBuilder
import SyntaxSupport
import Utils

extension Child {
var requiresLeadingSpace: Bool? {
switch self.kind {
case .token(choices: _, requiresLeadingSpace: let requiresLeadingSpace, requiresTrailingSpace: _):
return requiresLeadingSpace
case .nodeChoices(choices: let choices):
for choice in choices {
if let requiresLeadingSpace = choice.requiresLeadingSpace {
return requiresLeadingSpace
}
}
default:
break
}
return nil
}

var requiresTrailingSpace: Bool? {
switch self.kind {
case .token(choices: _, requiresLeadingSpace: _, requiresTrailingSpace: let requiresTrailingSpace):
return requiresTrailingSpace
case .nodeChoices(choices: let choices):
for choice in choices {
if let requiresTrailingSpace = choice.requiresTrailingSpace {
return requiresTrailingSpace
}
}
default:
break
}
return nil
}
}

let basicFormatFile = SourceFileSyntax {
DeclSyntax(
"""
Expand Down Expand Up @@ -126,14 +160,34 @@ let basicFormatFile = SourceFileSyntax {
}
}

try FunctionDeclSyntax(
"""
/// If this returns a value that is not `nil`, it overrides the default
/// leading space behavior of a token.
open func requiresLeadingSpace(_ keyPath: AnyKeyPath) -> Bool?
"""
) {
try SwitchStmtSyntax("switch keyPath") {
for node in SYNTAX_NODES where !node.isBase {
for child in node.children {
if let requiresLeadingSpace = child.requiresLeadingSpace {
SwitchCaseSyntax("case \\\(raw: node.type.syntaxBaseName).\(raw: child.swiftName):") {
StmtSyntax("return \(literal: requiresLeadingSpace)")
}
}
}
}
SwitchCaseSyntax("default:") {
StmtSyntax("return nil")
}
}
}

try FunctionDeclSyntax("open func requiresLeadingSpace(_ token: TokenSyntax) -> Bool") {
StmtSyntax(
"""
switch (token.previousToken(viewMode: .sourceAccurate)?.tokenKind, token.tokenKind) {
case (.leftParen, .binaryOperator): // Ensures there is no space in @available(*, deprecated)
return false
default:
break
if let keyPath = getKeyPath(token), let requiresLeadingSpace = requiresLeadingSpace(keyPath) {
return requiresLeadingSpace
}
"""
)
Expand All @@ -157,17 +211,34 @@ let basicFormatFile = SourceFileSyntax {
}
}

try FunctionDeclSyntax(
"""
/// If this returns a value that is not `nil`, it overrides the default
/// trailing space behavior of a token.
open func requiresTrailingSpace(_ keyPath: AnyKeyPath) -> Bool?
"""
) {
try SwitchStmtSyntax("switch keyPath") {
for node in SYNTAX_NODES where !node.isBase {
for child in node.children {
if let requiresTrailingSpace = child.requiresTrailingSpace {
SwitchCaseSyntax("case \\\(raw: node.type.syntaxBaseName).\(raw: child.swiftName):") {
StmtSyntax("return \(literal: requiresTrailingSpace)")
}
}
}
}
SwitchCaseSyntax("default:") {
StmtSyntax("return nil")
}
}
}

try FunctionDeclSyntax("open func requiresTrailingSpace(_ token: TokenSyntax) -> Bool") {
StmtSyntax(
"""
switch (token.tokenKind, token.parent?.kind) {
case (.colon, .dictionaryExpr): // Ensures there is not space in `[:]`
return false
case (.exclamationMark, .tryExpr), // Ensures there is a space in `try! foo`
(.postfixQuestionMark, .tryExpr): // Ensures there is a space in `try? foo`
return true
default:
break
if let keyPath = getKeyPath(token), let requiresTrailingSpace = requiresTrailingSpace(keyPath) {
return requiresTrailingSpace
}
"""
)
Expand All @@ -179,14 +250,11 @@ let basicFormatFile = SourceFileSyntax {
(.keyword(.as), .postfixQuestionMark), // Ensures there is not space in `as?`
(.exclamationMark, .leftParen), // Ensures there is not space in `myOptionalClosure!()`
(.exclamationMark, .period), // Ensures there is not space in `myOptionalBar!.foo()`
(.keyword(.`init`), .leftParen), // Ensures there is not space in `init()`
(.keyword(.`init`), .postfixQuestionMark), // Ensures there is not space in `init?`
(.postfixQuestionMark, .leftParen), // Ensures there is not space in `init?()`
(.postfixQuestionMark, .leftParen), // Ensures there is not space in `init?()` or `myOptionalClosure?()`s
(.postfixQuestionMark, .rightAngle), // Ensures there is not space in `ContiguousArray<RawSyntax?>`
(.postfixQuestionMark, .rightParen), // Ensures there is not space in `myOptionalClosure?()`
(.keyword(.try), .exclamationMark), // Ensures there is not space in `try!`
(.keyword(.try), .postfixQuestionMark), // Ensures there is not space in `try?`
(.binaryOperator, .comma): // Ensures there is no space in `@available(*, deprecated)`
(.keyword(.try), .postfixQuestionMark): // Ensures there is not space in `try?`:
return false
default:
break
Expand Down Expand Up @@ -215,7 +283,7 @@ let basicFormatFile = SourceFileSyntax {

DeclSyntax(
"""
private func getKeyPath(_ node: Syntax) -> AnyKeyPath? {
private func getKeyPath<T: SyntaxProtocol>(_ node: T) -> AnyKeyPath? {
guard let parent = node.parent else {
return nil
}
Expand Down
50 changes: 32 additions & 18 deletions Sources/SwiftBasicFormat/generated/BasicFormat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,20 @@ open class BasicFormat: SyntaxRewriter {
}
}

open func requiresLeadingSpace(_ token: TokenSyntax) -> Bool {
switch (token.previousToken(viewMode: .sourceAccurate)?.tokenKind, token.tokenKind) {
case (.leftParen, .binaryOperator): // Ensures there is no space in @available(*, deprecated)
/// If this returns a value that is not `nil`, it overrides the default
/// leading space behavior of a token.
open func requiresLeadingSpace(_ keyPath: AnyKeyPath) -> Bool? {
switch keyPath {
case \AvailabilityArgumentSyntax.entry:
return false
default:
break
return nil
}
}

open func requiresLeadingSpace(_ token: TokenSyntax) -> Bool {
if let keyPath = getKeyPath(token), let requiresLeadingSpace = requiresLeadingSpace(keyPath) {
return requiresLeadingSpace
}
switch token.tokenKind {
case .leftBrace:
Expand All @@ -163,29 +171,37 @@ open class BasicFormat: SyntaxRewriter {
}
}

open func requiresTrailingSpace(_ token: TokenSyntax) -> Bool {
switch (token.tokenKind, token.parent?.kind) {
case (.colon, .dictionaryExpr): // Ensures there is not space in `[:]`
/// If this returns a value that is not `nil`, it overrides the default
/// trailing space behavior of a token.
open func requiresTrailingSpace(_ keyPath: AnyKeyPath) -> Bool? {
switch keyPath {
case \AvailabilityArgumentSyntax.entry:
return false
case (.exclamationMark, .tryExpr), // Ensures there is a space in `try! foo`
(.postfixQuestionMark, .tryExpr): // Ensures there is a space in `try? foo`
case \DictionaryExprSyntax.content:
return false
case \DynamicReplacementArgumentsSyntax.forLabel:
return false
case \TryExprSyntax.questionOrExclamationMark:
return true
default:
break
return nil
}
}

open func requiresTrailingSpace(_ token: TokenSyntax) -> Bool {
if let keyPath = getKeyPath(token), let requiresTrailingSpace = requiresTrailingSpace(keyPath) {
return requiresTrailingSpace
}
switch (token.tokenKind, token.nextToken(viewMode: .sourceAccurate)?.tokenKind) {
case (.keyword(.as), .exclamationMark), // Ensures there is not space in `as!`
(.keyword(.as), .postfixQuestionMark), // Ensures there is not space in `as?`
(.exclamationMark, .leftParen), // Ensures there is not space in `myOptionalClosure!()`
(.exclamationMark, .period), // Ensures there is not space in `myOptionalBar!.foo()`
(.keyword(.`init`), .leftParen), // Ensures there is not space in `init()`
(.keyword(.`init`), .postfixQuestionMark), // Ensures there is not space in `init?`
(.postfixQuestionMark, .leftParen), // Ensures there is not space in `init?()`
(.postfixQuestionMark, .leftParen), // Ensures there is not space in `init?()` or `myOptionalClosure?()`s
(.postfixQuestionMark, .rightAngle), // Ensures there is not space in `ContiguousArray<RawSyntax?>`
(.postfixQuestionMark, .rightParen), // Ensures there is not space in `myOptionalClosure?()`
(.keyword(.try), .exclamationMark), // Ensures there is not space in `try!`
(.keyword(.try), .postfixQuestionMark), // Ensures there is not space in `try?`
(.binaryOperator, .comma): // Ensures there is no space in `@available(*, deprecated)`
(.keyword(.try), .postfixQuestionMark): // Ensures there is not space in `try?`:
return false
default:
break
Expand Down Expand Up @@ -257,8 +273,6 @@ open class BasicFormat: SyntaxRewriter {
return true
case .keyword(.`in`):
return true
case .keyword(.`init`):
return true
case .keyword(.`inout`):
return true
case .keyword(.`internal`):
Expand Down Expand Up @@ -310,7 +324,7 @@ open class BasicFormat: SyntaxRewriter {
}
}

private func getKeyPath(_ node: Syntax) -> AnyKeyPath? {
private func getKeyPath<T: SyntaxProtocol>(_ node: T) -> AnyKeyPath? {
guard let parent = node.parent else {
return nil
}
Expand Down
3 changes: 2 additions & 1 deletion gyb_syntax_support/AttributeNodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,8 @@
The arguments for the '@_dynamicReplacement' attribute
''',
children=[
Child('ForLabel', kind='KeywordToken', token_choices=['KeywordToken|for']),
Child('ForLabel', kind='KeywordToken', token_choices=['KeywordToken|for'],
requires_trailing_space=False),
Child('Colon', kind='ColonToken'),
Child('Declname', kind='DeclName'),
]),
Expand Down
4 changes: 3 additions & 1 deletion gyb_syntax_support/AvailabilityNodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
description='The actual argument',
node_choices=[
Child('Token', kind='Token',
token_choices=['BinaryOperatorToken', 'IdentifierToken']),
token_choices=['BinaryOperatorToken', 'IdentifierToken'],
requires_leading_space=False,
requires_trailing_space=False),
Child('AvailabilityVersionRestriction',
kind='AvailabilityVersionRestriction'),
Child('AvailabilityLabeledArgument',
Expand Down
6 changes: 5 additions & 1 deletion gyb_syntax_support/Child.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ def __init__(self, name, kind, name_for_diagnostics=None, description=None, is_o
token_choices=None, text_choices=None, node_choices=None,
collection_element_name=None,
classification=None, force_classification=False,
is_indented=False, requires_leading_newline=False):
is_indented=False, requires_leading_newline=False,
requires_leading_space=None,
requires_trailing_space=None):
"""
If a classification is passed, it specifies the color identifiers in
that subtree should inherit for syntax coloring. Must be a member of
Expand All @@ -33,6 +35,8 @@ def __init__(self, name, kind, name_for_diagnostics=None, description=None, is_o
self.force_classification = force_classification
self.is_indented = is_indented
self.requires_leading_newline = requires_leading_newline
self.requires_leading_space = requires_leading_space
self.requires_trailing_space = requires_trailing_space

# If the child ends with "token" in the kind, it's considered
# a token node. Grab the existing reference to that token from the
Expand Down
5 changes: 3 additions & 2 deletions gyb_syntax_support/ExprNodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
token_choices=[
'PostfixQuestionMarkToken',
'ExclamationMarkToken',
]),
],
requires_trailing_space=True),
Child('Expression', kind='Expr'),
]),

Expand Down Expand Up @@ -222,7 +223,7 @@
Child('LeftSquare', kind='LeftSquareBracketToken'),
Child('Content', kind='Syntax',
node_choices=[
Child('Colon', kind='ColonToken'),
Child('Colon', kind='ColonToken', requires_trailing_space=False),
Child('Elements', kind='DictionaryElementList'),
], is_indented=True),
Child('RightSquare', kind='RightSquareBracketToken'),
Expand Down