Skip to content

Commit a85aa07

Browse files
authored
Merge pull request #2515 from mateusrodriguesxyz/trailing-comma
Trailing comma implementation
2 parents 4824d4d + 376ddbb commit a85aa07

File tree

10 files changed

+340
-42
lines changed

10 files changed

+340
-42
lines changed

CodeGeneration/Sources/SyntaxSupport/ExperimentalFeatures.swift

+3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public enum ExperimentalFeature: String, CaseIterable {
1919
case nonescapableTypes
2020
case transferringArgsAndResults
2121
case borrowingSwitch
22+
case trailingComma
2223

2324
/// The name of the feature, which is used in the doc comment.
2425
public var featureName: String {
@@ -35,6 +36,8 @@ public enum ExperimentalFeature: String, CaseIterable {
3536
return "TransferringArgsAndResults"
3637
case .borrowingSwitch:
3738
return "borrowing pattern matching"
39+
case .trailingComma:
40+
return "trailing comma"
3841
}
3942
}
4043

Sources/SwiftParser/Attributes.swift

+6-2
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ extension Parser {
372372
}
373373
case nil:
374374
return parseAttribute(argumentMode: .customAttribute) { parser in
375-
let arguments = parser.parseArgumentListElements(pattern: .none)
375+
let arguments = parser.parseArgumentListElements(pattern: .none, allowTrailingComma: false)
376376
return .argumentList(RawLabeledExprListSyntax(elements: arguments, arena: parser.arena))
377377
}
378378
}
@@ -420,7 +420,11 @@ extension Parser {
420420
trailingComma: roleTrailingComma,
421421
arena: self.arena
422422
)
423-
let additionalArgs = self.parseArgumentListElements(pattern: .none, flavor: .attributeArguments)
423+
let additionalArgs = self.parseArgumentListElements(
424+
pattern: .none,
425+
flavor: .attributeArguments,
426+
allowTrailingComma: false
427+
)
424428
return [roleElement] + additionalArgs
425429
}
426430
}

Sources/SwiftParser/Declarations.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -2025,7 +2025,7 @@ extension Parser {
20252025
let unexpectedBeforeRightParen: RawUnexpectedNodesSyntax?
20262026
let rightParen: RawTokenSyntax?
20272027
if leftParen != nil {
2028-
args = parseArgumentListElements(pattern: .none)
2028+
args = parseArgumentListElements(pattern: .none, allowTrailingComma: false)
20292029
(unexpectedBeforeRightParen, rightParen) = self.expect(.rightParen)
20302030
} else {
20312031
args = []

Sources/SwiftParser/Expressions.swift

+21-11
Original file line numberDiff line numberDiff line change
@@ -742,7 +742,11 @@ extension Parser {
742742

743743
// If there is an expr-call-suffix, parse it and form a call.
744744
if let lparen = self.consume(if: TokenSpec(.leftParen, allowAtStartOfLine: false)) {
745-
let args = self.parseArgumentListElements(pattern: pattern, flavor: flavor.callArgumentFlavor)
745+
let args = self.parseArgumentListElements(
746+
pattern: pattern,
747+
flavor: flavor.callArgumentFlavor,
748+
allowTrailingComma: experimentalFeatures.contains(.trailingComma)
749+
)
746750
let (unexpectedBeforeRParen, rparen) = self.expect(.rightParen)
747751

748752
// If we can parse trailing closures, do so.
@@ -778,7 +782,7 @@ extension Parser {
778782
if self.at(.rightSquare) {
779783
args = []
780784
} else {
781-
args = self.parseArgumentListElements(pattern: pattern)
785+
args = self.parseArgumentListElements(pattern: pattern, allowTrailingComma: false)
782786
}
783787
let (unexpectedBeforeRSquare, rsquare) = self.expect(.rightSquare)
784788

@@ -1011,7 +1015,7 @@ extension Parser {
10111015
if self.at(.rightSquare) {
10121016
args = []
10131017
} else {
1014-
args = self.parseArgumentListElements(pattern: pattern)
1018+
args = self.parseArgumentListElements(pattern: pattern, allowTrailingComma: false)
10151019
}
10161020
let (unexpectedBeforeRSquare, rsquare) = self.expect(.rightSquare)
10171021

@@ -1306,7 +1310,7 @@ extension Parser {
13061310
let unexpectedBeforeRightParen: RawUnexpectedNodesSyntax?
13071311
let rightParen: RawTokenSyntax?
13081312
if leftParen != nil {
1309-
args = parseArgumentListElements(pattern: pattern)
1313+
args = parseArgumentListElements(pattern: pattern, allowTrailingComma: false)
13101314
(unexpectedBeforeRightParen, rightParen) = self.expect(.rightParen)
13111315
} else {
13121316
args = []
@@ -1420,7 +1424,10 @@ extension Parser {
14201424
/// Parse a tuple expression.
14211425
mutating func parseTupleExpression(pattern: PatternContext) -> RawTupleExprSyntax {
14221426
let (unexpectedBeforeLParen, lparen) = self.expect(.leftParen)
1423-
let elements = self.parseArgumentListElements(pattern: pattern)
1427+
let elements = self.parseArgumentListElements(
1428+
pattern: pattern,
1429+
allowTrailingComma: experimentalFeatures.contains(.trailingComma)
1430+
)
14241431
let (unexpectedBeforeRParen, rparen) = self.expect(.rightParen)
14251432
return RawTupleExprSyntax(
14261433
unexpectedBeforeLParen,
@@ -1862,7 +1869,8 @@ extension Parser {
18621869
/// this will be a dedicated argument list type.
18631870
mutating func parseArgumentListElements(
18641871
pattern: PatternContext,
1865-
flavor: ExprFlavor = .basic
1872+
flavor: ExprFlavor = .basic,
1873+
allowTrailingComma: Bool
18661874
) -> [RawLabeledExprSyntax] {
18671875
if let remainingTokens = remainingTokensIfMaximumNestingLevelReached() {
18681876
return [
@@ -1921,9 +1929,13 @@ extension Parser {
19211929
arena: self.arena
19221930
)
19231931
)
1924-
} while keepGoing != nil && self.hasProgressed(&loopProgress)
1932+
} while keepGoing != nil && !atArgumentListTerminator(allowTrailingComma) && self.hasProgressed(&loopProgress)
19251933
return result
19261934
}
1935+
1936+
mutating func atArgumentListTerminator(_ allowTrailingComma: Bool) -> Bool {
1937+
return allowTrailingComma && self.at(.rightParen)
1938+
}
19271939
}
19281940

19291941
extension Parser {
@@ -2101,9 +2113,7 @@ extension Parser {
21012113

21022114
extension Parser {
21032115
/// Parse an if statement/expression.
2104-
mutating func parseIfExpression(
2105-
ifHandle: RecoveryConsumptionHandle
2106-
) -> RawIfExprSyntax {
2116+
mutating func parseIfExpression(ifHandle: RecoveryConsumptionHandle) -> RawIfExprSyntax {
21072117
let (unexpectedBeforeIfKeyword, ifKeyword) = self.eat(ifHandle)
21082118

21092119
let conditions: RawConditionElementListSyntax
@@ -2120,7 +2130,7 @@ extension Parser {
21202130
arena: self.arena
21212131
)
21222132
} else {
2123-
conditions = self.parseConditionList()
2133+
conditions = self.parseConditionList(isGuardStatement: false)
21242134
}
21252135

21262136
let body = self.parseCodeBlock(introducer: ifKeyword)

Sources/SwiftParser/Statements.swift

+70-5
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ extension Parser {
137137
/// Parse a guard statement.
138138
mutating func parseGuardStatement(guardHandle: RecoveryConsumptionHandle) -> RawGuardStmtSyntax {
139139
let (unexpectedBeforeGuardKeyword, guardKeyword) = self.eat(guardHandle)
140-
let conditions = self.parseConditionList()
140+
let conditions = self.parseConditionList(isGuardStatement: true)
141141
let (unexpectedBeforeElseKeyword, elseKeyword) = self.expect(.keyword(.else))
142142
let body = self.parseCodeBlock(introducer: guardKeyword)
143143
return RawGuardStmtSyntax(
@@ -154,7 +154,7 @@ extension Parser {
154154

155155
extension Parser {
156156
/// Parse a list of condition elements.
157-
mutating func parseConditionList() -> RawConditionElementListSyntax {
157+
mutating func parseConditionList(isGuardStatement: Bool) -> RawConditionElementListSyntax {
158158
// We have a simple comma separated list of clauses, but also need to handle
159159
// a variety of common errors situations (including migrating from Swift 2
160160
// syntax).
@@ -183,11 +183,25 @@ extension Parser {
183183
arena: self.arena
184184
)
185185
)
186-
} while keepGoing != nil && self.hasProgressed(&loopProgress)
186+
} while keepGoing != nil && !atConditionListTerminator(isGuardStatement: isGuardStatement)
187+
&& self.hasProgressed(&loopProgress)
187188

188189
return RawConditionElementListSyntax(elements: elements, arena: self.arena)
189190
}
190191

192+
mutating func atConditionListTerminator(isGuardStatement: Bool) -> Bool {
193+
guard experimentalFeatures.contains(.trailingComma) else {
194+
return false
195+
}
196+
// Condition terminator is `else` for `guard` statements.
197+
if isGuardStatement, self.at(.keyword(.else)) {
198+
return true
199+
}
200+
// Condition terminator is start of statement body for `if` or `while` statements.
201+
// Missing `else` is a common mistake for `guard` statements so we fall back to lookahead for a body.
202+
return self.at(.leftBrace) && withLookahead({ $0.atStartOfConditionalStatementBody() })
203+
}
204+
191205
/// Parse a condition element.
192206
///
193207
/// `lastBindingKind` will be used to get a correct fall back, when there is missing `var` or `let` in a `if` statement etc.
@@ -498,7 +512,6 @@ extension Parser {
498512
mutating func parseWhileStatement(whileHandle: RecoveryConsumptionHandle) -> RawWhileStmtSyntax {
499513
let (unexpectedBeforeWhileKeyword, whileKeyword) = self.eat(whileHandle)
500514
let conditions: RawConditionElementListSyntax
501-
502515
if self.at(.leftBrace) {
503516
conditions = RawConditionElementListSyntax(
504517
elements: [
@@ -511,9 +524,11 @@ extension Parser {
511524
arena: self.arena
512525
)
513526
} else {
514-
conditions = self.parseConditionList()
527+
conditions = self.parseConditionList(isGuardStatement: false)
515528
}
529+
516530
let body = self.parseCodeBlock(introducer: whileKeyword)
531+
517532
return RawWhileStmtSyntax(
518533
unexpectedBeforeWhileKeyword,
519534
whileKeyword: whileKeyword,
@@ -1053,4 +1068,54 @@ extension Parser.Lookahead {
10531068
} while lookahead.at(.poundIf, .poundElseif, .poundElse) && lookahead.hasProgressed(&loopProgress)
10541069
return lookahead.atStartOfSwitchCase()
10551070
}
1071+
1072+
/// Returns `true` if the current token represents the start of an `if` or `while` statement body.
1073+
mutating func atStartOfConditionalStatementBody() -> Bool {
1074+
guard at(.leftBrace) else {
1075+
// Statement bodies always start with a '{'. If there is no '{', we can't be at the statement body.
1076+
return false
1077+
}
1078+
skipSingle()
1079+
if self.at(.endOfFile) {
1080+
// There's nothing else in the source file that could be the statement body, so this must be it.
1081+
return true
1082+
}
1083+
if self.at(.semicolon) {
1084+
// We can't have a semicolon between the condition and the statement body, so this must be the statement body.
1085+
return true
1086+
}
1087+
if self.at(.keyword(.else)) {
1088+
// If the current token is an `else` keyword, this must be the statement body of an `if` statement since conditions can't be followed by `else`.
1089+
return true
1090+
}
1091+
if self.at(.rightBrace, .rightParen) {
1092+
// A right brace or parenthesis cannot start a statement body, nor can the condition list continue afterwards. So, this must be the statement body.
1093+
// This covers cases like `if true, { if true, { } }` or `( if true, { print(0) } )`. While the latter is not valid code, it improves diagnostics.
1094+
return true
1095+
}
1096+
if self.atStartOfLine {
1097+
// If the current token is at the start of a line, it is most likely a statement body. The only exceptions are:
1098+
if self.at(.comma) {
1099+
// If newline begins with ',' it must be a condition trailing comma, so this can't be the statement body, e.g.
1100+
// if true, { true }
1101+
// , true { print("body") }
1102+
return false
1103+
}
1104+
if self.at(.binaryOperator) {
1105+
// If current token is a binary operator this can't be the statement body since an `if` expression can't be the left-hand side of an operator, e.g.
1106+
// if true, { true }
1107+
// != nil
1108+
// {
1109+
// print("body")
1110+
// }
1111+
return false
1112+
}
1113+
// Excluded the above exceptions, this must be the statement body.
1114+
return true
1115+
} else {
1116+
// If the current token isn't at the start of a line and isn't `EOF`, `;`, `else`, `)` or `}` this can't be the statement body.
1117+
return false
1118+
}
1119+
}
1120+
10561121
}

Sources/SwiftParser/StringLiterals.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,7 @@ extension Parser {
547547
)
548548
let leftParen = self.expectWithoutRecoveryOrLeadingTrivia(.leftParen)
549549
let expressions = RawLabeledExprListSyntax(
550-
elements: self.parseArgumentListElements(pattern: .none),
550+
elements: self.parseArgumentListElements(pattern: .none, allowTrailingComma: false),
551551
arena: self.arena
552552
)
553553

Sources/SwiftParser/Types.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -1054,7 +1054,7 @@ extension Parser {
10541054
}
10551055
case nil: // Custom attribute
10561056
return parseAttribute(argumentMode: .customAttribute) { parser in
1057-
let arguments = parser.parseArgumentListElements(pattern: .none)
1057+
let arguments = parser.parseArgumentListElements(pattern: .none, allowTrailingComma: false)
10581058
return .argumentList(RawLabeledExprListSyntax(elements: arguments, arena: parser.arena))
10591059
}
10601060

Sources/SwiftParser/generated/ExperimentalFeatures.swift

+3
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,7 @@ extension Parser.ExperimentalFeatures {
4141

4242
/// Whether to enable the parsing of borrowing pattern matching.
4343
public static let borrowingSwitch = Self (rawValue: 1 << 5)
44+
45+
/// Whether to enable the parsing of trailing comma.
46+
public static let trailingComma = Self (rawValue: 1 << 6)
4447
}

0 commit comments

Comments
 (0)