Skip to content
21 changes: 17 additions & 4 deletions src/pipeline/pass_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,8 @@ static const cbm_gbuf_node_t *calls_find_source(cbm_pipeline_ctx_t *ctx, const c

/* Resolve one call and emit the appropriate edge. Returns 1 if resolved, 0 if not. */
static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
const CBMResolvedCallArray *lsp_calls, const char *rel,
const CBMResolvedCallArray *lsp_calls,
const CBMImportArray *file_imports, const char *rel,
const char *module_qn, const char **imp_keys, const char **imp_vals,
int imp_count, CBMLanguage lang) {
const cbm_gbuf_node_t *source_node = calls_find_source(ctx, rel, call->enclosing_func_qn);
Expand Down Expand Up @@ -644,13 +645,25 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
/* The member guard's one exemption (Python, self/cls-rooted receiver,
* unique_name, not a builtin type's method) — see
* cbm_weak_member_unique_name_exempt. MUST match pass_parallel.c exactly. */
/* #1355: `import { eq } from "drizzle-orm"` binds `eq` to a package that is
* not in the indexed tree, so a project-wide same-name guess must not turn
* `eq(...)` into a CALLS edge to an unrelated project `eq`. Joined to
* drop_plain_call for the reason spelled out above: dropping the call here
* would also skip route/HTTP/CONFIG classification, and a route
* registration reached through a static import (`import static
* spark.Spark.get` + `get("/x", handler)`) is exactly a bare call bound by
* a package specifier. Suppressing only the plain-CALLS fall-through keeps
* every Route node and service edge main-identical. */
bool drop_plain_call =
(cbm_suppress_weak_member_match(suppress_weak_member, call->is_method, res.strategy) &&
!cbm_weak_member_unique_name_exempt(lang == CBM_LANG_PYTHON,
call->receiver_is_self_attribute, call->callee_name,
res.strategy)) ||
cbm_suppress_weak_local_binding_call(suppress_weak_local_binding,
call->callee_is_locally_bound, res.strategy);
call->callee_is_locally_bound, res.strategy) ||
cbm_suppress_external_import_shadow(call->callee_name, res.strategy, file_imports, imp_keys,
imp_count, cbm_pipeline_get_pkgmap(),
cbm_pipeline_get_nsmap());

/* Service-pattern HTTP/ASYNC calls to an EXTERNAL client library (e.g.
* `requests.get("/api/orders/{id}")`) resolve to a QN containing the library
Expand Down Expand Up @@ -841,8 +854,8 @@ int cbm_pipeline_pass_calls(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *file
continue;
}
total_calls++;
if (resolve_single_call(ctx, call, &result->resolved_calls, rel, module_qn, imp_keys,
imp_vals, imp_count, files[i].language)) {
if (resolve_single_call(ctx, call, &result->resolved_calls, &result->imports, rel,
module_qn, imp_keys, imp_vals, imp_count, files[i].language)) {
resolved++;
} else {
unresolved++;
Expand Down
4 changes: 3 additions & 1 deletion src/pipeline/pass_definitions.c
Original file line number Diff line number Diff line change
Expand Up @@ -911,7 +911,9 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
create_channel_edges_for_file(ctx, result, files[i].rel_path);
cbm_pipeline_create_env_configures_for_file(ctx, result, files[i].rel_path);
}
cbm_pipeline_namespace_map_free(namespace_map);
/* Publish instead of free — see the twin in pass_parallel.c (#1355). */
cbm_pipeline_namespace_map_free(cbm_pipeline_get_nsmap());
cbm_pipeline_set_nsmap(namespace_map);
if (owns_local_cache) {
for (int i = 0; i < file_count; i++) {
if (local_cache[i]) {
Expand Down
16 changes: 14 additions & 2 deletions src/pipeline/pass_parallel.c
Original file line number Diff line number Diff line change
Expand Up @@ -1753,7 +1753,11 @@ int cbm_build_registry_from_cache(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
}
}

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

cbm_log_info("parallel.registry.done", "entries", itoa_log(reg_entries), "defines",
itoa_log(defines_edges), "imports", itoa_log(imports_edges));
Expand Down Expand Up @@ -2951,13 +2955,21 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
* This gate MUST stay identical to the one there. */
bool suppress_weak_local_binding = lang == CBM_LANG_PYTHON;
/* The member guard's one exemption — MUST match pass_calls.c exactly. */
/* #1355: same guard as pass_calls.c, and joined to drop_plain_call for
* the same reason — a `continue` here would also skip route/HTTP/CONFIG
* classification, and a static-import route registration is a bare call
* bound by a package specifier. Suppress only the plain-CALLS
* fall-through so every Route node stays main-identical. */
bool drop_plain_call =
(cbm_suppress_weak_member_match(suppress_weak_member, call->is_method, res.strategy) &&
!cbm_weak_member_unique_name_exempt(lang == CBM_LANG_PYTHON,
call->receiver_is_self_attribute,
call->callee_name, res.strategy)) ||
cbm_suppress_weak_local_binding_call(suppress_weak_local_binding,
call->callee_is_locally_bound, res.strategy);
call->callee_is_locally_bound, res.strategy) ||
cbm_suppress_external_import_shadow(call->callee_name, res.strategy, &result->imports,
imp_keys, imp_count, cbm_pipeline_get_pkgmap(),
cbm_pipeline_get_nsmap());

/* Service-pattern HTTP/ASYNC client call (`requests.get(url)`): the
* service signal lives in the callee_name. The registry can mis-resolve
Expand Down
14 changes: 14 additions & 0 deletions src/pipeline/pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,18 @@ void cbm_pipeline_set_pkgmap(CBMHashTable *map) {
g_pkgmap = map;
}

/* ── Global declared-package map (one active pipeline at a time) ── */

static CBMHashTable *g_nsmap = NULL;

CBMHashTable *cbm_pipeline_get_nsmap(void) {
return g_nsmap;
}

void cbm_pipeline_set_nsmap(CBMHashTable *map) {
g_nsmap = map;
}

bool cbm_pipeline_had_format_migration(const cbm_pipeline_t *p) {
return p && p->format_migration;
}
Expand Down Expand Up @@ -2870,6 +2882,8 @@ static int cbm_pipeline_run_staged(cbm_pipeline_t *p) {
cleanup:
cbm_pkgmap_free(cbm_pipeline_get_pkgmap());
cbm_pipeline_set_pkgmap(NULL);
cbm_pipeline_namespace_map_free(cbm_pipeline_get_nsmap());
cbm_pipeline_set_nsmap(NULL);
cbm_discover_free(files, file_count);
cbm_pipeline_free_semantic_manifest(baseline_manifest, baseline_count);
cbm_gbuf_free(p->gbuf);
Expand Down
24 changes: 22 additions & 2 deletions src/pipeline/pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@
#include <stdint.h>
#include <stdatomic.h>

#include "discover/discover.h" /* cbm_ignored_file_t (#963) */
#include "foundation/constants.h" /* CBM_SZ_512 */
#include "discover/discover.h" /* cbm_ignored_file_t (#963) */
#include "foundation/constants.h" /* CBM_SZ_512 */
#include "foundation/hash_table.h" /* CBMHashTable (#1355 pkgmap lookup) */

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

/* #725: drop a suffix_match CALLS edge when the caller language and the
* target file's language disagree. unique_name (candidates == 1) is #1572
Expand Down
6 changes: 6 additions & 0 deletions src/pipeline/pipeline_incremental.c
Original file line number Diff line number Diff line change
Expand Up @@ -1625,6 +1625,8 @@ static int closure_probe_surfaces(cbm_pipeline_t *p, const char *project,
* leaked one map per probed run (LSan, macOS CI). */
cbm_pkgmap_free(cbm_pipeline_get_pkgmap());
cbm_pipeline_set_pkgmap(NULL);
cbm_pipeline_namespace_map_free(cbm_pipeline_get_nsmap());
cbm_pipeline_set_nsmap(NULL);
cbm_path_alias_collection_free(aliases);
cbm_gbuf_free(probe_gbuf);
return rc;
Expand Down Expand Up @@ -2196,6 +2198,8 @@ static int run_closure_delta(cbm_pipeline_t *p, const char *db_path, const char
}
cbm_pkgmap_free(cbm_pipeline_get_pkgmap());
cbm_pipeline_set_pkgmap(NULL);
cbm_pipeline_namespace_map_free(cbm_pipeline_get_nsmap());
cbm_pipeline_set_nsmap(NULL);
if (phase_rc != 0) {
cbm_log_error("delta.err", "phase", "extract_resolve", "rc", itoa_buf(phase_rc));
goto out;
Expand Down Expand Up @@ -2775,6 +2779,8 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
* full pipeline's ownership boundary on both success and failure. */
cbm_pkgmap_free(cbm_pipeline_get_pkgmap());
cbm_pipeline_set_pkgmap(NULL);
cbm_pipeline_namespace_map_free(cbm_pipeline_get_nsmap());
cbm_pipeline_set_nsmap(NULL);

if (phase_rc != 0) {
cbm_log_error("incremental.err", "phase", "extract_resolve", "rc", itoa_buf(phase_rc));
Expand Down
10 changes: 10 additions & 0 deletions src/pipeline/pipeline_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,16 @@ static inline int cbm_pipeline_relpath_is_excluded(const char *rel_path, char *c
CBMHashTable *cbm_pipeline_get_pkgmap(void);
void cbm_pipeline_set_pkgmap(CBMHashTable *map);

/* Get the current pipeline's declared-package map (NULL if none): the
* namespace/package → File-QN map built by cbm_pipeline_namespace_map_build
* from every indexed file's own `package`/`namespace`/`using` declaration.
* The import passes build it to resolve namespace imports; it is published
* here so the call passes can ask whether a package-path specifier names a
* package the tree itself declares (#1355). Set before resolve starts, read
* only afterwards — same single-writer contract as the pkgmap above. */
CBMHashTable *cbm_pipeline_get_nsmap(void);
void cbm_pipeline_set_nsmap(CBMHashTable *map);

/* Unified module resolver: relative → pkgmap → fqn_module fallback.
* Handles bare specifiers via pkgmap lookup with prefix matching.
* Caller must free() the returned string. */
Expand Down
Loading
Loading