Skip to content

Commit 8781308

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 c47db5b commit 8781308

File tree

13 files changed

+150
-52
lines changed

13 files changed

+150
-52
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: 87 additions & 19 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
"""
@@ -126,14 +160,34 @@ let basicFormatFile = SourceFileSyntax {
126160
}
127161
}
128162

163+
try FunctionDeclSyntax(
164+
"""
165+
/// If this returns a value that is not `nil`, it overrides the default
166+
/// leading space behavior of a token.
167+
open func requiresLeadingSpace(_ keyPath: AnyKeyPath) -> Bool?
168+
"""
169+
) {
170+
try SwitchStmtSyntax("switch keyPath") {
171+
for node in SYNTAX_NODES where !node.isBase {
172+
for child in node.children {
173+
if let requiresLeadingSpace = child.requiresLeadingSpace {
174+
SwitchCaseSyntax("case \\\(raw: node.type.syntaxBaseName).\(raw: child.swiftName):") {
175+
StmtSyntax("return \(literal: requiresLeadingSpace)")
176+
}
177+
}
178+
}
179+
}
180+
SwitchCaseSyntax("default:") {
181+
StmtSyntax("return nil")
182+
}
183+
}
184+
}
185+
129186
try FunctionDeclSyntax("open func requiresLeadingSpace(_ token: TokenSyntax) -> Bool") {
130187
StmtSyntax(
131188
"""
132-
switch (token.previousToken(viewMode: .sourceAccurate)?.tokenKind, token.tokenKind) {
133-
case (.leftParen, .binaryOperator): // Ensures there is no space in @available(*, deprecated)
134-
return false
135-
default:
136-
break
189+
if let keyPath = getKeyPath(token), let requiresLeadingSpace = requiresLeadingSpace(keyPath) {
190+
return requiresLeadingSpace
137191
}
138192
"""
139193
)
@@ -157,17 +211,34 @@ let basicFormatFile = SourceFileSyntax {
157211
}
158212
}
159213

214+
try FunctionDeclSyntax(
215+
"""
216+
/// If this returns a value that is not `nil`, it overrides the default
217+
/// trailing space behavior of a token.
218+
open func requiresTrailingSpace(_ keyPath: AnyKeyPath) -> Bool?
219+
"""
220+
) {
221+
try SwitchStmtSyntax("switch keyPath") {
222+
for node in SYNTAX_NODES where !node.isBase {
223+
for child in node.children {
224+
if let requiresTrailingSpace = child.requiresTrailingSpace {
225+
SwitchCaseSyntax("case \\\(raw: node.type.syntaxBaseName).\(raw: child.swiftName):") {
226+
StmtSyntax("return \(literal: requiresTrailingSpace)")
227+
}
228+
}
229+
}
230+
}
231+
SwitchCaseSyntax("default:") {
232+
StmtSyntax("return nil")
233+
}
234+
}
235+
}
236+
160237
try FunctionDeclSyntax("open func requiresTrailingSpace(_ token: TokenSyntax) -> Bool") {
161238
StmtSyntax(
162239
"""
163-
switch (token.tokenKind, token.parent?.kind) {
164-
case (.colon, .dictionaryExpr): // Ensures there is not space in `[:]`
165-
return false
166-
case (.exclamationMark, .tryExpr), // Ensures there is a space in `try! foo`
167-
(.postfixQuestionMark, .tryExpr): // Ensures there is a space in `try? foo`
168-
return true
169-
default:
170-
break
240+
if let keyPath = getKeyPath(token), let requiresTrailingSpace = requiresTrailingSpace(keyPath) {
241+
return requiresTrailingSpace
171242
}
172243
"""
173244
)
@@ -179,14 +250,11 @@ let basicFormatFile = SourceFileSyntax {
179250
(.keyword(.as), .postfixQuestionMark), // Ensures there is not space in `as?`
180251
(.exclamationMark, .leftParen), // Ensures there is not space in `myOptionalClosure!()`
181252
(.exclamationMark, .period), // Ensures there is not space in `myOptionalBar!.foo()`
182-
(.keyword(.`init`), .leftParen), // Ensures there is not space in `init()`
183-
(.keyword(.`init`), .postfixQuestionMark), // Ensures there is not space in `init?`
184-
(.postfixQuestionMark, .leftParen), // Ensures there is not space in `init?()`
253+
(.postfixQuestionMark, .leftParen), // Ensures there is not space in `init?()` or `myOptionalClosure?()`s
185254
(.postfixQuestionMark, .rightAngle), // Ensures there is not space in `ContiguousArray<RawSyntax?>`
186255
(.postfixQuestionMark, .rightParen), // Ensures there is not space in `myOptionalClosure?()`
187256
(.keyword(.try), .exclamationMark), // Ensures there is not space in `try!`
188-
(.keyword(.try), .postfixQuestionMark), // Ensures there is not space in `try?`
189-
(.binaryOperator, .comma): // Ensures there is no space in `@available(*, deprecated)`
257+
(.keyword(.try), .postfixQuestionMark): // Ensures there is not space in `try?`:
190258
return false
191259
default:
192260
break
@@ -215,7 +283,7 @@ let basicFormatFile = SourceFileSyntax {
215283

216284
DeclSyntax(
217285
"""
218-
private func getKeyPath(_ node: Syntax) -> AnyKeyPath? {
286+
private func getKeyPath<T: SyntaxProtocol>(_ node: T) -> AnyKeyPath? {
219287
guard let parent = node.parent else {
220288
return nil
221289
}

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)