From 5c47c908f1d1022d4b532ba473bac1858300f30b Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Fri, 2 Feb 2024 15:58:12 -0800 Subject: [PATCH 1/2] Add "lexical context" information to the macro expansion context The lexical context of a particular macro expansion involves all of the enclosing contexts, including functions, types and extensions, properties and subscripts, and so on. The lexical context stack can be used to, for example, determine which type a macro was expanded within, or gather the parameters of the enclosing function for an assertion, etc. --- .../CompilerPluginMessageHandler.swift | 17 +- .../Macros.swift | 44 ++- .../PluginMacroExpansionContext.swift | 6 +- .../PluginMessages.swift | 6 +- .../BasicMacroExpansionContext.swift | 94 ++++-- .../MacroSystem.swift | 59 ++-- Sources/SwiftSyntaxMacros/CMakeLists.txt | 1 + .../MacroExpansionContext.swift | 15 + .../Syntax+LexicalContext.swift | 91 ++++++ .../Assertions.swift | 7 +- .../LexicalContextTests.swift | 298 ++++++++++++++++++ .../MultiRoleMacroTests.swift | 2 +- .../StringInterpolationErrorTests.swift | 4 +- 13 files changed, 572 insertions(+), 72 deletions(-) create mode 100644 Sources/SwiftSyntaxMacros/Syntax+LexicalContext.swift create mode 100644 Tests/SwiftSyntaxMacroExpansionTest/LexicalContextTests.swift 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..af693f03c52 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]? ) /// 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]? ) /// Optionally implemented message to load a dynamic link library. diff --git a/Sources/SwiftSyntaxMacroExpansion/BasicMacroExpansionContext.swift b/Sources/SwiftSyntaxMacroExpansion/BasicMacroExpansionContext.swift index dd4e8f59c78..2a6dad5a7fb 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..d575bc457dd 100644 --- a/Sources/SwiftSyntaxMacros/MacroExpansionContext.swift +++ b/Sources/SwiftSyntaxMacros/MacroExpansionContext.swift @@ -47,6 +47,21 @@ 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, which could be + /// the syntax node to which + 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..32f734cbfb4 100644 --- a/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift +++ b/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift @@ -322,10 +322,15 @@ public func assertMacroExpansion( // Expand all macros in the source. let context = BasicMacroExpansionContext( + lexicalContext: /*FIXME:*/ [], 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..2757acaaf8e --- /dev/null +++ b/Tests/SwiftSyntaxMacroExpansionTest/LexicalContextTests.swift @@ -0,0 +1,298 @@ +//===----------------------------------------------------------------------===// +// +// 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?, + isSubscript: Bool = false + ) -> 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, + isSubscript: true + ) + } + + 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) + } + + struct X { + var computed: String { + get { + #function + } + } + + init(from: String) { + #function + } + + subscript(a: Int) -> String { + #function + } + + subscript(a a: Int) -> String { + #function + } + } + + extension A { + static var staticProp: String = #function + } + """, + expandedSource: """ + func f(a: Int, _: Double, c: Int) { + print( "f(a:_:c:)") + } + + struct X { + var computed: String { + get { + "computed" + } + } + + init(from: String) { + "init(from:)" + } + + subscript(a: Int) -> String { + "subscript(_:)" + } + + subscript(a a: Int) -> String { + "subscript(a:)" + } + } + + 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] + ) + } +} 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")) From 3a5367ed1844cb3fae4f62bb1883b9c84684b1ba Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Sun, 4 Feb 2024 10:43:11 -0800 Subject: [PATCH 2/2] Address code review and update release notes --- Release Notes/511.md | 9 +++ .../PluginMessages.swift | 4 +- .../BasicMacroExpansionContext.swift | 2 +- .../MacroExpansionContext.swift | 6 +- .../Assertions.swift | 1 - .../LexicalContextTests.swift | 71 ++++++++++++------- 6 files changed, 63 insertions(+), 30 deletions(-) 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/PluginMessages.swift b/Sources/SwiftCompilerPluginMessageHandling/PluginMessages.swift index af693f03c52..10ecd96a4bd 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/PluginMessages.swift +++ b/Sources/SwiftCompilerPluginMessageHandling/PluginMessages.swift @@ -24,7 +24,7 @@ public enum HostToPluginMessage: Codable { macroRole: PluginMessage.MacroRole? = nil, discriminator: String, syntax: PluginMessage.Syntax, - lexicalContext: [PluginMessage.Syntax]? + lexicalContext: [PluginMessage.Syntax]? = nil ) /// Expand an '@attached' macro. @@ -37,7 +37,7 @@ public enum HostToPluginMessage: Codable { parentDeclSyntax: PluginMessage.Syntax?, extendedTypeSyntax: PluginMessage.Syntax?, conformanceListSyntax: PluginMessage.Syntax?, - lexicalContext: [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 2a6dad5a7fb..b83709e3cb5 100644 --- a/Sources/SwiftSyntaxMacroExpansion/BasicMacroExpansionContext.swift +++ b/Sources/SwiftSyntaxMacroExpansion/BasicMacroExpansionContext.swift @@ -73,7 +73,7 @@ public class BasicMacroExpansionContext { /// Create a new macro evaluation context. public init( - lexicalContext: [Syntax], + lexicalContext: [Syntax] = [], expansionDiscriminator: String = "__macro_local_", sourceFiles: [SourceFileSyntax: KnownSourceFile] = [:] ) { diff --git a/Sources/SwiftSyntaxMacros/MacroExpansionContext.swift b/Sources/SwiftSyntaxMacros/MacroExpansionContext.swift index d575bc457dd..44f1f92629b 100644 --- a/Sources/SwiftSyntaxMacros/MacroExpansionContext.swift +++ b/Sources/SwiftSyntaxMacros/MacroExpansionContext.swift @@ -59,8 +59,10 @@ public protocol MacroExpansionContext: AnyObject { /// member lists emptied, and properties and subscripts have their accessor /// blocks removed. /// - /// The first entry in the array is the innermost context, which could be - /// the syntax node to which + /// 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 } } diff --git a/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift b/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift index 32f734cbfb4..1a09d5284bf 100644 --- a/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift +++ b/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift @@ -322,7 +322,6 @@ public func assertMacroExpansion( // Expand all macros in the source. let context = BasicMacroExpansionContext( - lexicalContext: /*FIXME:*/ [], sourceFiles: [origSourceFile: .init(moduleName: testModuleName, fullFilePath: testFileName)] ) diff --git a/Tests/SwiftSyntaxMacroExpansionTest/LexicalContextTests.swift b/Tests/SwiftSyntaxMacroExpansionTest/LexicalContextTests.swift index 2757acaaf8e..82f75e4f2a4 100644 --- a/Tests/SwiftSyntaxMacroExpansionTest/LexicalContextTests.swift +++ b/Tests/SwiftSyntaxMacroExpansionTest/LexicalContextTests.swift @@ -65,8 +65,7 @@ extension SyntaxProtocol { /// Form a function name. private func formFunctionName( _ baseName: String, - _ parameters: FunctionParameterClauseSyntax?, - isSubscript: Bool = false + _ parameters: FunctionParameterClauseSyntax? ) -> String { let argumentNames: [String] = parameters?.parameters.map { param in @@ -97,8 +96,7 @@ extension SyntaxProtocol { if let subscriptDecl = self.as(SubscriptDeclSyntax.self) { return formFunctionName( "subscript", - subscriptDecl.parameterClause, - isSubscript: true + subscriptDecl.parameterClause ) } @@ -193,14 +191,19 @@ final class LexicalContextTests: XCTestCase { func f(a: Int, _: Double, c: Int) { print(#function) } - - struct X { - var computed: String { - get { - #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 } @@ -213,23 +216,9 @@ final class LexicalContextTests: XCTestCase { #function } } - - extension A { - static var staticProp: String = #function - } """, expandedSource: """ - func f(a: Int, _: Double, c: Int) { - print( "f(a:_:c:)") - } - struct X { - var computed: String { - get { - "computed" - } - } - init(from: String) { "init(from:)" } @@ -242,7 +231,37 @@ final class LexicalContextTests: XCTestCase { "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" } @@ -294,5 +313,9 @@ final class LexicalContextTests: XCTestCase { """, 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 }") } }