Skip to content

Fix/build alias collision disambiguation#1713

Open
mallyskies wants to merge 2 commits into
Graphify-Labs:v8from
masquepublishing:fix/build-alias-collision-disambiguation
Open

Fix/build alias collision disambiguation#1713
mallyskies wants to merge 2 commits into
Graphify-Labs:v8from
masquepublishing:fix/build-alias-collision-disambiguation

Conversation

@mallyskies

Copy link
Copy Markdown

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 with dict.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.h and ping.php in different directories both alias to bare ping. Whichever file happens to be visited first while iterating a Python set — 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 #include it 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_stem prefix. When a same-directory .h/.cpp pair collides on their shared pre-extension id, _disambiguate_colliding_node_ids salts both apart into ids like tools_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 path query result — an 8-hop route between two unrelated C++ symbols that cut through an unrelated PHP file — back to Dev/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 bare ping / utility target. Two same-named PHP scripts under an unrelated www/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 #include path resolution).

The salting wrinkle above surfaced while verifying the fix against the real repo: utility specifically kept resolving to the wrong file even after the first commit, because the real Tools/server/utility.h/.cpp pair'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. Full tests/test_build.py: 38 passing (was 35; +3 new tests), same 4 pre-existing failures before and after (unrelated — a networkx version mismatch in this environment, not caused by this change).

…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant