Skip to content

Commit 10ab613

Browse files
authored
Merge pull request #1766 from Yyunozor/fix/1355-external-import-shadow
fix(registry): stop external package imports binding to project homonyms
2 parents a3d266d + 01368e6 commit 10ab613

10 files changed

Lines changed: 1108 additions & 4 deletions

‎src/pipeline/pass_calls.c‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,6 +1143,15 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
11431143
/* The member guard's one exemption (Python, self/cls-rooted receiver,
11441144
* unique_name, not a builtin type's method) — see
11451145
* cbm_weak_member_unique_name_exempt. MUST match pass_parallel.c exactly. */
1146+
/* #1355: `import { eq } from "drizzle-orm"` binds `eq` to a package that is
1147+
* not in the indexed tree, so a project-wide same-name guess must not turn
1148+
* `eq(...)` into a CALLS edge to an unrelated project `eq`. Joined to
1149+
* drop_plain_call for the reason spelled out above: dropping the call here
1150+
* would also skip route/HTTP/CONFIG classification, and a route
1151+
* registration reached through a static import (`import static
1152+
* spark.Spark.get` + `get("/x", handler)`) is exactly a bare call bound by
1153+
* a package specifier. Suppressing only the plain-CALLS fall-through keeps
1154+
* every Route node and service edge main-identical. */
11461155
bool drop_plain_call =
11471156
(cbm_suppress_weak_member_match(suppress_weak_member, call->is_method, res.strategy) &&
11481157
!cbm_weak_member_unique_name_exempt(lang == CBM_LANG_PYTHON,
@@ -1159,6 +1168,9 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
11591168
(lang == CBM_LANG_PYTHON && cbm_suppress_weak_import_bound_call(true, true, res.strategy) &&
11601169
cbm_python_import_binding_contradicts(imports, call->callee_name, res.qualified_name,
11611170
ctx->gbuf, ctx->project_name, rel)) ||
1171+
cbm_suppress_external_import_shadow(call->callee_name, res.strategy, imports, imp_keys,
1172+
imp_count, cbm_pipeline_get_pkgmap(),
1173+
cbm_pipeline_get_nsmap()) ||
11621174
/* MUST match pass_parallel.c exactly. */
11631175
(lang == CBM_LANG_PHP && cbm_suppress_weak_member_match(true, true, res.strategy) &&
11641176
cbm_pipeline_php_receiver_type_unindexed(lsp_calls, call, ctx->registry));

‎src/pipeline/pass_definitions.c‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1184,7 +1184,9 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
11841184
cbm_pipeline_diagram_arrows(ctx, result);
11851185
cbm_pipeline_create_env_configures_for_file(ctx, result, files[i].rel_path);
11861186
}
1187-
cbm_pipeline_namespace_map_free(namespace_map);
1187+
/* Publish instead of free — see the twin in pass_parallel.c (#1355). */
1188+
cbm_pipeline_namespace_map_free(cbm_pipeline_get_nsmap());
1189+
cbm_pipeline_set_nsmap(namespace_map);
11881190
if (owns_local_cache) {
11891191
for (int i = 0; i < file_count; i++) {
11901192
if (local_cache[i]) {

‎src/pipeline/pass_parallel.c‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2041,7 +2041,11 @@ int cbm_build_registry_from_cache(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
20412041
}
20422042
}
20432043

2044-
cbm_pipeline_namespace_map_free(namespace_map);
2044+
/* Publish instead of free: the call pass needs the same declared-package
2045+
* set to tell an in-tree package path from a third-party one (#1355).
2046+
* The pipeline owns it from here and frees it in its cleanup. */
2047+
cbm_pipeline_namespace_map_free(cbm_pipeline_get_nsmap());
2048+
cbm_pipeline_set_nsmap(namespace_map);
20452049

20462050
cbm_log_info("parallel.registry.done", "entries", itoa_log(reg_entries), "defines",
20472051
itoa_log(defines_edges), "imports", itoa_log(imports_edges));
@@ -3406,6 +3410,11 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
34063410
* This gate MUST stay identical to the one there. */
34073411
bool suppress_weak_local_binding = lang == CBM_LANG_PYTHON;
34083412
/* The member guard's one exemption — MUST match pass_calls.c exactly. */
3413+
/* #1355: same guard as pass_calls.c, and joined to drop_plain_call for
3414+
* the same reason — a `continue` here would also skip route/HTTP/CONFIG
3415+
* classification, and a static-import route registration is a bare call
3416+
* bound by a package specifier. Suppress only the plain-CALLS
3417+
* fall-through so every Route node stays main-identical. */
34093418
bool drop_plain_call =
34103419
(cbm_suppress_weak_member_match(suppress_weak_member, call->is_method, res.strategy) &&
34113420
!cbm_weak_member_unique_name_exempt(lang == CBM_LANG_PYTHON,
@@ -3422,6 +3431,9 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
34223431
cbm_python_import_binding_contradicts(&result->imports, call->callee_name,
34233432
res.qualified_name, rc->main_gbuf,
34243433
rc->project_name, rel)) ||
3434+
cbm_suppress_external_import_shadow(call->callee_name, res.strategy, &result->imports,
3435+
imp_keys, imp_count, cbm_pipeline_get_pkgmap(),
3436+
cbm_pipeline_get_nsmap()) ||
34253437
/* MUST match pass_calls.c exactly. */
34263438
(lang == CBM_LANG_PHP && cbm_suppress_weak_member_match(true, true, res.strategy) &&
34273439
cbm_pipeline_php_receiver_type_unindexed(&result->resolved_calls, call, rc->registry));

‎src/pipeline/pipeline.c‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,18 @@ void cbm_pipeline_set_pkgmap(CBMHashTable *map) {
792792
g_pkgmap = map;
793793
}
794794

795+
/* ── Global declared-package map (one active pipeline at a time) ── */
796+
797+
static CBMHashTable *g_nsmap = NULL;
798+
799+
CBMHashTable *cbm_pipeline_get_nsmap(void) {
800+
return g_nsmap;
801+
}
802+
803+
void cbm_pipeline_set_nsmap(CBMHashTable *map) {
804+
g_nsmap = map;
805+
}
806+
795807
bool cbm_pipeline_had_format_migration(const cbm_pipeline_t *p) {
796808
return p && p->format_migration;
797809
}
@@ -4095,6 +4107,8 @@ static int cbm_pipeline_run_staged(cbm_pipeline_t *p) {
40954107
cleanup:
40964108
cbm_pkgmap_free(cbm_pipeline_get_pkgmap());
40974109
cbm_pipeline_set_pkgmap(NULL);
4110+
cbm_pipeline_namespace_map_free(cbm_pipeline_get_nsmap());
4111+
cbm_pipeline_set_nsmap(NULL);
40984112
cbm_discover_free(files, file_count);
40994113
if (p->frozen) {
41004114
p->frozen->baseline = baseline_manifest;

‎src/pipeline/pipeline.h‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@
1919
#include <stdint.h>
2020
#include <stdatomic.h>
2121

22-
#include "discover/discover.h" /* cbm_ignored_file_t (#963) */
23-
#include "foundation/constants.h" /* CBM_SZ_512 */
22+
#include "discover/discover.h" /* cbm_ignored_file_t (#963) */
23+
#include "foundation/constants.h" /* CBM_SZ_512 */
24+
#include "foundation/hash_table.h" /* CBMHashTable (#1355 pkgmap lookup) */
2425

2526
/* Forward declarations */
2627
typedef struct cbm_store cbm_store_t;
@@ -411,6 +412,25 @@ bool cbm_weak_member_unique_name_exempt(bool is_python, bool receiver_is_self_at
411412
* Pure; unit-tested in test_registry.c. */
412413
bool cbm_suppress_weak_local_binding_call(bool enabled, bool callee_is_locally_bound,
413414
const char *strategy);
415+
/* #1355: drop a project-wide same-name guess (suffix_match / unique_name /
416+
* field_type_hint / fuzzy) for a BARE call whose name the calling file binds to
417+
* a NON-RELATIVE (package) import that resolved to nothing in the graph —
418+
* `import { eq } from "drizzle-orm"` must not make `eq(...)` a CALLS edge to an
419+
* unrelated project `eq`. A name the import map does bind, a relative
420+
* specifier, a member/qualified callee, and every import-/receiver-aware
421+
* strategy are all kept. `indexed_packages` is the pipeline package map: a
422+
* specifier naming a package the tree itself declares (a workspace sibling)
423+
* counts as in-tree and is kept too; NULL disables that check.
424+
* `declared_packages` is the pipeline namespace map and does the same job for
425+
* package-path specifiers, which have no in-tree/external shape of their own:
426+
* `import org.example.util.assertThing` is kept when some indexed file
427+
* declares `package org.example.util`; NULL disables that check.
428+
* Pure; unit-tested in test_registry.c. */
429+
bool cbm_suppress_external_import_shadow(const char *callee_name, const char *strategy,
430+
const CBMImportArray *file_imports,
431+
const char **import_map_keys, int import_map_count,
432+
const CBMHashTable *indexed_packages,
433+
const CBMHashTable *declared_packages);
414434

415435
/* Go analog of the TS/JS guard, same failure class: a selector call whose
416436
* receiver the Go LSP could not type must not be bound by a receiver-blind

‎src/pipeline/pipeline_incremental.c‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2312,6 +2312,8 @@ static int closure_probe_surfaces(cbm_pipeline_t *p, const char *project,
23122312
* leaked one map per probed run (LSan, macOS CI). */
23132313
cbm_pkgmap_free(cbm_pipeline_get_pkgmap());
23142314
cbm_pipeline_set_pkgmap(NULL);
2315+
cbm_pipeline_namespace_map_free(cbm_pipeline_get_nsmap());
2316+
cbm_pipeline_set_nsmap(NULL);
23152317
cbm_path_alias_collection_free(aliases);
23162318
cbm_gbuf_free(probe_gbuf);
23172319
return rc;
@@ -3537,6 +3539,8 @@ static int run_closure_delta(cbm_pipeline_t *p, const char *db_path, const char
35373539
}
35383540
cbm_pkgmap_free(cbm_pipeline_get_pkgmap());
35393541
cbm_pipeline_set_pkgmap(NULL);
3542+
cbm_pipeline_namespace_map_free(cbm_pipeline_get_nsmap());
3543+
cbm_pipeline_set_nsmap(NULL);
35403544
if (phase_rc != 0) {
35413545
cbm_log_error("delta.err", "phase", "extract_resolve", "rc", itoa_buf(phase_rc));
35423546
if (phase_rc == CBM_PIPELINE_ABORT_PRESERVE_DB) {
@@ -4293,6 +4297,8 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
42934297
* full pipeline's ownership boundary on both success and failure. */
42944298
cbm_pkgmap_free(cbm_pipeline_get_pkgmap());
42954299
cbm_pipeline_set_pkgmap(NULL);
4300+
cbm_pipeline_namespace_map_free(cbm_pipeline_get_nsmap());
4301+
cbm_pipeline_set_nsmap(NULL);
42964302

42974303
if (phase_rc != 0) {
42984304
cbm_log_error("incremental.err", "phase", "extract_resolve", "rc", itoa_buf(phase_rc));

‎src/pipeline/pipeline_internal.h‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,16 @@ typedef enum {
760760
cbm_psr4_member_t cbm_pipeline_psr4_member_qn(const cbm_gbuf_t *gbuf, const char *class_fqn,
761761
const char *member, const char **out_qn);
762762

763+
/* Get the current pipeline's declared-package map (NULL if none): the
764+
* namespace/package → File-QN map built by cbm_pipeline_namespace_map_build
765+
* from every indexed file's own `package`/`namespace`/`using` declaration.
766+
* The import passes build it to resolve namespace imports; it is published
767+
* here so the call passes can ask whether a package-path specifier names a
768+
* package the tree itself declares (#1355). Set before resolve starts, read
769+
* only afterwards — same single-writer contract as the pkgmap above. */
770+
CBMHashTable *cbm_pipeline_get_nsmap(void);
771+
void cbm_pipeline_set_nsmap(CBMHashTable *map);
772+
763773
/* Unified module resolver: relative → pkgmap → fqn_module fallback.
764774
* Handles bare specifiers via pkgmap lookup with prefix matching.
765775
* Caller must free() the returned string. */

0 commit comments

Comments
 (0)