Skip to content

Commit 3e8deb8

Browse files
committed
Parse some P? and any P? as (some P)? and (any P)?.
1 parent aacb56b commit 3e8deb8

6 files changed

Lines changed: 447 additions & 31 deletions

File tree

Sources/SwiftParser/Types.swift

Lines changed: 133 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -126,23 +126,54 @@ extension Parser {
126126

127127
let someOrAny = self.consume(if: .keyword(.some), .keyword(.any))
128128

129-
var base = self.parseSimpleType()
130-
guard self.atContextualPunctuator("&") else {
131-
if let someOrAny {
132-
return RawTypeSyntax(
133-
RawSomeOrAnyTypeSyntax(
134-
someOrAnySpecifier: someOrAny,
135-
constraint: base,
129+
// Consumes all trailing optional sugar (`?` or `!`), wrapping the base type
130+
// in the correct optional or IUO syntax node types.
131+
func wrapWhileConsumingOptionalSugar(_ base: RawTypeSyntax) -> RawTypeSyntax {
132+
var base = base
133+
var loopProgress = LoopProgressCondition()
134+
135+
func tagWithDiagnosticIfAmbiguous(_ token: RawTokenSyntax) -> RawTokenSyntax {
136+
guard base.as(RawSomeOrAnyTypeSyntax.self)?.constraint.is(RawCompositionTypeSyntax.self) == true else {
137+
return token
138+
}
139+
return token.tokenView.withTokenDiagnostic(
140+
tokenDiagnostic: TokenDiagnostic(.ambiguousOptionalAfterSomeOrAnyComposition, byteOffset: 0),
141+
arena: self.arena
142+
)
143+
}
144+
145+
while self.hasProgressed(&loopProgress) {
146+
if self.at(.postfixQuestionMark) {
147+
var optional = self.parseOptionalType(base)
148+
optional = RawOptionalTypeSyntax(
149+
wrappedType: optional.wrappedType,
150+
questionMark: tagWithDiagnosticIfAmbiguous(optional.questionMark),
136151
arena: self.arena
137152
)
138-
)
139-
} else {
140-
return base
153+
base = RawTypeSyntax(optional)
154+
} else if self.at(.exclamationMark) {
155+
var iuo = self.parseImplicitlyUnwrappedOptionalType(base)
156+
iuo = RawImplicitlyUnwrappedOptionalTypeSyntax(
157+
wrappedType: iuo.wrappedType,
158+
exclamationMark: tagWithDiagnosticIfAmbiguous(iuo.exclamationMark),
159+
arena: self.arena
160+
)
161+
base = RawTypeSyntax(iuo)
162+
} else {
163+
break
164+
}
141165
}
166+
return base
142167
}
143168

144-
var elements = [RawCompositionTypeElementSyntax]()
169+
var base = self.parseSimpleType(allowTrailingOptional: someOrAny == nil)
170+
171+
// This is the happy path for compositions, such as `P & Q` or `some P & Q`.
172+
// If we have a composition that is interrupted by trailing optional sugar
173+
// (e.g., `some P? & Q`), this check fails and we fall through to the
174+
// recovery path below.
145175
if let firstAmpersand = self.consumeIfContextualPunctuator("&") {
176+
var elements = [RawCompositionTypeElementSyntax]()
146177
elements.append(
147178
RawCompositionTypeElementSyntax(
148179
type: base,
@@ -154,7 +185,32 @@ extension Parser {
154185
var keepGoing: RawTokenSyntax? = nil
155186
var loopProgress = LoopProgressCondition()
156187
repeat {
157-
let elementType = self.parseSimpleType()
188+
var elementType: RawTypeSyntax
189+
if someOrAny == nil {
190+
elementType = self.parseSimpleType()
191+
} else {
192+
// If trailing `?` or `!` is present, determine whether it should bind
193+
// to the element type instead of the entire `some`/`any` type. This
194+
// happens in two cases:
195+
//
196+
// 1. The optional is the base of a member type construction, like
197+
// `some P?.Q`.
198+
// 2. The user wrote the invalid type construction `some P? & Q`,
199+
// which we still want to parse syntactically as a composition for
200+
// recovery purposes; the type checker will reject it.
201+
elementType = self.parseSimpleType(allowTrailingOptional: false)
202+
let shouldBindOptionalToElement = self.withLookahead {
203+
var hasOptional = false
204+
while $0.consume(if: .postfixQuestionMark, .exclamationMark) != nil {
205+
hasOptional = true
206+
}
207+
return hasOptional && ($0.atContextualPunctuator("&") || $0.at(.period))
208+
}
209+
if shouldBindOptionalToElement {
210+
elementType = wrapWhileConsumingOptionalSugar(elementType)
211+
}
212+
}
213+
158214
keepGoing = self.consumeIfContextualPunctuator("&")
159215
elements.append(
160216
RawCompositionTypeElementSyntax(
@@ -173,33 +229,72 @@ extension Parser {
173229
)
174230
}
175231

232+
// If `some`/`any` was present, apply it to the base, and *then* apply any
233+
// trailing optionals.
176234
if let someOrAny {
177-
return RawTypeSyntax(
235+
base = RawTypeSyntax(
178236
RawSomeOrAnyTypeSyntax(
179237
someOrAnySpecifier: someOrAny,
180238
constraint: base,
181239
arena: self.arena
182240
)
183241
)
184-
} else {
185-
return base
242+
base = wrapWhileConsumingOptionalSugar(base)
243+
}
244+
245+
// Recovery path for broken compositions, like `some P? & Q ...`: consume
246+
// the rest of the composition with the `some` already applied to the first
247+
// element (`(some P)? & Q ...`). This ensures we produce a reasonable AST,
248+
// and the type checker will reject it.
249+
if let firstAmpersand = self.consumeIfContextualPunctuator("&") {
250+
var elements = [RawCompositionTypeElementSyntax]()
251+
elements.append(
252+
RawCompositionTypeElementSyntax(
253+
type: base,
254+
ampersand: firstAmpersand,
255+
arena: self.arena
256+
)
257+
)
258+
259+
var keepGoing: RawTokenSyntax? = nil
260+
var loopProgress = LoopProgressCondition()
261+
repeat {
262+
let elementType = self.parseSimpleType()
263+
keepGoing = self.consumeIfContextualPunctuator("&")
264+
elements.append(
265+
RawCompositionTypeElementSyntax(
266+
type: elementType,
267+
ampersand: keepGoing,
268+
arena: self.arena
269+
)
270+
)
271+
} while keepGoing != nil && self.hasProgressed(&loopProgress)
272+
273+
base = RawTypeSyntax(
274+
RawCompositionTypeSyntax(
275+
elements: RawCompositionTypeElementListSyntax(elements: elements, arena: self.arena),
276+
arena: self.arena
277+
)
278+
)
186279
}
280+
281+
return base
187282
}
188283

189284
/// Parse the subset of types that we allow in attribute names.
190285
mutating func parseAttributeName() -> RawTypeSyntax {
191-
return parseSimpleType(forAttributeName: true)
286+
return parseSimpleType(allowTrailingOptional: false)
192287
}
193288

194289
mutating func parseSimpleType(
195290
allowMemberTypes: Bool = true,
196-
forAttributeName: Bool = false
291+
allowTrailingOptional: Bool = true
197292
) -> RawTypeSyntax {
198293
let tilde = self.consumeIfContextualPunctuator("~", remapping: .prefixOperator)
199294

200295
let baseType = self.parseUnsuppressedSimpleType(
201296
allowMemberTypes: allowMemberTypes,
202-
forAttributeName: forAttributeName
297+
allowTrailingOptional: allowTrailingOptional
203298
)
204299

205300
guard let tilde else {
@@ -213,7 +308,7 @@ extension Parser {
213308

214309
mutating func parseUnsuppressedSimpleType(
215310
allowMemberTypes: Bool = true,
216-
forAttributeName: Bool = false
311+
allowTrailingOptional: Bool = true
217312
) -> RawTypeSyntax {
218313
enum TypeBaseStart: TokenSpecSet {
219314
case `Self`
@@ -319,18 +414,25 @@ extension Parser {
319414
continue
320415
}
321416

322-
// Do not allow ? or ! suffixes when parsing attribute names.
323-
if forAttributeName {
324-
break
325-
}
326-
327-
if self.at(TokenSpec(.postfixQuestionMark, allowAtStartOfLine: false)) {
328-
base = RawTypeSyntax(self.parseOptionalType(base))
329-
continue
330-
}
331-
if self.at(TokenSpec(.exclamationMark, allowAtStartOfLine: false)) {
332-
base = RawTypeSyntax(self.parseImplicitlyUnwrappedOptionalType(base))
333-
continue
417+
if self.at(.postfixQuestionMark, .exclamationMark) {
418+
// Even if we're not generally allowing trailing optional sugar (e.g.,
419+
// at the end of a `some` type), we still need to consume them if they
420+
// are followed by a member access (`.`), because we need to support
421+
// valid types like `some P?.Q`.
422+
let shouldConsume =
423+
allowTrailingOptional
424+
|| self.withLookahead {
425+
$0.consumeAnyToken()
426+
return $0.at(.period)
427+
}
428+
if shouldConsume {
429+
if self.at(.postfixQuestionMark) {
430+
base = RawTypeSyntax(self.parseOptionalType(base))
431+
} else {
432+
base = RawTypeSyntax(self.parseImplicitlyUnwrappedOptionalType(base))
433+
}
434+
continue
435+
}
334436
}
335437

336438
break

Sources/SwiftParserDiagnostics/LexerDiagnosticMessages.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,8 @@ extension SwiftSyntax.TokenDiagnostic {
197197
}
198198

199199
switch self.kind {
200+
case .ambiguousOptionalAfterSomeOrAnyComposition:
201+
return StaticParserError.ambiguousOptionalComposition
200202
case .editorPlaceholder: return StaticTokenError.editorPlaceholder
201203
case .equalMustHaveConsistentWhitespaceOnBothSides:
202204
return StaticTokenError.equalMustHaveConsistentWhitespaceOnBothSides

Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1450,6 +1450,80 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
14501450
return handleMissingSyntax(node, additionalHandledNodes: [node.placeholder.id])
14511451
}
14521452

1453+
public override func visit(_ node: OptionalTypeSyntax) -> SyntaxVisitorContinueKind {
1454+
if handledNodes.contains(node.id) {
1455+
return .skipChildren
1456+
}
1457+
1458+
if let someOrAny = node.wrappedType.as(SomeOrAnyTypeSyntax.self),
1459+
let composition = someOrAny.constraint.as(CompositionTypeSyntax.self)
1460+
{
1461+
let parenthesizedComposition = TupleTypeSyntax(
1462+
elements: TupleTypeElementListSyntax([
1463+
TupleTypeElementSyntax(type: composition)
1464+
])
1465+
)
1466+
1467+
addDiagnostic(
1468+
node,
1469+
position: node.questionMark.positionAfterSkippingLeadingTrivia,
1470+
.ambiguousOptionalComposition,
1471+
fixIts: [
1472+
FixIt(
1473+
message: .parenthesizeComposition,
1474+
changes: [
1475+
.replace(
1476+
oldNode: Syntax(composition),
1477+
newNode: Syntax(parenthesizedComposition)
1478+
)
1479+
]
1480+
)
1481+
],
1482+
handledNodes: [node.id, node.questionMark.id]
1483+
)
1484+
} else {
1485+
handledNodes.append(node.id)
1486+
}
1487+
return .visitChildren
1488+
}
1489+
1490+
public override func visit(_ node: ImplicitlyUnwrappedOptionalTypeSyntax) -> SyntaxVisitorContinueKind {
1491+
if handledNodes.contains(node.id) {
1492+
return .skipChildren
1493+
}
1494+
1495+
if let someOrAny = node.wrappedType.as(SomeOrAnyTypeSyntax.self),
1496+
let composition = someOrAny.constraint.as(CompositionTypeSyntax.self)
1497+
{
1498+
let parenthesizedComposition = TupleTypeSyntax(
1499+
elements: TupleTypeElementListSyntax([
1500+
TupleTypeElementSyntax(type: composition)
1501+
])
1502+
)
1503+
1504+
addDiagnostic(
1505+
node,
1506+
position: node.exclamationMark.positionAfterSkippingLeadingTrivia,
1507+
.ambiguousOptionalComposition,
1508+
fixIts: [
1509+
FixIt(
1510+
message: .parenthesizeComposition,
1511+
changes: [
1512+
.replace(
1513+
oldNode: Syntax(composition),
1514+
newNode: Syntax(parenthesizedComposition)
1515+
)
1516+
]
1517+
)
1518+
],
1519+
handledNodes: [node.id, node.exclamationMark.id]
1520+
)
1521+
} else {
1522+
handledNodes.append(node.id)
1523+
}
1524+
return .visitChildren
1525+
}
1526+
14531527
override open func visit(_ node: OriginallyDefinedInAttributeArgumentsSyntax) -> SyntaxVisitorContinueKind {
14541528
if shouldSkip(node) {
14551529
return .skipChildren

Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ extension DiagnosticMessage where Self == StaticParserError {
101101
public static var allStatementsInSwitchMustBeCoveredByCase: Self {
102102
.init("all statements inside a switch must be covered by a 'case' or 'default' label")
103103
}
104+
public static var ambiguousOptionalComposition: Self {
105+
.init("confusing use of optional after a 'some' or 'any' composition; use parentheses to clarify precedence")
106+
}
104107
public static var associatedTypeCannotUsePack: Self {
105108
.init("associated types cannot be variadic")
106109
}
@@ -721,6 +724,9 @@ extension FixItMessage where Self == StaticParserFixIt {
721724
public static var insertExtraClosingPounds: Self {
722725
.init("insert additional closing '#' delimiters")
723726
}
727+
public static var parenthesizeComposition: Self {
728+
.init("parenthesize composition")
729+
}
724730
public static var removeExtraneousWhitespace: Self {
725731
.init("remove whitespace")
726732
}

Sources/SwiftSyntax/TokenDiagnostic.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public struct TokenDiagnostic: Hashable, Sendable {
2323
public enum Kind: Sendable {
2424
// Please order these alphabetically
2525

26+
case ambiguousOptionalAfterSomeOrAnyComposition
2627
case editorPlaceholder
2728
case equalMustHaveConsistentWhitespaceOnBothSides
2829
case expectedBinaryExponentInHexFloatLiteral
@@ -67,6 +68,7 @@ public struct TokenDiagnostic: Hashable, Sendable {
6768
/// The severity of the diagnostic, i.e. whether it’s a warning or error.
6869
var severity: Severity {
6970
switch self {
71+
case .ambiguousOptionalAfterSomeOrAnyComposition: return .warning
7072
case .editorPlaceholder: return .error
7173
case .equalMustHaveConsistentWhitespaceOnBothSides: return .error
7274
case .expectedBinaryExponentInHexFloatLiteral: return .error

0 commit comments

Comments
 (0)