Skip to content

SwiftSyntax support for module selectors #3091

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,7 @@ public let ATTRIBUTE_NODES: [Node] = [
Node(
kind: .implementsAttributeArguments,
base: .syntax,
nameForDiagnostics: "@_implements arguemnts",
nameForDiagnostics: "@_implements arguments",
documentation:
"The arguments for the `@_implements` attribute of the form `Type, methodName(arg1Label:arg2Label:)`",
children: [
Expand Down
45 changes: 33 additions & 12 deletions CodeGeneration/Sources/SyntaxSupport/Child.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,19 @@ public class Child: NodeChoiceConvertible {
}
}

/// Should this child be hidden?
///
/// A hidden child is one that is not accessible in any way at a specific point in the history, but still needs to be
/// (default) initialized. As always, its `newestChildPath` indicates the current way to access it.
///
/// Hidden children are used for `Refactoring.introduced` and for the implicit changeset that creates
/// non-experimental APIs that ignore experimental children.
public let isHidden: Bool

/// True if this child was created by a `childHistory` change set. Such children
/// are part of the compatibility layer and are therefore deprecated.
public var isHistorical: Bool

/// A name of this child as an identifier.
public var identifier: TokenSyntax {
return .identifier(lowercaseFirstWord(name: name))
Expand All @@ -161,8 +174,8 @@ public class Child: NodeChoiceConvertible {
return "\(raw: newestName.withFirstCharacterUppercased)Options"
}

/// If this child is deprecated, describes the sequence of accesses necessary
/// to reach the equivalent value using non-deprecated children; if the child
/// If this child is part of a compatibility layer, describes the sequence of accesses necessary
/// to reach the equivalent value using non-compatibility-layer children; if the child
/// is not deprecated, this array is empty.
///
/// Think of the elements of this array like components in a key path:
Expand Down Expand Up @@ -199,12 +212,6 @@ public class Child: NodeChoiceConvertible {
/// of the child. That information is not directly available anywhere.
public let newestChildPath: [Child]

/// True if this child was created by a `Child.Refactoring`. Such children
/// are part of the compatibility layer and are therefore deprecated.
public var isHistorical: Bool {
!newestChildPath.isEmpty
}

/// Replaces the nodes in `newerChildPath` with their own `newerChildPath`s,
/// if any, to form a child path enitrely of non-historical nodes.
static private func makeNewestChildPath(from newerChildPath: [Child]) -> [Child] {
Expand All @@ -214,7 +221,7 @@ public class Child: NodeChoiceConvertible {
var workStack = Array(newerChildPath.reversed())

while let elem = workStack.popLast() {
if elem.isHistorical {
if !elem.newestChildPath.isEmpty {
// There's an even newer version. Start working on that.
workStack.append(contentsOf: elem.newestChildPath.reversed())
} else {
Expand Down Expand Up @@ -308,7 +315,8 @@ public class Child: NodeChoiceConvertible {
documentation: String? = nil,
isOptional: Bool = false,
providesDefaultInitialization: Bool = true,
newerChildPath: [Child] = []
newerChildPath: [Child] = [],
isHistorical: Bool = false
) {
precondition(name.first?.isLowercase ?? true, "The first letter of a child’s name should be lowercase")
self.name = name
Expand All @@ -320,11 +328,18 @@ public class Child: NodeChoiceConvertible {
self.documentationAbstract = String(documentation?.split(whereSeparator: \.isNewline).first ?? "")
self.isOptional = isOptional
self.providesDefaultInitialization = providesDefaultInitialization
self.isHidden = false
self.isHistorical = isHistorical
}

/// Create a node that is a copy of the last node in `newerChildPath`, but
/// with modifications.
init(renamingTo replacementName: String? = nil, newerChildPath: [Child]) {
init(
renamingTo replacementName: String? = nil,
makingHistorical: Bool = false,
makingHidden: Bool = false,
newerChildPath: [Child]
) {
let other = newerChildPath.last!

self.name = replacementName ?? other.name
Expand All @@ -336,6 +351,8 @@ public class Child: NodeChoiceConvertible {
self.documentationAbstract = other.documentationAbstract
self.isOptional = other.isOptional
self.providesDefaultInitialization = other.providesDefaultInitialization
self.isHidden = makingHidden || other.isHidden
self.isHistorical = makingHistorical || other.isHistorical
}

/// Create a child for the unexpected nodes between two children (either or
Expand All @@ -361,7 +378,8 @@ public class Child: NodeChoiceConvertible {
documentation: nil,
isOptional: true,
providesDefaultInitialization: true,
newerChildPath: newerChildPath
newerChildPath: newerChildPath,
isHistorical: (earlier?.isHistorical ?? false) || (later?.isHistorical ?? false)
)
}
}
Expand Down Expand Up @@ -417,5 +435,8 @@ extension Child {
/// point in the past, so deprecated aliases that flatten the other node's
/// children into this node should be provided.
case extracted

/// A new child was added (and it's important to preserve the names around it).
case introduced
}
}
39 changes: 37 additions & 2 deletions CodeGeneration/Sources/SyntaxSupport/CommonNodes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,14 @@ public let COMMON_NODES: [Node] = [
"MissingNode"
],
children: [
Child(
name: "moduleSelector",
kind: .node(kind: .moduleSelector),
experimentalFeature: .moduleSelector,
documentation:
"A module selector. Some expressions can be prefixed with module selectors, so if one is parsed before an invalid expression, it will be inserted here.",
isOptional: true
),
Copy link
Member

@rintaro rintaro Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel using MissingExprSyntax as dangling module selector is the way to go. MissingExprSyntax is a "placeholder" for expression with unknown kind. But IMO, we can reasonably assume the user is to add an identifier after a module selector to form DeclReferenceExprSyntax.
I feel it's more natural to model it as DeclReferenceExprSyntax with missing baseName.

Same for MissingTypeSyntax

Child(
name: "placeholder",
kind: .token(choices: [.token(.identifier)], requiresLeadingSpace: false, requiresTrailingSpace: false),
Expand All @@ -241,7 +249,7 @@ public let COMMON_NODES: [Node] = [

This token should always have `presence = .missing`.
"""
)
),
]
),

Expand Down Expand Up @@ -318,6 +326,14 @@ public let COMMON_NODES: [Node] = [
"MissingNode"
],
children: [
Child(
name: "moduleSelector",
kind: .node(kind: .moduleSelector),
experimentalFeature: .moduleSelector,
documentation:
"A module selector. Some types can be prefixed with module selectors, so if one is parsed before an invalid type, it will be inserted here.",
isOptional: true
),
Child(
name: "placeholder",
kind: .token(choices: [.token(.identifier)], requiresLeadingSpace: false, requiresTrailingSpace: false),
Expand All @@ -326,7 +342,26 @@ public let COMMON_NODES: [Node] = [

This token should always have `presence = .missing`.
"""
)
),
]
),

Node(
kind: .moduleSelector,
base: .syntax,
experimentalFeature: .moduleSelector,
nameForDiagnostics: "module selector",
children: [
Child(
name: "moduleName",
kind: .token(choices: [.token(.identifier)]),
nameForDiagnostics: "module name"
),
Child(
name: "colonColon",
kind: .token(choices: [.token(.colonColon)]),
nameForDiagnostics: "'::' operator"
),
]
),

Expand Down
Loading