Skip to content

Commit 2807c92

Browse files
committed
Support module selectors in scoped imports
1 parent 0db0b44 commit 2807c92

File tree

9 files changed

+212
-23
lines changed

9 files changed

+212
-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: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ extension Parser {
431431
) -> RawImportDeclSyntax {
432432
let (unexpectedBeforeImportKeyword, importKeyword) = self.eat(handle)
433433
let kind = self.parseImportKind()
434-
let path = self.parseImportPath()
434+
let path = self.parseImportPath(hasImportKind: kind != nil)
435435
return RawImportDeclSyntax(
436436
attributes: attrs.attributes,
437437
modifiers: attrs.modifiers,
@@ -447,21 +447,66 @@ extension Parser {
447447
return self.consume(ifAnyIn: ImportDeclSyntax.ImportKindSpecifierOptions.self)
448448
}
449449

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

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
@@ -1249,6 +1249,40 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
12491249
return .visitChildren
12501250
}
12511251

1252+
public override func visit(_ node: ImportPathComponentSyntax) -> SyntaxVisitorContinueKind {
1253+
if shouldSkip(node) {
1254+
return .skipChildren
1255+
}
1256+
1257+
if let colonColon = node.unexpectedAfterTrailingPeriod?.first?.as(TokenSyntax.self),
1258+
colonColon.tokenKind == .colonColon,
1259+
colonColon.isPresent,
1260+
let trailingPeriod = node.trailingPeriod,
1261+
trailingPeriod.tokenKind == .period,
1262+
trailingPeriod.isMissing
1263+
{
1264+
addDiagnostic(
1265+
colonColon,
1266+
.submoduleCannotBeImportedUsingModuleSelector,
1267+
fixIts: [
1268+
FixIt(
1269+
message: ReplaceTokensFixIt(replaceTokens: [colonColon], replacements: [trailingPeriod]),
1270+
changes: [
1271+
.makeMissing(colonColon),
1272+
.makePresent(trailingPeriod),
1273+
]
1274+
)
1275+
],
1276+
handledNodes: [
1277+
colonColon.id,
1278+
trailingPeriod.id,
1279+
]
1280+
)
1281+
}
1282+
1283+
return .visitChildren
1284+
}
1285+
12521286
public override func visit(_ node: InitializerClauseSyntax) -> SyntaxVisitorContinueKind {
12531287
if shouldSkip(node) {
12541288
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)