mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
test(repro): grammar batteries (scripting, functional) + Go/Python LSP passes
- repro_grammar_scripting: Python/Ruby/PHP/JS/TS/TSX/Lua/Perl/R/Julia/Groovy/Dart full invariant battery (Perl/R/Julia/Groovy/Dart callable-sourcing RED). - repro_grammar_functional: Haskell/OCaml/F#/Elixir/Erlang/Elm/Clojure/Scheme/ Racket/CommonLisp/EmacsLisp/Lean/Gleam (Elm calls-extraction RED; all dim-7 RED via enclosing-func drift). - repro_lsp_go_py: 7 Go strategies (direct/type/embed/interface_resolve+dispatch/ cross_file/unresolved) + 14 Python strategies (method/super/super_init/ module_attr/dict_dispatch/operator_dunder/builtin*/generic_method/method_union), each asserting callable-sourcing + strategy-presence. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
cee59ee2fd
commit
0b232ba14a
5 files changed
+1608
-1
No files matched your search
+4
-1
@@ -431,7 +431,10 @@ TEST_REPRO_SRCS = \
|
||||
tests/repro/repro_invariant_lsp_rescue.c \
|
||||
tests/repro/repro_invariant_discovery_fqn.c \
|
||||
tests/repro/repro_grammar_core.c \
|
||||
tests/repro/repro_lsp_c_cpp.c
|
||||
tests/repro/repro_grammar_scripting.c \
|
||||
tests/repro/repro_grammar_functional.c \
|
||||
tests/repro/repro_lsp_c_cpp.c \
|
||||
tests/repro/repro_lsp_go_py.c
|
||||
|
||||
ALL_TEST_SRCS =$(TEST_FOUNDATION_SRCS) $(TEST_EXTRACTION_SRCS) $(TEST_STORE_SRCS) $(TEST_CYPHER_SRCS) $(TEST_MCP_SRCS) $(TEST_DISCOVER_SRCS) $(TEST_GRAPH_BUFFER_SRCS) $(TEST_PIPELINE_SRCS) $(TEST_WATCHER_SRCS) $(TEST_LZ4_SRCS) $(TEST_ZSTD_SRCS) $(TEST_ARTIFACT_SRCS) $(TEST_SQLITE_WRITER_SRCS) $(TEST_GO_LSP_SRCS) $(TEST_C_LSP_SRCS) $(TEST_PHP_LSP_SRCS) $(TEST_CS_LSP_SRCS) $(TEST_CS_LSP_BENCH_SRCS) $(TEST_SCOPE_SRCS) $(TEST_TYPE_REP_SRCS) $(TEST_PY_LSP_SRCS) $(TEST_PY_LSP_BENCH_SRCS) $(TEST_PY_LSP_STRESS_SRCS) $(TEST_PY_LSP_SCALE_SRCS) $(TEST_TS_LSP_SRCS) $(TEST_JAVA_LSP_SRCS) $(TEST_KOTLIN_LSP_SRCS) $(TEST_RUST_LSP_SRCS) $(TEST_TRACES_SRCS) $(TEST_CLI_SRCS) $(TEST_MEM_SRCS) $(TEST_UI_SRCS) $(TEST_HTTPD_SRCS) $(TEST_SECURITY_SRCS) $(TEST_YAML_SRCS) $(TEST_SIMHASH_SRCS) $(TEST_STACK_OVERFLOW_SRCS) $(TEST_INTEGRATION_SRCS)
|
||||
|
||||
|
||||
@@ -0,0 +1,497 @@
|
||||
/*
|
||||
* repro_grammar_functional.c -- Per-grammar INVARIANT battery for the
|
||||
* FUNCTIONAL language family.
|
||||
*
|
||||
* One TEST() per language so per-language RED/GREEN shows on the bug-repro
|
||||
* board. Each test runs the same battery against a tiny idiomatic fixture for
|
||||
* that language (a named function/definition whose body calls another named
|
||||
* function). The shared single_file_battery() + pipeline_battery() helpers
|
||||
* below are a direct mirror of those in repro_grammar_core.c.
|
||||
*
|
||||
* Languages covered (13) and the CBM_LANG_* enum each uses:
|
||||
* Haskell -> CBM_LANG_HASKELL
|
||||
* OCaml -> CBM_LANG_OCAML
|
||||
* F# -> CBM_LANG_FSHARP
|
||||
* Elixir -> CBM_LANG_ELIXIR
|
||||
* Erlang -> CBM_LANG_ERLANG
|
||||
* Elm -> CBM_LANG_ELM
|
||||
* Clojure -> CBM_LANG_CLOJURE
|
||||
* Scheme -> CBM_LANG_SCHEME
|
||||
* Racket -> CBM_LANG_RACKET
|
||||
* Common Lisp -> CBM_LANG_COMMONLISP
|
||||
* Emacs Lisp -> CBM_LANG_EMACSLISP (note: not ELISP)
|
||||
* Lean 4 -> CBM_LANG_LEAN
|
||||
* Gleam -> CBM_LANG_GLEAM
|
||||
*
|
||||
* BATTERY DIMENSIONS (mirror of repro_grammar_core.c)
|
||||
* -----------------------------------------------------
|
||||
* SINGLE-FILE (cbm_extract_file, via inv_rx + inv_count_* helpers):
|
||||
* 1. extract-clean : inv_extract_clean(src,lang,file) == 1
|
||||
* 2. labels-valid : inv_count_bad_labels(r) == 0
|
||||
* 3. fqn-wellformed : inv_count_bad_fqns(r) == 0
|
||||
* 4. ranges-valid : inv_count_bad_ranges(r) == 0
|
||||
* 5. defs-present : inv_count_label(r, expect_label) > 0
|
||||
* 6. calls-extracted : inv_has_call(r, callee) == 1
|
||||
*
|
||||
* FULL-PIPELINE (rh_index_files -> cbm_store_t*, via inv_count_* store helpers):
|
||||
* 7. callable-sourcing : module_sourced == 0 AND callable_sourced >= 1
|
||||
* 8. no-dangling : inv_count_dangling_edges(store, project, "CALLS") == 0
|
||||
*
|
||||
* KNOWN GAPS (the point of this file)
|
||||
* -------------------------------------
|
||||
* Dimension 6 (calls-extracted) is RED for Elm: the scripting-callee path does
|
||||
* not yield a call name for Elm's function_call nodes on current code.
|
||||
*
|
||||
* Dimension 7 (callable-sourcing) is RED for all functional languages on current
|
||||
* code. cbm_enclosing_func_qn falls back to the module QN when
|
||||
* cbm_find_enclosing_func cannot match tree-sitter node types to
|
||||
* func_kinds_for_lang for the language (the same gap documented in
|
||||
* QUALITY_ANALYSIS.md section 6 / enclosing-func drift). Only ~3.69% of CALLS
|
||||
* edges are callable-sourced in the real graph; functional languages are not in
|
||||
* the known-GREEN set (Go/CUDA/D).
|
||||
*
|
||||
* RED rows ARE the deliverable: they document extraction gaps and serve as
|
||||
* permanent regression guards until the gaps are fixed.
|
||||
*
|
||||
* Coding rule: inline comments are line comments only (no block comments inside
|
||||
* block comments).
|
||||
*/
|
||||
|
||||
#include "test_framework.h"
|
||||
#include "repro_invariant_lib.h"
|
||||
#include <store/store.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
/* -- Shared single-file battery (dimensions 1-6) --------------------------
|
||||
*
|
||||
* Runs the six single-file invariants against one fixture. Returns 0 when all
|
||||
* pass, 1 otherwise (printing a per-dimension FAIL line). lang_tag is for
|
||||
* diagnostics only. expect_label is the def label the fixture is guaranteed to
|
||||
* produce (e.g. "Function"); callee is the in-body callee name that must
|
||||
* appear in the extracted calls.
|
||||
*/
|
||||
static int single_file_battery(const char *lang_tag, const char *src,
|
||||
CBMLanguage lang, const char *file,
|
||||
const char *expect_label,
|
||||
const char *callee) {
|
||||
const char *RED = tf_red();
|
||||
const char *RST = tf_reset();
|
||||
int fails = 0;
|
||||
|
||||
/* 1. extract-clean -- must hold before anything else is meaningful. */
|
||||
if (inv_extract_clean(src, lang, file) != 1) {
|
||||
printf(" %sFAIL%s [%s] extract-clean: NULL result or has_error set\n",
|
||||
RED, RST, lang_tag);
|
||||
return 1; /* nothing else can be trusted */
|
||||
}
|
||||
|
||||
CBMFileResult *r = inv_rx(src, lang, file);
|
||||
if (!r) {
|
||||
printf(" %sFAIL%s [%s] inv_rx returned NULL after clean extract\n",
|
||||
RED, RST, lang_tag);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 2. labels-valid */
|
||||
int bad_labels = inv_count_bad_labels(r);
|
||||
if (bad_labels != 0) {
|
||||
printf(" %sFAIL%s [%s] labels-valid: %d def(s) with invalid label\n",
|
||||
RED, RST, lang_tag, bad_labels);
|
||||
fails++;
|
||||
}
|
||||
|
||||
/* 3. fqn-wellformed */
|
||||
int bad_fqns = inv_count_bad_fqns(r);
|
||||
if (bad_fqns != 0) {
|
||||
printf(" %sFAIL%s [%s] fqn-wellformed: %d def(s) with malformed QN\n",
|
||||
RED, RST, lang_tag, bad_fqns);
|
||||
fails++;
|
||||
}
|
||||
|
||||
/* 4. ranges-valid */
|
||||
int bad_ranges = inv_count_bad_ranges(r);
|
||||
if (bad_ranges != 0) {
|
||||
printf(" %sFAIL%s [%s] ranges-valid: %d def(s) with invalid range\n",
|
||||
RED, RST, lang_tag, bad_ranges);
|
||||
fails++;
|
||||
}
|
||||
|
||||
/* 5. defs-present -- the function/definition the fixture wrote must be extracted. */
|
||||
if (expect_label && inv_count_label(r, expect_label) < 1) {
|
||||
printf(" %sFAIL%s [%s] defs-present: no def labelled \"%s\"\n",
|
||||
RED, RST, lang_tag, expect_label);
|
||||
fails++;
|
||||
}
|
||||
|
||||
/* 6. calls-extracted -- the in-body call must be captured. */
|
||||
if (inv_has_call(r, callee) != 1) {
|
||||
printf(" %sFAIL%s [%s] calls-extracted: no call to \"%s\" found"
|
||||
" -- known extraction gap\n",
|
||||
RED, RST, lang_tag, callee);
|
||||
fails++;
|
||||
}
|
||||
|
||||
cbm_free_result(r);
|
||||
return fails ? 1 : 0;
|
||||
}
|
||||
|
||||
/* -- Shared full-pipeline battery (dimensions 7-8) ------------------------
|
||||
*
|
||||
* Indexes the single-file fixture through the production pipeline and asserts
|
||||
* callable-sourcing (no Module-sourced in-body CALLS) and no dangling CALLS
|
||||
* edges. Returns 0 on PASS, 1 on FAIL. Dimension 7 is RED for all functional
|
||||
* languages on current code -- that is the intended signal.
|
||||
*/
|
||||
static int pipeline_battery(const char *lang_tag, const char *filename,
|
||||
const char *src) {
|
||||
const char *RED = tf_red();
|
||||
const char *RST = tf_reset();
|
||||
|
||||
RFile files[1];
|
||||
files[0].name = filename;
|
||||
files[0].content = src;
|
||||
|
||||
RProj lp;
|
||||
cbm_store_t *store = rh_index_files(&lp, files, 1);
|
||||
if (!store) {
|
||||
printf(" %sFAIL%s [%s] pipeline: rh_index_files returned NULL\n",
|
||||
RED, RST, lang_tag);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int fails = 0;
|
||||
|
||||
/* 7. callable-sourcing -- mod must be 0; we also require >=1 callable-sourced
|
||||
* edge so a fixture that produced zero CALLS edges cannot vacuously pass. */
|
||||
int module_sourced = 0;
|
||||
int callable_sourced = 0;
|
||||
inv_count_calls_by_source(store, lp.project, &module_sourced,
|
||||
&callable_sourced);
|
||||
if (module_sourced != 0) {
|
||||
printf(" %sFAIL%s [%s] callable-sourcing: %d in-body CALLS sourced at "
|
||||
"Module (callable=%d) -- known enclosing-func gap\n",
|
||||
RED, RST, lang_tag, module_sourced, callable_sourced);
|
||||
fails++;
|
||||
} else if (callable_sourced < 1) {
|
||||
printf(" %sFAIL%s [%s] callable-sourcing: 0 CALLS edges (fixture "
|
||||
"produced no in-body call edge to attribute)\n",
|
||||
RED, RST, lang_tag);
|
||||
fails++;
|
||||
}
|
||||
|
||||
/* 8. no-dangling -- every CALLS edge endpoint must resolve. */
|
||||
int dangling = inv_count_dangling_edges(store, lp.project, "CALLS");
|
||||
if (dangling != 0) {
|
||||
printf(" %sFAIL%s [%s] no-dangling: %d dangling CALLS endpoint(s)\n",
|
||||
RED, RST, lang_tag, dangling);
|
||||
fails++;
|
||||
}
|
||||
|
||||
rh_cleanup(&lp, store);
|
||||
return fails ? 1 : 0;
|
||||
}
|
||||
|
||||
/* -- Haskell ---------------------------------------------------------------
|
||||
* Idiomatic: module header, a helper function, a caller function whose body
|
||||
* applies the helper. Haskell function application is juxtaposition: `add x y`
|
||||
* inside the body of `compute` is the call. The tree-sitter-haskell grammar
|
||||
* emits `function` and `apply` nodes; extract_fp_callee handles `apply`.
|
||||
* Expected: dims 1-6 + 8 GREEN, dim 7 RED (no cross-LSP rescue for Haskell;
|
||||
* func_kinds_for_lang drift causes enclosing-func walk to fall back to Module).
|
||||
*/
|
||||
TEST(repro_grammar_functional_haskell) {
|
||||
static const char src[] =
|
||||
"module Calc where\n"
|
||||
"\n"
|
||||
"add :: Int -> Int -> Int\n"
|
||||
"add a b = a + b\n"
|
||||
"\n"
|
||||
"compute :: Int -> Int\n"
|
||||
"compute x = add x 1\n";
|
||||
if (single_file_battery("Haskell", src, CBM_LANG_HASKELL, "Calc.hs",
|
||||
"Function", "add") != 0)
|
||||
return 1;
|
||||
return pipeline_battery("Haskell", "Calc.hs", src);
|
||||
}
|
||||
|
||||
/* -- OCaml -----------------------------------------------------------------
|
||||
* Idiomatic: two `let` bindings at module top level; the second binding's body
|
||||
* calls the first. OCaml `let f x = expr` is a `value_definition` node;
|
||||
* extract_fp_callee handles `application_expression`. Labels: "Function".
|
||||
* Expected: dims 1-6 + 8 GREEN, dim 7 RED (same enclosing-func gap).
|
||||
*/
|
||||
TEST(repro_grammar_functional_ocaml) {
|
||||
static const char src[] =
|
||||
"let add a b = a + b\n"
|
||||
"\n"
|
||||
"let compute x = add x 1\n";
|
||||
if (single_file_battery("OCaml", src, CBM_LANG_OCAML, "calc.ml",
|
||||
"Function", "add") != 0)
|
||||
return 1;
|
||||
return pipeline_battery("OCaml", "calc.ml", src);
|
||||
}
|
||||
|
||||
/* -- F# --------------------------------------------------------------------
|
||||
* Idiomatic: two `let` bindings; the second calls the first inside its body.
|
||||
* F# `let f x = ...` is a `function_or_value_defn` node (or `value_declaration`
|
||||
* depending on grammar version); extract_fsharp_callee handles
|
||||
* `application_expression`. Labels: "Function".
|
||||
* Expected: dims 1-6 + 8 GREEN, dim 7 RED (enclosing-func gap applies;
|
||||
* no dedicated F# cross-LSP rescue).
|
||||
*/
|
||||
TEST(repro_grammar_functional_fsharp) {
|
||||
static const char src[] =
|
||||
"let add a b = a + b\n"
|
||||
"\n"
|
||||
"let compute x = add x 1\n";
|
||||
if (single_file_battery("F#", src, CBM_LANG_FSHARP, "Calc.fs",
|
||||
"Function", "add") != 0)
|
||||
return 1;
|
||||
return pipeline_battery("F#", "Calc.fs", src);
|
||||
}
|
||||
|
||||
/* -- Elixir ----------------------------------------------------------------
|
||||
* Idiomatic: a module with two `def` clauses; the caller's body invokes the
|
||||
* helper. Elixir `def` is extracted as a "call" node by tree-sitter-elixir;
|
||||
* extract_calls.c has a special Elixir branch for "call" nodes that extracts
|
||||
* the callee. Labels: "Function" (elixir_func_types includes "call").
|
||||
* Expected: dims 1-6 + 8 GREEN, dim 7 RED (enclosing-func gap).
|
||||
*/
|
||||
TEST(repro_grammar_functional_elixir) {
|
||||
static const char src[] =
|
||||
"defmodule Calc do\n"
|
||||
" def add(a, b), do: a + b\n"
|
||||
"\n"
|
||||
" def compute(x) do\n"
|
||||
" add(x, 1)\n"
|
||||
" end\n"
|
||||
"end\n";
|
||||
if (single_file_battery("Elixir", src, CBM_LANG_ELIXIR, "calc.ex",
|
||||
"Function", "add") != 0)
|
||||
return 1;
|
||||
return pipeline_battery("Elixir", "calc.ex", src);
|
||||
}
|
||||
|
||||
/* -- Erlang ----------------------------------------------------------------
|
||||
* Idiomatic: a module attribute, an exported function, and a helper function.
|
||||
* The exported function's body calls the helper. Erlang function clauses are
|
||||
* `function_clause` nodes; extract_erlang_callee handles `call` nodes.
|
||||
* Labels: "Function" (erlang_func_types = {"function_clause"}).
|
||||
* Expected: dims 1-6 + 8 GREEN, dim 7 RED (enclosing-func gap applies;
|
||||
* Erlang is not in the known-GREEN callable-sourcing set).
|
||||
*/
|
||||
TEST(repro_grammar_functional_erlang) {
|
||||
static const char src[] =
|
||||
"-module(calc).\n"
|
||||
"-export([compute/1]).\n"
|
||||
"\n"
|
||||
"add(A, B) -> A + B.\n"
|
||||
"\n"
|
||||
"compute(X) ->\n"
|
||||
" add(X, 1).\n";
|
||||
if (single_file_battery("Erlang", src, CBM_LANG_ERLANG, "calc.erl",
|
||||
"Function", "add") != 0)
|
||||
return 1;
|
||||
return pipeline_battery("Erlang", "calc.erl", src);
|
||||
}
|
||||
|
||||
/* -- Elm ------------------------------------------------------------------
|
||||
* Idiomatic: a module declaration, a helper function, and a caller function
|
||||
* whose body applies the helper. Elm `f x = body` is a `value_declaration`
|
||||
* node; elm_call_types = {"function_call", "function_call_expr"}. The call
|
||||
* extractor reaches extract_scripting_callee for Elm but currently does NOT
|
||||
* yield a callee name for Elm's function_call node -- dim 6 is RED.
|
||||
* Labels: "Function" (elm_func_types = {"value_declaration", ...}).
|
||||
* Expected: dims 1-5 + 8 GREEN, dim 6 RED (calls extraction gap -- this RED
|
||||
* assertion documents the gap), dim 7 RED (enclosing-func gap).
|
||||
*/
|
||||
TEST(repro_grammar_functional_elm) {
|
||||
static const char src[] =
|
||||
"module Calc exposing (compute)\n"
|
||||
"\n"
|
||||
"add : Int -> Int -> Int\n"
|
||||
"add a b =\n"
|
||||
" a + b\n"
|
||||
"\n"
|
||||
"compute : Int -> Int\n"
|
||||
"compute x =\n"
|
||||
" add x 1\n";
|
||||
if (single_file_battery("Elm", src, CBM_LANG_ELM, "Calc.elm",
|
||||
"Function", "add") != 0)
|
||||
return 1;
|
||||
return pipeline_battery("Elm", "Calc.elm", src);
|
||||
}
|
||||
|
||||
/* -- Clojure ---------------------------------------------------------------
|
||||
* Idiomatic: two `defn` forms; the second's body calls the first. In Clojure
|
||||
* both forms are `list_lit` nodes; `extract_lisp_def` labels them "Function".
|
||||
* `extract_lisp_callee` extracts the callee from the head of a `list_lit`.
|
||||
* Expected: dims 1-6 + 8 GREEN, dim 7 RED (enclosing-func gap; Clojure is not
|
||||
* in the known-GREEN callable-sourcing set).
|
||||
*/
|
||||
TEST(repro_grammar_functional_clojure) {
|
||||
static const char src[] =
|
||||
"(defn add [a b]\n"
|
||||
" (+ a b))\n"
|
||||
"\n"
|
||||
"(defn compute [x]\n"
|
||||
" (add x 1))\n";
|
||||
if (single_file_battery("Clojure", src, CBM_LANG_CLOJURE, "calc.clj",
|
||||
"Function", "add") != 0)
|
||||
return 1;
|
||||
return pipeline_battery("Clojure", "calc.clj", src);
|
||||
}
|
||||
|
||||
/* -- Scheme ----------------------------------------------------------------
|
||||
* Idiomatic: two `define` forms; the second's body calls the first. In
|
||||
* tree-sitter-scheme both forms are `list` nodes; `extract_lisp_def` (triggered
|
||||
* by SCHEME in walk_defs) labels them "Function".
|
||||
* NOTE: CBM_LANG_SCHEME has func_types = empty_types, so extract_func_def is
|
||||
* never triggered; definitions only appear via extract_lisp_def. The callee
|
||||
* is extracted by extract_lisp_callee (SCHEME is in the lisp group).
|
||||
* Expected: dims 1-6 + 8 GREEN, dim 7 RED (enclosing-func gap -- SCHEME not
|
||||
* in func_kinds_for_lang known-GREEN set).
|
||||
*/
|
||||
TEST(repro_grammar_functional_scheme) {
|
||||
static const char src[] =
|
||||
"(define (add a b)\n"
|
||||
" (+ a b))\n"
|
||||
"\n"
|
||||
"(define (compute x)\n"
|
||||
" (add x 1))\n";
|
||||
if (single_file_battery("Scheme", src, CBM_LANG_SCHEME, "calc.scm",
|
||||
"Function", "add") != 0)
|
||||
return 1;
|
||||
return pipeline_battery("Scheme", "calc.scm", src);
|
||||
}
|
||||
|
||||
/* -- Racket ----------------------------------------------------------------
|
||||
* Idiomatic: a `#lang racket` reader directive, two `define` forms; the
|
||||
* second's body calls the first. tree-sitter-racket emits `list` nodes;
|
||||
* `extract_lisp_def` (triggered by RACKET in walk_defs) labels them "Function".
|
||||
* NOTE: CBM_LANG_RACKET has func_types = empty_types, so definitions only
|
||||
* appear via extract_lisp_def. extract_lisp_callee handles RACKET.
|
||||
* Expected: dims 1-6 + 8 GREEN, dim 7 RED (enclosing-func gap -- RACKET not
|
||||
* in the known-GREEN callable-sourcing set).
|
||||
*/
|
||||
TEST(repro_grammar_functional_racket) {
|
||||
static const char src[] =
|
||||
"#lang racket\n"
|
||||
"\n"
|
||||
"(define (add a b)\n"
|
||||
" (+ a b))\n"
|
||||
"\n"
|
||||
"(define (compute x)\n"
|
||||
" (add x 1))\n";
|
||||
if (single_file_battery("Racket", src, CBM_LANG_RACKET, "calc.rkt",
|
||||
"Function", "add") != 0)
|
||||
return 1;
|
||||
return pipeline_battery("Racket", "calc.rkt", src);
|
||||
}
|
||||
|
||||
/* -- Common Lisp -----------------------------------------------------------
|
||||
* Idiomatic: two `defun` forms; the second's body calls the first. In
|
||||
* tree-sitter-commonlisp `defun` is the node kind; `commonlisp_func_types =
|
||||
* {"defun"}` triggers extract_func_def which labels it "Function".
|
||||
* extract_lisp_callee handles COMMONLISP.
|
||||
* Expected: dims 1-6 + 8 GREEN, dim 7 RED (enclosing-func gap -- COMMONLISP
|
||||
* not in the known-GREEN callable-sourcing set).
|
||||
*/
|
||||
TEST(repro_grammar_functional_commonlisp) {
|
||||
static const char src[] =
|
||||
"(defun add (a b)\n"
|
||||
" (+ a b))\n"
|
||||
"\n"
|
||||
"(defun compute (x)\n"
|
||||
" (add x 1))\n";
|
||||
if (single_file_battery("Common Lisp", src, CBM_LANG_COMMONLISP, "calc.lisp",
|
||||
"Function", "add") != 0)
|
||||
return 1;
|
||||
return pipeline_battery("Common Lisp", "calc.lisp", src);
|
||||
}
|
||||
|
||||
/* -- Emacs Lisp ------------------------------------------------------------
|
||||
* Idiomatic: two `defun` forms; the second's body calls the first. In
|
||||
* tree-sitter-elisp `defun` is a `list` node with head "defun";
|
||||
* `elisp_func_types = {"function_definition", "macro_definition"}` triggers
|
||||
* extract_func_def. extract_lisp_callee handles EMACSLISP (in the lisp group).
|
||||
* Note: the enum is CBM_LANG_EMACSLISP (not ELISP).
|
||||
* Expected: dims 1-6 + 8 GREEN, dim 7 RED (enclosing-func gap -- EMACSLISP
|
||||
* not in the known-GREEN callable-sourcing set).
|
||||
*/
|
||||
TEST(repro_grammar_functional_emacslisp) {
|
||||
static const char src[] =
|
||||
"(defun add (a b)\n"
|
||||
" (+ a b))\n"
|
||||
"\n"
|
||||
"(defun compute (x)\n"
|
||||
" (add x 1))\n";
|
||||
if (single_file_battery("Emacs Lisp", src, CBM_LANG_EMACSLISP, "calc.el",
|
||||
"Function", "add") != 0)
|
||||
return 1;
|
||||
return pipeline_battery("Emacs Lisp", "calc.el", src);
|
||||
}
|
||||
|
||||
/* -- Lean 4 ----------------------------------------------------------------
|
||||
* Idiomatic: two `def` declarations; the second's body calls the first.
|
||||
* `lean_func_types = {"def", "theorem", "instance", "abbrev"}` triggers
|
||||
* extract_func_def which labels the definitions "Function". extract_calls.c
|
||||
* has a Lean-specific guard (lean_is_in_type_position) for `apply` nodes.
|
||||
* Expected: dims 1-6 + 8 GREEN, dim 7 RED (enclosing-func gap -- Lean is not
|
||||
* in the known-GREEN callable-sourcing set).
|
||||
*/
|
||||
TEST(repro_grammar_functional_lean) {
|
||||
static const char src[] =
|
||||
"def add (a b : Nat) : Nat := a + b\n"
|
||||
"\n"
|
||||
"def compute (x : Nat) : Nat :=\n"
|
||||
" add x 1\n";
|
||||
if (single_file_battery("Lean", src, CBM_LANG_LEAN, "Calc.lean",
|
||||
"Function", "add") != 0)
|
||||
return 1;
|
||||
return pipeline_battery("Lean", "Calc.lean", src);
|
||||
}
|
||||
|
||||
/* -- Gleam ----------------------------------------------------------------
|
||||
* Idiomatic: two `fn` declarations; the second's body calls the first.
|
||||
* `gleam_func_types = {"function", "anonymous_function", "external_function",
|
||||
* ...}` triggers extract_func_def which labels them "Function".
|
||||
* Call extraction reaches extract_scripting_callee (no gleam-specific branch in
|
||||
* extract_callee_lang_specific); gleam_call_types = {"function_call"}.
|
||||
* Expected: dims 1-6 + 8 GREEN, dim 7 RED (enclosing-func gap -- Gleam not
|
||||
* in the known-GREEN callable-sourcing set).
|
||||
*/
|
||||
TEST(repro_grammar_functional_gleam) {
|
||||
static const char src[] =
|
||||
"fn add(a: Int, b: Int) -> Int {\n"
|
||||
" a + b\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"fn compute(x: Int) -> Int {\n"
|
||||
" add(x, 1)\n"
|
||||
"}\n";
|
||||
if (single_file_battery("Gleam", src, CBM_LANG_GLEAM, "calc.gleam",
|
||||
"Function", "add") != 0)
|
||||
return 1;
|
||||
return pipeline_battery("Gleam", "calc.gleam", src);
|
||||
}
|
||||
|
||||
/* -- Suite ---------------------------------------------------------------- */
|
||||
|
||||
SUITE(repro_grammar_functional) {
|
||||
RUN_TEST(repro_grammar_functional_haskell);
|
||||
RUN_TEST(repro_grammar_functional_ocaml);
|
||||
RUN_TEST(repro_grammar_functional_fsharp);
|
||||
RUN_TEST(repro_grammar_functional_elixir);
|
||||
RUN_TEST(repro_grammar_functional_erlang);
|
||||
RUN_TEST(repro_grammar_functional_elm);
|
||||
RUN_TEST(repro_grammar_functional_clojure);
|
||||
RUN_TEST(repro_grammar_functional_scheme);
|
||||
RUN_TEST(repro_grammar_functional_racket);
|
||||
RUN_TEST(repro_grammar_functional_commonlisp);
|
||||
RUN_TEST(repro_grammar_functional_emacslisp);
|
||||
RUN_TEST(repro_grammar_functional_lean);
|
||||
RUN_TEST(repro_grammar_functional_gleam);
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
/*
|
||||
* repro_grammar_scripting.c -- Exhaustive per-grammar INVARIANT battery for the
|
||||
* SCRIPTING / DYNAMIC language family.
|
||||
*
|
||||
* Mirror of repro_grammar_core.c (same helpers, same per-language battery, same
|
||||
* DRY single-file + pipeline runners). One TEST() per language so per-language
|
||||
* RED/GREEN shows on the bug-repro board. Each test runs the SAME battery
|
||||
* against a tiny idiomatic fixture for that language (a function/method that
|
||||
* CALLS another function strictly inside its body, a class where the language
|
||||
* has one idiomatically, and an idiomatic import where the language has one).
|
||||
*
|
||||
* Languages covered (12) and the CBM_LANG_* enum each uses:
|
||||
* Python -> CBM_LANG_PYTHON
|
||||
* Ruby -> CBM_LANG_RUBY
|
||||
* PHP -> CBM_LANG_PHP
|
||||
* JavaScript -> CBM_LANG_JAVASCRIPT
|
||||
* TypeScript -> CBM_LANG_TYPESCRIPT
|
||||
* TSX -> CBM_LANG_TSX
|
||||
* Lua -> CBM_LANG_LUA
|
||||
* Perl -> CBM_LANG_PERL
|
||||
* R -> CBM_LANG_R
|
||||
* Julia -> CBM_LANG_JULIA
|
||||
* Groovy -> CBM_LANG_GROOVY
|
||||
* Dart -> CBM_LANG_DART
|
||||
*
|
||||
* BATTERY DIMENSIONS
|
||||
* ------------------
|
||||
* SINGLE-FILE (cbm_extract_file, via inv_rx + inv_count_* helpers):
|
||||
* 1. extract-clean : inv_extract_clean(src,lang,file) == 1
|
||||
* (parser returned a result and did not set has_error;
|
||||
* a hard crash would not return at all).
|
||||
* 2. labels-valid : inv_count_bad_labels(r) == 0 (every def label is in
|
||||
* the known label set).
|
||||
* 3. fqn-wellformed : inv_count_bad_fqns(r) == 0 (no empty/".."/leading
|
||||
* or trailing '.'/whitespace QNs).
|
||||
* 4. ranges-valid : inv_count_bad_ranges(r) == 0 (start_line >= 1 and
|
||||
* start_line <= end_line for every def).
|
||||
* 5. defs-present : the function/class written in the fixture is extracted
|
||||
* (inv_count_label for the expected def labels > 0).
|
||||
* 6. calls-extracted : inv_has_call(r, "<callee>") == 1 (the in-body call was
|
||||
* captured).
|
||||
*
|
||||
* FULL-PIPELINE (rh_index_files -> cbm_store_t*, via inv_count_* store helpers):
|
||||
* 7. callable-sourcing : inv_count_calls_by_source(store,project,&mod,&call);
|
||||
* assert mod == 0 -- every in-body call must be sourced
|
||||
* at a Function/Method node, NEVER at a Module node.
|
||||
* 8. no-dangling : inv_count_dangling_edges(store,project,"CALLS") == 0
|
||||
* (every CALLS edge resolves both endpoints).
|
||||
*
|
||||
* EXPECTED RED/GREEN (dimension 7, callable-sourcing), per QUALITY_ANALYSIS.md
|
||||
* (2026-06-24), repro_invariant_calls.c, repro_invariant_breadth.c, and
|
||||
* repro_invariant_enclosing_parity.c:
|
||||
* GREEN (callable-sourced; regression guards):
|
||||
* Python -- func_kinds_python = {function_definition}; grep-validated
|
||||
* correct in QUALITY_ANALYSIS.
|
||||
* JavaScript -- func_kinds_js = {function_declaration, method_definition,
|
||||
* arrow_function, ...}; the simplest free-function case is
|
||||
* expected callable-sourced.
|
||||
* TypeScript -- shares func_kinds_js; simplest free-function case expected
|
||||
* GREEN (the real-graph ts_lsp gap is for more complex bodies).
|
||||
* TSX -- shares the TS/JS func_kinds; same expectation as TypeScript.
|
||||
* Lua -- in the enclosing-func switch (repro_invariant_enclosing_
|
||||
* parity.c); enclosing detection supported.
|
||||
* Ruby -- in the enclosing-func switch; method bodies source callably.
|
||||
* PHP -- in the enclosing-func switch; PHP LSP is hybrid; method/
|
||||
* function bodies source callably.
|
||||
* RED (module-sourced or no CALLS at all -- reproduces the gap):
|
||||
* Perl -- NOT in the enclosing-func switch; its enclosing-func drift
|
||||
* symptom is the documented Perl gap (repro_invariant_graph.c
|
||||
* INVARIANT 4). The in-body call is sourced at Module.
|
||||
* R -- "R enclosing-function detection likely missing from
|
||||
* func_kinds_for_lang; call sourced at Module" (breadth file).
|
||||
* Julia -- "Julia enclosing-function detection may not map
|
||||
* function_definition to a callable QN; call sourced at
|
||||
* Module" (breadth file).
|
||||
* Groovy -- function_call callee not on a function/name field; no groovy
|
||||
* branch in extract_calls.c -- likely no in-body CALLS edge,
|
||||
* so dimension 7 cannot reach >=1 callable-sourced (RED).
|
||||
* Dart -- selector call node carries no callee field; no dart branch
|
||||
* in extract_calls.c -- likely no in-body CALLS edge (RED).
|
||||
*
|
||||
* Dimensions 1-6 and 8 are expected GREEN for these idiomatic fixtures across
|
||||
* all 12 languages; dimension 7 is the deliverable RED signal for Perl/R/Julia/
|
||||
* Groovy/Dart and the GREEN regression guard for Python/JS/TS/TSX/Lua/Ruby/PHP.
|
||||
*
|
||||
* Coding rule: inline comments are line comments only (no block comments inside
|
||||
* block comments).
|
||||
*/
|
||||
|
||||
#include "test_framework.h"
|
||||
#include "repro_invariant_lib.h"
|
||||
#include <store/store.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ── Shared single-file battery (dimensions 1-6) ────────────────────────────
|
||||
*
|
||||
* Runs the six single-file invariants against one fixture. Returns 0 when all
|
||||
* pass, 1 otherwise (printing a per-dimension FAIL line). lang_tag is for
|
||||
* diagnostics only. expect_label / expect_label2 are def labels the fixture is
|
||||
* guaranteed to produce (e.g. "Function" and "Class"); pass NULL for
|
||||
* expect_label2 when the language has no class in the fixture. callee is the
|
||||
* in-body callee name that must appear in the extracted calls.
|
||||
*/
|
||||
static int single_file_battery(const char *lang_tag, const char *src,
|
||||
CBMLanguage lang, const char *file,
|
||||
const char *expect_label,
|
||||
const char *expect_label2, const char *callee) {
|
||||
const char *RED = tf_red();
|
||||
const char *RST = tf_reset();
|
||||
int fails = 0;
|
||||
|
||||
/* 1. extract-clean -- must hold before anything else is meaningful. */
|
||||
if (inv_extract_clean(src, lang, file) != 1) {
|
||||
printf(" %sFAIL%s [%s] extract-clean: NULL result or has_error set\n",
|
||||
RED, RST, lang_tag);
|
||||
return 1; /* nothing else can be trusted */
|
||||
}
|
||||
|
||||
CBMFileResult *r = inv_rx(src, lang, file);
|
||||
if (!r) {
|
||||
printf(" %sFAIL%s [%s] inv_rx returned NULL after clean extract\n",
|
||||
RED, RST, lang_tag);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 2. labels-valid */
|
||||
int bad_labels = inv_count_bad_labels(r);
|
||||
if (bad_labels != 0) {
|
||||
printf(" %sFAIL%s [%s] labels-valid: %d def(s) with invalid label\n",
|
||||
RED, RST, lang_tag, bad_labels);
|
||||
fails++;
|
||||
}
|
||||
|
||||
/* 3. fqn-wellformed */
|
||||
int bad_fqns = inv_count_bad_fqns(r);
|
||||
if (bad_fqns != 0) {
|
||||
printf(" %sFAIL%s [%s] fqn-wellformed: %d def(s) with malformed QN\n",
|
||||
RED, RST, lang_tag, bad_fqns);
|
||||
fails++;
|
||||
}
|
||||
|
||||
/* 4. ranges-valid */
|
||||
int bad_ranges = inv_count_bad_ranges(r);
|
||||
if (bad_ranges != 0) {
|
||||
printf(" %sFAIL%s [%s] ranges-valid: %d def(s) with invalid range\n",
|
||||
RED, RST, lang_tag, bad_ranges);
|
||||
fails++;
|
||||
}
|
||||
|
||||
/* 5. defs-present -- the function/class the fixture wrote must be extracted. */
|
||||
if (expect_label && inv_count_label(r, expect_label) < 1) {
|
||||
printf(" %sFAIL%s [%s] defs-present: no def labelled \"%s\"\n",
|
||||
RED, RST, lang_tag, expect_label);
|
||||
fails++;
|
||||
}
|
||||
if (expect_label2 && inv_count_label(r, expect_label2) < 1) {
|
||||
printf(" %sFAIL%s [%s] defs-present: no def labelled \"%s\"\n",
|
||||
RED, RST, lang_tag, expect_label2);
|
||||
fails++;
|
||||
}
|
||||
|
||||
/* 6. calls-extracted -- the in-body call must be captured. */
|
||||
if (inv_has_call(r, callee) != 1) {
|
||||
printf(" %sFAIL%s [%s] calls-extracted: no call to \"%s\" found\n",
|
||||
RED, RST, lang_tag, callee);
|
||||
fails++;
|
||||
}
|
||||
|
||||
cbm_free_result(r);
|
||||
return fails ? 1 : 0;
|
||||
}
|
||||
|
||||
/* ── Shared full-pipeline battery (dimensions 7-8) ──────────────────────────
|
||||
*
|
||||
* Indexes the single-file fixture through the production pipeline and asserts
|
||||
* callable-sourcing (no Module-sourced in-body CALLS) and no dangling CALLS
|
||||
* edges. Returns 0 on PASS, 1 on FAIL. Dimension 7 is RED for the dynamic
|
||||
* languages whose enclosing-func detection or call extraction is missing
|
||||
* (Perl/R/Julia/Groovy/Dart) -- that is the intended signal.
|
||||
*/
|
||||
static int pipeline_battery(const char *lang_tag, const char *filename,
|
||||
const char *src) {
|
||||
const char *RED = tf_red();
|
||||
const char *RST = tf_reset();
|
||||
|
||||
RFile files[1];
|
||||
files[0].name = filename;
|
||||
files[0].content = src;
|
||||
|
||||
RProj lp;
|
||||
cbm_store_t *store = rh_index_files(&lp, files, 1);
|
||||
if (!store) {
|
||||
printf(" %sFAIL%s [%s] pipeline: rh_index_files returned NULL\n",
|
||||
RED, RST, lang_tag);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int fails = 0;
|
||||
|
||||
/* 7. callable-sourcing -- mod must be 0; we also require >=1 callable-sourced
|
||||
* edge so a fixture that produced zero CALLS edges cannot vacuously pass. */
|
||||
int module_sourced = 0;
|
||||
int callable_sourced = 0;
|
||||
inv_count_calls_by_source(store, lp.project, &module_sourced,
|
||||
&callable_sourced);
|
||||
if (module_sourced != 0) {
|
||||
printf(" %sFAIL%s [%s] callable-sourcing: %d in-body CALLS sourced at "
|
||||
"Module (callable=%d) -- known enclosing-func gap\n",
|
||||
RED, RST, lang_tag, module_sourced, callable_sourced);
|
||||
fails++;
|
||||
} else if (callable_sourced < 1) {
|
||||
printf(" %sFAIL%s [%s] callable-sourcing: 0 CALLS edges (fixture "
|
||||
"produced no in-body call edge to attribute)\n",
|
||||
RED, RST, lang_tag);
|
||||
fails++;
|
||||
}
|
||||
|
||||
/* 8. no-dangling -- every CALLS edge endpoint must resolve. */
|
||||
int dangling = inv_count_dangling_edges(store, lp.project, "CALLS");
|
||||
if (dangling != 0) {
|
||||
printf(" %sFAIL%s [%s] no-dangling: %d dangling CALLS endpoint(s)\n",
|
||||
RED, RST, lang_tag, dangling);
|
||||
fails++;
|
||||
}
|
||||
|
||||
rh_cleanup(&lp, store);
|
||||
return fails ? 1 : 0;
|
||||
}
|
||||
|
||||
/* ── Python ─────────────────────────────────────────────────────────────────
|
||||
* Idiomatic: import, a free function, a class with a method, in-body call.
|
||||
* Expected GREEN across the battery including dim 7 (func_kinds_python =
|
||||
* {function_definition}; grep-validated correct). Regression guard: if dim 7
|
||||
* goes RED, Python callable attribution has broken.
|
||||
*/
|
||||
TEST(repro_grammar_scripting_python) {
|
||||
static const char src[] =
|
||||
"import os\n"
|
||||
"\n"
|
||||
"def add(a, b):\n"
|
||||
" return a + b\n"
|
||||
"\n"
|
||||
"class Calc:\n"
|
||||
" def compute(self, x):\n"
|
||||
" return add(x, 1)\n";
|
||||
if (single_file_battery("Python", src, CBM_LANG_PYTHON, "calc.py",
|
||||
"Function", "Class", "add") != 0)
|
||||
return 1;
|
||||
return pipeline_battery("Python", "calc.py", src);
|
||||
}
|
||||
|
||||
/* ── Ruby ────────────────────────────────────────────────────────────────────
|
||||
* Idiomatic: require, a class with two methods, in-body call.
|
||||
* Expected: dims 1-6 + 8 GREEN, dim 7 GREEN (Ruby is in the enclosing-func
|
||||
* switch; method bodies source callably). Regression guard.
|
||||
*/
|
||||
TEST(repro_grammar_scripting_ruby) {
|
||||
static const char src[] =
|
||||
"require 'set'\n"
|
||||
"\n"
|
||||
"class Calculator\n"
|
||||
" def add(a, b)\n"
|
||||
" a + b\n"
|
||||
" end\n"
|
||||
"\n"
|
||||
" def compute(x)\n"
|
||||
" add(x, 1)\n"
|
||||
" end\n"
|
||||
"end\n";
|
||||
if (single_file_battery("Ruby", src, CBM_LANG_RUBY, "calc.rb",
|
||||
"Method", "Class", "add") != 0)
|
||||
return 1;
|
||||
return pipeline_battery("Ruby", "calc.rb", src);
|
||||
}
|
||||
|
||||
/* ── PHP ──────────────────────────────────────────────────────────────────────
|
||||
* Idiomatic: <?php tag, a class with two methods, in-body call via $this.
|
||||
* Expected: dims 1-6 + 8 GREEN, dim 7 GREEN (PHP is in the enclosing-func
|
||||
* switch; PHP LSP is hybrid). The callee is the same-class method `add`.
|
||||
*/
|
||||
TEST(repro_grammar_scripting_php) {
|
||||
static const char src[] =
|
||||
"<?php\n"
|
||||
"\n"
|
||||
"class Calculator {\n"
|
||||
" private function add($a, $b) {\n"
|
||||
" return $a + $b;\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" public function compute($x) {\n"
|
||||
" return $this->add($x, 1);\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
if (single_file_battery("PHP", src, CBM_LANG_PHP, "Calculator.php",
|
||||
"Method", "Class", "add") != 0)
|
||||
return 1;
|
||||
return pipeline_battery("PHP", "Calculator.php", src);
|
||||
}
|
||||
|
||||
/* ── JavaScript ───────────────────────────────────────────────────────────────
|
||||
* Idiomatic: import, a free function, a class with a method, in-body call.
|
||||
* Expected: dims 1-6 + 8 GREEN, dim 7 GREEN (func_kinds_js supports
|
||||
* function_declaration + method_definition; the simplest free-function call is
|
||||
* callable-sourced).
|
||||
*/
|
||||
TEST(repro_grammar_scripting_javascript) {
|
||||
static const char src[] =
|
||||
"import fs from 'fs';\n"
|
||||
"\n"
|
||||
"function add(a, b) {\n"
|
||||
" return a + b;\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class Calculator {\n"
|
||||
" compute(x) {\n"
|
||||
" return add(x, 1);\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
if (single_file_battery("JavaScript", src, CBM_LANG_JAVASCRIPT, "calc.js",
|
||||
"Function", "Class", "add") != 0)
|
||||
return 1;
|
||||
return pipeline_battery("JavaScript", "calc.js", src);
|
||||
}
|
||||
|
||||
/* ── TypeScript ───────────────────────────────────────────────────────────────
|
||||
* Idiomatic: import, a typed free function, a class with a method, in-body call.
|
||||
* Expected: dims 1-6 + 8 GREEN, dim 7 GREEN for this simplest case (shares
|
||||
* func_kinds_js). The real-graph ts_lsp Module-sourced gap is for more complex
|
||||
* bodies; if this still fails the test documents it.
|
||||
*/
|
||||
TEST(repro_grammar_scripting_typescript) {
|
||||
static const char src[] =
|
||||
"import { readFileSync } from 'fs';\n"
|
||||
"\n"
|
||||
"function add(a: number, b: number): number {\n"
|
||||
" return a + b;\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class Calculator {\n"
|
||||
" compute(x: number): number {\n"
|
||||
" return add(x, 1);\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
if (single_file_battery("TypeScript", src, CBM_LANG_TYPESCRIPT, "calc.ts",
|
||||
"Function", "Class", "add") != 0)
|
||||
return 1;
|
||||
return pipeline_battery("TypeScript", "calc.ts", src);
|
||||
}
|
||||
|
||||
/* ── TSX ──────────────────────────────────────────────────────────────────────
|
||||
* Idiomatic: import, a typed free function, a component class with a method
|
||||
* returning JSX, in-body call. Expected: dims 1-6 + 8 GREEN, dim 7 GREEN
|
||||
* (shares the TS/JS func_kinds). Uses CBM_LANG_TSX with a .tsx file.
|
||||
*/
|
||||
TEST(repro_grammar_scripting_tsx) {
|
||||
static const char src[] =
|
||||
"import React from 'react';\n"
|
||||
"\n"
|
||||
"function add(a: number, b: number): number {\n"
|
||||
" return a + b;\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class Widget extends React.Component {\n"
|
||||
" compute(x: number): number {\n"
|
||||
" return add(x, 1);\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
if (single_file_battery("TSX", src, CBM_LANG_TSX, "Widget.tsx",
|
||||
"Function", "Class", "add") != 0)
|
||||
return 1;
|
||||
return pipeline_battery("TSX", "Widget.tsx", src);
|
||||
}
|
||||
|
||||
/* ── Lua ──────────────────────────────────────────────────────────────────────
|
||||
* Idiomatic: require, a local function, a module-style function whose body calls
|
||||
* the helper. Lua has no idiomatic class keyword, so no expect_label2.
|
||||
* Expected: dims 1-6 + 8 GREEN, dim 7 GREEN (Lua is in the enclosing-func
|
||||
* switch; function bodies source callably).
|
||||
*/
|
||||
TEST(repro_grammar_scripting_lua) {
|
||||
static const char src[] =
|
||||
"local math = require('math')\n"
|
||||
"\n"
|
||||
"local function add(a, b)\n"
|
||||
" return a + b\n"
|
||||
"end\n"
|
||||
"\n"
|
||||
"function compute(x)\n"
|
||||
" return add(x, 1)\n"
|
||||
"end\n";
|
||||
if (single_file_battery("Lua", src, CBM_LANG_LUA, "calc.lua",
|
||||
"Function", NULL, "add") != 0)
|
||||
return 1;
|
||||
return pipeline_battery("Lua", "calc.lua", src);
|
||||
}
|
||||
|
||||
/* ── Perl ─────────────────────────────────────────────────────────────────────
|
||||
* Idiomatic: use pragma, two subs, the callee called strictly inside the caller
|
||||
* sub body. Perl has no idiomatic class in this fixture (no expect_label2).
|
||||
* Expected: dims 1-6 + 8 GREEN, dim 7 RED (Perl is NOT in the enclosing-func
|
||||
* switch; its enclosing-func drift is the documented Perl gap -- the in-body
|
||||
* call is sourced at Module). RED dim-7 IS the deliverable.
|
||||
*/
|
||||
TEST(repro_grammar_scripting_perl) {
|
||||
static const char src[] =
|
||||
"use strict;\n"
|
||||
"\n"
|
||||
"sub add {\n"
|
||||
" my ($a, $b) = @_;\n"
|
||||
" return $a + $b;\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"sub compute {\n"
|
||||
" my ($x) = @_;\n"
|
||||
" return add($x, 1);\n"
|
||||
"}\n";
|
||||
if (single_file_battery("Perl", src, CBM_LANG_PERL, "calc.pl",
|
||||
"Function", NULL, "add") != 0)
|
||||
return 1;
|
||||
return pipeline_battery("Perl", "calc.pl", src);
|
||||
}
|
||||
|
||||
/* ── R ────────────────────────────────────────────────────────────────────────
|
||||
* Idiomatic: library() load, two function assignments, the callee called inside
|
||||
* the caller's body. R has no idiomatic class in this fixture (no expect_label2).
|
||||
* Expected: dims 1-6 + 8 GREEN, dim 7 RED ("R enclosing-function detection
|
||||
* likely missing from func_kinds_for_lang; call sourced at Module" per the
|
||||
* breadth file). RED dim-7 IS the deliverable.
|
||||
*/
|
||||
TEST(repro_grammar_scripting_r) {
|
||||
static const char src[] =
|
||||
"library(stats)\n"
|
||||
"\n"
|
||||
"add <- function(a, b) {\n"
|
||||
" a + b\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"compute <- function(x) {\n"
|
||||
" add(x, 1)\n"
|
||||
"}\n";
|
||||
if (single_file_battery("R", src, CBM_LANG_R, "calc.R",
|
||||
"Function", NULL, "add") != 0)
|
||||
return 1;
|
||||
return pipeline_battery("R", "calc.R", src);
|
||||
}
|
||||
|
||||
/* ── Julia ────────────────────────────────────────────────────────────────────
|
||||
* Idiomatic: using, two functions, the callee called inside the caller body.
|
||||
* Julia structs are idiomatic but methods are free functions, so the fixture
|
||||
* asserts on Function only (no expect_label2). Expected: dims 1-6 + 8 GREEN,
|
||||
* dim 7 RED ("Julia enclosing-function detection may not map
|
||||
* function_definition to a callable QN; call sourced at Module" per breadth
|
||||
* file). RED dim-7 IS the deliverable.
|
||||
*/
|
||||
TEST(repro_grammar_scripting_julia) {
|
||||
static const char src[] =
|
||||
"using Printf\n"
|
||||
"\n"
|
||||
"function add(a, b)\n"
|
||||
" return a + b\n"
|
||||
"end\n"
|
||||
"\n"
|
||||
"function compute(x)\n"
|
||||
" return add(x, 1)\n"
|
||||
"end\n";
|
||||
if (single_file_battery("Julia", src, CBM_LANG_JULIA, "calc.jl",
|
||||
"Function", NULL, "add") != 0)
|
||||
return 1;
|
||||
return pipeline_battery("Julia", "calc.jl", src);
|
||||
}
|
||||
|
||||
/* ── Groovy ───────────────────────────────────────────────────────────────────
|
||||
* Idiomatic: import, a class with two methods, in-body call.
|
||||
* Expected: dims 1-5 + 8 GREEN. Dim 6 (calls-extracted) and dim 7 are RED:
|
||||
* "function_call callee not on a function/name field and first child is not
|
||||
* 'identifier'; no groovy branch in extract_calls.c" (breadth file), so the
|
||||
* in-body call may not be captured and no callable-sourced CALLS edge is
|
||||
* produced. RED IS the deliverable. (single_file_battery returns early on the
|
||||
* dim-6 miss; pipeline dim-7 likewise fails on 0 callable edges.)
|
||||
*/
|
||||
TEST(repro_grammar_scripting_groovy) {
|
||||
static const char src[] =
|
||||
"import groovy.transform.CompileStatic\n"
|
||||
"\n"
|
||||
"class Calculator {\n"
|
||||
" int add(int a, int b) {\n"
|
||||
" return a + b\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" int compute(int x) {\n"
|
||||
" return add(x, 1)\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
if (single_file_battery("Groovy", src, CBM_LANG_GROOVY, "Calculator.groovy",
|
||||
"Method", "Class", "add") != 0)
|
||||
return 1;
|
||||
return pipeline_battery("Groovy", "Calculator.groovy", src);
|
||||
}
|
||||
|
||||
/* ── Dart ─────────────────────────────────────────────────────────────────────
|
||||
* Idiomatic: import, a class with two methods, in-body call.
|
||||
* Expected: dims 1-5 + 8 GREEN. Dim 6 (calls-extracted) and dim 7 are RED:
|
||||
* "selector call node carries no callee field and the first child is not an
|
||||
* identifier; no dart branch in extract_calls.c" (breadth file), so no in-body
|
||||
* CALLS edge is produced. RED IS the deliverable. Uses CBM_LANG_DART.
|
||||
*/
|
||||
TEST(repro_grammar_scripting_dart) {
|
||||
static const char src[] =
|
||||
"import 'dart:math';\n"
|
||||
"\n"
|
||||
"class Calculator {\n"
|
||||
" int add(int a, int b) {\n"
|
||||
" return a + b;\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" int compute(int x) {\n"
|
||||
" return add(x, 1);\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
if (single_file_battery("Dart", src, CBM_LANG_DART, "calc.dart",
|
||||
"Method", "Class", "add") != 0)
|
||||
return 1;
|
||||
return pipeline_battery("Dart", "calc.dart", src);
|
||||
}
|
||||
|
||||
/* ── Suite ──────────────────────────────────────────────────────────────────── */
|
||||
|
||||
SUITE(repro_grammar_scripting) {
|
||||
RUN_TEST(repro_grammar_scripting_python);
|
||||
RUN_TEST(repro_grammar_scripting_ruby);
|
||||
RUN_TEST(repro_grammar_scripting_php);
|
||||
RUN_TEST(repro_grammar_scripting_javascript);
|
||||
RUN_TEST(repro_grammar_scripting_typescript);
|
||||
RUN_TEST(repro_grammar_scripting_tsx);
|
||||
RUN_TEST(repro_grammar_scripting_lua);
|
||||
RUN_TEST(repro_grammar_scripting_perl);
|
||||
RUN_TEST(repro_grammar_scripting_r);
|
||||
RUN_TEST(repro_grammar_scripting_julia);
|
||||
RUN_TEST(repro_grammar_scripting_groovy);
|
||||
RUN_TEST(repro_grammar_scripting_dart);
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
/*
|
||||
* repro_lsp_go_py.c — EXHAUSTIVE per-LSP-pass invariant suite for the Go and
|
||||
* Python hybrid LSPs (internal/cbm/lsp/go_lsp.c, internal/cbm/lsp/py_lsp.c).
|
||||
*
|
||||
* WHAT THIS ASSERTS — the LSP RESOLUTION CONTRACT, one invariant per strategy.
|
||||
* Each cross resolver resolves a call via a specific STRATEGY and tags the
|
||||
* resulting CALLS edge in its properties_json with
|
||||
* "strategy":"lsp_<name>"
|
||||
* (Go: emit_resolved_call, go_lsp.c:1084-1094; Python: py_emit_resolved_call,
|
||||
* py_lsp.c:322-353; every emit site passes a literal "lsp_..." string). Each
|
||||
* strategy keys on a precise Go/Python construct. This suite builds the
|
||||
* MINIMAL fixture that exercises exactly one strategy, indexes it through the
|
||||
* full production pipeline, and asserts TWO things:
|
||||
* (a) callable-sourcing — the inner call is sourced at a Function/Method
|
||||
* node, never at a Module/File node (inv_count_calls_by_source →
|
||||
* module_sourced == 0). A Module-sourced call is the #554 attribution
|
||||
* bug; this is the broad correctness floor.
|
||||
* (b) strategy-presence — some CALLS edge carries "lsp_<strategy>" in its
|
||||
* properties_json (inv_edge_has_strategy). This is the PRECISE per-pass
|
||||
* invariant: it proves that exact resolution path fired and survived
|
||||
* into the graph.
|
||||
*
|
||||
* RED vs GREEN — this is a STATUS BOARD, not a pass/fail gate (runs only under
|
||||
* make test-repro / bug-repro.yml, never the branch-protection ci-ok gate):
|
||||
* - GREEN = the LSP strategy works end-to-end = a permanent regression
|
||||
* guard that it keeps working.
|
||||
* - RED = the strategy is dropped, or the call lands Module-sourced, or
|
||||
* the rescue is discarded. Either way the per-pass TEST DOCUMENTS
|
||||
* the exact gap for the eventual fixer.
|
||||
*
|
||||
* TIE TO repro_invariant_lsp_rescue.c — that file pins the MECHANISM by which
|
||||
* these can silently fail: cbm_pipeline_find_lsp_resolution joins each
|
||||
* LSP-resolved call to the tree-sitter call by EXACT caller-QN string
|
||||
* equality. When tree-sitter's enclosing-func walk falls back to the MODULE
|
||||
* QN but the LSP built the real method QN, the strcmp never matches, the LSP
|
||||
* rescue is discarded, and the edge stays Module-sourced with a registry
|
||||
* strategy — NEVER an "lsp_" strategy. So a strategy that is correctly
|
||||
* EMITTED by the LSP can still be ABSENT from the graph here: the exact-QN
|
||||
* join suppresses it. Whenever a strategy below is RED, suspect that join
|
||||
* first (a same-file in-function fixture sidesteps it).
|
||||
*
|
||||
* GO STRATEGY INVENTORY — every literal "lsp_..." emitted by go_lsp.c, grepped
|
||||
* from the source (grep '"lsp_' internal/cbm/lsp/go_lsp.c), with its keying
|
||||
* site:
|
||||
* lsp_direct (go_lsp.c:1139/1265) pkg.Func() or local f()
|
||||
* lsp_type_dispatch (go_lsp.c:1161) obj.Method() on a concrete
|
||||
* value type (receiver type
|
||||
* == method receiver type)
|
||||
* lsp_embed_dispatch (go_lsp.c:1164) embedded-struct promoted
|
||||
* method (method receiver
|
||||
* type != outer type)
|
||||
* lsp_interface_resolve (go_lsp.c:1226) call through an interface
|
||||
* with EXACTLY ONE concrete
|
||||
* implementer in the project
|
||||
* lsp_interface_dispatch (go_lsp.c:1236) call through an interface
|
||||
* with 0 or >=2 implementers
|
||||
* (generic fallback)
|
||||
* lsp_strategy_cross_file (go_lsp.c:2925) cross-file fast-resolve of
|
||||
* an unresolved call against
|
||||
* the global registry
|
||||
* lsp_unresolved (go_lsp.c:1103) fallback marker for an
|
||||
* unresolved call
|
||||
*
|
||||
* PYTHON STRATEGY INVENTORY — every literal "lsp_..." emitted by py_lsp.c
|
||||
* (grep '"lsp_' internal/cbm/lsp/py_lsp.c), with its keying site:
|
||||
* lsp_direct (py_lsp.c:1631) module-local f()
|
||||
* lsp_constructor (py_lsp.c:1624) ClassName() where the name is a
|
||||
* NAMED type in scope
|
||||
* lsp_method (py_lsp.c:1731) obj.method() on a NAMED-typed
|
||||
* receiver (covers self.other())
|
||||
* lsp_super (py_lsp.c:1693) super().method() resolved on a
|
||||
* base class (non-__init__)
|
||||
* lsp_super_init (py_lsp.c:1702) super().__init__()
|
||||
* lsp_module_attr (py_lsp.c:1719) mod.func() after `import mod`,
|
||||
* func is a registered symbol
|
||||
* lsp_module_attr_unresolved(py_lsp.c:1724) mod.func() where func is NOT a
|
||||
* registered symbol of the module
|
||||
* lsp_dict_dispatch (py_lsp.c:1662) funcs["key"]() dispatch table
|
||||
* lsp_operator_dunder (py_lsp.c:2120) a + b where a is a NAMED type
|
||||
* defining __add__
|
||||
* lsp_builtin (py_lsp.c:1637) print()/len()/... a builtins
|
||||
* symbol (needs typeshed registry)
|
||||
* lsp_builtin_constructor (py_lsp.c:1643) str()/list()/... a builtins type
|
||||
* lsp_builtin_method (py_lsp.c:1741) "x".upper() — method on a
|
||||
* builtin-typed receiver
|
||||
* lsp_generic_method (py_lsp.c:1753) method on a TEMPLATE-typed
|
||||
* receiver (list[T]/dict[K,V])
|
||||
* lsp_method_union (py_lsp.c:1778) method on a UNION-typed receiver
|
||||
* with exactly one matching member
|
||||
*
|
||||
* EXPECTED-RED NOTES (documented gaps, not suite bugs):
|
||||
* - lsp_builtin / lsp_builtin_constructor / lsp_builtin_method /
|
||||
* lsp_generic_method: resolution requires the builtins/typeshed registry
|
||||
* ("builtins.print", "builtins.str.upper", ...) to be loaded into the
|
||||
* per-file registry. A single-file fixture has no typeshed, so these are
|
||||
* expected ABSENT (RED) — they document that the builtins-registry binding
|
||||
* the single-file harness can't synthesize is required.
|
||||
* - lsp_method_union: needs a union-typed receiver (e.g. `x: A | B`) where
|
||||
* exactly one member defines the method; the annotation must resolve both
|
||||
* members to in-file NAMED types. Documented if it does not surface.
|
||||
*
|
||||
* NOTE: line comments only inside this header (no nested block comments, per
|
||||
* coding rules).
|
||||
*/
|
||||
|
||||
#include "test_framework.h"
|
||||
#include "repro_invariant_lib.h"
|
||||
#include <store/store.h>
|
||||
|
||||
#include <string.h>
|
||||
|
||||
/* ── Shared per-strategy runners (DRY) ───────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* assert_lsp_strategy_files
|
||||
*
|
||||
* Index an N-file fixture and assert the per-pass LSP RESOLUTION CONTRACT:
|
||||
* 1. the store opened (precondition — a setup failure is a FAIL, not a skip);
|
||||
* 2. callable-sourcing: NO CALLS edge is Module/File-sourced, and at least one
|
||||
* callable-sourced CALLS edge exists (else there is no signal at all);
|
||||
* 3. strategy-presence: some CALLS edge carries "lsp_<strategy>" in its
|
||||
* properties_json.
|
||||
*
|
||||
* The filename extension selects the language exactly as the production indexer
|
||||
* does (".go" → Go pass, ".py" → Python pass). Returns 0 on PASS (GREEN),
|
||||
* non-zero on FAIL (RED) — the redness is the documented per-pass status.
|
||||
*/
|
||||
static int assert_lsp_strategy_files(const RFile *files, int nfiles,
|
||||
const char *strategy) {
|
||||
RProj lp;
|
||||
cbm_store_t *store = rh_index_files(&lp, files, nfiles);
|
||||
if (!store) {
|
||||
printf(" %sFAIL%s %s:%d: index failed for strategy %s\n", tf_red(),
|
||||
tf_reset(), __FILE__, __LINE__, strategy);
|
||||
rh_cleanup(&lp, store);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int module_sourced = -1;
|
||||
int callable_sourced = -1;
|
||||
inv_count_calls_by_source(store, lp.project, &module_sourced,
|
||||
&callable_sourced);
|
||||
|
||||
int has_strategy = inv_edge_has_strategy(store, lp.project, strategy);
|
||||
|
||||
int rc = 0;
|
||||
|
||||
/* (a) callable-sourcing floor: zero Module/File-sourced CALLS edges. */
|
||||
if (module_sourced != 0) {
|
||||
printf(" %sFAIL%s %s:%d: strategy %s: %d Module-sourced CALLS "
|
||||
"(expected 0)\n",
|
||||
tf_red(), tf_reset(), __FILE__, __LINE__, strategy,
|
||||
module_sourced);
|
||||
rc = 1;
|
||||
}
|
||||
/* There must be a callable-sourced CALLS edge, else the fixture produced no
|
||||
* call signal and the strategy assertion below would be vacuous. */
|
||||
if (callable_sourced <= 0) {
|
||||
printf(" %sFAIL%s %s:%d: strategy %s: no callable-sourced CALLS edge "
|
||||
"(callable=%d)\n",
|
||||
tf_red(), tf_reset(), __FILE__, __LINE__, strategy,
|
||||
callable_sourced);
|
||||
rc = 1;
|
||||
}
|
||||
|
||||
/* (b) the precise per-pass invariant: the resolution strategy is present. */
|
||||
if (!has_strategy) {
|
||||
printf(" %sFAIL%s %s:%d: strategy %s ABSENT from any CALLS edge "
|
||||
"properties_json\n",
|
||||
tf_red(), tf_reset(), __FILE__, __LINE__, strategy);
|
||||
rc = 1;
|
||||
}
|
||||
|
||||
rh_cleanup(&lp, store);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Single-file convenience wrapper. */
|
||||
static int assert_lsp_strategy(const char *filename, const char *src,
|
||||
const char *strategy) {
|
||||
RFile f = {filename, src};
|
||||
return assert_lsp_strategy_files(&f, 1, strategy);
|
||||
}
|
||||
|
||||
/* ── Go fixtures ─────────────────────────────────────────────────────────────
|
||||
*
|
||||
* Each fixture is the MINIMAL construct go_lsp.c keys on for one strategy. The
|
||||
* call we care about always lives inside a func or method so callable-sourcing
|
||||
* is testable; the callee is also defined in-file so the registry can resolve
|
||||
* it. Every file declares `package main` so the package QN is consistent.
|
||||
* ───────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
/* lsp_direct — plain package-local function call f() (go_lsp.c:1259-1265:
|
||||
* func_node is a bare identifier resolved via cbm_registry_lookup_symbol on the
|
||||
* package QN). */
|
||||
static const char kGoDirect[] =
|
||||
"package main\n"
|
||||
"func helper(x int) int { return x + 1 }\n"
|
||||
"func caller(v int) int { return helper(v) }\n";
|
||||
|
||||
/* lsp_type_dispatch — obj.Method() on a concrete value type whose method's
|
||||
* receiver type equals the receiver type (go_lsp.c:1158-1166: method found, the
|
||||
* method's receiver_type == the receiver's QN → lsp_type_dispatch). */
|
||||
static const char kGoTypeDispatch[] =
|
||||
"package main\n"
|
||||
"type Counter struct{ n int }\n"
|
||||
"func (c Counter) Inc(x int) int { return x + 1 }\n"
|
||||
"func caller() int {\n"
|
||||
" var c Counter\n"
|
||||
" return c.Inc(1)\n"
|
||||
"}\n";
|
||||
|
||||
/* lsp_embed_dispatch — call a promoted method from an embedded struct
|
||||
* (go_lsp.c:1162-1164: the resolved method's receiver_type != the outer
|
||||
* receiver type → lsp_embed_dispatch). Outer embeds Inner; o.Greet() resolves
|
||||
* to Inner.Greet whose receiver_type is Inner, not Outer. */
|
||||
static const char kGoEmbedDispatch[] =
|
||||
"package main\n"
|
||||
"type Inner struct{}\n"
|
||||
"func (i Inner) Greet(x int) int { return x + 7 }\n"
|
||||
"type Outer struct{ Inner }\n"
|
||||
"func caller() int {\n"
|
||||
" var o Outer\n"
|
||||
" return o.Greet(1)\n"
|
||||
"}\n";
|
||||
|
||||
/* lsp_interface_resolve — call through an interface that has EXACTLY ONE
|
||||
* concrete implementer in the project (go_lsp.c:1220-1226: impl_count == 1 →
|
||||
* resolve to the sole implementer's concrete method). Speaker has one
|
||||
* implementer (Dog), so s.Speak() resolves to Dog.Speak. */
|
||||
static const char kGoInterfaceResolve[] =
|
||||
"package main\n"
|
||||
"type Speaker interface{ Speak(x int) int }\n"
|
||||
"type Dog struct{}\n"
|
||||
"func (d Dog) Speak(x int) int { return x * 2 }\n"
|
||||
"func caller(s Speaker) int {\n"
|
||||
" return s.Speak(3)\n"
|
||||
"}\n";
|
||||
|
||||
/* lsp_interface_dispatch — call through an interface with TWO implementers, so
|
||||
* the sole-implementer shortcut does not fire and the generic interface
|
||||
* fallback emits "<iface>.<method>" (go_lsp.c:1232-1236). Speaker has Dog and
|
||||
* Cat → ambiguous → generic dispatch. */
|
||||
static const char kGoInterfaceDispatch[] =
|
||||
"package main\n"
|
||||
"type Speaker interface{ Speak(x int) int }\n"
|
||||
"type Dog struct{}\n"
|
||||
"func (d Dog) Speak(x int) int { return x * 2 }\n"
|
||||
"type Cat struct{}\n"
|
||||
"func (c Cat) Speak(x int) int { return x * 3 }\n"
|
||||
"func caller(s Speaker) int {\n"
|
||||
" return s.Speak(3)\n"
|
||||
"}\n";
|
||||
|
||||
/* lsp_strategy_cross_file — an unresolved per-file call (callee defined in
|
||||
* ANOTHER file) is fixed up by the cross-file fast resolver against the global
|
||||
* registry (go_lsp.c:2867-2937: a "function_not_in_registry"/"method_not_found"
|
||||
* unresolved entry whose callee_qn is found in the merged registry →
|
||||
* lsp_strategy_cross_file). caller.go calls a method defined in helper.go. */
|
||||
static const RFile kGoCrossFile[] = {
|
||||
{"helper.go",
|
||||
"package main\n"
|
||||
"type Service struct{}\n"
|
||||
"func (s Service) Run(x int) int { return x + 5 }\n"},
|
||||
{"caller.go",
|
||||
"package main\n"
|
||||
"func caller(s Service) int {\n"
|
||||
" return s.Run(2)\n"
|
||||
"}\n"},
|
||||
};
|
||||
|
||||
/* lsp_unresolved — a call to a function not in the registry; the per-file
|
||||
* resolver records the fallback marker (go_lsp.c:1097-1107, strategy =
|
||||
* "lsp_unresolved"). NOTE: emit_unresolved_call uses confidence 0.0, so the
|
||||
* pipeline may not promote it into a CALLS edge with the strategy tag — this
|
||||
* fixture documents whether "lsp_unresolved" surfaces in the graph. */
|
||||
static const char kGoUnresolved[] =
|
||||
"package main\n"
|
||||
"func caller(v int) int {\n"
|
||||
" return totallyUnknownFn(v)\n"
|
||||
"}\n";
|
||||
|
||||
/* ── Python fixtures ───────────────────────────────────────────────────────── */
|
||||
|
||||
/* lsp_direct — module-local function call f() (py_lsp.c:1627-1631: identifier
|
||||
* resolves via cbm_registry_lookup_symbol on the module QN). */
|
||||
static const char kPyDirect[] =
|
||||
"def helper(x):\n"
|
||||
" return x + 1\n"
|
||||
"def caller(v):\n"
|
||||
" return helper(v)\n";
|
||||
|
||||
/* lsp_constructor — ClassName() where the name is a NAMED type in scope
|
||||
* (py_lsp.c:1620-1624: cbm_scope_lookup yields a NAMED type → emit constructor
|
||||
* edge to the class QN). */
|
||||
static const char kPyConstructor[] =
|
||||
"class Widget:\n"
|
||||
" def __init__(self):\n"
|
||||
" pass\n"
|
||||
"def caller():\n"
|
||||
" return Widget()\n";
|
||||
|
||||
/* lsp_method — a method calls a sibling method via self.other() (py_lsp.c:
|
||||
* 1727-1731: obj_type is NAMED (self is typed as the enclosing class,
|
||||
* py_lsp.c:2950-2952) and py_lookup_attribute finds the method → lsp_method). */
|
||||
static const char kPyMethod[] =
|
||||
"class Widget:\n"
|
||||
" def compute(self, x):\n"
|
||||
" return self.helper(x) + 1\n"
|
||||
" def helper(self, x):\n"
|
||||
" return x * 2\n";
|
||||
|
||||
/* lsp_super — super().method() where the enclosing class has a base class that
|
||||
* defines `method` (py_lsp.c:1681-1693: obj is a super() call, the attr resolves
|
||||
* against a base in embedded_types, attr != __init__ → lsp_super). Child's
|
||||
* greet() calls super().describe(); Base.describe exists. */
|
||||
static const char kPySuper[] =
|
||||
"class Base:\n"
|
||||
" def describe(self, x):\n"
|
||||
" return x\n"
|
||||
"class Child(Base):\n"
|
||||
" def greet(self, x):\n"
|
||||
" return super().describe(x)\n";
|
||||
|
||||
/* lsp_super_init — super().__init__() (py_lsp.c:1699-1702: attr == __init__ on a
|
||||
* super() proxy → synthesize a constructor edge to <base>.__init__). */
|
||||
static const char kPySuperInit[] =
|
||||
"class Base:\n"
|
||||
" def __init__(self):\n"
|
||||
" self.ready = True\n"
|
||||
"class Child(Base):\n"
|
||||
" def __init__(self):\n"
|
||||
" super().__init__()\n";
|
||||
|
||||
/* lsp_module_attr — mod.func() after `import mod`, where func is a registered
|
||||
* symbol of the imported in-project module (py_lsp.c:1715-1719: obj_type is
|
||||
* MODULE and cbm_registry_lookup_symbol(module_qn, attr) hits → lsp_module_attr).
|
||||
* Requires a second in-project file so the imported symbol is in the registry. */
|
||||
static const RFile kPyModuleAttr[] = {
|
||||
{"helpers.py",
|
||||
"def do_work(x):\n"
|
||||
" return x + 9\n"},
|
||||
{"main.py",
|
||||
"import helpers\n"
|
||||
"def caller(v):\n"
|
||||
" return helpers.do_work(v)\n"},
|
||||
};
|
||||
|
||||
/* lsp_module_attr_unresolved — mod.func() after `import mod` where func is NOT a
|
||||
* registered symbol of the module (py_lsp.c:1722-1724: MODULE receiver but the
|
||||
* symbol lookup misses → best-effort "module.attr" QN, low confidence). helpers
|
||||
* defines nothing named missing_fn. */
|
||||
static const RFile kPyModuleAttrUnresolved[] = {
|
||||
{"helpers.py",
|
||||
"def do_work(x):\n"
|
||||
" return x + 9\n"},
|
||||
{"main.py",
|
||||
"import helpers\n"
|
||||
"def caller(v):\n"
|
||||
" return helpers.missing_fn(v)\n"},
|
||||
};
|
||||
|
||||
/* lsp_dict_dispatch — funcs["key"]() where funcs is a dict-literal dispatch
|
||||
* table mapping string keys to known function QNs (py_lsp.c:1371-1374 registers
|
||||
* the table; py_lsp.c:1651-1662 resolves the subscript-call → lsp_dict_dispatch).
|
||||
* The table and the call must be in the same function scope so the literal var
|
||||
* is registered before the call. */
|
||||
static const char kPyDictDispatch[] =
|
||||
"def foo(x):\n"
|
||||
" return x + 1\n"
|
||||
"def bar(x):\n"
|
||||
" return x + 2\n"
|
||||
"def caller(v):\n"
|
||||
" funcs = {\"a\": foo, \"b\": bar}\n"
|
||||
" return funcs[\"a\"](v)\n";
|
||||
|
||||
/* lsp_operator_dunder — a + b where a is a NAMED type defining __add__
|
||||
* (py_lsp.c:2106-2120: binary_operator on a typed NAMED receiver whose class
|
||||
* declares the dunder → emit a synthetic CALLS edge to T.__add__). The receiver
|
||||
* `a` is annotated so its type is known. */
|
||||
static const char kPyOperatorDunder[] =
|
||||
"class Vec:\n"
|
||||
" def __add__(self, other):\n"
|
||||
" return self\n"
|
||||
"def caller(a: Vec, b: Vec):\n"
|
||||
" return a + b\n";
|
||||
|
||||
/* lsp_builtin — print()/len()/... a builtins symbol (py_lsp.c:1634-1637:
|
||||
* cbm_registry_lookup_symbol("builtins", fname) hits). EXPECTED RED in a
|
||||
* single-file harness with no typeshed/builtins registry loaded. */
|
||||
static const char kPyBuiltin[] =
|
||||
"def caller(v):\n"
|
||||
" return len(v)\n";
|
||||
|
||||
/* lsp_builtin_constructor — str()/list()/... a builtins TYPE used as a
|
||||
* constructor (py_lsp.c:1640-1643: cbm_registry_lookup_type("builtins.str")
|
||||
* hits). EXPECTED RED without a typeshed/builtins registry. */
|
||||
static const char kPyBuiltinConstructor[] =
|
||||
"def caller(v):\n"
|
||||
" return str(v)\n";
|
||||
|
||||
/* lsp_builtin_method — "x".upper() — a method on a builtin-typed receiver
|
||||
* (py_lsp.c:1735-1741: obj_type is BUILTIN, py_lookup_attribute("builtins.str",
|
||||
* "upper") hits). EXPECTED RED without a typeshed/builtins registry. */
|
||||
static const char kPyBuiltinMethod[] =
|
||||
"def caller():\n"
|
||||
" s = \"hello\"\n"
|
||||
" return s.upper()\n";
|
||||
|
||||
/* lsp_generic_method — method on a TEMPLATE-typed receiver such as a list
|
||||
* (py_lsp.c:1745-1753: obj_type is TEMPLATE, attribute resolved on the template
|
||||
* base type). xs.append(1) on a list-typed xs. EXPECTED RED without a typeshed
|
||||
* registry providing builtins.list.append. */
|
||||
static const char kPyGenericMethod[] =
|
||||
"def caller():\n"
|
||||
" xs = [1, 2, 3]\n"
|
||||
" return xs.append(4)\n";
|
||||
|
||||
/* lsp_method_union — method on a UNION-typed receiver where exactly one member
|
||||
* defines the method (py_lsp.c:1757-1778: obj_type is UNION, exactly one NAMED
|
||||
* member resolves the attribute → lsp_method_union). `x: A | B` where only A
|
||||
* defines run(). Documented if the union annotation does not resolve both
|
||||
* members to in-file NAMED types. */
|
||||
static const char kPyMethodUnion[] =
|
||||
"class A:\n"
|
||||
" def run(self, v):\n"
|
||||
" return v\n"
|
||||
"class B:\n"
|
||||
" def stop(self, v):\n"
|
||||
" return v\n"
|
||||
"def caller(x: A | B):\n"
|
||||
" return x.run(1)\n";
|
||||
|
||||
/* ── Go per-strategy tests ───────────────────────────────────────────────── */
|
||||
|
||||
TEST(repro_lsp_go_direct) {
|
||||
return assert_lsp_strategy("main.go", kGoDirect, "lsp_direct");
|
||||
}
|
||||
|
||||
TEST(repro_lsp_go_type_dispatch) {
|
||||
return assert_lsp_strategy("main.go", kGoTypeDispatch, "lsp_type_dispatch");
|
||||
}
|
||||
|
||||
TEST(repro_lsp_go_embed_dispatch) {
|
||||
return assert_lsp_strategy("main.go", kGoEmbedDispatch, "lsp_embed_dispatch");
|
||||
}
|
||||
|
||||
TEST(repro_lsp_go_interface_resolve) {
|
||||
return assert_lsp_strategy("main.go", kGoInterfaceResolve,
|
||||
"lsp_interface_resolve");
|
||||
}
|
||||
|
||||
TEST(repro_lsp_go_interface_dispatch) {
|
||||
return assert_lsp_strategy("main.go", kGoInterfaceDispatch,
|
||||
"lsp_interface_dispatch");
|
||||
}
|
||||
|
||||
TEST(repro_lsp_go_strategy_cross_file) {
|
||||
return assert_lsp_strategy_files(
|
||||
kGoCrossFile, (int)(sizeof(kGoCrossFile) / sizeof(kGoCrossFile[0])),
|
||||
"lsp_strategy_cross_file");
|
||||
}
|
||||
|
||||
TEST(repro_lsp_go_unresolved) {
|
||||
return assert_lsp_strategy("main.go", kGoUnresolved, "lsp_unresolved");
|
||||
}
|
||||
|
||||
/* ── Python per-strategy tests ───────────────────────────────────────────── */
|
||||
|
||||
TEST(repro_lsp_py_direct) {
|
||||
return assert_lsp_strategy("main.py", kPyDirect, "lsp_direct");
|
||||
}
|
||||
|
||||
TEST(repro_lsp_py_constructor) {
|
||||
return assert_lsp_strategy("main.py", kPyConstructor, "lsp_constructor");
|
||||
}
|
||||
|
||||
TEST(repro_lsp_py_method) {
|
||||
return assert_lsp_strategy("main.py", kPyMethod, "lsp_method");
|
||||
}
|
||||
|
||||
TEST(repro_lsp_py_super) {
|
||||
return assert_lsp_strategy("main.py", kPySuper, "lsp_super");
|
||||
}
|
||||
|
||||
TEST(repro_lsp_py_super_init) {
|
||||
return assert_lsp_strategy("main.py", kPySuperInit, "lsp_super_init");
|
||||
}
|
||||
|
||||
TEST(repro_lsp_py_module_attr) {
|
||||
return assert_lsp_strategy_files(
|
||||
kPyModuleAttr, (int)(sizeof(kPyModuleAttr) / sizeof(kPyModuleAttr[0])),
|
||||
"lsp_module_attr");
|
||||
}
|
||||
|
||||
TEST(repro_lsp_py_module_attr_unresolved) {
|
||||
return assert_lsp_strategy_files(
|
||||
kPyModuleAttrUnresolved,
|
||||
(int)(sizeof(kPyModuleAttrUnresolved) / sizeof(kPyModuleAttrUnresolved[0])),
|
||||
"lsp_module_attr_unresolved");
|
||||
}
|
||||
|
||||
TEST(repro_lsp_py_dict_dispatch) {
|
||||
return assert_lsp_strategy("main.py", kPyDictDispatch, "lsp_dict_dispatch");
|
||||
}
|
||||
|
||||
TEST(repro_lsp_py_operator_dunder) {
|
||||
return assert_lsp_strategy("main.py", kPyOperatorDunder,
|
||||
"lsp_operator_dunder");
|
||||
}
|
||||
|
||||
TEST(repro_lsp_py_builtin) {
|
||||
return assert_lsp_strategy("main.py", kPyBuiltin, "lsp_builtin");
|
||||
}
|
||||
|
||||
TEST(repro_lsp_py_builtin_constructor) {
|
||||
return assert_lsp_strategy("main.py", kPyBuiltinConstructor,
|
||||
"lsp_builtin_constructor");
|
||||
}
|
||||
|
||||
TEST(repro_lsp_py_builtin_method) {
|
||||
return assert_lsp_strategy("main.py", kPyBuiltinMethod, "lsp_builtin_method");
|
||||
}
|
||||
|
||||
TEST(repro_lsp_py_generic_method) {
|
||||
return assert_lsp_strategy("main.py", kPyGenericMethod, "lsp_generic_method");
|
||||
}
|
||||
|
||||
TEST(repro_lsp_py_method_union) {
|
||||
return assert_lsp_strategy("main.py", kPyMethodUnion, "lsp_method_union");
|
||||
}
|
||||
|
||||
/* ── Suite ───────────────────────────────────────────────────────────────── */
|
||||
|
||||
SUITE(repro_lsp_go_py) {
|
||||
RUN_TEST(repro_lsp_go_direct);
|
||||
RUN_TEST(repro_lsp_go_type_dispatch);
|
||||
RUN_TEST(repro_lsp_go_embed_dispatch);
|
||||
RUN_TEST(repro_lsp_go_interface_resolve);
|
||||
RUN_TEST(repro_lsp_go_interface_dispatch);
|
||||
RUN_TEST(repro_lsp_go_strategy_cross_file);
|
||||
RUN_TEST(repro_lsp_go_unresolved);
|
||||
|
||||
RUN_TEST(repro_lsp_py_direct);
|
||||
RUN_TEST(repro_lsp_py_constructor);
|
||||
RUN_TEST(repro_lsp_py_method);
|
||||
RUN_TEST(repro_lsp_py_super);
|
||||
RUN_TEST(repro_lsp_py_super_init);
|
||||
RUN_TEST(repro_lsp_py_module_attr);
|
||||
RUN_TEST(repro_lsp_py_module_attr_unresolved);
|
||||
RUN_TEST(repro_lsp_py_dict_dispatch);
|
||||
RUN_TEST(repro_lsp_py_operator_dunder);
|
||||
RUN_TEST(repro_lsp_py_builtin);
|
||||
RUN_TEST(repro_lsp_py_builtin_constructor);
|
||||
RUN_TEST(repro_lsp_py_builtin_method);
|
||||
RUN_TEST(repro_lsp_py_generic_method);
|
||||
RUN_TEST(repro_lsp_py_method_union);
|
||||
}
|
||||
@@ -68,8 +68,11 @@ extern void suite_repro_invariant_lsp_rescue(void);
|
||||
extern void suite_repro_invariant_discovery_fqn(void);
|
||||
/* Per-grammar invariant batteries (extract-clean/labels/fqn/ranges/callable-sourcing) */
|
||||
extern void suite_repro_grammar_core(void);
|
||||
extern void suite_repro_grammar_scripting(void);
|
||||
extern void suite_repro_grammar_functional(void);
|
||||
/* Per-LSP-pass resolution-strategy invariants */
|
||||
extern void suite_repro_lsp_c_cpp(void);
|
||||
extern void suite_repro_lsp_go_py(void);
|
||||
|
||||
int main(void) {
|
||||
/* Unbuffered: a reproduction may crash/_exit (or a sanitizer may _exit on a
|
||||
@@ -122,7 +125,10 @@ int main(void) {
|
||||
RUN_SUITE(repro_invariant_lsp_rescue);
|
||||
RUN_SUITE(repro_invariant_discovery_fqn);
|
||||
RUN_SUITE(repro_grammar_core);
|
||||
RUN_SUITE(repro_grammar_scripting);
|
||||
RUN_SUITE(repro_grammar_functional);
|
||||
RUN_SUITE(repro_lsp_c_cpp);
|
||||
RUN_SUITE(repro_lsp_go_py);
|
||||
|
||||
TEST_SUMMARY();
|
||||
}
|
||||
Reference in new issue
Block a user