Skip to content

Commit 20c6f29

Browse files
committed
Support module selectors in scoped imports
1 parent 6cf7845 commit 20c6f29

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
@@ -429,7 +429,7 @@ extension Parser {
429429
) -> RawImportDeclSyntax {
430430
let (unexpectedBeforeImportKeyword, importKeyword) = self.eat(handle)
431431
let kind = self.parseImportKind()
432-
let path = self.parseImportPath()
432+
let path = self.parseImportPath(hasImportKind: kind != nil)
433433
return RawImportDeclSyntax(
434434
attributes: attrs.attributes,
435435
modifiers: attrs.modifiers,
@@ -445,21 +445,66 @@ extension Parser {
445445
return self.consume(ifAnyIn: ImportDeclSyntax.ImportKindSpecifierOptions.self)
446446
}
447447

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

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)