Skip to content

Commit 6cfad27

Browse files
committed
Improve diagnostics for invalid version tuples
1 parent dd976be commit 6cfad27

File tree

4 files changed

+154
-62
lines changed

4 files changed

+154
-62
lines changed

Sources/SwiftParser/Availability.swift

Lines changed: 80 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -262,48 +262,96 @@ 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+
handle.unexpectedTokens > 0
296+
{
297+
for _ in 0..<handle.unexpectedTokens {
298+
unexpectedTokens.append(RawSyntax(self.consumeAnyToken()))
299+
}
300+
}
301+
return RawVersionTupleSyntax(
302+
RawUnexpectedNodesSyntax(unexpectedTokens, arena: self.arena),
303+
major: self.missingToken(.integerLiteral, text: nil),
304+
components: nil,
305+
arena: self.arena
306+
)
307+
}
308+
}
296309

297-
return RawVersionTupleSyntax(
298-
major: major,
299-
components: RawVersionComponentListSyntax(elements: components, arena: self.arena),
300-
unexpectedTrailingComponents,
301-
arena: self.arena
302-
)
310+
var unexpectedTrailingComponents: RawUnexpectedNodesSyntax?
311+
312+
if !trailingComponents.isEmpty {
313+
unexpectedTrailingComponents = RawUnexpectedNodesSyntax(elements: trailingComponents.compactMap { $0.as(RawSyntax.self) }, arena: self.arena)
314+
}
303315

316+
return RawVersionTupleSyntax(
317+
major: major,
318+
components: RawVersionComponentListSyntax(elements: components, arena: self.arena),
319+
unexpectedTrailingComponents,
320+
arena: self.arena
321+
)
322+
} else {
323+
let unexpectedToken = self.eat(.floatingLiteral)
324+
return RawVersionTupleSyntax(
325+
RawUnexpectedNodesSyntax([unexpectedToken], arena: self.arena),
326+
major: self.missingToken(.integerLiteral, text: nil),
327+
components: nil,
328+
arena: self.arena
329+
)
330+
}
304331
} else {
305332
let major = self.expectDecimalIntegerWithoutRecovery()
306-
return RawVersionTupleSyntax(major: major, components: nil, arena: self.arena)
333+
if major.isMissing {
334+
let unexpectedNodes: RawUnexpectedNodesSyntax?
335+
if let (_, handle) = self.canRecoverTo(anyIn: VersionTupleSyntax.EndOfVersionTupleOptions.self),
336+
handle.unexpectedTokens > 0
337+
{
338+
var unexpectedTokens = [RawSyntax]()
339+
for _ in 0..<handle.unexpectedTokens {
340+
unexpectedTokens.append(RawSyntax(self.consumeAnyToken()))
341+
}
342+
unexpectedNodes = RawUnexpectedNodesSyntax(elements: unexpectedTokens, arena: self.arena)
343+
} else {
344+
unexpectedNodes = nil
345+
}
346+
return RawVersionTupleSyntax(
347+
unexpectedNodes,
348+
major: major,
349+
components: nil,
350+
arena: self.arena
351+
)
352+
} else {
353+
return RawVersionTupleSyntax(major: major, components: nil, arena: self.arena)
354+
}
307355
}
308356
}
309357
}

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: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -142,34 +142,31 @@ final class AvailabilityTests: XCTestCase {
142142
)
143143
)
144144

145+
assertParse(
146+
"""
147+
@available(OSX 10.0.1, *)
148+
func test() {}
149+
"""
150+
)
151+
145152
assertParse(
146153
"""
147154
@available(OSX 1️⃣10e10)
148155
func test() {}
149156
""",
150157
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-
"""
158+
DiagnosticSpec(message: "cannot parse version 10e10")
159+
]
158160
)
159161

160162
assertParse(
161163
"""
162-
@available(OSX 10.1️⃣0e10)
164+
@available(OSX 1️⃣10.0e10)
163165
func test() {}
164166
""",
165167
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-
"""
168+
DiagnosticSpec(message: "cannot parse version 10.0e10")
169+
]
173170
)
174171

175172
assertParse(
@@ -178,28 +175,38 @@ final class AvailabilityTests: XCTestCase {
178175
func test() {}
179176
""",
180177
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-
"""
178+
DiagnosticSpec(message: "cannot parse version 0xff")
179+
]
180+
)
181+
182+
assertParse(
183+
"""
184+
@available(OSX 1️⃣1.0.0xff)
185+
func test() {}
186+
""",
187+
diagnostics: [
188+
DiagnosticSpec(message: "cannot parse version 1.0.0xff")
189+
]
190+
)
191+
192+
assertParse(
193+
"""
194+
@available(OSX 1️⃣1.0.0xff, *)
195+
func test() {}
196+
""",
197+
diagnostics: [
198+
DiagnosticSpec(message: "cannot parse version 1.0.0xff")
199+
]
188200
)
189201

190202
assertParse(
191203
"""
192-
@available(OSX 1.0.1️⃣0xff)
204+
@available(OSX 1️⃣1.0.0xff *)
193205
func test() {}
194206
""",
195207
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-
"""
208+
DiagnosticSpec(message: "cannot parse version 1.0.0xff *")
209+
]
203210
)
204211
}
205212
}

0 commit comments

Comments
 (0)