Skip to content

Commit f0eb715

Browse files
committed
Allow node children to override whether a token needs a leading/trailing space
The motivating example was that we should not add a space after `for` in `@_dynamicReplacement(for:…)`. IMO, this also cleans up a few rules in BasicFormat.
1 parent 8984572 commit f0eb715

File tree

13 files changed

+149
-55
lines changed

13 files changed

+149
-55
lines changed

CodeGeneration/Sources/SyntaxSupport/Child.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ public enum ChildKind {
3232
/// The child is a collection of `kind`.
3333
case collection(kind: String, collectionElementName: String)
3434
/// The child is a token that matches one of the given `choices`.
35-
case token(choices: [TokenChoice])
35+
/// If `requiresLeadingSpace` or `requiresTrailingSpace` is not `nil`, it
36+
/// overrides the default leading/trailing space behavior of the token.
37+
case token(choices: [TokenChoice], requiresLeadingSpace: Bool? = nil, requiresTrailingSpace: Bool? = nil)
3638

3739
public var isNodeChoices: Bool {
3840
if case .nodeChoices = self {
@@ -67,7 +69,7 @@ public class Child {
6769
return "syntax"
6870
case .collection(kind: let kind, collectionElementName: _):
6971
return kind
70-
case .token(choices: let choices):
72+
case .token(choices: let choices, requiresLeadingSpace: _, requiresTrailingSpace: _):
7173
if choices.count == 1 {
7274
switch choices.first! {
7375
case .keyword: return "KeywordToken"

CodeGeneration/Sources/SyntaxSupport/KeywordSpec.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ public let KEYWORDS: [KeywordSpec] = [
139139
KeywordSpec("in", isLexerClassified: true, requiresLeadingSpace: true, requiresTrailingSpace: true),
140140
KeywordSpec("indirect"),
141141
KeywordSpec("infix"),
142-
KeywordSpec("init", isLexerClassified: true, requiresTrailingSpace: true),
142+
KeywordSpec("init", isLexerClassified: true),
143143
KeywordSpec("inline"),
144144
KeywordSpec("inout", isLexerClassified: true, requiresTrailingSpace: true),
145145
KeywordSpec("internal", isLexerClassified: true, requiresTrailingSpace: true),

CodeGeneration/Sources/SyntaxSupport/gyb_generated/AttributeNodes.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,7 @@ public let ATTRIBUTE_NODES: [Node] = [
476476
kind: "Syntax",
477477
children: [
478478
Child(name: "ForLabel",
479-
kind: .token(choices: [.keyword(text: "for")])),
479+
kind: .token(choices: [.keyword(text: "for")], requiresTrailingSpace: false)),
480480
Child(name: "Colon",
481481
kind: .token(choices: [.token(tokenKind: "ColonToken")])),
482482
Child(name: "Declname",

CodeGeneration/Sources/SyntaxSupport/gyb_generated/AvailabilityNodes.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public let AVAILABILITY_NODES: [Node] = [
2626
Child(name: "Entry",
2727
kind: .nodeChoices(choices: [
2828
Child(name: "Token",
29-
kind: .token(choices: [.token(tokenKind: "BinaryOperatorToken"), .token(tokenKind: "IdentifierToken")])),
29+
kind: .token(choices: [.token(tokenKind: "BinaryOperatorToken"), .token(tokenKind: "IdentifierToken")], requiresLeadingSpace: false, requiresTrailingSpace: false)),
3030
Child(name: "AvailabilityVersionRestriction",
3131
kind: .node(kind: "AvailabilityVersionRestriction")),
3232
Child(name: "AvailabilityLabeledArgument",

CodeGeneration/Sources/SyntaxSupport/gyb_generated/ExprNodes.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public let EXPR_NODES: [Node] = [
5252
Child(name: "TryKeyword",
5353
kind: .token(choices: [.keyword(text: "try")])),
5454
Child(name: "QuestionOrExclamationMark",
55-
kind: .token(choices: [.token(tokenKind: "PostfixQuestionMarkToken"), .token(tokenKind: "ExclamationMarkToken")]),
55+
kind: .token(choices: [.token(tokenKind: "PostfixQuestionMarkToken"), .token(tokenKind: "ExclamationMarkToken")], requiresTrailingSpace: true),
5656
isOptional: true),
5757
Child(name: "Expression",
5858
kind: .node(kind: "Expr"))
@@ -284,7 +284,7 @@ public let EXPR_NODES: [Node] = [
284284
Child(name: "Content",
285285
kind: .nodeChoices(choices: [
286286
Child(name: "Colon",
287-
kind: .token(choices: [.token(tokenKind: "ColonToken")])),
287+
kind: .token(choices: [.token(tokenKind: "ColonToken")], requiresTrailingSpace: false)),
288288
Child(name: "Elements",
289289
kind: .node(kind: "DictionaryElementList"))
290290
]),

CodeGeneration/Sources/SyntaxSupport/gyb_helpers/utils.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,13 @@ def make_swift_child(child, spaces):
5959
mapped_choices = [f'.token(tokenKind: "{choice.name}Token")' for (choice, text) in child.token_choices if text is None]
6060
mapped_choices += [f'.keyword(text: "{text}")' for (choice, text) in child.token_choices if text is not None]
6161
joined_choices = ', '.join(mapped_choices)
62-
kind = f'.token(choices: [{joined_choices}])'
62+
token_arguments = [f'choices: [{joined_choices}]']
63+
if child.requires_leading_space is not None:
64+
token_arguments += ['requiresLeadingSpace: ' + ('true' if child.requires_leading_space else 'false')]
65+
if child.requires_trailing_space is not None:
66+
token_arguments += ['requiresTrailingSpace: ' + ('true' if child.requires_trailing_space else 'false')]
67+
arguments = ', '.join(token_arguments)
68+
kind = f'.token({arguments})'
6369
elif child.collection_element_name:
6470
kind = f'.collection(kind: "{child.syntax_kind}", collectionElementName: "{child.collection_element_name}")'
6571
elif child.node_choices:

CodeGeneration/Sources/Utils/SyntaxBuildableChild.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public extension Child {
4747

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

CodeGeneration/Sources/generate-swiftsyntax/templates/basicformat/BasicFormatFile.swift

Lines changed: 86 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,40 @@ import SwiftSyntaxBuilder
1515
import SyntaxSupport
1616
import Utils
1717

18+
extension Child {
19+
var requiresLeadingSpace: Bool? {
20+
switch self.kind {
21+
case .token(choices: _, requiresLeadingSpace: let requiresLeadingSpace, requiresTrailingSpace: _):
22+
return requiresLeadingSpace
23+
case .nodeChoices(choices: let choices):
24+
for choice in choices {
25+
if let requiresLeadingSpace = choice.requiresLeadingSpace {
26+
return requiresLeadingSpace
27+
}
28+
}
29+
default:
30+
break
31+
}
32+
return nil
33+
}
34+
35+
var requiresTrailingSpace: Bool? {
36+
switch self.kind {
37+
case .token(choices: _, requiresLeadingSpace: _, requiresTrailingSpace: let requiresTrailingSpace):
38+
return requiresTrailingSpace
39+
case .nodeChoices(choices: let choices):
40+
for choice in choices {
41+
if let requiresTrailingSpace = choice.requiresTrailingSpace {
42+
return requiresTrailingSpace
43+
}
44+
}
45+
default:
46+
break
47+
}
48+
return nil
49+
}
50+
}
51+
1852
let basicFormatFile = SourceFileSyntax {
1953
DeclSyntax("""
2054
\(raw: generateCopyrightHeader(for: "generate-swiftbasicformat"))
@@ -121,16 +155,34 @@ let basicFormatFile = SourceFileSyntax {
121155
}
122156
}
123157

158+
try FunctionDeclSyntax("""
159+
/// If this returns a value that is not `nil`, it overrides the default
160+
/// leading space behavior of a token.
161+
open func requiresLeadingSpace(_ keyPath: AnyKeyPath) -> Bool?
162+
""") {
163+
try SwitchStmtSyntax("switch keyPath") {
164+
for node in SYNTAX_NODES where !node.isBase {
165+
for child in node.children {
166+
if let requiresLeadingSpace = child.requiresLeadingSpace {
167+
SwitchCaseSyntax("case \\\(raw: node.type.syntaxBaseName).\(raw: child.swiftName):") {
168+
StmtSyntax("return \(literal: requiresLeadingSpace)")
169+
}
170+
}
171+
}
172+
}
173+
SwitchCaseSyntax("default:") {
174+
StmtSyntax("return nil")
175+
}
176+
}
177+
}
178+
124179
try FunctionDeclSyntax("open func requiresLeadingSpace(_ token: TokenSyntax) -> Bool") {
125180
StmtSyntax("""
126-
switch (token.previousToken(viewMode: .sourceAccurate)?.tokenKind, token.tokenKind) {
127-
case (.leftParen, .binaryOperator): // Ensures there is no space in @available(*, deprecated)
128-
return false
129-
default:
130-
break
131-
}
132-
""")
133-
181+
if let keyPath = getKeyPath(token), let requiresLeadingSpace = requiresLeadingSpace(keyPath) {
182+
return requiresLeadingSpace
183+
}
184+
""")
185+
134186
try SwitchStmtSyntax("switch token.tokenKind") {
135187
for token in SYNTAX_TOKENS {
136188
if token.requiresLeadingSpace {
@@ -150,16 +202,31 @@ let basicFormatFile = SourceFileSyntax {
150202
}
151203
}
152204

205+
try FunctionDeclSyntax("""
206+
/// If this returns a value that is not `nil`, it overrides the default
207+
/// trailing space behavior of a token.
208+
open func requiresTrailingSpace(_ keyPath: AnyKeyPath) -> Bool?
209+
""") {
210+
try SwitchStmtSyntax("switch keyPath") {
211+
for node in SYNTAX_NODES where !node.isBase {
212+
for child in node.children {
213+
if let requiresTrailingSpace = child.requiresTrailingSpace {
214+
SwitchCaseSyntax("case \\\(raw: node.type.syntaxBaseName).\(raw: child.swiftName):") {
215+
StmtSyntax("return \(literal: requiresTrailingSpace)")
216+
}
217+
}
218+
}
219+
}
220+
SwitchCaseSyntax("default:") {
221+
StmtSyntax("return nil")
222+
}
223+
}
224+
}
225+
153226
try FunctionDeclSyntax("open func requiresTrailingSpace(_ token: TokenSyntax) -> Bool") {
154227
StmtSyntax("""
155-
switch (token.tokenKind, token.parent?.kind) {
156-
case (.colon, .dictionaryExpr): // Ensures there is not space in `[:]`
157-
return false
158-
case (.exclamationMark, .tryExpr), // Ensures there is a space in `try! foo`
159-
(.postfixQuestionMark, .tryExpr): // Ensures there is a space in `try? foo`
160-
return true
161-
default:
162-
break
228+
if let keyPath = getKeyPath(token), let requiresTrailingSpace = requiresTrailingSpace(keyPath) {
229+
return requiresTrailingSpace
163230
}
164231
""")
165232

@@ -169,14 +236,11 @@ let basicFormatFile = SourceFileSyntax {
169236
(.keyword(.as), .postfixQuestionMark), // Ensures there is not space in `as?`
170237
(.exclamationMark, .leftParen), // Ensures there is not space in `myOptionalClosure!()`
171238
(.exclamationMark, .period), // Ensures there is not space in `myOptionalBar!.foo()`
172-
(.keyword(.`init`), .leftParen), // Ensures there is not space in `init()`
173-
(.keyword(.`init`), .postfixQuestionMark), // Ensures there is not space in `init?`
174-
(.postfixQuestionMark, .leftParen), // Ensures there is not space in `init?()`
239+
(.postfixQuestionMark, .leftParen), // Ensures there is not space in `init?()` or `myOptionalClosure?()`s
175240
(.postfixQuestionMark, .rightAngle), // Ensures there is not space in `ContiguousArray<RawSyntax?>`
176241
(.postfixQuestionMark, .rightParen), // Ensures there is not space in `myOptionalClosure?()`
177242
(.keyword(.try), .exclamationMark), // Ensures there is not space in `try!`
178-
(.keyword(.try), .postfixQuestionMark), // Ensures there is not space in `try?`
179-
(.binaryOperator, .comma): // Ensures there is no space in `@available(*, deprecated)`
243+
(.keyword(.try), .postfixQuestionMark): // Ensures there is not space in `try?`:
180244
return false
181245
default:
182246
break
@@ -203,7 +267,7 @@ let basicFormatFile = SourceFileSyntax {
203267
}
204268

205269
DeclSyntax("""
206-
private func getKeyPath(_ node: Syntax) -> AnyKeyPath? {
270+
private func getKeyPath<T: SyntaxProtocol>(_ node: T) -> AnyKeyPath? {
207271
guard let parent = node.parent else {
208272
return nil
209273
}

Sources/SwiftBasicFormat/generated/BasicFormat.swift

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -136,12 +136,20 @@ open class BasicFormat: SyntaxRewriter {
136136
}
137137
}
138138

139-
open func requiresLeadingSpace(_ token: TokenSyntax) -> Bool {
140-
switch (token.previousToken(viewMode: .sourceAccurate)?.tokenKind, token.tokenKind) {
141-
case (.leftParen, .binaryOperator): // Ensures there is no space in @available(*, deprecated)
139+
/// If this returns a value that is not `nil`, it overrides the default
140+
/// leading space behavior of a token.
141+
open func requiresLeadingSpace(_ keyPath: AnyKeyPath) -> Bool? {
142+
switch keyPath {
143+
case \AvailabilityArgumentSyntax.entry:
142144
return false
143145
default:
144-
break
146+
return nil
147+
}
148+
}
149+
150+
open func requiresLeadingSpace(_ token: TokenSyntax) -> Bool {
151+
if let keyPath = getKeyPath(token), let requiresLeadingSpace = requiresLeadingSpace(keyPath) {
152+
return requiresLeadingSpace
145153
}
146154
switch token.tokenKind {
147155
case .leftBrace:
@@ -163,29 +171,37 @@ open class BasicFormat: SyntaxRewriter {
163171
}
164172
}
165173

166-
open func requiresTrailingSpace(_ token: TokenSyntax) -> Bool {
167-
switch (token.tokenKind, token.parent?.kind) {
168-
case (.colon, .dictionaryExpr): // Ensures there is not space in `[:]`
174+
/// If this returns a value that is not `nil`, it overrides the default
175+
/// trailing space behavior of a token.
176+
open func requiresTrailingSpace(_ keyPath: AnyKeyPath) -> Bool? {
177+
switch keyPath {
178+
case \AvailabilityArgumentSyntax.entry:
169179
return false
170-
case (.exclamationMark, .tryExpr), // Ensures there is a space in `try! foo`
171-
(.postfixQuestionMark, .tryExpr): // Ensures there is a space in `try? foo`
180+
case \DictionaryExprSyntax.content:
181+
return false
182+
case \DynamicReplacementArgumentsSyntax.forLabel:
183+
return false
184+
case \TryExprSyntax.questionOrExclamationMark:
172185
return true
173186
default:
174-
break
187+
return nil
188+
}
189+
}
190+
191+
open func requiresTrailingSpace(_ token: TokenSyntax) -> Bool {
192+
if let keyPath = getKeyPath(token), let requiresTrailingSpace = requiresTrailingSpace(keyPath) {
193+
return requiresTrailingSpace
175194
}
176195
switch (token.tokenKind, token.nextToken(viewMode: .sourceAccurate)?.tokenKind) {
177196
case (.keyword(.as), .exclamationMark), // Ensures there is not space in `as!`
178197
(.keyword(.as), .postfixQuestionMark), // Ensures there is not space in `as?`
179198
(.exclamationMark, .leftParen), // Ensures there is not space in `myOptionalClosure!()`
180199
(.exclamationMark, .period), // Ensures there is not space in `myOptionalBar!.foo()`
181-
(.keyword(.`init`), .leftParen), // Ensures there is not space in `init()`
182-
(.keyword(.`init`), .postfixQuestionMark), // Ensures there is not space in `init?`
183-
(.postfixQuestionMark, .leftParen), // Ensures there is not space in `init?()`
200+
(.postfixQuestionMark, .leftParen), // Ensures there is not space in `init?()` or `myOptionalClosure?()`s
184201
(.postfixQuestionMark, .rightAngle), // Ensures there is not space in `ContiguousArray<RawSyntax?>`
185202
(.postfixQuestionMark, .rightParen), // Ensures there is not space in `myOptionalClosure?()`
186203
(.keyword(.try), .exclamationMark), // Ensures there is not space in `try!`
187-
(.keyword(.try), .postfixQuestionMark), // Ensures there is not space in `try?`
188-
(.binaryOperator, .comma): // Ensures there is no space in `@available(*, deprecated)`
204+
(.keyword(.try), .postfixQuestionMark): // Ensures there is not space in `try?`:
189205
return false
190206
default:
191207
break
@@ -257,8 +273,6 @@ open class BasicFormat: SyntaxRewriter {
257273
return true
258274
case .keyword(.`in`):
259275
return true
260-
case .keyword(.`init`):
261-
return true
262276
case .keyword(.`inout`):
263277
return true
264278
case .keyword(.`internal`):
@@ -310,7 +324,7 @@ open class BasicFormat: SyntaxRewriter {
310324
}
311325
}
312326

313-
private func getKeyPath(_ node: Syntax) -> AnyKeyPath? {
327+
private func getKeyPath<T: SyntaxProtocol>(_ node: T) -> AnyKeyPath? {
314328
guard let parent = node.parent else {
315329
return nil
316330
}

gyb_syntax_support/AttributeNodes.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,8 @@
493493
The arguments for the '@_dynamicReplacement' attribute
494494
''',
495495
children=[
496-
Child('ForLabel', kind='KeywordToken', token_choices=['KeywordToken|for']),
496+
Child('ForLabel', kind='KeywordToken', token_choices=['KeywordToken|for'],
497+
requires_trailing_space=False),
497498
Child('Colon', kind='ColonToken'),
498499
Child('Declname', kind='DeclName'),
499500
]),

gyb_syntax_support/AvailabilityNodes.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
description='The actual argument',
2323
node_choices=[
2424
Child('Token', kind='Token',
25-
token_choices=['BinaryOperatorToken', 'IdentifierToken']),
25+
token_choices=['BinaryOperatorToken', 'IdentifierToken'],
26+
requires_leading_space=False,
27+
requires_trailing_space=False),
2628
Child('AvailabilityVersionRestriction',
2729
kind='AvailabilityVersionRestriction'),
2830
Child('AvailabilityLabeledArgument',

gyb_syntax_support/Child.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ def __init__(self, name, kind, name_for_diagnostics=None, description=None, is_o
1313
token_choices=None, text_choices=None, node_choices=None,
1414
collection_element_name=None,
1515
classification=None, force_classification=False,
16-
is_indented=False, requires_leading_newline=False):
16+
is_indented=False, requires_leading_newline=False,
17+
requires_leading_space=None,
18+
requires_trailing_space=None):
1719
"""
1820
If a classification is passed, it specifies the color identifiers in
1921
that subtree should inherit for syntax coloring. Must be a member of
@@ -33,6 +35,8 @@ def __init__(self, name, kind, name_for_diagnostics=None, description=None, is_o
3335
self.force_classification = force_classification
3436
self.is_indented = is_indented
3537
self.requires_leading_newline = requires_leading_newline
38+
self.requires_leading_space = requires_leading_space
39+
self.requires_trailing_space = requires_trailing_space
3640

3741
# If the child ends with "token" in the kind, it's considered
3842
# a token node. Grab the existing reference to that token from the

gyb_syntax_support/ExprNodes.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
token_choices=[
3636
'PostfixQuestionMarkToken',
3737
'ExclamationMarkToken',
38-
]),
38+
],
39+
requires_trailing_space=True),
3940
Child('Expression', kind='Expr'),
4041
]),
4142

@@ -222,7 +223,7 @@
222223
Child('LeftSquare', kind='LeftSquareBracketToken'),
223224
Child('Content', kind='Syntax',
224225
node_choices=[
225-
Child('Colon', kind='ColonToken'),
226+
Child('Colon', kind='ColonToken', requires_trailing_space=False),
226227
Child('Elements', kind='DictionaryElementList'),
227228
], is_indented=True),
228229
Child('RightSquare', kind='RightSquareBracketToken'),

0 commit comments

Comments
 (0)