Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 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 @@ -668,6 +669,15 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
if (cbm_suppress_cross_language_suffix_match(lang, target_node->file_path, res.strategy)) {
return 0;
}
/* #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`. Placed with the
* #725 guard, after the service-pattern bypasses above, so no HTTP/route
* edge can be lost to it. */
if (cbm_suppress_external_import_shadow(call->callee_name, res.strategy, file_imports, imp_keys,
imp_count, cbm_pipeline_get_pkgmap())) {
return 0;
}
emit_classified_edge(ctx, call, source_node, target_node, &res, module_qn, imp_keys, imp_vals,
imp_count, drop_plain_call);
return SKIP_ONE;
Expand Down Expand Up @@ -827,8 +837,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
8 changes: 8 additions & 0 deletions src/pipeline/pass_parallel.c
Original file line number Diff line number Diff line change
Expand Up @@ -2569,6 +2569,14 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
* CALLS edge across a language boundary. */
continue;
}
if (target_node && source_node->id != target_node->id &&
cbm_suppress_external_import_shadow(call->callee_name, res.strategy, &result->imports,
imp_keys, imp_count, cbm_pipeline_get_pkgmap())) {
/* #1355: same guard as pass_calls.c — a bare call bound by an
* external package import must not become a CALLS edge to an
* unrelated project symbol of the same name. */
continue;
}
if (!target_node || source_node->id == target_node->id) {
/* HTTP/ASYNC calls to an EXTERNAL client library (`requests.get(url)`)
* resolve to an unindexed QN (target_node == NULL), but their edge
Expand Down
19 changes: 17 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 @@ -291,6 +292,20 @@ bool cbm_suppress_weak_member_match(bool enabled, bool is_method, const char *st
* 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.
* 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);

/* #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
147 changes: 147 additions & 0 deletions src/pipeline/registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ enum { REG_MAX_CANDIDATES = 256 };
#include "foundation/dyn_array.h"
#include "foundation/platform.h"

#include <ctype.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
Expand Down Expand Up @@ -501,6 +502,152 @@ bool cbm_suppress_weak_local_binding_call(bool enabled, bool callee_is_locally_b
return weak_short_name_strategy(strategy);
}

/* A module specifier that names a path inside the indexed tree ("./x", "../x",
* "/abs/x", and on Windows "C:\x", "C:/x", "\\server\share\x") rather than an
* external package. Package specifiers are everything else — "drizzle-orm",
* "rxjs/operators", "@scope/pkg". Windows paths must count as in-tree here:
* pr-smoke runs this pipeline on Windows, and misclassifying a drive-letter or
* UNC specifier as "external" is exactly the false-external edge this guard
* exists to prevent (#1355). */
static bool specifier_is_relative(const char *module_path) {
if (!module_path || !module_path[0]) {
return false;
}
if (module_path[0] == '.' || module_path[0] == '/') {
return true;
}
/* UNC share: \\server\share\... */
if (module_path[0] == '\\' && module_path[1] == '\\') {
return true;
}
/* Drive-letter absolute path: C:\repo\... or C:/repo/... */
if (isalpha((unsigned char)module_path[0]) && module_path[1] == ':' &&
(module_path[2] == '\\' || module_path[2] == '/')) {
return true;
}
return false;
}

/* True when the specifier names a package that the indexed tree DECLARES —
* a workspace sibling, not a third-party dependency. `indexed_packages` is the
* pipeline's package map, keyed by the `name` of every manifest found in the
* tree (package.json, go.mod, Cargo.toml, ...), so a pnpm/npm/cargo workspace
* registers each of its own packages here.
*
* The key is the bare package name, so a subpath specifier is walked back one
* slash at a time: "drizzle-orm/pg-core" -> "drizzle-orm". Scoped names keep
* their leading segment ("@scope/pkg/sub" -> "@scope/pkg" -> "@scope").
*
* Deliberately a NAME test, not a resolution test. A workspace package usually
* points `main`/`module` at a build artifact ("./index.cjs") that is not
* checked in, so asking whether the entry file exists in the graph answers
* "no" for exactly the monorepos this has to protect. Whether the tree claims
* the name is decidable from the manifest alone. */
static bool specifier_names_indexed_package(const CBMHashTable *indexed_packages,
const char *module_path) {
if (!indexed_packages || !module_path || !module_path[0]) {
return false;
}
if (cbm_ht_has(indexed_packages, module_path)) {
return true;
}
char buf[CBM_SZ_512];
if (strlen(module_path) >= sizeof(buf)) {
return false;
}
snprintf(buf, sizeof(buf), "%s", module_path);
for (char *slash = strrchr(buf, '/'); slash != NULL; slash = strrchr(buf, '/')) {
*slash = '\0';
if (cbm_ht_has(indexed_packages, buf)) {
return true;
}
}
return false;
}

static bool import_map_binds(const char **import_map_keys, int import_map_count,
const char *local_name) {
if (!import_map_keys || import_map_count <= 0) {
return false;
}
for (int i = 0; i < import_map_count; i++) {
if (import_map_keys[i] && strcmp(import_map_keys[i], local_name) == 0) {
return true;
}
}
return false;
}

/* #1355: a bare call whose name the calling file binds to an EXTERNAL package
* import must not fall back to a project-wide same-name guess.
*
* `import { eq } from "drizzle-orm"` binds `eq` in this file's scope. When the
* package is not part of the indexed tree it materializes no IMPORTS edge, so
* the import map has no entry, strategies 1-2 miss, and strategy 3/4 attach
* `eq(users.id, id)` to whatever project symbol happens to share the simple
* name — a CALLS edge into an unrelated local helper. The explicit import
* statement is positive evidence, taken from the caller's own source, that the
* identifier does NOT denote that symbol.
*
* Fires only when ALL of:
* - the callee is a BARE identifier: member (`x.foo()`) and package/namespace
* qualified callees are the receiver-aware guards' business, not this one;
* - the match came from a project-wide guess (suffix_match / unique_name;
* field_type_hint / fuzzy listed for the same defensive reason as the TS/JS
* guard) — every import-, receiver- or module-aware strategy is KEPT;
* - the file imports that exact local name from a NON-RELATIVE specifier;
* - no import-map key binds the name, i.e. that import resolved to nothing in
* the graph. A name the map does bind already had its chance at strategy 1
* and is import-aware by construction.
*
* Relative specifiers are deliberately excluded: they name a path inside the
* indexed tree, so a missing IMPORTS edge is an in-project resolution gap and
* the same-name fallback can still be right. A name imported from both a
* relative and a package specifier keeps the edge — the relative binding wins
* the tie explicitly, so the outcome does not depend on import order.
*
* `indexed_packages` extends that same reasoning to workspace monorepos, where
* a BARE specifier also names in-tree code: `import { eq } from "drizzle-orm"`
* inside the drizzle-orm repository is a sibling package, not a dependency. A
* specifier the tree's own manifests claim is treated exactly like a relative
* one. NULL disables the check and restores the specifier-shape-only contract.
*
* Pure + side-effect-free so the contract is unit-testable without a pipeline. */
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) {
if (!callee_name || !callee_name[0] || !strategy || !strategy[0]) {
return false;
}
if (strcmp(strategy, "suffix_match") != 0 && strcmp(strategy, "unique_name") != 0 &&
strcmp(strategy, "field_type_hint") != 0 && strcmp(strategy, "fuzzy") != 0) {
return false;
}
if (strchr(callee_name, '.') != NULL || strstr(callee_name, "::") != NULL) {
return false;
}
if (!file_imports || file_imports->count <= 0) {
return false;
}
if (import_map_binds(import_map_keys, import_map_count, callee_name)) {
return false;
}
bool external_binding = false;
for (int i = 0; i < file_imports->count; i++) {
const CBMImport *imp = &file_imports->items[i];
if (!imp->local_name || !imp->module_path || strcmp(imp->local_name, callee_name) != 0) {
continue;
}
if (specifier_is_relative(imp->module_path) ||
specifier_names_indexed_package(indexed_packages, imp->module_path)) {
return false; /* an in-tree binding for this name — never suppress */
}
external_binding = true;
}
return external_binding;
}

static bool js_ts_family(CBMLanguage lang) {
return lang == CBM_LANG_JAVASCRIPT || lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX ||
lang == CBM_LANG_ARKTS;
Expand Down
Loading
Loading