Fix/build alias collision disambiguation#1713
Open
mallyskies wants to merge 2 commits into
Open
Conversation
…iles build_from_json's "pre-migration alias index" (Graphify-Labs#1504) registers each real file's OLD-style bare-stem id (extension dropped) as an alias so a stale cached fragment referencing that old form still resolves after an id-scheme migration. It never checked for collisions: two unrelated real files easily compute the same bare alias (e.g. "ping.h" and "ping.php" in different directories both alias to "ping"), and dict.setdefault let whichever file happened to iterate first in a Python set win, arbitrarily. A dangling edge with a bare, deliberately-unscoped fallback target (e.g. the C/C++ extractor's last-resort id for an #include it couldn't resolve to a real path) would ride that alias onto whatever unrelated file won the collision -- silently wiring, say, a C++ server file to an unrelated PHP script, purely because both files happen to share a common basename somewhere in the corpus. Now every candidate for an alias is collected before any of them are committed to norm_to_id, and the alias is only trusted when exactly one real file claims it. Ambiguous aliases are dropped entirely, so the dangling edge correctly stays dangling (and gets discarded) instead of merging two unrelated files -- same "don't guess through ambiguity" principle already applied to stub-node disambiguation and cross-file call resolution elsewhere in this codebase. Found via graphify-practice round 2: a `path` query between two unrelated symbols routed through exactly this kind of bogus edge, traced back to Dev/Poker/TDServer/server.cpp's unresolved `#include "ping.h"` / `#include "utility.h"` landing on unrelated www.masque.com PHP scripts of the same bare name.
Follow-up to the previous commit on this branch. That fix's ambiguity check missed a case: it detects "is this node the file itself" by checking whether the node's id starts with the file's plain new_stem, but a same-directory .h/.cpp pair that collides on their shared pre-extension id gets salted apart by _disambiguate_colliding_node_ids into ids like "tools_aolserver_utility_h_tools_aolserver_utility" -- no longer a clean new_stem prefix. That salted header silently failed to compute an empty suffix, so it never entered the bare "utility" alias race at all, leaving an unrelated wwwapi.masque.com/pages/utility.php as the lone (wrong) "unambiguous" winner -- reproduced exactly against the real depot's Tools/aolserver/utility.h and .cpp. Detect "this node IS the file" by label instead: every file node's label is its own basename regardless of what its id looks like after salting. That keeps a salted file node in the alias competition, so the real collision between the C header and the PHP file is correctly caught as ambiguous.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What's wrong
build_from_json's "pre-migration alias index" (#1504) exists so a stale, un-re-extracted fragment referencing a file by its old, pre-migration short id (bare filename stem, extension dropped) still resolves to that file's current canonical id. It builds this alias withdict.setdefault(alias, nid)and never checks whether more than one real file computes the same alias.That collision is common: the old-stem form drops the extension and (for the file node itself) most of the path, so
ping.handping.phpin different directories both alias to bareping. Whichever file happens to be visited first while iterating a Pythonset— arbitrary, hash-order, not filesystem order — wins the alias.A dangling edge with a bare, deliberately-unscoped fallback target (e.g. the C/C++ extractor's last-resort id for an
#includeit couldn't resolve to a real path on disk) rides that alias onto whichever unrelated file won the collision. The result: a C++ server file's unresolved#include "ping.h"can end up wired to a same-named PHP script in a completely different part of the tree, purely by alias coincidence.The fix
Collect every candidate for a given alias before committing any of them to
norm_to_id, and only trust the alias when exactly one real file claims it. Ambiguous aliases are dropped entirely — the dangling edge stays dangling and gets discarded during the normal "skip edges to external/unresolved nodes" step, instead of silently merging two unrelated files.This mirrors the same "don't guess through ambiguity" principle the codebase already applies elsewhere (stub-node disambiguation, cross-file call resolution): no edge is better than a wrong edge.
One wrinkle, fixed in the second commit: a file node's own id isn't always a clean
new_stemprefix. When a same-directory.h/.cpppair collides on their shared pre-extension id,_disambiguate_colliding_node_idssalts both apart into ids liketools_aolserver_utility_h_tools_aolserver_utility. That salted id no longer string-prefixes cleanly, so the header silently failed to compute an empty suffix and dropped out of its own alias's race entirely — leaving an unrelated same-named file as the lone "unambiguous" winner. Fixed by detecting "this node is the file" via label (every file node's label is its own basename, regardless of id mangling) instead of via id shape.How we found this
We traced a bogus
pathquery result — an 8-hop route between two unrelated C++ symbols that cut through an unrelated PHP file — back toDev/Game/GameServer/server.cpp's unresolved#include "ping.h"/#include "utility.h". Both headers exist elsewhere in the same repo (outside the includer's own directory), so the extractor's local-path resolution missed them and fell back to a bareping/utilitytarget. Two same-named PHP scripts under an unrelatedwww/pages/tree happened to win the alias collision, so the edges landed there instead of staying dangling (or, ideally, resolving to the real header — a separate, out-of-scope improvement to#includepath resolution).The salting wrinkle above surfaced while verifying the fix against the real repo:
utilityspecifically kept resolving to the wrong file even after the first commit, because the realTools/server/utility.h/.cpppair's salted ids masked them from the alias race entirely.Testing
Added three regression tests: a two-real-file alias collision that must stay dangling, the original #1504 single-candidate case that must still resolve, and the salted-
.h/.cpp-pair case reproduced against the real repo. Fulltests/test_build.py: 38 passing (was 35; +3 new tests), same 4 pre-existing failures before and after (unrelated — anetworkxversion mismatch in this environment, not caused by this change).