Skip to content

Commit 1739cb3

Browse files
committed
Refine diagnostics for #if in @abi
This combination is not permitted, since `#if` can evaluate to zero or many declarations and cannot contain declaration headers (which will become supported in this position in a future PR).
1 parent 976f163 commit 1739cb3

File tree

5 files changed

+258
-0
lines changed

5 files changed

+258
-0
lines changed

Sources/SwiftParser/Declarations.swift

+25
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,17 @@ extension Parser {
205205
return .weakBracketed(closingDelimiter: .rightParen)
206206
}
207207
}
208+
209+
/// Is `#if` allowed in this context? If not, we parse it into unexpected syntax on whatever declaration is nested
210+
/// inside it.
211+
var allowsIfConfigDecl: Bool {
212+
switch self {
213+
case .topLevelOrCodeBlock, .memberDeclList:
214+
return true
215+
case .argumentList:
216+
return false
217+
}
218+
}
208219
}
209220

210221
/// Parse a declaration.
@@ -239,6 +250,20 @@ extension Parser {
239250
} syntax: { parser, elements in
240251
return .decls(RawMemberBlockItemListSyntax(elements: elements, arena: parser.arena))
241252
}
253+
if !context.allowsIfConfigDecl {
254+
// Convert the IfConfig to unexpected syntax around the first decl inside it, if any.
255+
return directive.makeUnexpectedKeepingFirstNode(of: RawDeclSyntax.self, arena: self.arena) { node in
256+
return !node.is(RawIfConfigDeclSyntax.self)
257+
} makeMissing: {
258+
return RawDeclSyntax(
259+
RawMissingDeclSyntax(
260+
attributes: self.emptyCollection(RawAttributeListSyntax.self),
261+
modifiers: self.emptyCollection(RawDeclModifierListSyntax.self),
262+
arena: self.arena
263+
)
264+
)
265+
}
266+
}
242267
return RawDeclSyntax(directive)
243268
}
244269

Sources/SwiftParser/SyntaxUtils.swift

+163
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,169 @@ extension RawUnexpectedNodesSyntax {
105105
}
106106
}
107107

108+
// MARK: - Converting nodes to unexpected nodes
109+
110+
extension RawSyntaxNodeProtocol {
111+
/// Select one node somewhere inside `self` and return a version of it with all of the other tokens in `self`
112+
/// converted to unexpected nodes.
113+
///
114+
/// This is the same as ``RawSyntaxNodeProtocol/makeUnexpectedKeepingNodes(of:arena:where:makeMissing:)`` except that
115+
/// it always keeps, and always returns, exactly one node (the first one that would be encountered in a depth-first
116+
/// search, or the missing node). The other nodes that would have been kept are converted to unexpected tokens as
117+
/// usual. See that method for details about the behavior.
118+
///
119+
/// - Parameters:
120+
/// - keptType: The type of node that should be kept and returned.
121+
/// - arena: The syntax arena to allocate new or copied nodes within.
122+
/// - predicate: Should return `true` for the node that should be kept. Allows more selectivity than `keptType`
123+
/// alone.
124+
/// - makeMissing: If there are no children which satisfy `keptType` and `predicate`, this closure will be called
125+
/// to create a substitute node for the unexpected syntax to be attached to. It's typically used to return a "missing syntax"
126+
/// node, though that's not a technical requirement.
127+
///
128+
/// - Returns: The kept node (or the missing node) with the other tokens in `self` attached to its leading and
129+
/// trailing unexpected node children.
130+
func makeUnexpectedKeepingFirstNode<KeptNode: RawSyntaxNodeProtocol>(
131+
of keptType: KeptNode.Type,
132+
arena: SyntaxArena,
133+
where predicate: (KeptNode) -> Bool,
134+
makeMissing: () -> KeptNode
135+
) -> KeptNode {
136+
var alreadyFoundFirst = false
137+
func compositePredicate(_ node: KeptNode) -> Bool {
138+
if alreadyFoundFirst || !predicate(node) {
139+
return false
140+
}
141+
alreadyFoundFirst = true
142+
return true
143+
}
144+
145+
return makeUnexpectedKeepingNodes(
146+
of: keptType,
147+
arena: arena,
148+
where: compositePredicate,
149+
makeMissing: makeMissing
150+
).first!
151+
}
152+
153+
/// Select a number of nodes inside `self` and return versions of them with all of the other tokens in`self`
154+
/// attached to them as unexpected nodes.
155+
///
156+
/// For instance, if you had a `RawIfConfigDeclSyntax` like this:
157+
///
158+
/// ```swift
159+
/// #if FOO
160+
/// func x() {}
161+
/// func y() {}
162+
/// #elseif BAR
163+
/// func a() {}
164+
/// func b() {}
165+
/// #endif
166+
/// ```
167+
///
168+
/// Then a call like this:
169+
///
170+
/// ```swift
171+
/// let functions = directive.makeUnexpectedKeepingNodes(
172+
/// of: RawFunctionDeclSyntax.self,
173+
/// arena: parser.arena,
174+
/// where: { node in true },
175+
/// makeMissing: { RawFunctionDeclSyntax(...) }
176+
/// )
177+
/// ```
178+
///
179+
/// Would return an array of four `RawFunctionDeclSyntax` nodes with the tokens for `#if FOO`, `#elseif BAR`, and
180+
/// `#endif` added to the nodes for `x()`, `a()`, and `b()` respectively.
181+
///
182+
/// Specifically, this method performs a depth-first recursive search of the children of `self`, collecting nodes of
183+
/// type `keptType` for which `predicate` returns `true`. These nodes are then modified to add all of the tokens
184+
/// within `self`, but outside of the kept nodes, to their leading or trailing `RawUnexpectedNodesSyntax` child. If
185+
/// no kept nodes are found, the `makeMissing` closure is used to create a "missing syntax" node the tokens can be
186+
/// attached to.
187+
///
188+
/// Tokens are usually added to the leading unexpected node child of the next kept node, except for tokens after the
189+
/// last kept node, which are added to the trailing unexpected node child of the last kept node.
190+
///
191+
/// Token and collection nodes cannot be kept, as they cannot have unexpected syntax attached to them; however,
192+
/// parents of such nodes can be kept.
193+
///
194+
/// - Parameters:
195+
/// - keptType: The type of node that should be kept and returned in the array.
196+
/// - arena: The syntax arena to allocate new or copied nodes within.
197+
/// - predicate: Should return `true` for nodes that should be kept. Allows more selectivity than `keptType` alone.
198+
/// - makeMissing: If there are no children which satisfy `keptType` and `predicate`, this closure will be called
199+
/// to create a substitute node for the unexpected syntax to be attached to. It's typically used to return a "missing syntax"
200+
/// node, though that's not a technical requirement.
201+
///
202+
/// - Returns: The kept nodes (or the missing node) with the other tokens in `self` attached to them at appropriate
203+
/// unexpected node children. Note that there is always at least one node in the array.
204+
func makeUnexpectedKeepingNodes<KeptNode: RawSyntaxNodeProtocol>(
205+
of keptType: KeptNode.Type,
206+
arena: SyntaxArena,
207+
where predicate: (KeptNode) -> Bool,
208+
makeMissing: () -> KeptNode
209+
) -> [KeptNode] {
210+
var keptNodes: [KeptNode] = []
211+
var accumulatedTokens: [RawTokenSyntax] = []
212+
213+
func attachAccumulatedTokensToLastKeptNode(appending: Bool) {
214+
if accumulatedTokens.isEmpty {
215+
// Nothing to add? Nothing to do.
216+
return
217+
}
218+
219+
let lastKeptNodeIndex = keptNodes.endIndex - 1
220+
let lastKeptNode = keptNodes[lastKeptNodeIndex].raw.layoutView!
221+
222+
let childIndex = appending ? lastKeptNode.children.endIndex - 1 : 0
223+
224+
let newUnexpected: RawUnexpectedNodesSyntax
225+
if let oldUnexpected = lastKeptNode.children[childIndex]?.cast(RawUnexpectedNodesSyntax.self) {
226+
if appending {
227+
newUnexpected = RawUnexpectedNodesSyntax(combining: oldUnexpected, accumulatedTokens, arena: arena)!
228+
} else {
229+
newUnexpected = RawUnexpectedNodesSyntax(combining: accumulatedTokens, oldUnexpected, arena: arena)!
230+
}
231+
} else {
232+
newUnexpected = RawUnexpectedNodesSyntax(accumulatedTokens, arena: arena)!
233+
}
234+
235+
keptNodes[lastKeptNodeIndex] = lastKeptNode.replacingChild(
236+
at: childIndex,
237+
with: newUnexpected.raw,
238+
arena: arena
239+
).cast(KeptNode.self)
240+
241+
accumulatedTokens.removeAll()
242+
}
243+
244+
func walk(_ raw: RawSyntax) {
245+
if let token = RawTokenSyntax(raw) {
246+
if !token.isMissing {
247+
accumulatedTokens.append(token)
248+
}
249+
} else if !raw.kind.isSyntaxCollection, let node = raw.as(KeptNode.self), predicate(node) {
250+
keptNodes.append(node)
251+
attachAccumulatedTokensToLastKeptNode(appending: false)
252+
} else {
253+
for case let child? in raw.layoutView!.children {
254+
walk(child)
255+
}
256+
}
257+
}
258+
259+
walk(self.raw)
260+
261+
if keptNodes.isEmpty {
262+
keptNodes.append(makeMissing())
263+
}
264+
265+
attachAccumulatedTokensToLastKeptNode(appending: true)
266+
267+
return keptNodes
268+
}
269+
}
270+
108271
// MARK: - Misc
109272

110273
extension SyntaxText {

Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift

+21
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,27 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
476476
SpaceSeparatedIdentifiersError(firstToken: previousToken, additionalTokens: tokens),
477477
fixIts: fixIts
478478
)
479+
} else if let parent = node.parent,
480+
node.firstToken(viewMode: .sourceAccurate)?.tokenKind == .poundIf,
481+
let otherNode = parent.children(viewMode: .sourceAccurate).last?.as(UnexpectedNodesSyntax.self),
482+
otherNode.lastToken(viewMode: .sourceAccurate)?.tokenKind == .poundEndif
483+
{
484+
let diagnoseOn = parent.parent ?? parent
485+
addDiagnostic(
486+
diagnoseOn,
487+
IfConfigDeclNotAllowedInContext(context: diagnoseOn),
488+
highlights: [Syntax(node), Syntax(otherNode)],
489+
fixIts: [
490+
FixIt(
491+
message: RemoveNodesFixIt([Syntax(node), Syntax(otherNode)]),
492+
changes: [
493+
.makeMissing([Syntax(node)], transferTrivia: false),
494+
.makeMissing([Syntax(otherNode)], transferTrivia: false),
495+
]
496+
)
497+
],
498+
handledNodes: [node.id, otherNode.id]
499+
)
479500
} else {
480501
addDiagnostic(node, UnexpectedNodesError(unexpectedNodes: node), highlights: [Syntax(node)])
481502
}

Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift

+15
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,21 @@ public struct IdentifierNotAllowedInOperatorName: ParserError {
407407
}
408408
}
409409

410+
public struct IfConfigDeclNotAllowedInContext: ParserError {
411+
public let context: Syntax
412+
413+
private var contextDescription: String {
414+
if let description = context.ancestorOrSelf(mapping: { $0.nodeTypeNameForDiagnostics(allowBlockNames: true) }) {
415+
return "in \(description)"
416+
}
417+
return "here"
418+
}
419+
420+
public var message: String {
421+
return "conditional compilation not permitted \(contextDescription)"
422+
}
423+
}
424+
410425
public struct InvalidFloatLiteralMissingLeadingZero: ParserError {
411426
public let decimalDigits: TokenSyntax
412427

Tests/SwiftParserTest/AttributeTests.swift

+34
Original file line numberDiff line numberDiff line change
@@ -1293,6 +1293,40 @@ final class AttributeTests: ParserTestCase {
12931293
""",
12941294
experimentalFeatures: [.abiAttribute]
12951295
)
1296+
1297+
// `#if` is banned inside an `@abi` attribute.
1298+
// The code that generates feature checks in module interfaces could easily fail in ways that would generate this
1299+
// syntax, so we want to make sure we give a good diagnostic.
1300+
1301+
assertParse(
1302+
"""
1303+
@abi(
1304+
1️⃣#if $TypedThrows
1305+
func _fn<E: Error>() throws(E)
1306+
#endif
1307+
)
1308+
func fn<E: Error>() throws(E) {}
1309+
""",
1310+
diagnostics: [
1311+
DiagnosticSpec(
1312+
locationMarker: "1️⃣",
1313+
message: "conditional compilation not permitted in ABI-providing declaration",
1314+
highlight: """
1315+
1316+
#if $TypedThrows
1317+
#endif
1318+
""",
1319+
fixIts: ["remove '#if $TypedThrows' and '#endif'"]
1320+
)
1321+
],
1322+
fixedSource: """
1323+
@abi(
1324+
func _fn<E: Error>() throws(E)
1325+
)
1326+
func fn<E: Error>() throws(E) {}
1327+
""",
1328+
experimentalFeatures: [.abiAttribute]
1329+
)
12961330
}
12971331

12981332
func testSpaceBetweenAtAndAttribute() {

0 commit comments

Comments
 (0)