Skip to content

Commit 357a3e6

Browse files
authored
Merge pull request #1464 from CippoX/fixing-available-cannot-be-used-as-an-expression
Fixed testAvailabilityQueryUnavailability34a
2 parents 82b5683 + 07fff06 commit 357a3e6

File tree

7 files changed

+119
-23
lines changed

7 files changed

+119
-23
lines changed

Sources/SwiftParser/Expressions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ extension TokenConsumer {
3232
} else {
3333
return true
3434
}
35-
case (.primaryExpressionStart(.atSign), let handle)?:
35+
case (.primaryExpressionStart(.atSign), _)?:
3636
break
3737
case (_, _)?:
3838
return true

Sources/SwiftParser/Statements.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,13 +187,14 @@ extension Parser {
187187
var loopProgress = LoopProgressCondition()
188188
repeat {
189189
let condition = self.parseConditionElement(lastBindingKind: elements.last?.condition.as(RawOptionalBindingConditionSyntax.self)?.bindingKeyword)
190-
let unexpectedBeforeKeepGoing: RawUnexpectedNodesSyntax?
190+
var unexpectedBeforeKeepGoing: RawUnexpectedNodesSyntax? = nil
191+
if let equalOperator = self.consumeIfContextualPunctuator("=="), let falseKeyword = self.consume(if: .keyword(.false)) {
192+
unexpectedBeforeKeepGoing = RawUnexpectedNodesSyntax([equalOperator, falseKeyword], arena: self.arena)
193+
}
191194
keepGoing = self.consume(if: .comma)
192195
if keepGoing == nil, let andOperator = self.consumeIfContextualPunctuator("&&") {
193-
unexpectedBeforeKeepGoing = RawUnexpectedNodesSyntax([andOperator], arena: self.arena)
196+
unexpectedBeforeKeepGoing = RawUnexpectedNodesSyntax(combining: unexpectedBeforeKeepGoing, andOperator, arena: self.arena)
194197
keepGoing = missingToken(.comma)
195-
} else {
196-
unexpectedBeforeKeepGoing = nil
197198
}
198199
elements.append(
199200
RawConditionElementSyntax(

Sources/SwiftParserDiagnostics/MissingNodesError.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,23 @@ func nodesDescription<SyntaxType: SyntaxProtocol>(_ nodes: [SyntaxType], format:
112112
func nodesDescriptionAndCommonParent<SyntaxType: SyntaxProtocol>(_ nodes: [SyntaxType], format: Bool) -> (commonAncestor: Syntax?, description: String) {
113113
let missingSyntaxNodes = nodes.map(Syntax.init)
114114

115-
// If all tokens in the parent are missing, return the parent type name.
115+
let isOnlyTokenWithNonMissingText: Bool
116+
if let token = nodes.only?.as(TokenSyntax.self) {
117+
isOnlyTokenWithNonMissingText = token.text != ""
118+
} else {
119+
isOnlyTokenWithNonMissingText = false
120+
}
121+
122+
// If all tokens in the parent are missing, return the parent type name unless
123+
// we are replacing by a single token that has explicit text, in which case we
124+
// return that explicit text.
116125
if let commonAncestor = findCommonAncestor(missingSyntaxNodes),
117126
commonAncestor.isMissingAllTokens,
118127
let firstToken = commonAncestor.firstToken(viewMode: .all),
119128
let lastToken = commonAncestor.lastToken(viewMode: .all),
120129
missingSyntaxNodes.contains(Syntax(firstToken)),
121-
missingSyntaxNodes.contains(Syntax(lastToken))
130+
missingSyntaxNodes.contains(Syntax(lastToken)),
131+
!isOnlyTokenWithNonMissingText
122132
{
123133
if let nodeTypeName = commonAncestor.nodeTypeNameForDiagnostics(allowBlockNames: true) {
124134
return (commonAncestor, nodeTypeName)

Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,22 @@ import SwiftDiagnostics
1414
import SwiftParser
1515
@_spi(RawSyntax) import SwiftSyntax
1616

17+
fileprivate func getTokens(between first: TokenSyntax, and second: TokenSyntax) -> [TokenSyntax] {
18+
var tokens: [TokenSyntax] = []
19+
var currentToken = first
20+
21+
while currentToken != second {
22+
tokens.append(currentToken)
23+
guard let nextToken = currentToken.nextToken(viewMode: .sourceAccurate) else {
24+
assertionFailure("second Token must occur after first Token")
25+
return tokens
26+
}
27+
currentToken = nextToken
28+
}
29+
tokens.append(second)
30+
return tokens
31+
}
32+
1733
fileprivate extension TokenSyntax {
1834
/// Assuming this token is a `poundAvailableKeyword` or `poundUnavailableKeyword`
1935
/// returns the opposite keyword.
@@ -239,7 +255,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
239255
unexpectedTokenCondition: isOfSameKind,
240256
correctTokens: [specifier],
241257
message: { _ in misspelledError },
242-
moveFixIt: { ReplaceTokensFixIt(replaceTokens: $0, replacement: specifier) },
258+
moveFixIt: { ReplaceTokensFixIt(replaceTokens: $0, replacements: [specifier]) },
243259
removeRedundantFixIt: { RemoveRedundantFixIt(removeTokens: $0) }
244260
)
245261
}
@@ -438,7 +454,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
438454
unexpectedTokenCondition: { $0.text == "||" },
439455
correctTokens: [node.trailingComma],
440456
message: { _ in .joinPlatformsUsingComma },
441-
moveFixIt: { ReplaceTokensFixIt(replaceTokens: $0, replacement: trailingComma) }
457+
moveFixIt: { ReplaceTokensFixIt(replaceTokens: $0, replacements: [trailingComma]) }
442458
)
443459
}
444460
return .visitChildren
@@ -467,13 +483,42 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
467483
if shouldSkip(node) {
468484
return .skipChildren
469485
}
486+
if let unexpected = node.unexpectedBetweenConditionAndTrailingComma,
487+
let availability = node.condition.as(AvailabilityConditionSyntax.self),
488+
let (_, falseKeyword) = unexpected.twoTokens(
489+
firstSatisfying: { $0.tokenKind == .binaryOperator("==") },
490+
secondSatisfying: { $0.tokenKind == .keyword(.false) }
491+
)
492+
{
493+
// Diagnose #available used as an expression
494+
let negatedAvailabilityKeyword = availability.availabilityKeyword.negatedAvailabilityKeyword
495+
let negatedCoditionElement = ConditionElementSyntax(
496+
condition: .availability(availability.with(\.availabilityKeyword, negatedAvailabilityKeyword)),
497+
trailingComma: node.trailingComma
498+
)
499+
if let negatedAvailability = negatedCoditionElement.condition.as(AvailabilityConditionSyntax.self) {
500+
addDiagnostic(
501+
unexpected,
502+
AvailabilityConditionAsExpression(availabilityToken: availability.availabilityKeyword, negatedAvailabilityToken: negatedAvailabilityKeyword),
503+
fixIts: [
504+
FixIt(
505+
message: ReplaceTokensFixIt(replaceTokens: getTokens(between: availability.availabilityKeyword, and: falseKeyword), replacements: getTokens(between: negatedAvailability.availabilityKeyword, and: negatedAvailability.rightParen)),
506+
changes: [
507+
.replace(oldNode: Syntax(node), newNode: Syntax(negatedCoditionElement))
508+
]
509+
)
510+
],
511+
handledNodes: [unexpected.id]
512+
)
513+
}
514+
}
470515
if let trailingComma = node.trailingComma {
471516
exchangeTokens(
472517
unexpected: node.unexpectedBetweenConditionAndTrailingComma,
473518
unexpectedTokenCondition: { $0.text == "&&" },
474519
correctTokens: [node.trailingComma],
475520
message: { _ in .joinConditionsUsingComma },
476-
moveFixIt: { ReplaceTokensFixIt(replaceTokens: $0, replacement: trailingComma) }
521+
moveFixIt: { ReplaceTokensFixIt(replaceTokens: $0, replacements: [trailingComma]) }
477522
)
478523
}
479524
return .visitChildren
@@ -563,7 +608,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
563608
.expectedCommaInWhereClause,
564609
fixIts: [
565610
FixIt(
566-
message: ReplaceTokensFixIt(replaceTokens: [token], replacement: .commaToken()),
611+
message: ReplaceTokensFixIt(replaceTokens: [token], replacements: [.commaToken()]),
567612
changes: [
568613
.makeMissing(token),
569614
.makePresent(trailingComma),
@@ -698,7 +743,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
698743
.typeParameterPackEllipsis,
699744
fixIts: [
700745
FixIt(
701-
message: ReplaceTokensFixIt(replaceTokens: [unexpectedEllipsis], replacement: .keyword(.each)),
746+
message: ReplaceTokensFixIt(replaceTokens: [unexpectedEllipsis], replacements: [.keyword(.each)]),
702747
changes: [
703748
.makeMissing(unexpected),
704749
.makePresent(each, trailingTrivia: .space),
@@ -714,7 +759,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
714759
unexpectedTokenCondition: { $0.tokenKind == .keyword(.class) },
715760
correctTokens: [inheritedTypeName],
716761
message: { _ in StaticParserError.classConstraintCanOnlyBeUsedInProtocol },
717-
moveFixIt: { ReplaceTokensFixIt(replaceTokens: $0, replacement: inheritedTypeName) }
762+
moveFixIt: { ReplaceTokensFixIt(replaceTokens: $0, replacements: [inheritedTypeName]) }
718763
)
719764
}
720765
return .visitChildren
@@ -745,7 +790,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
745790
NegatedAvailabilityCondition(avaialabilityCondition: availability, negatedAvailabilityKeyword: negatedAvailabilityKeyword),
746791
fixIts: [
747792
FixIt(
748-
message: ReplaceTokensFixIt(replaceTokens: [operatorToken, availability.availabilityKeyword], replacement: negatedAvailabilityKeyword),
793+
message: ReplaceTokensFixIt(replaceTokens: [operatorToken, availability.availabilityKeyword], replacements: [negatedAvailabilityKeyword]),
749794
changes: [
750795
.replace(oldNode: Syntax(conditionElement), newNode: Syntax(negatedCoditionElement))
751796
]
@@ -777,7 +822,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
777822
StaticParserError.unexpectedPoundElseSpaceIf,
778823
fixIts: [
779824
FixIt(
780-
message: ReplaceTokensFixIt(replaceTokens: unexpectedTokens, replacement: clause.poundKeyword),
825+
message: ReplaceTokensFixIt(replaceTokens: unexpectedTokens, replacements: [clause.poundKeyword]),
781826
changes: [
782827
.makeMissing(unexpectedBeforePoundKeyword, transferTrivia: false),
783828
.makePresent(clause.poundKeyword, leadingTrivia: unexpectedBeforePoundKeyword.leadingTrivia),
@@ -821,7 +866,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
821866
.expectedAssignmentInsteadOfComparisonOperator,
822867
fixIts: [
823868
FixIt(
824-
message: ReplaceTokensFixIt(replaceTokens: [.binaryOperator("==")], replacement: node.equal),
869+
message: ReplaceTokensFixIt(replaceTokens: [.binaryOperator("==")], replacements: [node.equal]),
825870
changes: [.makeMissing(unexpected), .makePresent(node.equal, leadingTrivia: [])]
826871
)
827872
],
@@ -835,7 +880,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
835880
unexpectedTokenCondition: { $0.tokenKind == .colon },
836881
correctTokens: [node.equal],
837882
message: { _ in StaticParserError.initializerInPattern },
838-
moveFixIt: { ReplaceTokensFixIt(replaceTokens: $0, replacement: node.equal) }
883+
moveFixIt: { ReplaceTokensFixIt(replaceTokens: $0, replacements: [node.equal]) }
839884
)
840885
}
841886
return .visitChildren
@@ -1052,7 +1097,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
10521097
}
10531098
if let singleQuote = node.unexpectedBetweenOpenDelimiterAndOpenQuote?.onlyToken(where: { $0.tokenKind == .singleQuote }) {
10541099
let fixIt = FixIt(
1055-
message: ReplaceTokensFixIt(replaceTokens: [singleQuote], replacement: node.openQuote),
1100+
message: ReplaceTokensFixIt(replaceTokens: [singleQuote], replacements: [node.openQuote]),
10561101
changes: [
10571102
.makeMissing(singleQuote, transferTrivia: false),
10581103
.makePresent(node.openQuote, leadingTrivia: singleQuote.leadingTrivia),
@@ -1228,7 +1273,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
12281273
unexpectedTokenCondition: { $0.tokenKind == .colon },
12291274
correctTokens: [node.equal],
12301275
message: { _ in MissingNodesError(missingNodes: [Syntax(node.equal)]) },
1231-
moveFixIt: { ReplaceTokensFixIt(replaceTokens: $0, replacement: node.equal) }
1276+
moveFixIt: { ReplaceTokensFixIt(replaceTokens: $0, replacements: [node.equal]) }
12321277
)
12331278
}
12341279
return .visitChildren

Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,15 @@ public struct AsyncMustPrecedeThrows: ParserError {
246246
}
247247
}
248248

249+
public struct AvailabilityConditionAsExpression: ParserError {
250+
public let availabilityToken: TokenSyntax
251+
public let negatedAvailabilityToken: TokenSyntax
252+
253+
public var message: String {
254+
return "\(availabilityToken) cannot be used as an expression, did you mean to use '\(negatedAvailabilityToken)'?"
255+
}
256+
}
257+
249258
public struct AvailabilityConditionInExpression: ParserError {
250259
public let availabilityCondition: AvailabilityConditionSyntax
251260

@@ -608,9 +617,9 @@ public struct RemoveNodesFixIt: ParserFixIt {
608617
public struct ReplaceTokensFixIt: ParserFixIt {
609618
public let replaceTokens: [TokenSyntax]
610619

611-
public let replacement: TokenSyntax
620+
public let replacements: [TokenSyntax]
612621

613622
public var message: String {
614-
"replace \(nodesDescription(replaceTokens, format: false)) with '\(replacement.text)'"
623+
"replace \(nodesDescription(replaceTokens, format: false)) with \(nodesDescription(replacements, format: false))"
615624
}
616625
}

Sources/SwiftParserDiagnostics/SyntaxExtensions.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,22 @@ extension UnexpectedNodesSyntax {
3939
return nil
4040
}
4141
}
42+
43+
/// If this only contains two tokens, the first satisfying `firstCondition`, and the second satisfying `secondCondition`,
44+
/// return these tokens as a tuple, otherwise return `nil`.
45+
func twoTokens(
46+
firstSatisfying firstCondition: (TokenSyntax) -> Bool,
47+
secondSatisfying secondCondition: (TokenSyntax) -> Bool
48+
) -> (first: TokenSyntax, second: TokenSyntax)? {
49+
let sourceAccurateChildren = self.children(viewMode: .sourceAccurate).compactMap({ $0.as(TokenSyntax.self) })
50+
guard sourceAccurateChildren.count == 2 else {
51+
return nil
52+
}
53+
guard firstCondition(sourceAccurateChildren[0]) && secondCondition(sourceAccurateChildren[1]) else {
54+
return nil
55+
}
56+
return (sourceAccurateChildren[0], sourceAccurateChildren[1])
57+
}
4258
}
4359

4460
extension Syntax {

Tests/SwiftParserTest/translated/AvailabilityQueryUnavailabilityTests.swift

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -441,13 +441,28 @@ final class AvailabilityQueryUnavailabilityTests: XCTestCase {
441441
}
442442
""",
443443
diagnostics: [
444-
// 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 = ''
445-
DiagnosticSpec(message: "unexpected code '== false' in 'if' statement")
444+
DiagnosticSpec(message: "#available cannot be used as an expression, did you mean to use '#unavailable'?", fixIts: ["replace '#available(*) == false' with '#unavailable(*)'"])
446445
]
447446
)
448447
}
449448

450449
func testAvailabilityQueryUnavailability34b() {
450+
assertParse(
451+
"""
452+
// Diagnose wrong spellings of unavailability
453+
if #available(*) 1️⃣== false && 2️⃣true {
454+
}
455+
""",
456+
diagnostics: [
457+
DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected code '== false &&' in 'if' statement"),
458+
DiagnosticSpec(locationMarker: "2️⃣", message: "expected ',' in 'if' statement", fixIts: ["insert ','"]),
459+
// 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 = ''
460+
// TODO: Old parser expected error on line 2: expected ',' joining parts of a multi-clause condition, Fix-It replacements: 27 - 28 = ','
461+
]
462+
)
463+
}
464+
465+
func testAvailabilityQueryUnavailability34c() {
451466
assertParse(
452467
"""
453468
if !1️⃣#available(*) {

0 commit comments

Comments
 (0)