Skip to content

Commit dfcf40c

Browse files
committed
Improve diagnostics for #available nodes in expression positions
1 parent 59e8e32 commit dfcf40c

File tree

6 files changed

+149
-51
lines changed

6 files changed

+149
-51
lines changed

Sources/SwiftParser/Expressions.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,12 +1209,20 @@ extension Parser {
12091209
arena: self.arena
12101210
)
12111211
)
1212-
12131212
case (.pound, _)?:
12141213
return RawExprSyntax(
12151214
self.parseMacroExpansionExpr(pattern: pattern, flavor: flavor)
12161215
)
1217-
1216+
case (.poundAvailableKeyword, _)?, (.poundUnavailableKeyword, _)?:
1217+
let poundAvailable = self.parsePoundAvailableConditionElement()
1218+
return RawExprSyntax(
1219+
RawIdentifierExprSyntax(
1220+
RawUnexpectedNodesSyntax([poundAvailable], arena: self.arena),
1221+
identifier: missingToken(.identifier),
1222+
declNameArguments: nil,
1223+
arena: self.arena
1224+
)
1225+
)
12181226
case (.leftBrace, _)?: // expr-closure
12191227
return RawExprSyntax(self.parseClosureExpression())
12201228
case (.period, let handle)?: // .foo

Sources/SwiftParser/RawTokenKindSubset.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,8 @@ enum PrimaryExpressionStart: RawTokenKindSubset {
693693
case nilKeyword
694694
case period
695695
case pound
696+
case poundAvailableKeyword // For recovery
697+
case poundUnavailableKeyword // For recovery
696698
case regexLiteral
697699
case selfKeyword
698700
case stringLiteral
@@ -716,6 +718,8 @@ enum PrimaryExpressionStart: RawTokenKindSubset {
716718
case RawTokenKindMatch(.nil): self = .nilKeyword
717719
case RawTokenKindMatch(.period): self = .period
718720
case RawTokenKindMatch(.pound): self = .pound
721+
case RawTokenKindMatch(.poundAvailableKeyword): self = .poundAvailableKeyword
722+
case RawTokenKindMatch(.poundUnavailableKeyword): self = .poundUnavailableKeyword
719723
case RawTokenKindMatch(.regexLiteral): self = .regexLiteral
720724
case RawTokenKindMatch(.self): self = .selfKeyword
721725
case RawTokenKindMatch(.stringLiteral): self = .stringLiteral
@@ -742,6 +746,8 @@ enum PrimaryExpressionStart: RawTokenKindSubset {
742746
case .nilKeyword: return .keyword(.nil)
743747
case .period: return .period
744748
case .pound: return .pound
749+
case .poundAvailableKeyword: return .poundAvailableKeyword
750+
case .poundUnavailableKeyword: return .poundUnavailableKeyword
745751
case .regexLiteral: return .regexLiteral
746752
case .selfKeyword: return .keyword(.self)
747753
case .stringLiteral: return .stringLiteral

Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift

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

17+
fileprivate extension TokenSyntax {
18+
/// Assuming this token is a `poundAvailableKeyword` or `poundUnavailableKeyword`
19+
/// returns the opposite keyword.
20+
var negatedAvailabilityKeyword: TokenSyntax {
21+
switch self.tokenKind {
22+
case .poundAvailableKeyword:
23+
return self.withKind(.poundUnavailableKeyword)
24+
case .poundUnavailableKeyword:
25+
return self.withKind(.poundAvailableKeyword)
26+
default:
27+
assertionFailure("The availability token of an AvailabilityConditionSyntax should always be #available or #unavailable")
28+
return self
29+
}
30+
}
31+
}
32+
1733
public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
1834
private var diagnostics: [Diagnostic] = []
1935

@@ -464,11 +480,39 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
464480
if shouldSkip(node) {
465481
return .skipChildren
466482
}
467-
if node.identifier.presence == .missing,
468-
let unexpected = node.unexpectedBeforeIdentifier,
469-
unexpected.first?.as(TokenSyntax.self)?.tokenKind == .pound
470-
{
471-
addDiagnostic(unexpected, UnknownDirectiveError(unexpected: unexpected), handledNodes: [unexpected.id, node.identifier.id])
483+
if node.identifier.presence == .missing, let unexpected = node.unexpectedBeforeIdentifier {
484+
if unexpected.first?.as(TokenSyntax.self)?.tokenKind == .pound {
485+
addDiagnostic(unexpected, UnknownDirectiveError(unexpected: unexpected), handledNodes: [unexpected.id, node.identifier.id])
486+
} else if let availability = unexpected.first?.as(AvailabilityConditionSyntax.self) {
487+
if let prefixOperatorExpr = node.parent?.as(PrefixOperatorExprSyntax.self),
488+
let operatorToken = prefixOperatorExpr.operatorToken,
489+
operatorToken.text == "!",
490+
let conditionElement = prefixOperatorExpr.parent?.as(ConditionElementSyntax.self)
491+
{
492+
// Diagnose !#available(...) and !#unavailable(...)
493+
494+
let negatedAvailabilityKeyword = availability.availabilityKeyword.negatedAvailabilityKeyword
495+
let negatedCoditionElement = ConditionElementSyntax(
496+
condition: .availability(availability.withAvailabilityKeyword(negatedAvailabilityKeyword)),
497+
trailingComma: conditionElement.trailingComma
498+
)
499+
addDiagnostic(
500+
unexpected,
501+
NegatedAvailabilityCondition(avaialabilityCondition: availability, negatedAvailabilityKeyword: negatedAvailabilityKeyword),
502+
fixIts: [
503+
FixIt(
504+
message: ReplaceTokensFixIt(replaceTokens: [operatorToken, availability.availabilityKeyword], replacement: negatedAvailabilityKeyword),
505+
changes: [
506+
.replace(oldNode: Syntax(conditionElement), newNode: Syntax(negatedCoditionElement))
507+
]
508+
)
509+
],
510+
handledNodes: [unexpected.id, node.identifier.id]
511+
)
512+
} else {
513+
addDiagnostic(unexpected, AvailabilityConditionInExpression(avaialabilityCondition: availability), handledNodes: [unexpected.id, node.identifier.id])
514+
}
515+
}
472516
}
473517
return .visitChildren
474518
}

Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,14 @@ extension DiagnosticMessage where Self == StaticParserError {
171171

172172
// MARK: - Diagnostics (please sort alphabetically)
173173

174+
public struct AvailabilityConditionInExpression: ParserError {
175+
public let avaialabilityCondition: AvailabilityConditionSyntax
176+
177+
public var message: String {
178+
return "\(nodesDescription([avaialabilityCondition], format: false)) cannot be used in an expression, only as a condition of 'if' or 'guard'"
179+
}
180+
}
181+
174182
public struct EffectsSpecifierAfterArrow: ParserError {
175183
public let effectsSpecifiersAfterArrow: [TokenSyntax]
176184

@@ -225,6 +233,15 @@ public struct MissingAttributeArgument: ParserError {
225233
}
226234
}
227235

236+
public struct NegatedAvailabilityCondition: ParserError {
237+
public let avaialabilityCondition: AvailabilityConditionSyntax
238+
public let negatedAvailabilityKeyword: TokenSyntax
239+
240+
public var message: String {
241+
return "\(nodesDescription([avaialabilityCondition], format: false)) cannot be used in an expression; did you mean \(nodesDescription([negatedAvailabilityKeyword], format: false))?"
242+
}
243+
}
244+
228245
public struct SpaceSeparatedIdentifiersError: ParserError {
229246
public let firstToken: TokenSyntax
230247
public let additionalTokens: [TokenSyntax]

Tests/SwiftParserTest/translated/AvailabilityQueryTests.swift

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,7 @@ final class AvailabilityQueryTests: XCTestCase {
3131
if (1️⃣#available(OSX 10.51, *)) {}
3232
""",
3333
diagnostics: [
34-
// TODO: Old parser expected error on line 2: #available may only be used as condition of an 'if', 'guard'
35-
DiagnosticSpec(message: "expected value in tuple"),
36-
DiagnosticSpec(message: "unexpected code '#available(OSX 10.51, *)' in tuple"),
34+
DiagnosticSpec(message: "availability condition cannot be used in an expression, only as a condition of 'if' or 'guard'")
3735
]
3836
)
3937
}
@@ -44,9 +42,7 @@ final class AvailabilityQueryTests: XCTestCase {
4442
let x = 1️⃣#available(OSX 10.51, *)
4543
""",
4644
diagnostics: [
47-
// TODO: Old parser expected error on line 1: #available may only be used as condition of
48-
DiagnosticSpec(message: "expected expression in variable"),
49-
DiagnosticSpec(message: "extraneous code '#available(OSX 10.51, *)' at top level"),
45+
DiagnosticSpec(message: "availability condition cannot be used in an expression, only as a condition of 'if' or 'guard'")
5046
]
5147
)
5248
}
@@ -57,28 +53,35 @@ final class AvailabilityQueryTests: XCTestCase {
5753
(1️⃣#available(OSX 10.51, *) ? 1 : 0)
5854
""",
5955
diagnostics: [
60-
// TODO: Old parser expected error on line 1: #available may only be used as condition of an
61-
DiagnosticSpec(message: "expected value in tuple"),
62-
DiagnosticSpec(message: "unexpected code '#available(OSX 10.51, *) ? 1 : 0' in tuple"),
56+
DiagnosticSpec(message: "availability condition cannot be used in an expression, only as a condition of 'if' or 'guard'")
6357
]
6458
)
6559
}
6660

67-
func testAvailabilityQuery5() {
61+
func testAvailabilityQuery5a() {
6862
AssertParse(
6963
"""
7064
if !1️⃣#available(OSX 10.52, *) {
7165
}
72-
if let _ = Optional(5), !2️⃣#available(OSX 10.52, *) {
66+
""",
67+
diagnostics: [
68+
DiagnosticSpec(message: "availability condition cannot be used in an expression; did you mean '#unavailable'?", fixIts: ["replace '!#available' by '#unavailable'"])
69+
],
70+
fixedSource: """
71+
if #unavailable(OSX 10.52, *) {
72+
}
73+
"""
74+
)
75+
}
76+
77+
func testAvailabilityQuery5b() {
78+
AssertParse(
79+
"""
80+
if let _ = Optional(5), !1️⃣#available(OSX 10.52, *) {
7381
}
7482
""",
7583
diagnostics: [
76-
// TODO: Old parser expected error on line 1: #available cannot be used as an expression, did you mean to use '#unavailable'?, Fix-It replacements: 4 - 15 = '#unavailable'
77-
DiagnosticSpec(locationMarker: "1️⃣", message: "expected expression in prefix operator expression"),
78-
DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected code '#available(OSX 10.52, *)' in 'if' statement"),
79-
// TODO: Old parser expected error on line 3: #available cannot be used as an expression, did you mean to use '#unavailable'?, Fix-It replacements: 25 - 36 = '#unavailable'
80-
DiagnosticSpec(locationMarker: "2️⃣", message: "expected expression in prefix operator expression"),
81-
DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code '#available(OSX 10.52, *)' in 'if' statement"),
84+
DiagnosticSpec(message: "availability condition cannot be used in an expression; did you mean '#unavailable'?", fixIts: ["replace '!#available' by '#unavailable'"])
8285
]
8386
)
8487
}

Tests/SwiftParserTest/translated/AvailabilityQueryUnavailabilityTests.swift

Lines changed: 47 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,34 +25,48 @@ final class AvailabilityQueryUnavailabilityTests: XCTestCase {
2525
)
2626
}
2727

28-
func testAvailabilityQueryUnavailability2() {
28+
func testAvailabilityQueryUnavailability2a() {
2929
AssertParse(
3030
"""
3131
// Disallow explicit wildcards.
3232
if #unavailable(OSX 10.51, *) {}
3333
// Disallow use as an expression.
34-
if (1️⃣#unavailable(OSX 10.51)) {}
35-
let x = 3️⃣#unavailable(OSX 10.51)
36-
(#unavailable(OSX 10.51) ? 1 : 0)
37-
if !#unavailable(OSX 10.52) {
34+
if (1️⃣#unavailable(OSX 10.51)) {}
35+
let x = 2️⃣#unavailable(OSX 10.51)
36+
(3️⃣#unavailable(OSX 10.51) ? 1 : 0)
37+
""",
38+
diagnostics: [
39+
DiagnosticSpec(locationMarker: "1️⃣", message: "availability condition cannot be used in an expression, only as a condition of 'if' or 'guard'"),
40+
DiagnosticSpec(locationMarker: "2️⃣", message: "availability condition cannot be used in an expression, only as a condition of 'if' or 'guard'"),
41+
DiagnosticSpec(locationMarker: "3️⃣", message: "availability condition cannot be used in an expression, only as a condition of 'if' or 'guard'"),
42+
]
43+
)
44+
}
45+
46+
func testAvailabilityQueryUnavailability2b() {
47+
AssertParse(
48+
"""
49+
if !1️⃣#unavailable(OSX 10.52) {
3850
}
39-
if let _ = Optional(5), 5️⃣!6️⃣#unavailable(OSX 10.52) {
51+
""",
52+
diagnostics: [
53+
DiagnosticSpec(message: "availability condition cannot be used in an expression; did you mean '#available'?", fixIts: ["replace '!#unavailable' by '#available'"])
54+
],
55+
fixedSource: """
56+
if #available(OSX 10.52) {
57+
}
58+
"""
59+
)
60+
}
61+
62+
func testAvailabilityQueryUnavailability2c() {
63+
AssertParse(
64+
"""
65+
if let _ = Optional(5), !1️⃣#unavailable(OSX 10.52) {
4066
}
4167
""",
4268
diagnostics: [
43-
// TODO: Old parser expected error on line 2: platform wildcard '*' is always implicit in #unavailable, Fix-It replacements: 28 - 29 = ''
44-
// TODO: Old parser expected error on line 4: #unavailable may only be used as condition of an 'if', 'guard'
45-
DiagnosticSpec(locationMarker: "1️⃣", message: "expected value in tuple"),
46-
DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected code '#unavailable(OSX 10.51)' in tuple"),
47-
// TODO: Old parser expected error on line 5: #unavailable may only be used as condition of
48-
DiagnosticSpec(locationMarker: "3️⃣", message: "expected expression in variable"),
49-
DiagnosticSpec(locationMarker: "3️⃣", message: "unexpected code before variable"),
50-
// TODO: Old parser expected error on line 6: #unavailable may only be used as condition of an
51-
// TODO: Old parser expected error on line 7: #unavailable may only be used as condition of an
52-
// TODO: Old parser expected error on line 9: #unavailable may only be used as condition
53-
DiagnosticSpec(locationMarker: "5️⃣", message: "expected pattern in variable"),
54-
DiagnosticSpec(locationMarker: "6️⃣", message: "expected expression in prefix operator expression"),
55-
DiagnosticSpec(locationMarker: "6️⃣", message: "extraneous code at top level"),
69+
DiagnosticSpec(message: "availability condition cannot be used in an expression; did you mean '#available'?", fixIts: ["replace '!#unavailable' by '#available'"])
5670
]
5771
)
5872
}
@@ -459,23 +473,29 @@ final class AvailabilityQueryUnavailabilityTests: XCTestCase {
459473
)
460474
}
461475

462-
func testAvailabilityQueryUnavailability34() {
476+
func testAvailabilityQueryUnavailability34a() {
463477
AssertParse(
464478
"""
465479
// Diagnose wrong spellings of unavailability
466-
if #available(*) 1️⃣== false {
467-
}
468-
if !2️⃣#available(*) {
480+
if #available(*) 1️⃣== false {
469481
}
470482
""",
471483
diagnostics: [
472484
// 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 = ''
473-
DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected code '== false' in 'if' statement"),
474-
// TODO: Old parser expected error on line 4: #available cannot be used as an expression, did you mean to use '#unavailable'?, Fix-It replacements: 4 - 15 = '#unavailable'
475-
DiagnosticSpec(locationMarker: "2️⃣", message: "expected expression in prefix operator expression"),
476-
DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code '#available(*)' in 'if' statement"),
485+
DiagnosticSpec(message: "unexpected code '== false' in 'if' statement")
477486
]
478487
)
479488
}
480489

490+
func testAvailabilityQueryUnavailability34b() {
491+
AssertParse(
492+
"""
493+
if !1️⃣#available(*) {
494+
}
495+
""",
496+
diagnostics: [
497+
DiagnosticSpec(message: "availability condition cannot be used in an expression; did you mean '#unavailable'?", fixIts: ["replace '!#available' by '#unavailable'"])
498+
]
499+
)
500+
}
481501
}

0 commit comments

Comments
 (0)