Conversation
fd9f2fb to
ab4e8bc
Compare
d43b6d2 to
89b33ab
Compare
89b33ab to
701cff3
Compare
There was a problem hiding this comment.
Pull Request Overview
This PR implements resolution of macro calls in Rust, providing better accuracy for $crate path resolution and reducing type inference inconsistencies. The changes introduce macro namespace support and improve path resolution across the codebase.
Key Changes:
- Added macro namespace support alongside existing value and type namespaces
- Implemented macro call resolution with new
MacroItemNodeclasses - Enhanced
$cratepath resolution within macro expansions - Updated path resolution logic to handle macro-specific visibility and scoping rules
Reviewed Changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| PathResolution.qll | Core implementation of macro namespace, macro item nodes, and enhanced path resolution logic |
| PathResolutionConsistency.qll | Updated consistency checks to handle macro resolution improvements |
| MacroCallImpl.qll | Added macro resolution capability to MacroCall AST nodes |
| PathResolutionInlineExpectationsTest.qll | Enhanced test filtering to exclude macro-specific test markers |
| Various .expected files | Updated test expectations reflecting improved macro resolution accuracy |
| Various test source files | Added macro resolution test annotations and examples |
|
|
||
| abstract Attr getAnAttr(); | ||
|
|
||
| pragma[nomagic] | ||
| final Attr getAttr(string name) { | ||
| result = this.getAnAttr() and | ||
| result.getMeta().getPath().(RelevantPath).isUnqualified(name) | ||
| } | ||
|
|
There was a problem hiding this comment.
The new attribute-related methods (getAnAttr, getAttr, hasAttr) lack documentation explaining their purpose and usage. These methods appear to be fundamental for macro resolution but need proper documentation.
| abstract Attr getAnAttr(); | |
| pragma[nomagic] | |
| final Attr getAttr(string name) { | |
| result = this.getAnAttr() and | |
| result.getMeta().getPath().(RelevantPath).isUnqualified(name) | |
| } | |
| /** | |
| * Gets an attribute attached to this item. | |
| * | |
| * Returns an attribute (`Attr`) that is associated with this item, if any. | |
| * This method may return any one of the attributes present; to retrieve a specific | |
| * attribute by name, use `getAttr`. | |
| */ | |
| abstract Attr getAnAttr(); | |
| /** | |
| * Gets an attribute with the given name attached to this item. | |
| * | |
| * @param name The unqualified name of the attribute to retrieve. | |
| * @return An attribute (`Attr`) with the specified name, if present on this item. | |
| * If multiple attributes with the same name are present, returns one of them. | |
| */ | |
| pragma[nomagic] | |
| final Attr getAttr(string name) { | |
| result = this.getAnAttr() and | |
| result.getMeta().getPath().(RelevantPath).isUnqualified(name) | |
| } | |
| /** | |
| * Holds if this item has an attribute with the given name. | |
| * | |
| * @param name The unqualified name of the attribute to check for. | |
| * @return True if this item has at least one attribute with the specified name. | |
| */ |
| override Namespace getNamespace() { | ||
| // see https://doc.rust-lang.org/reference/procedural-macros.html | ||
| if this.hasAttr(["proc_macro", "proc_macro_attribute", "proc_macro_derive"]) | ||
| then result.isMacro() | ||
| else result.isValue() | ||
| } |
There was a problem hiding this comment.
The logic for determining function namespace based on procedural macro attributes needs more detailed explanation. The comment references external documentation but doesn't explain the specific behavior being implemented.
| private predicate macroUseEdge( | ||
| ItemNode i, string name, SuccessorKind kind, UseOption useOpt, MacroItemNode macro | ||
| ) { | ||
| exists(ItemNode m | | ||
| m = i.getASuccessor(_, _, useOpt) and | ||
| m.hasAttr("macro_use") | ||
| | | ||
| macro = m.(ModuleItemNode).getASuccessor(name, kind, _) | ||
| or | ||
| macro = m.(ExternCrateItemNode).getASuccessor(_, _, _).getASuccessor(name, kind, _) | ||
| ) | ||
| } |
There was a problem hiding this comment.
The macroUseEdge predicate implements complex macro visibility rules but lacks documentation explaining the two different resolution paths (ModuleItemNode vs ExternCrateItemNode) and when each applies.
| exists(Path macroDefPath | | ||
| isInMacroExpansion(macroDefPath, n) and | ||
| crate.getASourceFile().getFile() = resolvePathCand(macroDefPath).getFile() |
There was a problem hiding this comment.
The isInMacroFromCrateExpansion predicate has a documented limitation ('may originate from crate') but the implementation logic for determining crate origin is not clearly explained. The file-based comparison seems fragile and needs clarification.
| exists(Path macroDefPath | | |
| isInMacroExpansion(macroDefPath, n) and | |
| crate.getASourceFile().getFile() = resolvePathCand(macroDefPath).getFile() | |
| exists(Path macroDefPath, ItemNode macroDefItem | | |
| isInMacroExpansion(macroDefPath, n) and | |
| macroDefItem = resolvePathCand(macroDefPath) and | |
| macroDefItem.getEnclosingCrate() = crate |
paldepind
left a comment
There was a problem hiding this comment.
Looks good!
If it's not too hard, I think it would be worthwhile adding tests for $crate as the implementation is not trivial, especially when $crate is in an expansion within an expansion.
| } | ||
|
|
||
| pragma[nomagic] | ||
| private predicate isMacroExpansion(AstNode expansion, Path macroDefPath) { |
There was a problem hiding this comment.
The argument order seems inconsistent with isInMacroExpansion below.
| private predicate isMacroExpansion(AstNode expansion, Path macroDefPath) { | |
| private predicate isMacroExpansion(Path macroDefPath, AstNode expansion) { |
| predicate isInMacroFromCrateExpansion(CrateItemNode crate, AstNode n) { | ||
| exists(Path macroDefPath | | ||
| isInMacroExpansion(macroDefPath, n) and | ||
| crate.getASourceFile().getFile() = resolvePathCand(macroDefPath).getFile() |
There was a problem hiding this comment.
This predicate is only used with n being a $crate path. Inlining that as below changes the predicate size from 1.354.637 to 4.683 on the path resolution test DB.
Making it a predicate with result might also be more natural?
| predicate isInMacroFromCrateExpansion(CrateItemNode crate, AstNode n) { | |
| exists(Path macroDefPath | | |
| isInMacroExpansion(macroDefPath, n) and | |
| crate.getASourceFile().getFile() = resolvePathCand(macroDefPath).getFile() | |
| CrateItemNode getDollarCratePathCrate(RelevantPath dollarCratePath) { | |
| dollarCratePath.isDollarCrate() and | |
| exists(Path macroDefPath | | |
| isInMacroExpansion(macroDefPath, dollarCratePath) and | |
| result.getASourceFile().getFile() = resolvePathCand(macroDefPath).getFile() |
| exists(RelevantPath path | | ||
| path.isUnqualified(name, encl) and | ||
| ancestor = encl and | ||
| exists(ns) and |
There was a problem hiding this comment.
This is not needed and ns is restricted in one of the cases below.
| exists(ns) and |
| | | ||
| pathUsesNamespace(path, ns) | ||
| or | ||
| not pathUsesNamespace(path, _) |
There was a problem hiding this comment.
We could add the exists(ns) here for clarity, but it doesn't change the semantics.
| not pathUsesNamespace(path, _) | |
| not pathUsesNamespace(path, _) and exists(ns) |
| // known limitation for `$crate` | ||
| not p.getQualifier*().(RelevantPath).isUnqualified("$crate") and | ||
| // `panic` is defined in both `std` and `core`; both are included in the prelude | ||
| not p.getText() = "panic" and |
There was a problem hiding this comment.
Is panic the only thing where this the case?
There was a problem hiding this comment.
It's the only thing I saw in the tests, at least.
paldepind
left a comment
There was a problem hiding this comment.
Why do path resolution inconsistencies increase for some projects? For instance, Tokio increases from 655 to 9030.
| } | ||
|
|
||
| pragma[nomagic] | ||
| predicate isInDollarCrateSupportedMacroExpansion(File macroDefFile, AstNode n) { |
There was a problem hiding this comment.
| predicate isInDollarCrateSupportedMacroExpansion(File macroDefFile, AstNode n) { | |
| private predicate isInDollarCrateSupportedMacroExpansion(File macroDefFile, AstNode n) { |
| * calls. | ||
| */ | ||
| pragma[nomagic] | ||
| predicate resolveDollarCrate(RelevantPath p, CrateItemNode crate) { |
There was a problem hiding this comment.
| predicate resolveDollarCrate(RelevantPath p, CrateItemNode crate) { | |
| private predicate resolveDollarCrate(RelevantPath p, CrateItemNode crate) { |
| macroDefFile = resolvePathCand(macroDefPath).getFile() | ||
| ) | ||
| or | ||
| isInDollarCrateSupportedMacroExpansion(macroDefFile, n.getParentNode()) |
There was a problem hiding this comment.
This predicate holds for all AST nodes within an expansion, but we only care about $crate notes. And for expansions within expansions we end up with 2 * nrOfAstNodes tuples?
Wouldn't it have been more efficient to have
- a predicate from
$cratepaths to their enclosing expansion and - a predicate from a macro call expansion to its enclosing macro call expansion.
And then use these in resolveDollarCrate to find all the potential crates? That would avoid a predicate that holds for all AST nodes within expansions.
There was a problem hiding this comment.
I'll use doublyBoundedFastTC.
There was a problem hiding this comment.
Thanks, after looking up what doublyBoundedFastTC I think that approach is really clean!
This happens because we are now also resolving paths to macros, and in some cases those are not unique. We could investigate follow-up. |
Thanks. That seems reasonable :) |
|
I've approved, but maybe we need another DCA run? |
This PR implements resolution of macro calls (macro expansion is still done in the extractor). The motivation for doing this, in addition to having the resolution information available, is to provide more accurate resolution of
$cratepaths, which in turn means fewer inconsistencies.DCA looks great; a significant reduction in type inference inconsistencies (down
20,267from83,257) and a small performance improvement, at the cost of only a small reduction in resolved calls.