Skip to content

Add correct diagnostic for missing parentheses #2012

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
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
53 changes: 26 additions & 27 deletions Sources/SwiftParser/Expressions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1746,7 +1746,9 @@ extension Parser {
var effectSpecifiers: RawTypeEffectSpecifiersSyntax?
var returnClause: RawReturnClauseSyntax? = nil
if !self.at(.keyword(.in)) {
if self.at(.leftParen) {
// If the next token is ':', then it looks like the code contained a non-shorthand closure parameter with a type annotation.
// These need to be wrapped in parentheses.
if self.at(.leftParen) || self.peek(isAt: .colon) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this test and it fails because we are peeking here.
When at this place current token is comparisonPredicate.

    assertParse(
      """
      withInvalidOrderings { (comparisonPredicate: @escaping (Int, Int) -> Bool) in
      }
      """
    )

So the peek seems not the correct solutions here.
Maybe we should do something like self.peek(isAt: .colon) && previousToken != .leftParen

What do you think @ahoppen

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ahoppen 😬

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, it’s been a busy week and I missed the comment. Thanks for the ping.

The issue happens because in canPareClosureSignature, we now stop at the arrow. Since we start doing skipSingle at comparisonPredicate. If you make the following change, then we’re calling skipSingle on the opening (, which consumes all the contents until the matching ) before the in, including the ->, which fixes the issue.

- if lookahead.consume(if: .leftParen) != nil {  // Consume the ')'.
+ if lookahead.at(.leftParen) {  // Consume the '('.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh okay nice!
Thanks a lot.

And no worries! Take the time to reply when you can 😁

// Parse the closure arguments.
let params = self.parseParameterClause(RawClosureParameterClauseSyntax.self) { parser in
parser.parseClosureParameter()
Expand Down Expand Up @@ -2430,52 +2432,49 @@ extension Parser.Lookahead {
}

// Parse pattern-tuple func-signature-result? 'in'.
if lookahead.consume(if: .leftParen) != nil { // Consume the ')'.

if lookahead.at(.leftParen) { // Consume the '('.
// While we don't have '->' or ')', eat balanced tokens.
var skipProgress = LoopProgressCondition()
while !lookahead.at(.endOfFile, .rightParen) && lookahead.hasProgressed(&skipProgress) {
while !lookahead.at(.endOfFile, .rightBrace, .keyword(.in)) && !lookahead.at(.arrow) && lookahead.hasProgressed(&skipProgress) {
Comment on lines -2437 to +2438
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, yes, that does make sense. skipSingle is looking for the rightParen now that we previously consumed manually in lookahead.consume(if: .leftParen).

lookahead.skipSingle()
}

// Consume the ')', if it's there.
if lookahead.consume(if: .rightParen) != nil {
lookahead.consumeEffectsSpecifiers()

// Parse the func-signature-result, if present.
if lookahead.consume(if: .arrow) != nil {
guard lookahead.canParseType() else {
return false
}

lookahead.consumeEffectsSpecifiers()
}
}
// Okay, we have a closure signature.
} else if lookahead.at(.identifier) || lookahead.at(.wildcard) {
// Parse identifier (',' identifier)*
lookahead.consumeAnyToken()

/// If the next token is a colon, interpret is as a type annotation and consume a type after it.
/// While type annotations aren’t allowed in shorthand closure parameters, we consume them to improve recovery.
func consumeOptionalTypeAnnotation() -> Bool {
if lookahead.consume(if: .colon) != nil {
return lookahead.canParseType()
} else {
return true
}
}

var parametersProgress = LoopProgressCondition()
while lookahead.consume(if: .comma) != nil && lookahead.hasProgressed(&parametersProgress) {
while consumeOptionalTypeAnnotation() && lookahead.consume(if: .comma) != nil && lookahead.hasProgressed(&parametersProgress) {
if lookahead.at(.identifier) || lookahead.at(.wildcard) {
lookahead.consumeAnyToken()
continue
}

return false
}
}

lookahead.consumeEffectsSpecifiers()
// Consume the ')', if it's there.
lookahead.consume(if: .rightParen)

// Parse the func-signature-result, if present.
if lookahead.consume(if: .arrow) != nil {
guard lookahead.canParseType() else {
return false
}
lookahead.consumeEffectsSpecifiers()

lookahead.consumeEffectsSpecifiers()
// Parse the func-signature-result, if present.
if lookahead.consume(if: .arrow) != nil {
guard lookahead.canParseType() else {
return false
}

lookahead.consumeEffectsSpecifiers()
}

// Parse the 'in' at the end.
Expand Down
110 changes: 110 additions & 0 deletions Tests/SwiftParserTest/ExpressionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2632,5 +2632,115 @@ final class StatementExpressionTests: ParserTestCase {
assertParse("_ = { (@_noImplicitCopy _ x: Int) -> () in }")

assertParse("_ = { (@Wrapper x) in }")

assertParse(
"""
withInvalidOrderings { (comparisonPredicate: @escaping (Int, Int) -> Bool) in
}
"""
)
}

func testClosureWithMissingParentheses() {
assertParse(
"""
_ = { 1️⃣a: Int, b: Int 2️⃣in
}
""",
diagnostics: [
DiagnosticSpec(
locationMarker: "1️⃣",
message: "expected '(' to start parameter clause",
fixIts: ["insert '('"]
),
DiagnosticSpec(
locationMarker: "2️⃣",
message: "expected ')' to end parameter clause",
fixIts: ["insert ')'"]
),
],
fixedSource: """
_ = { (a: Int, b: Int) in
}
"""
)
}

func testClosureWithReturnArrowAndMissingParentheses() {
assertParse(
"""
_ = { 1️⃣a: Int, b: 2️⃣Int 3️⃣-> Int 4️⃣in
}
""",
diagnostics: [
DiagnosticSpec(
locationMarker: "1️⃣",
message: "expected '(' to start parameter clause",
fixIts: ["insert '('"]
),
DiagnosticSpec(
locationMarker: "2️⃣",
message: "expected '(' to start function type",
fixIts: ["insert '('"]
),
DiagnosticSpec(
locationMarker: "3️⃣",
message: "expected ')' in function type",
fixIts: ["insert ')'"]
),
DiagnosticSpec(
locationMarker: "4️⃣",
message: "expected ')' to end parameter clause",
fixIts: ["insert ')'"]
),
],
fixedSource: """
_ = { (a: Int, b: (Int) -> Int) in
}
"""
)
}

func testClosureWithMissingLeftParenthese() {
assertParse(
"""
_ = { 1️⃣a: Int, b: Int) in
}
""",
diagnostics: [
DiagnosticSpec(
locationMarker: "1️⃣",
message: "expected '(' to start parameter clause",
fixIts: ["insert '('"]
)
],
fixedSource: """
_ = { (a: Int, b: Int) in
}
"""
)
}

func testClosureWithDollarIdentifier() {
assertParse(
"""
let (ids, (actions, tracking)) = state.withCriticalRegion { ($0.valueObservers(for: keyPath), $0.didSet(keyPath: keyPath)) }
"""
)

assertParse(
"""
let (ids, (actions, tracking)) = state.withCriticalRegion { ($0.valueObservers(for: keyPath), $0.didSet(keyPath: keyPath)) }
"""
)

assertParse(
"""
state.withCriticalRegion { (1 + 2) }
for action in tracking {
action()
}
"""
)
}
}