Skip to content

Commit 2b44b9c

Browse files
committed
Support module selectors in scoped imports
1 parent d3ed803 commit 2b44b9c

File tree

9 files changed

+213
-23
lines changed

9 files changed

+213
-23
lines changed

CodeGeneration/Sources/SyntaxSupport/DeclNodes.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public let DECL_NODES: [Node] = [
2828
),
2929
Child(
3030
name: "trailingPeriod",
31-
kind: .token(choices: [.token(.period)]),
31+
kind: .token(choices: [.token(.period), .token(.colonColon)]),
3232
isOptional: true
3333
),
3434
],

CodeGeneration/Tests/ValidateSyntaxNodes/ValidateSyntaxNodes.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -357,10 +357,6 @@ class ValidateSyntaxNodes: XCTestCase {
357357
"child 'leadingComma' has a comma keyword as its only token choice and should thus be named 'comma' or 'trailingComma'"
358358
),
359359
// This is similar to `TrailingComma`
360-
ValidationFailure(
361-
node: .importPathComponent,
362-
message: "child 'trailingPeriod' has a token as its only token choice and should thus be named 'period'"
363-
),
364360
// `~` is the only operator that’s allowed here
365361
ValidationFailure(
366362
node: .suppressedType,

Sources/SwiftParser/Declarations.swift

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ extension Parser {
435435
) -> RawImportDeclSyntax {
436436
let (unexpectedBeforeImportKeyword, importKeyword) = self.eat(handle)
437437
let kind = self.parseImportKind()
438-
let path = self.parseImportPath()
438+
let path = self.parseImportPath(hasImportKind: kind != nil)
439439
return RawImportDeclSyntax(
440440
attributes: attrs.attributes,
441441
modifiers: attrs.modifiers,
@@ -451,21 +451,67 @@ extension Parser {
451451
return self.consume(ifAnyIn: ImportDeclSyntax.ImportKindSpecifierOptions.self)
452452
}
453453

454-
mutating func parseImportPath() -> RawImportPathComponentListSyntax {
454+
mutating func parseImportPath(hasImportKind: Bool) -> RawImportPathComponentListSyntax {
455455
var elements = [RawImportPathComponentSyntax]()
456-
var keepGoing: RawTokenSyntax? = nil
457-
var loopProgress = LoopProgressCondition()
458-
repeat {
459-
let name = self.parseAnyIdentifier()
460-
keepGoing = self.consume(if: .period)
461-
elements.append(
456+
457+
// Special case: scoped import with module selector-style syntax. This always has exactly two path components
458+
// separated by '::'.
459+
if hasImportKind,
460+
let (moduleNameOrUnexpected, colonColon, unexpectedAfterColonColon) = self.consumeModuleSelectorTokensIfPresent()
461+
{
462+
// Is the token in module name position really a module name?
463+
let unexpectedBeforeModuleName: RawUnexpectedNodesSyntax?
464+
let moduleName: RawTokenSyntax
465+
if moduleNameOrUnexpected.tokenKind == .identifier {
466+
unexpectedBeforeModuleName = nil
467+
moduleName = moduleNameOrUnexpected
468+
} else {
469+
unexpectedBeforeModuleName = RawUnexpectedNodesSyntax([moduleNameOrUnexpected], arena: self.arena)
470+
moduleName = self.missingToken(.identifier)
471+
}
472+
473+
let declName = self.parseAnyIdentifier()
474+
475+
elements = [
462476
RawImportPathComponentSyntax(
463-
name: name,
464-
trailingPeriod: keepGoing,
477+
unexpectedBeforeModuleName,
478+
name: moduleName,
479+
trailingPeriod: colonColon,
480+
RawUnexpectedNodesSyntax(unexpectedAfterColonColon, arena: self.arena),
465481
arena: self.arena
482+
),
483+
RawImportPathComponentSyntax(
484+
name: declName,
485+
trailingPeriod: nil,
486+
arena: self.arena
487+
),
488+
]
489+
} else {
490+
var keepGoing: RawTokenSyntax? = nil
491+
var loopProgress = LoopProgressCondition()
492+
repeat {
493+
let name = self.parseAnyIdentifier()
494+
keepGoing = self.consume(if: .period)
495+
496+
// '::' is not valid if we got here, but someone might try to use it anyway.
497+
let unexpectedAfterTrailingPeriod: RawUnexpectedNodesSyntax?
498+
if keepGoing == nil, let colonColon = self.consume(if: .colonColon) {
499+
unexpectedAfterTrailingPeriod = RawUnexpectedNodesSyntax([colonColon], arena: self.arena)
500+
keepGoing = self.missingToken(.period)
501+
} else {
502+
unexpectedAfterTrailingPeriod = nil
503+
}
504+
505+
elements.append(
506+
RawImportPathComponentSyntax(
507+
name: name,
508+
trailingPeriod: keepGoing,
509+
unexpectedAfterTrailingPeriod,
510+
arena: self.arena
511+
)
466512
)
467-
)
468-
} while keepGoing != nil && self.hasProgressed(&loopProgress)
513+
} while keepGoing != nil && self.hasProgressed(&loopProgress)
514+
}
469515
return RawImportPathComponentListSyntax(elements: elements, arena: self.arena)
470516
}
471517
}

Sources/SwiftParser/generated/Parser+TokenSpecSet.swift

Lines changed: 52 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1264,6 +1264,40 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
12641264
return .visitChildren
12651265
}
12661266

1267+
public override func visit(_ node: ImportPathComponentSyntax) -> SyntaxVisitorContinueKind {
1268+
if shouldSkip(node) {
1269+
return .skipChildren
1270+
}
1271+
1272+
if let colonColon = node.unexpectedAfterTrailingPeriod?.first?.as(TokenSyntax.self),
1273+
colonColon.tokenKind == .colonColon,
1274+
colonColon.isPresent,
1275+
let trailingPeriod = node.trailingPeriod,
1276+
trailingPeriod.tokenKind == .period,
1277+
trailingPeriod.isMissing
1278+
{
1279+
addDiagnostic(
1280+
colonColon,
1281+
.submoduleCannotBeImportedUsingModuleSelector,
1282+
fixIts: [
1283+
FixIt(
1284+
message: ReplaceTokensFixIt(replaceTokens: [colonColon], replacements: [trailingPeriod]),
1285+
changes: [
1286+
.makeMissing(colonColon),
1287+
.makePresent(trailingPeriod),
1288+
]
1289+
)
1290+
],
1291+
handledNodes: [
1292+
colonColon.id,
1293+
trailingPeriod.id,
1294+
]
1295+
)
1296+
}
1297+
1298+
return .visitChildren
1299+
}
1300+
12671301
public override func visit(_ node: InitializerClauseSyntax) -> SyntaxVisitorContinueKind {
12681302
if shouldSkip(node) {
12691303
return .skipChildren

Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,9 @@ extension DiagnosticMessage where Self == StaticParserError {
233233
public static var subscriptsCannotHaveNames: Self {
234234
.init("subscripts cannot have a name")
235235
}
236+
public static var submoduleCannotBeImportedUsingModuleSelector: Self {
237+
.init("submodule cannot be imported using module selector")
238+
}
236239
public static var tooManyClosingPoundDelimiters: Self {
237240
.init("too many '#' characters in closing delimiter")
238241
}

Sources/SwiftSyntax/generated/raw/RawSyntaxValidation.swift

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesGHI.swift

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Tests/SwiftParserTest/translated/ModuleSelectorTests.swift

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,70 @@ final class ModuleSelectorTests: ParserTestCase {
2222
}
2323

2424
func testModuleSelectorImports() {
25-
XCTExpectFailure("imports not yet implemented")
26-
2725
assertParse(
2826
"""
29-
import ctypes::bits // FIXME: ban using :: with submodules?
3027
import struct ModuleSelectorTestingKit::A
28+
""",
29+
substructure: ImportDeclSyntax(
30+
importKindSpecifier: .keyword(.struct),
31+
path: [
32+
ImportPathComponentSyntax(
33+
name: .identifier("ModuleSelectorTestingKit"),
34+
trailingPeriod: .colonColonToken()
35+
),
36+
ImportPathComponentSyntax(
37+
name: .identifier("A")
38+
),
39+
]
40+
)
41+
)
42+
43+
assertParse(
44+
"""
45+
import struct 1️⃣_::A
46+
""",
47+
diagnostics: [
48+
DiagnosticSpec(message: "'_' cannot be used as an identifier here")
49+
]
50+
)
51+
52+
assertParse(
53+
"""
54+
import struct ModuleSelectorTestingKit::1️⃣Submodule::A
55+
""",
56+
diagnostics: [
57+
DiagnosticSpec(message: "unexpected code 'Submodule::' in import")
58+
]
59+
)
60+
61+
assertParse(
3162
"""
63+
import struct ModuleSelectorTestingKit.Submodule1️⃣::A
64+
""",
65+
diagnostics: [
66+
DiagnosticSpec(
67+
message: "submodule cannot be imported using module selector",
68+
fixIts: ["replace '::' with '.'"]
69+
)
70+
],
71+
fixedSource: """
72+
import struct ModuleSelectorTestingKit.Submodule.A
73+
"""
74+
)
75+
76+
assertParse(
77+
"""
78+
import ctypes1️⃣::bits
79+
""",
80+
diagnostics: [
81+
DiagnosticSpec(
82+
message: "submodule cannot be imported using module selector",
83+
fixIts: ["replace '::' with '.'"]
84+
)
85+
],
86+
fixedSource: """
87+
import ctypes.bits
88+
"""
3289
)
3390
}
3491

0 commit comments

Comments
 (0)