diff --git a/Release Notes/511.md b/Release Notes/511.md index bfe25b3aa22..7f751f7f8ed 100644 --- a/Release Notes/511.md +++ b/Release Notes/511.md @@ -36,6 +36,10 @@ - Description: `SwiftParser` adds an extension on `String` to check if it can be used as an identifier in a given context. - Pull Request: https://github.com/apple/swift-syntax/pull/2434 +- `SyntaxProtocol.asMacroLexicalContext()` and `allMacroLexicalContexts(enclosingSyntax:)` + - Description: Produce the lexical context for a given syntax node (if it has one), or the entire stack of lexical contexts enclosing a syntax node, for use in macro expansion. + - Pull request: https://github.com/apple/swift-syntax/pull/1554 + ## API Behavior Changes ## Deprecations @@ -93,6 +97,11 @@ - The new cases cover the newly introduced `ThrowsClauseSyntax` - Pull request: https://github.com/apple/swift-syntax/pull/2379 - Migration steps: In exhaustive switches over `SyntaxEnum` and `SyntaxKind`, cover the new case. + +- `MacroExpansionContext` now requires a property `lexicalContext`: + - Description: The new property provides the lexical context in which the macro is expanded, and has several paired API changes. Types that conform to `MacroExpansionContext` will need to implement this property. Additionally, the `HostToPluginMessage` cases `expandFreestandingMacro` and `expandAttachedMacro` now include an optional `lexicalContext`. Finally, the `SyntaxProtocol.expand(macros:in:indentationWidth:)` syntactic expansion operation has been deprecated in favor of a new version `expand(macros:contextGenerator:indentationWidth:)` that takes a function produces a new macro expansion context for each expansion. + - Pull request: https://github.com/apple/swift-syntax/pull/1554 + - Migration steps: Add the new property `lexicalContext` to any `MacroExpansionContext`-conforming types. If implementing the host-to-plugin message protocol, add support for `lexicalContext`. For macro expansion operations going through `SyntaxProtocol.expand`, provide a context generator that creates a fresh context including the lexical context. ## Template diff --git a/Sources/SwiftCompilerPluginMessageHandling/CompilerPluginMessageHandler.swift b/Sources/SwiftCompilerPluginMessageHandling/CompilerPluginMessageHandler.swift index dd836a2f73c..53dbf84f980 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/CompilerPluginMessageHandler.swift +++ b/Sources/SwiftCompilerPluginMessageHandling/CompilerPluginMessageHandler.swift @@ -114,12 +114,19 @@ extension CompilerPluginMessageHandler { ) try self.sendMessage(.getCapabilityResult(capability: capability)) - case .expandFreestandingMacro(let macro, let macroRole, let discriminator, let expandingSyntax): + case .expandFreestandingMacro( + let macro, + let macroRole, + let discriminator, + let expandingSyntax, + let lexicalContext + ): try expandFreestandingMacro( macro: macro, macroRole: macroRole, discriminator: discriminator, - expandingSyntax: expandingSyntax + expandingSyntax: expandingSyntax, + lexicalContext: lexicalContext ) case .expandAttachedMacro( @@ -130,7 +137,8 @@ extension CompilerPluginMessageHandler { let declSyntax, let parentDeclSyntax, let extendedTypeSyntax, - let conformanceListSyntax + let conformanceListSyntax, + let lexicalContext ): try expandAttachedMacro( macro: macro, @@ -140,7 +148,8 @@ extension CompilerPluginMessageHandler { declSyntax: declSyntax, parentDeclSyntax: parentDeclSyntax, extendedTypeSyntax: extendedTypeSyntax, - conformanceListSyntax: conformanceListSyntax + conformanceListSyntax: conformanceListSyntax, + lexicalContext: lexicalContext ) case .loadPluginLibrary(let libraryPath, let moduleName): diff --git a/Sources/SwiftCompilerPluginMessageHandling/Macros.swift b/Sources/SwiftCompilerPluginMessageHandling/Macros.swift index 6b3642bf8a9..4687cbb00ed 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/Macros.swift +++ b/Sources/SwiftCompilerPluginMessageHandling/Macros.swift @@ -23,18 +23,41 @@ extension CompilerPluginMessageHandler { try provider.resolveMacro(moduleName: ref.moduleName, typeName: ref.typeName) } + /// Resolve the lexical context + private static func resolveLexicalContext( + _ lexicalContext: [PluginMessage.Syntax]?, + sourceManager: SourceManager, + operatorTable: OperatorTable, + fallbackSyntax: some SyntaxProtocol + ) -> [Syntax] { + // If we weren't provided with a lexical context, retrieve it from the + // syntax node we were given. This is for dealing with older compilers. + guard let lexicalContext else { + return fallbackSyntax.allMacroLexicalContexts() + } + + return lexicalContext.map { sourceManager.add($0, foldingWith: operatorTable) } + } + /// Expand `@freestainding(XXX)` macros. func expandFreestandingMacro( macro: PluginMessage.MacroReference, macroRole pluginMacroRole: PluginMessage.MacroRole?, discriminator: String, - expandingSyntax: PluginMessage.Syntax + expandingSyntax: PluginMessage.Syntax, + lexicalContext: [PluginMessage.Syntax]? ) throws { let sourceManager = SourceManager() let syntax = sourceManager.add(expandingSyntax, foldingWith: .standardOperators) let context = PluginMacroExpansionContext( sourceManager: sourceManager, + lexicalContext: Self.resolveLexicalContext( + lexicalContext, + sourceManager: sourceManager, + operatorTable: .standardOperators, + fallbackSyntax: syntax + ), expansionDiscriminator: discriminator ) @@ -85,14 +108,10 @@ extension CompilerPluginMessageHandler { declSyntax: PluginMessage.Syntax, parentDeclSyntax: PluginMessage.Syntax?, extendedTypeSyntax: PluginMessage.Syntax?, - conformanceListSyntax: PluginMessage.Syntax? + conformanceListSyntax: PluginMessage.Syntax?, + lexicalContext: [PluginMessage.Syntax]? ) throws { let sourceManager = SourceManager() - let context = PluginMacroExpansionContext( - sourceManager: sourceManager, - expansionDiscriminator: discriminator - ) - let attributeNode = sourceManager.add( attributeSyntax, foldingWith: .standardOperators @@ -107,6 +126,17 @@ extension CompilerPluginMessageHandler { return placeholderStruct.inheritanceClause!.inheritedTypes } + let context = PluginMacroExpansionContext( + sourceManager: sourceManager, + lexicalContext: Self.resolveLexicalContext( + lexicalContext, + sourceManager: sourceManager, + operatorTable: .standardOperators, + fallbackSyntax: declarationNode + ), + expansionDiscriminator: discriminator + ) + // TODO: Make this a 'String?' and remove non-'hasExpandMacroResult' branches. let expandedSources: [String]? do { diff --git a/Sources/SwiftCompilerPluginMessageHandling/PluginMacroExpansionContext.swift b/Sources/SwiftCompilerPluginMessageHandling/PluginMacroExpansionContext.swift index b574c104a70..8dea01f6655 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/PluginMacroExpansionContext.swift +++ b/Sources/SwiftCompilerPluginMessageHandling/PluginMacroExpansionContext.swift @@ -192,6 +192,9 @@ fileprivate extension Syntax { class PluginMacroExpansionContext { private var sourceManger: SourceManager + /// The lexical context of the macro expansion described by this context. + let lexicalContext: [Syntax] + /// The macro expansion discriminator, which is used to form unique names /// when requested. /// @@ -208,8 +211,9 @@ class PluginMacroExpansionContext { /// macro. internal private(set) var diagnostics: [Diagnostic] = [] - init(sourceManager: SourceManager, expansionDiscriminator: String = "") { + init(sourceManager: SourceManager, lexicalContext: [Syntax], expansionDiscriminator: String = "") { self.sourceManger = sourceManager + self.lexicalContext = lexicalContext self.expansionDiscriminator = expansionDiscriminator } } diff --git a/Sources/SwiftCompilerPluginMessageHandling/PluginMessages.swift b/Sources/SwiftCompilerPluginMessageHandling/PluginMessages.swift index f81e7842720..10ecd96a4bd 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/PluginMessages.swift +++ b/Sources/SwiftCompilerPluginMessageHandling/PluginMessages.swift @@ -23,7 +23,8 @@ public enum HostToPluginMessage: Codable { macro: PluginMessage.MacroReference, macroRole: PluginMessage.MacroRole? = nil, discriminator: String, - syntax: PluginMessage.Syntax + syntax: PluginMessage.Syntax, + lexicalContext: [PluginMessage.Syntax]? = nil ) /// Expand an '@attached' macro. @@ -35,7 +36,8 @@ public enum HostToPluginMessage: Codable { declSyntax: PluginMessage.Syntax, parentDeclSyntax: PluginMessage.Syntax?, extendedTypeSyntax: PluginMessage.Syntax?, - conformanceListSyntax: PluginMessage.Syntax? + conformanceListSyntax: PluginMessage.Syntax?, + lexicalContext: [PluginMessage.Syntax]? = nil ) /// Optionally implemented message to load a dynamic link library. diff --git a/Sources/SwiftSyntaxMacroExpansion/BasicMacroExpansionContext.swift b/Sources/SwiftSyntaxMacroExpansion/BasicMacroExpansionContext.swift index dd4e8f59c78..b83709e3cb5 100644 --- a/Sources/SwiftSyntaxMacroExpansion/BasicMacroExpansionContext.swift +++ b/Sources/SwiftSyntaxMacroExpansion/BasicMacroExpansionContext.swift @@ -32,30 +32,37 @@ public class BasicMacroExpansionContext { } } - /// Create a new macro evaluation context. - public init( - expansionDiscriminator: String = "__macro_local_", - sourceFiles: [SourceFileSyntax: KnownSourceFile] = [:] - ) { - self.expansionDiscriminator = expansionDiscriminator - self.sourceFiles = sourceFiles + /// Describes state that is shared amongst all instances of the basic + /// macro expansion context. + private class SharedState { + /// The set of diagnostics that were emitted as part of expanding the + /// macro. + var diagnostics: [Diagnostic] = [] + + /// Mapping from the root source file syntax nodes to the known source-file + /// information about that source file. + var sourceFiles: [SourceFileSyntax: KnownSourceFile] = [:] + + /// Mapping from intentionally-disconnected syntax nodes to the corresponding + /// nodes in the original source file. + /// + /// This is used to establish the link between a node that been intentionally + /// disconnected from a source file to hide information from the macro + /// implementation. + var detachedNodes: [Syntax: Syntax] = [:] + + /// Counter for each of the uniqued names. + /// + /// Used in conjunction with `expansionDiscriminator`. + var uniqueNames: [String: Int] = [:] } - /// 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] = [:] + /// State shared by different instances of the macro expansion context, + /// which includes information about detached nodes and source file names. + private var sharedState: SharedState - /// Mapping from intentionally-disconnected syntax nodes to the corresponding - /// nodes in the original source file. - /// - /// This 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 detachedNodes: [Syntax: Syntax] = [:] + /// The lexical context of the macro expansion described by this context. + public let lexicalContext: [Syntax] /// The macro expansion discriminator, which is used to form unique names /// when requested. @@ -64,18 +71,41 @@ public class BasicMacroExpansionContext { /// 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] = [:] + /// Create a new macro evaluation context. + public init( + lexicalContext: [Syntax] = [], + expansionDiscriminator: String = "__macro_local_", + sourceFiles: [SourceFileSyntax: KnownSourceFile] = [:] + ) { + self.sharedState = SharedState() + self.lexicalContext = lexicalContext + self.expansionDiscriminator = expansionDiscriminator + self.sharedState.sourceFiles = sourceFiles + } + + /// Create a new macro evaluation context that shares most of its global + /// state (detached nodes, diagnostics, etc.) with the given context. + public init(sharingWith context: BasicMacroExpansionContext, lexicalContext: [Syntax]) { + self.sharedState = context.sharedState + self.lexicalContext = lexicalContext + self.expansionDiscriminator = context.expansionDiscriminator + } +} +extension BasicMacroExpansionContext { + /// The set of diagnostics that were emitted as part of expanding the + /// macro. + public private(set) var diagnostics: [Diagnostic] { + get { sharedState.diagnostics } + set { sharedState.diagnostics = newValue } + } } extension BasicMacroExpansionContext { /// Detach the given node, and record where it came from. public func detach(_ node: Node) -> Node { let detached = node.detached - detachedNodes[Syntax(detached)] = Syntax(node) + sharedState.detachedNodes[Syntax(detached)] = Syntax(node) return detached } @@ -88,7 +118,7 @@ extension BasicMacroExpansionContext { { // Folding operators doesn't change the source file and its associated locations // Record the `KnownSourceFile` information for the folded tree. - sourceFiles[newSourceFile] = sourceFiles[originalSourceFile] + sharedState.sourceFiles[newSourceFile] = sharedState.sourceFiles[originalSourceFile] } return folded } @@ -113,8 +143,8 @@ extension BasicMacroExpansionContext: MacroExpansionContext { let name = providedName.isEmpty ? "__local" : providedName // Grab a unique index value for this name. - let uniqueIndex = uniqueNames[name, default: 0] - uniqueNames[name] = uniqueIndex + 1 + let uniqueIndex = sharedState.uniqueNames[name, default: 0] + sharedState.uniqueNames[name] = uniqueIndex + 1 // Start with the expansion discriminator. var resultString = expansionDiscriminator @@ -153,7 +183,7 @@ extension BasicMacroExpansionContext: MacroExpansionContext { anchoredAt node: Syntax, fileName: String ) -> SourceLocation { - guard let nodeInOriginalTree = detachedNodes[node.root] else { + guard let nodeInOriginalTree = sharedState.detachedNodes[node.root] else { return SourceLocationConverter(fileName: fileName, tree: node.root).location(for: position) } let adjustedPosition = position + SourceLength(utf8Length: nodeInOriginalTree.position.utf8Offset) @@ -173,7 +203,7 @@ extension BasicMacroExpansionContext: MacroExpansionContext { // The syntax node came from the source file itself. rootSourceFile = directRootSourceFile offsetAdjustment = .zero - } else if let nodeInOriginalTree = detachedNodes[Syntax(node)] { + } else if let nodeInOriginalTree = sharedState.detachedNodes[Syntax(node)] { // The syntax node came from a disconnected root, so adjust for that. rootSourceFile = nodeInOriginalTree.root.as(SourceFileSyntax.self) offsetAdjustment = SourceLength(utf8Length: nodeInOriginalTree.position.utf8Offset) @@ -181,7 +211,7 @@ extension BasicMacroExpansionContext: MacroExpansionContext { return nil } - guard let rootSourceFile, let knownRoot = sourceFiles[rootSourceFile] else { + guard let rootSourceFile, let knownRoot = sharedState.sourceFiles[rootSourceFile] else { return nil } diff --git a/Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift b/Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift index 90f8205072c..fff217dbd4c 100644 --- a/Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift +++ b/Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift @@ -21,10 +21,24 @@ import SwiftSyntaxBuilder extension SyntaxProtocol { /// Expand all uses of the given set of macros within this syntax node. + @available(*, deprecated, message: "Use contextGenerator form to produce a specific context for each expansion node") public func expand( macros: [String: Macro.Type], in context: some MacroExpansionContext, indentationWidth: Trivia? = nil + ) -> Syntax { + return expand( + macros: macros, + contextGenerator: { _ in context }, + indentationWidth: indentationWidth + ) + } + + /// Expand all uses of the given set of macros within this syntax node. + public func expand( + macros: [String: Macro.Type], + contextGenerator: @escaping (Syntax) -> Context, + indentationWidth: Trivia? = nil ) -> Syntax { // Build the macro system. var system = MacroSystem() @@ -34,7 +48,7 @@ extension SyntaxProtocol { let applier = MacroApplication( macroSystem: system, - context: context, + contextGenerator: contextGenerator, indentationWidth: indentationWidth ) @@ -595,7 +609,7 @@ private enum MacroApplicationError: DiagnosticMessage, Error { /// Syntax rewriter that evaluates any macros encountered along the way. private class MacroApplication: SyntaxRewriter { let macroSystem: MacroSystem - var context: Context + var contextGenerator: (Syntax) -> Context var indentationWidth: Trivia /// Nodes that we are currently handling in `visitAny` and that should be /// visited using the node-specific handling function. @@ -607,11 +621,12 @@ private class MacroApplication: SyntaxRewriter { init( macroSystem: MacroSystem, - context: Context, + contextGenerator: @escaping (Syntax) -> Context, indentationWidth: Trivia? ) { self.macroSystem = macroSystem - self.context = context + self.contextGenerator = contextGenerator + // Default to 4 spaces if no indentation was passed. // In the future, we could consider inferring the indentation width from the // source file in which we expand the macros. @@ -670,7 +685,7 @@ private class MacroApplication: SyntaxRewriter { definition: definition, attributeNode: attributeNode, attachedTo: node, - in: context, + in: contextGenerator(Syntax(node)), indentationWidth: indentationWidth ) } @@ -681,7 +696,7 @@ private class MacroApplication: SyntaxRewriter { definition: definition, attributeNode: attributeNode, attachedTo: node, - in: context, + in: contextGenerator(Syntax(node)), indentationWidth: indentationWidth ).map { [$0] } } @@ -697,7 +712,7 @@ private class MacroApplication: SyntaxRewriter { guard let existingBody = node.body else { // Any leftover preamble statements have nowhere to go, complain and // exit. - context.addDiagnostics(from: MacroExpansionError.preambleWithoutBody, node: node) + contextGenerator(Syntax(node)).addDiagnostics(from: MacroExpansionError.preambleWithoutBody, node: node) return node } @@ -708,7 +723,7 @@ private class MacroApplication: SyntaxRewriter { body = expandedBodies[0] default: - context.addDiagnostics(from: MacroExpansionError.moreThanOneBodyMacro, node: node) + contextGenerator(Syntax(node)).addDiagnostics(from: MacroExpansionError.moreThanOneBodyMacro, node: node) body = expandedBodies[0] } @@ -855,7 +870,7 @@ private class MacroApplication: SyntaxRewriter { } guard node.bindings.count == 1, let binding = node.bindings.first else { - context.addDiagnostics(from: MacroApplicationError.accessorMacroOnVariableWithMultipleBindings, node: node) + contextGenerator(Syntax(node)).addDiagnostics(from: MacroApplicationError.accessorMacroOnVariableWithMultipleBindings, node: node) return DeclSyntax(node) } @@ -944,7 +959,7 @@ extension MacroApplication { result += expanded } } catch { - context.addDiagnostics(from: error, node: macroAttribute.attributeNode) + contextGenerator(Syntax(decl)).addDiagnostics(from: error, node: macroAttribute.attributeNode) } } return result @@ -963,7 +978,7 @@ extension MacroApplication { definition: definition, attributeNode: attributeNode, attachedTo: decl, - in: context, + in: contextGenerator(Syntax(decl)), indentationWidth: indentationWidth ) } @@ -983,7 +998,7 @@ extension MacroApplication { definition: definition, attributeNode: attributeNode, attachedTo: decl, - in: context, + in: contextGenerator(Syntax(decl)), indentationWidth: indentationWidth ) } @@ -998,7 +1013,7 @@ extension MacroApplication { definition: definition, attributeNode: attributeNode, attachedTo: decl, - in: context, + in: contextGenerator(Syntax(decl)), indentationWidth: indentationWidth ) } @@ -1011,7 +1026,7 @@ extension MacroApplication { definition: definition, attributeNode: attributeNode, attachedTo: decl, - in: context, + in: contextGenerator(Syntax(decl)), indentationWidth: indentationWidth ) } @@ -1032,7 +1047,7 @@ extension MacroApplication { attributeNode: attributeNode, attachedTo: parentDecl, providingAttributeFor: decl, - in: context, + in: contextGenerator(Syntax(decl)), indentationWidth: indentationWidth ) } @@ -1069,7 +1084,7 @@ extension MacroApplication { definition: macro.definition, attributeNode: macro.attributeNode, attachedTo: DeclSyntax(storage), - in: context, + in: contextGenerator(Syntax(storage)), indentationWidth: indentationWidth ) { checkExpansions(newAccessors) @@ -1086,7 +1101,7 @@ extension MacroApplication { definition: macro.definition, attributeNode: macro.attributeNode, attachedTo: DeclSyntax(storage), - in: context, + in: contextGenerator(Syntax(storage)), indentationWidth: indentationWidth ) { guard case .accessors(let accessorList) = newAccessors.accessors else { @@ -1105,7 +1120,7 @@ extension MacroApplication { } } } catch { - context.addDiagnostics(from: error, node: macro.attributeNode) + contextGenerator(Syntax(storage)).addDiagnostics(from: error, node: macro.attributeNode) } } return (newAccessorsBlock, expandsGetSet) @@ -1143,7 +1158,7 @@ extension MacroApplication { return .failure } } catch { - context.addDiagnostics(from: error, node: node) + contextGenerator(Syntax(node)).addDiagnostics(from: error, node: node) return .failure } } @@ -1161,7 +1176,7 @@ extension MacroApplication { return try expandFreestandingCodeItemList( definition: macro, node: node, - in: context, + in: contextGenerator(Syntax(node)), indentationWidth: indentationWidth ) } @@ -1180,7 +1195,7 @@ extension MacroApplication { return try expandFreestandingMemberDeclList( definition: macro, node: node, - in: context, + in: contextGenerator(Syntax(node)), indentationWidth: indentationWidth ) } @@ -1198,7 +1213,7 @@ extension MacroApplication { return try expandFreestandingExpr( definition: macro, node: node, - in: context, + in: contextGenerator(Syntax(node)), indentationWidth: indentationWidth ) } diff --git a/Sources/SwiftSyntaxMacros/CMakeLists.txt b/Sources/SwiftSyntaxMacros/CMakeLists.txt index 37d77e4481d..1d046f2c35a 100644 --- a/Sources/SwiftSyntaxMacros/CMakeLists.txt +++ b/Sources/SwiftSyntaxMacros/CMakeLists.txt @@ -25,6 +25,7 @@ add_swift_syntax_library(SwiftSyntaxMacros AbstractSourceLocation.swift MacroExpansionContext.swift MacroExpansionDiagnosticMessages.swift + Syntax+LexicalContext.swift ) target_link_swift_syntax_libraries(SwiftSyntaxMacros PUBLIC diff --git a/Sources/SwiftSyntaxMacros/MacroExpansionContext.swift b/Sources/SwiftSyntaxMacros/MacroExpansionContext.swift index 9419dff760c..44f1f92629b 100644 --- a/Sources/SwiftSyntaxMacros/MacroExpansionContext.swift +++ b/Sources/SwiftSyntaxMacros/MacroExpansionContext.swift @@ -47,6 +47,23 @@ public protocol MacroExpansionContext: AnyObject { at position: PositionInSyntaxNode, filePathMode: SourceLocationFilePathMode ) -> AbstractSourceLocation? + + /// Return an array of enclosing lexical contexts for the purpose of macros, + /// starting from the syntax node at which the macro expansion occurs + /// and containing all "context" nodes including functions, closures, types, + /// properties, subscripts, and extensions. + /// + /// Lexical contexts will have many of their details stripped out to prevent + /// macros from having visibility into unrelated code. For example, functions + /// and closures have their bodies removed, types and extensions have their + /// member lists emptied, and properties and subscripts have their accessor + /// blocks removed. + /// + /// The first entry in the array is the innermost context. For attached + /// macros, this is often the declaration to which the macro is attached. + /// This array can be empty if there is no context, for example when a + /// freestanding macro is used at file scope. + var lexicalContext: [Syntax] { get } } extension MacroExpansionContext { diff --git a/Sources/SwiftSyntaxMacros/Syntax+LexicalContext.swift b/Sources/SwiftSyntaxMacros/Syntax+LexicalContext.swift new file mode 100644 index 00000000000..3813b7ddaed --- /dev/null +++ b/Sources/SwiftSyntaxMacros/Syntax+LexicalContext.swift @@ -0,0 +1,91 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 SwiftSyntaxBuilder + +extension SyntaxProtocol { + /// If this syntax node acts as a lexical context from the perspective + /// of a macro, return a new syntax node based on this node that strips all + /// information that isn't supposed to be exposed as a lexical context, such + /// as function bodies or the members of types/extensions. + /// + /// Returns `nil` for any syntax node that isn't a lexical context. + public func asMacroLexicalContext() -> Syntax? { + switch Syntax(self).asProtocol(SyntaxProtocol.self) { + // Functions have their body removed. + case var function as WithOptionalCodeBlockSyntax & SyntaxProtocol: + function = function.detached + function.body = nil + return Syntax(function) as Syntax + + // Closures have their body removed. + case var closure as ClosureExprSyntax: + closure = closure.detached + closure.statements = CodeBlockItemListSyntax() + return Syntax(closure) + + // Nominal types and extensions have their member list cleared out. + case var typeOrExtension as HasTrailingMemberDeclBlock & SyntaxProtocol: + typeOrExtension = typeOrExtension.detached + typeOrExtension.memberBlock = MemberBlockSyntax(members: MemberBlockItemListSyntax()) + return Syntax(typeOrExtension) as Syntax + + // Subscripts have their accessors removed. + case var subscriptDecl as SubscriptDeclSyntax: + subscriptDecl = subscriptDecl.detached + subscriptDecl.accessorBlock = nil + return Syntax(subscriptDecl) + + // Enum cases are fine as-is. + case is EnumCaseElementSyntax: + return Syntax(self.detached) + + // Pattern bindings have their accessors and initializer removed. + case var patternBinding as PatternBindingSyntax: + patternBinding = patternBinding.detached + patternBinding.accessorBlock = nil + patternBinding.initializer = nil + return Syntax(patternBinding) + + default: + return nil + } + } + + /// Return an array of enclosing lexical contexts for the purpose of macros, + /// from the innermost enclosing lexical context (first in the array) to the + /// outermost. If this syntax node itself is a lexical context, it will be + /// the innermost lexical context. + /// + /// - Parameter enclosingSyntax: provides a parent node when the operation + /// has reached the outermost syntax node (i.e., it has no parent), allowing + /// the caller to provide a new syntax node that can continue the walk + /// to collect additional lexical contexts, e.g., from outer macro + /// expansions. + /// - Returns: the array of enclosing lexical contexts. + public func allMacroLexicalContexts( + enclosingSyntax: (Syntax) -> Syntax? = { _ in nil } + ) -> [Syntax] { + var parentContexts: [Syntax] = [] + var currentNode = Syntax(self) + while let parentNode = currentNode.parent ?? enclosingSyntax(currentNode) { + if let parentContext = parentNode.asMacroLexicalContext() { + parentContexts.append(parentContext) + } + + currentNode = parentNode + } + + return parentContexts + } +} diff --git a/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift b/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift index 7f7d2f40dac..1a09d5284bf 100644 --- a/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift +++ b/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift @@ -325,7 +325,11 @@ public func assertMacroExpansion( sourceFiles: [origSourceFile: .init(moduleName: testModuleName, fullFilePath: testFileName)] ) - let expandedSourceFile = origSourceFile.expand(macros: macros, in: context, indentationWidth: indentationWidth) + func contextGenerator(_ syntax: Syntax) -> BasicMacroExpansionContext { + return BasicMacroExpansionContext(sharingWith: context, lexicalContext: syntax.allMacroLexicalContexts()) + } + + let expandedSourceFile = origSourceFile.expand(macros: macros, contextGenerator: contextGenerator, indentationWidth: indentationWidth) let diags = ParseDiagnosticsGenerator.diagnostics(for: expandedSourceFile) if !diags.isEmpty { XCTFail( diff --git a/Tests/SwiftSyntaxMacroExpansionTest/LexicalContextTests.swift b/Tests/SwiftSyntaxMacroExpansionTest/LexicalContextTests.swift new file mode 100644 index 00000000000..82f75e4f2a4 --- /dev/null +++ b/Tests/SwiftSyntaxMacroExpansionTest/LexicalContextTests.swift @@ -0,0 +1,321 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 +// +//===----------------------------------------------------------------------===// + +//==========================================================================// +// IMPORTANT: The macros defined in this file are intended to test the // +// behavior of MacroSystem. Many of them do not serve as good examples of // +// how macros should be written. In particular, they often lack error // +// handling because it is not needed in the few test cases in which these // +// macros are invoked. // +//==========================================================================// + +import SwiftSyntax +import SwiftSyntaxMacroExpansion +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +extension PatternBindingSyntax { + /// When the variable is declaring a single binding, produce the name of + /// that binding. + fileprivate var singleBindingName: String? { + if let identifierPattern = pattern.as(IdentifierPatternSyntax.self) { + return identifierPattern.identifier.trimmedDescription + } + + return nil + } +} + +private extension TokenSyntax { + var asIdentifierToken: TokenSyntax? { + switch tokenKind { + case .identifier, .dollarIdentifier: return self.trimmed + default: return nil + } + } +} + +extension FunctionParameterSyntax { + var argumentName: TokenSyntax? { + // If we have two names, the first one is the argument label + if secondName != nil { + return firstName.asIdentifierToken + } + + // If we have only one name, it might be an argument label. + if let superparent = parent?.parent?.parent, superparent.is(SubscriptDeclSyntax.self) { + return nil + } + + return firstName.asIdentifierToken + } +} + +extension SyntaxProtocol { + /// Form a function name. + private func formFunctionName( + _ baseName: String, + _ parameters: FunctionParameterClauseSyntax? + ) -> String { + let argumentNames: [String] = + parameters?.parameters.map { param in + let argumentLabelText = param.argumentName?.text ?? "_" + return argumentLabelText + ":" + } ?? [] + + return "\(baseName)(\(argumentNames.joined(separator: "")))" + } + + /// Form the #function name for the given node. + fileprivate func functionName( + in context: Context + ) -> String? { + // Declarations with parameters. + // FIXME: Can we abstract over these? + if let function = self.as(FunctionDeclSyntax.self) { + return formFunctionName( + function.name.trimmedDescription, + function.signature.parameterClause + ) + } + + if let initializer = self.as(InitializerDeclSyntax.self) { + return formFunctionName("init", initializer.signature.parameterClause) + } + + if let subscriptDecl = self.as(SubscriptDeclSyntax.self) { + return formFunctionName( + "subscript", + subscriptDecl.parameterClause + ) + } + + if let enumCase = self.as(EnumCaseElementSyntax.self) { + guard let associatedValue = enumCase.parameterClause else { + return enumCase.name.text + } + + let argumentNames = associatedValue.parameters.map { param in + guard let firstName = param.firstName else { + return "_:" + } + + return firstName.text + ":" + }.joined() + + return "\(enumCase.name.text)(\(argumentNames))" + } + + // Accessors use their enclosing context, i.e., a subscript or pattern + // binding. + if self.is(AccessorDeclSyntax.self) { + guard let lexicalContext = context.lexicalContext.dropFirst().first else { + return nil + } + + return lexicalContext.functionName(in: context) + } + + // All declarations with identifiers. + if let identified = self.asProtocol(NamedDeclSyntax.self) { + return identified.name.trimmedDescription + } + + // Extensions + if let extensionDecl = self.as(ExtensionDeclSyntax.self) { + // FIXME: It would be nice to be able to switch on type syntax... + let extendedType = extensionDecl.extendedType + if let simple = extendedType.as(IdentifierTypeSyntax.self) { + return simple.name.trimmedDescription + } + + if let member = extendedType.as(MemberTypeSyntax.self) { + return member.name.trimmedDescription + } + } + + // Pattern bindings. + if let patternBinding = self.as(PatternBindingSyntax.self), + let singleVarName = patternBinding.singleBindingName + { + return singleVarName + } + + return nil + } +} + +public struct FunctionMacro: ExpressionMacro { + public static func expansion< + Node: FreestandingMacroExpansionSyntax, + Context: MacroExpansionContext + >( + of node: Node, + in context: Context + ) -> ExprSyntax { + guard let lexicalContext = context.lexicalContext.first, + let name = lexicalContext.functionName(in: context) + else { + return #""""# + } + + return ExprSyntax("\(literal: name)") + } +} + +public struct AllLexicalContextsMacro: DeclarationMacro { + public static func expansion( + of node: some FreestandingMacroExpansionSyntax, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + context.lexicalContext.compactMap { $0.as(DeclSyntax.self)?.trimmed } + } +} + +final class LexicalContextTests: XCTestCase { + private let indentationWidth: Trivia = .spaces(2) + + func testPoundFunction() { + assertMacroExpansion( + """ + func f(a: Int, _: Double, c: Int) { + print(#function) + } + """, + expandedSource: """ + func f(a: Int, _: Double, c: Int) { + print( "f(a:_:c:)") + } + """, + macros: ["function": FunctionMacro.self], + indentationWidth: indentationWidth + ) + + assertMacroExpansion( + """ + struct X { + init(from: String) { + #function + } + + subscript(a: Int) -> String { + #function + } + + subscript(a a: Int) -> String { + #function + } + } + """, + expandedSource: """ + struct X { + init(from: String) { + "init(from:)" + } + + subscript(a: Int) -> String { + "subscript(_:)" + } + + subscript(a a: Int) -> String { + "subscript(a:)" + } + } + """, + macros: ["function": FunctionMacro.self], + indentationWidth: indentationWidth + ) + + assertMacroExpansion( + """ + var computed: String { + get { + #function + } + } + """, + expandedSource: """ + var computed: String { + get { + "computed" + } + } + """, + macros: ["function": FunctionMacro.self], + indentationWidth: indentationWidth + ) + + assertMacroExpansion( + """ + extension A { + static var staticProp: String = #function + } + """, + expandedSource: """ + extension A { + static var staticProp: String = "staticProp" + } + """, + macros: ["function": FunctionMacro.self], + indentationWidth: indentationWidth + ) + } + + func testAllLexicalContexts() { + assertMacroExpansion( + """ + extension A { + struct B { + func f(a: Int, b: Int) { + class C { + @A subscript(i: Int) -> String { + func g() { + #allLexicalContexts + } + } + } + } + } + } + """, + expandedSource: """ + extension A { + struct B { + func f(a: Int, b: Int) { + class C { + @A subscript(i: Int) -> String { + func g() { + func g() + @A subscript(i: Int) -> String + class C { + } + func f(a: Int, b: Int) + struct B { + } + extension A { + } + } + } + } + } + } + } + """, + macros: ["allLexicalContexts": AllLexicalContextsMacro.self] + ) + + // Test closures separately, because they don't fit as declaration macros. + let closure: ExprSyntax = "{ (a, b) in print(a + b) }" + XCTAssertEqual(closure.asMacroLexicalContext()!.description, "{ (a, b) in }") + } +} diff --git a/Tests/SwiftSyntaxMacroExpansionTest/MultiRoleMacroTests.swift b/Tests/SwiftSyntaxMacroExpansionTest/MultiRoleMacroTests.swift index 57179e615ee..0843ed5302c 100644 --- a/Tests/SwiftSyntaxMacroExpansionTest/MultiRoleMacroTests.swift +++ b/Tests/SwiftSyntaxMacroExpansionTest/MultiRoleMacroTests.swift @@ -28,7 +28,7 @@ final class MultiRoleMacroTests: XCTestCase { private let indentationWidth: Trivia = .spaces(2) func testContextUniqueLocalNames() { - let context = BasicMacroExpansionContext() + let context = BasicMacroExpansionContext(lexicalContext: []) let t1 = context.makeUniqueName("mine") let t2 = context.makeUniqueName("mine") diff --git a/Tests/SwiftSyntaxMacroExpansionTest/StringInterpolationErrorTests.swift b/Tests/SwiftSyntaxMacroExpansionTest/StringInterpolationErrorTests.swift index f797c7fa23e..2656cbf2a96 100644 --- a/Tests/SwiftSyntaxMacroExpansionTest/StringInterpolationErrorTests.swift +++ b/Tests/SwiftSyntaxMacroExpansionTest/StringInterpolationErrorTests.swift @@ -43,7 +43,7 @@ private struct DummyMacro: ExtensionMacro { final class StringInterpolationErrorTests: XCTestCase { func testMacroExpansionContextAddDiagnosticsAddsSwiftSyntaxInterpolationErrorsWithWrappingMessage() throws { - let context = BasicMacroExpansionContext() + let context = BasicMacroExpansionContext(lexicalContext: []) let error = SyntaxStringInterpolationInvalidNodeTypeError(expectedType: DeclSyntax.self, actualNode: ExprSyntax("test")) // Since we only care about the error switch inside of addDagnostics, we don't care about the particular node we're passing in @@ -55,7 +55,7 @@ final class StringInterpolationErrorTests: XCTestCase { // Verify that any other error messages do not get "Internal macro error:" prefix. func testMacroExpansionContextAddDiagnosticsUsesErrorDescriptionForDiagMessage() throws { - let context = BasicMacroExpansionContext() + let context = BasicMacroExpansionContext(lexicalContext: []) let error = DummyError.diagnosticTestError context.addDiagnostics(from: error, node: ExprSyntax("1"))