Skip to content

Commit c570aac

Browse files
committed
Fix a parameter label recovery edge case
When `parseFunctionParameter()` parses two argument labels and then doesn’t find a type, it applies a heuristic to decide whether to reinterpret the second label as a type (and recover by inserting a colon between the labels). The code in this path could drop unexpected nodes between the two labels. Correct this issue and, if the unexpected syntax includes a module selector, reconstruct it and attach it to the type. This was probably a pre-existing bug, but the module selector tests managed to hit it through mutation testing.
1 parent e1c3cf2 commit c570aac

File tree

3 files changed

+140
-35
lines changed

3 files changed

+140
-35
lines changed

Sources/SwiftParser/Names.swift

Lines changed: 79 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,11 @@ private enum StructuralTokens: TokenSpecSet {
6565
case rightAngle
6666

6767
init?(lexeme: Lexer.Lexeme, experimentalFeatures: Parser.ExperimentalFeatures) {
68-
switch lexeme.rawTokenKind {
68+
self.init(lexeme.rawTokenKind)
69+
}
70+
71+
init?(_ rawTokenKind: RawTokenKind) {
72+
switch rawTokenKind {
6973
case .comma: self = .comma
7074
case .colon: self = .colon
7175
case .leftParen: self = .leftParen
@@ -124,6 +128,10 @@ extension TokenConsumer {
124128
return operation(&self)
125129
}
126130

131+
typealias ModuleSelectorTokens = (
132+
moduleNameOrUnexpected: Token, colonColonToken: Token, extra: [Token]
133+
)
134+
127135
/// If the subsequent tokens have the form of a module selector, valid or otherwise, consume and return them;
128136
/// otherwise consume nothing and return `nil`. Additionally consumes invalid chained module selectors.
129137
///
@@ -134,9 +142,7 @@ extension TokenConsumer {
134142
/// - `colonColonToken`: The `::` indicating this module selector. Always `.colonColon`, always present.
135143
/// - `extra`: Tokens for additional trailing module selectors. There is no situation in which two module selectors
136144
/// can be validly chained.
137-
mutating func consumeModuleSelectorTokens() -> (
138-
moduleNameOrUnexpected: Token, colonColonToken: Token, extra: [Token]
139-
)? {
145+
mutating func consumeModuleSelectorTokens() -> ModuleSelectorTokens? {
140146
guard self.isAtModuleSelector() else {
141147
return nil
142148
}
@@ -166,61 +172,100 @@ extension TokenConsumer {
166172
}
167173
}
168174

169-
extension Parser {
170-
/// Parses one or more module selectors, if present.
171-
mutating func parseModuleSelector() -> RawModuleSelectorSyntax? {
172-
guard let (moduleNameOrUnexpected, colonColon, extra) = consumeModuleSelectorTokens() else {
173-
return nil
175+
private func tokens(in node: (some RawSyntaxNodeProtocol)?) -> [RawTokenSyntax] {
176+
var tokens: [RawTokenSyntax] = []
177+
178+
func collect(from child: (some RawSyntaxNodeProtocol)?) {
179+
guard let child else { return }
180+
181+
guard let token = child.as(RawTokenSyntax.self) else {
182+
for child in child.raw.layoutView!.children {
183+
collect(from: child)
184+
}
185+
return
174186
}
175187

188+
if !token.isMissing {
189+
tokens.append(token)
190+
}
191+
}
192+
193+
collect(from: node)
194+
195+
return tokens
196+
}
197+
198+
extension Parser {
199+
/// Helper to form a module selector from a set of tokens, divorced from their source.
200+
private mutating func makeModuleSelector(from tokens: ModuleSelectorTokens) -> RawModuleSelectorSyntax {
176201
let leadingUnexpected: [RawSyntax]
177202
let moduleName: RawTokenSyntax
178203
let trailingUnexpected: [RawSyntax]
179204

180-
if moduleNameOrUnexpected.tokenKind == .identifier {
205+
if tokens.moduleNameOrUnexpected.tokenKind == .identifier {
181206
leadingUnexpected = []
182-
moduleName = moduleNameOrUnexpected
207+
moduleName = tokens.moduleNameOrUnexpected
183208
} else {
184-
leadingUnexpected = [RawSyntax(moduleNameOrUnexpected)]
185-
moduleName = RawTokenSyntax.init(missing: .identifier, arena: arena)
209+
leadingUnexpected = [RawSyntax(tokens.moduleNameOrUnexpected)]
210+
moduleName = self.missingToken(.identifier)
186211
}
187212

188-
trailingUnexpected = extra.map { RawSyntax($0) }
213+
trailingUnexpected = tokens.extra.map { RawSyntax($0) }
189214

190215
return RawModuleSelectorSyntax(
191216
RawUnexpectedNodesSyntax(leadingUnexpected, arena: arena),
192217
moduleName: moduleName,
193-
colonColon: colonColon,
218+
colonColon: tokens.colonColonToken,
194219
RawUnexpectedNodesSyntax(trailingUnexpected, arena: arena),
195220
arena: arena
196221
)
197222
}
198223

199-
/// Convert a raw syntax node to `RawUnexpectedNodesSyntax` by extracting its tokens.
200-
func unexpected(_ node: (some RawSyntaxNodeProtocol)?) -> RawUnexpectedNodesSyntax? {
201-
var tokens: [RawTokenSyntax] = []
224+
/// Parses one or more module selectors, if present.
225+
mutating func parseModuleSelector() -> RawModuleSelectorSyntax? {
226+
guard let tokens = consumeModuleSelectorTokens() else {
227+
return nil
228+
}
229+
return makeModuleSelector(from: tokens)
230+
}
202231

203-
func collect(from child: (some RawSyntaxNodeProtocol)?) {
204-
guard let child else { return }
232+
/// If possible, convert a ``RawUnexpectedNodesSyntax`` back into a module selector. Used in some specialized
233+
/// recovery paths.
234+
mutating func recoverModuleSelector(
235+
from unexpected: RawUnexpectedNodesSyntax?
236+
) -> (unexpectedBeforeModuleSelector: RawUnexpectedNodesSyntax?, moduleSelector: RawModuleSelectorSyntax?) {
237+
guard let unexpected, unexpected.containsToken(where: { $0.tokenKind == .colonColon }) else {
238+
return (unexpected, nil)
239+
}
205240

206-
guard let token = child.as(RawTokenSyntax.self) else {
207-
for child in child.raw.layoutView!.children {
208-
collect(from: child)
209-
}
210-
return
211-
}
241+
var remainingTokens = tokens(in: unexpected)
242+
let selectorTokens: ModuleSelectorTokens
212243

213-
if !token.isMissing {
214-
tokens.append(token)
215-
}
216-
}
244+
// Everything after the first `::` should be either extra (invalid) module selectors or just random extra garbage
245+
// we skipped during recovery.
246+
let afterColonColonIndex = remainingTokens.firstIndex(where: { $0.tokenKind == .colonColon })! + 1
247+
selectorTokens.extra = Array(remainingTokens[afterColonColonIndex...])
248+
remainingTokens.removeSubrange(afterColonColonIndex...)
217249

218-
collect(from: node)
250+
selectorTokens.colonColonToken = remainingTokens.removeLast()
219251

220-
guard !tokens.isEmpty else {
221-
return nil
252+
// This matches logic from `consumeModuleSelectorTokens()` and `isAtModuleSelector()`.
253+
if let tokenBeforeColonColon = remainingTokens.last, StructuralTokens(tokenBeforeColonColon.tokenKind) != nil {
254+
selectorTokens.moduleNameOrUnexpected = tokenBeforeColonColon
255+
remainingTokens.removeLast()
256+
} else {
257+
selectorTokens.moduleNameOrUnexpected = self.missingToken(.identifier)
222258
}
223-
return RawUnexpectedNodesSyntax(tokens, arena: self.arena)
259+
260+
return (
261+
RawUnexpectedNodesSyntax(remainingTokens, arena: self.arena),
262+
self.makeModuleSelector(from: selectorTokens)
263+
)
264+
}
265+
266+
/// Convert a raw syntax node to `RawUnexpectedNodesSyntax` by extracting its tokens.
267+
func unexpected(_ node: (some RawSyntaxNodeProtocol)?) -> RawUnexpectedNodesSyntax? {
268+
return RawUnexpectedNodesSyntax(tokens(in: node), arena: self.arena)
224269
}
225270

226271
/// Inject `moduleSelector` into `node`, its first child, its first child's first child, etc. If necessary,

Sources/SwiftParser/Parameters.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,11 @@ extension Parser {
129129
secondName.tokenText.isStartingWithUppercase
130130
{
131131
// Synthesize the secondName parameter as a type node.
132+
let (stillUnexpected, moduleSelector) = recoverModuleSelector(from: names.unexpectedBeforeSecondName)
132133
type = RawTypeSyntax(
133134
RawIdentifierTypeSyntax(
134-
moduleSelector: nil,
135+
stillUnexpected,
136+
moduleSelector: moduleSelector,
135137
name: secondName,
136138
genericArgumentClause: nil,
137139
arena: self.arena

Tests/SwiftParserTest/translated/ModuleSelectorTests.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,64 @@ final class ModuleSelectorTests: ParserTestCase {
516516
) {}
517517
"""
518518
)
519+
assertParse(
520+
"""
521+
func decl1(
522+
1️⃣main::p12️⃣3️⃣< ::A
523+
) {}
524+
""",
525+
diagnostics: [
526+
DiagnosticSpec(
527+
locationMarker: "1️⃣",
528+
message: "parameter cannot be qualified with a module selector",
529+
fixIts: ["remove 'main::'"]
530+
),
531+
DiagnosticSpec(
532+
locationMarker: "2️⃣",
533+
message: "expected ':' in parameter",
534+
fixIts: ["insert ':'"]
535+
),
536+
DiagnosticSpec(
537+
locationMarker: "3️⃣",
538+
message: "'<' is not a valid identifier"
539+
),
540+
541+
],
542+
fixedSource: """
543+
func decl1(
544+
p1: < ::A
545+
) {}
546+
"""
547+
)
548+
assertParse(
549+
"""
550+
func decl1(
551+
1️⃣main::p12️⃣3️⃣> ::A
552+
) {}
553+
""",
554+
diagnostics: [
555+
DiagnosticSpec(
556+
locationMarker: "1️⃣",
557+
message: "parameter cannot be qualified with a module selector",
558+
fixIts: ["remove 'main::'"]
559+
),
560+
DiagnosticSpec(
561+
locationMarker: "2️⃣",
562+
message: "expected ':' in parameter",
563+
fixIts: ["insert ':'"]
564+
),
565+
DiagnosticSpec(
566+
locationMarker: "3️⃣",
567+
message: "'>' is not a valid identifier"
568+
),
569+
570+
],
571+
fixedSource: """
572+
func decl1(
573+
p1: > ::A
574+
) {}
575+
"""
576+
)
519577
assertParse(
520578
"""
521579
func decl1(

0 commit comments

Comments
 (0)