Skip to content

Trailing comma implementation #2515

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
Apr 22, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public enum ExperimentalFeature: String, CaseIterable {
case nonescapableTypes
case transferringArgsAndResults
case borrowingSwitch
case trailingComma

/// The name of the feature, which is used in the doc comment.
public var featureName: String {
Expand All @@ -35,6 +36,8 @@ public enum ExperimentalFeature: String, CaseIterable {
return "TransferringArgsAndResults"
case .borrowingSwitch:
return "borrowing pattern matching"
case .trailingComma:
return "trailing comma"
}
}

Expand Down
8 changes: 6 additions & 2 deletions Sources/SwiftParser/Attributes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ extension Parser {
}
case nil:
return parseAttribute(argumentMode: .customAttribute) { parser in
let arguments = parser.parseArgumentListElements(pattern: .none)
let arguments = parser.parseArgumentListElements(pattern: .none, allowTrailingComma: false)
return .argumentList(RawLabeledExprListSyntax(elements: arguments, arena: parser.arena))
}
}
Expand Down Expand Up @@ -420,7 +420,11 @@ extension Parser {
trailingComma: roleTrailingComma,
arena: self.arena
)
let additionalArgs = self.parseArgumentListElements(pattern: .none, flavor: .attributeArguments)
let additionalArgs = self.parseArgumentListElements(
pattern: .none,
flavor: .attributeArguments,
allowTrailingComma: false
)
return [roleElement] + additionalArgs
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftParser/Declarations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2025,7 +2025,7 @@ extension Parser {
let unexpectedBeforeRightParen: RawUnexpectedNodesSyntax?
let rightParen: RawTokenSyntax?
if leftParen != nil {
args = parseArgumentListElements(pattern: .none)
args = parseArgumentListElements(pattern: .none, allowTrailingComma: false)
(unexpectedBeforeRightParen, rightParen) = self.expect(.rightParen)
} else {
args = []
Expand Down
32 changes: 21 additions & 11 deletions Sources/SwiftParser/Expressions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -742,7 +742,11 @@ extension Parser {

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

// If we can parse trailing closures, do so.
Expand Down Expand Up @@ -778,7 +782,7 @@ extension Parser {
if self.at(.rightSquare) {
args = []
} else {
args = self.parseArgumentListElements(pattern: pattern)
args = self.parseArgumentListElements(pattern: pattern, allowTrailingComma: false)
}
let (unexpectedBeforeRSquare, rsquare) = self.expect(.rightSquare)

Expand Down Expand Up @@ -1011,7 +1015,7 @@ extension Parser {
if self.at(.rightSquare) {
args = []
} else {
args = self.parseArgumentListElements(pattern: pattern)
args = self.parseArgumentListElements(pattern: pattern, allowTrailingComma: false)
}
let (unexpectedBeforeRSquare, rsquare) = self.expect(.rightSquare)

Expand Down Expand Up @@ -1306,7 +1310,7 @@ extension Parser {
let unexpectedBeforeRightParen: RawUnexpectedNodesSyntax?
let rightParen: RawTokenSyntax?
if leftParen != nil {
args = parseArgumentListElements(pattern: pattern)
args = parseArgumentListElements(pattern: pattern, allowTrailingComma: false)
(unexpectedBeforeRightParen, rightParen) = self.expect(.rightParen)
} else {
args = []
Expand Down Expand Up @@ -1420,7 +1424,10 @@ extension Parser {
/// Parse a tuple expression.
mutating func parseTupleExpression(pattern: PatternContext) -> RawTupleExprSyntax {
let (unexpectedBeforeLParen, lparen) = self.expect(.leftParen)
let elements = self.parseArgumentListElements(pattern: pattern)
let elements = self.parseArgumentListElements(
pattern: pattern,
allowTrailingComma: experimentalFeatures.contains(.trailingComma)
)
let (unexpectedBeforeRParen, rparen) = self.expect(.rightParen)
return RawTupleExprSyntax(
unexpectedBeforeLParen,
Expand Down Expand Up @@ -1862,7 +1869,8 @@ extension Parser {
/// this will be a dedicated argument list type.
mutating func parseArgumentListElements(
pattern: PatternContext,
flavor: ExprFlavor = .basic
flavor: ExprFlavor = .basic,
allowTrailingComma: Bool
) -> [RawLabeledExprSyntax] {
if let remainingTokens = remainingTokensIfMaximumNestingLevelReached() {
return [
Expand Down Expand Up @@ -1921,9 +1929,13 @@ extension Parser {
arena: self.arena
)
)
} while keepGoing != nil && self.hasProgressed(&loopProgress)
} while keepGoing != nil && !atArgumentListTerminator(allowTrailingComma) && self.hasProgressed(&loopProgress)
return result
}

mutating func atArgumentListTerminator(_ allowTrailingComma: Bool) -> Bool {
return allowTrailingComma && self.at(.rightParen)
}
}

extension Parser {
Expand Down Expand Up @@ -2101,9 +2113,7 @@ extension Parser {

extension Parser {
/// Parse an if statement/expression.
mutating func parseIfExpression(
ifHandle: RecoveryConsumptionHandle
) -> RawIfExprSyntax {
mutating func parseIfExpression(ifHandle: RecoveryConsumptionHandle) -> RawIfExprSyntax {
let (unexpectedBeforeIfKeyword, ifKeyword) = self.eat(ifHandle)

let conditions: RawConditionElementListSyntax
Expand All @@ -2120,7 +2130,7 @@ extension Parser {
arena: self.arena
)
} else {
conditions = self.parseConditionList()
conditions = self.parseConditionList(isGuardStatement: false)
}

let body = self.parseCodeBlock(introducer: ifKeyword)
Expand Down
75 changes: 70 additions & 5 deletions Sources/SwiftParser/Statements.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ extension Parser {
/// Parse a guard statement.
mutating func parseGuardStatement(guardHandle: RecoveryConsumptionHandle) -> RawGuardStmtSyntax {
let (unexpectedBeforeGuardKeyword, guardKeyword) = self.eat(guardHandle)
let conditions = self.parseConditionList()
let conditions = self.parseConditionList(isGuardStatement: true)
let (unexpectedBeforeElseKeyword, elseKeyword) = self.expect(.keyword(.else))
let body = self.parseCodeBlock(introducer: guardKeyword)
return RawGuardStmtSyntax(
Expand All @@ -154,7 +154,7 @@ extension Parser {

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

return RawConditionElementListSyntax(elements: elements, arena: self.arena)
}

mutating func atConditionListTerminator(isGuardStatement: Bool) -> Bool {
guard experimentalFeatures.contains(.trailingComma) else {
return false
}
// Condition terminator is `else` for `guard` statements.
if isGuardStatement, self.at(.keyword(.else)) {
return true
}
// Condition terminator is start of statement body for `if` or `while` statements.
// Missing `else` is a common mistake for `guard` statements so we fall back to lookahead for a body.
return self.at(.leftBrace) && withLookahead({ $0.atStartOfConditionalStatementBody() })
}

/// Parse a condition element.
///
/// `lastBindingKind` will be used to get a correct fall back, when there is missing `var` or `let` in a `if` statement etc.
Expand Down Expand Up @@ -498,7 +512,6 @@ extension Parser {
mutating func parseWhileStatement(whileHandle: RecoveryConsumptionHandle) -> RawWhileStmtSyntax {
let (unexpectedBeforeWhileKeyword, whileKeyword) = self.eat(whileHandle)
let conditions: RawConditionElementListSyntax

if self.at(.leftBrace) {
conditions = RawConditionElementListSyntax(
elements: [
Expand All @@ -511,9 +524,11 @@ extension Parser {
arena: self.arena
)
} else {
conditions = self.parseConditionList()
conditions = self.parseConditionList(isGuardStatement: false)
}

let body = self.parseCodeBlock(introducer: whileKeyword)

return RawWhileStmtSyntax(
unexpectedBeforeWhileKeyword,
whileKeyword: whileKeyword,
Expand Down Expand Up @@ -1053,4 +1068,54 @@ extension Parser.Lookahead {
} while lookahead.at(.poundIf, .poundElseif, .poundElse) && lookahead.hasProgressed(&loopProgress)
return lookahead.atStartOfSwitchCase()
}

/// Returns `true` if the current token represents the start of an `if` or `while` statement body.
mutating func atStartOfConditionalStatementBody() -> Bool {
guard at(.leftBrace) else {
// Statement bodies always start with a '{'. If there is no '{', we can't be at the statement body.
return false
}
skipSingle()
if self.at(.endOfFile) {
// There's nothing else in the source file that could be the statement body, so this must be it.
return true
}
if self.at(.semicolon) {
// We can't have a semicolon between the condition and the statement body, so this must be the statement body.
return true
}
if self.at(.keyword(.else)) {
// 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`.
return true
}
if self.at(.rightBrace, .rightParen) {
// A right brace or parenthesis cannot start a statement body, nor can the condition list continue afterwards. So, this must be the statement body.
// This covers cases like `if true, { if true, { } }` or `( if true, { print(0) } )`. While the latter is not valid code, it improves diagnostics.
return true
}
if self.atStartOfLine {
// If the current token is at the start of a line, it is most likely a statement body. The only exceptions are:
if self.at(.comma) {
// If newline begins with ',' it must be a condition trailing comma, so this can't be the statement body, e.g.
// if true, { true }
// , true { print("body") }
return false
}
if self.at(.binaryOperator) {
// 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.
// if true, { true }
// != nil
// {
// print("body")
// }
return false
}
// Excluded the above exceptions, this must be the statement body.
return true
} else {
// 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.
return false
}
}

}
2 changes: 1 addition & 1 deletion Sources/SwiftParser/StringLiterals.swift
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,7 @@ extension Parser {
)
let leftParen = self.expectWithoutRecoveryOrLeadingTrivia(.leftParen)
let expressions = RawLabeledExprListSyntax(
elements: self.parseArgumentListElements(pattern: .none),
elements: self.parseArgumentListElements(pattern: .none, allowTrailingComma: false),
arena: self.arena
)

Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftParser/Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1054,7 +1054,7 @@ extension Parser {
}
case nil: // Custom attribute
return parseAttribute(argumentMode: .customAttribute) { parser in
let arguments = parser.parseArgumentListElements(pattern: .none)
let arguments = parser.parseArgumentListElements(pattern: .none, allowTrailingComma: false)
return .argumentList(RawLabeledExprListSyntax(elements: arguments, arena: parser.arena))
}

Expand Down
3 changes: 3 additions & 0 deletions Sources/SwiftParser/generated/ExperimentalFeatures.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,7 @@ extension Parser.ExperimentalFeatures {

/// Whether to enable the parsing of borrowing pattern matching.
public static let borrowingSwitch = Self (rawValue: 1 << 5)

/// Whether to enable the parsing of trailing comma.
public static let trailingComma = Self (rawValue: 1 << 6)
}
Loading