diff --git a/Sources/SwiftParser/Expressions.swift b/Sources/SwiftParser/Expressions.swift index f76aabcc20d..a5f1b0e4c2c 100644 --- a/Sources/SwiftParser/Expressions.swift +++ b/Sources/SwiftParser/Expressions.swift @@ -32,7 +32,7 @@ extension TokenConsumer { } else { return true } - case (.primaryExpressionStart(.atSign), let handle)?: + case (.primaryExpressionStart(.atSign), _)?: break case (_, _)?: return true diff --git a/Sources/SwiftParser/Statements.swift b/Sources/SwiftParser/Statements.swift index c1743dc600d..8656ef4b920 100644 --- a/Sources/SwiftParser/Statements.swift +++ b/Sources/SwiftParser/Statements.swift @@ -187,13 +187,14 @@ extension Parser { var loopProgress = LoopProgressCondition() repeat { let condition = self.parseConditionElement(lastBindingKind: elements.last?.condition.as(RawOptionalBindingConditionSyntax.self)?.bindingKeyword) - let unexpectedBeforeKeepGoing: RawUnexpectedNodesSyntax? + var unexpectedBeforeKeepGoing: RawUnexpectedNodesSyntax? = nil + if let equalOperator = self.consumeIfContextualPunctuator("=="), let falseKeyword = self.consume(if: .keyword(.false)) { + unexpectedBeforeKeepGoing = RawUnexpectedNodesSyntax([equalOperator, falseKeyword], arena: self.arena) + } keepGoing = self.consume(if: .comma) if keepGoing == nil, let andOperator = self.consumeIfContextualPunctuator("&&") { - unexpectedBeforeKeepGoing = RawUnexpectedNodesSyntax([andOperator], arena: self.arena) + unexpectedBeforeKeepGoing = RawUnexpectedNodesSyntax(combining: unexpectedBeforeKeepGoing, andOperator, arena: self.arena) keepGoing = missingToken(.comma) - } else { - unexpectedBeforeKeepGoing = nil } elements.append( RawConditionElementSyntax( diff --git a/Sources/SwiftParserDiagnostics/MissingNodesError.swift b/Sources/SwiftParserDiagnostics/MissingNodesError.swift index bf562748ac3..d47d52d3b05 100644 --- a/Sources/SwiftParserDiagnostics/MissingNodesError.swift +++ b/Sources/SwiftParserDiagnostics/MissingNodesError.swift @@ -112,13 +112,23 @@ func nodesDescription(_ nodes: [SyntaxType], format: func nodesDescriptionAndCommonParent(_ nodes: [SyntaxType], format: Bool) -> (commonAncestor: Syntax?, description: String) { let missingSyntaxNodes = nodes.map(Syntax.init) - // If all tokens in the parent are missing, return the parent type name. + let isOnlyTokenWithNonMissingText: Bool + if let token = nodes.only?.as(TokenSyntax.self) { + isOnlyTokenWithNonMissingText = token.text != "" + } else { + isOnlyTokenWithNonMissingText = false + } + + // If all tokens in the parent are missing, return the parent type name unless + // we are replacing by a single token that has explicit text, in which case we + // return that explicit text. if let commonAncestor = findCommonAncestor(missingSyntaxNodes), commonAncestor.isMissingAllTokens, let firstToken = commonAncestor.firstToken(viewMode: .all), let lastToken = commonAncestor.lastToken(viewMode: .all), missingSyntaxNodes.contains(Syntax(firstToken)), - missingSyntaxNodes.contains(Syntax(lastToken)) + missingSyntaxNodes.contains(Syntax(lastToken)), + !isOnlyTokenWithNonMissingText { if let nodeTypeName = commonAncestor.nodeTypeNameForDiagnostics(allowBlockNames: true) { return (commonAncestor, nodeTypeName) diff --git a/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift b/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift index 91d5359810b..45577150253 100644 --- a/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift +++ b/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift @@ -14,6 +14,22 @@ import SwiftDiagnostics import SwiftParser @_spi(RawSyntax) import SwiftSyntax +fileprivate func getTokens(between first: TokenSyntax, and second: TokenSyntax) -> [TokenSyntax] { + var tokens: [TokenSyntax] = [] + var currentToken = first + + while currentToken != second { + tokens.append(currentToken) + guard let nextToken = currentToken.nextToken(viewMode: .sourceAccurate) else { + assertionFailure("second Token must occur after first Token") + return tokens + } + currentToken = nextToken + } + tokens.append(second) + return tokens +} + fileprivate extension TokenSyntax { /// Assuming this token is a `poundAvailableKeyword` or `poundUnavailableKeyword` /// returns the opposite keyword. @@ -239,7 +255,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { unexpectedTokenCondition: isOfSameKind, correctTokens: [specifier], message: { _ in misspelledError }, - moveFixIt: { ReplaceTokensFixIt(replaceTokens: $0, replacement: specifier) }, + moveFixIt: { ReplaceTokensFixIt(replaceTokens: $0, replacements: [specifier]) }, removeRedundantFixIt: { RemoveRedundantFixIt(removeTokens: $0) } ) } @@ -438,7 +454,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { unexpectedTokenCondition: { $0.text == "||" }, correctTokens: [node.trailingComma], message: { _ in .joinPlatformsUsingComma }, - moveFixIt: { ReplaceTokensFixIt(replaceTokens: $0, replacement: trailingComma) } + moveFixIt: { ReplaceTokensFixIt(replaceTokens: $0, replacements: [trailingComma]) } ) } return .visitChildren @@ -467,13 +483,42 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { if shouldSkip(node) { return .skipChildren } + if let unexpected = node.unexpectedBetweenConditionAndTrailingComma, + let availability = node.condition.as(AvailabilityConditionSyntax.self), + let (_, falseKeyword) = unexpected.twoTokens( + firstSatisfying: { $0.tokenKind == .binaryOperator("==") }, + secondSatisfying: { $0.tokenKind == .keyword(.false) } + ) + { + // Diagnose #available used as an expression + let negatedAvailabilityKeyword = availability.availabilityKeyword.negatedAvailabilityKeyword + let negatedCoditionElement = ConditionElementSyntax( + condition: .availability(availability.with(\.availabilityKeyword, negatedAvailabilityKeyword)), + trailingComma: node.trailingComma + ) + if let negatedAvailability = negatedCoditionElement.condition.as(AvailabilityConditionSyntax.self) { + addDiagnostic( + unexpected, + AvailabilityConditionAsExpression(availabilityToken: availability.availabilityKeyword, negatedAvailabilityToken: negatedAvailabilityKeyword), + fixIts: [ + FixIt( + message: ReplaceTokensFixIt(replaceTokens: getTokens(between: availability.availabilityKeyword, and: falseKeyword), replacements: getTokens(between: negatedAvailability.availabilityKeyword, and: negatedAvailability.rightParen)), + changes: [ + .replace(oldNode: Syntax(node), newNode: Syntax(negatedCoditionElement)) + ] + ) + ], + handledNodes: [unexpected.id] + ) + } + } if let trailingComma = node.trailingComma { exchangeTokens( unexpected: node.unexpectedBetweenConditionAndTrailingComma, unexpectedTokenCondition: { $0.text == "&&" }, correctTokens: [node.trailingComma], message: { _ in .joinConditionsUsingComma }, - moveFixIt: { ReplaceTokensFixIt(replaceTokens: $0, replacement: trailingComma) } + moveFixIt: { ReplaceTokensFixIt(replaceTokens: $0, replacements: [trailingComma]) } ) } return .visitChildren @@ -563,7 +608,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { .expectedCommaInWhereClause, fixIts: [ FixIt( - message: ReplaceTokensFixIt(replaceTokens: [token], replacement: .commaToken()), + message: ReplaceTokensFixIt(replaceTokens: [token], replacements: [.commaToken()]), changes: [ .makeMissing(token), .makePresent(trailingComma), @@ -698,7 +743,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { .typeParameterPackEllipsis, fixIts: [ FixIt( - message: ReplaceTokensFixIt(replaceTokens: [unexpectedEllipsis], replacement: .keyword(.each)), + message: ReplaceTokensFixIt(replaceTokens: [unexpectedEllipsis], replacements: [.keyword(.each)]), changes: [ .makeMissing(unexpected), .makePresent(each, trailingTrivia: .space), @@ -714,7 +759,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { unexpectedTokenCondition: { $0.tokenKind == .keyword(.class) }, correctTokens: [inheritedTypeName], message: { _ in StaticParserError.classConstraintCanOnlyBeUsedInProtocol }, - moveFixIt: { ReplaceTokensFixIt(replaceTokens: $0, replacement: inheritedTypeName) } + moveFixIt: { ReplaceTokensFixIt(replaceTokens: $0, replacements: [inheritedTypeName]) } ) } return .visitChildren @@ -745,7 +790,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { NegatedAvailabilityCondition(avaialabilityCondition: availability, negatedAvailabilityKeyword: negatedAvailabilityKeyword), fixIts: [ FixIt( - message: ReplaceTokensFixIt(replaceTokens: [operatorToken, availability.availabilityKeyword], replacement: negatedAvailabilityKeyword), + message: ReplaceTokensFixIt(replaceTokens: [operatorToken, availability.availabilityKeyword], replacements: [negatedAvailabilityKeyword]), changes: [ .replace(oldNode: Syntax(conditionElement), newNode: Syntax(negatedCoditionElement)) ] @@ -777,7 +822,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { StaticParserError.unexpectedPoundElseSpaceIf, fixIts: [ FixIt( - message: ReplaceTokensFixIt(replaceTokens: unexpectedTokens, replacement: clause.poundKeyword), + message: ReplaceTokensFixIt(replaceTokens: unexpectedTokens, replacements: [clause.poundKeyword]), changes: [ .makeMissing(unexpectedBeforePoundKeyword, transferTrivia: false), .makePresent(clause.poundKeyword, leadingTrivia: unexpectedBeforePoundKeyword.leadingTrivia), @@ -821,7 +866,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { .expectedAssignmentInsteadOfComparisonOperator, fixIts: [ FixIt( - message: ReplaceTokensFixIt(replaceTokens: [.binaryOperator("==")], replacement: node.equal), + message: ReplaceTokensFixIt(replaceTokens: [.binaryOperator("==")], replacements: [node.equal]), changes: [.makeMissing(unexpected), .makePresent(node.equal, leadingTrivia: [])] ) ], @@ -835,7 +880,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { unexpectedTokenCondition: { $0.tokenKind == .colon }, correctTokens: [node.equal], message: { _ in StaticParserError.initializerInPattern }, - moveFixIt: { ReplaceTokensFixIt(replaceTokens: $0, replacement: node.equal) } + moveFixIt: { ReplaceTokensFixIt(replaceTokens: $0, replacements: [node.equal]) } ) } return .visitChildren @@ -1052,7 +1097,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { } if let singleQuote = node.unexpectedBetweenOpenDelimiterAndOpenQuote?.onlyToken(where: { $0.tokenKind == .singleQuote }) { let fixIt = FixIt( - message: ReplaceTokensFixIt(replaceTokens: [singleQuote], replacement: node.openQuote), + message: ReplaceTokensFixIt(replaceTokens: [singleQuote], replacements: [node.openQuote]), changes: [ .makeMissing(singleQuote, transferTrivia: false), .makePresent(node.openQuote, leadingTrivia: singleQuote.leadingTrivia), @@ -1228,7 +1273,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { unexpectedTokenCondition: { $0.tokenKind == .colon }, correctTokens: [node.equal], message: { _ in MissingNodesError(missingNodes: [Syntax(node.equal)]) }, - moveFixIt: { ReplaceTokensFixIt(replaceTokens: $0, replacement: node.equal) } + moveFixIt: { ReplaceTokensFixIt(replaceTokens: $0, replacements: [node.equal]) } ) } return .visitChildren diff --git a/Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift b/Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift index 5977c1dd9e9..fa001d51a9e 100644 --- a/Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift +++ b/Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift @@ -246,6 +246,15 @@ public struct AsyncMustPrecedeThrows: ParserError { } } +public struct AvailabilityConditionAsExpression: ParserError { + public let availabilityToken: TokenSyntax + public let negatedAvailabilityToken: TokenSyntax + + public var message: String { + return "\(availabilityToken) cannot be used as an expression, did you mean to use '\(negatedAvailabilityToken)'?" + } +} + public struct AvailabilityConditionInExpression: ParserError { public let availabilityCondition: AvailabilityConditionSyntax @@ -608,9 +617,9 @@ public struct RemoveNodesFixIt: ParserFixIt { public struct ReplaceTokensFixIt: ParserFixIt { public let replaceTokens: [TokenSyntax] - public let replacement: TokenSyntax + public let replacements: [TokenSyntax] public var message: String { - "replace \(nodesDescription(replaceTokens, format: false)) with '\(replacement.text)'" + "replace \(nodesDescription(replaceTokens, format: false)) with \(nodesDescription(replacements, format: false))" } } diff --git a/Sources/SwiftParserDiagnostics/SyntaxExtensions.swift b/Sources/SwiftParserDiagnostics/SyntaxExtensions.swift index df6d38a5781..4c26a7351d3 100644 --- a/Sources/SwiftParserDiagnostics/SyntaxExtensions.swift +++ b/Sources/SwiftParserDiagnostics/SyntaxExtensions.swift @@ -39,6 +39,22 @@ extension UnexpectedNodesSyntax { return nil } } + + /// If this only contains two tokens, the first satisfying `firstCondition`, and the second satisfying `secondCondition`, + /// return these tokens as a tuple, otherwise return `nil`. + func twoTokens( + firstSatisfying firstCondition: (TokenSyntax) -> Bool, + secondSatisfying secondCondition: (TokenSyntax) -> Bool + ) -> (first: TokenSyntax, second: TokenSyntax)? { + let sourceAccurateChildren = self.children(viewMode: .sourceAccurate).compactMap({ $0.as(TokenSyntax.self) }) + guard sourceAccurateChildren.count == 2 else { + return nil + } + guard firstCondition(sourceAccurateChildren[0]) && secondCondition(sourceAccurateChildren[1]) else { + return nil + } + return (sourceAccurateChildren[0], sourceAccurateChildren[1]) + } } extension Syntax { diff --git a/Tests/SwiftParserTest/translated/AvailabilityQueryUnavailabilityTests.swift b/Tests/SwiftParserTest/translated/AvailabilityQueryUnavailabilityTests.swift index fc9aca876e1..62c27d4d9a5 100644 --- a/Tests/SwiftParserTest/translated/AvailabilityQueryUnavailabilityTests.swift +++ b/Tests/SwiftParserTest/translated/AvailabilityQueryUnavailabilityTests.swift @@ -441,13 +441,28 @@ final class AvailabilityQueryUnavailabilityTests: XCTestCase { } """, diagnostics: [ - // TODO: (good first issue) Old parser expected error on line 2: #available cannot be used as an expression, did you mean to use '#unavailable'?, Fix-It replacements: 4 - 14 = '#unavailable', 18 - 27 = '' - DiagnosticSpec(message: "unexpected code '== false' in 'if' statement") + DiagnosticSpec(message: "#available cannot be used as an expression, did you mean to use '#unavailable'?", fixIts: ["replace '#available(*) == false' with '#unavailable(*)'"]) ] ) } func testAvailabilityQueryUnavailability34b() { + assertParse( + """ + // Diagnose wrong spellings of unavailability + if #available(*) 1️⃣== false && 2️⃣true { + } + """, + diagnostics: [ + DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected code '== false &&' in 'if' statement"), + DiagnosticSpec(locationMarker: "2️⃣", message: "expected ',' in 'if' statement", fixIts: ["insert ','"]), + // TODO: Old parser expected error on line 2: #available cannot be used as an expression, did you mean to use '#unavailable'?, Fix-It replacements: 4 - 14 = '#unavailable', 18 - 27 = '' + // TODO: Old parser expected error on line 2: expected ',' joining parts of a multi-clause condition, Fix-It replacements: 27 - 28 = ',' + ] + ) + } + + func testAvailabilityQueryUnavailability34c() { assertParse( """ if !1️⃣#available(*) {