Skip to content

Commit 3ae9cdc

Browse files
committed
[NFC-ish] Factor out CompatibilityLayers
A new CompatibilityLayers type computes information about deprecated vars and inits, presenting it in a new way: by creating additional `Child` objects with the deprecated information, plus sufficient info to map it back to the current name. This causes minor non-functional changes to the bodies of compatibility builder inits, but otherwise has no real effect in this commit. However, this new setup with separate `Child` nodes for deprecated children and a global table of information about them prepares us for further changes.
1 parent 81013d4 commit 3ae9cdc

File tree

8 files changed

+387
-181
lines changed

8 files changed

+387
-181
lines changed

CodeGeneration/Sources/SyntaxSupport/Child.swift

Lines changed: 78 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -150,29 +150,44 @@ public class Child: NodeChoiceConvertible {
150150
/// For any other kind of child nodes, accessing this property crashes.
151151
public var syntaxChoicesType: TypeSyntax {
152152
precondition(kind.isNodeChoices, "Cannot get `syntaxChoicesType` for node that doesn’t have nodeChoices")
153-
return "\(raw: name.withFirstCharacterUppercased)"
153+
return "\(raw: newestName.withFirstCharacterUppercased)"
154154
}
155155

156156
/// If this child only has tokens, the type that the generated `TokenSpecSet` should get.
157157
///
158158
/// For any other kind of child nodes, accessing this property crashes.
159159
public var tokenSpecSetType: TypeSyntax {
160160
precondition(kind.isToken, "Cannot get `tokenSpecSetType` for node that isn’t a token")
161-
return "\(raw: name.withFirstCharacterUppercased)Options"
162-
}
163-
164-
/// The deprecated name of this child that's suitable to be used for variable or enum case names.
165-
public var deprecatedVarName: TokenSyntax? {
166-
guard let deprecatedName = deprecatedName else {
167-
return nil
168-
}
169-
return .identifier(lowercaseFirstWord(name: deprecatedName))
161+
return "\(raw: newestName.withFirstCharacterUppercased)Options"
170162
}
171163

172164
/// Determines if this child has a deprecated name
173165
public var hasDeprecatedName: Bool {
174166
return deprecatedName != nil
175167
}
168+
169+
/// If this child is actually part of another child's history, links back
170+
/// to that child. Nil if this is the newest version of the child.
171+
public let newerChild: Child?
172+
173+
/// True if this child was created by a `Refactoring`. Such children are part
174+
/// of a compatibility layer and are therefore deprecated.
175+
public var isHistorical: Bool {
176+
newerChild != nil
177+
}
178+
179+
/// Replaces the nodes in `newerChildPath` with their own `newerChildPath`s,
180+
/// if any, to form a child path enitrely of non-historical nodes.
181+
public func makeNewestChild() -> Child {
182+
guard let newerChild else {
183+
return self
184+
}
185+
return newerChild.makeNewestChild()
186+
}
187+
188+
private var newestName: String {
189+
return makeNewestChild().name
190+
}
176191

177192
/// If the child ends with "token" in the kind, it's considered a token node.
178193
/// Grab the existing reference to that token from the global list.
@@ -244,19 +259,15 @@ public class Child: NodeChoiceConvertible {
244259
return AttributeListSyntax("@_spi(ExperimentalLanguageFeatures)").with(\.trailingTrivia, .newline)
245260
}
246261

247-
/// If a classification is passed, it specifies the color identifiers in
248-
/// that subtree should inherit for syntax coloring. Must be a member of
249-
/// ``SyntaxClassification``.
250-
/// If `forceClassification` is also set to true, all child nodes (not only
251-
/// identifiers) inherit the syntax classification.
252262
init(
253263
name: String,
254264
deprecatedName: String? = nil,
255265
kind: ChildKind,
256266
experimentalFeature: ExperimentalFeature? = nil,
257267
nameForDiagnostics: String? = nil,
258268
documentation: String? = nil,
259-
isOptional: Bool = false
269+
isOptional: Bool = false,
270+
newerChild: Child? = nil
260271
) {
261272
precondition(name.first?.isLowercase ?? true, "The first letter of a child’s name should be lowercase")
262273
precondition(
@@ -265,11 +276,62 @@ public class Child: NodeChoiceConvertible {
265276
)
266277
self.name = name
267278
self.deprecatedName = deprecatedName
279+
self.newerChild = newerChild
268280
self.kind = kind
269281
self.experimentalFeature = experimentalFeature
270282
self.nameForDiagnostics = nameForDiagnostics
271283
self.documentationSummary = SwiftSyntax.Trivia.docCommentTrivia(from: documentation)
272284
self.documentationAbstract = String(documentation?.split(whereSeparator: \.isNewline).first ?? "")
273285
self.isOptional = isOptional
274286
}
287+
288+
/// Create a node that is a copy of the last node in `newerChildPath`, but
289+
/// with modifications.
290+
init(renamingTo replacementName: String? = nil, newerChild other: Child) {
291+
self.name = replacementName ?? other.name
292+
self.deprecatedName = nil
293+
self.newerChild = other
294+
self.kind = other.kind
295+
self.experimentalFeature = other.experimentalFeature
296+
self.nameForDiagnostics = other.nameForDiagnostics
297+
self.documentationSummary = other.documentationSummary
298+
self.documentationAbstract = other.documentationAbstract
299+
self.isOptional = other.isOptional
300+
}
301+
302+
/// Create a child for the unexpected nodes between two children (either or
303+
/// both of which may be `nil`).
304+
convenience init(forUnexpectedBetween earlier: Child?, and later: Child?, newerChild: Child? = nil) {
305+
let name = switch (earlier, later) {
306+
case (nil, let later?):
307+
"unexpectedBefore\(later.name.withFirstCharacterUppercased)"
308+
case (let earlier?, nil):
309+
"unexpectedAfter\(earlier.name.withFirstCharacterUppercased)"
310+
case (let earlier?, let later?):
311+
"unexpectedBetween\(earlier.name.withFirstCharacterUppercased)And\(later.name.withFirstCharacterUppercased)"
312+
case (nil, nil):
313+
"unexpected"
314+
}
315+
316+
self.init(
317+
name: name,
318+
deprecatedName: nil, // deprecation of unexpected nodes is handled in CompatibilityLayers
319+
kind: .collection(kind: .unexpectedNodes, collectionElementName: name.withFirstCharacterUppercased),
320+
experimentalFeature: earlier?.experimentalFeature ?? later?.experimentalFeature,
321+
nameForDiagnostics: nil,
322+
documentation: nil,
323+
isOptional: true,
324+
newerChild: newerChild
325+
)
326+
}
327+
}
328+
329+
extension Child: Hashable {
330+
public static func == (lhs: Child, rhs: Child) -> Bool {
331+
lhs === rhs
332+
}
333+
334+
public func hash(into hasher: inout Hasher) {
335+
hasher.combine(ObjectIdentifier(self))
336+
}
275337
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
/// Computes and caches information about properties and initializers that ought to be generated for the compatibility layer.
14+
public struct CompatibilityLayers /*: Sendable*/ {
15+
/// Properties that are needed in the compatibility layer, in the order they ought to appear in the generated file.
16+
public var deprecatedVarsByNode: [SyntaxNodeKind: [Child]] = [:]
17+
18+
/// Initializer signatures that are needed in the compatibility layer, in the order they ought to appear in the generated file.
19+
public var deprecatedInitSignaturesByNode: [SyntaxNodeKind: [InitSignature]] = [:]
20+
21+
internal init(nodes: [Node]) {
22+
for node in nodes {
23+
realizeLayers(for: node)
24+
}
25+
}
26+
27+
private mutating func replacementChild(for newerChild: Child) -> Child? {
28+
func make() -> Child? {
29+
guard let deprecatedName = newerChild.deprecatedName else {
30+
return nil
31+
}
32+
33+
return Child(renamingTo: deprecatedName, newerChild: newerChild)
34+
}
35+
36+
if cachedReplacementChildren[newerChild] == nil {
37+
cachedReplacementChildren[newerChild] = make()
38+
}
39+
return cachedReplacementChildren[newerChild]!
40+
}
41+
private var cachedReplacementChildren: [Child: Child?] = [:]
42+
43+
private mutating func realizeLayers(for node: Node) {
44+
guard deprecatedVarsByNode[node.syntaxNodeKind] == nil && deprecatedInitSignaturesByNode[node.syntaxNodeKind] == nil, let layoutNode = node.layoutNode else {
45+
return
46+
}
47+
48+
// The results that will ultimately be saved into the *ByNode dictionaries.
49+
var vars: [Child] = []
50+
var initSignatures: [InitSignature] = []
51+
52+
// Temporary working state.
53+
var children = layoutNode.children
54+
var knownVars = Set(children)
55+
56+
func firstIndexOfChild(named targetName: String) -> Int {
57+
guard let i = children.firstIndex(where: { $0.name == targetName }) else {
58+
fatalError("couldn't find '\(targetName)' in current children of \(node.syntaxNodeKind.rawValue): \(String(reflecting: children.map(\.name)))")
59+
}
60+
return i
61+
}
62+
63+
var unexpectedChildrenWithNewNames: Set<Child> = []
64+
65+
// First pass: Apply the changes explicitly specified in the change set.
66+
for i in children.indices {
67+
let currentName = children[i].name
68+
guard let replacementChild = replacementChild(for: children[i]) else {
69+
continue
70+
}
71+
children[i] = replacementChild
72+
73+
// Mark adjacent unexpected node children whose names have changed too.
74+
if currentName != replacementChild.name {
75+
unexpectedChildrenWithNewNames.insert(children[i - 1])
76+
unexpectedChildrenWithNewNames.insert(children[i + 1])
77+
}
78+
}
79+
80+
// Second pass: Update unexpected node children adjacent to those changes whose names have probably changed.
81+
for unexpectedChild in unexpectedChildrenWithNewNames {
82+
precondition(unexpectedChild.isUnexpectedNodes)
83+
let i = firstIndexOfChild(named: unexpectedChild.name)
84+
85+
let earlier = children[checked: i - 1]
86+
let later = children[checked: i + 1]
87+
precondition(!(earlier?.isUnexpectedNodes ?? false) && !(later?.isUnexpectedNodes ?? false))
88+
89+
let newChild = Child(forUnexpectedBetween: earlier, and: later, newerChild: unexpectedChild)
90+
precondition(newChild.name != unexpectedChild.name)
91+
assert(!children.contains { $0.name == newChild.name })
92+
93+
children[i] = newChild
94+
}
95+
96+
// Third pass: Append newly-created children to vars. We do this now so that changes from the first two passes are properly interleaved, preserving source order.
97+
vars += children.filter { knownVars.insert($0).inserted }
98+
99+
initSignatures.append(InitSignature(children: children))
100+
101+
deprecatedVarsByNode[node.syntaxNodeKind] = vars
102+
deprecatedInitSignaturesByNode[node.syntaxNodeKind] = initSignatures
103+
}
104+
}
105+
106+
extension Array {
107+
/// Returns `nil` if `i` is out of bounds, or the indicated element otherwise.
108+
fileprivate subscript(checked i: Index) -> Element? {
109+
get {
110+
return indices.contains(i) ? self[i] : nil
111+
}
112+
}
113+
}

CodeGeneration/Sources/SyntaxSupport/Node.swift

Lines changed: 4 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -135,58 +135,10 @@ public class Node: NodeChoiceConvertible {
135135
self.documentation = SwiftSyntax.Trivia.docCommentTrivia(from: documentation)
136136
self.parserFunction = parserFunction
137137

138-
let childrenWithUnexpected: [Child]
139-
if children.isEmpty {
140-
childrenWithUnexpected = [
141-
Child(
142-
name: "unexpected",
143-
kind: .collection(kind: .unexpectedNodes, collectionElementName: "Unexpected"),
144-
isOptional: true
145-
)
146-
]
147-
} else {
148-
// Add implicitly generated UnexpectedNodes children between
149-
// any two defined children
150-
childrenWithUnexpected =
151-
children.enumerated().flatMap { (i, child) -> [Child] in
152-
let childName = child.name.withFirstCharacterUppercased
153-
154-
let unexpectedName: String
155-
let unexpectedDeprecatedName: String?
156-
157-
if i == 0 {
158-
unexpectedName = "unexpectedBefore\(childName)"
159-
unexpectedDeprecatedName = child.deprecatedName.map { "unexpectedBefore\($0.withFirstCharacterUppercased)" }
160-
} else {
161-
unexpectedName = "unexpectedBetween\(children[i - 1].name.withFirstCharacterUppercased)And\(childName)"
162-
if let deprecatedName = children[i - 1].deprecatedName?.withFirstCharacterUppercased {
163-
unexpectedDeprecatedName =
164-
"unexpectedBetween\(deprecatedName)And\(child.deprecatedName?.withFirstCharacterUppercased ?? childName)"
165-
} else if let deprecatedName = child.deprecatedName?.withFirstCharacterUppercased {
166-
unexpectedDeprecatedName =
167-
"unexpectedBetween\(children[i - 1].name.withFirstCharacterUppercased)And\(deprecatedName)"
168-
} else {
169-
unexpectedDeprecatedName = nil
170-
}
171-
}
172-
let unexpectedBefore = Child(
173-
name: unexpectedName,
174-
deprecatedName: unexpectedDeprecatedName,
175-
kind: .collection(kind: .unexpectedNodes, collectionElementName: unexpectedName),
176-
isOptional: true
177-
)
178-
return [unexpectedBefore, child]
179-
} + [
180-
Child(
181-
name: "unexpectedAfter\(children.last!.name.withFirstCharacterUppercased)",
182-
deprecatedName: children.last!.deprecatedName.map { "unexpectedAfter\($0.withFirstCharacterUppercased)" },
183-
kind: .collection(
184-
kind: .unexpectedNodes,
185-
collectionElementName: "UnexpectedAfter\(children.last!.name.withFirstCharacterUppercased)"
186-
),
187-
isOptional: true
188-
)
189-
]
138+
let liftedChildren = children.lazy.map(Optional.some)
139+
let pairedChildren = zip([nil] + liftedChildren, liftedChildren + [nil])
140+
let childrenWithUnexpected = pairedChildren.flatMap { earlier, later in
141+
[earlier, Child(forUnexpectedBetween: earlier, and: later)].compactMap { $0 }
190142
}
191143
self.data = .layout(children: childrenWithUnexpected, traits: traits)
192144
}

CodeGeneration/Sources/SyntaxSupport/SyntaxNodes.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,5 @@ public let SYNTAX_NODE_MAP: [SyntaxNodeKind: Node] = Dictionary(
3535
)
3636

3737
public let NON_BASE_SYNTAX_NODES = SYNTAX_NODES.filter { !$0.kind.isBase }
38+
39+
public let SYNTAX_COMPATIBILITY_LAYERS = CompatibilityLayers(nodes: SYNTAX_NODES)

0 commit comments

Comments
 (0)