Fix handling of functions with multiple definitions#3
Fix handling of functions with multiple definitions#3eduard-bagdasaryan wants to merge 10 commits intomainfrom
Conversation
FunctionDeclMatchHandler::finalize(), working at a single TU at a time, accumulates dependencies in two separate 'Uses' 'Defs' sets: * Uses: All functions that are used but not defined in the TU * Defs: All functions that are defined but not used in the TU This lacks the case when a function is defined _and_ used in the TU at the same time. Such function was omitted and could later be considered as 'unused', e.g., if another definition was found in a tests 'stub' file.
There was a problem hiding this comment.
FunctionDeclMatchHandler::finalize(), working at a single TU at a time
I agree that finalize() is called once for each TU.
accumulates dependencies in two separate 'Uses' 'Defs' sets:
* Uses: All functions that are used but not defined in the TU
* Defs: All functions that are defined but not used in the TU
My reading of this code is very different!
finalize() does not accumulate something in Uses and Defs. finalize() does not change Uses and Defs at all (except in isWeak() corner case that this PR removes).
Instead, Uses is built by handleUse(), and Defs is built by run(). Neither handleUse() nor run() are called once for each TU. They are called as various matching AST nodes are found.
When finalize() iterates Defs and Uses, it does not look at functions from a single TU. It looks at functions from all already processed/seen TUs and the current TU.
finalize() populates and updates AllDecls.
finalize() loops adds the following AllDecls entries:
- The first loop adds functions that are defined but unused in seen TUs.
- The second loop adds functions that are used but not defined in seen TUs.
... where "seen TUs" are all TUs from previous finalize() calls and the current TU.
This lacks the case when a function is defined and used in the TU at the same time. Such function was omitted and could later be considered as 'unused', e.g., if another definition was found in a tests 'stub' file.
I more-or-less agree with this part. To be more precise, a function "use" was incorrectly skipped (i.e. the function was not added to AllDecls as used) when for every TU using that function, a seen TU contained that function definition. If some initial seen TUs sequence defined the function without using it, the function would be incorrectly reported as unused in such cases.
If I am right, then the bug we are trying to fix here is not specific to functions with multiple definitions. A single definition can trigger the same problem.
Please merge main branch and add the simplest test case (or two) that can trigger this problem. Alternatively, if you prefer, close this PR and start from scratch, keeping this review in mind.
As always, please keep PR title/description in sync with PR code (and with our understanding of the proposed changes).
main.cpp
Outdated
|
|
||
| // TODO: use in Uses and Defs sets to exclude duplicates | ||
| // (i.e., when multiple FunctionDecl reference a single object) | ||
| struct FunctionDeclPtrLess { |
There was a problem hiding this comment.
FunctionDeclPtrLess class is unused by current PR code. If it is not required to fix the problem, let's not add it (in this PR).
main.cpp
Outdated
There was a problem hiding this comment.
PR code cannot claim that this USR is unused. I suggest removing this debugging/comment and adding a new debugging/comment line to the first if statement below, with found new definition: or similar text.
main.cpp
Outdated
|
|
||
| for (auto *F : ExternalUses) { | ||
| for (auto *F : Uses) { | ||
| // llvm::errs() << "ExternalUses: " << F->getNameAsString() << "\n"; |
There was a problem hiding this comment.
PR code cannot claim that F represents an external use. I suggest removing this debugging/comment and adding a new debugging/comment line after the second if statement below, with saw ... uses: or similar text that will also report it_inserted.first->second.Uses counter.
main.cpp
Outdated
There was a problem hiding this comment.
Same concern here. Same recommended solution (i.e. replace both stale debugging/comments with one fresh debugging/comment line further below).
main.cpp
Outdated
| XUnusedASTConsumer() { | ||
| Matcher.addMatcher( | ||
| functionDecl(isDefinition(), unless(isImplicit())).bind("fnDecl"), | ||
| functionDecl(unless(isImplicit())).bind("fnDecl"), |
There was a problem hiding this comment.
Why do we need to remove isDefinition() here?
There was a problem hiding this comment.
isDefinition() is false when a TU has a function declaration without definition. Generally, as far as we are concerned to cover all function's occurrences, I think we should not use this filter. However, this change looks unnecessary in this particular PR and I will remove it.
main.cpp
Outdated
|
|
||
| struct DefInfo { | ||
| const FunctionDecl *Definition; | ||
| const FunctionDecl *Definition; // XXX: perhaps a dangling pointer (created during parsing a single TU) |
There was a problem hiding this comment.
Is it possible that FunctionDecl objects survive across TU boundaries? If we are not sure, and we do not really care/dereference this Definition pointer in this PR, I suggest removing this scary comment from this PR.
There was a problem hiding this comment.
Is it possible that FunctionDecl objects survive across TU boundaries?
I think it is not possible.
removing this scary comment
Ok.
main.cpp
Outdated
| @@ -107,14 +117,9 @@ class FunctionDeclMatchHandler : public MatchFinder::MatchCallback { | |||
| // Defs before checking which uses we need to consider in other TUs, | |||
| // so the functions overwritting the weak definition here are marked | |||
| // as used. | |||
There was a problem hiding this comment.
If we need to remove discard_if() below, then we should remove this comment as well, right?
Agreed. I think there was a mistake in the description: s/Uses/ExternalUses and s/Defs/UnusedDefs. Probably this happened because in the PR I replaced ExternalUses with Uses and UnusedDefs with Defs.
Agreed, run() is a callback used by Matcher.matchAST(). But XUnusedASTConsumer is created for each parsed TU (and hence FunctionDeclMatchHandler containing Uses and Defs).
Agreed in the sense that Uses and Defs represent functions from a single TU and AllDecls represent already processed TUs. |
eduard-bagdasaryan
left a comment
There was a problem hiding this comment.
Done up to 64204aa.
main.cpp
Outdated
|
|
||
| struct DefInfo { | ||
| const FunctionDecl *Definition; | ||
| const FunctionDecl *Definition; // XXX: perhaps a dangling pointer (created during parsing a single TU) |
There was a problem hiding this comment.
Is it possible that FunctionDecl objects survive across TU boundaries?
I think it is not possible.
removing this scary comment
Ok.
main.cpp
Outdated
|
|
||
| // TODO: use in Uses and Defs sets to exclude duplicates | ||
| // (i.e., when multiple FunctionDecl reference a single object) | ||
| struct FunctionDeclPtrLess { |
main.cpp
Outdated
main.cpp
Outdated
| @@ -107,14 +117,9 @@ class FunctionDeclMatchHandler : public MatchFinder::MatchCallback { | |||
| // Defs before checking which uses we need to consider in other TUs, | |||
| // so the functions overwritting the weak definition here are marked | |||
| // as used. | |||
main.cpp
Outdated
|
|
||
| for (auto *F : ExternalUses) { | ||
| for (auto *F : Uses) { | ||
| // llvm::errs() << "ExternalUses: " << F->getNameAsString() << "\n"; |
main.cpp
Outdated
| XUnusedASTConsumer() { | ||
| Matcher.addMatcher( | ||
| functionDecl(isDefinition(), unless(isImplicit())).bind("fnDecl"), | ||
| functionDecl(unless(isImplicit())).bind("fnDecl"), |
There was a problem hiding this comment.
isDefinition() is false when a TU has a function declaration without definition. Generally, as far as we are concerned to cover all function's occurrences, I think we should not use this filter. However, this change looks unnecessary in this particular PR and I will remove it.
rousskov
left a comment
There was a problem hiding this comment.
Alex: When finalize() iterates Defs and Uses, it does not look at functions from a single TU. It looks at functions from all already processed/seen TUs and the current TU.
Eduard: Agreed in the sense that Uses and Defs represent functions from a single TU
Agreed. I got Uses and Defs lifetime/scope wrong because I missed the fact that XUnusedFrontendActionFactory apparently creates a new XUnusedASTConsumer object for what xunused comment calls "each source file" (i.e. presumably each TU), creating a new XUnusedASTConsumer::Handler data member that holds Uses and Defs. This correction is good news because it means that current official code is closer to what it should be.
I adjusted PR description accordingly.
I adjusted the last PR description paragraphs based on our discussions. |
main.cpp
Outdated
There was a problem hiding this comment.
I wonder whether this PR would be the right place to change the Definition data member from a FunctionDecl pointer to a Definitions counter (similar to the existing Uses counter). Prior to this PR, only a single definition was allowed/supported, so a pointer to the corresponding declaration was appropriate (even if it was never dereferenced). After this PR, we allow multiple definitions, so a single pointer does not really make sense.
Please implement that change as the last dedicated branch commit (after fixing all other issues as discussed) so that we can easily revert it if necessary.
There was a problem hiding this comment.
Yes, it would be useful. I also adjusted debugging to report this counter.
main.cpp
Outdated
|
|
||
| for (auto *F : ExternalUses) { | ||
| // llvm::errs() << "ExternalUses: " << F->getNameAsString() << "\n"; | ||
| for (auto *F : Uses) { |
There was a problem hiding this comment.
If possible:
| for (auto *F : Uses) { | |
| for (const auto F: Uses) { |
Checked. |
FunctionDeclMatchHandler::finalize(), working at a single TU at a time,
accumulates
AllDeclsentries using two loops:A function "use" case was incorrectly ignored (i.e. the function was
not marked as used in
AllDecls) when every TU using that function alsocontained that function definition -- neither loop covered this case. If
some other TU defined that function without using it, the function would
be incorrectly reported as unused.
For example, a test program containing a foo() stub definition would
trigger an incorrect "foo() is unused" warning if the primary project
program was defining and using foo() in some TU while the test program
was not using foo() at all.
Instead of reporting all unused functions, xunused only reports unused
functions that are defined by the project. Ignoring undefined functions
is probably necessary to avoid reporting "externally" defined (e.g., by
a 3rd party library) functions as unused by the project. This change
preserves that approach, but stops using "each function can be defined
at most once" assumption (that could be appropriate for projects with a
single executable) to add support for projects where multiple
executables provide multiple definitions for the same function.
The two finalize() loops now have the following simplified scope: