1acd87912f
Java enum methods already resolve on main: find_class_member_body descends into enum_body_declarations (Java-gated) while find_class_body keeps returning enum_body, which is what extract_enum_members needs to reach the constants — they are siblings of enum_body_declarations, not children. That distinction was unguarded. java_enum_dedup_preserves_calls_issue1234 asserted the methods and the absence of duplicate Function defs, but never asserted the constants, so collapsing the two lookups would have passed the suite while silently dropping every enum constant from the graph. PR #984 proposed exactly that collapse — redirecting the shared find_class_body — which is what surfaced the gap. Verified binding: pointing extract_enum_members at find_class_member_body reddens the new Variable assertions and nothing else. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> Co-authored-by: sahil-mangla <manglasahil2017@gmail.com>
5821 lines
228 KiB
C
5821 lines
228 KiB
C
/*
|
|
* test_extraction.c — Regression tests for the extraction module.
|
|
*
|
|
* Port of internal/cbm/regression_test.go (1282 LOC, ~80 test cases).
|
|
* Exercises cbm_extract_file() on code snippets across 30+ languages,
|
|
* verifying definitions, calls, and imports are correctly extracted.
|
|
*/
|
|
#include "test_framework.h"
|
|
#include "cbm.h"
|
|
#include "../src/foundation/compat.h" /* cbm_clock_gettime (wide-flat scaling guard) */
|
|
#include "../src/foundation/compat_fs.h"
|
|
#include <time.h>
|
|
#include "macro_table.h"
|
|
#include "iris_export_xml.h"
|
|
|
|
/* ── Helpers ───────────────────────────────────────────────────── */
|
|
|
|
/* Check if any definition with the given label has the given name. */
|
|
static int has_def(CBMFileResult *r, const char *label, const char *name) {
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (strcmp(r->defs.items[i].label, label) == 0 && strcmp(r->defs.items[i].name, name) == 0)
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Check if any definition has the given name (any label). */
|
|
static int has_def_any(CBMFileResult *r, const char *name) {
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (strcmp(r->defs.items[i].name, name) == 0)
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Check if any call to the given callee exists. */
|
|
static int has_call(CBMFileResult *r, const char *callee) {
|
|
for (int i = 0; i < r->calls.count; i++) {
|
|
if (strstr(r->calls.items[i].callee_name, callee) != NULL)
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Check if any import with the given module path exists. */
|
|
static int __attribute__((unused)) has_import(CBMFileResult *r, const char *path_substr) {
|
|
for (int i = 0; i < r->imports.count; i++) {
|
|
if (r->imports.items[i].module_path &&
|
|
strstr(r->imports.items[i].module_path, path_substr) != NULL)
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Count definitions with a given label. */
|
|
/* Check for a definition with the given qualified name. Distinct from
|
|
* find_def_by_name, which returns the first match by NAME and so cannot tell two
|
|
* same-named definitions in different scopes apart — exactly the case that
|
|
* attrpath qualification exists to separate. */
|
|
static int has_def_qn(CBMFileResult *r, const char *qn) {
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (r->defs.items[i].qualified_name && strcmp(r->defs.items[i].qualified_name, qn) == 0)
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int count_defs_with_label(CBMFileResult *r, const char *label) {
|
|
int count = 0;
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (strcmp(r->defs.items[i].label, label) == 0)
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
/* Convenience: extract, assert no error, return result. Caller frees. */
|
|
static CBMFileResult *extract(const char *src, CBMLanguage lang, const char *proj,
|
|
const char *path) {
|
|
CBMFileResult *r = cbm_extract_file(src, (int)strlen(src), lang, proj, path, 0, NULL, NULL);
|
|
return r;
|
|
}
|
|
|
|
/* As extract(), but threads an ObjectScript macro table through. */
|
|
static CBMFileResult *extract_with_macros(const char *src, CBMLanguage lang, const char *proj,
|
|
const char *path, const CBMMacroTable *mt) {
|
|
CBMFileResult *r =
|
|
cbm_extract_file_ex(src, (int)strlen(src), lang, proj, path, 0, NULL, NULL, mt, NULL);
|
|
return r;
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Group A: OOP Languages
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
/* --- R: box::use imports (#218) + module$fn calls (#219) --- */
|
|
TEST(extract_r_box_use_imports_issue218) {
|
|
CBMFileResult *r = extract("box::use(\n"
|
|
" shiny[moduleServer, NS],\n"
|
|
" app/logic/validation[validate_input],\n"
|
|
")\n"
|
|
"library(dplyr)\n"
|
|
"source(\"helpers.R\")\n",
|
|
CBM_LANG_R, "t", "app.R");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
/* box::use specs → one IMPORTS edge per module (symbol list stripped). */
|
|
ASSERT(has_import(r, "shiny"));
|
|
ASSERT(has_import(r, "app/logic/validation"));
|
|
/* base-R imports work too. */
|
|
ASSERT(has_import(r, "dplyr"));
|
|
ASSERT(has_import(r, "helpers"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(extract_r_dollar_call_issue219) {
|
|
CBMFileResult *r = extract("validation$validate_input(x)\n", CBM_LANG_R, "t", "app.R");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
/* module$fn() now produces a CALLS edge (was silently dropped). */
|
|
ASSERT(has_call(r, "validation.validate_input"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- TS: object-literal arrow methods from a factory (Zustand, #341) --- */
|
|
TEST(extract_ts_factory_object_methods_issue341) {
|
|
CBMFileResult *r = extract("export function createItemActions(set, get) {\n"
|
|
" return {\n"
|
|
" addItem: (type, id) => { return 1; },\n"
|
|
" moveItem: (id, target) => { return 2; },\n"
|
|
" deleteItem: (id) => { return 3; },\n"
|
|
" };\n"
|
|
"}\n",
|
|
CBM_LANG_TYPESCRIPT, "t", "item-actions.ts");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
/* The factory itself + each returned arrow method are Function nodes. */
|
|
ASSERT(has_def_any(r, "createItemActions"));
|
|
ASSERT(has_def_any(r, "addItem"));
|
|
ASSERT(has_def_any(r, "moveItem"));
|
|
ASSERT(has_def_any(r, "deleteItem"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- C/C++ preprocessor macros become Macro nodes (#375) --- */
|
|
TEST(extract_c_macros_issue375) {
|
|
CBMFileResult *r = extract("#define SIMPLE_MACRO 1\n"
|
|
"#define FN_MACRO(x) (2 * (x))\n"
|
|
"#define EMPTY_MACRO\n"
|
|
"int main(void) { return FN_MACRO(SIMPLE_MACRO); }\n",
|
|
CBM_LANG_C, "p", "macros.c");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Macro", "SIMPLE_MACRO"));
|
|
ASSERT(has_def(r, "Macro", "FN_MACRO"));
|
|
ASSERT(has_def(r, "Macro", "EMPTY_MACRO"));
|
|
ASSERT(has_def(r, "Function", "main")); /* macros don't displace function defs */
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(extract_cpp_macros_issue375) {
|
|
CBMFileResult *r = extract("#define MAX(a, b) ((a) > (b) ? (a) : (b))\n"
|
|
"#define PI 3.14159\n"
|
|
"namespace n {\n"
|
|
"int f() { return MAX(1, 2); }\n"
|
|
"}\n",
|
|
CBM_LANG_CPP, "p", "macros.cpp");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Macro", "MAX"));
|
|
ASSERT(has_def(r, "Macro", "PI"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* #1071: a function-like macro invocation whose argument is a TYPE token
|
|
* (SYNTH_ALLOC_ARRAY(char, n)) makes tree-sitter's C++ grammar emit an ERROR
|
|
* node — it parses `char` in expression position — which cbm_collect_error_regions
|
|
* records as a `parse_partial` coverage gap, even though the file is a valid,
|
|
* in-file macro use with nothing actually missing from the graph. */
|
|
TEST(extract_cpp_functionlike_macro_type_arg_no_false_parse_partial_issue1071) {
|
|
CBMFileResult *r = extract("#include <cstddef>\n"
|
|
"#include <cstdlib>\n"
|
|
"\n"
|
|
"#define SYNTH_ALLOC_ARRAY(Type, Count) \\\n"
|
|
" ((Type*)std::malloc(sizeof(Type) * (Count)))\n"
|
|
"\n"
|
|
"struct Buffer {\n"
|
|
" char* data;\n"
|
|
" std::size_t size;\n"
|
|
"};\n"
|
|
"\n"
|
|
"Buffer make_buffer(std::size_t n) {\n"
|
|
" Buffer b;\n"
|
|
" b.data = SYNTH_ALLOC_ARRAY(char, n);\n"
|
|
" b.size = n;\n"
|
|
" return b;\n"
|
|
"}\n",
|
|
CBM_LANG_CPP, "t", "alloc.cpp");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->parse_incomplete); /* benign in-body macro call — not a coverage gap */
|
|
ASSERT(has_def(r, "Function", "make_buffer"));
|
|
ASSERT(has_def(r, "Macro", "SYNTH_ALLOC_ARRAY"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* #1071 guard: the suppression must be tight. A REAL parse error inside a
|
|
* function (not a macro call) must STILL be flagged, and a top-level macro
|
|
* invocation is covered by extract_cpp_preproc_macro_generated_callable_skipped_issue949. */
|
|
TEST(extract_cpp_real_in_body_error_still_flagged_issue1071) {
|
|
/* `int x = ;` is a genuine syntax error inside foo()'s body — no macro
|
|
* involved, so the coverage gap must not be suppressed. */
|
|
CBMFileResult *r = extract("int foo() {\n"
|
|
" int x = ;\n"
|
|
" return x;\n"
|
|
"}\n",
|
|
CBM_LANG_CPP, "t", "broken.cpp");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_TRUE(r->parse_incomplete); /* real gap stays reported */
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- GDScript: AST -> graph visitor (Godot, #186) --- */
|
|
TEST(extract_gdscript_issue186) {
|
|
CBMFileResult *r = extract("extends Node\n"
|
|
"class_name Player\n"
|
|
"\n"
|
|
"var health = 100\n"
|
|
"\n"
|
|
"func _ready():\n"
|
|
" take_damage(10)\n"
|
|
"\n"
|
|
"func take_damage(amount):\n"
|
|
" health -= amount\n"
|
|
"\n"
|
|
"class Inner:\n"
|
|
" func helper():\n"
|
|
" pass\n",
|
|
CBM_LANG_GDSCRIPT, "game", "player.gd");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "_ready"));
|
|
ASSERT(has_def(r, "Function", "take_damage"));
|
|
ASSERT(has_def(r, "Class", "Inner"));
|
|
ASSERT(has_call(r, "take_damage"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- PowerShell: AST -> graph visitor (#35) --- */
|
|
TEST(extract_powershell_issue35) {
|
|
CBMFileResult *r = extract("function Get-Greeting {\n"
|
|
" param($Name)\n"
|
|
" Write-Output \"Hello $Name\"\n"
|
|
"}\n"
|
|
"\n"
|
|
"function Set-Config {\n"
|
|
" Get-Greeting -Name 'World'\n"
|
|
"}\n",
|
|
CBM_LANG_POWERSHELL, "ops", "greet.ps1");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(count_defs_with_label(r, "Function") >= 2);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Luau: AST -> graph visitor (Roblox, #39) --- */
|
|
TEST(extract_luau_issue39) {
|
|
CBMFileResult *r = extract("local function add(a, b)\n"
|
|
" return a + b\n"
|
|
"end\n"
|
|
"\n"
|
|
"function multiply(a, b)\n"
|
|
" return add(a, a) * b\n"
|
|
"end\n",
|
|
CBM_LANG_LUAU, "game", "math.luau");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(count_defs_with_label(r, "Function") >= 2);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- QML: AST -> graph visitor (Qt, #42) --- */
|
|
TEST(extract_qml_issue42) {
|
|
CBMFileResult *r = extract("import QtQuick 2.15\n"
|
|
"\n"
|
|
"Rectangle {\n"
|
|
" id: root\n"
|
|
" width: 100\n"
|
|
" property int counter: 0\n"
|
|
" signal clicked(int value)\n"
|
|
"\n"
|
|
" function increment() {\n"
|
|
" counter += 1\n"
|
|
" compute(counter)\n"
|
|
" }\n"
|
|
"\n"
|
|
" function compute(n) {\n"
|
|
" return n * 2\n"
|
|
" }\n"
|
|
"}\n",
|
|
CBM_LANG_QML, "app", "Main.qml");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "increment"));
|
|
ASSERT(has_def(r, "Function", "compute"));
|
|
ASSERT(has_call(r, "compute"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- CFML script dialect: .cfc components (Lucee/ColdFusion, #38) --- */
|
|
TEST(extract_cfscript_issue38) {
|
|
CBMFileResult *r = extract("component {\n"
|
|
" public function getUser(numeric id) {\n"
|
|
" return loadUser(id);\n"
|
|
" }\n"
|
|
" function loadUser(id) {\n"
|
|
" return id * 2;\n"
|
|
" }\n"
|
|
"}\n",
|
|
CBM_LANG_CFSCRIPT, "app", "User.cfc");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(count_defs_with_label(r, "Function") >= 2);
|
|
ASSERT(has_def(r, "Function", "getUser"));
|
|
ASSERT(has_def(r, "Function", "loadUser"));
|
|
ASSERT(has_call(r, "loadUser"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- CFML tag dialect: .cfm templates with <cffunction> (#38) --- */
|
|
TEST(extract_cfml_tag_issue38) {
|
|
CBMFileResult *r = extract("<cffunction name=\"greet\" returntype=\"string\">\n"
|
|
" <cfargument name=\"who\" type=\"string\">\n"
|
|
" <cfreturn \"Hello \" & arguments.who>\n"
|
|
"</cffunction>\n",
|
|
CBM_LANG_CFML, "app", "index.cfm");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT(has_def(r, "Function", "greet"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Helm / Go template: named templates + include calls (#338) --- */
|
|
TEST(extract_helm_templates_issue338) {
|
|
CBMFileResult *r = extract("{{- define \"chart.fullname\" -}}\n"
|
|
"{{- .Release.Name -}}\n"
|
|
"{{- end -}}\n"
|
|
"\n"
|
|
"{{- define \"chart.labels\" -}}\n"
|
|
"app: {{ include \"chart.fullname\" . }}\n"
|
|
"chart: {{ template \"chart.fullname\" . }}\n"
|
|
"{{- end -}}\n",
|
|
CBM_LANG_GOTEMPLATE, "chart", "templates/_helpers.tpl");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
/* define -> Function nodes */
|
|
ASSERT(has_def(r, "Function", "chart.fullname"));
|
|
ASSERT(has_def(r, "Function", "chart.labels"));
|
|
/* include / template -> CALLS to the named template (not to "include") */
|
|
ASSERT(has_call(r, "chart.fullname"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Helm values.yaml: top-level keys only, no leaf flood (#338) --- */
|
|
TEST(extract_helm_values_toplevel_issue338) {
|
|
CBMFileResult *r = extract("image:\n"
|
|
" repository: nginx\n"
|
|
" tag: latest\n"
|
|
"replicaCount: 3\n"
|
|
"service:\n"
|
|
" port: 80\n",
|
|
CBM_LANG_YAML, "chart", "values.yaml");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Variable", "image"));
|
|
ASSERT(has_def(r, "Variable", "replicaCount"));
|
|
ASSERT(has_def(r, "Variable", "service"));
|
|
/* Nested leaf keys must NOT explode into separate nodes. */
|
|
ASSERT(!has_def(r, "Variable", "repository"));
|
|
ASSERT(!has_def(r, "Variable", "tag"));
|
|
ASSERT(!has_def(r, "Variable", "port"));
|
|
ASSERT_EQ(count_defs_with_label(r, "Variable"), 3);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Java --- */
|
|
TEST(java_class) {
|
|
CBMFileResult *r = extract(
|
|
"public class Animal { private String name; public String getName() { return name; } }",
|
|
CBM_LANG_JAVA, "t", "Animal.java");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Class", "Animal"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(java_method) {
|
|
CBMFileResult *r = extract(
|
|
"public class Svc { public void doWork() {} public int compute(int x) { return x; } }",
|
|
CBM_LANG_JAVA, "t", "Svc.java");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Method", "doWork"));
|
|
ASSERT(has_def(r, "Method", "compute"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(java_interface) {
|
|
CBMFileResult *r =
|
|
extract("public interface Repository { void save(Object o); Object findById(long id); }",
|
|
CBM_LANG_JAVA, "t", "Repo.java");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def_any(r, "Repository"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* Regression for #1234: Java interface/enum methods were emitted as both a
|
|
* Method node (correct, via extract_class_methods) and a duplicate Function
|
|
* node (incorrect, via walk_defs). Prevention in push_class_body_children
|
|
* (gated to Java) recognizes interface_body and enum_body as class body
|
|
* containers, stopping the fallback path from re-walking method_declaration
|
|
* children as top-level functions. */
|
|
TEST(java_interface_no_duplicate_function_issue1234) {
|
|
CBMFileResult *r =
|
|
extract("public interface MarketplaceService {\n"
|
|
" ReservationDTO createReservation(Authentication auth, RequestDTO req);\n"
|
|
" void cancelReservation(long id);\n"
|
|
"}\n",
|
|
CBM_LANG_JAVA, "t", "MarketplaceService.java");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
|
|
ASSERT(has_def(r, "Interface", "MarketplaceService"));
|
|
ASSERT(has_def(r, "Method", "createReservation"));
|
|
ASSERT(has_def(r, "Method", "cancelReservation"));
|
|
ASSERT_EQ(count_defs_with_label(r, "Function"), 0);
|
|
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(java_enum_dedup_preserves_calls_issue1234) {
|
|
CBMFileResult *r =
|
|
extract("package app;\n\nenum Day {\n"
|
|
" MON, TUE, WED, THU, FRI, SAT, SUN;\n\n"
|
|
" public boolean isWeekend() { return this == SAT || this == SUN; }\n"
|
|
" public String label() { return name().toLowerCase(); }\n}\n\n"
|
|
"class DayUtil {\n"
|
|
" static String describe(Day d) {\n"
|
|
" return d.label() + (d.isWeekend() ? \"(rest)\" : \"(work)\");\n }\n}\n",
|
|
CBM_LANG_JAVA, "t", "Day.java");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
|
|
ASSERT(has_def(r, "Enum", "Day"));
|
|
ASSERT(has_def(r, "Method", "isWeekend"));
|
|
ASSERT(has_def(r, "Method", "label"));
|
|
/* The enum CONSTANTS must survive alongside the methods. Reaching the
|
|
* methods means descending into enum_body_declarations, and the tempting
|
|
* way to do that -- redirecting the shared find_class_body -- also makes
|
|
* the constants unreachable, because they are siblings of that node rather
|
|
* than children. find_class_member_body exists to descend for members only
|
|
* and leave find_class_body (which extract_enum_members uses) alone; these
|
|
* two assertions are what stop that distinction being collapsed again. */
|
|
ASSERT(has_def(r, "Variable", "MON"));
|
|
ASSERT(has_def(r, "Variable", "SUN"));
|
|
ASSERT(has_def(r, "Class", "DayUtil"));
|
|
ASSERT(has_def(r, "Method", "describe"));
|
|
ASSERT_EQ(count_defs_with_label(r, "Function"), 0);
|
|
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* Regression for #279: a Java class declaring both `extends` and
|
|
* `implements` must produce one INHERITS edge per base — the extends parent
|
|
* AND every implements interface — with bare type names (not the keyword
|
|
* text "extends Bar" / "implements Baz, Qux"). Before the fix:
|
|
* 1) the field loop returned on the first match → only the superclass
|
|
* was emitted, the interfaces were dropped.
|
|
* 2) the emitted name was the full field text including the keyword. */
|
|
TEST(java_class_extends_and_implements) {
|
|
CBMFileResult *r = extract("public class DefaultLinkTool extends DefaultDiagramTool implements "
|
|
"ILinkTool, Closeable { }",
|
|
CBM_LANG_JAVA, "t", "DefaultLinkTool.java");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
|
|
/* Find the class def and inspect its base_classes list. */
|
|
CBMDefinition *cls = NULL;
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (strcmp(r->defs.items[i].label, "Class") == 0 &&
|
|
strcmp(r->defs.items[i].name, "DefaultLinkTool") == 0) {
|
|
cls = &r->defs.items[i];
|
|
break;
|
|
}
|
|
}
|
|
ASSERT_NOT_NULL(cls);
|
|
ASSERT_NOT_NULL(cls->base_classes);
|
|
|
|
bool saw_super = false;
|
|
bool saw_iface_a = false;
|
|
bool saw_iface_b = false;
|
|
for (const char **b = cls->base_classes; *b; b++) {
|
|
/* The keyword-text bug would surface as "extends ..." or
|
|
* "implements ..." literally inside one of the entries. */
|
|
ASSERT_NULL(strstr(*b, "extends"));
|
|
ASSERT_NULL(strstr(*b, "implements"));
|
|
if (strcmp(*b, "DefaultDiagramTool") == 0)
|
|
saw_super = true;
|
|
if (strcmp(*b, "ILinkTool") == 0)
|
|
saw_iface_a = true;
|
|
if (strcmp(*b, "Closeable") == 0)
|
|
saw_iface_b = true;
|
|
}
|
|
ASSERT_TRUE(saw_super);
|
|
ASSERT_TRUE(saw_iface_a);
|
|
ASSERT_TRUE(saw_iface_b);
|
|
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* REPRODUCTION (RED until fixed) — Python `class Animal(Base):` must extract the
|
|
* BARE base name "Base", but extract_base_classes captures the whole
|
|
* `superclasses` argument_list text "(Base)" instead: collect_bases_from_field
|
|
* (internal/cbm/extract_defs.c) matches only type_identifier / generic_type /
|
|
* qualified_name / scoped_type_identifier / user_type, while tree-sitter-python
|
|
* uses a plain `identifier` node for the base — so no child matches and the
|
|
* raw-field fallback grabs the argument_list text "(Base)" (parens included).
|
|
* DOWNSTREAM SYMPTOM: that malformed name never resolves to the Base class node,
|
|
* so EVERY Python subclass yields ZERO INHERITS edges (observed in the P6
|
|
* graph-contract suite). Fix: have collect_bases_from_field accept `identifier`
|
|
* (or strip the argument_list parens). This test stays RED as the regression
|
|
* guard / reproduction until the fix lands — see CLAUDE.md "Bug Fixing —
|
|
* Reproduce-First". */
|
|
TEST(python_class_base_extracted_bare) {
|
|
CBMFileResult *r = extract("class Base:\n pass\n\n\nclass Animal(Base):\n pass\n",
|
|
CBM_LANG_PYTHON, "t", "models.py");
|
|
ASSERT_NOT_NULL(r);
|
|
|
|
CBMDefinition *cls = NULL;
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (r->defs.items[i].label && strcmp(r->defs.items[i].label, "Class") == 0 &&
|
|
r->defs.items[i].name && strcmp(r->defs.items[i].name, "Animal") == 0) {
|
|
cls = &r->defs.items[i];
|
|
break;
|
|
}
|
|
}
|
|
ASSERT_NOT_NULL(cls);
|
|
ASSERT_NOT_NULL(cls->base_classes); /* a subclass must record at least one base */
|
|
|
|
bool saw_bare_base = false;
|
|
for (const char **b = cls->base_classes; *b; b++) {
|
|
if (strcmp(*b, "Base") == 0) {
|
|
saw_bare_base = true;
|
|
}
|
|
}
|
|
/* CURRENTLY FAILS: base_classes holds "(Base)" (argument_list text), not the
|
|
* bare "Base" needed for INHERITS resolution. */
|
|
ASSERT_TRUE(saw_bare_base);
|
|
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- PHP --- */
|
|
TEST(php_class) {
|
|
CBMFileResult *r = extract("<?php\nclass User { public string $name; public function "
|
|
"getName(): string { return $this->name; } }",
|
|
CBM_LANG_PHP, "t", "User.php");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Class", "User"));
|
|
ASSERT(has_def(r, "Method", "getName"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(php_function) {
|
|
CBMFileResult *r =
|
|
extract("<?php\nfunction greet(string $name): string { return 'Hello ' . $name; }",
|
|
CBM_LANG_PHP, "t", "helpers.php");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "greet"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Ruby --- */
|
|
TEST(ruby_class) {
|
|
CBMFileResult *r = extract("class Animal\n def initialize(name)\n @name = name\n end\n "
|
|
"def speak\n puts @name\n end\nend\n",
|
|
CBM_LANG_RUBY, "t", "animal.rb");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Class", "Animal"));
|
|
ASSERT(has_def(r, "Method", "speak"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(ruby_module) {
|
|
CBMFileResult *r = extract("module Greetable\n def greet\n \"Hello\"\n end\nend\n",
|
|
CBM_LANG_RUBY, "t", "greetable.rb");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def_any(r, "Greetable"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- C# --- */
|
|
TEST(csharp_class) {
|
|
CBMFileResult *r = extract("namespace App { public class Service { public void Run() {} public "
|
|
"int Compute(int x) => x * 2; } }",
|
|
CBM_LANG_CSHARP, "t", "Service.cs");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Class", "Service"));
|
|
ASSERT(has_def(r, "Method", "Run"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(csharp_interface) {
|
|
CBMFileResult *r = extract("public interface IService { void Execute(); string GetStatus(); }",
|
|
CBM_LANG_CSHARP, "t", "IService.cs");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def_any(r, "IService"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Swift --- */
|
|
TEST(swift_class) {
|
|
CBMFileResult *r = extract("class Vehicle {\n var speed: Int = 0\n func accelerate() { "
|
|
"speed += 10 }\n func stop() { speed = 0 }\n}\n",
|
|
CBM_LANG_SWIFT, "t", "Vehicle.swift");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Class", "Vehicle"));
|
|
ASSERT(has_def(r, "Method", "accelerate"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(swift_protocol) {
|
|
/* A protocol requirement is a bodyless func inside a protocol body. Swift
|
|
* codebases are heavily protocol-driven, so the requirement is very often
|
|
* the declaration a reader is actually looking for — before this it was
|
|
* absent from the graph entirely. */
|
|
CBMFileResult *r = extract("protocol StudyRunning {\n func generate() -> String\n}\n",
|
|
CBM_LANG_SWIFT, "t", "StudyRunning.swift");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Interface", "StudyRunning"));
|
|
ASSERT(has_def(r, "Method", "generate"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Kotlin --- */
|
|
TEST(kotlin_function) {
|
|
CBMFileResult *r = extract("fun greet(name: String): String = \"Hello $name\"\nfun main() { "
|
|
"println(greet(\"World\")) }\n",
|
|
CBM_LANG_KOTLIN, "t", "main.kt");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "greet"));
|
|
ASSERT(has_def(r, "Function", "main"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(kotlin_class) {
|
|
CBMFileResult *r =
|
|
extract("class User(val name: String) {\n fun display(): String = \"User: $name\"\n}\n",
|
|
CBM_LANG_KOTLIN, "t", "User.kt");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Class", "User"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Scala --- */
|
|
TEST(scala_function) {
|
|
CBMFileResult *r =
|
|
extract("object Main {\n def greet(name: String): String = s\"Hello $name\"\n def "
|
|
"main(args: Array[String]): Unit = println(greet(\"World\"))\n}\n",
|
|
CBM_LANG_SCALA, "t", "Main.scala");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Method", "greet"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(scala_class) {
|
|
CBMFileResult *r =
|
|
extract("class Animal(val name: String) {\n def speak(): String = s\"I am $name\"\n}\n",
|
|
CBM_LANG_SCALA, "t", "Animal.scala");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Class", "Animal"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Dart --- */
|
|
TEST(dart_class) {
|
|
CBMFileResult *r = extract("class Animal {\n String name;\n Animal(this.name);\n String "
|
|
"speak() => 'I am $name';\n}\n",
|
|
CBM_LANG_DART, "t", "animal.dart");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Class", "Animal"));
|
|
ASSERT(has_def(r, "Method", "speak"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Groovy --- */
|
|
TEST(groovy_class) {
|
|
CBMFileResult *r =
|
|
extract("class Greeter {\n String name\n String greet() { \"Hello, $name\" }\n "
|
|
"static void main(args) { println new Greeter(name:'World').greet() }\n}\n",
|
|
CBM_LANG_GROOVY, "t", "Greeter.groovy");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Class", "Greeter"));
|
|
ASSERT(has_def(r, "Method", "greet"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Group B: Systems Languages
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
/* --- Rust --- */
|
|
TEST(rust_function) {
|
|
CBMFileResult *r =
|
|
extract("fn main() { println!(\"Hello\"); }\npub fn add(a: i32, b: i32) -> i32 { a + b }\n",
|
|
CBM_LANG_RUST, "t", "main.rs");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "main"));
|
|
ASSERT(has_def(r, "Function", "add"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(rust_struct) {
|
|
CBMFileResult *r = extract("pub struct Point { pub x: f64, pub y: f64 }\nimpl Point { pub fn "
|
|
"new(x: f64, y: f64) -> Self { Point { x, y } } }\n",
|
|
CBM_LANG_RUST, "t", "point.rs");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Struct", "Point"));
|
|
ASSERT(has_def(r, "Method", "new"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Go --- */
|
|
TEST(go_function) {
|
|
CBMFileResult *r = extract("package main\nfunc Greet(name string) string { return \"Hello, \" "
|
|
"+ name }\nfunc main() { Greet(\"World\") }\n",
|
|
CBM_LANG_GO, "t", "main.go");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "Greet"));
|
|
ASSERT(has_def(r, "Function", "main"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(go_struct) {
|
|
CBMFileResult *r = extract("package main\ntype Server struct { Host string; Port int }\nfunc "
|
|
"(s *Server) Start() error { return nil }\n",
|
|
CBM_LANG_GO, "t", "server.go");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Struct", "Server"));
|
|
ASSERT(has_def(r, "Method", "Start"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(go_interface) {
|
|
CBMFileResult *r =
|
|
extract("package main\ntype Handler interface { ServeHTTP() error; Close() }\n",
|
|
CBM_LANG_GO, "t", "handler.go");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def_any(r, "Handler"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Zig --- */
|
|
TEST(zig_function) {
|
|
CBMFileResult *r =
|
|
extract("const std = @import(\"std\");\npub fn add(a: i32, b: i32) i32 { return a + b; }\n",
|
|
CBM_LANG_ZIG, "t", "main.zig");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "add"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- C --- */
|
|
TEST(c_function) {
|
|
CBMFileResult *r =
|
|
extract("int add(int a, int b) { return a + b; }\nvoid greet() { printf(\"Hello\"); }\n",
|
|
CBM_LANG_C, "t", "math.c");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "add"));
|
|
ASSERT(has_def(r, "Function", "greet"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(c_struct) {
|
|
CBMFileResult *r =
|
|
extract("struct Point { int x; int y; };\nvoid init_point(struct Point *p) { p->x = 0; }\n",
|
|
CBM_LANG_C, "t", "point.c");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "init_point"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- C++ --- */
|
|
TEST(cpp_class) {
|
|
CBMFileResult *r = extract(
|
|
"class Widget {\npublic:\n void draw() {}\n int width() const { return 0; }\n};\n",
|
|
CBM_LANG_CPP, "t", "widget.cpp");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Class", "Widget"));
|
|
ASSERT(has_def(r, "Method", "draw"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Group C: Scripting / Dynamic Languages
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
/* --- Python --- */
|
|
TEST(python_function) {
|
|
CBMFileResult *r = extract(
|
|
"def greet(name):\n return f\"Hello {name}\"\n\ndef main():\n greet(\"World\")\n",
|
|
CBM_LANG_PYTHON, "t", "main.py");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "greet"));
|
|
ASSERT(has_def(r, "Function", "main"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(python_class) {
|
|
CBMFileResult *r =
|
|
extract("class Dog:\n def __init__(self, name):\n self.name = name\n def "
|
|
"speak(self):\n return f\"Woof from {self.name}\"\n",
|
|
CBM_LANG_PYTHON, "t", "dog.py");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Class", "Dog"));
|
|
ASSERT(has_def(r, "Method", "speak"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- JavaScript --- */
|
|
TEST(js_function) {
|
|
CBMFileResult *r =
|
|
extract("function greet(name) { return `Hello ${name}`; }\nconst add = (a, b) => a + b;\n",
|
|
CBM_LANG_JAVASCRIPT, "t", "util.js");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "greet"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(js_class) {
|
|
CBMFileResult *r =
|
|
extract("class Counter {\n constructor() { this.count = 0; }\n increment() { "
|
|
"this.count++; }\n get value() { return this.count; }\n}\n",
|
|
CBM_LANG_JAVASCRIPT, "t", "counter.js");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Class", "Counter"));
|
|
ASSERT(has_def(r, "Method", "increment"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- TypeScript --- */
|
|
TEST(ts_function) {
|
|
CBMFileResult *r = extract("export function greet(name: string): string { return `Hello "
|
|
"${name}`; }\nfunction helper(): void {}\n",
|
|
CBM_LANG_TYPESCRIPT, "t", "util.ts");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "greet"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(ts_class) {
|
|
CBMFileResult *r =
|
|
extract("class Service {\n private name: string;\n constructor(name: string) { this.name "
|
|
"= name; }\n getName(): string { return this.name; }\n}\n",
|
|
CBM_LANG_TYPESCRIPT, "t", "service.ts");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Class", "Service"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(body_tokens_type_identifier) {
|
|
CBMFileResult *r = extract("function serialize(obj: MyModel): SerializedResult {\n"
|
|
" const result: SerializedResult = new SerializedResult();\n"
|
|
" return result;\n"
|
|
"}\n",
|
|
CBM_LANG_TYPESCRIPT, "t", "serial.ts");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (strcmp(r->defs.items[i].name, "serialize") == 0) {
|
|
ASSERT_NOT_NULL(r->defs.items[i].body_tokens);
|
|
ASSERT(strstr(r->defs.items[i].body_tokens, "SerializedResult") != NULL);
|
|
break;
|
|
}
|
|
}
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Lua --- */
|
|
TEST(lua_function) {
|
|
CBMFileResult *r = extract(
|
|
"function greet(name)\n return \"Hello \" .. name\nend\nlocal function helper() end\n",
|
|
CBM_LANG_LUA, "t", "main.lua");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "greet"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Bash --- */
|
|
TEST(bash_function) {
|
|
CBMFileResult *r =
|
|
extract("greet() {\n echo \"Hello $1\"\n}\nmain() {\n greet \"World\"\n}\n",
|
|
CBM_LANG_BASH, "t", "script.sh");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "greet"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Perl --- */
|
|
TEST(perl_function) {
|
|
CBMFileResult *r = extract("sub greet {\n my ($name) = @_;\n return \"Hello "
|
|
"$name\";\n}\nsub main { greet(\"World\"); }\n",
|
|
CBM_LANG_PERL, "t", "main.pl");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "greet"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- R --- */
|
|
TEST(r_function) {
|
|
CBMFileResult *r = extract("add <- function(x, y) x + y\nmultiply <- function(x, y) x * y\n",
|
|
CBM_LANG_R, "t", "math.R");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "add"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Group D: Functional Languages
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
/* --- Elixir --- */
|
|
TEST(elixir_function) {
|
|
CBMFileResult *r = extract("defmodule Greeter do\n def greet(name), do: \"Hello #{name}\"\n "
|
|
"defp helper, do: nil\nend\n",
|
|
CBM_LANG_ELIXIR, "t", "greeter.ex");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "greet"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Haskell --- */
|
|
TEST(haskell_function) {
|
|
CBMFileResult *r = extract("add :: Int -> Int -> Int\nadd x y = x + y\n\nmultiply :: Int -> "
|
|
"Int -> Int\nmultiply x y = x * y\n",
|
|
CBM_LANG_HASKELL, "t", "Math.hs");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "add"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- OCaml --- */
|
|
TEST(ocaml_function) {
|
|
CBMFileResult *r =
|
|
extract("let add x y = x + y\nlet multiply x y = x * y\n", CBM_LANG_OCAML, "t", "math.ml");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "add"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Erlang --- */
|
|
TEST(erlang_function) {
|
|
CBMFileResult *r = extract(
|
|
"-module(math).\n-export([add/2]).\nadd(X, Y) -> X + Y.\nmultiply(X, Y) -> X * Y.\n",
|
|
CBM_LANG_ERLANG, "t", "math.erl");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "add"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Group E: Markup / Config / Helper Languages
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
/* --- YAML --- */
|
|
TEST(yaml_variables) {
|
|
CBMFileResult *r =
|
|
extract("name: myapp\nversion: 1.0\ndatabase:\n host: localhost\n port: 5432\n",
|
|
CBM_LANG_YAML, "t", "config.yml");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
/* YAML should extract top-level keys as variables */
|
|
ASSERT_GT(r->defs.count, 0);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- HCL --- */
|
|
TEST(hcl_blocks) {
|
|
CBMFileResult *r = extract("resource \"aws_instance\" \"web\" {\n ami = \"abc-123\"\n "
|
|
"instance_type = \"t2.micro\"\n}\n"
|
|
"variable \"region\" {\n default = \"us-east-1\"\n}\n",
|
|
CBM_LANG_HCL, "t", "main.tf");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GT(r->defs.count, 0);
|
|
/* Block labels are folded into the name so blocks are distinguishable (#337). */
|
|
ASSERT(has_def(r, "Class", "resource.aws_instance.web"));
|
|
ASSERT(has_def(r, "Class", "variable.region"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- SQL --- */
|
|
TEST(sql_create_table) {
|
|
CBMFileResult *r = extract("CREATE TABLE users (\n id INTEGER PRIMARY KEY,\n name TEXT NOT "
|
|
"NULL\n);\nCREATE VIEW active_users AS SELECT * FROM users;\n",
|
|
CBM_LANG_SQL, "t", "schema.sql");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Dockerfile --- */
|
|
TEST(dockerfile_stages) {
|
|
CBMFileResult *r = extract(
|
|
"FROM node:18 AS builder\nRUN npm install\nFROM node:18-slim\nCOPY --from=builder /app .\n",
|
|
CBM_LANG_DOCKERFILE, "t", "Dockerfile");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Group F: Scientific / Math Languages
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
/* --- MATLAB --- */
|
|
TEST(matlab_function) {
|
|
CBMFileResult *r =
|
|
extract("function y = square(x)\n y = x.^2;\nend\n", CBM_LANG_MATLAB, "t", "square.m");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "square"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Lean 4 --- */
|
|
TEST(lean_function) {
|
|
CBMFileResult *r =
|
|
extract("def add (x y : Nat) : Nat := x + y\n", CBM_LANG_LEAN, "t", "Math.lean");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "add"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- FORM --- */
|
|
TEST(form_procedure) {
|
|
CBMFileResult *r = extract("#procedure doSomething\n id x = y;\n#endprocedure\n",
|
|
CBM_LANG_FORM, "t", "test.frm");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "doSomething"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Wolfram --- */
|
|
TEST(wolfram_function) {
|
|
CBMFileResult *r =
|
|
extract("square[x_] := x^2\nadd[x_, y_] := x + y\n", CBM_LANG_WOLFRAM, "t", "math.wl");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "square"));
|
|
ASSERT(has_def(r, "Function", "add"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Magma --- */
|
|
TEST(magma_function) {
|
|
CBMFileResult *r = extract("function Factorial(n)\n if n le 1 then\n return 1;\n end "
|
|
"if;\n return n * Factorial(n - 1);\nend function;\n",
|
|
CBM_LANG_MAGMA, "t", "test.m");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "Factorial"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Group G: v0.5 Expansion Languages
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
/* --- F# --- */
|
|
TEST(fsharp_function) {
|
|
/* Go test only asserts >=1 def — F# name extraction is incomplete */
|
|
CBMFileResult *r = extract("module Greeter\nlet greet name = sprintf \"Hello %s\" name\n",
|
|
CBM_LANG_FSHARP, "t", "Greeter.fs");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->defs.count, 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Julia --- */
|
|
TEST(julia_function) {
|
|
CBMFileResult *r = extract("function add(x, y)\n x + y\nend\nadd2(x, y) = x + y\n",
|
|
CBM_LANG_JULIA, "t", "math.jl");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "add"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Elm --- */
|
|
TEST(elm_function) {
|
|
CBMFileResult *r =
|
|
extract("add x y = x + y\nmultiply x y = x * y\n", CBM_LANG_ELM, "t", "Math.elm");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "add"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Nix --- */
|
|
TEST(nix_function) {
|
|
CBMFileResult *r =
|
|
extract("{ pkgs ? import <nixpkgs> {} }:\nlet\n hello = pkgs.writeShellScriptBin "
|
|
"\"hello\" ''echo hello'';\nin { inherit hello; }\n",
|
|
CBM_LANG_NIX, "t", "default.nix");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* A Nix file's root expression is normally itself a function (`{ pkgs, lib, ... }:`)
|
|
* — the near-universal library/module header. Definitions must survive that header.
|
|
* Each shape below is asserted separately so a regression names the shape it broke.
|
|
* `nix_function` above deliberately stays as-is: its binding is an application, not
|
|
* a function, so it pins the "parses, no def" case and cannot cover this. */
|
|
TEST(nix_defs_in_let_rooted_file) {
|
|
CBMFileResult *r = extract("let\n alpha = x: x + 1;\n beta = { a, b }: a + b;\nin\n"
|
|
"{ inherit alpha beta; }\n",
|
|
CBM_LANG_NIX, "t", "bare.nix");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "alpha"));
|
|
ASSERT(has_def(r, "Function", "beta"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(nix_defs_in_attrset_rooted_file) {
|
|
CBMFileResult *r = extract("{\n epsilon = x: x + 1;\n zeta = { a, b }: a + b;\n}\n",
|
|
CBM_LANG_NIX, "t", "attrset.nix");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "epsilon"));
|
|
ASSERT(has_def(r, "Function", "zeta"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(nix_defs_in_nested_let) {
|
|
CBMFileResult *r = extract("let\n outer =\n let theta = x: x + 1;\n in theta;\n"
|
|
"in\n{ inherit outer; }\n",
|
|
CBM_LANG_NIX, "t", "nested.nix");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "theta"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* The regression this suite previously could not see: the root `{ prelude }:` matches
|
|
* nix_func_types, resolves no name of its own, and — before the fix — terminated the
|
|
* walk, so nothing below the header was ever visited. */
|
|
TEST(nix_defs_survive_function_header_let) {
|
|
CBMFileResult *r = extract("{ prelude }:\nlet\n gamma = x: x + 1;\n"
|
|
" delta = { a, b }: a + b;\nin\n{ inherit gamma delta; }\n",
|
|
CBM_LANG_NIX, "t", "wrapped.nix");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "gamma"));
|
|
ASSERT(has_def(r, "Function", "delta"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(nix_defs_survive_function_header_attrset) {
|
|
CBMFileResult *r = extract("{ prelude }:\n{\n eta = x: x + 1;\n}\n", CBM_LANG_NIX, "t",
|
|
"wrapped_attrset.nix");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "eta"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* A curried header — `final: prev: { … }` is the standard nixpkgs overlay signature, and
|
|
* the most common multi-arm header in the ecosystem. Two nested function_expressions sit
|
|
* between the file root and the body. */
|
|
TEST(nix_defs_survive_curried_header) {
|
|
CBMFileResult *r =
|
|
extract("final: prev: {\n kappa = x: x + 1;\n}\n", CBM_LANG_NIX, "t", "overlay.nix");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "kappa"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* A Nix binding's name is a PATH, and the extractor previously took only its first
|
|
* segment. Convention here matches C++ namespaces — `ns::serialize` is name
|
|
* `serialize`, QN `proj.file.ns.serialize` — so a Nix binding is name = leaf
|
|
* segment, QN = enclosing scope + leaf. */
|
|
TEST(nix_attrset_scope_disambiguates_leaf_names) {
|
|
CBMFileResult *r =
|
|
extract("{\n setA = { dup = x: x + 1; };\n setB = { dup = y: y + 2; };\n}\n",
|
|
CBM_LANG_NIX, "t", "collide.nix");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
/* Both survive. Unqualified, these shared one QN, so the second definition —
|
|
* and every CALLS edge sourced from it — was silently discarded at write. */
|
|
ASSERT(count_defs_with_label(r, "Function") == 2);
|
|
ASSERT(has_def_qn(r, "t.collide.setA.dup"));
|
|
ASSERT(has_def_qn(r, "t.collide.setB.dup"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* `a.b.fn = …` is sugar for `a = { b = { fn = …; }; }`. Both spellings must yield
|
|
* the same name and the same QN; the leading segments are scope, not name. */
|
|
TEST(nix_dotted_attrpath_qualifies_like_nested) {
|
|
CBMFileResult *r = extract("{\n wrap.deep.fn = z: z + 1;\n}\n", CBM_LANG_NIX, "t", "d.nix");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "fn"));
|
|
ASSERT(has_def_qn(r, "t.d.wrap.deep.fn"));
|
|
|
|
CBMFileResult *n =
|
|
extract("{\n wrap = { deep = { fn = z: z + 1; }; };\n}\n", CBM_LANG_NIX, "t", "d.nix");
|
|
ASSERT_NOT_NULL(n);
|
|
ASSERT_FALSE(n->has_error);
|
|
/* The equality that makes this a correctness fix rather than a preference. */
|
|
ASSERT(has_def_qn(n, "t.d.wrap.deep.fn"));
|
|
cbm_free_result(n);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* A quoted segment is an ordinary name that merely needs quoting in source. The
|
|
* delimiters are not part of it, and leaving them in means every consumer keying
|
|
* on the name has to know to re-quote. */
|
|
TEST(nix_quoted_attr_name_strips_quotes) {
|
|
CBMFileResult *r = extract("{\n \"kebab-case\" = a: a;\n svc.\"my.name\" = b: b;\n}\n",
|
|
CBM_LANG_NIX, "t", "q.nix");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "kebab-case"));
|
|
ASSERT_FALSE(has_def_any(r, "\"kebab-case\""));
|
|
ASSERT(has_def_qn(r, "t.q.kebab-case"));
|
|
/* A quoted segment may itself contain dots; they are part of the name, not
|
|
* path separators, but the QN is a dotted string either way. */
|
|
ASSERT(has_def(r, "Function", "my.name"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* `"${x}" = …` has no statically knowable name. Minting it produces a def named
|
|
* `"${x}"` that nothing can look up or resolve a call against, so mint nothing —
|
|
* the same call the Makefile dot-prefix guard makes. */
|
|
TEST(nix_interpolated_attr_mints_no_def) {
|
|
CBMFileResult *r =
|
|
extract("{\n \"${dynamic}\" = a: a;\n fixed = b: b;\n}\n", CBM_LANG_NIX, "t", "i.nix");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "fixed"));
|
|
ASSERT(count_defs_with_label(r, "Function") == 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* Nix Variables. `nix_var_types` has always declared `binding`, but no Nix name
|
|
* resolver existed, so the count was unconditionally zero.
|
|
*
|
|
* Scope follows the rule every other language uses: extract_variables mints FILE
|
|
* scope and never locals — a C++ declaration inside a function body is not a
|
|
* Variable. For Nix, file scope is the `let` bindings and the returned attrset;
|
|
* anything in a deeper attrset is not. Without that bound a NixOS module's
|
|
* settings tree would mint a node per `enable = true`. */
|
|
TEST(nix_module_level_bindings_mint_variables) {
|
|
CBMFileResult *r = extract("{ pkgs, lib, ... }:\n"
|
|
"let\n"
|
|
" privateConst = 42;\n"
|
|
"in\n"
|
|
"{\n"
|
|
" exported = \"value\";\n"
|
|
" services.nginx.enable = true;\n"
|
|
"}\n",
|
|
CBM_LANG_NIX, "t", "mod.nix");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
/* A let binding is file scope in the same sense a C++ file-static is. */
|
|
ASSERT(has_def(r, "Variable", "privateConst"));
|
|
ASSERT(has_def(r, "Variable", "exported"));
|
|
/* The QN carries the attrpath, exactly as it does for functions. */
|
|
ASSERT(has_def(r, "Variable", "enable"));
|
|
ASSERT(has_def_qn(r, "t.mod.services.nginx.enable"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* The flood guard. Each absence assertion is paired with a positive one on the
|
|
* same predicate in the same result, so none can pass by extracting nothing. */
|
|
TEST(nix_nested_bindings_are_not_module_level) {
|
|
CBMFileResult *r = extract("{ pkgs }:\n"
|
|
"{\n"
|
|
" topLevel = 1;\n"
|
|
" deep = {\n"
|
|
" nested = {\n"
|
|
" shouldNotAppear = 2;\n"
|
|
" };\n"
|
|
" };\n"
|
|
"}\n",
|
|
CBM_LANG_NIX, "t", "deep.nix");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Variable", "topLevel")); /* positive control */
|
|
ASSERT_FALSE(has_def_any(r, "shouldNotAppear"));
|
|
/* `deep` is an attrset — a scope, not a value — so not a Variable either. */
|
|
ASSERT_FALSE(has_def(r, "Variable", "deep"));
|
|
ASSERT_FALSE(has_def(r, "Variable", "nested"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* A lambda-valued binding is already minted as a Function by the def walk. Minting
|
|
* it again here would double-count every helper in the ecosystem. */
|
|
TEST(nix_lambda_binding_is_function_not_variable) {
|
|
CBMFileResult *r =
|
|
extract("{\n fn = x: x + 1;\n val = 7;\n}\n", CBM_LANG_NIX, "t", "mix.nix");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "fn"));
|
|
ASSERT_FALSE(has_def(r, "Variable", "fn"));
|
|
ASSERT(has_def(r, "Variable", "val")); /* positive control */
|
|
ASSERT_FALSE(has_def(r, "Function", "val"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* Descending past the header must not mint a second def from a curried lambda's inner
|
|
* arm: `iota = a: b: ...` is one named function, not two. The inner `b:` has a
|
|
* function_expression parent, resolves no name, and must stay out. */
|
|
TEST(nix_curried_lambda_mints_one_def) {
|
|
CBMFileResult *r = extract("{ prelude }:\nlet\n iota = a: b: a + b;\nin\n{ inherit iota; }\n",
|
|
CBM_LANG_NIX, "t", "curried.nix");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "iota"));
|
|
ASSERT(count_defs_with_label(r, "Function") == 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Fortran --- */
|
|
TEST(fortran_function) {
|
|
/* Fortran subroutine name extraction is incomplete — just verify no crash */
|
|
CBMFileResult *r = extract("subroutine greet(name)\n character(*), intent(in) :: name\n "
|
|
"print *, 'Hello ', name\nend subroutine\n",
|
|
CBM_LANG_FORTRAN, "t", "greet.f90");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Group A2: Missing OOP / Systems variants
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
/* --- Swift struct --- */
|
|
TEST(swift_struct) {
|
|
CBMFileResult *r = extract("struct Point {\n var x: Double\n var y: Double\n func "
|
|
"distance() -> Double { return (x*x + y*y).squareRoot() }\n}\n",
|
|
CBM_LANG_SWIFT, "t", "Point.swift");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Method", "distance"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Swift calls (port of PR #47 Go tests) --- */
|
|
TEST(swift_simple_call) {
|
|
CBMFileResult *r = extract("func main() { greet() }\nfunc greet() { print(\"hello\") }\n",
|
|
CBM_LANG_SWIFT, "t", "main.swift");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_call(r, "greet"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(swift_method_call) {
|
|
CBMFileResult *r =
|
|
extract("class Foo {\n func bar() { baz.run() }\n}\n", CBM_LANG_SWIFT, "t", "Foo.swift");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_call(r, "baz.run"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(swift_constructor_call) {
|
|
CBMFileResult *r =
|
|
extract("func create() { let x = MyClass() }\n", CBM_LANG_SWIFT, "t", "create.swift");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_call(r, "MyClass"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(swift_chained_call) {
|
|
CBMFileResult *r = extract("func setup() { AlarmScheduler.shared.startKeepAlive() }\n",
|
|
CBM_LANG_SWIFT, "t", "setup.swift");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(r->calls.count > 0);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Objective-C --- */
|
|
TEST(objc_interface) {
|
|
CBMFileResult *r =
|
|
extract("@interface Animal : NSObject\n- (NSString *)name;\n- (void)speak;\n@end\n",
|
|
CBM_LANG_OBJC, "t", "Animal.h");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->defs.count, 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(objc_implementation) {
|
|
CBMFileResult *r = extract("@implementation Animal\n- (NSString *)name { return @\"Animal\"; "
|
|
"}\n- (void)speak { NSLog(@\"...\"); }\n@end\n",
|
|
CBM_LANG_OBJC, "t", "Animal.m");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->defs.count, 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Dart top-level function --- */
|
|
TEST(dart_top_level_function) {
|
|
CBMFileResult *r = extract(
|
|
"void main() {\n print('Hello');\n}\nString greet(String name) => 'Hello $name';\n",
|
|
CBM_LANG_DART, "t", "main.dart");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "main"));
|
|
ASSERT(has_def(r, "Function", "greet"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Rust enum --- */
|
|
TEST(rust_enum) {
|
|
CBMFileResult *r =
|
|
extract("pub enum Direction { North, South, East, West }\n", CBM_LANG_RUST, "t", "dir.rs");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->defs.count, 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Zig struct --- */
|
|
TEST(zig_struct) {
|
|
CBMFileResult *r = extract("const Point = struct { x: f32, y: f32, pub fn dist(self: Point) "
|
|
"f32 { return self.x + self.y; } };\n",
|
|
CBM_LANG_ZIG, "t", "point.zig");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->defs.count, 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- C++ function (standalone) --- */
|
|
TEST(cpp_function) {
|
|
CBMFileResult *r = extract("#include <string>\nstd::string greet(const std::string& name) { "
|
|
"return \"Hello \" + name; }\nint main() { return 0; }\n",
|
|
CBM_LANG_CPP, "t", "main.cpp");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->defs.count, 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* #1266: GoogleTest TEST() macros with the same name collapse into a single
|
|
* node when multiple tests share a file. Each must mint a distinct Function
|
|
* node whose name encodes the suite and case arguments. */
|
|
TEST(cpp_gtest_same_name_collision_issue1266) {
|
|
CBMFileResult *r = extract(
|
|
"namespace demo { int assembleWidget(int s) { return s * 2; } }\n"
|
|
"TEST(WidgetSuite, DoublesSmallSize) { demo::assembleWidget(1); }\n"
|
|
"TEST(WidgetSuite, DoublesZero) { demo::assembleWidget(0); }\n"
|
|
"TEST(WidgetSuite, DoublesLargeSize) {\n"
|
|
" demo::assembleWidget(1000);\n"
|
|
"}\n",
|
|
CBM_LANG_CPP, "t", "direct_test.cpp");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT(has_def(r, "Function", "TEST_WidgetSuite_DoublesSmallSize"));
|
|
ASSERT(has_def(r, "Function", "TEST_WidgetSuite_DoublesZero"));
|
|
ASSERT(has_def(r, "Function", "TEST_WidgetSuite_DoublesLargeSize"));
|
|
ASSERT(!has_def(r, "Function", "TEST"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* #1266: TEST_F fixture macro also produces unique names. */
|
|
TEST(cpp_gtest_f_unique_name_issue1266) {
|
|
CBMFileResult *r = extract(
|
|
"TEST_F(MyFixture, FirstTest) { doStuff(); }\n"
|
|
"TEST_F(MyFixture, SecondTest) { doOtherStuff(); }\n",
|
|
CBM_LANG_CPP, "t", "fixture_test.cpp");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT(has_def(r, "Function", "TEST_F_MyFixture_FirstTest"));
|
|
ASSERT(has_def(r, "Function", "TEST_F_MyFixture_SecondTest"));
|
|
ASSERT(!has_def(r, "Function", "TEST_F"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- C++ out-of-line method definitions (#428) ---
|
|
* A .cpp defining methods of a class declared elsewhere (not in this TU).
|
|
* Pre-fix these were recorded as free Functions (label "Function", no
|
|
* parent_class); they must be Methods linked to their enclosing class via
|
|
* parent_class. (The helper also descends nested scopes `ns::Class::method` to
|
|
* the immediate class, but tree-sitter-cpp parses a synthetic doubly-qualified
|
|
* out-of-line def in isolation as an ERROR node, so that path is exercised by
|
|
* real codebases rather than this isolated unit fixture.) */
|
|
TEST(cpp_out_of_line_method_issue428) {
|
|
CBMFileResult *r = extract("void Foo::bar() {}\n"
|
|
"int Foo::baz() { return 0; }\n",
|
|
CBM_LANG_CPP, "t", "foo.cpp");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT(has_def(r, "Method", "bar"));
|
|
ASSERT(has_def(r, "Method", "baz"));
|
|
ASSERT(!has_def(r, "Function", "bar")); /* not a free function */
|
|
/* parent_class links to the enclosing class QN */
|
|
int checked = 0;
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
const CBMDefinition *d = &r->defs.items[i];
|
|
if (strcmp(d->name, "bar") == 0 && strcmp(d->label, "Method") == 0) {
|
|
ASSERT_NOT_NULL(d->parent_class);
|
|
ASSERT(strstr(d->parent_class, "Foo") != NULL);
|
|
checked = 1;
|
|
}
|
|
}
|
|
ASSERT(checked);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- COBOL paragraph --- */
|
|
TEST(cobol_paragraph) {
|
|
CBMFileResult *r =
|
|
extract("IDENTIFICATION DIVISION.\nPROGRAM-ID. HELLO.\nPROCEDURE DIVISION.\n "
|
|
"DISPLAY-GREETING.\n DISPLAY 'HELLO WORLD'.\n STOP RUN.\n",
|
|
CBM_LANG_COBOL, "t", "hello.cbl");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->defs.count, 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Verilog module --- */
|
|
TEST(verilog_module) {
|
|
CBMFileResult *r =
|
|
extract("module adder(input a, input b, output sum);\n assign sum = a + b;\nendmodule\n",
|
|
CBM_LANG_VERILOG, "t", "adder.v");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->defs.count, 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- CUDA kernel --- */
|
|
TEST(cuda_kernel) {
|
|
CBMFileResult *r = extract("__global__ void vectorAdd(float *a, float *b, float *c, int n) {\n "
|
|
" int i = blockIdx.x * blockDim.x + threadIdx.x;\n if (i < n) "
|
|
"c[i] = a[i] + b[i];\n}\nint main() { return 0; }\n",
|
|
CBM_LANG_CUDA, "t", "vector.cu");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->defs.count, 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Python decorator --- */
|
|
TEST(python_decorator) {
|
|
CBMFileResult *r = extract("class Router:\n @staticmethod\n def route(path: str):\n "
|
|
" def decorator(func): return func\n return decorator\n",
|
|
CBM_LANG_PYTHON, "t", "router.py");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Class", "Router"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- TypeScript interface --- */
|
|
TEST(ts_interface) {
|
|
CBMFileResult *r = extract("export interface Repository<T> { findById(id: number): T; "
|
|
"save(entity: T): void; delete(id: number): void; }\n",
|
|
CBM_LANG_TYPESCRIPT, "t", "repo.ts");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->defs.count, 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- TSX component --- */
|
|
TEST(tsx_component) {
|
|
CBMFileResult *r = extract(
|
|
"import React from 'react';\ninterface Props { name: string; }\nexport function Greeting({ "
|
|
"name }: Props) {\n return <div>Hello {name}</div>;\n}\nexport default Greeting;\n",
|
|
CBM_LANG_TSX, "t", "Greeting.tsx");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "Greeting"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Lua table method --- */
|
|
TEST(lua_table_method) {
|
|
CBMFileResult *r =
|
|
extract("local M = {}\nfunction M.create(name)\n return { name = name }\nend\nfunction "
|
|
"M.greet(self)\n return 'Hi ' .. self.name\nend\nreturn M\n",
|
|
CBM_LANG_LUA, "t", "module.lua");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
/* Should extract at least one Function from Lua table method */
|
|
int fn_count = 0;
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (strcmp(r->defs.items[i].label, "Function") == 0)
|
|
fn_count++;
|
|
}
|
|
ASSERT_GTE(fn_count, 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Emacs Lisp defun --- */
|
|
TEST(emacs_lisp_defun) {
|
|
CBMFileResult *r = extract("(defun greet (name)\n (message \"Hello %s\" name))\n(defun main "
|
|
"()\n (greet \"World\"))\n",
|
|
CBM_LANG_EMACSLISP, "t", "init.el");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "greet"));
|
|
ASSERT(has_def(r, "Function", "main"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Emacs Lisp defvar --- */
|
|
TEST(emacs_lisp_defvar) {
|
|
CBMFileResult *r = extract("(defvar my-count 0 \"A counter.\")\n(defcustom my-name \"World\" "
|
|
"\"The name.\"\n :type 'string)\n",
|
|
CBM_LANG_EMACSLISP, "t", "vars.el");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->defs.count, 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Haskell data type --- */
|
|
TEST(haskell_data_type) {
|
|
CBMFileResult *r =
|
|
extract("data Shape = Circle Double | Rectangle Double Double\narea :: Shape -> "
|
|
"Double\narea (Circle r) = pi * r * r\narea (Rectangle w h) = w * h\n",
|
|
CBM_LANG_HASKELL, "t", "Shape.hs");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->defs.count, 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Clojure function (known limitation: defn produces list_lit) --- */
|
|
TEST(clojure_function) {
|
|
CBMFileResult *r = extract("(ns greeter.core)\n(defn greet [name]\n (str \"Hello \" "
|
|
"name))\n(defn -main [& args]\n (println (greet \"World\")))\n",
|
|
CBM_LANG_CLOJURE, "t", "core.clj");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
/* Clojure uses list_lit for all forms — no function defs extracted (known limitation) */
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Group E2: Missing Config / Markup Languages
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
/* --- HTML elements --- */
|
|
TEST(html_elements) {
|
|
CBMFileResult *r = extract(
|
|
"<!DOCTYPE "
|
|
"html><html><head><title>Test</title></head><body><h1>Hello</h1><p>World</p></body></html>",
|
|
CBM_LANG_HTML, "t", "index.html");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->defs.count, 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- SQL function (CREATE FUNCTION) --- */
|
|
TEST(sql_function) {
|
|
CBMFileResult *r = extract("CREATE FUNCTION get_user_count() RETURNS INTEGER AS $$ SELECT "
|
|
"COUNT(*) FROM users; $$ LANGUAGE SQL;\n",
|
|
CBM_LANG_SQL, "t", "funcs.sql");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->defs.count, 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(sql_ddl_node_labels) {
|
|
CBMFileResult *r = extract("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);\n"
|
|
"CREATE VIEW active_users AS SELECT * FROM users;\n",
|
|
CBM_LANG_SQL, "t", "schema.sql");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Table", "users"));
|
|
ASSERT(has_def(r, "View", "active_users"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(sql_view_lineage_usages) {
|
|
/* A view's FROM/JOIN relations are emitted as usages (ref_name = table),
|
|
* which pass_usages later resolves into view -> table USAGE lineage edges. */
|
|
CBMFileResult *r = extract("CREATE TABLE users (id INTEGER);\n"
|
|
"CREATE VIEW active_users AS SELECT * FROM users;\n",
|
|
CBM_LANG_SQL, "t", "schema.sql");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
int found_users = 0;
|
|
for (int i = 0; i < r->usages.count; i++) {
|
|
if (r->usages.items[i].ref_name && strcmp(r->usages.items[i].ref_name, "users") == 0) {
|
|
found_users = 1;
|
|
}
|
|
}
|
|
ASSERT(found_users);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(sql_schema_qualified_name) {
|
|
/* schema-qualified DDL (schema.table) is named by the table, not the schema,
|
|
* and FROM schema.table resolves to that table for lineage. */
|
|
CBMFileResult *r = extract("CREATE TABLE app.users (id INTEGER);\n"
|
|
"CREATE VIEW app.active AS SELECT * FROM app.users;\n",
|
|
CBM_LANG_SQL, "t", "schema.sql");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Table", "users"));
|
|
ASSERT(has_def(r, "View", "active"));
|
|
int found_users = 0;
|
|
for (int i = 0; i < r->usages.count; i++) {
|
|
if (r->usages.items[i].ref_name && strcmp(r->usages.items[i].ref_name, "users") == 0) {
|
|
found_users = 1;
|
|
}
|
|
}
|
|
ASSERT(found_users);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- dbt Jinja lineage --- */
|
|
|
|
/* Helper: does the file's usage list carry `name`? */
|
|
static int has_usage(CBMFileResult *r, const char *name) {
|
|
for (int i = 0; i < r->usages.count; i++) {
|
|
if (r->usages.items[i].ref_name && strcmp(r->usages.items[i].ref_name, name) == 0) {
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
TEST(dbt_model_and_ref_lineage) {
|
|
/* A dbt model: the file stem is the model identity, and each ref() is a
|
|
* dependency on another model. The SQL grammar cannot read `{{ ref(..) }}`
|
|
* at all, so without the dbt pass this file yields no lineage whatsoever. */
|
|
CBMFileResult *r = extract("SELECT o.id, c.name\n"
|
|
"FROM {{ ref('stg_orders') }} o\n"
|
|
"JOIN {{ ref('stg_customers') }} c ON c.id = o.customer_id\n",
|
|
CBM_LANG_SQL, "t", "models/marts/orders_enriched.sql");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT(has_def(r, "Model", "orders_enriched"));
|
|
ASSERT(has_usage(r, "stg_orders"));
|
|
ASSERT(has_usage(r, "stg_customers"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(dbt_source_and_two_arg_ref) {
|
|
/* Both dbt builtins name the relation in their LAST string argument:
|
|
* source('group','table') -> table, and the two-argument
|
|
* ref('package','model') form -> model. */
|
|
CBMFileResult *r = extract("SELECT * FROM {{ source('raw', 'customers') }}\n"
|
|
"UNION ALL SELECT * FROM {{ ref('analytics', 'legacy_customers') }}\n",
|
|
CBM_LANG_SQL, "t", "models/stg_customers.sql");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT(has_def(r, "Model", "stg_customers"));
|
|
ASSERT(has_usage(r, "customers"));
|
|
ASSERT(has_usage(r, "legacy_customers"));
|
|
/* the group/package argument is not the relation */
|
|
ASSERT_FALSE(has_usage(r, "raw"));
|
|
ASSERT_FALSE(has_usage(r, "analytics"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(dbt_ignores_non_dbt_jinja) {
|
|
/* Templated SQL is not dbt SQL. An Airflow-style parameter substitution has
|
|
* Jinja but no dbt builtin, so the dbt pass must contribute NOTHING — no
|
|
* Model node named after the file, and no usage minted from the template
|
|
* variables. This is the gate that keeps every non-dbt repository free of
|
|
* fabricated data-lineage vocabulary.
|
|
*
|
|
* The ordinary SQL identifier path is unaffected and still sees the literal
|
|
* `FROM events`; the second extraction below is the control proving that
|
|
* usage is pre-existing SQL behaviour rather than anything dbt added. */
|
|
CBMFileResult *r = extract("SELECT * FROM events WHERE day = '{{ ds }}'\n"
|
|
" AND region = '{{ params.region_code }}'\n",
|
|
CBM_LANG_SQL, "t", "queries/daily_events.sql");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(has_def(r, "Model", "daily_events"));
|
|
|
|
/* Control: the same statement with the templates replaced by plain string
|
|
* literals. Both parse as SQL identically, so an equal usage count is the
|
|
* precise statement of "the dbt pass contributed nothing here" — stronger
|
|
* than naming individual identifiers, and immune to how SQL happens to
|
|
* tokenize the template text. */
|
|
CBMFileResult *plain = extract("SELECT * FROM events WHERE day = '2026-01-01'\n"
|
|
" AND region = 'eu-west'\n",
|
|
CBM_LANG_SQL, "t", "queries/daily_events.sql");
|
|
ASSERT_NOT_NULL(plain);
|
|
ASSERT_FALSE(has_def(plain, "Model", "daily_events"));
|
|
ASSERT_EQ(r->usages.count, plain->usages.count);
|
|
ASSERT_EQ(r->defs.count, plain->defs.count);
|
|
cbm_free_result(plain);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(dbt_plain_sql_untouched) {
|
|
/* Plain DDL keeps producing exactly the Table/View relations it did before
|
|
* the dbt pass existed — no Model node, and the FROM lineage is unchanged. */
|
|
CBMFileResult *r = extract("CREATE TABLE users (id INTEGER);\n"
|
|
"CREATE VIEW active_users AS SELECT * FROM users;\n",
|
|
CBM_LANG_SQL, "t", "schema.sql");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Table", "users"));
|
|
ASSERT(has_def(r, "View", "active_users"));
|
|
ASSERT_FALSE(has_def(r, "Model", "schema"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Meson project --- */
|
|
TEST(meson_project) {
|
|
CBMFileResult *r = extract(
|
|
"project('myapp', 'c', version: '1.0.0')\nexecutable('myapp', 'main.c', install: true)\n",
|
|
CBM_LANG_MESON, "t", "meson.build");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->defs.count, 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- CSS rules --- */
|
|
TEST(css_rules) {
|
|
CBMFileResult *r = extract(
|
|
".container { display: flex; width: 100%; }\n.button { background: #007bff; color: white; "
|
|
"border: none; }\n@media (max-width: 768px) { .container { flex-direction: column; } }\n",
|
|
CBM_LANG_CSS, "t", "styles.css");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->defs.count, 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- SCSS rules --- */
|
|
TEST(scss_rules) {
|
|
CBMFileResult *r = extract("$primary: #007bff;\n.container {\n width: 100%;\n .button {\n "
|
|
"background: $primary;\n &:hover { opacity: 0.8; }\n }\n}\n",
|
|
CBM_LANG_SCSS, "t", "styles.scss");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->defs.count, 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- TOML basic --- */
|
|
TEST(toml_basic) {
|
|
CBMFileResult *r = extract("[server]\nhost = \"localhost\"\nport = 8080\n\n[database]\nurl = "
|
|
"\"postgres://localhost/db\"\nmax_connections = 10\n",
|
|
CBM_LANG_TOML, "t", "config.toml");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Class", "server"));
|
|
ASSERT(has_def(r, "Class", "database"));
|
|
ASSERT(has_def(r, "Variable", "host"));
|
|
ASSERT(has_def(r, "Variable", "port"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- CMake function --- */
|
|
TEST(cmake_function) {
|
|
CBMFileResult *r = extract(
|
|
"cmake_minimum_required(VERSION 3.16)\nproject(MyApp VERSION 1.0)\nadd_executable(myapp "
|
|
"main.cpp)\ntarget_compile_features(myapp PRIVATE cxx_std_17)\n",
|
|
CBM_LANG_CMAKE, "t", "CMakeLists.txt");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->defs.count, 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- JSON object --- */
|
|
TEST(json_object) {
|
|
CBMFileResult *r = extract("{\"name\": \"myapp\", \"version\": \"1.0.0\", \"scripts\": "
|
|
"{\"build\": \"go build\", \"test\": \"go test ./...\"}}",
|
|
CBM_LANG_JSON, "t", "config.json");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Variable", "name"));
|
|
ASSERT(has_def(r, "Variable", "version"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Protobuf message --- */
|
|
TEST(protobuf_message) {
|
|
CBMFileResult *r = extract(
|
|
"syntax = \"proto3\";\npackage user;\nmessage User { int64 id = 1; string name = 2; string "
|
|
"email = 3; }\nservice UserService { rpc GetUser(User) returns (User); }\n",
|
|
CBM_LANG_PROTOBUF, "t", "user.proto");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->defs.count, 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- GraphQL type --- */
|
|
TEST(graphql_type) {
|
|
CBMFileResult *r = extract("type User {\n id: ID!\n name: String!\n email: String!\n}\ntype "
|
|
"Query {\n user(id: ID!): User\n users: [User!]!\n}\n",
|
|
CBM_LANG_GRAPHQL, "t", "schema.graphql");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->defs.count, 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Svelte component --- */
|
|
TEST(svelte_component) {
|
|
CBMFileResult *r = extract("<script>\n let name = 'World';\n function greet() {\n return "
|
|
"`Hello ${name}`;\n }\n</script>\n<h1>{greet()}</h1>\n",
|
|
CBM_LANG_SVELTE, "t", "App.svelte");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->defs.count, 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Vue component --- */
|
|
TEST(vue_component) {
|
|
CBMFileResult *r =
|
|
extract("<template><div>{{ message }}</div></template>\n<script>\nexport default {\n "
|
|
"name: 'App',\n data() { return { message: 'Hello World' }; },\n methods: { "
|
|
"greet() { return this.message; } }\n};\n</script>\n",
|
|
CBM_LANG_VUE, "t", "App.vue");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->defs.count, 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- GLSL shader --- */
|
|
TEST(glsl_shader) {
|
|
CBMFileResult *r = extract(
|
|
"#version 330 core\nvoid main() {\n gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n}\nvec3 "
|
|
"transform(vec3 pos, mat4 mvp) {\n return (mvp * vec4(pos, 1.0)).xyz;\n}\n",
|
|
CBM_LANG_GLSL, "t", "vertex.glsl");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->defs.count, 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- VimScript function --- */
|
|
TEST(vimscript_function) {
|
|
CBMFileResult *r = extract("function! SayHello()\n echo 'Hello'\nendfunction\n",
|
|
CBM_LANG_VIMSCRIPT, "t", "plugin.vim");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
/* VimScript extraction may or may not produce named functions */
|
|
int fn_count = 0;
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (strcmp(r->defs.items[i].label, "Function") == 0)
|
|
fn_count++;
|
|
}
|
|
if (fn_count > 0) {
|
|
ASSERT(has_def(r, "Function", "SayHello"));
|
|
}
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Group H: Scientific / Math — extended tests
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
/* --- MATLAB parse (simple expression) --- */
|
|
TEST(matlab_parse) {
|
|
CBMFileResult *r = extract("x = 1;\ny = x + 2;\n", CBM_LANG_MATLAB, "t", "simple.matlab");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- MATLAB call --- */
|
|
TEST(matlab_call) {
|
|
CBMFileResult *r = extract("function y = foo(x)\n y = inv(x);\n disp hello\nend\n",
|
|
CBM_LANG_MATLAB, "t", "foo.matlab");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GT(r->calls.count, 0);
|
|
ASSERT(has_call(r, "inv"));
|
|
ASSERT(has_call(r, "disp"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Lean parse (theorem) --- */
|
|
TEST(lean_parse) {
|
|
CBMFileResult *r = extract("theorem add_comm (a b : Nat) : a + b = b + a := by omega\n",
|
|
CBM_LANG_LEAN, "t", "Comm.lean");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Lean call (recursive fib) --- */
|
|
TEST(lean_call) {
|
|
CBMFileResult *r = extract("def fib : Nat \xe2\x86\x92 Nat\n | 0 => 1\n | 1 => 1\n | n + 2 "
|
|
"=> fib (n + 1) + fib n\n",
|
|
CBM_LANG_LEAN, "t", "Fib.lean");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GT(r->calls.count, 0);
|
|
ASSERT(has_call(r, "fib"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Lean type annotation not call --- */
|
|
TEST(lean_type_annotation_not_call) {
|
|
CBMFileResult *r = extract(
|
|
"def listLen (xs : List Nat) : Nat := 0\ndef greet : IO Unit := IO.println \"hi\"\n",
|
|
CBM_LANG_LEAN, "t", "Types.lean");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
/* "List" in binder type position should NOT be extracted as a call */
|
|
for (int i = 0; i < r->calls.count; i++) {
|
|
ASSERT_FALSE(strcmp(r->calls.items[i].callee_name, "List") == 0);
|
|
}
|
|
/* IO.println in the body should be present */
|
|
int found_println = 0;
|
|
for (int i = 0; i < r->calls.count; i++) {
|
|
if (strstr(r->calls.items[i].callee_name, "println") != NULL) {
|
|
found_println = 1;
|
|
}
|
|
}
|
|
ASSERT_TRUE(found_println);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- FORM parse (simple expression) --- */
|
|
TEST(form_parse) {
|
|
CBMFileResult *r = extract("Symbols x, y;\nLocal F = x + y;\nPrint;\n.end\n", CBM_LANG_FORM,
|
|
"t", "example.frm");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- FORM call (#call) --- */
|
|
TEST(form_call) {
|
|
CBMFileResult *r = extract("#procedure myproc(x)\n id `x' = 0;\n#endprocedure\n#procedure "
|
|
"caller()\n #call myproc(1)\n#endprocedure\n",
|
|
CBM_LANG_FORM, "t", "calc.frm");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GT(r->calls.count, 0);
|
|
ASSERT(has_call(r, "myproc"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Magma procedure --- */
|
|
TEST(magma_procedure) {
|
|
CBMFileResult *r = extract("procedure PrintHello()\n print \"Hello\";\nend procedure;\n",
|
|
CBM_LANG_MAGMA, "t", "hello.mag");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
int fn_count = 0;
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (strcmp(r->defs.items[i].label, "Function") == 0)
|
|
fn_count++;
|
|
}
|
|
if (fn_count > 0) {
|
|
ASSERT(has_def(r, "Function", "PrintHello"));
|
|
}
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Magma parse (simple) --- */
|
|
TEST(magma_parse) {
|
|
CBMFileResult *r = extract("x := 42;\ny := x + 1;\n", CBM_LANG_MAGMA, "t", "simple.mag");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Magma import (load) --- */
|
|
TEST(magma_import) {
|
|
CBMFileResult *r = extract("load \"utils.mag\";\nload \"lib/helpers.mag\";\n", CBM_LANG_MAGMA,
|
|
"t", "main.mag");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->imports.count, 2);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Magma call --- */
|
|
TEST(magma_call) {
|
|
CBMFileResult *r = extract("function Foo(x)\n y := Bar(x);\n return y;\nend function;\n",
|
|
CBM_LANG_MAGMA, "t", "calls.mag");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GT(r->calls.count, 0);
|
|
ASSERT(has_call(r, "Bar"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Magma disambiguation (.m file as Magma) --- */
|
|
TEST(magma_disambiguation) {
|
|
CBMFileResult *r = extract("function Factorial(n)\n if n le 1 then\n return 1;\n end "
|
|
"if;\n return n * Factorial(n - 1);\nend function;\n",
|
|
CBM_LANG_MAGMA, "t", "test.m");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
int fn_count = 0;
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (strcmp(r->defs.items[i].label, "Function") == 0)
|
|
fn_count++;
|
|
}
|
|
ASSERT_GTE(fn_count, 1);
|
|
if (fn_count > 0) {
|
|
ASSERT(has_def(r, "Function", "Factorial"));
|
|
}
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Wolfram function (both := and =) --- */
|
|
TEST(wolfram_function_extended) {
|
|
CBMFileResult *r = extract("f[x_] := x^2\ng[x_] = x + 1\n", CBM_LANG_WOLFRAM, "t", "funcs.wl");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
int fn_count = 0;
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (strcmp(r->defs.items[i].label, "Function") == 0)
|
|
fn_count++;
|
|
}
|
|
ASSERT_GTE(fn_count, 2);
|
|
ASSERT(has_def(r, "Function", "f"));
|
|
ASSERT(has_def(r, "Function", "g"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Wolfram call --- */
|
|
TEST(wolfram_call) {
|
|
CBMFileResult *r = extract("f[x_] := g[x] + h[x]\n", CBM_LANG_WOLFRAM, "t", "calls.wl");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GT(r->calls.count, 0);
|
|
ASSERT(has_call(r, "g"));
|
|
ASSERT(has_call(r, "h"));
|
|
/* "f" should NOT appear as a call (it's the definition LHS) */
|
|
for (int i = 0; i < r->calls.count; i++) {
|
|
ASSERT_FALSE(strcmp(r->calls.items[i].callee_name, "f") == 0);
|
|
}
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Wolfram caller attribution --- */
|
|
TEST(wolfram_caller_attribution) {
|
|
CBMFileResult *r = extract("f[x_] := g[x] + h[x]\n", CBM_LANG_WOLFRAM, "t", "caller.wl");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GT(r->calls.count, 0);
|
|
/* Calls inside f[] should have f as enclosing function, not the module path */
|
|
for (int i = 0; i < r->calls.count; i++) {
|
|
if (strcmp(r->calls.items[i].callee_name, "g") == 0 ||
|
|
strcmp(r->calls.items[i].callee_name, "h") == 0) {
|
|
/* enclosing_func_qn must NOT be empty or the file path */
|
|
ASSERT_NOT_NULL(r->calls.items[i].enclosing_func_qn);
|
|
ASSERT_FALSE(strcmp(r->calls.items[i].enclosing_func_qn, "") == 0);
|
|
ASSERT_FALSE(strcmp(r->calls.items[i].enclosing_func_qn, "t.caller") == 0);
|
|
}
|
|
}
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* Issue #438: a C function_definition has no `name` field — the name lives in the
|
|
* declarator chain. Calls inside a C function must be attributed to the enclosing
|
|
* function, not the module. Pre-fix, enclosing_func_qn fell back to the module QN. */
|
|
TEST(c_caller_attribution) {
|
|
CBMFileResult *r = extract("int helper(int x) { return x; }\n"
|
|
"int caller(void) { return helper(1); }\n",
|
|
CBM_LANG_C, "t", "main.c");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GT(r->calls.count, 0);
|
|
int saw_helper = 0;
|
|
for (int i = 0; i < r->calls.count; i++) {
|
|
if (strcmp(r->calls.items[i].callee_name, "helper") == 0) {
|
|
saw_helper = 1;
|
|
/* enclosing_func_qn must be the function, NOT empty and NOT the module QN. */
|
|
ASSERT_NOT_NULL(r->calls.items[i].enclosing_func_qn);
|
|
ASSERT_FALSE(strcmp(r->calls.items[i].enclosing_func_qn, "") == 0);
|
|
ASSERT_FALSE(strcmp(r->calls.items[i].enclosing_func_qn, "t.main") == 0);
|
|
}
|
|
}
|
|
ASSERT(saw_helper);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* adc8304 (the dedup refactor bundled into #463) re-pointed the C/C++ enclosing-
|
|
* function resolver at the canonical declarator walker: qualified names (Foo::bar)
|
|
* now resolve via resolve_qualified_name(), and `type_identifier` was dropped from
|
|
* the terminal-name set. These guard that out-of-line C++ method / ctor / dtor
|
|
* definitions still attribute their inner calls to the enclosing function, not the
|
|
* module — i.e. that the dedup did not reintroduce the #438 regression on the
|
|
* qualified-declarator path. Module QN for "m.cpp" under prefix "t" is "t.m". */
|
|
TEST(cpp_out_of_line_method_caller_attribution) {
|
|
CBMFileResult *r = extract("struct Foo { void bar(); };\n"
|
|
"int helper(int x) { return x; }\n"
|
|
"void Foo::bar() { helper(1); }\n",
|
|
CBM_LANG_CPP, "t", "m.cpp");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GT(r->calls.count, 0);
|
|
int saw_helper = 0;
|
|
for (int i = 0; i < r->calls.count; i++) {
|
|
if (strcmp(r->calls.items[i].callee_name, "helper") == 0) {
|
|
saw_helper = 1;
|
|
/* enclosing must be the out-of-line method, NOT empty and NOT the module. */
|
|
ASSERT_NOT_NULL(r->calls.items[i].enclosing_func_qn);
|
|
ASSERT_FALSE(strcmp(r->calls.items[i].enclosing_func_qn, "") == 0);
|
|
ASSERT_FALSE(strcmp(r->calls.items[i].enclosing_func_qn, "t.m") == 0);
|
|
}
|
|
}
|
|
ASSERT(saw_helper);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* Out-of-line constructor (Foo::Foo) and destructor (Foo::~Foo) exercise the
|
|
* identifier and destructor_name branches of resolve_qualified_name(). A call
|
|
* inside either must attribute to that special member, not the module. */
|
|
TEST(cpp_out_of_line_ctor_dtor_caller_attribution) {
|
|
CBMFileResult *r = extract("struct Foo { Foo(); ~Foo(); };\n"
|
|
"int helper(int x) { return x; }\n"
|
|
"Foo::Foo() { helper(1); }\n"
|
|
"Foo::~Foo() { helper(2); }\n",
|
|
CBM_LANG_CPP, "t", "m.cpp");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GT(r->calls.count, 0);
|
|
int helper_calls = 0;
|
|
for (int i = 0; i < r->calls.count; i++) {
|
|
if (strcmp(r->calls.items[i].callee_name, "helper") == 0) {
|
|
helper_calls++;
|
|
ASSERT_NOT_NULL(r->calls.items[i].enclosing_func_qn);
|
|
ASSERT_FALSE(strcmp(r->calls.items[i].enclosing_func_qn, "") == 0);
|
|
ASSERT_FALSE(strcmp(r->calls.items[i].enclosing_func_qn, "t.m") == 0);
|
|
}
|
|
}
|
|
ASSERT(helper_calls >= 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Wolfram parse (simple assignment) --- */
|
|
TEST(wolfram_parse) {
|
|
CBMFileResult *r = extract("x = 42;\ny = x + 1;\n", CBM_LANG_WOLFRAM, "t", "simple.wl");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Wolfram import --- */
|
|
TEST(wolfram_import) {
|
|
CBMFileResult *r =
|
|
extract("<< \"utils.wl\"\nNeeds[\"Package`\"]\n", CBM_LANG_WOLFRAM, "t", "main.wl");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GT(r->imports.count, 0);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Wolfram nested def --- */
|
|
TEST(wolfram_nested_def) {
|
|
CBMFileResult *r = extract("main[x_] := Module[{localF}, localF[t_] := t + 1; localF[x]]\n",
|
|
CBM_LANG_WOLFRAM, "t", "nested.wl");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "main"));
|
|
ASSERT(has_def(r, "Function", "localF"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Group I: cbm_test.go ports
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
TEST(python_docstring) {
|
|
CBMFileResult *r = extract(
|
|
"def compute(x, y):\n \"\"\"Compute the sum of x and y.\"\"\"\n return x + y\n",
|
|
CBM_LANG_PYTHON, "test", "test.py");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "compute"));
|
|
/* Check docstring is present */
|
|
int found = 0;
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (strcmp(r->defs.items[i].name, "compute") == 0) {
|
|
found = 1;
|
|
ASSERT_NOT_NULL(r->defs.items[i].docstring);
|
|
ASSERT_TRUE(strlen(r->defs.items[i].docstring) > 0);
|
|
}
|
|
}
|
|
ASSERT_TRUE(found);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(go_function_extraction) {
|
|
CBMFileResult *r =
|
|
extract("package main\n\n// Greet returns a greeting.\nfunc Greet(name string) string "
|
|
"{\n\treturn \"Hello, \" + name\n}\n\nfunc main() {\n\tGreet(\"world\")\n}\n",
|
|
CBM_LANG_GO, "test", "main.go");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "Greet"));
|
|
ASSERT(has_def(r, "Function", "main"));
|
|
ASSERT(has_call(r, "Greet"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(js_arrow_function) {
|
|
CBMFileResult *r = extract("const greet = (name) => {\n return \"Hello \" + "
|
|
"name;\n};\n\nconst result = greet(\"world\");\n",
|
|
CBM_LANG_JAVASCRIPT, "test", "app.js");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->defs.count, 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Group J: language_failures_test.go ports
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
/* CommonLisp — defun extraction (known limitation: grammar produces list_lit) */
|
|
TEST(commonlisp_defun) {
|
|
CBMFileResult *r =
|
|
extract("(defun hello () \"world\")\n", CBM_LANG_COMMONLISP, "test", "hello.lisp");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
/* Known limitation: CommonLisp grammar produces list_lit, not defun nodes.
|
|
* Function extraction returns 0 — this test documents the limitation. */
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(commonlisp_multiple_functions) {
|
|
CBMFileResult *r = extract("(defun add (a b) (+ a b))\n(defun mul (a b) (* a b))\n",
|
|
CBM_LANG_COMMONLISP, "test", "math.lisp");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(commonlisp_defmacro) {
|
|
CBMFileResult *r =
|
|
extract("(defmacro when2 (condition &body body)\n `(if ,condition (progn ,@body)))\n",
|
|
CBM_LANG_COMMONLISP, "test", "macros.lisp");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(makefile_rule_as_function) {
|
|
CBMFileResult *r = extract("all:\n\t@echo hello\n", CBM_LANG_MAKEFILE, "test", "Makefile");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "all"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(makefile_multiple_targets) {
|
|
CBMFileResult *r = extract("all: main.o\n\tgcc -o all main.o\n\nbuild:\n\tgo build ./...\n",
|
|
CBM_LANG_MAKEFILE, "test", "Makefile");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "all"));
|
|
ASSERT(has_def(r, "Function", "build"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(makefile_variable_extraction) {
|
|
CBMFileResult *r =
|
|
extract("CC := gcc\nCFLAGS := -Wall\n", CBM_LANG_MAKEFILE, "test", "Makefile");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
/* Variable extraction may or may not work depending on Makefile grammar support */
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(vimscript_function_extraction) {
|
|
CBMFileResult *r = extract("function! SayHello()\n echo 'Hello'\nendfunction\n",
|
|
CBM_LANG_VIMSCRIPT, "test", "plugin.vim");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
/* VimScript function extraction may or may not produce named functions */
|
|
int fn_count = 0;
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (strcmp(r->defs.items[i].label, "Function") == 0)
|
|
fn_count++;
|
|
}
|
|
if (fn_count > 0) {
|
|
ASSERT(has_def(r, "Function", "SayHello"));
|
|
}
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(vimscript_function_without_bang) {
|
|
CBMFileResult *r = extract("function MyFunc(arg)\n return arg\nendfunction\n",
|
|
CBM_LANG_VIMSCRIPT, "test", "plugin.vim");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
int fn_count = 0;
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (strcmp(r->defs.items[i].label, "Function") == 0)
|
|
fn_count++;
|
|
}
|
|
if (fn_count > 0) {
|
|
ASSERT(has_def(r, "Function", "MyFunc"));
|
|
}
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(julia_function_extraction) {
|
|
CBMFileResult *r = extract("function hello()\n println(\"Hello, World!\")\nend\n",
|
|
CBM_LANG_JULIA, "test", "hello.jl");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
int fn_count = 0;
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (strcmp(r->defs.items[i].label, "Function") == 0)
|
|
fn_count++;
|
|
}
|
|
if (fn_count > 0) {
|
|
ASSERT(has_def(r, "Function", "hello"));
|
|
}
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(julia_function_with_args) {
|
|
CBMFileResult *r = extract("function add(a::Int, b::Int)::Int\n return a + b\nend\n",
|
|
CBM_LANG_JULIA, "test", "math.jl");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
int fn_count = 0;
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (strcmp(r->defs.items[i].label, "Function") == 0)
|
|
fn_count++;
|
|
}
|
|
if (fn_count > 0) {
|
|
ASSERT(has_def(r, "Function", "add"));
|
|
}
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Cross-cutting: Calls + Imports
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
TEST(python_calls) {
|
|
CBMFileResult *r =
|
|
extract("import os\ndef main():\n os.path.exists('/tmp')\n print('hello')\n",
|
|
CBM_LANG_PYTHON, "t", "main.py");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
/* Python unified extraction produces calls — verify at least some exist */
|
|
ASSERT_GT(r->calls.count, 0);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(python_iris_classMethodValue) {
|
|
CBMFileResult *r =
|
|
extract("import iris\n"
|
|
"iris_obj = iris.cls('%Library.ObjectScript')\n"
|
|
"def call_bfs(n):\n"
|
|
" return iris_obj.classMethodValue('Graph.KG.TraversalBFS', 'BFSFastJson', n)\n",
|
|
CBM_LANG_PYTHON, "t", "store.py");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_call(r, "Graph.KG.TraversalBFS.BFSFastJson"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(go_calls) {
|
|
CBMFileResult *r =
|
|
extract("package main\nimport \"fmt\"\nfunc main() { fmt.Println(\"hello\") }\n",
|
|
CBM_LANG_GO, "t", "main.go");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_call(r, "fmt.Println"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(python_imports) {
|
|
CBMFileResult *r =
|
|
extract("import os\nfrom sys import argv\nfrom collections import defaultdict\n",
|
|
CBM_LANG_PYTHON, "t", "main.py");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GT(r->imports.count, 0);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(js_imports) {
|
|
CBMFileResult *r = extract("import React from 'react';\nimport { useState } from "
|
|
"'react';\nconst fs = require('fs');\n",
|
|
CBM_LANG_JAVASCRIPT, "t", "app.js");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GT(r->imports.count, 0);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(go_imports) {
|
|
CBMFileResult *r =
|
|
extract("package main\n\nimport \"fmt\"\nimport (\n \"os\"\n net \"net/http\"\n)\n",
|
|
CBM_LANG_GO, "t", "main.go");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GT(r->imports.count, 0);
|
|
ASSERT(has_import(r, "fmt"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(java_imports) {
|
|
CBMFileResult *r = extract(
|
|
"import java.util.List;\nimport java.util.ArrayList;\nimport static java.lang.Math.PI;\n"
|
|
"public class Foo {}\n",
|
|
CBM_LANG_JAVA, "t", "Foo.java");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GT(r->imports.count, 0);
|
|
ASSERT(has_import(r, "java.util.List"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(rust_imports) {
|
|
CBMFileResult *r = extract(
|
|
"use std::collections::HashMap;\nuse std::io::{self, Write};\nuse serde::Serialize;\n"
|
|
"fn main() {}\n",
|
|
CBM_LANG_RUST, "t", "main.rs");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GT(r->imports.count, 0);
|
|
ASSERT(has_import(r, "std::collections::HashMap"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(c_imports) {
|
|
CBMFileResult *r = extract("#include <stdio.h>\n#include <stdlib.h>\n#include "
|
|
"\"mylib.h\"\n\nint main() { return 0; }\n",
|
|
CBM_LANG_C, "t", "main.c");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GT(r->imports.count, 0);
|
|
ASSERT(has_import(r, "stdio.h"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(ruby_imports) {
|
|
CBMFileResult *r = extract(
|
|
"require 'json'\nrequire 'net/http'\nrequire_relative 'helpers'\n\nclass Foo; end\n",
|
|
CBM_LANG_RUBY, "t", "app.rb");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GT(r->imports.count, 0);
|
|
ASSERT(has_import(r, "json"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(lua_imports) {
|
|
CBMFileResult *r = extract("local json = require(\"dkjson\")\nlocal http = "
|
|
"require(\"socket.http\")\n\nlocal function greet() end\n",
|
|
CBM_LANG_LUA, "t", "main.lua");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GT(r->imports.count, 0);
|
|
ASSERT(has_import(r, "dkjson"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(import_stress_go) {
|
|
/* Stress test: 5,000 single-line Go imports.
|
|
* Verifies O(N) behaviour — would hang indefinitely with the O(N²) loop. */
|
|
const int N = 5000;
|
|
/* Each line: import "pkg/NNNNN"\n = ~20 chars; total ~100KB */
|
|
int buf_size = N * 24 + 64;
|
|
char *src = malloc((size_t)buf_size);
|
|
ASSERT_NOT_NULL(src);
|
|
|
|
int pos = 0;
|
|
pos += snprintf(src + pos, (size_t)(buf_size - pos), "package stress\n");
|
|
for (int k = 0; k < N; k++) {
|
|
pos += snprintf(src + pos, (size_t)(buf_size - pos), "import \"pkg/%05d\"\n", k);
|
|
}
|
|
|
|
CBMFileResult *r = extract(src, CBM_LANG_GO, "t", "stress.go");
|
|
free(src);
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_EQ(r->imports.count, N);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Embedded-language import extraction
|
|
* Host grammars (Svelte, Vue, HTML, Astro) keep <script> bodies as
|
|
* raw_text — the embedded-imports walker re-parses each block with the
|
|
* JS grammar so the standard ES import extractor sees real
|
|
* import_statement nodes.
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
TEST(svelte_imports_basic) {
|
|
/* Default import + named imports + namespace import */
|
|
CBMFileResult *r = extract("<script>\n"
|
|
"import Foo from './Foo.svelte';\n"
|
|
"import { bar, baz } from '../lib/utils';\n"
|
|
"import * as helpers from './helpers';\n"
|
|
"export let value = 42;\n"
|
|
"</script>\n"
|
|
"<h1>Hello {value}</h1>\n",
|
|
CBM_LANG_SVELTE, "t", "Comp.svelte");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->imports.count, 3);
|
|
ASSERT(has_import(r, "Foo.svelte"));
|
|
ASSERT(has_import(r, "lib/utils"));
|
|
ASSERT(has_import(r, "helpers"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(svelte_imports_no_script) {
|
|
/* .svelte with no <script> block must not crash, 0 imports */
|
|
CBMFileResult *r = extract("<h1>Static page</h1>\n"
|
|
"<p>No script here.</p>\n",
|
|
CBM_LANG_SVELTE, "t", "Static.svelte");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_EQ(r->imports.count, 0);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(vue_imports_basic) {
|
|
/* Vue SFC: same document→script_element→raw_text AST structure */
|
|
CBMFileResult *r = extract("<template><div>{{ msg }}</div></template>\n"
|
|
"<script>\n"
|
|
"import MyComp from './MyComp.vue';\n"
|
|
"import { ref } from 'vue';\n"
|
|
"export default { name: 'App' };\n"
|
|
"</script>\n",
|
|
CBM_LANG_VUE, "t", "App.vue");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->imports.count, 2);
|
|
ASSERT(has_import(r, "MyComp.vue"));
|
|
ASSERT(has_import(r, "vue"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(html_imports_basic) {
|
|
/* Plain HTML with inline ES module imports — same generic walker. */
|
|
CBMFileResult *r = extract("<!DOCTYPE html><html><head>\n"
|
|
"<script type=\"module\">\n"
|
|
"import { renderApp } from './app.js';\n"
|
|
"import * as utils from './utils.js';\n"
|
|
"renderApp();\n"
|
|
"</script>\n"
|
|
"</head><body></body></html>\n",
|
|
CBM_LANG_HTML, "t", "index.html");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(r->imports.count, 2);
|
|
ASSERT(has_import(r, "app.js"));
|
|
ASSERT(has_import(r, "utils.js"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* config_extraction_test.go ports (25 tests)
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
/* --- TOML (8 tests) --- */
|
|
|
|
TEST(toml_basic_table_and_pair) {
|
|
CBMFileResult *r = extract("[database]\nhost = \"localhost\"\nport = 5432\n", CBM_LANG_TOML,
|
|
"t", "config.toml");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_EQ(count_defs_with_label(r, "Class"), 1);
|
|
ASSERT(has_def(r, "Class", "database"));
|
|
ASSERT_GTE(count_defs_with_label(r, "Variable"), 2);
|
|
ASSERT(has_def(r, "Variable", "host"));
|
|
ASSERT(has_def(r, "Variable", "port"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(toml_nested_table) {
|
|
CBMFileResult *r = extract("[server.http]\nport = 8080\n", CBM_LANG_TOML, "t", "config.toml");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(count_defs_with_label(r, "Class"), 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(toml_table_array_element) {
|
|
CBMFileResult *r = extract("[[servers]]\nname = \"alpha\"\n[[servers]]\nname = \"beta\"\n",
|
|
CBM_LANG_TOML, "t", "config.toml");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(count_defs_with_label(r, "Class"), 2);
|
|
ASSERT_GTE(count_defs_with_label(r, "Variable"), 2);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(toml_dotted_key) {
|
|
CBMFileResult *r =
|
|
extract("database.host = \"localhost\"\n", CBM_LANG_TOML, "t", "config.toml");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(count_defs_with_label(r, "Variable"), 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(toml_quoted_key) {
|
|
CBMFileResult *r = extract("\"unusual-key\" = \"value\"\n", CBM_LANG_TOML, "t", "config.toml");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(count_defs_with_label(r, "Variable"), 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(toml_empty_table) {
|
|
CBMFileResult *r = extract("[empty]\n", CBM_LANG_TOML, "t", "config.toml");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_EQ(count_defs_with_label(r, "Class"), 1);
|
|
ASSERT(has_def(r, "Class", "empty"));
|
|
ASSERT_EQ(count_defs_with_label(r, "Variable"), 0);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(toml_comments_only) {
|
|
CBMFileResult *r =
|
|
extract("# just a comment\n# another comment\n", CBM_LANG_TOML, "t", "config.toml");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_EQ(count_defs_with_label(r, "Class"), 0);
|
|
ASSERT_EQ(count_defs_with_label(r, "Variable"), 0);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(toml_boolean_and_integer_values) {
|
|
CBMFileResult *r =
|
|
extract("enabled = true\ncount = 42\nname = \"test\"\n", CBM_LANG_TOML, "t", "config.toml");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(count_defs_with_label(r, "Variable"), 3);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- INI (4 tests) --- */
|
|
|
|
TEST(ini_basic_section_and_setting) {
|
|
CBMFileResult *r =
|
|
extract("[database]\nhost = localhost\nport = 5432\n", CBM_LANG_INI, "t", "config.ini");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(count_defs_with_label(r, "Class"), 1);
|
|
ASSERT_GTE(count_defs_with_label(r, "Variable"), 2);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(ini_multiple_sections) {
|
|
CBMFileResult *r = extract("[section1]\nkey1 = val1\n[section2]\nkey2 = val2\n", CBM_LANG_INI,
|
|
"t", "config.ini");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(count_defs_with_label(r, "Class"), 2);
|
|
ASSERT_GTE(count_defs_with_label(r, "Variable"), 2);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(ini_global_keys) {
|
|
CBMFileResult *r = extract("key1 = value1\nkey2 = value2\n", CBM_LANG_INI, "t", "config.ini");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_EQ(count_defs_with_label(r, "Class"), 0);
|
|
ASSERT_GTE(count_defs_with_label(r, "Variable"), 2);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(ini_comments) {
|
|
CBMFileResult *r = extract("; comment\n# another comment\n[section]\nkey = val\n", CBM_LANG_INI,
|
|
"t", "config.ini");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(count_defs_with_label(r, "Class"), 1);
|
|
ASSERT_GTE(count_defs_with_label(r, "Variable"), 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- JSON (5 tests) --- */
|
|
|
|
TEST(json_basic_pair) {
|
|
CBMFileResult *r =
|
|
extract("{\"host\": \"localhost\", \"port\": 5432}", CBM_LANG_JSON, "t", "config.json");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(count_defs_with_label(r, "Variable"), 2);
|
|
ASSERT(has_def(r, "Variable", "host"));
|
|
ASSERT(has_def(r, "Variable", "port"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(json_nested_object) {
|
|
CBMFileResult *r = extract("{\"database\": {\"host\": \"localhost\", \"port\": 5432}}",
|
|
CBM_LANG_JSON, "t", "config.json");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(count_defs_with_label(r, "Variable"), 3);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(json_empty_object) {
|
|
CBMFileResult *r = extract("{}", CBM_LANG_JSON, "t", "config.json");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_EQ(count_defs_with_label(r, "Variable"), 0);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(json_boolean_null_values) {
|
|
CBMFileResult *r = extract("{\"enabled\": true, \"value\": null, \"name\": \"test\"}",
|
|
CBM_LANG_JSON, "t", "config.json");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(count_defs_with_label(r, "Variable"), 3);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(json_package_json_deps) {
|
|
CBMFileResult *r =
|
|
extract("{\"name\":\"pkg\",\"dependencies\":{\"express\":\"^4.0\",\"lodash\":\"^4.17\"}}",
|
|
CBM_LANG_JSON, "t", "package.json");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(count_defs_with_label(r, "Variable"), 4);
|
|
ASSERT(has_def(r, "Variable", "name"));
|
|
ASSERT(has_def(r, "Variable", "dependencies"));
|
|
ASSERT(has_def(r, "Variable", "express"));
|
|
ASSERT(has_def(r, "Variable", "lodash"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- XML (4 tests) --- */
|
|
|
|
TEST(xml_basic_element) {
|
|
CBMFileResult *r = extract(
|
|
"<?xml version=\"1.0\"?><config><database><host>localhost</host></database></config>",
|
|
CBM_LANG_XML, "t", "config.xml");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(count_defs_with_label(r, "Class"), 3);
|
|
ASSERT(has_def(r, "Class", "config"));
|
|
ASSERT(has_def(r, "Class", "database"));
|
|
ASSERT(has_def(r, "Class", "host"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(xml_self_closing_tag) {
|
|
CBMFileResult *r =
|
|
extract("<?xml version=\"1.0\"?><config><feature enabled=\"true\"/></config>", CBM_LANG_XML,
|
|
"t", "config.xml");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(count_defs_with_label(r, "Class"), 2);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(xml_empty_document) {
|
|
CBMFileResult *r = extract("<?xml version=\"1.0\"?><root/>", CBM_LANG_XML, "t", "config.xml");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(count_defs_with_label(r, "Class"), 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(xml_multiple_children) {
|
|
CBMFileResult *r =
|
|
extract("<?xml version=\"1.0\"?><servers><server/><server/><server/></servers>",
|
|
CBM_LANG_XML, "t", "config.xml");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(count_defs_with_label(r, "Class"), 4); /* servers + 3x server */
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* --- Markdown (4 tests) --- */
|
|
|
|
TEST(markdown_atx_headings) {
|
|
CBMFileResult *r =
|
|
extract("# Title\n## Section\n### Subsection\n", CBM_LANG_MARKDOWN, "t", "README.md");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(count_defs_with_label(r, "Section"), 3);
|
|
ASSERT_EQ(count_defs_with_label(r, "Class"), 0); /* Markdown: Section, not Class */
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(markdown_setext_headings) {
|
|
CBMFileResult *r =
|
|
extract("Title\n=====\nSection\n------\n", CBM_LANG_MARKDOWN, "t", "README.md");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(count_defs_with_label(r, "Section"), 2);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(markdown_heading_content) {
|
|
CBMFileResult *r = extract("# Installation Guide\n## Prerequisites\n## Setup\n",
|
|
CBM_LANG_MARKDOWN, "t", "README.md");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_GTE(count_defs_with_label(r, "Section"), 3);
|
|
ASSERT(has_def(r, "Section", "Installation Guide"));
|
|
ASSERT(has_def(r, "Section", "Prerequisites"));
|
|
ASSERT(has_def(r, "Section", "Setup"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(markdown_no_headings) {
|
|
CBMFileResult *r =
|
|
extract("Just a paragraph\n\nAnother paragraph\n", CBM_LANG_MARKDOWN, "t", "README.md");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_EQ(count_defs_with_label(r, "Section"), 0);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Python __init__.py Module QN collision regression
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
TEST(python_init_module_qn_not_collide_with_folder) {
|
|
/* Bug: __init__.py Module QN was identical to the Folder QN for the
|
|
* same directory, causing the Folder node to be overwritten when the
|
|
* Module was upserted. The Module QN must contain "__init__" to
|
|
* distinguish it from the Folder QN. */
|
|
CBMFileResult *r = extract("class Config:\n DEBUG = True\n\ndef setup():\n pass\n",
|
|
CBM_LANG_PYTHON, "proj", "mypackage/__init__.py");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
|
|
/* Module node must exist */
|
|
ASSERT_GTE(r->defs.count, 1);
|
|
ASSERT_STR_EQ(r->defs.items[0].label, "Module");
|
|
|
|
/* Module QN must contain __init__ (not be stripped to just "proj.mypackage") */
|
|
ASSERT_NOT_NULL(r->module_qn);
|
|
ASSERT_NOT_NULL(strstr(r->module_qn, "__init__"));
|
|
|
|
/* But symbols inside __init__.py should NOT have __init__ in their QN */
|
|
int found_config = 0;
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (strcmp(r->defs.items[i].name, "Config") == 0) {
|
|
ASSERT_NOT_NULL(r->defs.items[i].qualified_name);
|
|
/* Should be "proj.mypackage.Config", NOT "proj.mypackage.__init__.Config" */
|
|
ASSERT_STR_EQ(r->defs.items[i].qualified_name, "proj.mypackage.Config");
|
|
found_config = 1;
|
|
}
|
|
}
|
|
ASSERT_EQ(found_config, 1);
|
|
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(python_init_nested_module_qn) {
|
|
/* Deeply nested __init__.py — same collision must not happen */
|
|
CBMFileResult *r = extract("def greet():\n return 'hello'\n", CBM_LANG_PYTHON, "proj",
|
|
"docker-images/cloud-runs/bq-sync-api/__init__.py");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_NOT_NULL(r->module_qn);
|
|
/* Must contain __init__ to not collide with Folder QN */
|
|
ASSERT_NOT_NULL(strstr(r->module_qn, "__init__"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(js_index_module_qn_not_collide_with_folder) {
|
|
/* Same bug for JS/TS index.ts files */
|
|
CBMFileResult *r = extract("export function App() { return null; }\n", CBM_LANG_TYPESCRIPT,
|
|
"proj", "src/components/index.ts");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_NOT_NULL(r->module_qn);
|
|
/* Must contain "index" to not collide with Folder QN */
|
|
ASSERT_NOT_NULL(strstr(r->module_qn, "index"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(python_regular_module_qn_unchanged) {
|
|
/* Non-__init__.py Python files should be unaffected */
|
|
CBMFileResult *r =
|
|
extract("def helper():\n pass\n", CBM_LANG_PYTHON, "proj", "mypackage/utils.py");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT_NOT_NULL(r->module_qn);
|
|
/* Regular module QN should not contain __init__ or index */
|
|
ASSERT_STR_EQ(r->module_qn, "proj.mypackage.utils");
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* Find a definition by name; returns the item or NULL. */
|
|
static const CBMDefinition *find_def_by_name(CBMFileResult *r, const char *name) {
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (r->defs.items[i].name && strcmp(r->defs.items[i].name, name) == 0) {
|
|
return &r->defs.items[i];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static int decorators_contain(const CBMDefinition *d, const char *needle) {
|
|
if (!d || !d->decorators) {
|
|
return 0;
|
|
}
|
|
for (int i = 0; d->decorators[i]; i++) {
|
|
if (strstr(d->decorators[i], needle)) {
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Issue #382: Java Method nodes had empty decorators / signature. */
|
|
TEST(extract_java_method_annotations_issue382) {
|
|
CBMFileResult *r = extract("public class C {\n"
|
|
" @GetMapping(\"/x\")\n"
|
|
" public String cmd(String c) { return c; }\n"
|
|
"}\n",
|
|
CBM_LANG_JAVA, "t", "C.java");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
const CBMDefinition *m = find_def_by_name(r, "cmd");
|
|
ASSERT_NOT_NULL(m);
|
|
ASSERT(decorators_contain(m, "GetMapping"));
|
|
ASSERT_NOT_NULL(m->signature);
|
|
ASSERT(m->signature[0] != '\0');
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* Issue #1005: JAX-RS splits a route across two annotations (@GET carries the
|
|
* verb, a sibling @Path carries the path). Returning on the first mapping
|
|
* annotation dropped every method-level @Path, and the class-level @Path
|
|
* prefix was never recognized at all. */
|
|
TEST(extract_java_jaxrs_path_composition_issue1005) {
|
|
CBMFileResult *r = extract("import jakarta.ws.rs.GET;\n"
|
|
"import jakarta.ws.rs.Path;\n"
|
|
"@Path(\"/api/v1/widgets\")\n"
|
|
"public class WidgetResource {\n"
|
|
" @GET\n"
|
|
" public String list() { return \"\"; }\n"
|
|
" @GET\n"
|
|
" @Path(\"/count\")\n"
|
|
" public String count() { return \"\"; }\n"
|
|
"}\n",
|
|
CBM_LANG_JAVA, "t", "WidgetResource.java");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
const CBMDefinition *list = find_def_by_name(r, "list");
|
|
ASSERT_NOT_NULL(list);
|
|
ASSERT_NOT_NULL(list->route_path);
|
|
ASSERT_STR_EQ(list->route_path, "/api/v1/widgets");
|
|
ASSERT_STR_EQ(list->route_method, "GET");
|
|
const CBMDefinition *count = find_def_by_name(r, "count");
|
|
ASSERT_NOT_NULL(count);
|
|
ASSERT_NOT_NULL(count->route_path);
|
|
ASSERT_STR_EQ(count->route_path, "/api/v1/widgets/count");
|
|
ASSERT_STR_EQ(count->route_method, "GET");
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* A comment between decorators must not drop the decorators above it.
|
|
* Comments are NAMED tree-sitter nodes, so the prev-sibling walk used to stop
|
|
* at one — a documented route (@Post + @HttpCode above an explanatory comment)
|
|
* silently lost those decorators and disappeared from route/authz queries. */
|
|
TEST(extract_ts_decorators_survive_interleaved_comment) {
|
|
CBMFileResult *r = extract("class AuthController {\n"
|
|
" @Post('login')\n"
|
|
" @HttpCode(HttpStatus.OK)\n"
|
|
" // throttled per IP and per account\n"
|
|
" @Throttle({ default: { ttl: 900_000, limit: 5 } })\n"
|
|
" async login(dto: LoginDto) { return 1; }\n"
|
|
"}\n",
|
|
CBM_LANG_TYPESCRIPT, "t", "auth.controller.ts");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
const CBMDefinition *m = find_def_by_name(r, "login");
|
|
ASSERT_NOT_NULL(m);
|
|
ASSERT(decorators_contain(m, "Throttle")); /* below the comment — always worked */
|
|
ASSERT(decorators_contain(m, "HttpCode")); /* above the comment — was dropped */
|
|
ASSERT(decorators_contain(m, "Post")); /* above the comment — was dropped */
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* Find an in-body call by its raw callee text; returns the call or NULL. */
|
|
static const CBMCall *find_call_by_callee(CBMFileResult *r, const char *callee) {
|
|
for (int i = 0; i < r->calls.count; i++) {
|
|
if (r->calls.items[i].callee_name && strcmp(r->calls.items[i].callee_name, callee) == 0) {
|
|
return &r->calls.items[i];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Issue #1006: JS/TS template-literal URLs must flatten ${...} substitutions
|
|
* to the canonical "{}" placeholder, both as call arguments (HTTP_CALLS) and
|
|
* as URL-shaped string_refs collected from const/return positions. */
|
|
TEST(extract_ts_template_string_url_issue1006) {
|
|
CBMFileResult *r = extract("export function detailPath(id: string): string {\n"
|
|
" return `/api/v1/things/${id}/detail`;\n"
|
|
"}\n"
|
|
"export function load(id: string) {\n"
|
|
" return fetch(`/api/v1/things/${id}`);\n"
|
|
"}\n",
|
|
CBM_LANG_TYPESCRIPT, "t", "paths.ts");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
const CBMCall *c = find_call_by_callee(r, "fetch");
|
|
ASSERT_NOT_NULL(c);
|
|
ASSERT_NOT_NULL(c->first_string_arg);
|
|
ASSERT_STR_EQ(c->first_string_arg, "/api/v1/things/{}");
|
|
int found = 0;
|
|
for (int i = 0; i < r->string_refs.count; i++) {
|
|
if (r->string_refs.items[i].value &&
|
|
strcmp(r->string_refs.items[i].value, "/api/v1/things/{}/detail") == 0) {
|
|
found = 1;
|
|
break;
|
|
}
|
|
}
|
|
ASSERT(found);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* Issue #1249: a mux route built as `configVar + "/literal"` (Go's idiomatic
|
|
* configurable-base-path pattern) must index the literal suffix, both for a
|
|
* route registration and for an outbound URL built the same way. A real BFF
|
|
* with 47 such registrations produced only 9 Route nodes before this fix. */
|
|
TEST(extract_go_binary_concat_url_issue1249) {
|
|
CBMFileResult *r = extract("package main\n"
|
|
"import \"net/http\"\n"
|
|
"func setup(mux *http.ServeMux, base string) {\n"
|
|
" mux.HandleFunc(base+\"/login\", loginHandler)\n"
|
|
"}\n"
|
|
"func report(host string, port string) {\n"
|
|
" http.Get(\"http://\" + host + \":\" + port + \"/log\")\n"
|
|
"}\n",
|
|
CBM_LANG_GO, "t", "routes.go");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
|
|
const CBMCall *reg = find_call_by_callee(r, "mux.HandleFunc");
|
|
ASSERT_NOT_NULL(reg);
|
|
ASSERT_NOT_NULL(reg->first_string_arg);
|
|
ASSERT_STR_EQ(reg->first_string_arg, "/login");
|
|
|
|
const CBMCall *out = find_call_by_callee(r, "http.Get");
|
|
ASSERT_NOT_NULL(out);
|
|
ASSERT_NOT_NULL(out->first_string_arg);
|
|
ASSERT_STR_EQ(out->first_string_arg, "/log");
|
|
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* Same issue: when the right side of the concatenation is not itself a
|
|
* literal (`base + suffixVar`), there is no literal route to recover. The
|
|
* fix must leave this unresolved rather than fabricate a path. */
|
|
TEST(extract_go_binary_concat_url_no_literal_suffix_issue1249) {
|
|
CBMFileResult *r = extract("package main\n"
|
|
"func setup(mux *http.ServeMux, base string, suffix string) {\n"
|
|
" mux.HandleFunc(base+suffix, dynHandler)\n"
|
|
"}\n",
|
|
CBM_LANG_GO, "t", "routes.go");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
|
|
const CBMCall *reg = find_call_by_callee(r, "mux.HandleFunc");
|
|
ASSERT_NOT_NULL(reg);
|
|
ASSERT_NULL(reg->first_string_arg);
|
|
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
|
|
/* Reproduce-first: Java module QN must derive from the CONTAINING DIRECTORY, not
|
|
* the filename stem, so a top-level class `Outer` in `Outer.java` is `t.Outer`,
|
|
* NOT the doubled `t.Outer.Outer`. The nested method def QN must also equal the
|
|
* QN the textual calls-enclosing path records for an in-body call (the
|
|
* lsp_resolve join keys on exact caller_qn == enclosing_func_qn equality). */
|
|
TEST(extract_java_no_double_class_qn) {
|
|
CBMFileResult *r = extract("class Outer {\n"
|
|
" int helper(int x) { return x + 2; }\n"
|
|
" class Inner {\n"
|
|
" int run(int v) { return helper(v); }\n"
|
|
" }\n"
|
|
"}\n",
|
|
CBM_LANG_JAVA, "t", "Outer.java");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
|
|
/* Module QN is the directory (root) → just the project. */
|
|
ASSERT_NOT_NULL(r->module_qn);
|
|
ASSERT_STR_EQ(r->module_qn, "t");
|
|
|
|
/* No def QN anywhere may double the top-level class name. */
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
const char *qn = r->defs.items[i].qualified_name;
|
|
if (qn) {
|
|
ASSERT_EQ(strstr(qn, "Outer.Outer"), NULL);
|
|
}
|
|
}
|
|
|
|
/* The nested class and its method carry the single-form QN. */
|
|
const CBMDefinition *outer = find_def_by_name(r, "Outer");
|
|
ASSERT_NOT_NULL(outer);
|
|
ASSERT_STR_EQ(outer->qualified_name, "t.Outer");
|
|
|
|
const CBMDefinition *run = find_def_by_name(r, "run");
|
|
ASSERT_NOT_NULL(run);
|
|
ASSERT_STR_EQ(run->qualified_name, "t.Outer.Inner.run");
|
|
|
|
/* The in-body call to helper() must be attributed to the SAME QN as the
|
|
* method def — this is the equality the LSP cross-resolution join relies on
|
|
* for nested classes (the lsp_outer_dispatch repro). */
|
|
const CBMCall *call = find_call_by_callee(r, "helper");
|
|
ASSERT_NOT_NULL(call);
|
|
ASSERT_NOT_NULL(call->enclosing_func_qn);
|
|
ASSERT_STR_EQ(call->enclosing_func_qn, run->qualified_name);
|
|
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* Reproduce-first: Go module QN must derive from the CONTAINING DIRECTORY
|
|
* (package), not the filename stem, so a type/method in `myapp/db/conn.go`
|
|
* belongs to module `proj.myapp.db` and is NOT polluted with the `.conn.`
|
|
* filename segment. */
|
|
TEST(extract_go_no_filename_in_module_qn) {
|
|
CBMFileResult *r = extract("package db\n\n"
|
|
"type Conn struct{}\n\n"
|
|
"func (c *Conn) Query() {}\n",
|
|
CBM_LANG_GO, "proj", "myapp/db/conn.go");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
|
|
/* Module is the directory `myapp/db`, NOT `myapp/db/conn`. */
|
|
ASSERT_NOT_NULL(r->module_qn);
|
|
ASSERT_STR_EQ(r->module_qn, "proj.myapp.db");
|
|
|
|
/* The type and method QNs must not contain the filename segment `.conn.`. */
|
|
const CBMDefinition *conn = find_def_by_name(r, "Conn");
|
|
ASSERT_NOT_NULL(conn);
|
|
ASSERT_STR_EQ(conn->qualified_name, "proj.myapp.db.Conn");
|
|
|
|
/* Go method nodes keep a FLAT QN (module + name) with a separate
|
|
* parent_class link to the receiver type — the QN must carry the
|
|
* directory-based module and NOT the `.conn.` filename segment. */
|
|
const CBMDefinition *query = find_def_by_name(r, "Query");
|
|
ASSERT_NOT_NULL(query);
|
|
ASSERT_STR_EQ(query->qualified_name, "proj.myapp.db.Query");
|
|
ASSERT_EQ(strstr(query->qualified_name, ".conn."), NULL);
|
|
/* The method's parent_class must match the type node QN (for DEFINES_METHOD). */
|
|
ASSERT_NOT_NULL(query->parent_class);
|
|
ASSERT_STR_EQ(query->parent_class, "proj.myapp.db.Conn");
|
|
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* Issue #213: large TS files were indexed as a File node with zero children. */
|
|
TEST(extract_large_ts_has_functions_issue213) {
|
|
enum { NFUNCS = 4000 };
|
|
size_t cap = (size_t)NFUNCS * 80 + 64;
|
|
char *src = (char *)malloc(cap);
|
|
ASSERT_NOT_NULL(src);
|
|
size_t off = 0;
|
|
for (int i = 0; i < NFUNCS; i++) {
|
|
off +=
|
|
(size_t)snprintf(src + off, cap - off,
|
|
"export function fn%d(a: number): number { return a + %d; }\n", i, i);
|
|
}
|
|
CBMFileResult *r =
|
|
cbm_extract_file(src, (int)off, CBM_LANG_TYPESCRIPT, "t", "big.ts", 0, NULL, NULL);
|
|
ASSERT_NOT_NULL(r);
|
|
int fns = count_defs_with_label(r, "Function");
|
|
ASSERT_GT(fns, 0); /* must not silently produce zero children */
|
|
cbm_free_result(r);
|
|
free(src);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Group: per-function complexity metrics (Tier A — local AST metrics)
|
|
*
|
|
* cbm_compute_complexity stamps each Function/Method with cyclomatic,
|
|
* cognitive, loop_count and loop_depth in the same tree-sitter walk that
|
|
* extracts the definition. loop_depth (max nested-loop depth) is the
|
|
* polynomial-degree proxy used as a queryable bottleneck signal.
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
/* Return the first definition with the given name, or NULL. */
|
|
static const CBMDefinition *find_def(CBMFileResult *r, const char *name) {
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (strcmp(r->defs.items[i].name, name) == 0)
|
|
return &r->defs.items[i];
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
TEST(complexity_nested_loops_depth) {
|
|
CBMFileResult *r = extract("package p\n"
|
|
"func deepLoops() {\n"
|
|
" for i := 0; i < 10; i++ {\n"
|
|
" for j := 0; j < 10; j++ {\n"
|
|
" for k := 0; k < 10; k++ {\n"
|
|
" doWork()\n"
|
|
" }\n"
|
|
" }\n"
|
|
" }\n"
|
|
"}\n",
|
|
CBM_LANG_GO, "t", "deep.go");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
const CBMDefinition *d = find_def(r, "deepLoops");
|
|
ASSERT_NOT_NULL(d);
|
|
ASSERT_EQ(d->loop_depth, 3); /* three nested for-loops */
|
|
ASSERT_EQ(d->loop_count, 3);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(complexity_loop_with_branch) {
|
|
CBMFileResult *r = extract("package p\n"
|
|
"func single() {\n"
|
|
" for i := 0; i < 10; i++ {\n"
|
|
" if i > 5 {\n"
|
|
" doWork()\n"
|
|
" }\n"
|
|
" }\n"
|
|
"}\n",
|
|
CBM_LANG_GO, "t", "single.go");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
const CBMDefinition *d = find_def(r, "single");
|
|
ASSERT_NOT_NULL(d);
|
|
ASSERT_EQ(d->loop_depth, 1);
|
|
ASSERT_EQ(d->loop_count, 1);
|
|
/* the nested `if` contributes a branch, so cyclomatic > 1 and the
|
|
* nesting-weighted cognitive score is non-zero. */
|
|
ASSERT_GT(d->complexity, 1);
|
|
ASSERT_GT(d->cognitive, 0);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(complexity_flat_no_loops) {
|
|
CBMFileResult *r = extract("package p\n"
|
|
"func flat() {\n"
|
|
" doWork()\n"
|
|
" doMore()\n"
|
|
"}\n",
|
|
CBM_LANG_GO, "t", "flat.go");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
const CBMDefinition *d = find_def(r, "flat");
|
|
ASSERT_NOT_NULL(d);
|
|
ASSERT_EQ(d->loop_depth, 0);
|
|
ASSERT_EQ(d->loop_count, 0);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* A linear-scan call (contains) inside a loop → hidden O(n^2) signal. */
|
|
TEST(complexity_linear_scan_in_loop) {
|
|
CBMFileResult *r = extract("package p\n"
|
|
"func scanInLoop(xs []int, t int) bool {\n"
|
|
" for i := 0; i < len(xs); i++ {\n"
|
|
" if contains(xs, t) {\n"
|
|
" return true\n"
|
|
" }\n"
|
|
" }\n"
|
|
" return false\n"
|
|
"}\n",
|
|
CBM_LANG_GO, "t", "scan.go");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
const CBMDefinition *d = find_def(r, "scanInLoop");
|
|
ASSERT_NOT_NULL(d);
|
|
ASSERT_GT(d->linear_scan_in_loop, 0); /* contains() called inside the for-loop */
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* Self-call inside a loop, not guarded by any conditional → recursion_in_loop
|
|
* and unguarded_recursion both set. */
|
|
TEST(complexity_recursion_in_loop_unguarded) {
|
|
CBMFileResult *r = extract("package p\n"
|
|
"func recurInLoop(n int) {\n"
|
|
" for i := 0; i < n; i++ {\n"
|
|
" recurInLoop(n - 1)\n"
|
|
" }\n"
|
|
"}\n",
|
|
CBM_LANG_GO, "t", "recur.go");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
const CBMDefinition *d = find_def(r, "recurInLoop");
|
|
ASSERT_NOT_NULL(d);
|
|
ASSERT_TRUE(d->is_recursive);
|
|
ASSERT_TRUE(d->recursion_in_loop);
|
|
ASSERT_TRUE(d->unguarded_recursion); /* no self-call inside a conditional */
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* Self-call inside an `if` (a base-case guard) → recursive but NOT unguarded. */
|
|
TEST(complexity_guarded_recursion) {
|
|
CBMFileResult *r = extract("package p\n"
|
|
"func guarded(n int) int {\n"
|
|
" if n > 0 {\n"
|
|
" return guarded(n - 1)\n"
|
|
" }\n"
|
|
" return 0\n"
|
|
"}\n",
|
|
CBM_LANG_GO, "t", "guarded.go");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
const CBMDefinition *d = find_def(r, "guarded");
|
|
ASSERT_NOT_NULL(d);
|
|
ASSERT_TRUE(d->is_recursive);
|
|
ASSERT_FALSE(d->recursion_in_loop);
|
|
ASSERT_FALSE(d->unguarded_recursion); /* self-call is guarded by `if n > 0` */
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* #599: super().save() inside a method named save is a parent-class call —
|
|
* the receiver is super(), never self — so it must NOT flag self-recursion.
|
|
* super()-ONLY fixture: no self.save() alongside, so the assertion cannot pass
|
|
* vacuously off a genuine self-call. */
|
|
TEST(complexity_super_only_not_recursive) {
|
|
CBMFileResult *r = extract("class B(A):\n"
|
|
" def save(self):\n"
|
|
" super().save()\n",
|
|
CBM_LANG_PYTHON, "t", "super_only.py");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
const CBMDefinition *d = find_def(r, "save");
|
|
ASSERT_NOT_NULL(d);
|
|
ASSERT_FALSE(d->is_recursive); /* parent-class call, not self-recursion */
|
|
ASSERT_FALSE(d->unguarded_recursion);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* #599: a same-named call on an unrelated receiver (axios.get inside a
|
|
* function also named get) is delegation, not self-recursion. */
|
|
TEST(complexity_same_name_other_receiver_not_recursive) {
|
|
CBMFileResult *r = extract("function get(url) {\n"
|
|
" return axios.get(url);\n"
|
|
"}\n",
|
|
CBM_LANG_JAVASCRIPT, "t", "axios_get.js");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
const CBMDefinition *d = find_def(r, "get");
|
|
ASSERT_NOT_NULL(d);
|
|
ASSERT_FALSE(d->is_recursive); /* axios.get targets axios, not this fn */
|
|
ASSERT_FALSE(d->unguarded_recursion);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* Guard: genuine self-recursion through self/this receivers still trips the
|
|
* detector after the receiver-aware narrowing (#599). */
|
|
TEST(complexity_self_receiver_still_recursive) {
|
|
/* Python: self.recur() — same object. */
|
|
CBMFileResult *r = extract("class C:\n"
|
|
" def recur(self, n):\n"
|
|
" if n > 0:\n"
|
|
" self.recur(n - 1)\n",
|
|
CBM_LANG_PYTHON, "t", "self_recur.py");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
const CBMDefinition *d = find_def(r, "recur");
|
|
ASSERT_NOT_NULL(d);
|
|
ASSERT_TRUE(d->is_recursive);
|
|
ASSERT_FALSE(d->unguarded_recursion); /* guarded by `if n > 0` */
|
|
cbm_free_result(r);
|
|
|
|
/* JS: this.step() — same object. */
|
|
r = extract("class C {\n"
|
|
" step(n) {\n"
|
|
" if (n > 0) { this.step(n - 1); }\n"
|
|
" }\n"
|
|
"}\n",
|
|
CBM_LANG_JAVASCRIPT, "t", "this_step.js");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
d = find_def(r, "step");
|
|
ASSERT_NOT_NULL(d);
|
|
ASSERT_TRUE(d->is_recursive);
|
|
ASSERT_FALSE(d->unguarded_recursion); /* guarded by `if (n > 0)` */
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* #599: chained receiver — self.obj.recur() inside recur targets self's FIELD
|
|
* obj, a different object. The whole receiver chain ("self.obj") must be
|
|
* compared, not just its first segment ("self"). */
|
|
TEST(complexity_chained_receiver_not_self) {
|
|
CBMFileResult *r = extract("class C:\n"
|
|
" def recur(self, n):\n"
|
|
" self.obj.recur(n)\n",
|
|
CBM_LANG_PYTHON, "t", "chained_recur.py");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
const CBMDefinition *d = find_def(r, "recur");
|
|
ASSERT_NOT_NULL(d);
|
|
ASSERT_FALSE(d->is_recursive); /* self.obj is not self */
|
|
ASSERT_FALSE(d->unguarded_recursion);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* #599: Go method receiver — the enclosing def's own receiver identifier
|
|
* (`s` in `func (s *Store) save()`) is whitelisted dynamically from
|
|
* CBMDefinition.receiver, so s.save() still counts as self-recursion while
|
|
* s.backup.save() (a field's same-named method) does not. */
|
|
TEST(complexity_go_method_receiver_self_recursion) {
|
|
CBMFileResult *r = extract("package p\n"
|
|
"type Store struct{}\n"
|
|
"func (s *Store) save(n int) {\n"
|
|
" if n > 0 {\n"
|
|
" s.save(n - 1)\n"
|
|
" }\n"
|
|
"}\n",
|
|
CBM_LANG_GO, "t", "store.go");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
const CBMDefinition *d = find_def(r, "save");
|
|
ASSERT_NOT_NULL(d);
|
|
ASSERT_TRUE(d->is_recursive); /* s.save() == receiver s → self */
|
|
ASSERT_FALSE(d->unguarded_recursion); /* guarded by `if n > 0` */
|
|
cbm_free_result(r);
|
|
|
|
/* Same-named method on a field of the receiver: NOT self-recursion. */
|
|
r = extract("package p\n"
|
|
"type Store struct{ backup *Store }\n"
|
|
"func (s *Store) save(n int) {\n"
|
|
" s.backup.save(n)\n"
|
|
"}\n",
|
|
CBM_LANG_GO, "t", "store_backup.go");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
d = find_def(r, "save");
|
|
ASSERT_NOT_NULL(d);
|
|
ASSERT_FALSE(d->is_recursive); /* s.backup is not s */
|
|
ASSERT_FALSE(d->unguarded_recursion);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* #876: the delegation/store bucket left open after #599 — same-named calls
|
|
* whose receiver is a CALL RESULT (_get_store().get), a module singleton
|
|
* (_default.check), or a local cursor inside a loop (cur.execute) are not
|
|
* self-recursion. All three are the reporter's exact v0.8.1 false positives;
|
|
* fixed by the receiver-aware narrowing (87091ed), guarded here so the
|
|
* bucket never regresses. */
|
|
TEST(complexity_delegation_receivers_not_recursive_issue876) {
|
|
/* Store wrapper: _get_store().get() inside get — receiver is the call
|
|
* result, a different object. */
|
|
CBMFileResult *r = extract("def _get_store():\n"
|
|
" return object()\n"
|
|
"\n"
|
|
"def get(collection, record_id):\n"
|
|
" rec = _get_store().get(collection, record_id)\n"
|
|
" return rec\n",
|
|
CBM_LANG_PYTHON, "t", "soil.py");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
const CBMDefinition *d = find_def(r, "get");
|
|
ASSERT_NOT_NULL(d);
|
|
ASSERT_FALSE(d->is_recursive); /* _get_store() result is not this fn */
|
|
ASSERT_FALSE(d->unguarded_recursion);
|
|
cbm_free_result(r);
|
|
|
|
/* Module-singleton delegation: _default.check() inside check. */
|
|
r = extract("class _Checker:\n"
|
|
" def check(self, app_id, tool_name):\n"
|
|
" return (True, \"ok\")\n"
|
|
"\n"
|
|
"_default = _Checker()\n"
|
|
"\n"
|
|
"def check(app_id, tool_name):\n"
|
|
" return _default.check(app_id, tool_name)\n",
|
|
CBM_LANG_PYTHON, "t", "gleipnir.py");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
/* The module-level wrapper (not the method) must stay clean. */
|
|
d = NULL;
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (r->defs.items[i].name && strcmp(r->defs.items[i].name, "check") == 0 &&
|
|
strcmp(r->defs.items[i].label, "Function") == 0) {
|
|
d = &r->defs.items[i];
|
|
}
|
|
}
|
|
ASSERT_NOT_NULL(d);
|
|
ASSERT_FALSE(d->is_recursive); /* _default is not this fn */
|
|
ASSERT_FALSE(d->unguarded_recursion);
|
|
cbm_free_result(r);
|
|
|
|
/* Same-named method on a local, called inside a loop (willow.nuke case):
|
|
* neither recursive nor recursion_in_loop. */
|
|
r = extract("def execute(conn, paths):\n"
|
|
" for p in paths:\n"
|
|
" cur = conn.cursor()\n"
|
|
" cur.execute(\"DELETE FROM t WHERE p = %s\", (p,))\n"
|
|
" return True\n",
|
|
CBM_LANG_PYTHON, "t", "nuke.py");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
d = find_def(r, "execute");
|
|
ASSERT_NOT_NULL(d);
|
|
ASSERT_FALSE(d->is_recursive); /* cur is not this fn */
|
|
ASSERT_FALSE(d->unguarded_recursion);
|
|
ASSERT_FALSE(d->recursion_in_loop);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* Deep chained member access + parameter count structure smells. */
|
|
TEST(complexity_access_depth_and_params) {
|
|
CBMFileResult *r = extract("package p\n"
|
|
"func deepAccess(x Foo, a int, b int, c int) int {\n"
|
|
" return x.alpha.beta.gamma.delta\n"
|
|
"}\n",
|
|
CBM_LANG_GO, "t", "access.go");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
const CBMDefinition *d = find_def(r, "deepAccess");
|
|
ASSERT_NOT_NULL(d);
|
|
ASSERT_GT(d->max_access_depth, 2); /* x.alpha.beta.gamma.delta */
|
|
ASSERT_GTE(d->param_count, 3); /* x, a, b, c (grouping may vary) */
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Perl call-graph noise (#459 follow-up)
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
/* Count calls whose callee_name is exactly `name` (has_call is substring). */
|
|
static int count_calls_exact(CBMFileResult *r, const char *name) {
|
|
int n = 0;
|
|
for (int i = 0; i < r->calls.count; i++) {
|
|
if (r->calls.items[i].callee_name && strcmp(r->calls.items[i].callee_name, name) == 0)
|
|
n++;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
/* (b) A dotted config string must never be extracted as a callee. */
|
|
TEST(extract_perl_config_string_not_a_callee) {
|
|
CBMFileResult *r = extract("package C;\n"
|
|
"sub run {\n"
|
|
" my $cfg = { \"log4perl.appender.File.utf8\" => 1 };\n"
|
|
" helper();\n"
|
|
"}\n"
|
|
"sub helper { return 1; }\n"
|
|
"1;\n",
|
|
CBM_LANG_PERL, "t", "app.pl");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
/* No callee may contain a '.' (config/string tokens are rejected). */
|
|
for (int i = 0; i < r->calls.count; i++) {
|
|
ASSERT_TRUE(strchr(r->calls.items[i].callee_name, '.') == NULL);
|
|
}
|
|
/* (d) The genuine intra-file function call is still extracted. */
|
|
ASSERT_TRUE(count_calls_exact(r, "helper") >= 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* (a) A Perl builtin call is extracted as a non-method callee. Suppression of
|
|
* the resulting CALLS edge happens in the resolver (see test_registry.c /
|
|
* end-to-end); extraction itself keeps the bare builtin token. */
|
|
TEST(extract_perl_builtin_call_is_function_not_method) {
|
|
CBMFileResult *r = extract("package B;\n"
|
|
"sub run {\n"
|
|
" my @x;\n"
|
|
" push @x, 1;\n"
|
|
" keys %h;\n"
|
|
"}\n"
|
|
"1;\n",
|
|
CBM_LANG_PERL, "t", "b.pl");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
/* push / keys are extracted (they are valid identifiers) ... */
|
|
ASSERT_TRUE(has_call(r, "push"));
|
|
/* ... and crucially are NOT flagged as method calls. */
|
|
for (int i = 0; i < r->calls.count; i++) {
|
|
ASSERT_FALSE(r->calls.items[i].is_method);
|
|
}
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* (c) An arrow/method call is extracted with is_method=true so the resolver
|
|
* can suppress generic short-name matching for it. */
|
|
TEST(extract_perl_method_call_flags_is_method) {
|
|
CBMFileResult *r = extract("package M;\n"
|
|
"sub run {\n"
|
|
" my $self = shift;\n"
|
|
" $self->commit();\n"
|
|
" $dbh->commit();\n"
|
|
" helper();\n"
|
|
"}\n"
|
|
"sub helper { return 1; }\n"
|
|
"1;\n",
|
|
CBM_LANG_PERL, "t", "m.pl");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
/* Every "commit" call is a method call (is_method set). */
|
|
int commit_calls = 0;
|
|
for (int i = 0; i < r->calls.count; i++) {
|
|
if (strcmp(r->calls.items[i].callee_name, "commit") == 0) {
|
|
commit_calls++;
|
|
ASSERT_TRUE(r->calls.items[i].is_method);
|
|
}
|
|
/* The genuine function call is NOT a method. */
|
|
if (strcmp(r->calls.items[i].callee_name, "helper") == 0) {
|
|
ASSERT_FALSE(r->calls.items[i].is_method);
|
|
}
|
|
}
|
|
ASSERT_TRUE(commit_calls >= 1);
|
|
/* (d) genuine intra-file function call still extracted. */
|
|
ASSERT_TRUE(count_calls_exact(r, "helper") >= 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* Languages OUTSIDE the is_method flag set (only Perl and TS/JS/TSX set it) must
|
|
* be unaffected: a Go method call never sets is_method. */
|
|
TEST(extract_flag_exempt_method_call_not_flagged_is_method) {
|
|
CBMFileResult *r = extract("package m\n"
|
|
"func run(o Obj) { o.Commit(); helper() }\n",
|
|
CBM_LANG_GO, "t", "x.go");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
for (int i = 0; i < r->calls.count; i++) {
|
|
ASSERT_FALSE(r->calls.items[i].is_method);
|
|
}
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* TS/JS/TSX receiver-aware flag (#592/#606; same intent as the Perl flag above).
|
|
* A member call x.foo() with a non-this/super receiver is flagged is_method so
|
|
* the resolver can suppress a weak short-name match (`re.test()` must not bind a
|
|
* project `test`); a bare call is not flagged. */
|
|
TEST(extract_ts_member_call_flags_is_method) {
|
|
CBMFileResult *r = extract("const re = /^a+$/;\n"
|
|
"export function checkFormat(s: string) { return re.test(s); }\n"
|
|
"function helper() { return 1; }\n"
|
|
"export function run() { return helper(); }\n",
|
|
CBM_LANG_TYPESCRIPT, "t", "a.ts");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
int member = 0;
|
|
int bare = 0;
|
|
for (int i = 0; i < r->calls.count; i++) {
|
|
const char *cn = r->calls.items[i].callee_name;
|
|
if (strcmp(cn, "re.test") == 0) {
|
|
member++;
|
|
ASSERT_TRUE(r->calls.items[i].is_method);
|
|
}
|
|
if (strcmp(cn, "helper") == 0) {
|
|
bare++;
|
|
ASSERT_FALSE(r->calls.items[i].is_method);
|
|
}
|
|
}
|
|
ASSERT_TRUE(member >= 1); /* re.test() flagged */
|
|
ASSERT_TRUE(bare >= 1); /* helper() not flagged */
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* this/super receivers keep the enclosing-class target, where a weak
|
|
* namespace-proximity match is usually correct — so they are NOT flagged. A
|
|
* new_expression has no member receiver and is never flagged either. */
|
|
TEST(extract_ts_this_super_receiver_not_flagged) {
|
|
CBMFileResult *r = extract("class A extends B {\n"
|
|
" m() { this.helper(); super.render(); return new A(); }\n"
|
|
" helper() {}\n"
|
|
"}\n",
|
|
CBM_LANG_TYPESCRIPT, "t", "b.ts");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
/* Every call here has a self receiver (this/super) or is a new-expression,
|
|
* so NONE may be flagged. */
|
|
for (int i = 0; i < r->calls.count; i++) {
|
|
ASSERT_FALSE(r->calls.items[i].is_method);
|
|
}
|
|
/* Sanity: the this/super member calls were actually extracted. */
|
|
ASSERT_TRUE(has_call(r, "helper")); /* this.helper */
|
|
ASSERT_TRUE(has_call(r, "render")); /* super.render */
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* JS dialect behaves like TS (the flag is gated on the language set, not the
|
|
* dialect). Replaces the pre-#592 test that asserted JS never flags is_method. */
|
|
TEST(extract_js_member_call_flags_is_method) {
|
|
CBMFileResult *r =
|
|
extract("function run(o){ o.commit(); helper(); }\n", CBM_LANG_JAVASCRIPT, "t", "x.js");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
int member = 0;
|
|
int bare = 0;
|
|
for (int i = 0; i < r->calls.count; i++) {
|
|
const char *cn = r->calls.items[i].callee_name;
|
|
if (strcmp(cn, "o.commit") == 0) {
|
|
member++;
|
|
ASSERT_TRUE(r->calls.items[i].is_method);
|
|
}
|
|
if (strcmp(cn, "helper") == 0) {
|
|
bare++;
|
|
ASSERT_FALSE(r->calls.items[i].is_method);
|
|
}
|
|
}
|
|
ASSERT_TRUE(member >= 1); /* o.commit() flagged */
|
|
ASSERT_TRUE(bare >= 1); /* helper() not flagged */
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* #961: a C function whose body braces are split across #ifdef/#else
|
|
* branches (one open brace per branch, a single shared close) parses with
|
|
* an ERROR region on the raw source — both branches are present at once —
|
|
* and the defs walk silently dropped the function while its callers stayed
|
|
* (cbm_path_within_root, handle_process_kill). The preprocessed second
|
|
* pass (simplecpp picks one branch, same-file token lines stay aligned)
|
|
* must recover the definition with its original line. */
|
|
TEST(extract_c_ifdef_split_brace_fn_recovered_issue961) {
|
|
CBMFileResult *r = extract("static int a(void) { return 1; }\n"
|
|
"static int b(void) { return 2; }\n"
|
|
"int split_brace_fn(int x) {\n"
|
|
"#ifdef _WIN32\n"
|
|
" if (a() && b()) {\n"
|
|
"#else\n"
|
|
" if (a() || b()) {\n"
|
|
"#endif\n"
|
|
" x += 1;\n"
|
|
" }\n"
|
|
" return x;\n"
|
|
"}\n"
|
|
"int after_fn(int x) { return x; }\n",
|
|
CBM_LANG_C, "t", "split.c");
|
|
ASSERT_NOT_NULL(r);
|
|
const CBMDefinition *d = find_def(r, "split_brace_fn");
|
|
if (!d) {
|
|
fprintf(stderr, " [961] FAIL split_brace_fn dropped (defs walk lost the "
|
|
"#ifdef-split function)\n");
|
|
}
|
|
ASSERT_NOT_NULL(d);
|
|
ASSERT_EQ((int)d->start_line, 3);
|
|
/* Error recovery must stay localized: neighbours extract either way. */
|
|
ASSERT_NOT_NULL(find_def(r, "a"));
|
|
ASSERT_NOT_NULL(find_def(r, "after_fn"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
static const char *CPP_PREPROC_SIGNATURE_GAP_SRC =
|
|
"struct Rect {};\n"
|
|
"struct IBinder {};\n"
|
|
"struct IRegionSamplingListener {};\n"
|
|
"typedef int status_t;\n"
|
|
"\n"
|
|
"class SurfaceFlinger {\n"
|
|
"public:\n"
|
|
" status_t addRegionSamplingListener(const Rect&, const IBinder&,\n"
|
|
" const IRegionSamplingListener&, bool);\n"
|
|
" status_t addRegionSamplingListener(const Rect&, const IBinder&,\n"
|
|
" const IRegionSamplingListener&);\n"
|
|
" void commit();\n"
|
|
" void composite();\n"
|
|
"};\n"
|
|
"\n"
|
|
"#ifdef FLYME_GRAPHICS_EXTEND_LUMARGB\n"
|
|
"status_t SurfaceFlinger::addRegionSamplingListener(const Rect& samplingArea,\n"
|
|
" const IBinder& stopLayerHandle,\n"
|
|
" const IRegionSamplingListener& listener,\n"
|
|
" const bool rgbSample) {\n"
|
|
"#else\n"
|
|
"status_t SurfaceFlinger::addRegionSamplingListener(const Rect& samplingArea,\n"
|
|
" const IBinder& stopLayerHandle,\n"
|
|
" const IRegionSamplingListener& listener) "
|
|
"{\n"
|
|
"#endif\n"
|
|
" return 0;\n"
|
|
"}\n"
|
|
"\n"
|
|
"void SurfaceFlinger::commit() {}\n"
|
|
"\n"
|
|
"void SurfaceFlinger::composite() {}\n";
|
|
|
|
/* #946 fixture from the original report: both preprocessor choices must keep
|
|
* raw definitions primary while recovering later methods at original lines. */
|
|
TEST(extract_cpp_preproc_signature_gap_issue946) {
|
|
const char *defines[] = {"FLYME_GRAPHICS_EXTEND_LUMARGB", NULL};
|
|
for (int enabled = 0; enabled < 2; enabled++) {
|
|
CBMFileResult *r = cbm_extract_file(
|
|
CPP_PREPROC_SIGNATURE_GAP_SRC, (int)strlen(CPP_PREPROC_SIGNATURE_GAP_SRC), CBM_LANG_CPP,
|
|
"t", "SurfaceFlinger.cpp", 0, enabled ? defines : NULL, NULL);
|
|
ASSERT_NOT_NULL(r);
|
|
const CBMDefinition *add = find_def(r, "addRegionSamplingListener");
|
|
const CBMDefinition *commit = find_def(r, "commit");
|
|
const CBMDefinition *composite = find_def(r, "composite");
|
|
ASSERT_NOT_NULL(add);
|
|
ASSERT_NOT_NULL(commit);
|
|
ASSERT_NOT_NULL(composite);
|
|
ASSERT_EQ(add->start_line, 22u);
|
|
ASSERT_EQ(add->end_line, 27u);
|
|
ASSERT_EQ(commit->start_line, 29u);
|
|
ASSERT_EQ(commit->end_line, 29u);
|
|
ASSERT_EQ(composite->start_line, 31u);
|
|
ASSERT_EQ(composite->end_line, 31u);
|
|
cbm_free_result(r);
|
|
}
|
|
PASS();
|
|
}
|
|
|
|
/* Macro expansion can produce callable-looking AST nodes, but no callable
|
|
* definition exists in the original span; recovery must fail closed. */
|
|
TEST(extract_cpp_preproc_macro_generated_callable_skipped_issue949) {
|
|
const char *src = "#define MAKE_FN(name) int name() { return 1; }\n"
|
|
"#ifdef ENABLE_GENERATED\n"
|
|
"MAKE_FN(generated)\n"
|
|
"#endif\n"
|
|
"int visible() { return 0; }\n";
|
|
const char *defines[] = {"ENABLE_GENERATED", NULL};
|
|
CBMFileResult *r =
|
|
cbm_extract_file(src, (int)strlen(src), CBM_LANG_CPP, "t", "macro.cpp", 0, defines, NULL);
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_NULL(find_def(r, "generated"));
|
|
ASSERT_NOT_NULL(find_def(r, "visible"));
|
|
ASSERT_TRUE(r->parse_incomplete);
|
|
ASSERT_GTE(r->error_region_count, 1);
|
|
ASSERT_NOT_NULL(r->error_ranges);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* #949 follow-up: an included header shifts physical lines in simplecpp's
|
|
* expanded output. The #1050 name-on-same-line guard skipped this recoverable
|
|
* definition; explicit source ownership mapping must restore its original
|
|
* coordinates while keeping header definitions out of the main file. */
|
|
TEST(extract_c_ifdef_split_brace_after_include_remapped_issue949) {
|
|
char tmpdir[512];
|
|
snprintf(tmpdir, sizeof(tmpdir), "%s/cbm_line_map_XXXXXX", cbm_tmpdir());
|
|
ASSERT_NOT_NULL(cbm_mkdtemp(tmpdir));
|
|
|
|
char header_path[512];
|
|
snprintf(header_path, sizeof(header_path), "%s/padding.h", tmpdir);
|
|
FILE *header = cbm_fopen(header_path, "wb");
|
|
ASSERT_NOT_NULL(header);
|
|
for (int i = 0; i < 40; i++) {
|
|
ASSERT_GTE(fprintf(header, "static int header_pad_%d(void) { return %d; }\n", i, i), 0);
|
|
}
|
|
ASSERT_EQ(fclose(header), 0);
|
|
|
|
const char *includes[] = {tmpdir, NULL};
|
|
const char *src = "#include \"padding.h\"\n"
|
|
"static int before(void) { return 1; }\n"
|
|
"int shifted_split(int x) {\n"
|
|
"#ifdef _WIN32\n"
|
|
" if (before()) {\n"
|
|
"#else\n"
|
|
" if (x > 0) {\n"
|
|
"#endif\n"
|
|
" x += 1;\n"
|
|
" }\n"
|
|
" return x;\n"
|
|
"}\n"
|
|
"int after(void) { return 2; }\n";
|
|
CBMFileResult *r =
|
|
cbm_extract_file(src, (int)strlen(src), CBM_LANG_C, "t", "shifted.c", 0, NULL, includes);
|
|
cbm_unlink(header_path);
|
|
cbm_rmdir(tmpdir);
|
|
|
|
ASSERT_NOT_NULL(r);
|
|
const CBMDefinition *d = find_def(r, "shifted_split");
|
|
ASSERT_NOT_NULL(d);
|
|
ASSERT_EQ(d->start_line, 3u);
|
|
ASSERT_EQ(d->end_line, 12u);
|
|
ASSERT_NULL(find_def(r, "header_pad_0"));
|
|
ASSERT_NOT_NULL(find_def(r, "after"));
|
|
ASSERT_FALSE(r->parse_incomplete);
|
|
ASSERT_EQ(r->error_region_count, 0);
|
|
ASSERT_NULL(r->error_ranges);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* #961 inverse guard: a clean C file must not gain duplicate or phantom
|
|
* defs from the recovery path (it only engages on raw-parse ERROR regions). */
|
|
TEST(extract_c_clean_file_no_recovery_duplicates_issue961) {
|
|
CBMFileResult *r = extract("#ifdef _WIN32\n"
|
|
"static int w(void) { return 1; }\n"
|
|
"#else\n"
|
|
"static int u(void) { return 2; }\n"
|
|
"#endif\n"
|
|
"int use(int x) { return x; }\n",
|
|
CBM_LANG_C, "t", "clean.c");
|
|
ASSERT_NOT_NULL(r);
|
|
int use_count = 0;
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (r->defs.items[i].name && strcmp(r->defs.items[i].name, "use") == 0)
|
|
use_count++;
|
|
}
|
|
ASSERT_EQ(use_count, 1);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* #668: walk_defs used a fixed `walk_defs_frame_t stack[4096]` — a ~160 KB
|
|
* C-stack frame that overflowed small thread stacks (the reporter's crash was in
|
|
* the "definitions pass" on a large SQL file), and whose `top < 4096` push guards
|
|
* SILENTLY DROPPED every top-level definition past 4096. The growable heap stack
|
|
* fixes both: a file with > 4096 top-level defs must extract ALL of them. RED
|
|
* before the fix (extracted count capped near 4096), GREEN after. */
|
|
TEST(walk_defs_no_truncation_over_4096_issue668) {
|
|
enum { N = 5000 };
|
|
/* N top-level Python defs → N Function defs, all direct module children, so
|
|
* walk_defs pushes all N children at once — the >4096 truncation site. */
|
|
size_t cap = (size_t)N * 24 + 1;
|
|
char *src = (char *)malloc(cap);
|
|
ASSERT_NOT_NULL(src);
|
|
size_t off = 0;
|
|
for (int i = 0; i < N; i++) {
|
|
off += (size_t)snprintf(src + off, cap - off, "def f%d(): pass\n", i);
|
|
}
|
|
CBMFileResult *r = extract(src, CBM_LANG_PYTHON, "t", "big.py");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
int nfuncs = count_defs_with_label(r, "Function");
|
|
ASSERT(nfuncs >= N); /* all N present — not truncated at the old 4096 cap */
|
|
cbm_free_result(r);
|
|
free(src);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Suite
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
/* Rust: inline #[test]/#[tokio::test] functions must be marked is_test so the
|
|
* store.c `is_test != 1` filter excludes them from graph context. Detection is
|
|
* otherwise file-path-based (cbm_is_test_file), so test fns in a regular .rs
|
|
* file leak. (#855) */
|
|
TEST(extract_rust_test_attr_marks_is_test_issue855) {
|
|
CBMFileResult *r = extract("pub fn real_fn() {}\n"
|
|
"\n"
|
|
"#[test]\n"
|
|
"fn sync_test() {}\n"
|
|
"\n"
|
|
"#[tokio::test]\n"
|
|
"async fn async_test() {}\n",
|
|
CBM_LANG_RUST, "t", "src/lib.rs");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
|
|
int real = -1, sync = -1, asyn = -1;
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
const char *n = r->defs.items[i].name;
|
|
if (!n) {
|
|
continue;
|
|
}
|
|
if (strcmp(n, "real_fn") == 0) {
|
|
real = r->defs.items[i].is_test ? 1 : 0;
|
|
} else if (strcmp(n, "sync_test") == 0) {
|
|
sync = r->defs.items[i].is_test ? 1 : 0;
|
|
} else if (strcmp(n, "async_test") == 0) {
|
|
asyn = r->defs.items[i].is_test ? 1 : 0;
|
|
}
|
|
}
|
|
ASSERT(real >= 0 && sync >= 0 && asyn >= 0 && "all three fns extracted");
|
|
ASSERT(real == 0 && "real_fn is NOT a test");
|
|
ASSERT(sync == 1 && "#[test] fn is_test");
|
|
ASSERT(asyn == 1 && "#[tokio::test] fn is_test");
|
|
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* Function.is_test must follow the file's test-directory membership, not just
|
|
* a test_/_test basename convention: a helper under tests/ with none of those
|
|
* naming conventions (tests/helpers/fixtures.c) was previously indexed as an
|
|
* ordinary, non-test Function — invisible to store.c's `is_test != 1`
|
|
* dead-code/search filters even though trace_path's own independent path
|
|
* check already treated the same file as a test (#1294). */
|
|
TEST(extract_c_test_dir_marks_is_test_issue1294) {
|
|
const char *src = "void helper(void) {}\n";
|
|
|
|
/* Regression: a test_*-named file directly under tests/ must remain a
|
|
* test (this already worked before the fix). */
|
|
CBMFileResult *r1 = extract(src, CBM_LANG_C, "t", "tests/test_pipeline.c");
|
|
ASSERT_NOT_NULL(r1);
|
|
ASSERT_FALSE(r1->has_error);
|
|
ASSERT(has_def(r1, "Function", "helper"));
|
|
int reg = -1;
|
|
for (int i = 0; i < r1->defs.count; i++) {
|
|
if (strcmp(r1->defs.items[i].label, "Function") == 0 && r1->defs.items[i].name &&
|
|
strcmp(r1->defs.items[i].name, "helper") == 0) {
|
|
reg = r1->defs.items[i].is_test ? 1 : 0;
|
|
}
|
|
}
|
|
ASSERT(reg == 1 && "test_*.c directly under tests/ is a test (regression)");
|
|
cbm_free_result(r1);
|
|
|
|
/* Positive: a file under tests/ that matches none of the test_/_test
|
|
* naming conventions must now ALSO be a test. */
|
|
CBMFileResult *r2 = extract(src, CBM_LANG_C, "t", "tests/helpers/fixtures.c");
|
|
ASSERT_NOT_NULL(r2);
|
|
ASSERT_FALSE(r2->has_error);
|
|
ASSERT(has_def(r2, "Function", "helper"));
|
|
int nonconv = -1;
|
|
for (int i = 0; i < r2->defs.count; i++) {
|
|
if (strcmp(r2->defs.items[i].label, "Function") == 0 && r2->defs.items[i].name &&
|
|
strcmp(r2->defs.items[i].name, "helper") == 0) {
|
|
nonconv = r2->defs.items[i].is_test ? 1 : 0;
|
|
}
|
|
}
|
|
ASSERT(nonconv == 1 && "non-test_-named file under tests/ is now a test");
|
|
cbm_free_result(r2);
|
|
|
|
/* Negative: a file with no test/ directory or test_/_test naming in its
|
|
* path must never be flagged — this fix must not widen detection beyond
|
|
* the tests/ (and sibling) directory tree. */
|
|
CBMFileResult *r3 = extract(src, CBM_LANG_C, "t", "src/pipeline/helper.c");
|
|
ASSERT_NOT_NULL(r3);
|
|
ASSERT_FALSE(r3->has_error);
|
|
ASSERT(has_def(r3, "Function", "helper"));
|
|
int outside = -1;
|
|
for (int i = 0; i < r3->defs.count; i++) {
|
|
if (strcmp(r3->defs.items[i].label, "Function") == 0 && r3->defs.items[i].name &&
|
|
strcmp(r3->defs.items[i].name, "helper") == 0) {
|
|
outside = r3->defs.items[i].is_test ? 1 : 0;
|
|
}
|
|
}
|
|
ASSERT(outside == 0 && "file outside tests/ is never a test");
|
|
cbm_free_result(r3);
|
|
|
|
PASS();
|
|
}
|
|
|
|
/* Same convergence for Method definitions (push_method_def), which take a
|
|
* separate code path from free functions: a class method on a class defined
|
|
* under tests/ with no test_/_test naming (e.g. tests/helpers/base.py) must
|
|
* be marked is_test too, and a method on the same class outside tests/ must
|
|
* not be (#1294). */
|
|
TEST(extract_python_method_test_dir_marks_is_test_issue1294) {
|
|
const char *src = "class Foo:\n"
|
|
" def helper(self):\n"
|
|
" pass\n";
|
|
|
|
/* Python's LSP layer injects synthetic builtin stub Methods (str.upper,
|
|
* dict.get, ...) into defs.items alongside real ones (py_builtins.c), so
|
|
* matching must key on name, not just label="Method". */
|
|
CBMFileResult *r1 = extract(src, CBM_LANG_PYTHON, "t", "tests/helpers/base.py");
|
|
ASSERT_NOT_NULL(r1);
|
|
ASSERT_FALSE(r1->has_error);
|
|
ASSERT(has_def(r1, "Method", "helper"));
|
|
int in_tests = -1;
|
|
for (int i = 0; i < r1->defs.count; i++) {
|
|
if (strcmp(r1->defs.items[i].label, "Method") == 0 && r1->defs.items[i].name &&
|
|
strcmp(r1->defs.items[i].name, "helper") == 0) {
|
|
in_tests = r1->defs.items[i].is_test ? 1 : 0;
|
|
}
|
|
}
|
|
ASSERT(in_tests == 1 && "method on a class under tests/helpers/ is a test");
|
|
cbm_free_result(r1);
|
|
|
|
CBMFileResult *r2 = extract(src, CBM_LANG_PYTHON, "t", "app/models/base.py");
|
|
ASSERT_NOT_NULL(r2);
|
|
ASSERT_FALSE(r2->has_error);
|
|
ASSERT(has_def(r2, "Method", "helper"));
|
|
int outside_tests = -1;
|
|
for (int i = 0; i < r2->defs.count; i++) {
|
|
if (strcmp(r2->defs.items[i].label, "Method") == 0 && r2->defs.items[i].name &&
|
|
strcmp(r2->defs.items[i].name, "helper") == 0) {
|
|
outside_tests = r2->defs.items[i].is_test ? 1 : 0;
|
|
}
|
|
}
|
|
ASSERT(outside_tests == 0 && "method on a class outside tests/ is never a test");
|
|
cbm_free_result(r2);
|
|
|
|
PASS();
|
|
}
|
|
|
|
/* #1017: docstring truncation at MAX_COMMENT_LEN (500 bytes) can split a
|
|
* multi-byte UTF-8 character, leaving an incomplete byte sequence.
|
|
* Craft a Go comment whose 498th-500th bytes are a 3-byte CJK character
|
|
* (U+6210 = 成 = e6 88 90). The raw byte truncation at offset 500 lands
|
|
* one byte past the character start, splitting it. After the fix the
|
|
* truncated string must end on a complete codepoint boundary. */
|
|
TEST(docstring_utf8_truncation_boundary_issue1017) {
|
|
/* Build a comment: "// " (3 bytes) + 495 ASCII 'A' + "成成成" (9 bytes)
|
|
* Total comment text = 3 + 495 + 9 = 507 bytes.
|
|
* MAX_COMMENT_LEN = 500. The first kanji (成 = e6 88 90) occupies
|
|
* offsets 498-500, so text[500] = '\0' keeps bytes 0-499: the lead
|
|
* byte 0xe6 plus one continuation 0x88 — an incomplete 2-of-3 sequence.
|
|
* Before fix: the truncated string ended with that broken pair. */
|
|
char comment[600];
|
|
int off = 0;
|
|
comment[off++] = '/';
|
|
comment[off++] = '/';
|
|
comment[off++] = ' ';
|
|
for (int i = 0; i < 495; i++)
|
|
comment[off++] = 'A';
|
|
/* U+6210 (成) = 0xe6 0x88 0x90 — 3-byte UTF-8 */
|
|
const char *kanji = "\xe6\x88\x90";
|
|
for (int k = 0; k < 3; k++) {
|
|
memcpy(comment + off, kanji, 3);
|
|
off += 3;
|
|
}
|
|
comment[off] = '\0';
|
|
|
|
/* Wrap in a Go function so the comment becomes the docstring. */
|
|
char src[800];
|
|
snprintf(src, sizeof(src), "package main\n\n%s\nfunc Compute() {}\n", comment);
|
|
|
|
CBMFileResult *r = extract(src, CBM_LANG_GO, "test", "main.go");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "Compute"));
|
|
|
|
const char *doc = NULL;
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (strcmp(r->defs.items[i].name, "Compute") == 0) {
|
|
doc = r->defs.items[i].docstring;
|
|
break;
|
|
}
|
|
}
|
|
ASSERT_NOT_NULL(doc);
|
|
|
|
/* Verify every byte in the truncated docstring is valid UTF-8:
|
|
* no trailing incomplete multi-byte sequence. */
|
|
size_t len = strlen(doc);
|
|
ASSERT_TRUE(len <= 500);
|
|
const unsigned char *u = (const unsigned char *)doc;
|
|
size_t i = 0;
|
|
while (i < len) {
|
|
unsigned char c = u[i];
|
|
int seq_len;
|
|
if (c < 0x80)
|
|
seq_len = 1;
|
|
else if ((c & 0xE0) == 0xC0)
|
|
seq_len = 2;
|
|
else if ((c & 0xF0) == 0xE0)
|
|
seq_len = 3;
|
|
else if ((c & 0xF8) == 0xF0)
|
|
seq_len = 4;
|
|
else
|
|
FAIL("invalid UTF-8 lead byte");
|
|
ASSERT_TRUE(i + (size_t)seq_len <= len);
|
|
for (int j = 1; j < seq_len; j++)
|
|
ASSERT_TRUE((u[i + (size_t)j] & 0xC0) == 0x80);
|
|
i += (size_t)seq_len;
|
|
}
|
|
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* Reproduce-first (ms-typescript reallyLargeFile.ts, 2026-07-07): a file
|
|
* whose root node has hundreds of thousands of FLAT SIBLINGS (580k ////
|
|
* comment lines in the 3.5 MB fourslash fixture) hung extraction for over
|
|
* 15 minutes: walk_defs pushed children via index-based ts_node_child(i),
|
|
* which is O(i) per call in tree-sitter — O(n^2) per wide node (~1.7e11
|
|
* iterator steps on the real file; 100% of stack samples inside
|
|
* ts_node_child_iterator_next). The supervisor then killed the silent
|
|
* worker as a hang and, via the stale extraction marker, quarantined
|
|
* INNOCENT files on every retry.
|
|
*
|
|
* This fixture is an 80k-sibling flat file: quadratic child access needs
|
|
* minutes under ASan; the linear TSTreeCursor collection finishes in
|
|
* milliseconds. The 30 s bound has ~100x headroom over the fixed cost —
|
|
* RED on index-based child pushes, GREEN on the cursor walk. The def-count
|
|
* guard keeps the test honest: extraction must actually process the whole
|
|
* breadth, not skip it. */
|
|
/* Extract a wide-flat comment-sibling fixture of n lines (the exact shape of
|
|
* ms-typescript's reallyLargeFile.ts: hundreds of thousands of flat comment
|
|
* children under the root, plus sparse real defs so the breadth check cannot
|
|
* pass vacuously). Returns elapsed milliseconds; stores the def count. */
|
|
static long extract_wide_flat_ms(int n, int *out_defs) {
|
|
const size_t cap = (size_t)n * 24 + (size_t)8192; /* "// wide filler N\n" <= 24 chars */
|
|
char *src = malloc(cap);
|
|
if (!src) {
|
|
return -1;
|
|
}
|
|
size_t off = 0;
|
|
/* CONSTANT def count (10), independent of n: defs that scale WITH n make
|
|
* the fixture superlinear through the separate per-def sibling-scan cost
|
|
* (O(defs x siblings)) — on windows-CLANG64 ASan that pushed the ratio
|
|
* of LINEAR walk code to 43x. Ten spread-out defs keep the breadth check
|
|
* honest while the sibling-scan term stays 10 x n = linear. */
|
|
const int def_stride = n / 10;
|
|
for (int i = 0; i < n; i++) {
|
|
off += (size_t)snprintf(src + off, cap - off, "// wide filler %d\n", i);
|
|
if (i % def_stride == 0) {
|
|
off += (size_t)snprintf(src + off, cap - off, "var wide_a%d = %d;\n", i, i);
|
|
}
|
|
}
|
|
struct timespec a;
|
|
struct timespec b;
|
|
cbm_clock_gettime(CLOCK_MONOTONIC, &a);
|
|
CBMFileResult *r =
|
|
cbm_extract_file(src, (int)off, CBM_LANG_JAVASCRIPT, "proj", "wide.js", 0, NULL, NULL);
|
|
cbm_clock_gettime(CLOCK_MONOTONIC, &b);
|
|
free(src);
|
|
if (!r) {
|
|
return -1;
|
|
}
|
|
*out_defs = r->defs.count;
|
|
cbm_free_result(r);
|
|
return (b.tv_sec - a.tv_sec) * 1000L + (b.tv_nsec - a.tv_nsec) / 1000000L;
|
|
}
|
|
|
|
/* Best-of-N. Timing noise only ever ADDS time, so the minimum of a few runs is
|
|
* the cheapest good estimate of the noise-free cost. A single sample at each
|
|
* size made the RATIO carry the noise of BOTH measurements: on a loaded Windows
|
|
* VM this read 184ms -> 9387ms (51x) for code that measures ~20x unloaded, and
|
|
* tripped a bound calibrated for exactly that linear case.
|
|
*
|
|
* Deliberately NOT solved by raising WF_RATIO_MAX: the bound sits where it does
|
|
* because linear (~20x) and quadratic (~128x) are each >=2x away from it, so
|
|
* inflating it moves the test toward the very signal it exists to catch. This
|
|
* keeps the threshold and removes the variance instead. */
|
|
static long extract_wide_flat_ms_best_of(int n, int reps, int *out_defs) {
|
|
long best = -1;
|
|
for (int i = 0; i < reps; i++) {
|
|
int defs = 0;
|
|
long ms = extract_wide_flat_ms(n, &defs);
|
|
if (ms < 0) {
|
|
return ms;
|
|
}
|
|
if (best < 0 || ms < best) {
|
|
best = ms;
|
|
*out_defs = defs;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
TEST(extract_wide_flat_file_is_linear) {
|
|
/* SCALING-RATIO guard: assert the COMPLEXITY CLASS, not a wall-clock
|
|
* bound. Index-based ts_node_child(i) child loops are O(i) per call —
|
|
* quadratic per wide node — and hung a 580k-sibling file for hours
|
|
* (ms-typescript reallyLargeFile.ts). An absolute time bound conflates
|
|
* machine speed with complexity: the gcc-13-ARM ASan CI leg runs this
|
|
* extraction ~200x slower than clang at the SAME (measured perfectly
|
|
* linear: 27.1s/54.2s/108.3s for 50k/100k/200k) complexity, and flunked
|
|
* a 30s bound on linear code. Growing the input 4x must grow the time
|
|
* ~4x when linear and ~16x when quadratic; the 10x bound splits those
|
|
* decisively on every toolchain. The 120ms floor keeps clock noise from
|
|
* mattering on fast machines. */
|
|
/* Growth and bound CALIBRATED FROM MEASUREMENT, not models. The ASan
|
|
* test build carries a large LINEAR per-line baseline (~31us/line on
|
|
* clang-macOS, ~540us/line on gcc-13-ARM) that dilutes small-growth
|
|
* ratios: at 8x growth the measured quadratic ratio was 22.4 — a 24x
|
|
* bound false-passed the known-quadratic pre-merge walk. At 20x growth
|
|
* the measured ratios are ~20x for linear code (both toolchains) and
|
|
* ~128x for the quadratic walk (clang-macOS) — bound 40 sits >=2x from
|
|
* both. The 120ms floor keeps clock noise irrelevant on fast hosts. */
|
|
enum { WF_SMALL = 20 * 1000, WF_BIG = 400 * 1000, WF_RATIO_MAX = 40, WF_FLOOR_MS = 120 };
|
|
int defs_small = 0;
|
|
int defs_big = 0;
|
|
/* Small is cheap, so sample it more; big dominates runtime, so twice is the
|
|
* affordable compromise that still discards one unlucky sample. */
|
|
long t_small = extract_wide_flat_ms_best_of(WF_SMALL, 3, &defs_small);
|
|
long t_big = extract_wide_flat_ms_best_of(WF_BIG, 2, &defs_big);
|
|
ASSERT_GTE(t_small, 0);
|
|
ASSERT_GTE(t_big, 0);
|
|
/* Anti-vacuous guard: the breadth was actually walked at both sizes. */
|
|
ASSERT_GTE(defs_small, 8);
|
|
ASSERT_GTE(defs_big, 8);
|
|
fprintf(stderr, " [wide-flat] t(%d)=%ldms t(%d)=%ldms\n", WF_SMALL, t_small, WF_BIG, t_big);
|
|
long base = t_small > WF_FLOOR_MS ? t_small : WF_FLOOR_MS;
|
|
if (t_big > WF_RATIO_MAX * base) {
|
|
char msg[160];
|
|
snprintf(msg, sizeof(msg),
|
|
"wide-flat scaling 20x input: %ldms -> %ldms (> %dx base %ldms) — "
|
|
"quadratic child access",
|
|
t_small, t_big, WF_RATIO_MAX, base);
|
|
FAIL(msg);
|
|
}
|
|
PASS();
|
|
}
|
|
|
|
#if defined(CBM_CALL_REFERENCE_LOOKUP_TEST_API) && CBM_CALL_REFERENCE_LOOKUP_TEST_API
|
|
/* A flat block of value-reference statements exercises occurrence-role
|
|
* classification for every identifier. The old parent/child field lookup
|
|
* restarts at the block's first child for each statement, so 8x more source
|
|
* produces about 64x more lookup work. Count work instead of wall time: this
|
|
* RED is deterministic, sanitizer-independent, and finishes quickly even on
|
|
* the known-quadratic implementation. */
|
|
static uint64_t extract_wide_reference_field_work(int statement_count, int *out_usages,
|
|
uint64_t *out_slow_parent_fallbacks) {
|
|
static const char prefix[] = "function target() {}\nfunction wide() {\n";
|
|
static const char statement[] = " target;\n";
|
|
static const char suffix[] = "}\n";
|
|
size_t capacity = sizeof(prefix) + (size_t)statement_count * sizeof(statement) + sizeof(suffix);
|
|
char *source = malloc(capacity);
|
|
if (!source) {
|
|
return UINT64_MAX;
|
|
}
|
|
size_t offset = 0;
|
|
memcpy(source + offset, prefix, sizeof(prefix) - 1U);
|
|
offset += sizeof(prefix) - 1U;
|
|
for (int i = 0; i < statement_count; i++) {
|
|
memcpy(source + offset, statement, sizeof(statement) - 1U);
|
|
offset += sizeof(statement) - 1U;
|
|
}
|
|
memcpy(source + offset, suffix, sizeof(suffix));
|
|
offset += sizeof(suffix) - 1U;
|
|
|
|
cbm_usage_field_lookup_test_reset();
|
|
CBMFileResult *result = cbm_extract_file(source, (int)offset, CBM_LANG_JAVASCRIPT, "proj",
|
|
"wide-references.js", 0, NULL, NULL);
|
|
free(source);
|
|
if (!result) {
|
|
return UINT64_MAX;
|
|
}
|
|
int usages = 0;
|
|
for (int i = 0; i < result->usages.count; i++) {
|
|
if (result->usages.items[i].ref_name &&
|
|
strcmp(result->usages.items[i].ref_name, "target") == 0) {
|
|
usages++;
|
|
}
|
|
}
|
|
uint64_t work = cbm_usage_field_lookup_test_work();
|
|
*out_slow_parent_fallbacks = cbm_usage_slow_parent_fallback_test_count();
|
|
cbm_free_result(result);
|
|
*out_usages = usages;
|
|
return work;
|
|
}
|
|
|
|
TEST(extract_wide_flat_reference_fields_are_linear) {
|
|
enum { SMALL = 128, BIG = 1024, INPUT_GROWTH = 8, WORK_RATIO_MAX = 12 };
|
|
int small_usages = 0;
|
|
int big_usages = 0;
|
|
uint64_t small_slow_parent_fallbacks = 0;
|
|
uint64_t big_slow_parent_fallbacks = 0;
|
|
uint64_t small_work =
|
|
extract_wide_reference_field_work(SMALL, &small_usages, &small_slow_parent_fallbacks);
|
|
uint64_t big_work =
|
|
extract_wide_reference_field_work(BIG, &big_usages, &big_slow_parent_fallbacks);
|
|
ASSERT_TRUE(small_work != UINT64_MAX);
|
|
ASSERT_TRUE(big_work != UINT64_MAX);
|
|
ASSERT_EQ(small_usages, SMALL);
|
|
ASSERT_EQ(big_usages, BIG);
|
|
ASSERT_EQ(small_slow_parent_fallbacks, 0);
|
|
ASSERT_EQ(big_slow_parent_fallbacks, 0);
|
|
ASSERT_GTE(small_work, (uint64_t)SMALL);
|
|
fprintf(stderr, " [wide-reference-fields] work(%d)=%llu work(%d)=%llu input_growth=%dx\n",
|
|
SMALL, (unsigned long long)small_work, BIG, (unsigned long long)big_work, INPUT_GROWTH);
|
|
uint64_t maximum = small_work * WORK_RATIO_MAX + 256U;
|
|
if (big_work > maximum) {
|
|
char message[192];
|
|
snprintf(message, sizeof(message),
|
|
"wide-reference field lookup grew from %llu to %llu for %dx input "
|
|
"(maximum %dx + 256) -- quadratic sibling scan",
|
|
(unsigned long long)small_work, (unsigned long long)big_work, INPUT_GROWTH,
|
|
WORK_RATIO_MAX);
|
|
FAIL(message);
|
|
}
|
|
PASS();
|
|
}
|
|
#endif
|
|
|
|
/* ===================================================================
|
|
* Group H3: ObjectScript return type extraction
|
|
* =================================================================== */
|
|
|
|
TEST(objectscript_udl_method_return_type) {
|
|
CBMFileResult *r = extract("Class MyApp.Factory Extends %RegisteredObject\n"
|
|
"{\n"
|
|
"Method GetAdapter() As EnsLib.SQL.OutboundAdapter\n"
|
|
"{\n"
|
|
" Quit ##class(EnsLib.SQL.OutboundAdapter).%New()\n"
|
|
"}\n"
|
|
"}\n",
|
|
CBM_LANG_OBJECTSCRIPT_UDL, "t", "Factory.cls");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
bool found_rt = false;
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (strcmp(r->defs.items[i].name, "GetAdapter") == 0) {
|
|
ASSERT_NOT_NULL(r->defs.items[i].return_type);
|
|
ASSERT(strstr(r->defs.items[i].return_type, "EnsLib.SQL.OutboundAdapter") != NULL);
|
|
found_rt = true;
|
|
}
|
|
}
|
|
ASSERT(found_rt);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(objectscript_udl_scalar_return_type_not_resolved) {
|
|
CBMFileResult *r = extract("Class MyApp.Counter Extends %RegisteredObject\n"
|
|
"{\n"
|
|
"Method GetName() As %String\n"
|
|
"{\n"
|
|
" Quit \"hello\"\n"
|
|
"}\n"
|
|
"}\n",
|
|
CBM_LANG_OBJECTSCRIPT_UDL, "t", "Counter.cls");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (strcmp(r->defs.items[i].name, "GetName") == 0) {
|
|
ASSERT_NOT_NULL(r->defs.items[i].return_type);
|
|
ASSERT(strstr(r->defs.items[i].return_type, "%String") != NULL);
|
|
}
|
|
}
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* ===================================================================
|
|
* Group H2: ObjectScript macro expansion
|
|
* =================================================================== */
|
|
|
|
TEST(objectscript_udl_class) {
|
|
CBMFileResult *r = extract("Class MyApp.Patient Extends %Persistent\n{\n}\n",
|
|
CBM_LANG_OBJECTSCRIPT_UDL, "t", "Patient.cls");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Class", "MyApp.Patient"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(objectscript_udl_methods_after_goto_label) {
|
|
CBMFileResult *r = extract("Class Graph.KG.Test Extends %RegisteredObject\n"
|
|
"{\n"
|
|
"ClassMethod First() As %String\n"
|
|
"{\n"
|
|
" If 1 { Goto Done }\n"
|
|
"Done\n"
|
|
" Quit \"x\"\n"
|
|
"}\n"
|
|
"ClassMethod Second() As %String\n"
|
|
"{\n"
|
|
" Quit \"y\"\n"
|
|
"}\n"
|
|
"ClassMethod Third() As %String\n"
|
|
"{\n"
|
|
" Quit \"z\"\n"
|
|
"}\n"
|
|
"}\n",
|
|
CBM_LANG_OBJECTSCRIPT_UDL, "t", "Test.cls");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Class", "Graph.KG.Test"));
|
|
ASSERT(has_def(r, "Method", "First"));
|
|
ASSERT(has_def(r, "Method", "Second"));
|
|
ASSERT(has_def(r, "Method", "Third"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(objectscript_udl_methods) {
|
|
CBMFileResult *r = extract("Class MyApp.Utils Extends %RegisteredObject\n"
|
|
"{\n"
|
|
"ClassMethod Format(pVal As %String) As %String\n"
|
|
"{\n"
|
|
" Quit pVal\n"
|
|
"}\n"
|
|
"Method Save() As %Status\n"
|
|
"{\n"
|
|
" Quit ..%Save()\n"
|
|
"}\n"
|
|
"}\n",
|
|
CBM_LANG_OBJECTSCRIPT_UDL, "t", "Utils.cls");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Class", "MyApp.Utils"));
|
|
ASSERT(has_def(r, "Method", "Format"));
|
|
ASSERT(has_def(r, "Method", "Save"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(objectscript_udl_base_classes) {
|
|
CBMFileResult *r = extract("Class MyApp.Patient Extends %Persistent\n"
|
|
"{\n"
|
|
"}\n",
|
|
CBM_LANG_OBJECTSCRIPT_UDL, "t", "Patient.cls");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Class", "MyApp.Patient"));
|
|
int found = 0;
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (strcmp(r->defs.items[i].name, "MyApp.Patient") == 0) {
|
|
found = 1;
|
|
ASSERT_NOT_NULL(r->defs.items[i].base_classes);
|
|
ASSERT_NOT_NULL(r->defs.items[i].base_classes[0]);
|
|
ASSERT_STR_EQ(r->defs.items[i].base_classes[0], "%Persistent");
|
|
}
|
|
}
|
|
ASSERT_TRUE(found);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(objectscript_udl_multiple_bases) {
|
|
CBMFileResult *r = extract("Class MyApp.Dual Extends (MyApp.Base, %RegisteredObject)\n"
|
|
"{\n"
|
|
"}\n",
|
|
CBM_LANG_OBJECTSCRIPT_UDL, "t", "Dual.cls");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
int found = 0;
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (strcmp(r->defs.items[i].name, "MyApp.Dual") == 0) {
|
|
found = 1;
|
|
ASSERT_NOT_NULL(r->defs.items[i].base_classes);
|
|
ASSERT_NOT_NULL(r->defs.items[i].base_classes[0]);
|
|
ASSERT_NOT_NULL(r->defs.items[i].base_classes[1]);
|
|
}
|
|
}
|
|
ASSERT_TRUE(found);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(objectscript_udl_properties) {
|
|
CBMFileResult *r = extract("Class MyApp.Patient Extends %Persistent\n"
|
|
"{\n"
|
|
"Property Name As %String;\n"
|
|
"Property DOB As %Date;\n"
|
|
"}\n",
|
|
CBM_LANG_OBJECTSCRIPT_UDL, "t", "Patient.cls");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Class", "MyApp.Patient"));
|
|
ASSERT(has_def(r, "Variable", "Name"));
|
|
ASSERT(has_def(r, "Variable", "DOB"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(objectscript_routine_tags) {
|
|
CBMFileResult *r = extract("UTILS\n"
|
|
" Quit\n"
|
|
"\n"
|
|
"Format(value,fmt)\n"
|
|
" Set result = $ZDate(value, fmt)\n"
|
|
" Quit result\n"
|
|
"\n"
|
|
"Log(msg)\n"
|
|
" Write msg,!\n"
|
|
" Quit\n",
|
|
CBM_LANG_OBJECTSCRIPT_ROUTINE, "t", "Utils.mac");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Function", "Format"));
|
|
ASSERT(has_def(r, "Function", "Log"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(objectscript_udl_query_member) {
|
|
CBMFileResult *r =
|
|
extract("Class MyApp.Repo Extends %Persistent\n"
|
|
"{\n"
|
|
"Query FindAll(name As %String) As %SQLQuery { SELECT * FROM MyApp_Repo }\n"
|
|
"}\n",
|
|
CBM_LANG_OBJECTSCRIPT_UDL, "t", "Repo.cls");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Class", "MyApp.Repo"));
|
|
ASSERT(has_def(r, "Method", "FindAll"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(objectscript_udl_index_member) {
|
|
CBMFileResult *r = extract("Class MyApp.Repo Extends %Persistent\n"
|
|
"{\n"
|
|
"Property Name As %String;\n"
|
|
"Index NameIdx On Name;\n"
|
|
"}\n",
|
|
CBM_LANG_OBJECTSCRIPT_UDL, "t", "Repo.cls");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Index", "NameIdx"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(objectscript_udl_xdata_member) {
|
|
CBMFileResult *r = extract("Class MyApp.Service Extends %CSP.REST\n"
|
|
"{\n"
|
|
"XData UrlMap { <Routes/> }\n"
|
|
"}\n",
|
|
CBM_LANG_OBJECTSCRIPT_UDL, "t", "Service.cls");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "XData", "UrlMap"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(objectscript_udl_trigger_member) {
|
|
CBMFileResult *r = extract("Class MyApp.Log Extends %Persistent\n"
|
|
"{\n"
|
|
"Trigger AfterInsert [ Event = INSERT ] { }\n"
|
|
"}\n",
|
|
CBM_LANG_OBJECTSCRIPT_UDL, "t", "Log.cls");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Trigger", "AfterInsert"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(objectscript_udl_trigger_body_quit) {
|
|
CBMFileResult *r = extract("Class MyApp.Patient Extends %Persistent\n"
|
|
"{\n"
|
|
"Trigger OnDeleteSQL [ Event = DELETE, Time = AFTER ] {\n"
|
|
" Quit\n"
|
|
"}\n"
|
|
"}\n",
|
|
CBM_LANG_OBJECTSCRIPT_UDL, "t", "Patient.cls");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Trigger", "OnDeleteSQL"));
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (strcmp(r->defs.items[i].label, "Trigger") == 0 &&
|
|
strcmp(r->defs.items[i].name, "OnDeleteSQL") == 0) {
|
|
ASSERT_NOT_NULL(r->defs.items[i].docstring);
|
|
ASSERT(strstr(r->defs.items[i].docstring, "trigger_body") != NULL);
|
|
ASSERT(strstr(r->defs.items[i].docstring, "Quit") != NULL);
|
|
break;
|
|
}
|
|
}
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(objectscript_udl_trigger_body_tokens) {
|
|
CBMFileResult *r = extract("Class MyApp.Order Extends %Persistent\n"
|
|
"{\n"
|
|
"Trigger AfterInsert [ Event = INSERT, Time = AFTER ] {\n"
|
|
" Set id = ..%Id()\n"
|
|
" Do ##class(MyApp.Audit).Log(id)\n"
|
|
" Quit\n"
|
|
"}\n"
|
|
"}\n",
|
|
CBM_LANG_OBJECTSCRIPT_UDL, "t", "Order.cls");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Trigger", "AfterInsert"));
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (strcmp(r->defs.items[i].label, "Trigger") == 0 &&
|
|
strcmp(r->defs.items[i].name, "AfterInsert") == 0) {
|
|
ASSERT_NOT_NULL(r->defs.items[i].docstring);
|
|
ASSERT(strstr(r->defs.items[i].docstring, "trigger_body") != NULL);
|
|
ASSERT_NOT_NULL(r->defs.items[i].body_tokens);
|
|
ASSERT(strstr(r->defs.items[i].body_tokens, "Log") != NULL ||
|
|
strstr(r->defs.items[i].body_tokens, "Audit") != NULL ||
|
|
strstr(r->defs.items[i].body_tokens, "id") != NULL);
|
|
break;
|
|
}
|
|
}
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(objectscript_udl_self_call_relative_dot_method) {
|
|
CBMFileResult *r =
|
|
extract("Class HS.Flash.UpdateManager Extends Ens.BusinessProcess\n"
|
|
"{\n"
|
|
"Method MakeMRNUpToDate(pRequest As HS.Message.FlashQueueUpdate) As %Status\n"
|
|
"{\n"
|
|
" Set tSC = ..processStreamlet(pSession, pTS, tMPIID, tSourceMRN, ii)\n"
|
|
" Quit tSC\n"
|
|
"}\n"
|
|
"Method processStreamlet(pSession As %Integer) As %Status\n"
|
|
"{\n"
|
|
" Quit $$$OK\n"
|
|
"}\n"
|
|
"}\n",
|
|
CBM_LANG_OBJECTSCRIPT_UDL, "t", "UpdateManager.cls");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "Method", "MakeMRNUpToDate"));
|
|
ASSERT(has_call(r, "HS.Flash.UpdateManager.processStreamlet"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(objectscript_udl_calls_typed_new) {
|
|
CBMFileResult *r = extract("Class MyApp.Caller Extends %RegisteredObject\n"
|
|
"{\n"
|
|
"Method Run() As %Status\n"
|
|
"{\n"
|
|
" Set adapter = ##class(EnsLib.SQL.OutboundAdapter).%New()\n"
|
|
" Do adapter.ExecuteQuery(\"SELECT 1\")\n"
|
|
" Quit $$$OK\n"
|
|
"}\n"
|
|
"}\n",
|
|
CBM_LANG_OBJECTSCRIPT_UDL, "t", "Caller.cls");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_call(r, "EnsLib.SQL.OutboundAdapter.ExecuteQuery"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(objectscript_udl_ensemble_production_def_parses_items) {
|
|
CBMFileResult *r =
|
|
extract("Class Sample.Production Extends Ens.Production\n"
|
|
"{\n"
|
|
"XData ProductionDefinition\n"
|
|
"{\n"
|
|
"<Production Name=\"Sample.Production\">\n"
|
|
" <Item Name=\"MyService\" ClassName=\"Sample.Service\" Enabled=\"true\">\n"
|
|
" </Item>\n"
|
|
" <Item Name=\"MyOperation\" ClassName=\"Sample.Operation\" Enabled=\"false\">\n"
|
|
" </Item>\n"
|
|
"</Production>\n"
|
|
"}\n"
|
|
"}\n",
|
|
CBM_LANG_OBJECTSCRIPT_UDL, "t", "Production.cls");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "XData", "ProductionDefinition"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(objectscript_udl_ensemble_production_def_hs_settings) {
|
|
CBMFileResult *r = extract(
|
|
"Class HS.Flash.Production Extends Ens.Production\n"
|
|
"{\n"
|
|
"XData ProductionDefinition\n"
|
|
"{\n"
|
|
"<Production Name=\"HS.Flash.Production\">\n"
|
|
" <Item Name=\"FHIRService\" ClassName=\"HS.Flash.FHIRService\" Enabled=\"true\">\n"
|
|
" <Setting Target=\"Host\" Name=\"TargetConfigName\">FHIROps</Setting>\n"
|
|
" <Setting Target=\"Host\" Name=\"PatientHost\">PatientOps</Setting>\n"
|
|
" <Setting Target=\"Host\" Name=\"ConformanceOperation\">ConformOps</Setting>\n"
|
|
" </Item>\n"
|
|
"</Production>\n"
|
|
"}\n"
|
|
"}\n",
|
|
CBM_LANG_OBJECTSCRIPT_UDL, "t", "HSProduction.cls");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_def(r, "XData", "ProductionDefinition"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(objectscript_udl_ensemble_production_def_absent_no_error) {
|
|
CBMFileResult *r = extract("Class Sample.NonProduction Extends %Persistent\n"
|
|
"{\n"
|
|
"Method DoSomething() As %Status\n"
|
|
"{\n"
|
|
" Quit $$$OK\n"
|
|
"}\n"
|
|
"}\n",
|
|
CBM_LANG_OBJECTSCRIPT_UDL, "t", "NonProduction.cls");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(!has_def(r, "XData", "ProductionDefinition"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(objectscript_udl_calls_typed_param) {
|
|
CBMFileResult *r = extract("Class MyApp.Handler Extends %RegisteredObject\n"
|
|
"{\n"
|
|
"Method Process(req As Ens.Request) As %Status\n"
|
|
"{\n"
|
|
" Do req.Send()\n"
|
|
" Quit $$$OK\n"
|
|
"}\n"
|
|
"}\n",
|
|
CBM_LANG_OBJECTSCRIPT_UDL, "t", "Handler.cls");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_call(r, "Ens.Request.Send"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(objectscript_udl_calls_typed_property) {
|
|
CBMFileResult *r = extract("Class MyApp.Service Extends Ens.BusinessService\n"
|
|
"{\n"
|
|
"Property Adapter As EnsLib.SQL.InboundAdapter;\n"
|
|
"Method OnProcessInput() As %Status\n"
|
|
"{\n"
|
|
" Do ..Adapter.ExecuteQuery(\"SELECT 1\")\n"
|
|
" Quit $$$OK\n"
|
|
"}\n"
|
|
"}\n",
|
|
CBM_LANG_OBJECTSCRIPT_UDL, "t", "Service.cls");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_call(r, "EnsLib.SQL.InboundAdapter.ExecuteQuery"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* ===================================================================
|
|
* Group H2: ObjectScript macro expansion
|
|
* =================================================================== */
|
|
|
|
TEST(objectscript_macro_expand_system) {
|
|
CBMMacroTable mt;
|
|
cbm_macro_table_init_system(&mt);
|
|
CBMFileResult *r = extract_with_macros("Class MyApp.Caller Extends %RegisteredObject\n"
|
|
"{\n"
|
|
"Method Run(sc As %Status) As %Status\n"
|
|
"{\n"
|
|
" If $$$ISERR(sc) { Quit sc }\n"
|
|
" Quit $$$OK\n"
|
|
"}\n"
|
|
"}\n",
|
|
CBM_LANG_OBJECTSCRIPT_UDL, "t", "Caller.cls", &mt);
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_call(r, "%SYSTEM.Status.IsError"));
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Group H3: ObjectScript DATA_FLOWS argument extraction
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
static int find_call_args(const CBMFileResult *r, const char *callee, const char **out_arg0,
|
|
const char **out_arg1) {
|
|
if (out_arg0)
|
|
*out_arg0 = NULL;
|
|
if (out_arg1)
|
|
*out_arg1 = NULL;
|
|
for (int i = 0; i < r->calls.count; i++) {
|
|
if (strstr(r->calls.items[i].callee_name, callee)) {
|
|
if (out_arg0 && r->calls.items[i].arg_count > 0)
|
|
*out_arg0 = r->calls.items[i].args[0].expr;
|
|
if (out_arg1 && r->calls.items[i].arg_count > 1)
|
|
*out_arg1 = r->calls.items[i].args[1].expr;
|
|
return r->calls.items[i].arg_count;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
TEST(objectscript_data_flows_class_method_args) {
|
|
CBMFileResult *r = extract("Class MyApp.Caller Extends %RegisteredObject\n"
|
|
"{\n"
|
|
"Method Run() As %Status\n"
|
|
"{\n"
|
|
" Set sql = \"SELECT 1\"\n"
|
|
" Do ##class(MyApp.Utils).Transform(sql, \"JSON\")\n"
|
|
" Quit $$$OK\n"
|
|
"}\n"
|
|
"}\n",
|
|
CBM_LANG_OBJECTSCRIPT_UDL, "t", "Caller.cls");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_call(r, "MyApp.Utils.Transform"));
|
|
const char *arg0 = NULL;
|
|
const char *arg1 = NULL;
|
|
int argc = find_call_args(r, "MyApp.Utils.Transform", &arg0, &arg1);
|
|
ASSERT(argc == 2);
|
|
ASSERT_NOT_NULL(arg0);
|
|
ASSERT(strstr(arg0, "sql") != NULL);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(objectscript_macro_expand_local) {
|
|
CBMMacroTable mt;
|
|
cbm_macro_table_init_system(&mt);
|
|
CBMArena arena;
|
|
cbm_arena_init(&arena);
|
|
const char *inc_content = "ROUTINE MyApp.Include [Type=INC]\n"
|
|
"#define MyCheck(%sc) ##class(MyApp.Utils).Validate(%sc)\n";
|
|
cbm_parse_inc_file(&mt, &arena, inc_content);
|
|
CBMFileResult *r = extract_with_macros("Class MyApp.Caller Extends %RegisteredObject\n"
|
|
"{\n"
|
|
"Method Run(sc As %Status) As %Status\n"
|
|
"{\n"
|
|
" If $$$MyCheck(sc) { Quit $$$OK }\n"
|
|
" Quit $$$OK\n"
|
|
"}\n"
|
|
"}\n",
|
|
CBM_LANG_OBJECTSCRIPT_UDL, "t", "Caller.cls", &mt);
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_call(r, "MyApp.Utils.Validate"));
|
|
cbm_free_result(r);
|
|
cbm_arena_destroy(&arena);
|
|
PASS();
|
|
}
|
|
|
|
TEST(objectscript_macro_constant_no_extra_call) {
|
|
CBMMacroTable mt;
|
|
cbm_macro_table_init_system(&mt);
|
|
CBMArena arena;
|
|
cbm_arena_init(&arena);
|
|
const char *inc_content = "ROUTINE MyApp.Include [Type=INC]\n"
|
|
"#define MyConst 42\n";
|
|
cbm_parse_inc_file(&mt, &arena, inc_content);
|
|
CBMFileResult *r = extract_with_macros("Class MyApp.Caller Extends %RegisteredObject\n"
|
|
"{\n"
|
|
"Method Run() As %Integer\n"
|
|
"{\n"
|
|
" Set x = $$$MyConst\n"
|
|
" Quit x\n"
|
|
"}\n"
|
|
"}\n",
|
|
CBM_LANG_OBJECTSCRIPT_UDL, "t", "Caller.cls", &mt);
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(!has_call(r, "$$$MyConst"));
|
|
cbm_free_result(r);
|
|
cbm_arena_destroy(&arena);
|
|
PASS();
|
|
}
|
|
|
|
TEST(objectscript_data_flows_instance_method_args) {
|
|
CBMFileResult *r = extract("Class MyApp.Service Extends %RegisteredObject\n"
|
|
"{\n"
|
|
"Method Run() As %Status\n"
|
|
"{\n"
|
|
" Set adapter = ##class(EnsLib.SQL.OutboundAdapter).%New()\n"
|
|
" Do adapter.ExecuteQuery(\"SELECT 1\")\n"
|
|
" Quit $$$OK\n"
|
|
"}\n"
|
|
"}\n",
|
|
CBM_LANG_OBJECTSCRIPT_UDL, "t", "Service.cls");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_call(r, "EnsLib.SQL.OutboundAdapter.ExecuteQuery"));
|
|
const char *arg0 = NULL;
|
|
int argc = find_call_args(r, "EnsLib.SQL.OutboundAdapter.ExecuteQuery", &arg0, NULL);
|
|
ASSERT(argc == 1);
|
|
ASSERT_NOT_NULL(arg0);
|
|
cbm_free_result(r);
|
|
PASS();
|
|
}
|
|
|
|
/* ===================================================================
|
|
* Group H4: IRIS Export XML → UDL transcoder
|
|
* =================================================================== */
|
|
|
|
#define SIMPLE_EXPORT \
|
|
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" \
|
|
"<Export generator=\"Cache\" version=\"25\">\n" \
|
|
"<Class name=\"Test.Simple\">\n" \
|
|
"<Super>%RegisteredObject</Super>\n" \
|
|
"<Method name=\"Hello\">\n" \
|
|
"<ReturnType>%String</ReturnType>\n" \
|
|
"<Implementation><![CDATA[\n" \
|
|
"\tQuit \"hello\"\n" \
|
|
"]]></Implementation>\n" \
|
|
"</Method>\n" \
|
|
"</Class>\n" \
|
|
"</Export>\n"
|
|
|
|
TEST(iris_export_xml_simple_class) {
|
|
CBMArena arena;
|
|
cbm_arena_init(&arena);
|
|
int count = 0;
|
|
char **udl = cbm_iris_export_to_udl(&arena, SIMPLE_EXPORT, (int)strlen(SIMPLE_EXPORT), &count);
|
|
ASSERT_NOT_NULL(udl);
|
|
ASSERT(count == 1);
|
|
ASSERT_NOT_NULL(udl[0]);
|
|
ASSERT(strstr(udl[0], "Test.Simple") != NULL);
|
|
ASSERT(strstr(udl[0], "%RegisteredObject") != NULL);
|
|
ASSERT(strstr(udl[0], "Hello") != NULL);
|
|
ASSERT(strstr(udl[0], "Quit \"hello\"") != NULL);
|
|
cbm_arena_destroy(&arena);
|
|
PASS();
|
|
}
|
|
|
|
#define CLASSMETHOD_EXPORT \
|
|
"<?xml version=\"1.0\"?>\n" \
|
|
"<Export generator=\"Cache\" version=\"25\">\n" \
|
|
"<Class name=\"Test.CM\">\n" \
|
|
"<Method name=\"Run\">\n" \
|
|
"<ClassMethod>1</ClassMethod>\n" \
|
|
"<FormalSpec>pArg:%String,pFlag:%Boolean=0</FormalSpec>\n" \
|
|
"<ReturnType>%Status</ReturnType>\n" \
|
|
"<Implementation><![CDATA[\n" \
|
|
"\tQuit $$$OK\n" \
|
|
"]]></Implementation>\n" \
|
|
"</Method>\n" \
|
|
"</Class>\n" \
|
|
"</Export>\n"
|
|
|
|
TEST(iris_export_xml_classmethod) {
|
|
CBMArena arena;
|
|
cbm_arena_init(&arena);
|
|
int count = 0;
|
|
char **udl =
|
|
cbm_iris_export_to_udl(&arena, CLASSMETHOD_EXPORT, (int)strlen(CLASSMETHOD_EXPORT), &count);
|
|
ASSERT_NOT_NULL(udl);
|
|
ASSERT(count == 1);
|
|
ASSERT(strstr(udl[0], "ClassMethod") != NULL);
|
|
ASSERT(strstr(udl[0], "pArg") != NULL);
|
|
ASSERT(strstr(udl[0], "pFlag") != NULL);
|
|
ASSERT(strstr(udl[0], "%Status") != NULL);
|
|
cbm_arena_destroy(&arena);
|
|
PASS();
|
|
}
|
|
|
|
#define MEMBER_EXPORT \
|
|
"<?xml version=\"1.0\"?>\n" \
|
|
"<Export generator=\"Cache\" version=\"25\">\n" \
|
|
"<Class name=\"Test.Members\">\n" \
|
|
"<Property name=\"Name\">\n" \
|
|
"<Type>%String</Type>\n" \
|
|
"<Parameter name=\"MAXLEN\" value=\"200\"/>\n" \
|
|
"</Property>\n" \
|
|
"<Parameter name=\"VERSION\">\n" \
|
|
"<Default>1</Default>\n" \
|
|
"</Parameter>\n" \
|
|
"<Index name=\"NameIdx\">\n" \
|
|
"<Properties>Name</Properties>\n" \
|
|
"<Unique>1</Unique>\n" \
|
|
"</Index>\n" \
|
|
"</Class>\n" \
|
|
"</Export>\n"
|
|
|
|
TEST(iris_export_xml_property_parameter_index) {
|
|
CBMArena arena;
|
|
cbm_arena_init(&arena);
|
|
int count = 0;
|
|
char **udl = cbm_iris_export_to_udl(&arena, MEMBER_EXPORT, (int)strlen(MEMBER_EXPORT), &count);
|
|
ASSERT_NOT_NULL(udl);
|
|
ASSERT(count == 1);
|
|
ASSERT(strstr(udl[0], "Property Name") != NULL);
|
|
ASSERT(strstr(udl[0], "%String") != NULL);
|
|
ASSERT(strstr(udl[0], "Parameter VERSION") != NULL);
|
|
ASSERT(strstr(udl[0], "Index NameIdx") != NULL);
|
|
cbm_arena_destroy(&arena);
|
|
PASS();
|
|
}
|
|
|
|
#define CALLS_EXPORT \
|
|
"<?xml version=\"1.0\"?>\n" \
|
|
"<Export generator=\"Cache\" version=\"25\">\n" \
|
|
"<Class name=\"Test.Caller\">\n" \
|
|
"<Super>%RegisteredObject</Super>\n" \
|
|
"<Method name=\"Run\">\n" \
|
|
"<ClassMethod>1</ClassMethod>\n" \
|
|
"<ReturnType>%Status</ReturnType>\n" \
|
|
"<Implementation><![CDATA[\n" \
|
|
"\tSet obj = ##class(Target.Worker).%New()\n" \
|
|
"\tDo obj.Execute()\n" \
|
|
"\tQuit $$$OK\n" \
|
|
"]]></Implementation>\n" \
|
|
"</Method>\n" \
|
|
"</Class>\n" \
|
|
"</Export>\n"
|
|
|
|
TEST(iris_export_xml_calls_extracted) {
|
|
CBMArena arena;
|
|
cbm_arena_init(&arena);
|
|
int count = 0;
|
|
char **udl = cbm_iris_export_to_udl(&arena, CALLS_EXPORT, (int)strlen(CALLS_EXPORT), &count);
|
|
ASSERT_NOT_NULL(udl);
|
|
ASSERT(count == 1);
|
|
CBMFileResult *r = cbm_extract_file(udl[0], (int)strlen(udl[0]), CBM_LANG_OBJECTSCRIPT_UDL, "t",
|
|
"Caller.cls", 0, NULL, NULL);
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
ASSERT(has_call(r, "Target.Worker.Execute"));
|
|
cbm_free_result(r);
|
|
cbm_arena_destroy(&arena);
|
|
PASS();
|
|
}
|
|
|
|
#define MULTI_EXPORT \
|
|
"<?xml version=\"1.0\"?>\n" \
|
|
"<Export generator=\"Cache\" version=\"25\">\n" \
|
|
"<Class name=\"Test.First\">\n" \
|
|
"<Method name=\"One\"><Implementation><![CDATA[\tQuit 1\n]]></Implementation></Method>\n" \
|
|
"</Class>\n" \
|
|
"<Class name=\"Test.Second\">\n" \
|
|
"<Method name=\"Two\"><Implementation><![CDATA[\tQuit 2\n]]></Implementation></Method>\n" \
|
|
"</Class>\n" \
|
|
"</Export>\n"
|
|
|
|
TEST(iris_export_xml_multi_class) {
|
|
CBMArena arena;
|
|
cbm_arena_init(&arena);
|
|
int count = 0;
|
|
char **udl = cbm_iris_export_to_udl(&arena, MULTI_EXPORT, (int)strlen(MULTI_EXPORT), &count);
|
|
ASSERT_NOT_NULL(udl);
|
|
ASSERT(count == 2);
|
|
ASSERT(strstr(udl[0], "Test.First") != NULL || strstr(udl[1], "Test.First") != NULL);
|
|
ASSERT(strstr(udl[0], "Test.Second") != NULL || strstr(udl[1], "Test.Second") != NULL);
|
|
cbm_arena_destroy(&arena);
|
|
PASS();
|
|
}
|
|
|
|
SUITE(extraction) {
|
|
/* Initialize extraction library */
|
|
cbm_init();
|
|
|
|
/* Wide-flat-file linearity (ms-typescript hang) */
|
|
RUN_TEST(extract_wide_flat_file_is_linear);
|
|
#if defined(CBM_CALL_REFERENCE_LOOKUP_TEST_API) && CBM_CALL_REFERENCE_LOOKUP_TEST_API
|
|
RUN_TEST(extract_wide_flat_reference_fields_are_linear);
|
|
#endif
|
|
|
|
/* Perl call-graph noise (#459 follow-up) */
|
|
RUN_TEST(extract_perl_config_string_not_a_callee);
|
|
RUN_TEST(extract_perl_builtin_call_is_function_not_method);
|
|
RUN_TEST(extract_perl_method_call_flags_is_method);
|
|
RUN_TEST(extract_flag_exempt_method_call_not_flagged_is_method);
|
|
RUN_TEST(extract_ts_member_call_flags_is_method);
|
|
RUN_TEST(extract_ts_this_super_receiver_not_flagged);
|
|
RUN_TEST(extract_js_member_call_flags_is_method);
|
|
|
|
/* InterSystems ObjectScript (UDL / routine / Export XML). */
|
|
RUN_TEST(objectscript_udl_class);
|
|
RUN_TEST(objectscript_udl_methods_after_goto_label);
|
|
RUN_TEST(objectscript_udl_methods);
|
|
RUN_TEST(objectscript_udl_base_classes);
|
|
RUN_TEST(objectscript_udl_multiple_bases);
|
|
RUN_TEST(objectscript_udl_properties);
|
|
RUN_TEST(objectscript_routine_tags);
|
|
RUN_TEST(objectscript_udl_query_member);
|
|
RUN_TEST(objectscript_udl_index_member);
|
|
RUN_TEST(objectscript_udl_xdata_member);
|
|
RUN_TEST(objectscript_udl_trigger_member);
|
|
RUN_TEST(objectscript_udl_trigger_body_quit);
|
|
RUN_TEST(objectscript_udl_trigger_body_tokens);
|
|
RUN_TEST(objectscript_udl_ensemble_production_def_parses_items);
|
|
RUN_TEST(objectscript_udl_ensemble_production_def_hs_settings);
|
|
RUN_TEST(objectscript_udl_ensemble_production_def_absent_no_error);
|
|
RUN_TEST(objectscript_udl_self_call_relative_dot_method);
|
|
RUN_TEST(objectscript_udl_calls_typed_new);
|
|
RUN_TEST(objectscript_udl_calls_typed_param);
|
|
RUN_TEST(objectscript_udl_calls_typed_property);
|
|
RUN_TEST(objectscript_macro_expand_system);
|
|
RUN_TEST(objectscript_macro_expand_local);
|
|
RUN_TEST(objectscript_macro_constant_no_extra_call);
|
|
RUN_TEST(objectscript_udl_method_return_type);
|
|
RUN_TEST(objectscript_udl_scalar_return_type_not_resolved);
|
|
RUN_TEST(objectscript_data_flows_class_method_args);
|
|
RUN_TEST(objectscript_data_flows_instance_method_args);
|
|
RUN_TEST(iris_export_xml_simple_class);
|
|
RUN_TEST(iris_export_xml_classmethod);
|
|
RUN_TEST(iris_export_xml_property_parameter_index);
|
|
RUN_TEST(iris_export_xml_calls_extracted);
|
|
RUN_TEST(iris_export_xml_multi_class);
|
|
|
|
/* R box-module imports + member calls */
|
|
RUN_TEST(extract_r_box_use_imports_issue218);
|
|
RUN_TEST(extract_r_dollar_call_issue219);
|
|
RUN_TEST(extract_ts_factory_object_methods_issue341);
|
|
RUN_TEST(extract_c_macros_issue375);
|
|
RUN_TEST(extract_cpp_macros_issue375);
|
|
RUN_TEST(extract_cpp_functionlike_macro_type_arg_no_false_parse_partial_issue1071);
|
|
RUN_TEST(extract_cpp_real_in_body_error_still_flagged_issue1071);
|
|
RUN_TEST(extract_gdscript_issue186);
|
|
RUN_TEST(extract_powershell_issue35);
|
|
RUN_TEST(extract_luau_issue39);
|
|
RUN_TEST(extract_qml_issue42);
|
|
RUN_TEST(extract_cfscript_issue38);
|
|
RUN_TEST(extract_cfml_tag_issue38);
|
|
RUN_TEST(extract_helm_templates_issue338);
|
|
RUN_TEST(extract_helm_values_toplevel_issue338);
|
|
|
|
/* OOP */
|
|
RUN_TEST(java_class);
|
|
RUN_TEST(java_method);
|
|
RUN_TEST(java_interface);
|
|
RUN_TEST(java_interface_no_duplicate_function_issue1234);
|
|
RUN_TEST(java_enum_dedup_preserves_calls_issue1234);
|
|
RUN_TEST(java_class_extends_and_implements);
|
|
RUN_TEST(python_class_base_extracted_bare);
|
|
RUN_TEST(php_class);
|
|
RUN_TEST(php_function);
|
|
RUN_TEST(ruby_class);
|
|
RUN_TEST(ruby_module);
|
|
RUN_TEST(csharp_class);
|
|
RUN_TEST(csharp_interface);
|
|
RUN_TEST(swift_class);
|
|
RUN_TEST(swift_protocol);
|
|
RUN_TEST(kotlin_function);
|
|
RUN_TEST(kotlin_class);
|
|
RUN_TEST(scala_function);
|
|
RUN_TEST(scala_class);
|
|
RUN_TEST(dart_class);
|
|
RUN_TEST(groovy_class);
|
|
|
|
/* Systems */
|
|
RUN_TEST(rust_function);
|
|
RUN_TEST(rust_struct);
|
|
RUN_TEST(go_function);
|
|
RUN_TEST(go_struct);
|
|
RUN_TEST(go_interface);
|
|
RUN_TEST(zig_function);
|
|
RUN_TEST(c_function);
|
|
RUN_TEST(c_struct);
|
|
RUN_TEST(cpp_class);
|
|
|
|
/* Scripting */
|
|
RUN_TEST(python_function);
|
|
RUN_TEST(python_class);
|
|
RUN_TEST(js_function);
|
|
RUN_TEST(js_class);
|
|
RUN_TEST(ts_function);
|
|
RUN_TEST(ts_class);
|
|
RUN_TEST(body_tokens_type_identifier);
|
|
RUN_TEST(lua_function);
|
|
RUN_TEST(bash_function);
|
|
RUN_TEST(perl_function);
|
|
RUN_TEST(r_function);
|
|
|
|
/* Functional */
|
|
RUN_TEST(elixir_function);
|
|
RUN_TEST(haskell_function);
|
|
RUN_TEST(ocaml_function);
|
|
RUN_TEST(erlang_function);
|
|
|
|
/* Markup/Config */
|
|
RUN_TEST(yaml_variables);
|
|
RUN_TEST(hcl_blocks);
|
|
RUN_TEST(sql_create_table);
|
|
RUN_TEST(dockerfile_stages);
|
|
|
|
/* Scientific */
|
|
RUN_TEST(matlab_function);
|
|
RUN_TEST(lean_function);
|
|
RUN_TEST(form_procedure);
|
|
RUN_TEST(wolfram_function);
|
|
RUN_TEST(magma_function);
|
|
|
|
/* v0.5 expansion */
|
|
RUN_TEST(fsharp_function);
|
|
RUN_TEST(julia_function);
|
|
RUN_TEST(elm_function);
|
|
RUN_TEST(nix_function);
|
|
RUN_TEST(nix_defs_in_let_rooted_file);
|
|
RUN_TEST(nix_defs_in_attrset_rooted_file);
|
|
RUN_TEST(nix_defs_in_nested_let);
|
|
RUN_TEST(nix_defs_survive_function_header_let);
|
|
RUN_TEST(nix_defs_survive_function_header_attrset);
|
|
RUN_TEST(nix_defs_survive_curried_header);
|
|
RUN_TEST(nix_attrset_scope_disambiguates_leaf_names);
|
|
RUN_TEST(nix_dotted_attrpath_qualifies_like_nested);
|
|
RUN_TEST(nix_quoted_attr_name_strips_quotes);
|
|
RUN_TEST(nix_interpolated_attr_mints_no_def);
|
|
RUN_TEST(nix_module_level_bindings_mint_variables);
|
|
RUN_TEST(nix_nested_bindings_are_not_module_level);
|
|
RUN_TEST(nix_lambda_binding_is_function_not_variable);
|
|
RUN_TEST(nix_curried_lambda_mints_one_def);
|
|
RUN_TEST(fortran_function);
|
|
|
|
/* OOP/Systems variants */
|
|
RUN_TEST(swift_struct);
|
|
RUN_TEST(swift_simple_call);
|
|
RUN_TEST(swift_method_call);
|
|
RUN_TEST(swift_constructor_call);
|
|
RUN_TEST(swift_chained_call);
|
|
RUN_TEST(objc_interface);
|
|
RUN_TEST(objc_implementation);
|
|
RUN_TEST(dart_top_level_function);
|
|
RUN_TEST(rust_enum);
|
|
RUN_TEST(zig_struct);
|
|
RUN_TEST(cpp_function);
|
|
RUN_TEST(cpp_gtest_same_name_collision_issue1266);
|
|
RUN_TEST(cpp_gtest_f_unique_name_issue1266);
|
|
RUN_TEST(cpp_out_of_line_method_issue428);
|
|
RUN_TEST(cobol_paragraph);
|
|
RUN_TEST(verilog_module);
|
|
RUN_TEST(cuda_kernel);
|
|
RUN_TEST(python_decorator);
|
|
RUN_TEST(ts_interface);
|
|
RUN_TEST(tsx_component);
|
|
RUN_TEST(lua_table_method);
|
|
RUN_TEST(emacs_lisp_defun);
|
|
RUN_TEST(emacs_lisp_defvar);
|
|
RUN_TEST(haskell_data_type);
|
|
RUN_TEST(clojure_function);
|
|
|
|
/* Config/Markup */
|
|
RUN_TEST(html_elements);
|
|
RUN_TEST(sql_function);
|
|
RUN_TEST(sql_ddl_node_labels);
|
|
RUN_TEST(sql_view_lineage_usages);
|
|
RUN_TEST(sql_schema_qualified_name);
|
|
RUN_TEST(dbt_model_and_ref_lineage);
|
|
RUN_TEST(dbt_source_and_two_arg_ref);
|
|
RUN_TEST(dbt_ignores_non_dbt_jinja);
|
|
RUN_TEST(dbt_plain_sql_untouched);
|
|
RUN_TEST(meson_project);
|
|
RUN_TEST(css_rules);
|
|
RUN_TEST(scss_rules);
|
|
RUN_TEST(toml_basic);
|
|
RUN_TEST(cmake_function);
|
|
RUN_TEST(json_object);
|
|
RUN_TEST(protobuf_message);
|
|
RUN_TEST(graphql_type);
|
|
RUN_TEST(svelte_component);
|
|
RUN_TEST(vue_component);
|
|
RUN_TEST(glsl_shader);
|
|
RUN_TEST(vimscript_function);
|
|
|
|
/* Scientific extended */
|
|
RUN_TEST(matlab_parse);
|
|
RUN_TEST(matlab_call);
|
|
RUN_TEST(lean_parse);
|
|
RUN_TEST(lean_call);
|
|
RUN_TEST(lean_type_annotation_not_call);
|
|
RUN_TEST(form_parse);
|
|
RUN_TEST(form_call);
|
|
RUN_TEST(magma_procedure);
|
|
RUN_TEST(magma_parse);
|
|
RUN_TEST(magma_import);
|
|
RUN_TEST(magma_call);
|
|
RUN_TEST(magma_disambiguation);
|
|
RUN_TEST(wolfram_function_extended);
|
|
RUN_TEST(wolfram_call);
|
|
RUN_TEST(wolfram_caller_attribution);
|
|
RUN_TEST(c_caller_attribution);
|
|
RUN_TEST(cpp_out_of_line_method_caller_attribution);
|
|
RUN_TEST(cpp_out_of_line_ctor_dtor_caller_attribution);
|
|
RUN_TEST(wolfram_parse);
|
|
RUN_TEST(wolfram_import);
|
|
RUN_TEST(wolfram_nested_def);
|
|
|
|
/* cbm_test.go ports */
|
|
RUN_TEST(python_docstring);
|
|
RUN_TEST(go_function_extraction);
|
|
RUN_TEST(js_arrow_function);
|
|
|
|
/* language_failures_test.go ports */
|
|
RUN_TEST(commonlisp_defun);
|
|
RUN_TEST(commonlisp_multiple_functions);
|
|
RUN_TEST(commonlisp_defmacro);
|
|
RUN_TEST(makefile_rule_as_function);
|
|
RUN_TEST(makefile_multiple_targets);
|
|
RUN_TEST(makefile_variable_extraction);
|
|
RUN_TEST(vimscript_function_extraction);
|
|
RUN_TEST(vimscript_function_without_bang);
|
|
RUN_TEST(julia_function_extraction);
|
|
RUN_TEST(julia_function_with_args);
|
|
|
|
/* Cross-cutting */
|
|
RUN_TEST(python_calls);
|
|
RUN_TEST(python_iris_classMethodValue);
|
|
RUN_TEST(go_calls);
|
|
RUN_TEST(python_imports);
|
|
RUN_TEST(js_imports);
|
|
RUN_TEST(go_imports);
|
|
RUN_TEST(java_imports);
|
|
RUN_TEST(rust_imports);
|
|
RUN_TEST(c_imports);
|
|
RUN_TEST(ruby_imports);
|
|
RUN_TEST(lua_imports);
|
|
RUN_TEST(import_stress_go);
|
|
RUN_TEST(svelte_imports_basic);
|
|
RUN_TEST(svelte_imports_no_script);
|
|
RUN_TEST(vue_imports_basic);
|
|
RUN_TEST(html_imports_basic);
|
|
|
|
/* config_extraction_test.go ports */
|
|
RUN_TEST(toml_basic_table_and_pair);
|
|
RUN_TEST(toml_nested_table);
|
|
RUN_TEST(toml_table_array_element);
|
|
RUN_TEST(toml_dotted_key);
|
|
RUN_TEST(toml_quoted_key);
|
|
RUN_TEST(toml_empty_table);
|
|
RUN_TEST(toml_comments_only);
|
|
RUN_TEST(toml_boolean_and_integer_values);
|
|
RUN_TEST(ini_basic_section_and_setting);
|
|
RUN_TEST(ini_multiple_sections);
|
|
RUN_TEST(ini_global_keys);
|
|
RUN_TEST(ini_comments);
|
|
RUN_TEST(json_basic_pair);
|
|
RUN_TEST(json_nested_object);
|
|
RUN_TEST(json_empty_object);
|
|
RUN_TEST(json_boolean_null_values);
|
|
RUN_TEST(json_package_json_deps);
|
|
RUN_TEST(xml_basic_element);
|
|
RUN_TEST(xml_self_closing_tag);
|
|
RUN_TEST(xml_empty_document);
|
|
RUN_TEST(xml_multiple_children);
|
|
RUN_TEST(markdown_atx_headings);
|
|
RUN_TEST(markdown_setext_headings);
|
|
RUN_TEST(markdown_heading_content);
|
|
RUN_TEST(markdown_no_headings);
|
|
|
|
/* __init__.py / index.ts Module QN collision regression */
|
|
RUN_TEST(python_init_module_qn_not_collide_with_folder);
|
|
RUN_TEST(python_init_nested_module_qn);
|
|
RUN_TEST(js_index_module_qn_not_collide_with_folder);
|
|
RUN_TEST(python_regular_module_qn_unchanged);
|
|
RUN_TEST(extract_java_method_annotations_issue382);
|
|
RUN_TEST(extract_java_jaxrs_path_composition_issue1005);
|
|
RUN_TEST(extract_ts_template_string_url_issue1006);
|
|
RUN_TEST(extract_go_binary_concat_url_issue1249);
|
|
RUN_TEST(extract_go_binary_concat_url_no_literal_suffix_issue1249);
|
|
RUN_TEST(extract_java_no_double_class_qn);
|
|
RUN_TEST(extract_go_no_filename_in_module_qn);
|
|
RUN_TEST(extract_large_ts_has_functions_issue213);
|
|
|
|
/* Per-function complexity metrics (Tier A) */
|
|
RUN_TEST(complexity_nested_loops_depth);
|
|
RUN_TEST(complexity_loop_with_branch);
|
|
RUN_TEST(complexity_flat_no_loops);
|
|
RUN_TEST(complexity_linear_scan_in_loop);
|
|
RUN_TEST(complexity_recursion_in_loop_unguarded);
|
|
RUN_TEST(complexity_guarded_recursion);
|
|
RUN_TEST(complexity_super_only_not_recursive);
|
|
RUN_TEST(complexity_same_name_other_receiver_not_recursive);
|
|
RUN_TEST(complexity_self_receiver_still_recursive);
|
|
RUN_TEST(complexity_chained_receiver_not_self);
|
|
RUN_TEST(complexity_go_method_receiver_self_recursion);
|
|
RUN_TEST(complexity_delegation_receivers_not_recursive_issue876);
|
|
RUN_TEST(complexity_access_depth_and_params);
|
|
RUN_TEST(extract_c_ifdef_split_brace_fn_recovered_issue961);
|
|
RUN_TEST(extract_cpp_preproc_signature_gap_issue946);
|
|
RUN_TEST(extract_cpp_preproc_macro_generated_callable_skipped_issue949);
|
|
RUN_TEST(extract_c_ifdef_split_brace_after_include_remapped_issue949);
|
|
RUN_TEST(extract_c_clean_file_no_recovery_duplicates_issue961);
|
|
RUN_TEST(walk_defs_no_truncation_over_4096_issue668);
|
|
RUN_TEST(extract_rust_test_attr_marks_is_test_issue855);
|
|
RUN_TEST(extract_c_test_dir_marks_is_test_issue1294);
|
|
RUN_TEST(extract_python_method_test_dir_marks_is_test_issue1294);
|
|
RUN_TEST(docstring_utf8_truncation_boundary_issue1017);
|
|
RUN_TEST(extract_ts_decorators_survive_interleaved_comment);
|
|
|
|
cbm_shutdown();
|
|
}
|