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

174 lines
6.4 KiB
C

/*
* repro_new_py_tuple_unpack.c -- Reproduce-first case for a NEW, un-filed
* bug discovered during QA sweep (2026-06-26).
*
* BUG: Python module-level tuple-unpacking assignments silently produce no
* Variable definitions. `x, y = some_func()` is in py_var_types
* (as "assignment") but the Python branch of extract_vars_mainstream()
* only emits a def when the `left` child is a plain `identifier`. When
* `left` is a `pattern_list` (the tree-sitter node type for comma-separated
* LHS in an assignment), the guard fails silently and zero Variable defs
* are emitted for x or y.
*
* PATTERN AFFECTED:
* x, y = some_func() # left is pattern_list
* a, b, c = 1, 2, 3 # left is pattern_list
* result, err = parse(data) # common Go-style unpack in Python
*
* ROOT CAUSE -- extract_defs.c, extract_vars_mainstream(), Python case
* (~line 4068):
*
* case CBM_LANG_PYTHON: {
* TSNode left = ts_node_child_by_field_name(node, TS_FIELD("left"));
* if (!ts_node_is_null(left) && strcmp(ts_node_type(left), "identifier") == 0) {
* push_var_def(ctx, cbm_node_text(a, left, ctx->source), node);
* }
* break;
* }
*
* The guard `strcmp(ts_node_type(left), "identifier") == 0` passes only
* for single-variable assignments (`x = 1`). For `x, y = func()` the
* tree-sitter-python grammar produces `left` as a `pattern_list` node
* containing two `identifier` children. The strcmp fails -> no
* push_var_def is called -> both `x` and `y` are silently dropped.
*
* py_var_types (lang_specs.c) includes both "assignment" AND
* "augmented_assignment", so the walk_variables path DOES reach
* extract_vars_mainstream for these nodes -- the gap is purely inside
* the Python case guard.
*
* EXPECTED (correct) behavior:
* `x, y = some_func()` at module level must produce AT LEAST one
* Variable def; ideally one for `x` and one for `y`.
* `result, err = parse(data)` must produce Variable defs for `result`
* and `err`.
*
* ACTUAL (buggy) behavior:
* r->defs contains zero Variable defs for these assignments.
* ASSERT_GT(count, 0) fires -> RED.
*
* HOW TO CONFIRM WITHOUT COMPILING:
* 1. lang_specs.c: py_var_types = {"assignment", "augmented_assignment", NULL}
* -> walk_variables correctly calls extract_var_names for "assignment" nodes.
* 2. extract_defs.c extract_vars_mainstream() Python case (~4068):
* left node for `x, y = ...` is of type "pattern_list" (confirmed by
* tree-sitter-python grammar symbol sym_pattern_list = 200).
* 3. The strcmp("pattern_list", "identifier") == 0 check FAILS -> no def.
*
* FIX LOCATION (not implemented here):
* extract_defs.c extract_vars_mainstream() Python case: when left is
* "pattern_list", iterate its named children and call push_var_def for
* each child that is an "identifier".
*/
#include "test_framework.h"
#include "cbm.h"
#include <string.h>
static CBMFileResult *rx_py(const char *src) {
return cbm_extract_file(src, (int)strlen(src), CBM_LANG_PYTHON, "proj", "mod.py",
0, NULL, NULL);
}
static int count_var_defs(CBMFileResult *r) {
int n = 0;
for (int i = 0; i < r->defs.count; i++) {
if (r->defs.items[i].label && strcmp(r->defs.items[i].label, "Variable") == 0)
n++;
}
return n;
}
static int has_var_def(CBMFileResult *r, const char *name) {
for (int i = 0; i < r->defs.count; i++) {
CBMDefinition *d = &r->defs.items[i];
if (d->label && strcmp(d->label, "Variable") == 0 &&
d->name && strcmp(d->name, name) == 0)
return 1;
}
return 0;
}
/*
* repro_new_py_tuple_unpack_two_vars
*
* `x, y = some_func()` must produce at least one Variable def.
*
* Precondition: single-var assignment `z = 1` must work (tests the
* happy path so we know Variable extraction is wired up at all).
*
* WHY RED on current code:
* extract_vars_mainstream() Python case checks
* strcmp(ts_node_type(left), "identifier") == 0.
* For `x, y = some_func()` the left node is "pattern_list" -> check
* fails -> push_var_def is never called -> count_var_defs returns 0
* for the tuple assignment -> ASSERT_GT(count, 0) fires -> RED.
*/
TEST(repro_new_py_tuple_unpack_two_vars) {
static const char *src =
"def some_func():\n"
" return 1, 2\n"
"\n"
"z = 1\n"
"x, y = some_func()\n";
CBMFileResult *r = rx_py(src);
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
/* Precondition: single-var `z = 1` must yield a Variable def for z.
* If RED here, the Variable extraction path itself is broken, not the
* tuple-unpack case specifically. */
ASSERT_TRUE(has_var_def(r, "z")); /* should already pass */
/* PRIMARY ASSERTION: at least one Variable def must come from `x, y = ...`.
* Because we already confirmed `z` works, any Variable count > 1 means
* the tuple-unpack path is working.
* WHY RED: the pattern_list branch is missing; push_var_def is never called
* for x or y -> total count stays at 1 (only z) -> ASSERT_GT(count, 1)
* fails -> RED. */
int total = count_var_defs(r);
ASSERT_GT(total, 1); /* RED on buggy code: count == 1 (only z) */
cbm_free_result(r);
PASS();
}
/*
* repro_new_py_tuple_unpack_named_vars
*
* Stronger assertion: x and y must each appear as named Variable defs.
*
* WHY RED on current code:
* has_var_def(r, "x") and has_var_def(r, "y") both return 0 since
* push_var_def is never called for pattern_list assignments.
*/
TEST(repro_new_py_tuple_unpack_named_vars) {
static const char *src =
"def parse(data):\n"
" return data, None\n"
"\n"
"result, err = parse('hello')\n";
CBMFileResult *r = rx_py(src);
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
/* PRIMARY ASSERTION: both unpacked names must appear as Variable defs.
* WHY RED: pattern_list is not handled; neither "result" nor "err" is
* emitted -> has_var_def returns 0 for both -> at least one ASSERT_TRUE
* fires -> RED. */
ASSERT_TRUE(has_var_def(r, "result")); /* RED on buggy code */
ASSERT_TRUE(has_var_def(r, "err")); /* RED on buggy code */
cbm_free_result(r);
PASS();
}
/* ---- Suite --------------------------------------------------------------- */
SUITE(repro_new_py_tuple_unpack) {
RUN_TEST(repro_new_py_tuple_unpack_two_vars);
RUN_TEST(repro_new_py_tuple_unpack_named_vars);
}