Skip to content

Commit a050bdc

Browse files
committed
Re-work MacroSystem on top of the same parsing functions that the compiler invokes
This should make the MacroSystem behave more similarly to the expansions performed by the compiler.
1 parent b14b6ba commit a050bdc

File tree

12 files changed

+1611
-769
lines changed

12 files changed

+1611
-769
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ let package = Package(
182182

183183
.target(
184184
name: "SwiftSyntaxMacroExpansion",
185-
dependencies: ["SwiftSyntax", "SwiftSyntaxMacros", "SwiftDiagnostics"],
185+
dependencies: ["SwiftSyntax", "SwiftSyntaxBuilder", "SwiftSyntaxMacros", "SwiftDiagnostics"],
186186
exclude: ["CMakeLists.txt"]
187187
),
188188

Sources/SwiftSyntaxBuilder/Indenter.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,16 @@ extension Trivia {
2525
}
2626
}
2727

28-
class Indenter: SyntaxRewriter {
28+
/// Adds a given amount of indentation after every newline in a syntax tree.
29+
public class Indenter: SyntaxRewriter {
2930
let indentation: Trivia
3031

3132
init(indentation: Trivia) {
3233
self.indentation = indentation
3334
super.init(viewMode: .sourceAccurate)
3435
}
3536

36-
/// Adds `indentation` after all newlines in the syntax tree.
37+
/// Add `indentation` after all newlines in the syntax tree.
3738
public static func indent<SyntaxType: SyntaxProtocol>(
3839
_ node: SyntaxType,
3940
indentation: Trivia

Sources/SwiftSyntaxMacroExpansion/BasicMacroExpansionContext.swift

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,13 @@ public class BasicMacroExpansionContext {
4848
/// information about that source file.
4949
private var sourceFiles: [SourceFileSyntax: KnownSourceFile] = [:]
5050

51-
/// Mapping from intentionally-disconnected syntax node roots to the
52-
/// absolute offsets that have within a given source file, which is used
53-
/// to establish the link between a node that been intentionally disconnected
54-
/// from a source file to hide information from the macro implementation.
55-
private var disconnectedNodes: [Syntax: (SourceFileSyntax, Int)] = [:]
51+
/// Mapping from intentionally-disconnected syntax nodes to the corresponding
52+
/// nodes in the original source file.
53+
///
54+
/// This is used to establish the link between a node that been intentionally
55+
/// disconnected from a source file to hide information from the macro
56+
/// implementation.
57+
private var detachedNodes: [Syntax: Syntax] = [:]
5658

5759
/// The macro expansion discriminator, which is used to form unique names
5860
/// when requested.
@@ -69,24 +71,10 @@ public class BasicMacroExpansionContext {
6971
}
7072

7173
extension BasicMacroExpansionContext {
72-
/// Note that the given node that was at the given position in the provided
73-
/// source file has been disconnected and is now a new root.
74-
private func addDisconnected(
75-
_ node: some SyntaxProtocol,
76-
at offset: AbsolutePosition,
77-
in sourceFile: SourceFileSyntax
78-
) {
79-
disconnectedNodes[Syntax(node)] = (sourceFile, offset.utf8Offset)
80-
}
81-
8274
/// Detach the given node, and record where it came from.
8375
public func detach<Node: SyntaxProtocol>(_ node: Node) -> Node {
8476
let detached = node.detached
85-
86-
if let rootSourceFile = node.root.as(SourceFileSyntax.self) {
87-
addDisconnected(detached, at: node.position, in: rootSourceFile)
88-
}
89-
77+
detachedNodes[Syntax(detached)] = Syntax(node)
9078
return detached
9179
}
9280
}
@@ -136,28 +124,49 @@ extension BasicMacroExpansionContext: MacroExpansionContext {
136124
diagnostics.append(diagnostic)
137125
}
138126

127+
/// Translates a position from a detached node to the corresponding location
128+
/// in the original source file.
129+
///
130+
/// - Parameters:
131+
/// - position: The position to translate
132+
/// - node: The node at which the position is anchored. This node is used to
133+
/// find the offset in the original source file
134+
/// - fileName: The file name that should be used in the `SourceLocation`
135+
/// - Returns: The location in the original source file
136+
public func location(
137+
for position: AbsolutePosition,
138+
anchoredAt node: Syntax,
139+
fileName: String
140+
) -> SourceLocation {
141+
guard let nodeInOriginalTree = detachedNodes[node.root] else {
142+
return SourceLocationConverter(file: fileName, tree: node.root).location(for: position)
143+
}
144+
let adjustedPosition = position + SourceLength(utf8Length: nodeInOriginalTree.position.utf8Offset)
145+
return SourceLocationConverter(file: fileName, tree: nodeInOriginalTree.root).location(for: adjustedPosition)
146+
}
147+
139148
public func location(
140149
of node: some SyntaxProtocol,
141150
at position: PositionInSyntaxNode,
142151
filePathMode: SourceLocationFilePathMode
143152
) -> AbstractSourceLocation? {
144153
// Dig out the root source file and figure out how we need to adjust the
145154
// offset of the given syntax node to adjust for it.
146-
let rootSourceFile: SourceFileSyntax
147-
let offsetAdjustment: Int
155+
let rootSourceFile: SourceFileSyntax?
156+
let offsetAdjustment: SourceLength
148157
if let directRootSourceFile = node.root.as(SourceFileSyntax.self) {
149158
// The syntax node came from the source file itself.
150159
rootSourceFile = directRootSourceFile
151-
offsetAdjustment = 0
152-
} else if let (adjustedSourceFile, offset) = disconnectedNodes[Syntax(node)] {
160+
offsetAdjustment = .zero
161+
} else if let nodeInOriginalTree = detachedNodes[Syntax(node)] {
153162
// The syntax node came from a disconnected root, so adjust for that.
154-
rootSourceFile = adjustedSourceFile
155-
offsetAdjustment = offset
163+
rootSourceFile = nodeInOriginalTree.root.as(SourceFileSyntax.self)
164+
offsetAdjustment = SourceLength(utf8Length: nodeInOriginalTree.position.utf8Offset)
156165
} else {
157166
return nil
158167
}
159168

160-
guard let knownRoot = sourceFiles[rootSourceFile] else {
169+
guard let rootSourceFile, let knownRoot = sourceFiles[rootSourceFile] else {
161170
return nil
162171
}
163172

@@ -189,6 +198,6 @@ extension BasicMacroExpansionContext: MacroExpansionContext {
189198

190199
// Do the location lookup.
191200
let converter = SourceLocationConverter(file: fileName, tree: rootSourceFile)
192-
return AbstractSourceLocation(converter.location(for: rawPosition.advanced(by: offsetAdjustment)))
201+
return AbstractSourceLocation(converter.location(for: rawPosition + offsetAdjustment))
193202
}
194203
}

Sources/SwiftSyntaxMacroExpansion/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
add_swift_host_library(SwiftSyntaxMacroExpansion
22
BasicMacroExpansionContext.swift
33
FunctionParameterUtils.swift
4+
IndentationUtils.swift
45
MacroExpansion.swift
56
MacroReplacement.swift
67
MacroSystem.swift
7-
Syntax+MacroEvaluation.swift
88
)
99

1010
target_link_libraries(SwiftSyntaxMacroExpansion PUBLIC
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2023 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+
import SwiftSyntax
14+
15+
// MARK: SyntaxProtocol.indentationOfFirstLine
16+
17+
extension SyntaxProtocol {
18+
/// The indentation of the first line in this token.
19+
var indentationOfFirstLine: Trivia {
20+
guard let firstToken = self.firstToken(viewMode: .sourceAccurate) else {
21+
return Trivia()
22+
}
23+
return firstToken.indentationOfLine
24+
}
25+
}
26+
27+
// MARK: String.indented
28+
29+
extension String {
30+
/// Indents every new line in this string literal by `indentation`.
31+
///
32+
/// - Note: The first line in the string gets indented as well.
33+
func indented(by indentation: Trivia) -> String {
34+
if isEmpty || indentation.isEmpty {
35+
return self
36+
}
37+
38+
var indented = ""
39+
var remaining = self[...]
40+
while let nextNewline = remaining.firstIndex(where: { $0.isNewline }) {
41+
if nextNewline != remaining.startIndex {
42+
// Don’t add indentation if the line is empty.
43+
indentation.write(to: &indented)
44+
}
45+
indented += remaining[...nextNewline]
46+
remaining = remaining[remaining.index(after: nextNewline)...]
47+
}
48+
49+
if !remaining.isEmpty {
50+
indentation.write(to: &indented)
51+
indented += remaining
52+
}
53+
54+
return indented
55+
}
56+
}
57+
58+
// MARK: SyntaxProtocol.stripp
59+
60+
fileprivate class IndentationStripper: SyntaxRewriter {
61+
override func visit(_ token: TokenSyntax) -> TokenSyntax {
62+
if token.leadingTrivia.contains(where: \.isNewline) || token.trailingTrivia.contains(where: \.isNewline) {
63+
return
64+
token
65+
.with(\.leadingTrivia, token.leadingTrivia.removingIndentation)
66+
.with(\.trailingTrivia, token.trailingTrivia.removingIndentation)
67+
} else {
68+
return token
69+
}
70+
}
71+
}
72+
73+
extension Trivia {
74+
/// Remove all indentation from the trivia.
75+
var removingIndentation: Trivia {
76+
var resultPieces: [TriviaPiece] = []
77+
var isAfterNewline = false
78+
for piece in pieces {
79+
if piece.isSpaceOrTab && isAfterNewline {
80+
// Don’t add whitespace after a newline
81+
continue
82+
}
83+
isAfterNewline = piece.isNewline
84+
resultPieces.append(piece)
85+
}
86+
return Trivia(pieces: resultPieces)
87+
}
88+
}
89+
90+
extension SyntaxProtocol {
91+
/// This syntax node with all indentation removed.
92+
var withIndentationRemoved: Self {
93+
return IndentationStripper().rewrite(self).cast(Self.self)
94+
}
95+
}

0 commit comments

Comments
 (0)