Files
wehub-resource-sync 41cb1c0170
OpenSSF Scorecard / scorecard (push) Failing after 0s
DCO / dco (push) Failing after 0s
CodeQL SAST / analyze (push) Failing after 1s
Deploy Pages / deploy (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:28:05 +08:00

403 lines
15 KiB
C

/*
* repro_issue842.c -- Reproduce-first / regression guard for bug #842.
*
* Issue #842: "graph: same clobber class as #787 in env-access CONFIGURES
* sources and resolve_file_throws"
*
* Follow-up disclosed during #828's review (stable USAGE-edge ownership,
* #787): #828 fixed three source-node finders (find_enclosing_node in
* pass_usages.c, find_source_node in pass_parallel.c, calls_find_source in
* pass_calls.c) so a class-level construct's enclosing-QN lookup that lands
* on the shared Folder/Project node (directory-module languages, Java/Go)
* falls back to the per-file File node instead of conflating every
* same-package file onto one source. #828 deliberately left two adjacent
* direct enclosing-QN lookups out of scope:
*
* 1. create_env_configures_for_file (pass_definitions.c) — CONFIGURES edge
* source for an env-var access.
* 2. resolve_file_throws (pass_parallel.c) — THROWS/RAISES edge source for
* a thrown/raised exception.
*
* Both called cbm_gbuf_find_by_qn(enclosing_func_qn) directly, with no guard
* against the lookup landing on a Folder/Project node — the exact #787 root
* cause, just unguarded at two more sites.
*
* FIXTURE: two directory-module languages, one per call site, both exercising
* the same "no enclosing function -> enclosing_func_qn falls back to the
* MODULE QN" condition (helpers.c:900-902), which for a directory-based-module
* language collides with the shared package Folder node's QN:
*
* - THROWS: two same-package Java files (owner/ServiceA.java,
* owner/ServiceB.java), each throwing a shared exception type from a
* static initializer block (no enclosing method/constructor).
* - CONFIGURES: two same-package Go files (goenv/service_a.go,
* goenv/service_b.go), each reading the same env var in a package-level
* var declaration (no enclosing function). Go was used here instead of
* Java because Java's env-access detection (System.getenv) is separately
* broken -- extract_env_key_from_call reads a "function" field that
* Java's method_invocation node doesn't have (it uses "object"/"name"
* instead), so no EnvVar node is ever created for Java. That is a
* pre-existing, unrelated bug in the extraction layer, out of scope here.
*
* Correct THROWS sources targeting the ConfigException Class node, and
* CONFIGURES sources targeting the DB_URL EnvVar node, must each be exactly
* two distinct files.
*
* RED on unfixed HEAD: both same-package files collapse onto the single
* shared Folder/Module node for their package, so each query returns at most
* 1 distinct source instead of 2. Confirmed empirically that this only
* reproduces on resolve_file_throws's parallel (>MIN_FILES_FOR_PARALLEL=50
* files) worker-merge path -- the small sequential fixture already attributes
* correctly even on unfixed HEAD, so the THROWS test pads with 60 inert
* same-package filler classes (see build_parallel_fixture) to force that
* path, mirroring #787's own two-fixture (sequential + parallel) precedent.
* create_env_configures_for_file's CONFIGURES bug, by contrast, reproduces on
* the small sequential fixture already, so the CONFIGURES test needs no
* padding.
*/
#include <foundation/compat.h>
#include "test_framework.h"
#include "repro_harness.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
/* ── Fixture files ──────────────────────────────────────────────────────── */
/* ConfigException: the type that will be a THROWS target. Named to avoid
* "Error"/"Panic" so is_checked_exception (pass_parallel.c) classifies it as
* a checked exception and emits a THROWS edge rather than RAISES. */
static const char k_config_exception[] =
"package org.example.owner;\n"
"\n"
"public class ConfigException extends RuntimeException {\n"
" public ConfigException(String m) { super(m); }\n"
"}\n";
/* ServiceA: env access + throw, both inside a static initializer — no
* enclosing method, so both extractions record enclosing_func_qn as the
* MODULE QN (the #787/#842 collision condition). */
static const char k_service_a[] =
"package org.example.owner;\n"
"\n"
"public class ServiceA {\n"
" static {\n"
" String url = System.getenv(\"DB_URL\");\n"
" if (url == null) {\n"
" throw new ConfigException(\"DB_URL not set\");\n"
" }\n"
" }\n"
"}\n";
/* ServiceB: same shape as ServiceA — same-package, same-class-level-only
* env access and throw. */
static const char k_service_b[] =
"package org.example.owner;\n"
"\n"
"public class ServiceB {\n"
" static {\n"
" String url = System.getenv(\"DB_URL\");\n"
" if (url == null) {\n"
" throw new ConfigException(\"DB_URL not set\");\n"
" }\n"
" }\n"
"}\n";
/* Go env-access fixture: two same-package files, each reading DB_URL in a
* package-level var declaration (no enclosing function -> module QN, the
* same collision condition as the Java static blocks above). */
static const char k_go_service_a[] =
"package goenv\n"
"\n"
"import \"os\"\n"
"\n"
"var dbURLA = os.Getenv(\"DB_URL\")\n";
static const char k_go_service_b[] =
"package goenv\n"
"\n"
"import \"os\"\n"
"\n"
"var dbURLB = os.Getenv(\"DB_URL\")\n";
static const RFile k_files[] = {
{"owner/ConfigException.java", k_config_exception},
{"owner/ServiceA.java", k_service_a},
{"owner/ServiceB.java", k_service_b},
{"goenv/service_a.go", k_go_service_a},
{"goenv/service_b.go", k_go_service_b},
};
static const int k_nfiles = (int)(sizeof(k_files) / sizeof(k_files[0]));
/* Expected distinct THROWS source files. */
static const char *k_expected_throws[] = {
"owner/ServiceA.java",
"owner/ServiceB.java",
};
static const int k_nexpected_throws = (int)(sizeof(k_expected_throws) / sizeof(k_expected_throws[0]));
/* Expected distinct CONFIGURES source files. */
static const char *k_expected_configures[] = {
"goenv/service_a.go",
"goenv/service_b.go",
};
static const int k_nexpected_configures =
(int)(sizeof(k_expected_configures) / sizeof(k_expected_configures[0]));
/* resolve_file_throws only misattributes on the >MIN_FILES_FOR_PARALLEL=50
* worker-merge path (pipeline.c) -- confirmed empirically: the 5-file
* sequential fixture above already attributes THROWS correctly even on
* unfixed HEAD (resolve_file_rw's shared find_source_node ran the same
* lookup earlier in the same pass and the by-then-stable Folder/Module node,
* protected from clobbering since #844, happens not to collide on the
* sequential path for this construct). This mirrors #787's own "two faces"
* (sequential upsert vs. parallel merge_update_existing) -- resolve_file_throws
* apparently only has the parallel face. Padding past the threshold with
* inert same-package filler classes reproduces it. */
enum { REPRO842_FILLER_COUNT = 60, REPRO842_NAME_SZ = 64, REPRO842_BODY_SZ = 192 };
/* Build the >50-file fixture into files[]/name_bufs[]/body_bufs[] (caller
* arrays sized k_nfiles + REPRO842_FILLER_COUNT). Returns total file count.
* Caller frees name_bufs/body_bufs entries [k_nfiles..total). */
static int build_parallel_fixture(RFile *files, char **name_bufs, char **body_bufs) {
int n = 0;
for (int i = 0; i < k_nfiles; i++) {
files[n++] = k_files[i];
}
for (int i = 0; i < REPRO842_FILLER_COUNT; i++) {
char *name = malloc(REPRO842_NAME_SZ);
char *body = malloc(REPRO842_BODY_SZ);
if (!name || !body) {
free(name);
free(body);
break;
}
snprintf(name, REPRO842_NAME_SZ, "owner/Filler%02d.java", i);
snprintf(body, REPRO842_BODY_SZ,
"package org.example.owner;\n"
"\n"
"public class Filler%02d {\n"
" private int value%02d;\n"
"}\n",
i, i);
name_bufs[n] = name;
body_bufs[n] = body;
files[n].name = name;
files[n].content = body;
n++;
}
return n;
}
/* ── Helper: collect distinct inbound-edge source file_paths for a node ──── */
/*
* collect_edge_sources: index `files`/`nfiles`, find the node named
* `node_name` carrying label `node_label`, walk all `edge_type` edges
* targeting it, and write the distinct source file_paths (up to `cap`) into
* `out`. DISTINCT-file (set) semantics: ownership is per FILE, so duplicate
* paths are collapsed. Returns the number of distinct sources found (may be
* truncated by `cap`), or -1 on setup/lookup failure.
*
* The caller is responsible for free()ing each string in out[0..return-1].
*/
static int collect_edge_sources(const RFile *files, int nfiles, const char *node_name,
const char *node_label, const char *edge_type, char **out,
int cap) {
RProj lp;
cbm_store_t *store = rh_index_files(&lp, files, nfiles);
if (!store) {
return -1;
}
cbm_node_t *candidates = NULL;
int ncand = 0;
int rc = cbm_store_find_nodes_by_name(store, lp.project, node_name, &candidates, &ncand);
if (rc != CBM_STORE_OK || ncand == 0) {
cbm_store_free_nodes(candidates, ncand);
rh_cleanup(&lp, store);
return -1;
}
int64_t target_id = 0;
for (int i = 0; i < ncand; i++) {
const char *lbl = candidates[i].label ? candidates[i].label : "";
if (strcmp(lbl, node_label) == 0) {
target_id = candidates[i].id;
break;
}
}
cbm_store_free_nodes(candidates, ncand);
if (!target_id) {
rh_cleanup(&lp, store);
return -1;
}
cbm_edge_t *edges = NULL;
int nedges = 0;
rc = cbm_store_find_edges_by_target_type(store, target_id, edge_type, &edges, &nedges);
if (rc != CBM_STORE_OK) {
rh_cleanup(&lp, store);
return -1;
}
int found = 0;
for (int i = 0; i < nedges && found < cap; i++) {
cbm_node_t src_node;
if (cbm_store_find_node_by_id(store, edges[i].source_id, &src_node) != CBM_STORE_OK) {
continue;
}
if (!src_node.file_path || !src_node.file_path[0]) {
continue;
}
/* Set semantics: skip a file_path already collected. */
int dup = 0;
for (int j = 0; j < found; j++) {
if (strcmp(out[j], src_node.file_path) == 0) {
dup = 1;
break;
}
}
if (dup) {
continue;
}
out[found++] = strdup(src_node.file_path);
}
cbm_store_free_edges(edges, nedges);
rh_cleanup(&lp, store);
return found;
}
/* Check that paths[] contains exactly the expected file suffixes, once each.
* Returns 1 if all expected suffixes appear exactly once, 0 otherwise.
* Prints a diagnostic on mismatch. */
static int check_sources_exact(char **paths, int count, const char **expected, int nexpected) {
if (count != nexpected) {
printf(" source count %d != expected %d\n", count, nexpected);
for (int i = 0; i < count; i++) {
printf(" got: %s\n", paths[i] ? paths[i] : "(null)");
}
return 0;
}
for (int e = 0; e < nexpected; e++) {
int seen = 0;
for (int i = 0; i < count; i++) {
if (paths[i] && strstr(paths[i], expected[e])) {
seen++;
}
}
if (seen != 1) {
printf(" expected suffix '%s' appears %d time(s) (want 1)\n", expected[e], seen);
for (int i = 0; i < count; i++) {
printf(" got: %s\n", paths[i] ? paths[i] : "(null)");
}
return 0;
}
}
return 1;
}
/* ── Tests ──────────────────────────────────────────────────────────────── */
/*
* repro_issue842_env_configures_exact_sources
*
* Class-level env access (static initializer, no enclosing method) must
* attribute its CONFIGURES edge to its own file's File node, not the shared
* package Folder node.
*
* RED on buggy HEAD: create_env_configures_for_file resolves `src` via a
* direct cbm_gbuf_find_by_qn(enclosing_func_qn) with no dir-container guard,
* so both files' static-block env accesses source from the one shared
* Folder/Module node for org.example.owner — the distinct source set has 1
* entry (whichever file's Module def was upserted/merged last) instead of 2.
*/
TEST(repro_issue842_env_configures_exact_sources) {
#define MAX_SRCS 16
char *paths[MAX_SRCS];
memset(paths, 0, sizeof(paths));
int count = collect_edge_sources(k_files, k_nfiles, "DB_URL", "EnvVar", "CONFIGURES", paths, MAX_SRCS);
if (count < 0) {
FAIL("fixture indexing or DB_URL lookup failed");
}
int ok = check_sources_exact(paths, count, k_expected_configures, k_nexpected_configures);
for (int i = 0; i < MAX_SRCS; i++) {
free(paths[i]);
paths[i] = NULL;
}
#undef MAX_SRCS
if (!ok) {
FAIL("CONFIGURES edge source file_paths do not match expected set");
}
PASS();
}
/*
* repro_issue842_throws_exact_sources
*
* Class-level throw (static initializer, no enclosing method) must attribute
* its THROWS edge to its own file's File node, not the shared package Folder
* node. Padded past MIN_FILES_FOR_PARALLEL=50 (pipeline.c) to force the
* worker-merge path -- confirmed empirically that resolve_file_throws only
* misattributes there, not on the small sequential fixture (see
* build_parallel_fixture's comment).
*
* RED on buggy HEAD: resolve_file_throws resolves `src` via a direct
* cbm_gbuf_find_by_qn(enclosing_func_qn) with no dir-container guard and no
* File-node fallback at all (a miss just skips the edge) — both files'
* static-block throws source from the one shared Folder/Module node, so the
* distinct source set has 1 entry instead of 2.
*/
TEST(repro_issue842_throws_exact_sources) {
#define MAX_SRCS 16
#define MAX_TOTAL_FILES (k_nfiles + REPRO842_FILLER_COUNT)
RFile files[MAX_TOTAL_FILES];
char *name_bufs[MAX_TOTAL_FILES];
char *body_bufs[MAX_TOTAL_FILES];
memset(name_bufs, 0, sizeof(name_bufs));
memset(body_bufs, 0, sizeof(body_bufs));
int total = build_parallel_fixture(files, name_bufs, body_bufs);
char *paths[MAX_SRCS];
memset(paths, 0, sizeof(paths));
int count = collect_edge_sources(files, total, "ConfigException", "Class", "THROWS", paths, MAX_SRCS);
for (int i = 0; i < total; i++) {
free(name_bufs[i]);
free(body_bufs[i]);
}
#undef MAX_TOTAL_FILES
if (count < 0) {
FAIL("fixture indexing or ConfigException lookup failed");
}
int ok = check_sources_exact(paths, count, k_expected_throws, k_nexpected_throws);
for (int i = 0; i < MAX_SRCS; i++) {
free(paths[i]);
paths[i] = NULL;
}
#undef MAX_SRCS
if (!ok) {
FAIL("THROWS edge source file_paths do not match expected set");
}
PASS();
}
/* ── Suite ──────────────────────────────────────────────────────────────── */
SUITE(repro_issue842) {
RUN_TEST(repro_issue842_env_configures_exact_sources);
RUN_TEST(repro_issue842_throws_exact_sources);
}