Skip to content

Commit f7667e1

Browse files
authored
Merge pull request #1288 from DougGregor/macro-expansion-context-as-protocol
2 parents 5c98b24 + 2b43cfb commit f7667e1

22 files changed

+577
-264
lines changed

Package.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ let package = Package(
4040
.library(name: "SwiftSyntax", type: .static, targets: ["SwiftSyntax"]),
4141
.library(name: "SwiftSyntaxParser", type: .static, targets: ["SwiftSyntaxParser"]),
4242
.library(name: "SwiftSyntaxBuilder", type: .static, targets: ["SwiftSyntaxBuilder"]),
43-
.library(name: "_SwiftSyntaxMacros", type: .static, targets: ["_SwiftSyntaxMacros"]),
43+
.library(name: "SwiftSyntaxMacros", type: .static, targets: ["SwiftSyntaxMacros"]),
4444
.library(name: "SwiftRefactor", type: .static, targets: ["SwiftRefactor"]),
4545
],
4646
targets: [
@@ -115,7 +115,7 @@ let package = Package(
115115
]
116116
),
117117
.target(
118-
name: "_SwiftSyntaxMacros",
118+
name: "SwiftSyntaxMacros",
119119
dependencies: [
120120
"SwiftSyntax", "SwiftSyntaxBuilder", "SwiftParser", "SwiftDiagnostics"
121121
],
@@ -158,7 +158,7 @@ let package = Package(
158158
name: "SwiftSyntaxMacrosTest",
159159
dependencies: ["SwiftDiagnostics", "SwiftOperators", "SwiftParser",
160160
"_SwiftSyntaxTestSupport", "SwiftSyntaxBuilder",
161-
"_SwiftSyntaxMacros"]
161+
"SwiftSyntaxMacros"]
162162
),
163163
.testTarget(
164164
name: "PerformanceTest",

Sources/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,5 @@ add_subdirectory(SwiftParser)
3838
add_subdirectory(SwiftParserDiagnostics)
3939
add_subdirectory(SwiftOperators)
4040
add_subdirectory(SwiftSyntaxBuilder)
41-
add_subdirectory(_SwiftSyntaxMacros)
41+
add_subdirectory(SwiftSyntaxMacros)
4242
add_subdirectory(IDEUtils)
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2022 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 SwiftDiagnostics
14+
import SwiftSyntax
15+
16+
/// An implementation of the `MacroExpansionContext` protocol that is
17+
/// suitable for testing purposes.
18+
public class BasicMacroExpansionContext {
19+
/// A single source file that is known to the macro expansion context.
20+
public struct KnownSourceFile {
21+
/// The name of the module in which this source file resides.
22+
let moduleName: String
23+
24+
/// The full path to the file.
25+
let fullFilePath: String
26+
27+
public init(moduleName: String, fullFilePath: String) {
28+
self.moduleName = moduleName
29+
self.fullFilePath = fullFilePath
30+
}
31+
}
32+
33+
/// Create a new macro evaluation context.
34+
public init(
35+
expansionDiscriminator: String = "__macro_local_",
36+
sourceFiles: [SourceFileSyntax: KnownSourceFile] = [:]
37+
) {
38+
self.expansionDiscriminator = expansionDiscriminator
39+
self.sourceFiles = sourceFiles
40+
}
41+
42+
/// The set of diagnostics that were emitted as part of expanding the
43+
/// macro.
44+
public private(set) var diagnostics: [Diagnostic] = []
45+
46+
/// Mapping from the root source file syntax nodes to the known source-file
47+
/// information about that source file.
48+
private var sourceFiles: [SourceFileSyntax: KnownSourceFile] = [:]
49+
50+
/// Mapping from intentionally-disconnected syntax node roots to the
51+
/// absolute offsets that have within a given source file, which is used
52+
/// to establish the link between a node that been intentionally disconnected
53+
/// from a source file to hide information from the macro implementation.
54+
private var disconnectedNodes: [Syntax: (SourceFileSyntax, Int)] = [:]
55+
56+
/// The macro expansion discriminator, which is used to form unique names
57+
/// when requested.
58+
///
59+
/// The expansion discriminator is combined with the `uniqueNames` counters
60+
/// to produce unique names.
61+
private var expansionDiscriminator: String = ""
62+
63+
/// Counter for each of the uniqued names.
64+
///
65+
/// Used in conjunction with `expansionDiscriminator`.
66+
private var uniqueNames: [String: Int] = [:]
67+
68+
}
69+
70+
extension BasicMacroExpansionContext {
71+
/// Note that the given node that was at the given position in the provided
72+
/// source file has been disconnected and is now a new root.
73+
private func addDisconnected<Node: SyntaxProtocol>(
74+
_ node: Node,
75+
at offset: AbsolutePosition,
76+
in sourceFile: SourceFileSyntax
77+
) {
78+
disconnectedNodes[Syntax(node)] = (sourceFile, offset.utf8Offset)
79+
}
80+
81+
/// Detach the given node, and record where it came from.
82+
public func detach<Node: SyntaxProtocol>(_ node: Node) -> Node {
83+
let detached = node.detach()
84+
85+
if let rootSourceFile = node.root.as(SourceFileSyntax.self) {
86+
addDisconnected(detached, at: node.position, in: rootSourceFile)
87+
}
88+
89+
return detached
90+
}
91+
}
92+
93+
extension String {
94+
/// Retrieve the base name of a string that represents a path, removing the
95+
/// directory.
96+
fileprivate var basename: String {
97+
guard let lastSlash = lastIndex(of: "/") else {
98+
return self
99+
}
100+
101+
return String(self[index(after: lastSlash)...])
102+
}
103+
104+
}
105+
extension BasicMacroExpansionContext: MacroExpansionContext {
106+
/// Generate a unique name for use in the macro.
107+
public func createUniqueName(_ providedName: String) -> TokenSyntax {
108+
// If provided with an empty name, substitute in something.
109+
let name = providedName.isEmpty ? "__local" : providedName
110+
111+
// Grab a unique index value for this name.
112+
let uniqueIndex = uniqueNames[name, default: 0]
113+
uniqueNames[name] = uniqueIndex + 1
114+
115+
// Start with the expansion discriminator.
116+
var resultString = expansionDiscriminator
117+
118+
// Mangle the name
119+
resultString += "\(name.count)\(name)"
120+
121+
// Mangle the operator for unique macro names.
122+
resultString += "fMu"
123+
124+
// Mangle the index.
125+
if uniqueIndex == 0 {
126+
resultString += "_"
127+
} else {
128+
resultString += "\(uniqueIndex - 1)"
129+
}
130+
131+
return TokenSyntax(.identifier(resultString), presence: .present)
132+
}
133+
134+
/// Produce a diagnostic while expanding the macro.
135+
public func diagnose(_ diagnostic: Diagnostic) {
136+
diagnostics.append(diagnostic)
137+
}
138+
139+
public func location<Node: SyntaxProtocol>(
140+
of node: Node,
141+
at position: PositionInSyntaxNode,
142+
filePathMode: SourceLocationFilePathMode
143+
) -> SourceLocation? {
144+
// Dig out the root source file and figure out how we need to adjust the
145+
// offset of the given syntax node to adjust for it.
146+
let rootSourceFile: SourceFileSyntax
147+
let offsetAdjustment: Int
148+
if let directRootSourceFile = node.root.as(SourceFileSyntax.self) {
149+
// The syntax node came from the source file itself.
150+
rootSourceFile = directRootSourceFile
151+
offsetAdjustment = 0
152+
} else if let (adjustedSourceFile, offset) = disconnectedNodes[Syntax(node)] {
153+
// The syntax node came from a disconnected root, so adjust for that.
154+
rootSourceFile = adjustedSourceFile
155+
offsetAdjustment = offset
156+
} else {
157+
return nil
158+
}
159+
160+
guard let knownRoot = sourceFiles[rootSourceFile] else {
161+
return nil
162+
}
163+
164+
// Determine the filename to use in the resulting location.
165+
let fileName: String
166+
switch filePathMode {
167+
case .fileID:
168+
fileName = "\(knownRoot.moduleName)/\(knownRoot.fullFilePath.basename)"
169+
170+
case .filePath:
171+
fileName = knownRoot.fullFilePath
172+
}
173+
174+
// Find the node's offset relative to its root.
175+
let rawPosition: AbsolutePosition
176+
switch position {
177+
case .beforeLeadingTrivia:
178+
rawPosition = node.position
179+
180+
case .afterLeadingTrivia:
181+
rawPosition = node.positionAfterSkippingLeadingTrivia
182+
183+
case .beforeTrailingTrivia:
184+
rawPosition = node.endPositionBeforeTrailingTrivia
185+
186+
case .afterTrailingTrivia:
187+
rawPosition = node.endPosition
188+
}
189+
190+
// Do the location lookup.
191+
let converter = SourceLocationConverter(file: fileName, tree: rootSourceFile)
192+
return converter.location(for: rawPosition.advanced(by: offsetAdjustment))
193+
}
194+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# This source file is part of the Swift.org open source project
2+
#
3+
# Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
4+
# Licensed under Apache License v2.0 with Runtime Library Exception
5+
#
6+
# See http://swift.org/LICENSE.txt for license information
7+
# See http://swift.org/CONTRIBUTORS.txt for Swift project authors
8+
9+
add_swift_host_library(SwiftSyntaxMacros
10+
MacroProtocols/AccessorMacro.swift
11+
MacroProtocols/AttachedMacro.swift
12+
MacroProtocols/CodeItemMacro.swift
13+
MacroProtocols/DeclarationMacro.swift
14+
MacroProtocols/ExpressionMacro.swift
15+
MacroProtocols/FreestandingMacro.swift
16+
MacroProtocols/Macro.swift
17+
MacroProtocols/MemberAttributeMacro.swift
18+
MacroProtocols/MemberMacro.swift
19+
MacroProtocols/PeerMacro.swift
20+
21+
BasicMacroExpansionContext.swift
22+
MacroExpansionContext.swift
23+
MacroSystem.swift
24+
Syntax+MacroEvaluation.swift
25+
)
26+
27+
target_link_libraries(SwiftSyntaxMacros PUBLIC
28+
SwiftParser
29+
SwiftSyntaxBuilder
30+
)
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2022 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+
import SwiftDiagnostics
15+
16+
/// Interface to extract information about the context in which a given
17+
/// macro is expanded.
18+
public protocol MacroExpansionContext: AnyObject {
19+
/// Generate a unique name for use in the macro.
20+
///
21+
/// - Parameters:
22+
/// - name: The name to use as a basis for the uniquely-generated name,
23+
/// which will appear in the unique name that's produced here.
24+
///
25+
/// - Returns: an identifier token containing a unique name that will not
26+
/// conflict with any other name in a well-formed program.
27+
func createUniqueName(_ name: String) -> TokenSyntax
28+
29+
/// Produce a diagnostic while expanding the macro.
30+
func diagnose(_ diagnostic: Diagnostic)
31+
32+
/// Retrieve a source location for the given syntax node.
33+
///
34+
/// - Parameters:
35+
/// - node: The syntax node whose source location to produce.
36+
/// - position: The position within the syntax node for the resulting
37+
/// location.
38+
/// - filePathMode: How the file name contained in the source location is
39+
/// formed.
40+
///
41+
/// - Returns: the source location within the given node, or `nil` if the
42+
/// given syntax node is not rooted in a source file that the macro
43+
/// expansion context knows about.
44+
func location<Node: SyntaxProtocol>(
45+
of node: Node,
46+
at position: PositionInSyntaxNode,
47+
filePathMode: SourceLocationFilePathMode
48+
) -> SourceLocation?
49+
}
50+
51+
extension MacroExpansionContext {
52+
/// Retrieve a source location for the given syntax node's starting token
53+
/// (after leading trivia) using file naming according to `#fileID`.
54+
///
55+
/// - Parameters:
56+
/// - node: The syntax node whose source location to produce.
57+
///
58+
/// - Returns: the source location within the given node, or `nil` if the
59+
/// given syntax node is not rooted in a source file that the macro
60+
/// expansion context knows about.
61+
public func location<Node: SyntaxProtocol>(
62+
of node: Node
63+
) -> SourceLocation? {
64+
return location(of: node, at: .afterLeadingTrivia, filePathMode: .fileID)
65+
}
66+
}
67+
68+
/// Describe the position within a syntax node that can be used to compute
69+
/// source locations.
70+
public enum PositionInSyntaxNode {
71+
/// Refers to the start of the syntax node's leading trivia, which is
72+
/// the first source location covered by the syntax node.
73+
case beforeLeadingTrivia
74+
75+
/// Refers to the start of the syntax node's first token, which
76+
/// immediately follows the leading trivia.
77+
case afterLeadingTrivia
78+
79+
/// Refers to the end of the syntax node's last token, right before the
80+
/// trailing trivia.
81+
case beforeTrailingTrivia
82+
83+
/// Refers just past the end of the source text that is covered by the
84+
/// syntax node, after all trailing trivia.
85+
case afterTrailingTrivia
86+
}
87+
88+
/// Describes how the a source location file path
89+
public enum SourceLocationFilePathMode {
90+
/// A file ID consisting of the module name and file name (without full path),
91+
/// as would be generated by the macro expansion `#fileID`.
92+
case fileID
93+
94+
/// A full path name as would be generated by the macro expansion `#filePath`,
95+
/// e.g., `/home/taylor/alison.swift`.
96+
case filePath
97+
}

Sources/_SwiftSyntaxMacros/AccessorMacro.swift renamed to Sources/SwiftSyntaxMacros/MacroProtocols/AccessorMacro.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@ public protocol AccessorMacro: AttachedMacro {
1414
/// Expand a macro that's expressed as a custom attribute attached to
1515
/// the given declaration. The result is a set of accessors for the
1616
/// declaration.
17-
static func expansion(
17+
static func expansion<
18+
Context: MacroExpansionContext,
19+
Declaration: DeclSyntaxProtocol
20+
>(
1821
of node: AttributeSyntax,
19-
attachedTo declaration: DeclSyntax,
20-
in context: inout MacroExpansionContext
22+
providingAccessorsOf declaration: Declaration,
23+
in context: Context
2124
) throws -> [AccessorDeclSyntax]
2225
}
2326

Sources/_SwiftSyntaxMacros/CodeItemMacro.swift renamed to Sources/SwiftSyntaxMacros/MacroProtocols/CodeItemMacro.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import SwiftSyntax
1313
public protocol CodeItemMacro: FreestandingMacro {
1414
/// Expand a macro described by the given freestanding macro expansion
1515
/// declaration within the given context to produce a set of declarations.
16-
static func expansion(
16+
static func expansion<Context: MacroExpansionContext>(
1717
of node: MacroExpansionDeclSyntax,
18-
in context: inout MacroExpansionContext
18+
in context: Context
1919
) throws -> [CodeBlockItemSyntax]
2020
}

Sources/_SwiftSyntaxMacros/DeclarationMacro.swift renamed to Sources/SwiftSyntaxMacros/MacroProtocols/DeclarationMacro.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ import SwiftSyntax
1313
public protocol DeclarationMacro: FreestandingMacro {
1414
/// Expand a macro described by the given freestanding macro expansion
1515
/// declaration within the given context to produce a set of declarations.
16-
static func expansion(
16+
static func expansion<
17+
Context: MacroExpansionContext
18+
>(
1719
of node: MacroExpansionDeclSyntax,
18-
in context: inout MacroExpansionContext
20+
in context: Context
1921
) throws -> [DeclSyntax]
2022
}
2123

0 commit comments

Comments
 (0)