Skip to content

Rework MacroExpansionContext and macro protocols #1288

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

Merged
Merged
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
6 changes: 3 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ let package = Package(
.library(name: "SwiftSyntax", type: .static, targets: ["SwiftSyntax"]),
.library(name: "SwiftSyntaxParser", type: .static, targets: ["SwiftSyntaxParser"]),
.library(name: "SwiftSyntaxBuilder", type: .static, targets: ["SwiftSyntaxBuilder"]),
.library(name: "_SwiftSyntaxMacros", type: .static, targets: ["_SwiftSyntaxMacros"]),
.library(name: "SwiftSyntaxMacros", type: .static, targets: ["SwiftSyntaxMacros"]),
.library(name: "SwiftRefactor", type: .static, targets: ["SwiftRefactor"]),
],
targets: [
Expand Down Expand Up @@ -115,7 +115,7 @@ let package = Package(
]
),
.target(
name: "_SwiftSyntaxMacros",
name: "SwiftSyntaxMacros",
dependencies: [
"SwiftSyntax", "SwiftSyntaxBuilder", "SwiftParser", "SwiftDiagnostics"
],
Expand Down Expand Up @@ -158,7 +158,7 @@ let package = Package(
name: "SwiftSyntaxMacrosTest",
dependencies: ["SwiftDiagnostics", "SwiftOperators", "SwiftParser",
"_SwiftSyntaxTestSupport", "SwiftSyntaxBuilder",
"_SwiftSyntaxMacros"]
"SwiftSyntaxMacros"]
),
.testTarget(
name: "PerformanceTest",
Expand Down
2 changes: 1 addition & 1 deletion Sources/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,5 @@ add_subdirectory(SwiftParser)
add_subdirectory(SwiftParserDiagnostics)
add_subdirectory(SwiftOperators)
add_subdirectory(SwiftSyntaxBuilder)
add_subdirectory(_SwiftSyntaxMacros)
add_subdirectory(SwiftSyntaxMacros)
add_subdirectory(IDEUtils)
194 changes: 194 additions & 0 deletions Sources/SwiftSyntaxMacros/BasicMacroExpansionContext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import SwiftDiagnostics
import SwiftSyntax

/// An implementation of the `MacroExpansionContext` protocol that is
/// suitable for testing purposes.
public class BasicMacroExpansionContext {
/// A single source file that is known to the macro expansion context.
public struct KnownSourceFile {
/// The name of the module in which this source file resides.
let moduleName: String

/// The full path to the file.
let fullFilePath: String

public init(moduleName: String, fullFilePath: String) {
self.moduleName = moduleName
self.fullFilePath = fullFilePath
}
}

/// Create a new macro evaluation context.
public init(
expansionDiscriminator: String = "__macro_local_",
sourceFiles: [SourceFileSyntax: KnownSourceFile] = [:]
) {
self.expansionDiscriminator = expansionDiscriminator
self.sourceFiles = sourceFiles
}

/// The set of diagnostics that were emitted as part of expanding the
/// macro.
public private(set) var diagnostics: [Diagnostic] = []

/// Mapping from the root source file syntax nodes to the known source-file
/// information about that source file.
private var sourceFiles: [SourceFileSyntax: KnownSourceFile] = [:]

/// Mapping from intentionally-disconnected syntax node roots to the
/// absolute offsets that have within a given source file, which is used
/// to establish the link between a node that been intentionally disconnected
/// from a source file to hide information from the macro implementation.
private var disconnectedNodes: [Syntax: (SourceFileSyntax, Int)] = [:]

/// The macro expansion discriminator, which is used to form unique names
/// when requested.
///
/// The expansion discriminator is combined with the `uniqueNames` counters
/// to produce unique names.
private var expansionDiscriminator: String = ""

/// Counter for each of the uniqued names.
///
/// Used in conjunction with `expansionDiscriminator`.
private var uniqueNames: [String: Int] = [:]

}

extension BasicMacroExpansionContext {
/// Note that the given node that was at the given position in the provided
/// source file has been disconnected and is now a new root.
private func addDisconnected<Node: SyntaxProtocol>(
_ node: Node,
at offset: AbsolutePosition,
in sourceFile: SourceFileSyntax
) {
disconnectedNodes[Syntax(node)] = (sourceFile, offset.utf8Offset)
}

/// Detach the given node, and record where it came from.
public func detach<Node: SyntaxProtocol>(_ node: Node) -> Node {
let detached = node.detach()

if let rootSourceFile = node.root.as(SourceFileSyntax.self) {
addDisconnected(detached, at: node.position, in: rootSourceFile)
}

return detached
}
}

extension String {
/// Retrieve the base name of a string that represents a path, removing the
/// directory.
fileprivate var basename: String {
guard let lastSlash = lastIndex(of: "/") else {
return self
}

return String(self[index(after: lastSlash)...])
}

}
extension BasicMacroExpansionContext: MacroExpansionContext {
/// Generate a unique name for use in the macro.
public func createUniqueName(_ providedName: String) -> TokenSyntax {
// If provided with an empty name, substitute in something.
let name = providedName.isEmpty ? "__local" : providedName

// Grab a unique index value for this name.
let uniqueIndex = uniqueNames[name, default: 0]
uniqueNames[name] = uniqueIndex + 1

// Start with the expansion discriminator.
var resultString = expansionDiscriminator

// Mangle the name
resultString += "\(name.count)\(name)"

// Mangle the operator for unique macro names.
resultString += "fMu"

// Mangle the index.
if uniqueIndex == 0 {
resultString += "_"
} else {
resultString += "\(uniqueIndex - 1)"
}

return TokenSyntax(.identifier(resultString), presence: .present)
}

/// Produce a diagnostic while expanding the macro.
public func diagnose(_ diagnostic: Diagnostic) {
diagnostics.append(diagnostic)
}

public func location<Node: SyntaxProtocol>(
of node: Node,
at position: PositionInSyntaxNode,
filePathMode: SourceLocationFilePathMode
) -> SourceLocation? {
// Dig out the root source file and figure out how we need to adjust the
// offset of the given syntax node to adjust for it.
let rootSourceFile: SourceFileSyntax
let offsetAdjustment: Int
if let directRootSourceFile = node.root.as(SourceFileSyntax.self) {
// The syntax node came from the source file itself.
rootSourceFile = directRootSourceFile
offsetAdjustment = 0
} else if let (adjustedSourceFile, offset) = disconnectedNodes[Syntax(node)] {
// The syntax node came from a disconnected root, so adjust for that.
rootSourceFile = adjustedSourceFile
offsetAdjustment = offset
} else {
return nil
}

guard let knownRoot = sourceFiles[rootSourceFile] else {
return nil
}

// Determine the filename to use in the resulting location.
let fileName: String
switch filePathMode {
case .fileID:
fileName = "\(knownRoot.moduleName)/\(knownRoot.fullFilePath.basename)"

case .filePath:
fileName = knownRoot.fullFilePath
}

// Find the node's offset relative to its root.
let rawPosition: AbsolutePosition
switch position {
case .beforeLeadingTrivia:
rawPosition = node.position

case .afterLeadingTrivia:
rawPosition = node.positionAfterSkippingLeadingTrivia

case .beforeTrailingTrivia:
rawPosition = node.endPositionBeforeTrailingTrivia

case .afterTrailingTrivia:
rawPosition = node.endPosition
}

// Do the location lookup.
let converter = SourceLocationConverter(file: fileName, tree: rootSourceFile)
return converter.location(for: rawPosition.advanced(by: offsetAdjustment))
}
}
30 changes: 30 additions & 0 deletions Sources/SwiftSyntaxMacros/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# This source file is part of the Swift.org open source project
#
# Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
# Licensed under Apache License v2.0 with Runtime Library Exception
#
# See http://swift.org/LICENSE.txt for license information
# See http://swift.org/CONTRIBUTORS.txt for Swift project authors

add_swift_host_library(SwiftSyntaxMacros
MacroProtocols/AccessorMacro.swift
MacroProtocols/AttachedMacro.swift
MacroProtocols/CodeItemMacro.swift
MacroProtocols/DeclarationMacro.swift
MacroProtocols/ExpressionMacro.swift
MacroProtocols/FreestandingMacro.swift
MacroProtocols/Macro.swift
MacroProtocols/MemberAttributeMacro.swift
MacroProtocols/MemberMacro.swift
MacroProtocols/PeerMacro.swift

BasicMacroExpansionContext.swift
MacroExpansionContext.swift
MacroSystem.swift
Syntax+MacroEvaluation.swift
)

target_link_libraries(SwiftSyntaxMacros PUBLIC
SwiftParser
SwiftSyntaxBuilder
)
97 changes: 97 additions & 0 deletions Sources/SwiftSyntaxMacros/MacroExpansionContext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import SwiftSyntax
import SwiftDiagnostics

/// Interface to extract information about the context in which a given
/// macro is expanded.
public protocol MacroExpansionContext: AnyObject {
/// Generate a unique name for use in the macro.
///
/// - Parameters:
/// - name: The name to use as a basis for the uniquely-generated name,
/// which will appear in the unique name that's produced here.
///
/// - Returns: an identifier token containing a unique name that will not
/// conflict with any other name in a well-formed program.
func createUniqueName(_ name: String) -> TokenSyntax

/// Produce a diagnostic while expanding the macro.
func diagnose(_ diagnostic: Diagnostic)

/// Retrieve a source location for the given syntax node.
///
/// - Parameters:
/// - node: The syntax node whose source location to produce.
/// - position: The position within the syntax node for the resulting
/// location.
/// - filePathMode: How the file name contained in the source location is
/// formed.
///
/// - Returns: the source location within the given node, or `nil` if the
/// given syntax node is not rooted in a source file that the macro
/// expansion context knows about.
func location<Node: SyntaxProtocol>(
of node: Node,
at position: PositionInSyntaxNode,
filePathMode: SourceLocationFilePathMode
) -> SourceLocation?
}

extension MacroExpansionContext {
/// Retrieve a source location for the given syntax node's starting token
/// (after leading trivia) using file naming according to `#fileID`.
///
/// - Parameters:
/// - node: The syntax node whose source location to produce.
///
/// - Returns: the source location within the given node, or `nil` if the
/// given syntax node is not rooted in a source file that the macro
/// expansion context knows about.
public func location<Node: SyntaxProtocol>(
of node: Node
) -> SourceLocation? {
return location(of: node, at: .afterLeadingTrivia, filePathMode: .fileID)
}
}

/// Describe the position within a syntax node that can be used to compute
/// source locations.
public enum PositionInSyntaxNode {
/// Refers to the start of the syntax node's leading trivia, which is
/// the first source location covered by the syntax node.
case beforeLeadingTrivia

/// Refers to the start of the syntax node's first token, which
/// immediately follows the leading trivia.
case afterLeadingTrivia

/// Refers to the end of the syntax node's last token, right before the
/// trailing trivia.
case beforeTrailingTrivia

/// Refers just past the end of the source text that is covered by the
/// syntax node, after all trailing trivia.
case afterTrailingTrivia
}

/// Describes how the a source location file path
public enum SourceLocationFilePathMode {
/// A file ID consisting of the module name and file name (without full path),
/// as would be generated by the macro expansion `#fileID`.
case fileID

/// A full path name as would be generated by the macro expansion `#filePath`,
/// e.g., `/home/taylor/alison.swift`.
case filePath
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ public protocol AccessorMacro: AttachedMacro {
/// Expand a macro that's expressed as a custom attribute attached to
/// the given declaration. The result is a set of accessors for the
/// declaration.
static func expansion(
static func expansion<
Context: MacroExpansionContext,
Declaration: DeclSyntaxProtocol
>(
of node: AttributeSyntax,
attachedTo declaration: DeclSyntax,
in context: inout MacroExpansionContext
providingAccessorsOf declaration: Declaration,
in context: Context
) throws -> [AccessorDeclSyntax]
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import SwiftSyntax
public protocol CodeItemMacro: FreestandingMacro {
/// Expand a macro described by the given freestanding macro expansion
/// declaration within the given context to produce a set of declarations.
static func expansion(
static func expansion<Context: MacroExpansionContext>(
of node: MacroExpansionDeclSyntax,
in context: inout MacroExpansionContext
in context: Context
) throws -> [CodeBlockItemSyntax]
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import SwiftSyntax
public protocol DeclarationMacro: FreestandingMacro {
/// Expand a macro described by the given freestanding macro expansion
/// declaration within the given context to produce a set of declarations.
static func expansion(
static func expansion<
Context: MacroExpansionContext
>(
of node: MacroExpansionDeclSyntax,
in context: inout MacroExpansionContext
in context: Context
) throws -> [DeclSyntax]
}

Expand Down
Loading