mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 08:13:37 +08:00
Merge pull request #1766 from Yyunozor/fix/1355-external-import-shadow
fix(registry): stop external package imports binding to project homonyms
This commit is contained in:
@@ -1143,6 +1143,15 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
|
||||
/* 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,
|
||||
@@ -1159,6 +1168,9 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
|
||||
(lang == CBM_LANG_PYTHON && cbm_suppress_weak_import_bound_call(true, true, res.strategy) &&
|
||||
cbm_python_import_binding_contradicts(imports, call->callee_name, res.qualified_name,
|
||||
ctx->gbuf, ctx->project_name, rel)) ||
|
||||
cbm_suppress_external_import_shadow(call->callee_name, res.strategy, imports, imp_keys,
|
||||
imp_count, cbm_pipeline_get_pkgmap(),
|
||||
cbm_pipeline_get_nsmap()) ||
|
||||
/* MUST match pass_parallel.c exactly. */
|
||||
(lang == CBM_LANG_PHP && cbm_suppress_weak_member_match(true, true, res.strategy) &&
|
||||
cbm_pipeline_php_receiver_type_unindexed(lsp_calls, call, ctx->registry));
|
||||
|
||||
@@ -1184,7 +1184,9 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
|
||||
cbm_pipeline_diagram_arrows(ctx, result);
|
||||
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]) {
|
||||
|
||||
@@ -2041,7 +2041,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));
|
||||
@@ -3406,6 +3410,11 @@ 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,
|
||||
@@ -3422,6 +3431,9 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
|
||||
cbm_python_import_binding_contradicts(&result->imports, call->callee_name,
|
||||
res.qualified_name, rc->main_gbuf,
|
||||
rc->project_name, rel)) ||
|
||||
cbm_suppress_external_import_shadow(call->callee_name, res.strategy, &result->imports,
|
||||
imp_keys, imp_count, cbm_pipeline_get_pkgmap(),
|
||||
cbm_pipeline_get_nsmap()) ||
|
||||
/* MUST match pass_calls.c exactly. */
|
||||
(lang == CBM_LANG_PHP && cbm_suppress_weak_member_match(true, true, res.strategy) &&
|
||||
cbm_pipeline_php_receiver_type_unindexed(&result->resolved_calls, call, rc->registry));
|
||||
|
||||
@@ -792,6 +792,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;
|
||||
}
|
||||
@@ -4095,6 +4107,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);
|
||||
if (p->frozen) {
|
||||
p->frozen->baseline = baseline_manifest;
|
||||
|
||||
+22
-2
@@ -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;
|
||||
@@ -411,6 +412,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);
|
||||
|
||||
/* Go analog of the TS/JS guard, same failure class: a selector call whose
|
||||
* receiver the Go LSP could not type must not be bound by a receiver-blind
|
||||
|
||||
@@ -2312,6 +2312,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;
|
||||
@@ -3537,6 +3539,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));
|
||||
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
|
||||
* 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));
|
||||
|
||||
@@ -760,6 +760,16 @@ typedef enum {
|
||||
cbm_psr4_member_t cbm_pipeline_psr4_member_qn(const cbm_gbuf_t *gbuf, const char *class_fqn,
|
||||
const char *member, const char **out_qn);
|
||||
|
||||
/* 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. */
|
||||
|
||||
@@ -33,6 +33,7 @@ enum { REG_MAX_CANDIDATES = 256 };
|
||||
#include "foundation/platform.h"
|
||||
#include "callable_sig.h" /* cbm_qn_callable_base_len_named: overloads share the name key */
|
||||
|
||||
#include <ctype.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -750,6 +751,241 @@ 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;
|
||||
}
|
||||
|
||||
/* True when the specifier is a package PATH the indexed tree itself DECLARES.
|
||||
* `declared_packages` is the pipeline's namespace map, keyed by the
|
||||
* dot-normalized `package` / `namespace` / `use` declaration of every indexed
|
||||
* file (cbm_pipeline_namespace_map_build), so this is the JVM/CLR/PHP
|
||||
* counterpart of the manifest-name test above: those languages have no
|
||||
* specifier shape that separates in-tree from third-party, and their manifests
|
||||
* (pom.xml, build.gradle) name artifacts, not packages.
|
||||
*
|
||||
* A package-path specifier names a member inside a package, so the trailing
|
||||
* segment is walked off one at a time, exactly like the namespace-map lookup in
|
||||
* cbm_pipeline_resolve_import_node: `org.jetbrains.exposed.v1.tests.shared
|
||||
* .assertEquals` -> `org.jetbrains.exposed.v1.tests.shared`, which Exposed's
|
||||
* own Assert.kt declares, so the call keeps its edge. `java.net.URL` walks to
|
||||
* `java.net` then `java`, which no project file declares, so it stays external
|
||||
* and the guard still cuts its bind to an in-tree `URL` class.
|
||||
*
|
||||
* Separator normalization mirrors the map builder's: PHP writes `\`, some
|
||||
* grammars `::` or `/`. NULL disables the check. */
|
||||
static bool specifier_names_declared_package(const CBMHashTable *declared_packages,
|
||||
const char *module_path) {
|
||||
if (!declared_packages || !module_path || !module_path[0]) {
|
||||
return false;
|
||||
}
|
||||
char buf[CBM_SZ_512];
|
||||
if (strlen(module_path) >= sizeof(buf)) {
|
||||
return false;
|
||||
}
|
||||
snprintf(buf, sizeof(buf), "%s", module_path);
|
||||
char *brace = strchr(buf, '{');
|
||||
if (brace) {
|
||||
*brace = '\0'; /* grouped import — the prefix is the package */
|
||||
}
|
||||
char *alias = strstr(buf, " as ");
|
||||
if (alias) {
|
||||
*alias = '\0';
|
||||
}
|
||||
for (char *p = buf; *p; p++) {
|
||||
if (*p == '\\' || *p == ':' || *p == '/') {
|
||||
*p = '.';
|
||||
}
|
||||
}
|
||||
/* Collapse the empty segments "::" just produced, then strip glob/
|
||||
* separator noise off the tail. */
|
||||
for (;;) {
|
||||
char *dd = strstr(buf, "..");
|
||||
if (!dd) {
|
||||
break;
|
||||
}
|
||||
memmove(dd, dd + 1, strlen(dd + 1) + 1);
|
||||
}
|
||||
size_t len = strlen(buf);
|
||||
while (len > 0 && (buf[len - 1] == '*' || buf[len - 1] == '.' || buf[len - 1] == ' ')) {
|
||||
buf[--len] = '\0';
|
||||
}
|
||||
if (buf[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
if (cbm_ht_has(declared_packages, buf)) {
|
||||
return true;
|
||||
}
|
||||
/* The walk has no floor, and that is a deliberate false-NEGATIVE: once the
|
||||
* tree declares `com.example`, EVERY specifier under `com.example.**` reads
|
||||
* as in-tree, including a third-party library that happens to publish under
|
||||
* a declared prefix. Such a call keeps the same-name fallback it has on
|
||||
* main, so the cost is a fabricated edge this guard could have removed —
|
||||
* never a real edge lost. Stopping earlier would need a rule for where a
|
||||
* package path ends and a type begins, and getting that wrong fails the
|
||||
* other way. Pinned by external_import_shadow_declared_prefix_is_permissive
|
||||
* in test_registry.c. */
|
||||
for (char *dot = strrchr(buf, '.'); dot != NULL; dot = strrchr(buf, '.')) {
|
||||
*dot = '\0';
|
||||
if (cbm_ht_has(declared_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.
|
||||
*
|
||||
* `declared_packages` closes the same hole for the package-path languages,
|
||||
* where NO specifier shape says in-tree: `import
|
||||
* org.jetbrains.exposed.v1.tests.shared.assertEquals` is spelled exactly like
|
||||
* `import kotlin.test.assertEquals`, and only the tree's own `package`
|
||||
* declarations tell them apart. It is permissive by construction — any
|
||||
* specifier UNDER a declared package prefix counts as in-tree, see the note on
|
||||
* the walk — so it can leave a fabricated edge in place, never remove a real
|
||||
* one. NULL disables the check.
|
||||
*
|
||||
* 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,
|
||||
const CBMHashTable *declared_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) ||
|
||||
specifier_names_declared_package(declared_packages, imp->module_path)) {
|
||||
return false; /* an in-tree binding for this name — never suppress */
|
||||
}
|
||||
external_binding = true;
|
||||
}
|
||||
return external_binding;
|
||||
}
|
||||
|
||||
bool cbm_go_suppress_weak_method_match(bool is_go, bool is_method, const char *strategy,
|
||||
double confidence) {
|
||||
/* Go analog of the TS/JS guard above, same failure class: a selector call
|
||||
|
||||
@@ -11745,6 +11745,486 @@ static int count_nodes_named(cbm_store_t *s, const char *project, const char *na
|
||||
* (axios.get, api.patch on a renamed-axios instance, supertest request(app).get).
|
||||
* The regex false edge must stay suppressed in parallel too. CBM_WORKERS forces
|
||||
* >1 worker so the parallel path is taken regardless of the host core count. */
|
||||
/* #1355: padding count for write_external_import_shadow_fixture, deliberately
|
||||
* well past MIN_FILES_FOR_PARALLEL (=50, a private #define in
|
||||
* src/pipeline/pipeline.c — not exposed to tests, so this cannot be a
|
||||
* static_assert against it) rather than sitting right at the threshold. The
|
||||
* margin, not the exact value, is what the test depends on: a modest bump to
|
||||
* the real threshold must not silently drop this fixture back onto the
|
||||
* sequential-only path and leave the parallel resolver unexercised. Same
|
||||
* pattern as ET_PARALLEL_PAD / CP_PARALLEL_PAD in test_edge_types_probe.c /
|
||||
* test_convergence_probe.c. */
|
||||
enum { EXTERNAL_IMPORT_SHADOW_PARALLEL_PAD = 64 };
|
||||
|
||||
/* #1355: write the shared external-import-shadow fixture into `dir`.
|
||||
* `pad_files` filler modules push the run over MIN_FILES_FOR_PARALLEL so the
|
||||
* same tree can be indexed by both resolvers. */
|
||||
static void write_external_import_shadow_fixture(const char *dir, int pad_files) {
|
||||
/* The lone project symbols named eq/sql — ordinary local helpers. */
|
||||
write_temp_file(dir, "src/text-utils.ts",
|
||||
"export function eq(a: string, b: string): boolean {\n"
|
||||
" return a.trim() === b.trim();\n"
|
||||
"}\n"
|
||||
"export function sql(chunk: string): string {\n"
|
||||
" return chunk.replace(/\\s+/g, ' ');\n"
|
||||
"}\n"
|
||||
"export function normalize(s: string): string {\n"
|
||||
" return s.trim().toLowerCase();\n"
|
||||
"}\n");
|
||||
/* `eq` and `sql` come from an external package that is not in the tree, so
|
||||
* the registry falls through to a project-wide same-name guess. These are
|
||||
* the fabricated edges. `normalize` is imported relatively from the SAME
|
||||
* file the guess would have picked — it must survive. */
|
||||
write_temp_file(dir, "src/queries.ts",
|
||||
"import { eq, sql } from 'drizzle-orm';\n"
|
||||
"import { normalize } from './text-utils';\n"
|
||||
"export function buildQuery(id: string): unknown {\n"
|
||||
" return [eq({ id }, id), sql('select 1'), normalize(id)];\n"
|
||||
"}\n");
|
||||
/* No import of `eq` at all: nothing in the caller's source contradicts the
|
||||
* guess, so the pre-existing fallback keeps its edge. */
|
||||
write_temp_file(dir, "src/no-import.ts",
|
||||
"export function callsWithoutImport(a: string, b: string): boolean {\n"
|
||||
" return eq(a, b);\n"
|
||||
"}\n");
|
||||
for (int i = 0; i < pad_files; i++) {
|
||||
char name[64];
|
||||
char body[128];
|
||||
snprintf(name, sizeof(name), "src/pad_%02d.ts", i);
|
||||
snprintf(body, sizeof(body), "export function shadowPad%02d(): number { return %d; }\n", i,
|
||||
i);
|
||||
write_temp_file(dir, name, body);
|
||||
}
|
||||
}
|
||||
|
||||
/* #1355: assert the guard's whole contract against one indexed store. */
|
||||
static int assert_external_import_shadow_contract(const char *dir, const char *db_name) {
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/%s", dir, db_name);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(dir, db_path, CBM_MODE_FULL);
|
||||
if (!p) {
|
||||
return 1;
|
||||
}
|
||||
if (cbm_pipeline_run(p) != 0) {
|
||||
cbm_pipeline_free(p);
|
||||
return 2;
|
||||
}
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
if (!s) {
|
||||
cbm_pipeline_free(p);
|
||||
return 3;
|
||||
}
|
||||
int rc = 0;
|
||||
/* (1) the reported bug: an externally-imported name must not bind to the
|
||||
* local homonym (RED before the fix, on both resolvers). */
|
||||
if (cross_file_call_exists(s, project, "buildQuery", "eq")) {
|
||||
rc = 4;
|
||||
}
|
||||
if (rc == 0 && cross_file_call_exists(s, project, "buildQuery", "sql")) {
|
||||
rc = 5;
|
||||
}
|
||||
/* (2) the relative import to the very same file still resolves. */
|
||||
if (rc == 0 && !cross_file_call_exists(s, project, "buildQuery", "normalize")) {
|
||||
rc = 6;
|
||||
}
|
||||
/* (3) a bare call the caller never imports keeps its pre-existing edge. */
|
||||
if (rc == 0 && !cross_file_call_exists(s, project, "callsWithoutImport", "eq")) {
|
||||
rc = 7;
|
||||
}
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
return rc;
|
||||
}
|
||||
|
||||
TEST(pipeline_external_import_shadow_not_bound_to_local_homonym_issue1355) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_ext_import_shadow_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
/* Enough files that CBM_WORKERS can take the fused-parallel path; the same
|
||||
* tree is then indexed by each resolver in turn, because the guard lives at
|
||||
* two independent emit sites (pass_calls.c and pass_parallel.c). */
|
||||
write_external_import_shadow_fixture(tmp, EXTERNAL_IMPORT_SHADOW_PARALLEL_PAD);
|
||||
|
||||
/* getenv() returns a pointer into the process environment that must be
|
||||
* treated as read-only; strdup() below only ever reads through it. */
|
||||
const char *old_workers = getenv("CBM_WORKERS");
|
||||
char *saved_workers = old_workers ? strdup(old_workers) : NULL;
|
||||
const char *old_single = getenv("CBM_INDEX_SINGLE_THREAD");
|
||||
char *saved_single = old_single ? strdup(old_single) : NULL;
|
||||
|
||||
cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1);
|
||||
int sequential = assert_external_import_shadow_contract(tmp, "shadow-sequential.db");
|
||||
|
||||
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
|
||||
cbm_setenv("CBM_WORKERS", "4", 1);
|
||||
int parallel = assert_external_import_shadow_contract(tmp, "shadow-parallel.db");
|
||||
|
||||
if (saved_workers) {
|
||||
cbm_setenv("CBM_WORKERS", saved_workers, 1);
|
||||
free(saved_workers);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_WORKERS");
|
||||
}
|
||||
if (saved_single) {
|
||||
cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1);
|
||||
free(saved_single);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
|
||||
}
|
||||
th_rmtree(tmp);
|
||||
|
||||
ASSERT_EQ(sequential, 0);
|
||||
ASSERT_EQ(parallel, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #1355 / #1732: the same tree, but the bare specifier now names a WORKSPACE
|
||||
* package that the repository itself ships. `main` deliberately points at a
|
||||
* build artifact that is not checked in — the shape every real monorepo has,
|
||||
* and the reason an entry-file lookup cannot answer this question. */
|
||||
static void write_workspace_shadow_fixture(const char *dir, int pad_files) {
|
||||
/* One directory level only: write_temp_file's mkdir is documented as
|
||||
* "simple version, one level", and a deeper path fails silently — which
|
||||
* makes the fixture look healthy while the file never lands. */
|
||||
write_temp_file(dir, "lib/package.json",
|
||||
"{\n \"name\": \"@fixture/lib\",\n \"main\": \"./dist/index.js\"\n}\n");
|
||||
write_temp_file(dir, "lib/index.ts",
|
||||
"export function wsEq(a: string, b: string): boolean {\n"
|
||||
" return a === b;\n"
|
||||
"}\n");
|
||||
write_temp_file(dir, "app/package.json", "{\n \"name\": \"@fixture/app\"\n}\n");
|
||||
/* The lone project symbol named wsSpec — an ordinary local helper that a
|
||||
* project-wide guess would happily bind an external `wsSpec` to. */
|
||||
write_temp_file(dir, "app/spec-helpers.ts",
|
||||
"export function wsSpec(label: string): string {\n"
|
||||
" return label;\n"
|
||||
"}\n");
|
||||
write_temp_file(dir, "app/query.ts",
|
||||
"import { wsEq } from '@fixture/lib';\n"
|
||||
"import { wsSpec } from 'vitest';\n"
|
||||
"export function wsBuild(id: string): unknown {\n"
|
||||
" return [wsEq(id, id), wsSpec('case')];\n"
|
||||
"}\n");
|
||||
for (int i = 0; i < pad_files; i++) {
|
||||
char name[64];
|
||||
char body[128];
|
||||
snprintf(name, sizeof(name), "app/pad_%02d.ts", i);
|
||||
snprintf(body, sizeof(body), "export function wsPad%02d(): number { return %d; }\n", i, i);
|
||||
write_temp_file(dir, name, body);
|
||||
}
|
||||
}
|
||||
|
||||
static int assert_workspace_shadow_contract(const char *dir, const char *db_name) {
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/%s", dir, db_name);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(dir, db_path, CBM_MODE_FULL);
|
||||
if (!p) {
|
||||
return 1;
|
||||
}
|
||||
if (cbm_pipeline_run(p) != 0) {
|
||||
cbm_pipeline_free(p);
|
||||
return 2;
|
||||
}
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
if (!s) {
|
||||
cbm_pipeline_free(p);
|
||||
return 3;
|
||||
}
|
||||
int rc = 0;
|
||||
/* (1) the workspace sibling: the tree declares @fixture/lib, so the bare
|
||||
* specifier is in-project and the fallback keeps its chance. RED without
|
||||
* the package-map check — this is the 1377-edge class measured on
|
||||
* drizzle-team/drizzle-orm. */
|
||||
if (!cross_file_call_exists(s, project, "wsBuild", "wsEq")) {
|
||||
rc = 4;
|
||||
}
|
||||
/* (2) a genuine third-party package in the very same file is still cut:
|
||||
* the check must not disarm the guard wholesale. */
|
||||
if (rc == 0 && cross_file_call_exists(s, project, "wsBuild", "wsSpec")) {
|
||||
rc = 5;
|
||||
}
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
return rc;
|
||||
}
|
||||
|
||||
TEST(pipeline_external_import_shadow_keeps_workspace_sibling_issue1355) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_ws_import_shadow_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
write_workspace_shadow_fixture(tmp, EXTERNAL_IMPORT_SHADOW_PARALLEL_PAD);
|
||||
|
||||
const char *old_workers = getenv("CBM_WORKERS");
|
||||
char *saved_workers = old_workers ? strdup(old_workers) : NULL;
|
||||
const char *old_single = getenv("CBM_INDEX_SINGLE_THREAD");
|
||||
char *saved_single = old_single ? strdup(old_single) : NULL;
|
||||
|
||||
cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1);
|
||||
int sequential = assert_workspace_shadow_contract(tmp, "ws-sequential.db");
|
||||
|
||||
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
|
||||
cbm_setenv("CBM_WORKERS", "4", 1);
|
||||
int parallel = assert_workspace_shadow_contract(tmp, "ws-parallel.db");
|
||||
|
||||
if (saved_workers) {
|
||||
cbm_setenv("CBM_WORKERS", saved_workers, 1);
|
||||
free(saved_workers);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_WORKERS");
|
||||
}
|
||||
if (saved_single) {
|
||||
cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1);
|
||||
free(saved_single);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
|
||||
}
|
||||
th_rmtree(tmp);
|
||||
|
||||
ASSERT_EQ(sequential, 0);
|
||||
ASSERT_EQ(parallel, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #1355: the same tree once more, in a package-path language. A Kotlin/Java
|
||||
* specifier is a dotted package path whether the package is the project's own
|
||||
* or a dependency, so specifier shape decides nothing and the package map only
|
||||
* knows manifest artifact names. The tree's own `package` declarations are the
|
||||
* evidence, and the pipeline already collects them (the namespace map the
|
||||
* import passes build). `assertEquals` is imported twice — from the project's
|
||||
* package and from kotlin.test — exactly as JetBrains/Exposed writes it, which
|
||||
* is what leaves the import map without a key and hands the call to the
|
||||
* project-wide guess. */
|
||||
static void write_declared_package_shadow_fixture(const char *dir, int pad_files) {
|
||||
/* One directory level only — write_temp_file's mkdir does not recurse, and
|
||||
* a deeper path fails silently. The package path lives in the `package`
|
||||
* declaration, not in the directory layout, which is the whole point. */
|
||||
write_temp_file(dir, "util/Assert.kt",
|
||||
"package com.example.util\n"
|
||||
"\n"
|
||||
"fun pkgEq(a: String, b: String): Boolean {\n"
|
||||
" return a == b\n"
|
||||
"}\n");
|
||||
/* The lone project symbol named pkgSpec, in a third package so the guess
|
||||
* that reaches it is a project-wide one and not a same-module hit. */
|
||||
write_temp_file(dir, "spec/SpecHelpers.kt",
|
||||
"package com.example.spec\n"
|
||||
"\n"
|
||||
"fun pkgSpec(label: String): Boolean {\n"
|
||||
" return label.isNotEmpty()\n"
|
||||
"}\n");
|
||||
write_temp_file(dir, "app/Query.kt",
|
||||
"package com.example.app\n"
|
||||
"\n"
|
||||
"import com.example.util.pkgEq\n"
|
||||
"import kotlin.test.pkgEq\n"
|
||||
"import kotlin.test.pkgSpec\n"
|
||||
"import org.junit.pkgSpec\n"
|
||||
"\n"
|
||||
"fun pkgBuild(id: String): Boolean {\n"
|
||||
" return pkgEq(id, id) && pkgSpec(id)\n"
|
||||
"}\n");
|
||||
for (int i = 0; i < pad_files; i++) {
|
||||
char name[64];
|
||||
char body[128];
|
||||
snprintf(name, sizeof(name), "app/Pad%02d.kt", i);
|
||||
snprintf(body, sizeof(body),
|
||||
"package com.example.app\n\nfun pkgPad%02d(): Int {\n return %d\n}\n", i, i);
|
||||
write_temp_file(dir, name, body);
|
||||
}
|
||||
}
|
||||
|
||||
static int assert_declared_package_shadow_contract(const char *dir, const char *db_name) {
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/%s", dir, db_name);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(dir, db_path, CBM_MODE_FULL);
|
||||
if (!p) {
|
||||
return 1;
|
||||
}
|
||||
if (cbm_pipeline_run(p) != 0) {
|
||||
cbm_pipeline_free(p);
|
||||
return 2;
|
||||
}
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
if (!s) {
|
||||
cbm_pipeline_free(p);
|
||||
return 3;
|
||||
}
|
||||
int rc = 0;
|
||||
/* The package the tree declares: `com.example.util` is in-project, so the
|
||||
* call keeps its edge. RED without the declared-package check — this is the
|
||||
* 426-edge class measured on JetBrains/Exposed.
|
||||
*
|
||||
* The matching negative — an undeclared package is still cut — is asserted
|
||||
* where it can be reached on BOTH resolvers: directly on the predicate in
|
||||
* test_registry.c (external_import_shadow_declared_package_kept, including
|
||||
* `java.net.URL`), and end-to-end in the route fixture below. The
|
||||
* sequential resolver builds its own import map straight from the
|
||||
* extraction records, without the ambiguity check its parallel twin
|
||||
* applies, so a doubly-imported Kotlin name resolves by `import_map` there
|
||||
* and never reaches this guard at all — asserting the negative here would
|
||||
* assert that quirk, not this contract. */
|
||||
if (!cross_file_call_exists(s, project, "pkgBuild", "pkgEq")) {
|
||||
rc = 4;
|
||||
}
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
return rc;
|
||||
}
|
||||
|
||||
TEST(pipeline_external_import_shadow_keeps_declared_package_issue1355) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_pkg_import_shadow_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
write_declared_package_shadow_fixture(tmp, EXTERNAL_IMPORT_SHADOW_PARALLEL_PAD);
|
||||
|
||||
const char *old_workers = getenv("CBM_WORKERS");
|
||||
char *saved_workers = old_workers ? strdup(old_workers) : NULL;
|
||||
const char *old_single = getenv("CBM_INDEX_SINGLE_THREAD");
|
||||
char *saved_single = old_single ? strdup(old_single) : NULL;
|
||||
|
||||
cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1);
|
||||
int sequential = assert_declared_package_shadow_contract(tmp, "pkg-sequential.db");
|
||||
|
||||
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
|
||||
cbm_setenv("CBM_WORKERS", "4", 1);
|
||||
int parallel = assert_declared_package_shadow_contract(tmp, "pkg-parallel.db");
|
||||
|
||||
if (saved_workers) {
|
||||
cbm_setenv("CBM_WORKERS", saved_workers, 1);
|
||||
free(saved_workers);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_WORKERS");
|
||||
}
|
||||
if (saved_single) {
|
||||
cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1);
|
||||
free(saved_single);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
|
||||
}
|
||||
th_rmtree(tmp);
|
||||
|
||||
ASSERT_EQ(sequential, 0);
|
||||
ASSERT_EQ(parallel, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #1355: the guard must suppress ONLY the plain-CALLS fall-through, never the
|
||||
* route/HTTP/CONFIG classification that runs inside the emitters. A bare call
|
||||
* bound by a package import is exactly the shape a route registration takes
|
||||
* when the router is a dependency, and dropping it before classification costs
|
||||
* the Route node and every HANDLES edge downstream of it. Same reasoning, and
|
||||
* the same drop_plain_call seam, as the #592/#606 member guard. */
|
||||
static void write_route_shadow_fixture(const char *dir, int pad_files) {
|
||||
/* The project symbols the guess would bind to. The file name puts "express"
|
||||
* in their qualified names, which is what makes the resolved QN classify as
|
||||
* a route registration — the same substring match main uses on real trees. */
|
||||
write_temp_file(dir, "app/express-routes.ts",
|
||||
"export function regGet(path: string, handler: unknown): string {\n"
|
||||
" return path;\n"
|
||||
"}\n"
|
||||
"export function regSpec(label: string): string {\n"
|
||||
" return label;\n"
|
||||
"}\n");
|
||||
/* Both names come from a package outside the tree. `regGet` carries a
|
||||
* path-shaped first argument, so it is a route registration; `regSpec` is
|
||||
* an ordinary fabricated edge and must still go. */
|
||||
write_temp_file(dir, "app/server.ts",
|
||||
"import { regGet, regSpec } from 'vendor-router';\n"
|
||||
"export function boot(): string {\n"
|
||||
" return regGet('/orders', () => 'ok') + regSpec('case');\n"
|
||||
"}\n");
|
||||
for (int i = 0; i < pad_files; i++) {
|
||||
char name[64];
|
||||
char body[128];
|
||||
snprintf(name, sizeof(name), "app/pad_%02d.ts", i);
|
||||
snprintf(body, sizeof(body), "export function routePad%02d(): number { return %d; }\n", i,
|
||||
i);
|
||||
write_temp_file(dir, name, body);
|
||||
}
|
||||
}
|
||||
|
||||
static int assert_route_shadow_contract(const char *dir, const char *db_name) {
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/%s", dir, db_name);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(dir, db_path, CBM_MODE_FULL);
|
||||
if (!p) {
|
||||
return 1;
|
||||
}
|
||||
if (cbm_pipeline_run(p) != 0) {
|
||||
cbm_pipeline_free(p);
|
||||
return 2;
|
||||
}
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
if (!s) {
|
||||
cbm_pipeline_free(p);
|
||||
return 3;
|
||||
}
|
||||
int rc = 0;
|
||||
/* (1) the Route node survives the guard. RED when the guard returns before
|
||||
* the emitter: main mints __route__ANY__/orders here and the published
|
||||
* guard lost it, together with every HANDLES edge that hangs off it. */
|
||||
if (count_nodes_named(s, project, "/orders") < 1) {
|
||||
rc = 4;
|
||||
}
|
||||
/* (2) the fabricated plain-CALLS edge in the same file is still removed, so
|
||||
* (1) cannot pass by the guard having been disarmed. */
|
||||
if (rc == 0 && cross_file_call_exists(s, project, "boot", "regSpec")) {
|
||||
rc = 5;
|
||||
}
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
return rc;
|
||||
}
|
||||
|
||||
TEST(pipeline_external_import_shadow_keeps_route_registration_issue1355) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_route_import_shadow_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
write_route_shadow_fixture(tmp, EXTERNAL_IMPORT_SHADOW_PARALLEL_PAD);
|
||||
|
||||
const char *old_workers = getenv("CBM_WORKERS");
|
||||
char *saved_workers = old_workers ? strdup(old_workers) : NULL;
|
||||
const char *old_single = getenv("CBM_INDEX_SINGLE_THREAD");
|
||||
char *saved_single = old_single ? strdup(old_single) : NULL;
|
||||
|
||||
cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1);
|
||||
int sequential = assert_route_shadow_contract(tmp, "route-sequential.db");
|
||||
|
||||
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
|
||||
cbm_setenv("CBM_WORKERS", "4", 1);
|
||||
int parallel = assert_route_shadow_contract(tmp, "route-parallel.db");
|
||||
|
||||
if (saved_workers) {
|
||||
cbm_setenv("CBM_WORKERS", saved_workers, 1);
|
||||
free(saved_workers);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_WORKERS");
|
||||
}
|
||||
if (saved_single) {
|
||||
cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1);
|
||||
free(saved_single);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
|
||||
}
|
||||
th_rmtree(tmp);
|
||||
|
||||
ASSERT_EQ(sequential, 0);
|
||||
ASSERT_EQ(parallel, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pipeline_tsjs_receiver_parallel_keeps_service_edges) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_tsjs_par_XXXXXX");
|
||||
@@ -27711,6 +28191,10 @@ SUITE(pipeline) {
|
||||
RUN_TEST(pipeline_python_receiver_parallel_suppresses_weak_method_edges);
|
||||
RUN_TEST(pipeline_python_bare_local_binding_suppresses_weak_edge);
|
||||
RUN_TEST(pipeline_python_bare_local_binding_parallel_suppresses_weak_edge);
|
||||
RUN_TEST(pipeline_external_import_shadow_not_bound_to_local_homonym_issue1355);
|
||||
RUN_TEST(pipeline_external_import_shadow_keeps_workspace_sibling_issue1355);
|
||||
RUN_TEST(pipeline_external_import_shadow_keeps_declared_package_issue1355);
|
||||
RUN_TEST(pipeline_external_import_shadow_keeps_route_registration_issue1355);
|
||||
RUN_TEST(pipeline_python_package_relative_imports);
|
||||
RUN_TEST(pipeline_python_package_relative_imports_parallel);
|
||||
RUN_TEST(pipeline_python_from_import_bare_call_issue1237);
|
||||
|
||||
@@ -1688,6 +1688,307 @@ TEST(weak_call_guards_share_one_drop_list) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(external_import_shadow_drops_package_bound_bare_call) {
|
||||
/* #1355: `import { eq, sql } from "drizzle-orm"` binds both names to a
|
||||
* package outside the indexed tree, so no import-map key exists for them and
|
||||
* a project-wide same-name guess is a fabricated edge. */
|
||||
CBMImport imports[] = {
|
||||
{.local_name = "eq", .module_path = "drizzle-orm"},
|
||||
{.local_name = "sql", .module_path = "drizzle-orm"},
|
||||
{.local_name = "users", .module_path = "./schema"},
|
||||
};
|
||||
CBMImportArray arr = {.items = imports, .count = 3, .cap = 3};
|
||||
/* Only the relative import materialized an IMPORTS edge. */
|
||||
const char *keys[] = {"users"};
|
||||
|
||||
ASSERT_TRUE(
|
||||
cbm_suppress_external_import_shadow("eq", "unique_name", &arr, keys, 1, NULL, NULL));
|
||||
ASSERT_TRUE(
|
||||
cbm_suppress_external_import_shadow("sql", "unique_name", &arr, keys, 1, NULL, NULL));
|
||||
ASSERT_TRUE(
|
||||
cbm_suppress_external_import_shadow("eq", "suffix_match", &arr, keys, 1, NULL, NULL));
|
||||
ASSERT_TRUE(
|
||||
cbm_suppress_external_import_shadow("eq", "field_type_hint", &arr, keys, 1, NULL, NULL));
|
||||
ASSERT_TRUE(cbm_suppress_external_import_shadow("eq", "fuzzy", &arr, keys, 1, NULL, NULL));
|
||||
/* A file whose imports all failed to materialize is still covered: the
|
||||
* evidence is the import statement, not the map. */
|
||||
ASSERT_TRUE(
|
||||
cbm_suppress_external_import_shadow("eq", "unique_name", &arr, NULL, 0, NULL, NULL));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(external_import_shadow_keeps_everything_else) {
|
||||
/* Negative space: every input that is NOT "bare call bound to a package
|
||||
* import the graph could not resolve" must keep its edge. */
|
||||
CBMImport imports[] = {
|
||||
{.local_name = "eq", .module_path = "drizzle-orm"},
|
||||
{.local_name = "normalize", .module_path = "./text-utils"},
|
||||
{.local_name = "getRootContainer", .module_path = "@repo/shared"},
|
||||
};
|
||||
CBMImportArray arr = {.items = imports, .count = 3, .cap = 3};
|
||||
/* The relative import and the workspace package both materialized. */
|
||||
const char *keys[] = {"normalize", "getRootContainer"};
|
||||
const int nkeys = 2;
|
||||
|
||||
/* Import-/receiver-/module-aware strategies are never this guard's business. */
|
||||
ASSERT_FALSE(
|
||||
cbm_suppress_external_import_shadow("eq", "import_map", &arr, keys, nkeys, NULL, NULL));
|
||||
ASSERT_FALSE(cbm_suppress_external_import_shadow("eq", "import_map_suffix", &arr, keys, nkeys,
|
||||
NULL, NULL));
|
||||
ASSERT_FALSE(
|
||||
cbm_suppress_external_import_shadow("eq", "same_module", &arr, keys, nkeys, NULL, NULL));
|
||||
ASSERT_FALSE(cbm_suppress_external_import_shadow("eq", "qualified_suffix", &arr, keys, nkeys,
|
||||
NULL, NULL));
|
||||
ASSERT_FALSE(
|
||||
cbm_suppress_external_import_shadow("eq", "lsp_ts_import", &arr, keys, nkeys, NULL, NULL));
|
||||
ASSERT_FALSE(
|
||||
cbm_suppress_external_import_shadow("eq", "lsp_ts_method", &arr, keys, nkeys, NULL, NULL));
|
||||
ASSERT_FALSE(cbm_suppress_external_import_shadow("eq", "service_pattern", &arr, keys, nkeys,
|
||||
NULL, NULL));
|
||||
/* A relative specifier names a path inside the tree — a missing IMPORTS edge
|
||||
* there is an in-project gap, not an external binding. */
|
||||
ASSERT_FALSE(cbm_suppress_external_import_shadow("normalize", "unique_name", &arr, keys, nkeys,
|
||||
NULL, NULL));
|
||||
ASSERT_FALSE(
|
||||
cbm_suppress_external_import_shadow("normalize", "unique_name", &arr, NULL, 0, NULL, NULL));
|
||||
/* A package the import map DOES bind resolved in-graph (workspace package). */
|
||||
ASSERT_FALSE(cbm_suppress_external_import_shadow("getRootContainer", "unique_name", &arr, keys,
|
||||
nkeys, NULL, NULL));
|
||||
/* A name the file never imports (called with no import at all). */
|
||||
ASSERT_FALSE(cbm_suppress_external_import_shadow("helper", "unique_name", &arr, keys, nkeys,
|
||||
NULL, NULL));
|
||||
/* Member and package/namespace-qualified callees belong to the
|
||||
* receiver-aware guards, not to this one. */
|
||||
ASSERT_FALSE(cbm_suppress_external_import_shadow("eq.apply", "unique_name", &arr, keys, nkeys,
|
||||
NULL, NULL));
|
||||
ASSERT_FALSE(cbm_suppress_external_import_shadow("eq::apply", "unique_name", &arr, keys, nkeys,
|
||||
NULL, NULL));
|
||||
/* Empty / absent inputs. */
|
||||
ASSERT_FALSE(cbm_suppress_external_import_shadow("eq", NULL, &arr, keys, nkeys, NULL, NULL));
|
||||
ASSERT_FALSE(cbm_suppress_external_import_shadow("eq", "", &arr, keys, nkeys, NULL, NULL));
|
||||
ASSERT_FALSE(
|
||||
cbm_suppress_external_import_shadow(NULL, "unique_name", &arr, keys, nkeys, NULL, NULL));
|
||||
ASSERT_FALSE(
|
||||
cbm_suppress_external_import_shadow("", "unique_name", &arr, keys, nkeys, NULL, NULL));
|
||||
ASSERT_FALSE(
|
||||
cbm_suppress_external_import_shadow("eq", "unique_name", NULL, keys, nkeys, NULL, NULL));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(external_import_shadow_relative_binding_wins_the_tie) {
|
||||
/* A name imported from BOTH a package and a relative path keeps its edge,
|
||||
* and does so independently of the order the imports were extracted in —
|
||||
* the relative binding is an explicit tie-break, not an emergent property
|
||||
* of iteration order. */
|
||||
CBMImport pkg_first[] = {
|
||||
{.local_name = "eq", .module_path = "drizzle-orm"},
|
||||
{.local_name = "eq", .module_path = "./text-utils"},
|
||||
};
|
||||
CBMImport rel_first[] = {
|
||||
{.local_name = "eq", .module_path = "./text-utils"},
|
||||
{.local_name = "eq", .module_path = "drizzle-orm"},
|
||||
};
|
||||
CBMImportArray a = {.items = pkg_first, .count = 2, .cap = 2};
|
||||
CBMImportArray b = {.items = rel_first, .count = 2, .cap = 2};
|
||||
ASSERT_FALSE(cbm_suppress_external_import_shadow("eq", "unique_name", &a, NULL, 0, NULL, NULL));
|
||||
ASSERT_FALSE(cbm_suppress_external_import_shadow("eq", "unique_name", &b, NULL, 0, NULL, NULL));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(external_import_shadow_windows_relative_specifier_kept) {
|
||||
/* #1355 follow-up: a Windows-style specifier —
|
||||
* drive-letter absolute (`C:\...` / `C:/...`) or UNC share (`\\server\...`)
|
||||
* — names a path inside the indexed tree exactly like a POSIX "./x" or
|
||||
* "/x". pr-smoke runs this pipeline on Windows; before this fix these
|
||||
* specifiers fell through to "external package" and the same-name guess
|
||||
* they'd otherwise validate got suppressed on that platform only. */
|
||||
CBMImport imports[] = {
|
||||
{.local_name = "eq", .module_path = "C:\\repo\\src\\text-utils.ts"},
|
||||
{.local_name = "sql", .module_path = "C:/repo/src/text-utils.ts"},
|
||||
{.local_name = "normalize", .module_path = "\\\\server\\share\\text-utils.ts"},
|
||||
};
|
||||
CBMImportArray arr = {.items = imports, .count = 3, .cap = 3};
|
||||
|
||||
/* None of these materialized an import-map key (as if resolution missed
|
||||
* them, mirroring the reported scenario) — the specifier alone must still
|
||||
* keep the edge because it is in-tree, not external. RED before the fix
|
||||
* (specifier_is_relative saw '.' / '/' only, so all three were classified
|
||||
* external and suppressed); GREEN after. */
|
||||
ASSERT_FALSE(
|
||||
cbm_suppress_external_import_shadow("eq", "unique_name", &arr, NULL, 0, NULL, NULL));
|
||||
ASSERT_FALSE(
|
||||
cbm_suppress_external_import_shadow("sql", "unique_name", &arr, NULL, 0, NULL, NULL));
|
||||
ASSERT_FALSE(
|
||||
cbm_suppress_external_import_shadow("normalize", "unique_name", &arr, NULL, 0, NULL, NULL));
|
||||
/* A bare package specifier without any drive letter or leading separator
|
||||
* (e.g. "drizzle-orm") must still be external — this predicate must not
|
||||
* become "anything with a colon or backslash". */
|
||||
CBMImport pkg_only[] = {
|
||||
{.local_name = "eq", .module_path = "drizzle-orm"},
|
||||
};
|
||||
CBMImportArray parr = {.items = pkg_only, .count = 1, .cap = 1};
|
||||
ASSERT_TRUE(
|
||||
cbm_suppress_external_import_shadow("eq", "unique_name", &parr, NULL, 0, NULL, NULL));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(external_import_shadow_workspace_sibling_kept) {
|
||||
/* #1355 / #1732: inside a workspace monorepo a BARE specifier can still name
|
||||
* in-tree code — `import { eq } from "drizzle-orm"` written inside the
|
||||
* drizzle-orm repository is a sibling package, not a dependency. Measured on
|
||||
* drizzle-team/drizzle-orm at b7862528: without this check the guard removed
|
||||
* 2609 CALLS edges, 1377 of which pointed at exactly the file the specifier
|
||||
* names. The package map is keyed by each in-tree manifest's `name`, so the
|
||||
* tree's own claim decides. */
|
||||
CBMImport imports[] = {
|
||||
{.local_name = "eq", .module_path = "drizzle-orm"},
|
||||
{.local_name = "pgTable", .module_path = "drizzle-orm/pg-core"},
|
||||
{.local_name = "hoisted", .module_path = "@scope/kit/sub"},
|
||||
{.local_name = "test", .module_path = "vitest"},
|
||||
};
|
||||
CBMImportArray arr = {.items = imports, .count = 4, .cap = 4};
|
||||
|
||||
CBMHashTable *pkgs = cbm_ht_create(8);
|
||||
ASSERT_NOT_NULL(pkgs);
|
||||
cbm_ht_set(pkgs, "drizzle-orm", (void *)"qn");
|
||||
cbm_ht_set(pkgs, "@scope/kit", (void *)"qn");
|
||||
|
||||
/* Exact name, subpath, and scoped subpath all resolve to an in-tree
|
||||
* package — the same-name fallback keeps its chance. */
|
||||
ASSERT_FALSE(
|
||||
cbm_suppress_external_import_shadow("eq", "unique_name", &arr, NULL, 0, pkgs, NULL));
|
||||
ASSERT_FALSE(
|
||||
cbm_suppress_external_import_shadow("pgTable", "suffix_match", &arr, NULL, 0, pkgs, NULL));
|
||||
ASSERT_FALSE(
|
||||
cbm_suppress_external_import_shadow("hoisted", "unique_name", &arr, NULL, 0, pkgs, NULL));
|
||||
/* A genuine third-party dependency is untouched by the new check: the tree
|
||||
* declares no package called "vitest", so the #1355 removal still happens. */
|
||||
ASSERT_TRUE(
|
||||
cbm_suppress_external_import_shadow("test", "suffix_match", &arr, NULL, 0, pkgs, NULL));
|
||||
|
||||
/* A tree with no manifests at all (pkgmap NULL) keeps the pre-existing
|
||||
* specifier-shape-only contract — every one of these is external again. */
|
||||
ASSERT_TRUE(
|
||||
cbm_suppress_external_import_shadow("eq", "unique_name", &arr, NULL, 0, NULL, NULL));
|
||||
ASSERT_TRUE(
|
||||
cbm_suppress_external_import_shadow("pgTable", "suffix_match", &arr, NULL, 0, NULL, NULL));
|
||||
|
||||
/* An unrelated package map must not accidentally match by prefix: "drizzle"
|
||||
* is not "drizzle-orm", and the walk is by path segment, not by substring. */
|
||||
CBMHashTable *other = cbm_ht_create(8);
|
||||
ASSERT_NOT_NULL(other);
|
||||
cbm_ht_set(other, "drizzle", (void *)"qn");
|
||||
ASSERT_TRUE(
|
||||
cbm_suppress_external_import_shadow("eq", "unique_name", &arr, NULL, 0, other, NULL));
|
||||
|
||||
cbm_ht_free(other);
|
||||
cbm_ht_free(pkgs);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(external_import_shadow_declared_package_kept) {
|
||||
/* #1355: package-path specifiers (Java, Kotlin, C#, PHP) carry NO shape
|
||||
* that separates in-tree from third-party — `import
|
||||
* org.jetbrains.exposed.v1.tests.shared.assertEquals` is spelled exactly
|
||||
* like `import kotlin.test.assertEquals`, and neither is "relative". Only
|
||||
* the tree's own `package` declarations tell them apart; the manifests the
|
||||
* package map reads (pom.xml, build.gradle) name artifacts, not packages.
|
||||
* Measured on JetBrains/Exposed at 0e4d81a4: without this check the guard
|
||||
* removed 426 CALLS edges, every one of them a real in-project call into
|
||||
* the repository's own Assert.kt. */
|
||||
CBMImport imports[] = {
|
||||
{.local_name = "assertEquals", .module_path = "org.example.tests.shared.assertEquals"},
|
||||
{.local_name = "assertEquals", .module_path = "kotlin.test.assertEquals"},
|
||||
{.local_name = "Helper", .module_path = "org.example.util.Helper"},
|
||||
{.local_name = "URL", .module_path = "java.net.URL"},
|
||||
{.local_name = "Logger", .module_path = "App\\Support\\Logger"},
|
||||
};
|
||||
CBMImportArray arr = {.items = imports, .count = 5, .cap = 5};
|
||||
|
||||
CBMHashTable *nspkgs = cbm_ht_create(8);
|
||||
ASSERT_NOT_NULL(nspkgs);
|
||||
cbm_ht_set(nspkgs, "org.example.tests.shared", (void *)"qn");
|
||||
cbm_ht_set(nspkgs, "org.example.util", (void *)"qn");
|
||||
cbm_ht_set(nspkgs, "App.Support", (void *)"qn");
|
||||
|
||||
/* The specifier names a member of a package the tree declares, so the walk
|
||||
* strips the member segment and finds it — the call keeps its edge. */
|
||||
ASSERT_FALSE(cbm_suppress_external_import_shadow("assertEquals", "suffix_match", &arr, NULL, 0,
|
||||
NULL, nspkgs));
|
||||
ASSERT_FALSE(
|
||||
cbm_suppress_external_import_shadow("Helper", "unique_name", &arr, NULL, 0, NULL, nspkgs));
|
||||
/* PHP writes its separator as a backslash; the key normalization has to
|
||||
* match the namespace map's, which stores dots. */
|
||||
ASSERT_FALSE(
|
||||
cbm_suppress_external_import_shadow("Logger", "unique_name", &arr, NULL, 0, NULL, nspkgs));
|
||||
/* Negative space, and the effect the census called valuable: `java.net.URL`
|
||||
* walks to "java.net" then "java", neither declared by any project file, so
|
||||
* a bare `URL` bound to an in-tree URL class is still cut. */
|
||||
ASSERT_TRUE(
|
||||
cbm_suppress_external_import_shadow("URL", "suffix_match", &arr, NULL, 0, NULL, nspkgs));
|
||||
/* NULL restores the previous contract: without the declared-package set
|
||||
* every one of these reads as external again. */
|
||||
ASSERT_TRUE(cbm_suppress_external_import_shadow("assertEquals", "suffix_match", &arr, NULL, 0,
|
||||
NULL, NULL));
|
||||
ASSERT_TRUE(
|
||||
cbm_suppress_external_import_shadow("Helper", "unique_name", &arr, NULL, 0, NULL, NULL));
|
||||
|
||||
/* The walk is by dotted segment, not by substring: a tree that declares
|
||||
* "org.example.tests" must not claim "org.example.testsuite.helper". */
|
||||
CBMImport near[] = {
|
||||
{.local_name = "near", .module_path = "org.example.testsuite.helper.near"},
|
||||
};
|
||||
CBMImportArray narr = {.items = near, .count = 1, .cap = 1};
|
||||
CBMHashTable *prefix_only = cbm_ht_create(8);
|
||||
ASSERT_NOT_NULL(prefix_only);
|
||||
cbm_ht_set(prefix_only, "org.example.tests", (void *)"qn");
|
||||
ASSERT_TRUE(cbm_suppress_external_import_shadow("near", "unique_name", &narr, NULL, 0, NULL,
|
||||
prefix_only));
|
||||
|
||||
cbm_ht_free(prefix_only);
|
||||
cbm_ht_free(nspkgs);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(external_import_shadow_declared_prefix_is_permissive) {
|
||||
/* #1355, known limitation pinned deliberately. The declared-package walk
|
||||
* has no floor: once the tree declares `com.example`, a third-party
|
||||
* library publishing under `com.example.vendor` reads as in-tree and the
|
||||
* guard stands down. The call then keeps exactly the edge it has on main,
|
||||
* so this direction costs a fabricated edge the guard could have removed —
|
||||
* it never removes a real one, which is the side a call-resolution
|
||||
* suppressor has to fail on.
|
||||
*
|
||||
* This test exists so the behaviour cannot change silently: tightening the
|
||||
* walk (stopping at the first type-shaped segment, say) must flip these
|
||||
* two assertions, not slip through unnoticed. */
|
||||
CBMImport imports[] = {
|
||||
{.local_name = "vendorCall", .module_path = "com.example.vendor.pkg.vendorCall"},
|
||||
};
|
||||
CBMImportArray arr = {.items = imports, .count = 1, .cap = 1};
|
||||
|
||||
CBMHashTable *declares_prefix = cbm_ht_create(8);
|
||||
ASSERT_NOT_NULL(declares_prefix);
|
||||
cbm_ht_set(declares_prefix, "com.example", (void *)"qn");
|
||||
/* The tree declares only the prefix, yet the whole subtree is treated as
|
||||
* in-tree — the permissive direction. */
|
||||
ASSERT_FALSE(cbm_suppress_external_import_shadow("vendorCall", "unique_name", &arr, NULL, 0,
|
||||
NULL, declares_prefix));
|
||||
|
||||
/* Change nothing but the declared package and the guard fires again, which
|
||||
* is what proves the previous assertion came from the prefix walk and not
|
||||
* from some other clause standing down. */
|
||||
CBMHashTable *declares_other = cbm_ht_create(8);
|
||||
ASSERT_NOT_NULL(declares_other);
|
||||
cbm_ht_set(declares_other, "com.other", (void *)"qn");
|
||||
ASSERT_TRUE(cbm_suppress_external_import_shadow("vendorCall", "unique_name", &arr, NULL, 0,
|
||||
NULL, declares_other));
|
||||
|
||||
cbm_ht_free(declares_other);
|
||||
cbm_ht_free(declares_prefix);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(go_suppress_drops_weak_selector_matches) {
|
||||
/* Go selector call with an untyped receiver, landed via a receiver-blind
|
||||
* short-name strategy → drop (same failure class as #592/#606).
|
||||
@@ -2286,6 +2587,13 @@ SUITE(registry) {
|
||||
RUN_TEST(python_import_binding_contradicts_only_foreign_chains);
|
||||
RUN_TEST(python_external_base_contradicts_django_shapes);
|
||||
RUN_TEST(weak_call_guards_share_one_drop_list);
|
||||
RUN_TEST(external_import_shadow_drops_package_bound_bare_call);
|
||||
RUN_TEST(external_import_shadow_keeps_everything_else);
|
||||
RUN_TEST(external_import_shadow_relative_binding_wins_the_tie);
|
||||
RUN_TEST(external_import_shadow_windows_relative_specifier_kept);
|
||||
RUN_TEST(external_import_shadow_workspace_sibling_kept);
|
||||
RUN_TEST(external_import_shadow_declared_package_kept);
|
||||
RUN_TEST(external_import_shadow_declared_prefix_is_permissive);
|
||||
RUN_TEST(go_suppress_drops_weak_selector_matches);
|
||||
RUN_TEST(go_suppress_keeps_typed_and_import_aware_matches);
|
||||
RUN_TEST(registry_python_function_local_is_exact_only_issue1548);
|
||||
|
||||
Reference in New Issue
Block a user