Skip to content

Commit 2209cc8

Browse files
committed
Improve diagnostics for invalid version tuples
1 parent dd976be commit 2209cc8

File tree

4 files changed

+148
-62
lines changed

4 files changed

+148
-62
lines changed

Sources/SwiftParser/Availability.swift

Lines changed: 81 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -262,48 +262,97 @@ extension Parser {
262262
/// version-list -> version-tuple-element version-list?
263263
/// version-tuple-element -> '.' interger-literal
264264
mutating func parseVersionTuple(maxComponentCount: Int) -> RawVersionTupleSyntax {
265-
if self.at(.floatingLiteral),
266-
let periodIndex = self.currentToken.tokenText.firstIndex(of: UInt8(ascii: ".")),
267-
self.currentToken.tokenText[0..<periodIndex].allSatisfy({ Unicode.Scalar($0).isDigit })
268-
{
269-
// The lexer generates a float literal '1.2' for the major and minor version.
270-
// Split it into two integers if possible
271-
let major = self.consumePrefix(SyntaxText(rebasing: self.currentToken.tokenText[0..<periodIndex]), as: .integerLiteral)
265+
if self.at(.floatingLiteral) {
272266

273-
var components: [RawVersionComponentSyntax] = []
274-
var trailingComponents: [RawVersionComponentSyntax] = []
267+
if let periodIndex = self.currentToken.tokenText.firstIndex(of: UInt8(ascii: ".")),
268+
self.currentToken.tokenText[0..<periodIndex].allSatisfy({ Unicode.Scalar($0).isDigit })
269+
{
270+
// The lexer generates a float literal '1.2' for the major and minor version.
271+
// Split it into two integers if possible
272+
let major = self.consumePrefix(SyntaxText(rebasing: self.currentToken.tokenText[0..<periodIndex]), as: .integerLiteral)
275273

276-
for i in 1... {
277-
guard let period = self.consume(if: .period) else {
278-
break
279-
}
280-
let version = self.expectDecimalIntegerWithoutRecovery()
274+
var components: [RawVersionComponentSyntax] = []
275+
var trailingComponents: [RawVersionComponentSyntax] = []
281276

282-
let versionComponent = RawVersionComponentSyntax(period: period, number: version, arena: self.arena)
277+
for i in 1... {
278+
guard let period = self.consume(if: .period) else {
279+
break
280+
}
281+
let version = self.expectDecimalIntegerWithoutRecovery()
283282

284-
if i < maxComponentCount {
285-
components.append(versionComponent)
286-
} else {
287-
trailingComponents.append(versionComponent)
288-
}
289-
}
283+
let versionComponent = RawVersionComponentSyntax(period: period, number: version, arena: self.arena)
290284

291-
var unexpectedTrailingComponents: RawUnexpectedNodesSyntax?
285+
if i < maxComponentCount {
286+
components.append(versionComponent)
287+
} else {
288+
trailingComponents.append(versionComponent)
289+
}
292290

293-
if !trailingComponents.isEmpty {
294-
unexpectedTrailingComponents = RawUnexpectedNodesSyntax(elements: trailingComponents.compactMap { $0.as(RawSyntax.self) }, arena: self.arena)
295-
}
291+
if versionComponent.hasError {
292+
let unexpectedComponents = components + trailingComponents
293+
var unexpectedTokens = [RawSyntax(major)] + unexpectedComponents.map(RawSyntax.init)
294+
if let (_, handle) = self.canRecoverTo(anyIn: VersionTupleSyntax.EndOfVersionTupleOptions.self) {
295+
for _ in 0..<handle.unexpectedTokens {
296+
unexpectedTokens.append(RawSyntax(self.consumeAnyToken()))
297+
}
298+
}
299+
return RawVersionTupleSyntax(
300+
RawUnexpectedNodesSyntax(unexpectedTokens, arena: self.arena),
301+
major: self.missingToken(.integerLiteral, text: nil),
302+
components: nil,
303+
arena: self.arena
304+
)
305+
}
306+
}
296307

297-
return RawVersionTupleSyntax(
298-
major: major,
299-
components: RawVersionComponentListSyntax(elements: components, arena: self.arena),
300-
unexpectedTrailingComponents,
301-
arena: self.arena
302-
)
308+
var unexpectedTrailingComponents: RawUnexpectedNodesSyntax?
303309

310+
if !trailingComponents.isEmpty {
311+
unexpectedTrailingComponents = RawUnexpectedNodesSyntax(elements: trailingComponents.compactMap { $0.as(RawSyntax.self) }, arena: self.arena)
312+
}
313+
314+
return RawVersionTupleSyntax(
315+
major: major,
316+
components: RawVersionComponentListSyntax(elements: components, arena: self.arena),
317+
unexpectedTrailingComponents,
318+
arena: self.arena
319+
)
320+
} else {
321+
let unexpectedToken = self.eat(.floatingLiteral)
322+
return RawVersionTupleSyntax(
323+
RawUnexpectedNodesSyntax([unexpectedToken], arena: self.arena),
324+
major: self.missingToken(.integerLiteral, text: nil),
325+
components: nil,
326+
arena: self.arena
327+
)
328+
}
304329
} else {
305330
let major = self.expectDecimalIntegerWithoutRecovery()
306-
return RawVersionTupleSyntax(major: major, components: nil, arena: self.arena)
331+
if major.hasError {
332+
let unexpectedNodes: RawUnexpectedNodesSyntax?
333+
var unexpectedTokens = [RawSyntax]()
334+
if !major.isEmpty {
335+
unexpectedTokens.append(RawSyntax(major))
336+
}
337+
if let (_, handle) = self.canRecoverTo(anyIn: VersionTupleSyntax.EndOfVersionTupleOptions.self) {
338+
for _ in 0..<handle.unexpectedTokens {
339+
unexpectedTokens.append(RawSyntax(self.consumeAnyToken()))
340+
}
341+
}
342+
if !unexpectedTokens.isEmpty {
343+
unexpectedNodes = RawUnexpectedNodesSyntax(elements: unexpectedTokens, arena: self.arena)
344+
} else {
345+
unexpectedNodes = nil
346+
}
347+
return RawVersionTupleSyntax(
348+
unexpectedNodes,
349+
major: self.missingToken(.integerLiteral, text: nil),
350+
components: nil,
351+
arena: self.arena
352+
)
353+
} else {
354+
return RawVersionTupleSyntax(major: major, components: nil, arena: self.arena)
355+
}
307356
}
308357
}
309358
}

Sources/SwiftParser/generated/Parser+TokenSpecSet.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2084,3 +2084,30 @@ extension VariableDeclSyntax {
20842084
}
20852085
}
20862086
}
2087+
2088+
extension VersionTupleSyntax {
2089+
enum EndOfVersionTupleOptions: TokenSpecSet {
2090+
case comma
2091+
case rightParen
2092+
2093+
init?(lexeme: Lexer.Lexeme) {
2094+
switch PrepareForKeywordMatch(lexeme) {
2095+
case TokenSpec(.comma):
2096+
self = .comma
2097+
case TokenSpec(.rightParen):
2098+
self = .rightParen
2099+
default:
2100+
return nil
2101+
}
2102+
}
2103+
2104+
var spec: TokenSpec {
2105+
switch self {
2106+
case .comma:
2107+
return .comma
2108+
case .rightParen:
2109+
return .rightParen
2110+
}
2111+
}
2112+
}
2113+
}

Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1893,6 +1893,16 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
18931893
return .skipChildren
18941894
}
18951895

1896+
if let unexpectedBeforeMajor = node.unexpectedBeforeMajor,
1897+
node.major.isMissing
1898+
{
1899+
addDiagnostic(
1900+
unexpectedBeforeMajor,
1901+
CannotParseVersionTuple(versionTuple: unexpectedBeforeMajor),
1902+
handledNodes: [unexpectedBeforeMajor.id, node.major.id]
1903+
)
1904+
}
1905+
18961906
if let trailingComponents = node.unexpectedAfterComponents,
18971907
let components = node.components
18981908
{

Tests/SwiftParserTest/AvailabilityTests.swift

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -148,28 +148,18 @@ final class AvailabilityTests: XCTestCase {
148148
func test() {}
149149
""",
150150
diagnostics: [
151-
DiagnosticSpec(message: "expected version tuple in version restriction", fixIts: ["insert version tuple"]),
152-
DiagnosticSpec(message: "unexpected code '10e10' in attribute"),
153-
],
154-
fixedSource: """
155-
@available(OSX <#integer literal#>10e10)
156-
func test() {}
157-
"""
151+
DiagnosticSpec(message: "cannot parse version 10e10")
152+
]
158153
)
159154

160155
assertParse(
161156
"""
162-
@available(OSX 10.1️⃣0e10)
157+
@available(OSX 1️⃣10.0e10)
163158
func test() {}
164159
""",
165160
diagnostics: [
166-
DiagnosticSpec(message: "expected integer literal in version tuple", fixIts: ["insert integer literal"]),
167-
DiagnosticSpec(message: "unexpected code '0e10' in attribute"),
168-
],
169-
fixedSource: """
170-
@available(OSX 10.<#integer literal#>0e10)
171-
func test() {}
172-
"""
161+
DiagnosticSpec(message: "cannot parse version 10.0e10")
162+
]
173163
)
174164

175165
assertParse(
@@ -178,28 +168,38 @@ final class AvailabilityTests: XCTestCase {
178168
func test() {}
179169
""",
180170
diagnostics: [
181-
DiagnosticSpec(message: "expected version tuple in version restriction", fixIts: ["insert version tuple"]),
182-
DiagnosticSpec(message: "unexpected code '0xff' in attribute"),
183-
],
184-
fixedSource: """
185-
@available(OSX <#integer literal#>0xff)
186-
func test() {}
187-
"""
171+
DiagnosticSpec(message: "cannot parse version 0xff")
172+
]
188173
)
189174

190175
assertParse(
191176
"""
192-
@available(OSX 1.0.1️⃣0xff)
177+
@available(OSX 1️⃣1.0.0xff)
193178
func test() {}
194179
""",
195180
diagnostics: [
196-
DiagnosticSpec(message: "expected integer literal in version tuple", fixIts: ["insert integer literal"]),
197-
DiagnosticSpec(message: "unexpected code '0xff' in attribute"),
198-
],
199-
fixedSource: """
200-
@available(OSX 1.0.<#integer literal#>0xff)
201-
func test() {}
202-
"""
181+
DiagnosticSpec(message: "cannot parse version 1.0.0xff")
182+
]
183+
)
184+
185+
assertParse(
186+
"""
187+
@available(OSX 1️⃣1.0.0xff, *)
188+
func test() {}
189+
""",
190+
diagnostics: [
191+
DiagnosticSpec(message: "cannot parse version 1.0.0xff")
192+
]
193+
)
194+
195+
assertParse(
196+
"""
197+
@available(OSX 1️⃣1.0.0xff *)
198+
func test() {}
199+
""",
200+
diagnostics: [
201+
DiagnosticSpec(message: "cannot parse version 1.0.0xff *")
202+
]
203203
)
204204
}
205205
}

0 commit comments

Comments
 (0)