mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-27 06:53:11 +08:00
fix: preserve semantic graph relationships
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
+55
-13
@@ -66,13 +66,19 @@ SANITIZE = -fsanitize=address,undefined -fno-omit-frame-pointer
|
||||
EDITOR_TEST_DEFINES = -DCBM_JSON_LIKE_ENABLE_TEST_API=1 \
|
||||
-DCBM_TOML_EDIT_ENABLE_TEST_API=1 -DCBM_YAML_ENABLE_TEST_API=1 \
|
||||
-DCBM_TEXT_EDIT_ENABLE_TEST_API=1 -DCBM_CLI_ENABLE_TEST_API=1
|
||||
CFLAGS_TEST = $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) -g -O1 $(SANITIZE)
|
||||
KOTLIN_DEDUP_TEST_DEFINE = -DCBM_KOTLIN_DEDUP_TEST_API=1
|
||||
CALL_REFERENCE_LOOKUP_TEST_DEFINE = -DCBM_CALL_REFERENCE_LOOKUP_TEST_API=1
|
||||
INCREMENTAL_TEST_DEFINE = -DCBM_INCREMENTAL_TEST_API=1
|
||||
CFLAGS_TEST = $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) $(KOTLIN_DEDUP_TEST_DEFINE) \
|
||||
$(CALL_REFERENCE_LOOKUP_TEST_DEFINE) $(INCREMENTAL_TEST_DEFINE) \
|
||||
-g -O1 $(SANITIZE)
|
||||
CXXFLAGS_TEST = $(CXXFLAGS_COMMON) -g -O1 $(SANITIZE)
|
||||
|
||||
# TSan (can't combine with ASan)
|
||||
TSAN_SANITIZE = -fsanitize=thread -fno-omit-frame-pointer
|
||||
CFLAGS_TSAN = $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) -g -O1 \
|
||||
$(TSAN_SANITIZE)
|
||||
CFLAGS_TSAN = $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) $(KOTLIN_DEDUP_TEST_DEFINE) \
|
||||
$(CALL_REFERENCE_LOOKUP_TEST_DEFINE) $(INCREMENTAL_TEST_DEFINE) \
|
||||
-g -O1 $(TSAN_SANITIZE)
|
||||
CXXFLAGS_TSAN = $(CXXFLAGS_COMMON) -g -O1 \
|
||||
$(TSAN_SANITIZE)
|
||||
|
||||
@@ -156,6 +162,14 @@ LSP_UNITY_DEPS = $(CBM_DIR)/lsp_all.c \
|
||||
|
||||
# Tree-sitter runtime
|
||||
TS_RUNTIME_SRC = $(CBM_DIR)/ts_runtime.c
|
||||
# ts_runtime.c is a unity wrapper that textually includes the vendored runtime.
|
||||
# Track those included sources and headers explicitly: without compiler-generated
|
||||
# dependencies, changing stack.c (including its GLR recursion cap) otherwise
|
||||
# leaves every runtime object stale while the final binary is merely relinked.
|
||||
TS_RUNTIME_DEPS = $(TS_RUNTIME_SRC) \
|
||||
$(wildcard $(TS_SRC)/*.c $(TS_SRC)/*.h \
|
||||
$(TS_SRC)/portable/*.h $(TS_SRC)/unicode/*.h \
|
||||
$(TS_INCLUDE)/tree_sitter/*.h)
|
||||
|
||||
# All 64 grammar shim files
|
||||
GRAMMAR_SRCS = $(wildcard $(CBM_DIR)/grammar_*.c)
|
||||
@@ -357,7 +371,7 @@ TEST_DISCOVER_SRCS = \
|
||||
|
||||
TEST_GRAPH_BUFFER_SRCS = tests/test_graph_buffer.c
|
||||
|
||||
TEST_PIPELINE_SRCS = tests/test_registry.c tests/test_pipeline.c tests/test_fqn.c tests/test_route_canon.c tests/test_path_alias.c tests/test_configlink.c tests/test_infrascan.c tests/test_worker_pool.c tests/test_parallel.c tests/test_index_resilience.c
|
||||
TEST_PIPELINE_SRCS = tests/test_registry.c tests/test_pipeline.c tests/test_fqn.c tests/test_route_canon.c tests/test_path_alias.c tests/test_configlink.c tests/test_infrascan.c tests/test_worker_pool.c tests/test_parallel.c tests/test_index_resilience.c tests/test_call_reference_contract.c tests/repro/repro_call_scope_usages.c tests/repro/repro_call_argument_usages.c tests/repro/repro_reference_precision.c tests/repro/repro_lexical_binding_precision.c tests/repro/repro_call_argument_matrix_a.c tests/repro/repro_call_argument_matrix_b.c tests/repro/repro_call_node_behaviors.c tests/repro/repro_language_registry.c tests/repro/repro_call_node_manifest.c tests/repro/repro_lsp_ordered_signatures.c tests/repro/repro_lsp_ordered_local.c tests/repro/repro_ts_overload_return_chains.c tests/repro/repro_harness_cleanup.c tests/repro/repro_runner_filter.c
|
||||
|
||||
TEST_WATCHER_SRCS = tests/test_watcher.c
|
||||
|
||||
@@ -423,12 +437,26 @@ TEST_SIMHASH_SRCS = tests/test_simhash.c
|
||||
TEST_STACK_OVERFLOW_SRCS = tests/test_stack_overflow.c
|
||||
|
||||
# Cumulative BUG-REPRODUCTION suite (separate runner, NOT in ALL_TEST_SRCS).
|
||||
# These cases are RED by design (one open bug each) — see tests/repro/repro_main.c.
|
||||
# Contains RED reproductions plus GREEN controls — see tests/repro/repro_main.c.
|
||||
# Kept out of the gating `make test` so `ci-ok` stays green; run via `make test-repro`.
|
||||
TEST_REPRO_SRCS = \
|
||||
tests/repro/repro_main.c \
|
||||
tests/repro/repro_parallel_determinism.c \
|
||||
tests/repro/repro_extraction.c \
|
||||
tests/repro/repro_runner_filter.c \
|
||||
tests/repro/repro_harness_cleanup.c \
|
||||
tests/repro/repro_language_registry.c \
|
||||
tests/repro/repro_call_node_manifest.c \
|
||||
tests/repro/repro_call_scope_usages.c \
|
||||
tests/repro/repro_call_argument_usages.c \
|
||||
tests/repro/repro_lsp_ordered_signatures.c \
|
||||
tests/repro/repro_lsp_ordered_local.c \
|
||||
tests/repro/repro_ts_overload_return_chains.c \
|
||||
tests/repro/repro_reference_precision.c \
|
||||
tests/repro/repro_lexical_binding_precision.c \
|
||||
tests/repro/repro_call_argument_matrix_a.c \
|
||||
tests/repro/repro_call_argument_matrix_b.c \
|
||||
tests/repro/repro_call_node_behaviors.c \
|
||||
tests/repro/repro_issue495.c \
|
||||
tests/repro/repro_issue521.c \
|
||||
tests/repro/repro_issue382.c \
|
||||
@@ -485,6 +513,11 @@ TEST_REPRO_SRCS = \
|
||||
tests/repro/repro_lsp_java_cs.c \
|
||||
tests/repro/repro_lsp_kt_php_rust.c
|
||||
|
||||
TEST_SHARED_HEADERS = tests/test_framework.h tests/test_helpers.h tests/grammar_cases.h \
|
||||
tests/repro/repro_harness.h tests/repro/repro_runner.h
|
||||
TEST_REPRO_HEADERS = $(wildcard tests/repro/*.h) tests/test_framework.h tests/test_helpers.h
|
||||
PROD_HEADERS = $(wildcard src/*.h src/*/*.h $(CBM_DIR)/*.h $(CBM_DIR)/lsp/*.h)
|
||||
|
||||
ALL_TEST_SRCS =$(TEST_FOUNDATION_SRCS) $(TEST_EXTRACTION_SRCS) $(TEST_STORE_SRCS) $(TEST_CYPHER_SRCS) $(TEST_MCP_SRCS) $(TEST_DISCOVER_SRCS) $(TEST_GRAPH_BUFFER_SRCS) $(TEST_PIPELINE_SRCS) $(TEST_WATCHER_SRCS) $(TEST_LZ4_SRCS) $(TEST_ZSTD_SRCS) $(TEST_ARTIFACT_SRCS) $(TEST_SQLITE_WRITER_SRCS) $(TEST_GO_LSP_SRCS) $(TEST_C_LSP_SRCS) $(TEST_PHP_LSP_SRCS) $(TEST_CS_LSP_SRCS) $(TEST_CS_LSP_BENCH_SRCS) $(TEST_PERL_LSP_SRCS) $(TEST_SCOPE_SRCS) $(TEST_TYPE_REP_SRCS) $(TEST_PY_LSP_SRCS) $(TEST_PY_LSP_BENCH_SRCS) $(TEST_PY_LSP_STRESS_SRCS) $(TEST_PY_LSP_SCALE_SRCS) $(TEST_TS_LSP_SRCS) $(TEST_JAVA_LSP_SRCS) $(TEST_KOTLIN_LSP_SRCS) $(TEST_RUST_LSP_SRCS) $(TEST_TRACES_SRCS) $(TEST_CLI_SRCS) $(TEST_MEM_SRCS) $(TEST_UI_SRCS) $(TEST_HTTPD_SRCS) $(TEST_SECURITY_SRCS) $(TEST_YAML_SRCS) $(TEST_SEMANTIC_SRCS) $(TEST_AST_PROFILE_SRCS) $(TEST_SLAB_ALLOC_SRCS) $(TEST_SIMHASH_SRCS) $(TEST_STACK_OVERFLOW_SRCS) $(TEST_INTEGRATION_SRCS)
|
||||
|
||||
|
||||
@@ -531,7 +564,7 @@ test-foundation: $(BUILD_DIR)/test-foundation
|
||||
$(BUILD_DIR)/%.o: $(CBM_DIR)/%.c | $(BUILD_DIR)
|
||||
$(CC) $(GRAMMAR_CFLAGS_TEST) -c -o $@ $<
|
||||
|
||||
$(BUILD_DIR)/ts_runtime.o: $(CBM_DIR)/ts_runtime.c | $(BUILD_DIR)
|
||||
$(BUILD_DIR)/ts_runtime.o: $(TS_RUNTIME_DEPS) | $(BUILD_DIR)
|
||||
$(CC) $(GRAMMAR_CFLAGS_TEST) -c -o $@ $<
|
||||
|
||||
$(BUILD_DIR)/lsp_all.o: $(LSP_UNITY_DEPS) | $(BUILD_DIR)
|
||||
@@ -543,7 +576,7 @@ $(BUILD_DIR)/preprocessor.o: $(CBM_DIR)/preprocessor.cpp | $(BUILD_DIR)
|
||||
$(BUILD_DIR)/tsan_%.o: $(CBM_DIR)/%.c | $(BUILD_DIR)
|
||||
$(CC) $(GRAMMAR_CFLAGS_TSAN) -c -o $@ $<
|
||||
|
||||
$(BUILD_DIR)/tsan_ts_runtime.o: $(CBM_DIR)/ts_runtime.c | $(BUILD_DIR)
|
||||
$(BUILD_DIR)/tsan_ts_runtime.o: $(TS_RUNTIME_DEPS) | $(BUILD_DIR)
|
||||
$(CC) $(GRAMMAR_CFLAGS_TSAN) -c -o $@ $<
|
||||
|
||||
$(BUILD_DIR)/tsan_lsp_all.o: $(LSP_UNITY_DEPS) | $(BUILD_DIR)
|
||||
@@ -629,7 +662,7 @@ $(UNIXCODER_OBJ): $(UNIXCODER_BLOB_SRC) vendored/nomic/code_vectors.bin | $(BUIL
|
||||
OBJS_VENDORED_TEST = $(MIMALLOC_OBJ_TEST) $(SQLITE3_OBJ_TEST) $(TRE_OBJ_TEST) $(GRAMMAR_OBJS_TEST) $(TS_RUNTIME_OBJ_TEST) $(LSP_OBJ_TEST) $(PP_OBJ_TEST) $(LZ4_OBJ_TEST) $(ZSTD_OBJ_TEST) $(UNIXCODER_OBJ)
|
||||
OBJS_VENDORED_TSAN = $(MIMALLOC_OBJ_TSAN) $(SQLITE3_OBJ_TSAN) $(TRE_OBJ_TSAN) $(GRAMMAR_OBJS_TSAN) $(TS_RUNTIME_OBJ_TSAN) $(LSP_OBJ_TSAN) $(PP_OBJ_TSAN) $(LZ4_OBJ_TSAN) $(ZSTD_OBJ_TSAN) $(UNIXCODER_OBJ)
|
||||
|
||||
$(BUILD_DIR)/test-runner: $(ALL_TEST_SRCS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TEST) | $(BUILD_DIR)
|
||||
$(BUILD_DIR)/test-runner: $(ALL_TEST_SRCS) $(TEST_SHARED_HEADERS) $(PROD_HEADERS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TEST) | $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS_TEST) -Itests -Itests/repro -o $@ \
|
||||
$(ALL_TEST_SRCS) $(PROD_SRCS) \
|
||||
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \
|
||||
@@ -653,11 +686,11 @@ test-focused: $(BUILD_DIR)/test-runner
|
||||
(echo "TEST_SUITES is required for test-focused"; exit 2)
|
||||
cd $(CURDIR) && $(BUILD_DIR)/test-runner $(TEST_SUITES)
|
||||
|
||||
# ── Cumulative bug-reproduction runner (RED by design, non-gating) ──
|
||||
# ── Cumulative bug-reproduction runner (RED board by design, non-gating) ──
|
||||
# Mirrors test-runner's link line but uses repro_main.c (own main + counters)
|
||||
# and TEST_REPRO_SRCS instead of ALL_TEST_SRCS. Exits non-zero while any bug is
|
||||
# still reproduced (the expected state); bug-repro.yml surfaces it as a board.
|
||||
$(BUILD_DIR)/test-repro-runner: $(TEST_REPRO_SRCS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TEST) | $(BUILD_DIR)
|
||||
$(BUILD_DIR)/test-repro-runner: $(TEST_REPRO_SRCS) $(TEST_REPRO_HEADERS) $(PROD_HEADERS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TEST) | $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS_TEST) -Itests -o $@ \
|
||||
$(TEST_REPRO_SRCS) $(PROD_SRCS) \
|
||||
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \
|
||||
@@ -665,6 +698,15 @@ $(BUILD_DIR)/test-repro-runner: $(TEST_REPRO_SRCS) $(PROD_SRCS) $(EXTRACTION_SRC
|
||||
$(LDFLAGS_TEST)
|
||||
|
||||
test-repro: $(BUILD_DIR)/test-repro-runner
|
||||
cd $(CURDIR) && bash tests/repro/repro_script_summary.sh
|
||||
cd $(CURDIR) && bash tests/repro/repro_script_zero_selection.sh
|
||||
cd $(CURDIR) && bash tests/repro/repro_script_missing_summary.sh
|
||||
cd $(CURDIR) && bash tests/repro/repro_script_skipped_summary.sh
|
||||
cd $(CURDIR) && bash tests/repro/repro_runner_zero_selection.sh
|
||||
cd $(CURDIR) && bash tests/repro/repro_runner_passing_semantics.sh
|
||||
cd $(CURDIR) && bash tests/repro/repro_script_no_workspace_artifact.sh
|
||||
cd $(CURDIR) && bash tests/repro/repro_script_stderr_summary.sh
|
||||
cd $(CURDIR) && bash tests/repro/repro_make_tracks_headers.sh
|
||||
cd $(CURDIR) && $(BUILD_DIR)/test-repro-runner
|
||||
|
||||
# ── TSan full test ───────────────────────────────────────────────
|
||||
@@ -672,7 +714,7 @@ test-repro: $(BUILD_DIR)/test-repro-runner
|
||||
TEST_TSAN_SUITES ?= mem slab_alloc parallel
|
||||
TSAN_OPTIONS ?= halt_on_error=1
|
||||
|
||||
$(BUILD_DIR)/test-runner-tsan: $(ALL_TEST_SRCS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TSAN) | $(BUILD_DIR)
|
||||
$(BUILD_DIR)/test-runner-tsan: $(ALL_TEST_SRCS) $(TEST_SHARED_HEADERS) $(PROD_HEADERS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TSAN) | $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS_TSAN) -Itests -Itests/repro -o $@ \
|
||||
$(ALL_TEST_SRCS) $(PROD_SRCS) \
|
||||
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \
|
||||
@@ -693,7 +735,7 @@ PP_OBJ_PROD = $(BUILD_DIR)/prod_preprocessor.o
|
||||
$(BUILD_DIR)/prod_%.o: $(CBM_DIR)/%.c | $(BUILD_DIR)
|
||||
$(CC) $(GRAMMAR_CFLAGS) -c -o $@ $<
|
||||
|
||||
$(BUILD_DIR)/prod_ts_runtime.o: $(CBM_DIR)/ts_runtime.c | $(BUILD_DIR)
|
||||
$(BUILD_DIR)/prod_ts_runtime.o: $(TS_RUNTIME_DEPS) | $(BUILD_DIR)
|
||||
$(CC) $(GRAMMAR_CFLAGS) -c -o $@ $<
|
||||
|
||||
$(BUILD_DIR)/prod_lsp_all.o: $(LSP_UNITY_DEPS) | $(BUILD_DIR)
|
||||
@@ -718,7 +760,7 @@ OBJS_VENDORED_PROD = $(MIMALLOC_OBJ_PROD) $(SQLITE3_OBJ_PROD) $(TRE_OBJ_PROD) $(
|
||||
|
||||
MAIN_SRC = src/main.c
|
||||
|
||||
$(BUILD_DIR)/codebase-memory-mcp: $(MAIN_SRC) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_PROD) | $(BUILD_DIR)
|
||||
$(BUILD_DIR)/codebase-memory-mcp: $(MAIN_SRC) $(PROD_HEADERS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_PROD) | $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS_PROD) -o $@ \
|
||||
$(MAIN_SRC) $(PROD_SRCS) \
|
||||
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \
|
||||
|
||||
@@ -175,7 +175,10 @@ Removes owned agent config entries, skills, hooks, instructions, and the install
|
||||
- **Cross-repo architecture summary** combining services, routes, and dependencies across the indexed fleet
|
||||
|
||||
### Edge types (selected)
|
||||
- `CALLS`, `IMPORTS`, `DEFINES`, `IMPLEMENTS`, `INHERITS`
|
||||
- `CALLS` — a callable is invoked at the source site
|
||||
- `CALL_REFERENCE` — a callable is used at a supported reference site (for example, a direct value argument) and resolves to one exact target
|
||||
- `USAGE` — an identifier is used, but a unique callable target is not proven (including ambiguous or complex expressions)
|
||||
- `IMPORTS`, `DEFINES`, `IMPLEMENTS`, `INHERITS`
|
||||
- `HTTP_CALLS`, `ASYNC_CALLS` (cross-service)
|
||||
- `EMITS`, `LISTENS_ON` (channels)
|
||||
- `DATA_FLOWS` with arg-to-param mapping + field access chains
|
||||
@@ -568,7 +571,7 @@ codebase-memory-mcp cli --raw search_graph '{"project": "my-project", "label": "
|
||||
|
||||
### Edge Types
|
||||
|
||||
`CONTAINS_PACKAGE`, `CONTAINS_FOLDER`, `CONTAINS_FILE`, `DEFINES`, `DEFINES_METHOD`, `IMPORTS`, `CALLS`, `HTTP_CALLS`, `ASYNC_CALLS`, `IMPLEMENTS`, `HANDLES`, `USAGE`, `CONFIGURES`, `WRITES`, `MEMBER_OF`, `TESTS`, `USES_TYPE`, `FILE_CHANGES_WITH`
|
||||
`CONTAINS_PACKAGE`, `CONTAINS_FOLDER`, `CONTAINS_FILE`, `DEFINES`, `DEFINES_METHOD`, `IMPORTS`, `CALLS`, `CALL_REFERENCE`, `HTTP_CALLS`, `ASYNC_CALLS`, `IMPLEMENTS`, `HANDLES`, `USAGE`, `CONFIGURES`, `WRITES`, `MEMBER_OF`, `TESTS`, `USES_TYPE`, `FILE_CHANGES_WITH`
|
||||
|
||||
### Qualified Names
|
||||
|
||||
@@ -665,7 +668,7 @@ SQLite databases stored at `~/.cache/codebase-memory-mcp/`. Persists across rest
|
||||
|
||||
Tree-sitter alone gives a syntactic AST. That handles naming, structure, and call sites well, but it can't tell you that `user.profile.display_name()` resolves to `Profile.display_name` declared three modules away — tree-sitter doesn't track imports, generics, inheritance, or stdlib types.
|
||||
|
||||
codebase-memory-mcp ships a **lightweight C implementation of language type-resolution algorithms, structurally inspired by and compatible with major language servers** (tsserver / typescript-go, pyright, gopls, Roslyn, Eclipse JDT, rust-analyzer), embedded directly into the static binary. No language server process, no per-project setup, no API key. We call this layer **Hybrid LSP**: it runs alongside tree-sitter on every parse and refines `CALLS`, `USAGE`, and `RESOLVED_CALLS` edges with type information, so the resulting graph mirrors what an IDE "Go to Definition" would resolve.
|
||||
codebase-memory-mcp ships a **lightweight C implementation of language type-resolution algorithms, structurally inspired by and compatible with major language servers** (tsserver / typescript-go, pyright, gopls, Roslyn, Eclipse JDT, rust-analyzer), embedded directly into the static binary. No language server process, no per-project setup, no API key. We call this layer **Hybrid LSP**: it runs alongside tree-sitter on every parse and refines invocation resolution (`CALLS` / `RESOLVED_CALLS`) and callable-value resolution (`CALL_REFERENCE`, with ambiguous values retained as `USAGE`) using type information, so the resulting graph mirrors what an IDE "Go to Definition" would resolve.
|
||||
|
||||
**Languages with full Hybrid LSP:**
|
||||
|
||||
|
||||
+32
-3
@@ -1229,7 +1229,8 @@ CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLangua
|
||||
cbm_run_go_lsp(a, result, source, source_len, root);
|
||||
}
|
||||
if (language == CBM_LANG_C || language == CBM_LANG_CPP || language == CBM_LANG_CUDA) {
|
||||
cbm_run_c_lsp(a, result, source, source_len, root, language != CBM_LANG_C);
|
||||
cbm_run_c_lsp(a, result, source, source_len, root, language != CBM_LANG_C,
|
||||
CBM_SOURCE_ORIGIN_RAW);
|
||||
}
|
||||
if (language == CBM_LANG_PHP) {
|
||||
cbm_run_php_lsp(a, result, source, source_len, root);
|
||||
@@ -1289,8 +1290,11 @@ CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLangua
|
||||
if (preprocessed && preprocessed->source) {
|
||||
char *expanded = preprocessed->source;
|
||||
int expanded_len = (int)strlen(expanded);
|
||||
// Record calls count before second pass
|
||||
// Record every site-bearing array boundary before the second pass.
|
||||
// Numeric byte spans in `expanded` are not raw-source coordinates.
|
||||
int calls_before = result->calls.count;
|
||||
int usages_before = result->usages.count;
|
||||
int resolved_before = result->resolved_calls.count;
|
||||
|
||||
// Parse expanded source with fresh tree
|
||||
TSParser *pp_parser = get_thread_parser(ts_lang, language);
|
||||
@@ -1326,11 +1330,30 @@ CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLangua
|
||||
// harmless (pipeline deduplicates by caller+callee).
|
||||
cbm_extract_unified(&pp_ctx);
|
||||
|
||||
/* Stamp parser carriers before C-LSP performs any
|
||||
* origin-sensitive rewrite. Numeric spans in the expanded
|
||||
* buffer may collide with unrelated raw-source spans. */
|
||||
for (int i = calls_before; i < result->calls.count; i++) {
|
||||
result->calls.items[i].source_origin = CBM_SOURCE_ORIGIN_PREPROCESSED;
|
||||
}
|
||||
for (int i = usages_before; i < result->usages.count; i++) {
|
||||
result->usages.items[i].source_origin = CBM_SOURCE_ORIGIN_PREPROCESSED;
|
||||
}
|
||||
|
||||
// Also run LSP on expanded source for additional type-resolved
|
||||
// calls (language is already C/C++/CUDA — checked in enclosing
|
||||
// block). Runs in every mode.
|
||||
cbm_run_c_lsp(a, result, expanded, expanded_len, pp_root,
|
||||
language != CBM_LANG_C);
|
||||
language != CBM_LANG_C, CBM_SOURCE_ORIGIN_PREPROCESSED);
|
||||
|
||||
/* All C-LSP emitters stamp origin directly so rewrite-time
|
||||
* comparisons are already safe. Keep this boundary sweep as
|
||||
* a defensive invariant for any future emitter added to the
|
||||
* C resolver. */
|
||||
for (int i = resolved_before; i < result->resolved_calls.count; i++) {
|
||||
result->resolved_calls.items[i].source_origin =
|
||||
CBM_SOURCE_ORIGIN_PREPROCESSED;
|
||||
}
|
||||
|
||||
/* #961: a def whose body braces are split across
|
||||
* #ifdef/#else branches parses as an ERROR region on the
|
||||
@@ -1537,6 +1560,12 @@ void cbm_free_result(CBMFileResult *result) {
|
||||
ts_tree_delete(result->cached_tree);
|
||||
result->cached_tree = NULL;
|
||||
}
|
||||
for (int i = 0; i < result->owned_result_count; i++) {
|
||||
cbm_free_result(result->owned_results[i]);
|
||||
}
|
||||
free(result->owned_results);
|
||||
result->owned_results = NULL;
|
||||
result->owned_result_count = 0;
|
||||
cbm_arena_destroy(&result->arena);
|
||||
free(result);
|
||||
}
|
||||
|
||||
+104
-37
@@ -186,38 +186,44 @@ typedef struct {
|
||||
const char *file_path; // relative path
|
||||
uint32_t start_line;
|
||||
uint32_t end_line;
|
||||
const char *signature; // parameter text (NULL if none)
|
||||
const char *return_type; // return type text (NULL if none)
|
||||
const char *receiver; // Go method receiver (NULL if none)
|
||||
const char *docstring; // leading doc comment (NULL if none)
|
||||
const char *parent_class; // enclosing class QN for methods (NULL if none)
|
||||
const char **decorators; // NULL-terminated array (NULL if none)
|
||||
const char **base_classes; // NULL-terminated array (NULL if none)
|
||||
const char **param_names; // NULL-terminated array (NULL if none)
|
||||
const char **param_types; // NULL-terminated array (NULL if none)
|
||||
const char **return_types; // NULL-terminated array (NULL if none)
|
||||
const char *route_path; // HTTP route path from decorator (e.g., "/api/users") or NULL
|
||||
const char *route_method; // HTTP method from decorator (e.g., "POST") or NULL
|
||||
int complexity; // cyclomatic complexity
|
||||
int cognitive; // cognitive complexity (nesting-weighted)
|
||||
int loop_count; // number of loop constructs in the body
|
||||
int loop_depth; // max nested-loop depth (bottleneck proxy)
|
||||
bool is_recursive; // body contains a direct self-call (seed for "recursive")
|
||||
int param_count; // number of parameters (large = complexity smell)
|
||||
int max_access_depth; // deepest chained member/subscript access (a.b.c.d)
|
||||
int linear_scan_in_loop; // count of linear-scan calls (find/contains/indexOf) inside loops
|
||||
int alloc_in_loop; // count of allocation/append calls inside loops
|
||||
bool recursion_in_loop; // a self-call occurs inside a loop body
|
||||
bool unguarded_recursion; // recursive with no self-call guarded by a conditional
|
||||
int lines; // body line count
|
||||
uint32_t *fingerprint; // MinHash fingerprint (arena-allocated, K values) or NULL
|
||||
int fingerprint_k; // number of hash values (CBM_MINHASH_K or 0)
|
||||
const char *signature; // parameter text (NULL if none)
|
||||
const char *return_type; // return type text (NULL if none)
|
||||
const char *receiver; // Go method receiver (NULL if none)
|
||||
const char *docstring; // leading doc comment (NULL if none)
|
||||
const char *parent_class; // enclosing class QN for methods (NULL if none)
|
||||
const char **decorators; // NULL-terminated array (NULL if none)
|
||||
const char **base_classes; // NULL-terminated array (NULL if none)
|
||||
const char **param_names; // NULL-terminated array (NULL if none)
|
||||
const char **param_types; // NULL-terminated array (NULL if none)
|
||||
const char **signature_param_types; // ordered internal signature types; "?" means unknown
|
||||
int signature_param_count; // number of entries in signature_param_types
|
||||
const char **return_types; // NULL-terminated array (NULL if none)
|
||||
const char *route_path; // HTTP route path from decorator (e.g., "/api/users") or NULL
|
||||
const char *route_method; // HTTP method from decorator (e.g., "POST") or NULL
|
||||
int complexity; // cyclomatic complexity
|
||||
int cognitive; // cognitive complexity (nesting-weighted)
|
||||
int loop_count; // number of loop constructs in the body
|
||||
int loop_depth; // max nested-loop depth (bottleneck proxy)
|
||||
bool is_recursive; // body contains a direct self-call (seed for "recursive")
|
||||
int param_count; // number of parameters (large = complexity smell)
|
||||
int max_access_depth; // deepest chained member/subscript access (a.b.c.d)
|
||||
int linear_scan_in_loop; // count of linear-scan calls (find/contains/indexOf) inside loops
|
||||
int alloc_in_loop; // count of allocation/append calls inside loops
|
||||
bool recursion_in_loop; // a self-call occurs inside a loop body
|
||||
bool unguarded_recursion; // recursive with no self-call guarded by a conditional
|
||||
int lines; // body line count
|
||||
uint32_t *fingerprint; // MinHash fingerprint (arena-allocated, K values) or NULL
|
||||
int fingerprint_k; // number of hash values (CBM_MINHASH_K or 0)
|
||||
bool is_exported;
|
||||
bool is_abstract;
|
||||
bool is_test;
|
||||
bool is_entry_point;
|
||||
const char *structural_profile; // AST structural profile (arena-allocated) or NULL
|
||||
const char *body_tokens; // space-separated raw identifier tokens from body (arena) or NULL
|
||||
/* Rust only: raw trait path from the exact `impl Trait for Type` block
|
||||
* that declared this method. Kept at the tail so zero-initialised
|
||||
* callers in every other language remain ABI/source compatible. */
|
||||
const char *impl_trait;
|
||||
} CBMDefinition;
|
||||
|
||||
/* Argument captured from a call expression */
|
||||
@@ -230,6 +236,14 @@ typedef struct {
|
||||
|
||||
#define CBM_MAX_CALL_ARGS 8
|
||||
|
||||
/* Byte offsets are meaningful only within the source buffer that produced
|
||||
* them. C/C++/CUDA run both raw and preprocessed extraction passes, and those
|
||||
* buffers can contain unrelated occurrences at the same numeric span. */
|
||||
typedef enum {
|
||||
CBM_SOURCE_ORIGIN_RAW = 0,
|
||||
CBM_SOURCE_ORIGIN_PREPROCESSED,
|
||||
} CBMSourceOrigin;
|
||||
|
||||
typedef struct {
|
||||
const char *callee_name; // raw callee text ("pkg.Func", "foo")
|
||||
const char *enclosing_func_qn; // QN of enclosing function (or module QN)
|
||||
@@ -240,9 +254,14 @@ typedef struct {
|
||||
int loop_depth; // enclosing loop nesting at the call site
|
||||
int branch_depth; // enclosing branch nesting at the call site
|
||||
int start_line; // 1-based source line of the call (for def range-match)
|
||||
uint32_t site_start_byte; // exact AST occurrence span; end > start when present
|
||||
uint32_t site_end_byte; // exclusive byte offset in the source file
|
||||
CBMSourceOrigin source_origin; // raw source or C-family preprocessed buffer
|
||||
bool is_method; // method/member call with a non-self receiver. Perl:
|
||||
// arrow/method call ($obj->m). TS/JS/TSX: member call
|
||||
// x.foo() whose receiver is not this/super. Default false.
|
||||
bool requires_lsp_resolution; // synthetic semantic candidate (for example an implicit
|
||||
// C++ operator). Never fall back to textual resolution.
|
||||
} CBMCall;
|
||||
|
||||
typedef struct {
|
||||
@@ -250,9 +269,22 @@ typedef struct {
|
||||
const char *module_path; // resolved module path / QN
|
||||
} CBMImport;
|
||||
|
||||
typedef enum {
|
||||
CBM_USAGE_VALUE = 0,
|
||||
CBM_USAGE_CALL_REFERENCE,
|
||||
} CBMUsageKind;
|
||||
|
||||
typedef struct {
|
||||
const char *ref_name; // referenced identifier
|
||||
const char *enclosing_func_qn; // QN of enclosing function (or module QN)
|
||||
const char *ref_name; // referenced identifier
|
||||
const char *enclosing_func_qn; // QN of enclosing function (or module QN)
|
||||
CBMUsageKind kind; // ordinary USAGE or explicit callable reference
|
||||
bool may_be_call_reference; // syntactic candidate; exact LSP proof may upgrade its edge
|
||||
bool semantic_reference_blocked; // lexical evidence blocks only unproven textual fallback
|
||||
bool semantic_reference_local_shadow; // blocker belongs to a non-module lexical scope
|
||||
uint32_t lexical_scope_id; // extraction-local scope instance; never graph identity
|
||||
uint32_t site_start_byte; // exact reference-token span; end > start when present
|
||||
uint32_t site_end_byte; // exclusive byte offset in the source file
|
||||
CBMSourceOrigin source_origin; // raw source or C-family preprocessed buffer
|
||||
} CBMUsage;
|
||||
|
||||
typedef struct {
|
||||
@@ -327,15 +359,28 @@ typedef struct {
|
||||
typedef struct {
|
||||
const char *trait_name; // trait name (raw text)
|
||||
const char *struct_name; // struct/type name (raw text)
|
||||
/* Exact extracted QN of the implementing type. Unlike struct_name this
|
||||
* does not need a later leaf-name guess, and the relation exists even for
|
||||
* an empty `impl Trait for Type {}` block. */
|
||||
const char *struct_qn;
|
||||
} CBMImplTrait;
|
||||
|
||||
// LSP-resolved call: high-confidence type-aware call resolution
|
||||
typedef enum {
|
||||
CBM_RESOLVED_INVOCATION = 0,
|
||||
CBM_RESOLVED_CALL_REFERENCE,
|
||||
} CBMResolvedKind;
|
||||
|
||||
// LSP-resolved invocation/reference: high-confidence type-aware resolution.
|
||||
typedef struct {
|
||||
const char *caller_qn; // enclosing function QN
|
||||
const char *callee_qn; // resolved target QN (fully qualified)
|
||||
const char *strategy; // "lsp_type_dispatch", "lsp_direct", etc.
|
||||
float confidence; // 0.90-0.95
|
||||
const char *reason; // diagnostic label for unresolved calls (NULL if resolved)
|
||||
const char *caller_qn; // enclosing function QN
|
||||
const char *callee_qn; // resolved target QN (fully qualified)
|
||||
const char *strategy; // "lsp_type_dispatch", "lsp_direct", etc.
|
||||
float confidence; // 0.90-0.95
|
||||
const char *reason; // diagnostic label for unresolved calls (NULL if resolved)
|
||||
CBMResolvedKind kind; // invocation (CALLS) or explicit callable reference
|
||||
uint32_t site_start_byte; // exact source occurrence; end > start when present
|
||||
uint32_t site_end_byte; // exclusive byte offset in the source file
|
||||
CBMSourceOrigin source_origin; // raw source or C-family preprocessed buffer
|
||||
} CBMResolvedCall;
|
||||
|
||||
typedef struct {
|
||||
@@ -424,8 +469,8 @@ typedef struct {
|
||||
} CBMChannelArray;
|
||||
|
||||
// Full extraction result for one file.
|
||||
typedef struct {
|
||||
CBMArena arena; // owns all string memory
|
||||
typedef struct CBMFileResult {
|
||||
CBMArena arena; // owns local memory; composites may also retain child arenas below
|
||||
|
||||
CBMDefArray defs;
|
||||
CBMCallArray calls;
|
||||
@@ -437,7 +482,7 @@ typedef struct {
|
||||
CBMEnvAccessArray env_accesses;
|
||||
CBMTypeAssignArray type_assigns;
|
||||
CBMImplTraitArray impl_traits; // Rust: impl Trait for Struct pairs
|
||||
CBMResolvedCallArray resolved_calls; // LSP-resolved calls (high confidence)
|
||||
CBMResolvedCallArray resolved_calls; // LSP-resolved invocations/references (high confidence)
|
||||
CBMStringRefArray string_refs; // URL/config string literals from AST
|
||||
CBMInfraBindingArray infra_bindings; // topic→URL pairs from IaC configs
|
||||
CBMChannelArray channels; // Socket.IO / EventEmitter pub/sub participation
|
||||
@@ -474,6 +519,13 @@ typedef struct {
|
||||
// file (defs/calls already extracted are unaffected).
|
||||
const char *source;
|
||||
int source_len;
|
||||
|
||||
// Composite extraction results (currently ObjectScript Studio Export)
|
||||
// retain their per-unit results so shallow-copied carrier strings remain
|
||||
// valid for the composite's full lifetime. Owned and recursively released
|
||||
// by cbm_free_result(); ordinary single-file results leave these zeroed.
|
||||
struct CBMFileResult **owned_results;
|
||||
int owned_result_count;
|
||||
} CBMFileResult;
|
||||
|
||||
// --- Enclosing function cache ---
|
||||
@@ -628,6 +680,21 @@ uint64_t cbm_get_preprocess_ns(void);
|
||||
uint64_t cbm_get_files_preprocessed(void);
|
||||
void cbm_reset_profile(void);
|
||||
|
||||
#if defined(CBM_KOTLIN_DEDUP_TEST_API) && CBM_KOTLIN_DEDUP_TEST_API
|
||||
// Test-build-only operation counter for Kotlin operator-carrier deduplication.
|
||||
// Production builds do not expose or retain this instrumentation.
|
||||
void cbm_kotlin_operator_dedup_test_reset(void);
|
||||
uint64_t cbm_kotlin_operator_dedup_test_comparisons(void);
|
||||
#endif
|
||||
|
||||
#if defined(CBM_CALL_REFERENCE_LOOKUP_TEST_API) && CBM_CALL_REFERENCE_LOOKUP_TEST_API
|
||||
// Test-build-only work counter for resolving a node's field role while
|
||||
// classifying value references. Production builds retain no instrumentation.
|
||||
void cbm_usage_field_lookup_test_reset(void);
|
||||
uint64_t cbm_usage_field_lookup_test_work(void);
|
||||
uint64_t cbm_usage_slow_parent_fallback_test_count(void);
|
||||
#endif
|
||||
|
||||
// Toggle C/C++ preprocessor Macro-node extraction (#375). The pipeline enables
|
||||
// it only for full/advanced index modes (it dominates extraction on macro-dense
|
||||
// codebases). Default ON. Set before extraction; read-only during.
|
||||
|
||||
+1213
-189
File diff suppressed because it is too large
Load Diff
+399
-50
@@ -684,9 +684,12 @@ TSNode cbm_resolve_func_name(TSNode node, CBMLanguage lang) {
|
||||
return null_node;
|
||||
}
|
||||
|
||||
// ObjectScript routine tag is its own name node.
|
||||
if (lang == CBM_LANG_OBJECTSCRIPT_ROUTINE && strcmp(kind, "tag") == 0) {
|
||||
return node;
|
||||
// A parameterized ObjectScript routine wraps its tag and body in a
|
||||
// procedure node. Use the direct tag as the callable name so the
|
||||
// definition spans the complete procedure instead of only its label.
|
||||
if (lang == CBM_LANG_OBJECTSCRIPT_ROUTINE &&
|
||||
(strcmp(kind, "tag") == 0 || strcmp(kind, "procedure") == 0)) {
|
||||
return strcmp(kind, "tag") == 0 ? node : cbm_find_child_by_kind(node, "tag");
|
||||
}
|
||||
// ObjectScript method/classmethod: name lives under method_definition ->
|
||||
// method_name -> first named child.
|
||||
@@ -702,6 +705,9 @@ TSNode cbm_resolve_func_name(TSNode node, CBMLanguage lang) {
|
||||
TSNode null_node = {0};
|
||||
return null_node;
|
||||
}
|
||||
if (lang == CBM_LANG_OBJECTSCRIPT_UDL && strcmp(kind, "query") == 0) {
|
||||
return cbm_find_child_by_kind(node, "query_name");
|
||||
}
|
||||
|
||||
TSNode name = func_name_node(node);
|
||||
|
||||
@@ -2821,6 +2827,14 @@ static char *clean_type_name(CBMArena *a, const char *raw) {
|
||||
static char *resolve_param_name(CBMArena *a, TSNode param, const char *source) {
|
||||
const char *pk = ts_node_type(param);
|
||||
|
||||
if (strcmp(pk, "argument") == 0 || strcmp(pk, "tag_parameter") == 0) {
|
||||
TSNode name = find_first_descendant_by_kind(param, "objectscript_identifier", 6);
|
||||
if (ts_node_is_null(name)) {
|
||||
name = find_first_descendant_by_kind(param, "objectscript_identifier_special", 6);
|
||||
}
|
||||
return ts_node_is_null(name) ? NULL : cbm_node_text(a, name, source);
|
||||
}
|
||||
|
||||
if (strcmp(pk, "parameter_declaration") == 0) {
|
||||
TSNode nm = ts_node_child_by_field_name(param, TS_FIELD("name"));
|
||||
if (!ts_node_is_null(nm)) {
|
||||
@@ -3024,6 +3038,15 @@ static char *resolve_param_type_text(CBMArena *a, TSNode param, const char *sour
|
||||
CBMLanguage lang) {
|
||||
const char *pk = ts_node_type(param);
|
||||
|
||||
if ((lang == CBM_LANG_OBJECTSCRIPT_UDL || lang == CBM_LANG_OBJECTSCRIPT_ROUTINE) &&
|
||||
(strcmp(pk, "argument") == 0 || strcmp(pk, "tag_parameter") == 0)) {
|
||||
TSNode return_type = cbm_find_child_by_kind(param, "return_type");
|
||||
TSNode typename = ts_node_is_null(return_type)
|
||||
? (TSNode){0}
|
||||
: cbm_find_child_by_kind(return_type, "typename");
|
||||
return ts_node_is_null(typename) ? NULL : cbm_node_text(a, typename, source);
|
||||
}
|
||||
|
||||
if (lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX) {
|
||||
if (strcmp(pk, "required_parameter") == 0 || strcmp(pk, "optional_parameter") == 0) {
|
||||
return extract_ts_param_type(a, param, source);
|
||||
@@ -3094,6 +3117,226 @@ static const char **extract_param_types(CBMArena *a, TSNode params, const char *
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Internal, occurrence-preserving signature extraction. `param_types` above is
|
||||
* the legacy USES_TYPE projection: it intentionally drops builtins and
|
||||
* duplicate types. Semantic call resolution needs a different contract — one
|
||||
* type entry per source-level call argument, in declaration order. */
|
||||
static const char signature_unknown_type[] = "?";
|
||||
|
||||
static char *signature_receiver_name(CBMArena *a, TSNode param, const char *source,
|
||||
bool unwrap_wrappers) {
|
||||
char *name = resolve_param_name(a, param, source);
|
||||
if (name && name[0]) {
|
||||
return name;
|
||||
}
|
||||
|
||||
TSNode candidate = ts_node_child_by_field_name(param, TS_FIELD("pattern"));
|
||||
if (ts_node_is_null(candidate)) {
|
||||
candidate = ts_node_child_by_field_name(param, TS_FIELD("name"));
|
||||
}
|
||||
if (ts_node_is_null(candidate)) {
|
||||
uint32_t nc = ts_node_named_child_count(param);
|
||||
for (uint32_t i = 0; i < nc; i++) {
|
||||
TSNode child = ts_node_named_child(param, i);
|
||||
const char *ck = ts_node_type(child);
|
||||
if (strcmp(ck, "type_annotation") == 0 || strcmp(ck, "decorator") == 0 ||
|
||||
strcmp(ck, "attribute_item") == 0) {
|
||||
continue;
|
||||
}
|
||||
candidate = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Python typed/splat wrappers and TS pattern wrappers place the actual
|
||||
* receiver identifier at the first named leaf. */
|
||||
for (int depth = 0; depth < 4 && !ts_node_is_null(candidate); depth++) {
|
||||
if (ts_node_named_child_count(candidate) == 0) {
|
||||
return cbm_node_text(a, candidate, source);
|
||||
}
|
||||
if (!unwrap_wrappers) {
|
||||
return NULL;
|
||||
}
|
||||
TSNode next = ts_node_child_by_field_name(candidate, TS_FIELD("pattern"));
|
||||
if (ts_node_is_null(next)) {
|
||||
next = ts_node_child_by_field_name(candidate, TS_FIELD("name"));
|
||||
}
|
||||
if (ts_node_is_null(next)) {
|
||||
next = ts_node_named_child(candidate, 0);
|
||||
}
|
||||
candidate = next;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static bool is_signature_implicit_receiver(CBMArena *a, TSNode param, const char *source,
|
||||
CBMLanguage lang, bool method, bool first_parameter) {
|
||||
const char *pk = ts_node_type(param);
|
||||
|
||||
/* These grammar nodes describe a receiver supplied by member-call syntax,
|
||||
* not an argument at the call site. */
|
||||
if (strcmp(pk, "self_parameter") == 0 || strcmp(pk, "receiver_parameter") == 0 ||
|
||||
strcmp(pk, "explicit_object_parameter_declaration") == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX) {
|
||||
char *name = signature_receiver_name(a, param, source, false);
|
||||
return name && strcmp(name, "this") == 0;
|
||||
}
|
||||
|
||||
/* Python instance/class receivers are conventional identifiers rather than
|
||||
* distinct AST nodes. Only exclude the leading slot on a method path. */
|
||||
if (method && first_parameter && lang == CBM_LANG_PYTHON) {
|
||||
char *name = signature_receiver_name(a, param, source, true);
|
||||
return name && (strcmp(name, "self") == 0 || strcmp(name, "cls") == 0);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool is_signature_non_parameter(TSNode param, const char *source, CBMLanguage lang) {
|
||||
const char *pk = ts_node_type(param);
|
||||
if (strcmp(pk, "comment") == 0 || strcmp(pk, "line_comment") == 0 ||
|
||||
strcmp(pk, "block_comment") == 0 || strcmp(pk, "attribute_item") == 0 ||
|
||||
strcmp(pk, "inner_attribute_item") == 0 || strcmp(pk, "attribute_specifier") == 0 ||
|
||||
strcmp(pk, "attribute_declaration") == 0 || strcmp(pk, "annotation") == 0 ||
|
||||
strcmp(pk, "decorator") == 0 || strcmp(pk, "modifiers") == 0 ||
|
||||
strcmp(pk, "positional_separator") == 0 || strcmp(pk, "keyword_separator") == 0 ||
|
||||
strcmp(pk, "type_parameters") == 0 || strcmp(pk, "type_parameter_declaration") == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* In C-family declarations `(void)` means zero parameters. The grammar
|
||||
* exposes `void` as a named primitive_type child rather than a parameter. */
|
||||
if ((lang == CBM_LANG_C || lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA ||
|
||||
lang == CBM_LANG_GLSL) &&
|
||||
strcmp(pk, "primitive_type") == 0) {
|
||||
uint32_t start = ts_node_start_byte(param);
|
||||
uint32_t end = ts_node_end_byte(param);
|
||||
return end - start == 4 && strncmp(source + start, "void", 4) == 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Go permits one declaration node to introduce several positional parameters:
|
||||
* `func f(a, b T)`. Count its direct name identifiers; unnamed parameter
|
||||
* declarations still represent one position. */
|
||||
static int signature_param_multiplicity(TSNode param, CBMLanguage lang) {
|
||||
if (lang != CBM_LANG_GO) {
|
||||
return 1;
|
||||
}
|
||||
const char *pk = ts_node_type(param);
|
||||
if (strcmp(pk, "parameter_declaration") != 0 &&
|
||||
strcmp(pk, "variadic_parameter_declaration") != 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int field_names = 0;
|
||||
int identifier_names = 0;
|
||||
uint32_t nc = ts_node_child_count(param);
|
||||
for (uint32_t i = 0; i < nc; i++) {
|
||||
TSNode child = ts_node_child(param, i);
|
||||
if (ts_node_is_null(child) || !ts_node_is_named(child)) {
|
||||
continue;
|
||||
}
|
||||
const char *field = ts_node_field_name_for_child(param, i);
|
||||
if (field && strcmp(field, "name") == 0) {
|
||||
field_names++;
|
||||
}
|
||||
if (strcmp(ts_node_type(child), "identifier") == 0) {
|
||||
identifier_names++;
|
||||
}
|
||||
}
|
||||
int names = field_names > 0 ? field_names : identifier_names;
|
||||
return names > 0 ? names : 1;
|
||||
}
|
||||
|
||||
/* Resolve the full source spelling where the legacy resolver has no supported
|
||||
* case (defaulted C++ parameters, Python annotations, complex TS types, etc.).
|
||||
* This deliberately does not clean or deduplicate the text. */
|
||||
static char *resolve_signature_param_type_text(CBMArena *a, TSNode param, const char *source,
|
||||
CBMLanguage lang) {
|
||||
TSNode type_node = ts_node_child_by_field_name(param, TS_FIELD("type"));
|
||||
if (!ts_node_is_null(type_node)) {
|
||||
/* TypeScript-family grammars may expose the `type_annotation` wrapper
|
||||
* (including its colon) through the field. Store the actual type node. */
|
||||
if (strcmp(ts_node_type(type_node), "type_annotation") == 0 &&
|
||||
ts_node_named_child_count(type_node) > 0) {
|
||||
type_node = ts_node_named_child(type_node, 0);
|
||||
}
|
||||
char *type_text = cbm_node_text(a, type_node, source);
|
||||
if (type_text && type_text[0]) {
|
||||
return type_text;
|
||||
}
|
||||
}
|
||||
|
||||
TSNode annotation = cbm_find_child_by_kind(param, "type_annotation");
|
||||
if (!ts_node_is_null(annotation)) {
|
||||
uint32_t nc = ts_node_named_child_count(annotation);
|
||||
if (nc > 0) {
|
||||
char *type_text = cbm_node_text(a, ts_node_named_child(annotation, 0), source);
|
||||
if (type_text && type_text[0]) {
|
||||
return type_text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TSNode typed_pattern = cbm_find_child_by_kind(param, "typed_pattern");
|
||||
if (!ts_node_is_null(typed_pattern)) {
|
||||
type_node = ts_node_child_by_field_name(typed_pattern, TS_FIELD("type"));
|
||||
if (!ts_node_is_null(type_node)) {
|
||||
char *type_text = cbm_node_text(a, type_node, source);
|
||||
if (type_text && type_text[0]) {
|
||||
return type_text;
|
||||
}
|
||||
}
|
||||
}
|
||||
return resolve_param_type_text(a, param, source, lang);
|
||||
}
|
||||
|
||||
static const char **extract_signature_param_types(CBMArena *a, TSNode params, const char *source,
|
||||
CBMLanguage lang, bool method, int *out_count) {
|
||||
*out_count = 0;
|
||||
if (ts_node_is_null(params)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *types[MAX_PARAMS];
|
||||
uint32_t nc = ts_node_child_count(params);
|
||||
for (uint32_t i = 0; i < nc && *out_count < MAX_PARAMS_MINUS_1; i++) {
|
||||
TSNode param = ts_node_child(params, i);
|
||||
if (ts_node_is_null(param) || !ts_node_is_named(param) ||
|
||||
is_signature_non_parameter(param, source, lang)) {
|
||||
continue;
|
||||
}
|
||||
if (is_signature_implicit_receiver(a, param, source, lang, method, *out_count == 0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
char *type_text = resolve_signature_param_type_text(a, param, source, lang);
|
||||
const char *entry = type_text && type_text[0] ? type_text : signature_unknown_type;
|
||||
int multiplicity = signature_param_multiplicity(param, lang);
|
||||
for (int n = 0; n < multiplicity && *out_count < MAX_PARAMS_MINUS_1; n++) {
|
||||
types[(*out_count)++] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
if (*out_count == 0) {
|
||||
return NULL;
|
||||
}
|
||||
const char **result =
|
||||
(const char **)cbm_arena_alloc(a, (size_t)(*out_count + NULL_TERM) * sizeof(const char *));
|
||||
if (!result) {
|
||||
*out_count = 0;
|
||||
return NULL;
|
||||
}
|
||||
for (int i = 0; i < *out_count; i++) {
|
||||
result[i] = types[i];
|
||||
}
|
||||
result[*out_count] = NULL;
|
||||
return result;
|
||||
}
|
||||
|
||||
// --- Function definition extraction ---
|
||||
|
||||
// For C++/CUDA template_declaration, find the inner function_definition or declaration.
|
||||
@@ -3108,17 +3351,57 @@ static TSNode unwrap_template_inner(TSNode node, CBMLanguage lang) {
|
||||
// C/C++/CUDA/GLSL: parameters live on function_declarator inside declarator chain.
|
||||
static TSNode find_c_params(TSNode func_node) {
|
||||
TSNode decl = ts_node_child_by_field_name(func_node, TS_FIELD("declarator"));
|
||||
for (int d = 0; d < C_RETURN_WALK_DEPTH && !ts_node_is_null(decl); d++) {
|
||||
for (int d = 0; d < DECLARATOR_DEPTH_LIMIT && !ts_node_is_null(decl); d++) {
|
||||
TSNode params = ts_node_child_by_field_name(decl, TS_FIELD("parameters"));
|
||||
if (!ts_node_is_null(params)) {
|
||||
return params;
|
||||
}
|
||||
decl = ts_node_child_by_field_name(decl, TS_FIELD("declarator"));
|
||||
TSNode nested = ts_node_child_by_field_name(decl, TS_FIELD("declarator"));
|
||||
/* tree-sitter-cpp and tree-sitter-cuda do not assign the nested
|
||||
* function declarator a `declarator` field when a reference return
|
||||
* wraps it (`Item& operator[](int)`). That wrapper has one named child;
|
||||
* descend only for this reproduced grammar shape. */
|
||||
if (ts_node_is_null(nested) && strcmp(ts_node_type(decl), "reference_declarator") == 0 &&
|
||||
ts_node_named_child_count(decl) > 0) {
|
||||
nested = ts_node_named_child(decl, 0);
|
||||
}
|
||||
decl = nested;
|
||||
}
|
||||
TSNode null_node = {0};
|
||||
return null_node;
|
||||
}
|
||||
|
||||
/* Resolve the parameter-list node across the grammars used by the generic
|
||||
* definition extractor. Newer tree-sitter-kotlin exposes
|
||||
* `function_value_parameters` as a direct child rather than a `parameters`
|
||||
* field. Keep the legacy and ordered signature extractors on the same node so
|
||||
* adding positional metadata does not replace or reinterpret `param_types`. */
|
||||
static TSNode find_function_params(TSNode func_node, CBMLanguage lang) {
|
||||
TSNode params = ts_node_child_by_field_name(func_node, TS_FIELD("parameters"));
|
||||
// ObjectScript exposes the parameter list under a `parameter_list` field.
|
||||
if (ts_node_is_null(params) &&
|
||||
(lang == CBM_LANG_OBJECTSCRIPT_UDL || lang == CBM_LANG_OBJECTSCRIPT_ROUTINE)) {
|
||||
params = ts_node_child_by_field_name(func_node, TS_FIELD("parameter_list"));
|
||||
}
|
||||
if (ts_node_is_null(params) && lang == CBM_LANG_OBJECTSCRIPT_ROUTINE) {
|
||||
params = cbm_find_child_by_kind(func_node, "parameter_list");
|
||||
}
|
||||
if (ts_node_is_null(params) && lang == CBM_LANG_OBJECTSCRIPT_UDL) {
|
||||
TSNode method_definition = cbm_find_child_by_kind(func_node, "method_definition");
|
||||
if (!ts_node_is_null(method_definition)) {
|
||||
params = cbm_find_child_by_kind(method_definition, "arguments");
|
||||
}
|
||||
}
|
||||
if (ts_node_is_null(params) && lang == CBM_LANG_KOTLIN) {
|
||||
params = cbm_find_child_by_kind(func_node, "function_value_parameters");
|
||||
}
|
||||
if (ts_node_is_null(params) && (lang == CBM_LANG_C || lang == CBM_LANG_CPP ||
|
||||
lang == CBM_LANG_CUDA || lang == CBM_LANG_GLSL)) {
|
||||
params = find_c_params(func_node);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
// C++: resolve trailing return type (auto f() -> Type) on a declarator node.
|
||||
// Updates def->return_type and def->return_types if trailing type found.
|
||||
static void resolve_cpp_trailing_return(CBMArena *a, TSNode func_node, const char *source,
|
||||
@@ -3237,7 +3520,8 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
|
||||
* enclosing scope here is always a namespace. The out-of-line method path
|
||||
* below overrides this for `Ns::Cls::method` definitions. */
|
||||
if (ctx->enclosing_class_qn &&
|
||||
(ctx->language == CBM_LANG_CPP || ctx->language == CBM_LANG_CUDA)) {
|
||||
(ctx->language == CBM_LANG_CPP || ctx->language == CBM_LANG_CUDA ||
|
||||
ctx->language == CBM_LANG_TYPESCRIPT || ctx->language == CBM_LANG_TSX)) {
|
||||
def.qualified_name = cbm_arena_sprintf(a, "%s.%s", ctx->enclosing_class_qn, name);
|
||||
}
|
||||
def.label = "Function";
|
||||
@@ -3246,18 +3530,19 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
|
||||
def.end_line = ts_node_end_point(node).row + TS_LINE_OFFSET;
|
||||
def.lines = (int)(def.end_line - def.start_line + TS_LINE_OFFSET);
|
||||
def.is_exported = cbm_is_exported(name, ctx->language);
|
||||
if (ctx->language == CBM_LANG_RUST &&
|
||||
strcmp(ts_node_type(node), "function_signature_item") == 0) {
|
||||
def.is_abstract = true;
|
||||
}
|
||||
|
||||
// Parameters — use func_node (inner function for templates)
|
||||
TSNode params = ts_node_child_by_field_name(func_node, TS_FIELD("parameters"));
|
||||
if (ts_node_is_null(params) &&
|
||||
(ctx->language == CBM_LANG_C || ctx->language == CBM_LANG_CPP ||
|
||||
ctx->language == CBM_LANG_CUDA || ctx->language == CBM_LANG_GLSL)) {
|
||||
params = find_c_params(func_node);
|
||||
}
|
||||
TSNode params = find_function_params(func_node, ctx->language);
|
||||
if (!ts_node_is_null(params)) {
|
||||
def.signature = cbm_node_text(a, params, ctx->source);
|
||||
def.param_names = extract_param_names(a, params, ctx->source, ctx->language);
|
||||
def.param_types = extract_param_types(a, params, ctx->source, ctx->language);
|
||||
def.signature_param_types = extract_signature_param_types(
|
||||
a, params, ctx->source, ctx->language, false, &def.signature_param_count);
|
||||
}
|
||||
|
||||
// Return type — use func_node (inner function for templates)
|
||||
@@ -4110,6 +4395,22 @@ static TSNode find_class_body(TSNode class_node, CBMLanguage lang) {
|
||||
return null_node;
|
||||
}
|
||||
|
||||
/* Java keeps enum constants directly in enum_body, but declarations that
|
||||
* follow the semicolon (methods, fields, and nested types) inside a dedicated
|
||||
* enum_body_declarations child. Keep find_class_body() returning the raw body
|
||||
* so enum-member extraction still sees the constants, and normalize only the
|
||||
* consumers that walk declaration members. */
|
||||
static TSNode find_class_member_body(TSNode class_node, CBMLanguage lang) {
|
||||
TSNode body = find_class_body(class_node, lang);
|
||||
if (ts_node_is_null(body) || lang != CBM_LANG_JAVA ||
|
||||
strcmp(ts_node_type(body), "enum_body") != 0) {
|
||||
return body;
|
||||
}
|
||||
|
||||
TSNode declarations = cbm_find_child_by_kind(body, "enum_body_declarations");
|
||||
return ts_node_is_null(declarations) ? body : declarations;
|
||||
}
|
||||
|
||||
// Dart: resolve method name from method_signature/function_signature.
|
||||
static TSNode resolve_dart_method_name(TSNode child, const char *ck) {
|
||||
if (strcmp(ck, "method_signature") == 0) {
|
||||
@@ -4247,16 +4548,17 @@ static void push_method_def(CBMExtractCtx *ctx, TSNode child, TSNode class_node,
|
||||
def.end_line = ts_node_end_point(child).row + TS_LINE_OFFSET;
|
||||
def.lines = (int)(def.end_line - def.start_line + TS_LINE_OFFSET);
|
||||
def.is_exported = cbm_is_exported(name, ctx->language);
|
||||
|
||||
TSNode params = ts_node_child_by_field_name(child, TS_FIELD("parameters"));
|
||||
// ObjectScript exposes the parameter list under a `parameter_list` field.
|
||||
if (ts_node_is_null(params) && (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL ||
|
||||
ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE)) {
|
||||
params = ts_node_child_by_field_name(child, TS_FIELD("parameter_list"));
|
||||
if (ctx->language == CBM_LANG_RUST &&
|
||||
strcmp(ts_node_type(child), "function_signature_item") == 0) {
|
||||
def.is_abstract = true;
|
||||
}
|
||||
|
||||
TSNode params = find_function_params(child, ctx->language);
|
||||
if (!ts_node_is_null(params)) {
|
||||
def.signature = cbm_node_text(a, params, ctx->source);
|
||||
def.param_types = extract_param_types(a, params, ctx->source, ctx->language);
|
||||
def.signature_param_types = extract_signature_param_types(
|
||||
a, params, ctx->source, ctx->language, true, &def.signature_param_count);
|
||||
}
|
||||
|
||||
// Return type (same fields as extract_func_def)
|
||||
@@ -4332,7 +4634,7 @@ static void extract_objc_impl_methods(CBMExtractCtx *ctx, TSNode impl_node, cons
|
||||
// Extract methods inside a class body
|
||||
static void extract_class_methods(CBMExtractCtx *ctx, TSNode class_node, const char *class_qn,
|
||||
const CBMLangSpec *spec) {
|
||||
TSNode body = find_class_body(class_node, ctx->language);
|
||||
TSNode body = find_class_member_body(class_node, ctx->language);
|
||||
if (ts_node_is_null(body)) {
|
||||
return;
|
||||
}
|
||||
@@ -4407,6 +4709,23 @@ static void extract_class_methods(CBMExtractCtx *ctx, TSNode class_node, const c
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Kotlin secondary constructors have no source-level name node. They
|
||||
* are nevertheless concrete callable declarations, so materialize one
|
||||
* under the owning class using the class name (`Explicit.Explicit`).
|
||||
* Classes with only an implicit constructor do not enter this branch
|
||||
* and therefore do not gain a fabricated callable target. */
|
||||
if (ctx->language == CBM_LANG_KOTLIN &&
|
||||
strcmp(ts_node_type(method_node), "secondary_constructor") == 0) {
|
||||
TSNode constructor_name = ts_node_child_by_field_name(class_node, TS_FIELD("name"));
|
||||
if (ts_node_is_null(constructor_name)) {
|
||||
constructor_name = cbm_find_child_by_kind(class_node, "type_identifier");
|
||||
}
|
||||
if (!ts_node_is_null(constructor_name)) {
|
||||
push_method_def(ctx, method_node, class_node, class_qn, spec, constructor_name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
TSNode name_node = resolve_method_name(method_node, ctx->language);
|
||||
if (ts_node_is_null(name_node)) {
|
||||
continue;
|
||||
@@ -4439,7 +4758,10 @@ static void extract_rust_impl(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
|
||||
}
|
||||
}
|
||||
|
||||
const char *type_qn = cbm_fqn_compute(a, ctx->project, ctx->rel_path, type_name);
|
||||
|
||||
// Check for "impl Trait for Struct" pattern
|
||||
const char *impl_trait = NULL;
|
||||
TSNode trait_node = ts_node_child_by_field_name(node, TS_FIELD("trait"));
|
||||
if (!ts_node_is_null(trait_node)) {
|
||||
char *trait_name = cbm_node_text(a, trait_node, ctx->source);
|
||||
@@ -4453,15 +4775,15 @@ static void extract_rust_impl(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
|
||||
}
|
||||
}
|
||||
if (trait_name && trait_name[0]) {
|
||||
CBMImplTrait it;
|
||||
CBMImplTrait it = {0};
|
||||
it.trait_name = trait_name;
|
||||
it.struct_name = type_name;
|
||||
it.struct_qn = type_qn;
|
||||
cbm_impltrait_push(&ctx->result->impl_traits, a, it);
|
||||
impl_trait = trait_name;
|
||||
}
|
||||
}
|
||||
|
||||
const char *type_qn = cbm_fqn_compute(a, ctx->project, ctx->rel_path, type_name);
|
||||
|
||||
// Extract methods inside impl body
|
||||
TSNode body = ts_node_child_by_field_name(node, TS_FIELD("body"));
|
||||
if (ts_node_is_null(body)) {
|
||||
@@ -4497,6 +4819,7 @@ static void extract_rust_impl(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
|
||||
def.label = "Method";
|
||||
def.file_path = ctx->rel_path;
|
||||
def.parent_class = type_qn;
|
||||
def.impl_trait = impl_trait;
|
||||
def.start_line = ts_node_start_point(child).row + TS_LINE_OFFSET;
|
||||
def.end_line = ts_node_end_point(child).row + TS_LINE_OFFSET;
|
||||
def.is_exported = cbm_is_exported(name, ctx->language);
|
||||
@@ -4505,6 +4828,8 @@ static void extract_rust_impl(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
|
||||
if (!ts_node_is_null(params)) {
|
||||
def.signature = cbm_node_text(a, params, ctx->source);
|
||||
def.param_types = extract_param_types(a, params, ctx->source, ctx->language);
|
||||
def.signature_param_types = extract_signature_param_types(
|
||||
a, params, ctx->source, ctx->language, true, &def.signature_param_count);
|
||||
}
|
||||
|
||||
if (spec->branching_node_types && spec->branching_node_types[0]) {
|
||||
@@ -5723,7 +6048,7 @@ static void extract_class_fields(CBMExtractCtx *ctx, TSNode class_node, const ch
|
||||
return;
|
||||
}
|
||||
|
||||
TSNode body = find_class_body(class_node, ctx->language);
|
||||
TSNode body = find_class_member_body(class_node, ctx->language);
|
||||
if (ts_node_is_null(body)) {
|
||||
return;
|
||||
}
|
||||
@@ -6038,7 +6363,7 @@ static void extract_class_variables(CBMExtractCtx *ctx, TSNode class_node,
|
||||
return;
|
||||
}
|
||||
|
||||
TSNode body = find_class_body(class_node, ctx->language);
|
||||
TSNode body = find_class_member_body(class_node, ctx->language);
|
||||
if (ts_node_is_null(body)) {
|
||||
return;
|
||||
}
|
||||
@@ -6218,20 +6543,14 @@ static bool is_template_class_node(TSNode node, CBMLanguage lang) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compute the enclosing class QN for a class node (for nested class context).
|
||||
// Compute the enclosing class/namespace QN for a scoped node.
|
||||
/* A namespace contributes a QN segment so a symbol declared in `namespace ns`
|
||||
* is `proj.file.ns.sym`, not a top-level `proj.file.sym`. Without the namespace
|
||||
* in the QN, namespace-aware resolution (C++ ADL) is starved: a bare call
|
||||
* collapses to the file scope and resolves directly instead. Unlike a class, a
|
||||
* namespace emits no def of its own — it only extends the enclosing scope for
|
||||
* its members. C#/PHP need the same treatment paired with their LSP resolvers
|
||||
* (a def-only change breaks their existing namespace handling), done separately. */
|
||||
static bool is_namespace_scope_kind(CBMLanguage lang, const char *kind) {
|
||||
if (lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA) {
|
||||
return strcmp(kind, "namespace_definition") == 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
* namespace extends the enclosing scope for its members. C#/PHP need the same
|
||||
* treatment paired with their LSP resolvers (a def-only change breaks their
|
||||
* existing namespace handling), done separately. */
|
||||
|
||||
static const char *compute_class_qn(CBMExtractCtx *ctx, TSNode node, const char *saved_enclosing) {
|
||||
TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name"));
|
||||
@@ -6259,26 +6578,53 @@ static const char *compute_class_qn(CBMExtractCtx *ctx, TSNode node, const char
|
||||
return saved_enclosing;
|
||||
}
|
||||
|
||||
static void extract_typescript_namespace_def(CBMExtractCtx *ctx, TSNode node,
|
||||
const char *saved_enclosing) {
|
||||
TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name"));
|
||||
if (ts_node_is_null(name_node)) {
|
||||
return;
|
||||
}
|
||||
char *name = cbm_node_text(ctx->arena, name_node, ctx->source);
|
||||
const char *namespace_qn = compute_class_qn(ctx, node, saved_enclosing);
|
||||
if (!name || !name[0] || !namespace_qn || !namespace_qn[0]) {
|
||||
return;
|
||||
}
|
||||
CBMDefinition def = {0};
|
||||
def.name = name;
|
||||
def.qualified_name = namespace_qn;
|
||||
def.label = "Module";
|
||||
def.file_path = ctx->rel_path;
|
||||
def.start_line = ts_node_start_point(node).row + TS_LINE_OFFSET;
|
||||
def.end_line = ts_node_end_point(node).row + TS_LINE_OFFSET;
|
||||
def.lines = (int)(def.end_line - def.start_line + TS_LINE_OFFSET);
|
||||
def.is_exported = true;
|
||||
cbm_defs_push(&ctx->result->defs, ctx->arena, def);
|
||||
}
|
||||
|
||||
// Push nested class children from a class body container onto the walk stack.
|
||||
static void push_class_body_children(TSNode node, const CBMLangSpec *spec, wd_stack_t *s,
|
||||
const char *new_enclosing, CBMArena *arena) {
|
||||
uint32_t nc = ts_node_child_count(node);
|
||||
for (uint32_t ci = 0; ci < nc; ci++) {
|
||||
TSNode child = ts_node_child(node, ci);
|
||||
const char *ck = ts_node_type(child);
|
||||
if (strcmp(ck, "field_declaration_list") == 0 || strcmp(ck, "class_body") == 0 ||
|
||||
strcmp(ck, "declaration_list") == 0 || strcmp(ck, "body") == 0 ||
|
||||
strcmp(ck, "block") == 0 || strcmp(ck, "suite") == 0 ||
|
||||
// Groovy class bodies are a `closure` node; routing through the
|
||||
// nested-class path keeps methods from being re-walked (and thus
|
||||
// double-extracted) as top-level functions. Gated to Groovy so other
|
||||
// grammars that also name a node "closure" are unaffected.
|
||||
(strcmp(ck, "closure") == 0 && spec->language == CBM_LANG_GROOVY)) {
|
||||
push_nested_class_nodes(child, spec, s, new_enclosing, arena);
|
||||
return;
|
||||
}
|
||||
/* Use the same language-aware body selection as method extraction. The old
|
||||
* independent spelling list omitted valid containers such as Scala's
|
||||
* `template_body` and Solidity's contract body. Methods were extracted
|
||||
* once by extract_class_methods(), then the generic walk descended through
|
||||
* the unrecognized body and extracted them again as free functions. */
|
||||
/* Smali's class node is itself the member container. Method extraction
|
||||
* currently preserves Smali's established file-level Function contract,
|
||||
* so the generic walk must still visit those direct method_definition
|
||||
* children. Treating the class itself as a completed body here would only
|
||||
* scan it for nested classes and silently drop every method. */
|
||||
TSNode body = {0};
|
||||
if (spec->language != CBM_LANG_SMALI) {
|
||||
body = find_class_member_body(node, spec->language);
|
||||
}
|
||||
if (!ts_node_is_null(body)) {
|
||||
push_nested_class_nodes(body, spec, s, new_enclosing, arena);
|
||||
return;
|
||||
}
|
||||
|
||||
// No body found — push all children directly
|
||||
uint32_t nc = ts_node_child_count(node);
|
||||
for (int ci = (int)nc - SKIP_CHAR; ci >= 0; ci--) {
|
||||
wd_push(s, ts_node_child(node, (uint32_t)ci), new_enclosing);
|
||||
}
|
||||
@@ -6779,8 +7125,11 @@ static void walk_defs(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec,
|
||||
* is walked normally — functions AND classes, unlike a class body which
|
||||
* routes methods through extract_class_methods. Do NOT emit a def or run
|
||||
* the class/func paths on the namespace node itself. */
|
||||
if (is_namespace_scope_kind(ctx->language, kind)) {
|
||||
if (cbm_is_namespace_scope_kind(ctx->language, kind)) {
|
||||
const char *new_enclosing = compute_class_qn(ctx, node, frame.enclosing_class_qn);
|
||||
if (ctx->language == CBM_LANG_TYPESCRIPT || ctx->language == CBM_LANG_TSX) {
|
||||
extract_typescript_namespace_def(ctx, node, frame.enclosing_class_qn);
|
||||
}
|
||||
wd_push_children_reverse(&s, node, new_enclosing);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -107,6 +107,17 @@ static const char *path_last(CBMArena *a, const char *path) {
|
||||
return path;
|
||||
}
|
||||
|
||||
/* An unaliased dotted Python import binds its first component, not its last:
|
||||
* `import xml.etree` introduces `xml`. Keep module_path intact for dependency
|
||||
* resolution while recording the language-correct local spelling. */
|
||||
static const char *python_import_root(CBMArena *a, const char *path) {
|
||||
if (!path) {
|
||||
return NULL;
|
||||
}
|
||||
const char *dot = strchr(path, '.');
|
||||
return dot ? cbm_arena_strndup(a, path, (size_t)(dot - path)) : path;
|
||||
}
|
||||
|
||||
// --- Go imports ---
|
||||
// import_declaration -> import_spec_list -> import_spec -> (name, path)
|
||||
|
||||
@@ -197,7 +208,7 @@ static void process_py_import_stmt(CBMExtractCtx *ctx, TSNode node) {
|
||||
if (strcmp(ck, "dotted_name") == 0 || strcmp(ck, "identifier") == 0) {
|
||||
char *mod = cbm_node_text(a, child, ctx->source);
|
||||
if (mod && mod[0]) {
|
||||
CBMImport imp = {.local_name = path_last(a, mod), .module_path = mod};
|
||||
CBMImport imp = {.local_name = python_import_root(a, mod), .module_path = mod};
|
||||
cbm_imports_push(&ctx->result->imports, a, imp);
|
||||
}
|
||||
} else if (strcmp(ck, "aliased_import") == 0) {
|
||||
@@ -211,7 +222,7 @@ static void process_py_import_stmt(CBMExtractCtx *ctx, TSNode node) {
|
||||
} else {
|
||||
char *mod = cbm_node_text(a, name_node, ctx->source);
|
||||
if (mod && mod[0]) {
|
||||
CBMImport imp = {.local_name = path_last(a, mod), .module_path = mod};
|
||||
CBMImport imp = {.local_name = python_import_root(a, mod), .module_path = mod};
|
||||
cbm_imports_push(&ctx->result->imports, a, imp);
|
||||
}
|
||||
}
|
||||
|
||||
+607
-36
@@ -14,14 +14,195 @@ enum { MAX_INFRA_BINDINGS = 8 };
|
||||
|
||||
// --- Scope stack management ---
|
||||
|
||||
static void push_scope(WalkState *state, uint8_t kind, uint32_t depth, const char *qn) {
|
||||
if (state->scope_top >= MAX_SCOPES) {
|
||||
return;
|
||||
static bool ensure_scope_capacity(WalkState *state) {
|
||||
if (state->scope_top < state->scope_capacity) {
|
||||
return true;
|
||||
}
|
||||
int new_capacity = state->scope_capacity > 0 ? state->scope_capacity * 2 : MAX_SCOPES;
|
||||
CBMWalkScope *grown =
|
||||
(CBMWalkScope *)cbm_arena_alloc(state->arena, (size_t)new_capacity * sizeof(*grown));
|
||||
if (!grown) {
|
||||
return false;
|
||||
}
|
||||
memcpy(grown, state->scopes, (size_t)state->scope_top * sizeof(*grown));
|
||||
state->scopes = grown;
|
||||
state->scope_capacity = new_capacity;
|
||||
return true;
|
||||
}
|
||||
|
||||
static const CBMLexicalScope *lexical_scope_by_id(const WalkState *state, uint32_t id) {
|
||||
return state && id > 0 && id <= (uint32_t)state->lexical_scope_count
|
||||
? &state->lexical_scopes[id - 1U]
|
||||
: NULL;
|
||||
}
|
||||
|
||||
static CBMLexicalScope *mutable_lexical_scope_by_id(WalkState *state, uint32_t id) {
|
||||
return state && id > 0 && id <= (uint32_t)state->lexical_scope_count
|
||||
? &state->lexical_scopes[id - 1U]
|
||||
: NULL;
|
||||
}
|
||||
|
||||
static uint32_t current_lexical_scope_id(const WalkState *state) {
|
||||
if (!state) {
|
||||
return 0;
|
||||
}
|
||||
for (int i = state->scope_top - 1; i >= 0; i--) {
|
||||
if (state->scopes[i].lexical_scope_id != 0) {
|
||||
return state->scopes[i].lexical_scope_id;
|
||||
}
|
||||
}
|
||||
return state->root_lexical_scope_id;
|
||||
}
|
||||
|
||||
static bool ensure_lexical_scope_capacity(WalkState *state) {
|
||||
if (state->lexical_scope_count < state->lexical_scope_capacity) {
|
||||
return true;
|
||||
}
|
||||
if (state->lexical_scope_capacity > INT32_MAX / PAIR_LEN) {
|
||||
state->lexical_binding_tracking_failed = true;
|
||||
return false;
|
||||
}
|
||||
int new_capacity = state->lexical_scope_capacity * PAIR_LEN;
|
||||
CBMLexicalScope *grown =
|
||||
(CBMLexicalScope *)cbm_arena_alloc(state->arena, (size_t)new_capacity * sizeof(*grown));
|
||||
if (!grown) {
|
||||
state->lexical_binding_tracking_failed = true;
|
||||
return false;
|
||||
}
|
||||
memcpy(grown, state->lexical_scopes, (size_t)state->lexical_scope_count * sizeof(*grown));
|
||||
state->lexical_scopes = grown;
|
||||
state->lexical_scope_capacity = new_capacity;
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint32_t python_function_lookup_parent(const WalkState *state,
|
||||
uint32_t structural_parent_id) {
|
||||
uint32_t id = structural_parent_id;
|
||||
int remaining = state ? state->lexical_scope_count : 0;
|
||||
while (id != 0 && remaining-- > 0) {
|
||||
const CBMLexicalScope *scope = lexical_scope_by_id(state, id);
|
||||
if (!scope) {
|
||||
return structural_parent_id;
|
||||
}
|
||||
if (scope->kind == CBM_LEXICAL_SCOPE_CLASS) {
|
||||
return scope->lookup_parent_id;
|
||||
}
|
||||
if (scope->kind == CBM_LEXICAL_SCOPE_FUNCTION ||
|
||||
scope->kind == CBM_LEXICAL_SCOPE_COMPREHENSION ||
|
||||
scope->kind == CBM_LEXICAL_SCOPE_MODULE) {
|
||||
return structural_parent_id;
|
||||
}
|
||||
id = scope->parent_id;
|
||||
}
|
||||
return structural_parent_id;
|
||||
}
|
||||
|
||||
static uint32_t add_lexical_scope(WalkState *state, TSNode node, CBMLexicalScopeKind kind) {
|
||||
if (!state || ts_node_is_null(node) || !ensure_lexical_scope_capacity(state)) {
|
||||
return 0;
|
||||
}
|
||||
uint32_t parent_id = current_lexical_scope_id(state);
|
||||
uint32_t lookup_parent_id = parent_id;
|
||||
/* Python method/function lookup bypasses the containing class namespace;
|
||||
* class attributes require qualification (`self.x`/`Cls.x`). */
|
||||
if (kind == CBM_LEXICAL_SCOPE_FUNCTION && state->language == CBM_LANG_PYTHON) {
|
||||
lookup_parent_id = python_function_lookup_parent(state, parent_id);
|
||||
}
|
||||
CBMLexicalScope *scope = &state->lexical_scopes[state->lexical_scope_count];
|
||||
scope->id = (uint32_t)state->lexical_scope_count + 1U;
|
||||
scope->parent_id = parent_id;
|
||||
scope->lookup_parent_id = lookup_parent_id;
|
||||
scope->start_byte = ts_node_start_byte(node);
|
||||
scope->end_byte = ts_node_end_byte(node);
|
||||
scope->kind = (uint8_t)kind;
|
||||
state->lexical_scope_count++;
|
||||
return scope->id;
|
||||
}
|
||||
|
||||
static bool push_scope(WalkState *state, uint8_t kind, uint32_t depth, const char *qn) {
|
||||
if (!ensure_scope_capacity(state)) {
|
||||
return false;
|
||||
}
|
||||
state->scopes[state->scope_top].kind = kind;
|
||||
state->scopes[state->scope_top].depth = depth;
|
||||
state->scopes[state->scope_top].qn = qn;
|
||||
state->scopes[state->scope_top].lexical_scope_id = 0;
|
||||
state->scopes[state->scope_top].invocation_kind = CBM_INVOCATION_NONE;
|
||||
state->scopes[state->scope_top].callee_expr = (TSNode){0};
|
||||
state->scopes[state->scope_top].callee_leaf = (TSNode){0};
|
||||
state->scope_top++;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool push_lexical_scope(WalkState *state, uint8_t walk_kind, uint32_t depth, const char *qn,
|
||||
TSNode node, CBMLexicalScopeKind lexical_kind) {
|
||||
if (!push_scope(state, walk_kind, depth, qn)) {
|
||||
state->lexical_binding_tracking_failed = true;
|
||||
return false;
|
||||
}
|
||||
state->scopes[state->scope_top - SKIP_ONE].lexical_scope_id =
|
||||
add_lexical_scope(state, node, lexical_kind);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool push_existing_lexical_scope(WalkState *state, uint8_t walk_kind, uint32_t depth,
|
||||
const char *qn, uint32_t lexical_scope_id, TSNode node) {
|
||||
CBMLexicalScope *scope = mutable_lexical_scope_by_id(state, lexical_scope_id);
|
||||
if (!scope || !push_scope(state, walk_kind, depth, qn)) {
|
||||
state->lexical_binding_tracking_failed = true;
|
||||
return false;
|
||||
}
|
||||
state->scopes[state->scope_top - SKIP_ONE].lexical_scope_id = lexical_scope_id;
|
||||
uint32_t end_byte = ts_node_end_byte(node);
|
||||
if (end_byte > scope->end_byte) {
|
||||
scope->end_byte = end_byte;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint32_t active_same_function_scope_id(const WalkState *state, const char *function_qn,
|
||||
TSNode node) {
|
||||
if (!state || !function_qn) {
|
||||
return 0;
|
||||
}
|
||||
uint32_t start = ts_node_start_byte(node);
|
||||
uint32_t end = ts_node_end_byte(node);
|
||||
for (int i = state->scope_top - 1; i >= 0; i--) {
|
||||
if (state->scopes[i].kind != SCOPE_FUNC || !state->scopes[i].qn ||
|
||||
strcmp(state->scopes[i].qn, function_qn) != 0) {
|
||||
continue;
|
||||
}
|
||||
const CBMLexicalScope *scope =
|
||||
lexical_scope_by_id(state, state->scopes[i].lexical_scope_id);
|
||||
if (scope && scope->start_byte <= start && end <= scope->end_byte) {
|
||||
return scope->id;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool push_function_scope(WalkState *state, uint32_t depth, const char *function_qn,
|
||||
TSNode node) {
|
||||
uint32_t existing_id = active_same_function_scope_id(state, function_qn, node);
|
||||
return existing_id ? push_existing_lexical_scope(state, SCOPE_FUNC, depth, function_qn,
|
||||
existing_id, node)
|
||||
: push_lexical_scope(state, SCOPE_FUNC, depth, function_qn, node,
|
||||
CBM_LEXICAL_SCOPE_FUNCTION);
|
||||
}
|
||||
|
||||
static void push_call_scope(WalkState *state, uint32_t depth,
|
||||
const CBMInvocationDescriptor *invocation) {
|
||||
if (!invocation ||
|
||||
(invocation->kind != CBM_INVOCATION_CALLABLE_REFERENCE && !invocation->raw_call_emitted) ||
|
||||
(ts_node_is_null(invocation->callee_expr) && ts_node_is_null(invocation->callee_leaf))) {
|
||||
return;
|
||||
}
|
||||
if (!push_scope(state, SCOPE_CALL, depth, NULL)) {
|
||||
return;
|
||||
}
|
||||
state->scopes[state->scope_top - SKIP_ONE].invocation_kind = invocation->kind;
|
||||
state->scopes[state->scope_top - SKIP_ONE].callee_expr = invocation->callee_expr;
|
||||
state->scopes[state->scope_top - SKIP_ONE].callee_leaf = invocation->callee_leaf;
|
||||
}
|
||||
|
||||
// Pop scopes that we've ascended out of (depth >= current cursor depth).
|
||||
@@ -35,7 +216,9 @@ static void pop_expired_scopes(WalkState *state, uint32_t cur_depth) {
|
||||
static void recompute_state(WalkState *state, const char *module_qn) {
|
||||
state->enclosing_func_qn = module_qn;
|
||||
state->enclosing_class_qn = NULL;
|
||||
state->inside_call = false;
|
||||
state->invocation_kind = CBM_INVOCATION_NONE;
|
||||
state->callee_expr = (TSNode){0};
|
||||
state->callee_leaf = (TSNode){0};
|
||||
state->inside_import = false;
|
||||
state->loop_depth = 0;
|
||||
state->branch_depth = 0;
|
||||
@@ -46,10 +229,13 @@ static void recompute_state(WalkState *state, const char *module_qn) {
|
||||
state->enclosing_func_qn = state->scopes[i].qn;
|
||||
break;
|
||||
case SCOPE_CLASS:
|
||||
case SCOPE_NAMESPACE:
|
||||
state->enclosing_class_qn = state->scopes[i].qn;
|
||||
break;
|
||||
case SCOPE_CALL:
|
||||
state->inside_call = true;
|
||||
state->invocation_kind = state->scopes[i].invocation_kind;
|
||||
state->callee_expr = state->scopes[i].callee_expr;
|
||||
state->callee_leaf = state->scopes[i].callee_leaf;
|
||||
break;
|
||||
case SCOPE_IMPORT:
|
||||
state->inside_import = true;
|
||||
@@ -453,10 +639,22 @@ static const char *objectscript_get_class_name(CBMExtractCtx *ctx, TSNode node)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Resolve the QN of an ObjectScript method/classmethod node for scope tracking.
|
||||
// Resolve the QN of an ObjectScript method/classmethod/query node for scope tracking.
|
||||
static const char *objectscript_get_method_qn(CBMExtractCtx *ctx, TSNode node,
|
||||
const char *enclosing_class_qn) {
|
||||
const char *nk = ts_node_type(node);
|
||||
if (strcmp(nk, "query") == 0) {
|
||||
TSNode query_name = cbm_find_child_by_kind(node, "query_name");
|
||||
if (ts_node_is_null(query_name)) {
|
||||
return NULL;
|
||||
}
|
||||
char *name = cbm_node_text(ctx->arena, query_name, ctx->source);
|
||||
if (!name || !name[0]) {
|
||||
return NULL;
|
||||
}
|
||||
return enclosing_class_qn ? cbm_arena_sprintf(ctx->arena, "%s.%s", enclosing_class_qn, name)
|
||||
: cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, name);
|
||||
}
|
||||
if (strcmp(nk, "method") != 0 && strcmp(nk, "classmethod") != 0) {
|
||||
return NULL;
|
||||
}
|
||||
@@ -495,8 +693,20 @@ static const char *compute_func_qn(CBMExtractCtx *ctx, TSNode node, const CBMLan
|
||||
return objectscript_get_method_qn(ctx, node, state->enclosing_class_qn);
|
||||
}
|
||||
if (ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE) {
|
||||
if (strcmp(ts_node_type(node), "tag") == 0) {
|
||||
char *name = cbm_node_text(ctx->arena, node, ctx->source);
|
||||
const char *kind = ts_node_type(node);
|
||||
TSNode tag = {0};
|
||||
if (strcmp(kind, "procedure") == 0) {
|
||||
tag = cbm_find_child_by_kind(node, "tag");
|
||||
} else if (strcmp(kind, "tag") == 0) {
|
||||
/* The enclosing procedure owns the full callable range and already
|
||||
* pushed this QN. Do not create a nested label-only function scope. */
|
||||
TSNode parent = ts_node_parent(node);
|
||||
if (ts_node_is_null(parent) || strcmp(ts_node_type(parent), "procedure") != 0) {
|
||||
tag = node;
|
||||
}
|
||||
}
|
||||
if (!ts_node_is_null(tag)) {
|
||||
char *name = cbm_node_text(ctx->arena, tag, ctx->source);
|
||||
if (name && name[0]) {
|
||||
return cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, name);
|
||||
}
|
||||
@@ -1315,6 +1525,15 @@ static void scan_infra_bindings(CBMExtractCtx *ctx, TSNode node) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Extra nodes are grammar-declared trivia (most commonly comments). They still
|
||||
* participate in cursor traversal and scope expiry, but none of the unified
|
||||
* semantic handlers consumes their contents; documentation is collected by
|
||||
* the definition/LSP passes. The grammar predicate covers every language
|
||||
* without confusing structured syntax such as SQL COMMENT statements. */
|
||||
static bool is_unified_trivia_node(TSNode node) {
|
||||
return ts_node_is_extra(node);
|
||||
}
|
||||
|
||||
// JS/TS `export_statement` appears in import_node_types so re-exports
|
||||
// (`export { X } from './m'`) are treated as an import boundary. But it also
|
||||
// wraps exported *declarations* (`export function f(cfg: Config) {}`), and
|
||||
@@ -1341,9 +1560,280 @@ static bool is_export_of_declaration(TSNode node) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Some languages encode imports with the same generic AST node used by every
|
||||
// ordinary invocation. Static node-kind membership is therefore not enough to
|
||||
// make the whole subtree an import scope.
|
||||
static bool is_actual_import_boundary(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) {
|
||||
bool direct_import = spec->import_node_types && cbm_kind_in_set(node, spec->import_node_types);
|
||||
bool from_import = spec->import_from_types && cbm_kind_in_set(node, spec->import_from_types);
|
||||
if (!direct_import && !from_import) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *kind = ts_node_type(node);
|
||||
TSNode head = ts_node_child_by_field_name(node, TS_FIELD("function"));
|
||||
if (ts_node_is_null(head)) {
|
||||
head = ts_node_child_by_field_name(node, TS_FIELD("method"));
|
||||
}
|
||||
if (ts_node_is_null(head)) {
|
||||
head = ts_node_child_by_field_name(node, TS_FIELD("command_name"));
|
||||
}
|
||||
if (ts_node_is_null(head)) {
|
||||
head = ts_node_child_by_field_name(node, TS_FIELD("name"));
|
||||
}
|
||||
if (ts_node_is_null(head) && ts_node_named_child_count(node) > 0) {
|
||||
head = ts_node_named_child(node, 0);
|
||||
}
|
||||
char *name = ts_node_is_null(head) ? NULL : cbm_node_text(ctx->arena, head, ctx->source);
|
||||
|
||||
switch (ctx->language) {
|
||||
case CBM_LANG_ELIXIR:
|
||||
return strcmp(kind, "call") != 0 ||
|
||||
(name && (strcmp(name, "import") == 0 || strcmp(name, "alias") == 0 ||
|
||||
strcmp(name, "require") == 0 || strcmp(name, "use") == 0));
|
||||
case CBM_LANG_LUA:
|
||||
return strcmp(kind, "function_call") != 0 || (name && strcmp(name, "require") == 0);
|
||||
case CBM_LANG_RUBY:
|
||||
return strcmp(kind, "call") != 0 ||
|
||||
(name && (strcmp(name, "require") == 0 || strcmp(name, "require_relative") == 0));
|
||||
case CBM_LANG_BASH:
|
||||
return strcmp(kind, "command") != 0 ||
|
||||
(name && (strcmp(name, "source") == 0 || strcmp(name, ".") == 0));
|
||||
case CBM_LANG_R:
|
||||
if (strcmp(kind, "call") != 0 || !name) {
|
||||
return strcmp(kind, "call") != 0;
|
||||
}
|
||||
return strcmp(name, "library") == 0 || strcmp(name, "require") == 0 ||
|
||||
strcmp(name, "requireNamespace") == 0 || strcmp(name, "loadNamespace") == 0 ||
|
||||
strcmp(name, "source") == 0 || strcmp(name, "box::use") == 0;
|
||||
case CBM_LANG_ZIG: {
|
||||
if (strcmp(kind, "builtin_function") != 0) {
|
||||
return true;
|
||||
}
|
||||
char *text = cbm_node_text(ctx->arena, node, ctx->source);
|
||||
return text && (strncmp(text, "@import", sizeof("@import") - 1) == 0 ||
|
||||
strncmp(text, "@cImport", sizeof("@cImport") - 1) == 0);
|
||||
}
|
||||
case CBM_LANG_SCSS:
|
||||
/* `@include` is an executable mixin invocation, not a module import. */
|
||||
return strcmp(kind, "include_statement") != 0;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/* NASM labels and instructions are flat siblings. For an actual instruction,
|
||||
* find the nearest preceding label at the first ancestor level that has one.
|
||||
* This is structural scope recovery; the instruction mnemonic is not used. */
|
||||
static TSNode nasm_wrapped_label(TSNode node, int remaining_depth) {
|
||||
if (ts_node_is_null(node) || remaining_depth < 0) {
|
||||
return (TSNode){0};
|
||||
}
|
||||
if (strcmp(ts_node_type(node), "label") == 0) {
|
||||
return node;
|
||||
}
|
||||
const char *kind = ts_node_type(node);
|
||||
if (strcmp(kind, "instruction") != 0 && strcmp(kind, "source_line") != 0) {
|
||||
return (TSNode){0};
|
||||
}
|
||||
uint32_t count = ts_node_named_child_count(node);
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
TSNode label = nasm_wrapped_label(ts_node_named_child(node, i), remaining_depth - 1);
|
||||
if (!ts_node_is_null(label)) {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
return (TSNode){0};
|
||||
}
|
||||
|
||||
static TSNode nasm_preceding_label(TSNode node) {
|
||||
TSNode current = node;
|
||||
for (int level = 0; level < 4 && !ts_node_is_null(current); level++) {
|
||||
TSNode previous = ts_node_prev_named_sibling(current);
|
||||
while (!ts_node_is_null(previous)) {
|
||||
TSNode label = nasm_wrapped_label(previous, 2);
|
||||
if (!ts_node_is_null(label)) {
|
||||
return label;
|
||||
}
|
||||
previous = ts_node_prev_named_sibling(previous);
|
||||
}
|
||||
TSNode parent = ts_node_parent(current);
|
||||
if (ts_node_is_null(parent) || strcmp(ts_node_type(current), "source_file") == 0) {
|
||||
break;
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
return (TSNode){0};
|
||||
}
|
||||
|
||||
/* Traditional ObjectScript routines are flat: a tag statement is followed by
|
||||
* sibling command statements until the next tag. Re-anchor each top-level
|
||||
* command statement to its nearest preceding tag, just as NASM instructions
|
||||
* are re-anchored to flat labels. Braced `procedure` nodes already provide a
|
||||
* real lexical boundary and deliberately bypass this recovery path. */
|
||||
static TSNode objectscript_routine_statement_tag(TSNode statement) {
|
||||
TSNode tag_statement = cbm_find_child_by_kind(statement, "tag_statement");
|
||||
return ts_node_is_null(tag_statement) ? (TSNode){0}
|
||||
: cbm_find_child_by_kind(tag_statement, "tag");
|
||||
}
|
||||
|
||||
static TSNode objectscript_routine_preceding_tag(TSNode node) {
|
||||
if (strcmp(ts_node_type(node), "statement") != 0) {
|
||||
return (TSNode){0};
|
||||
}
|
||||
TSNode parent = ts_node_parent(node);
|
||||
if (ts_node_is_null(parent) || strcmp(ts_node_type(parent), "source_file") != 0 ||
|
||||
!ts_node_is_null(cbm_find_child_by_kind(node, "tag_statement")) ||
|
||||
!ts_node_is_null(cbm_find_child_by_kind(node, "procedure"))) {
|
||||
return (TSNode){0};
|
||||
}
|
||||
for (TSNode previous = ts_node_prev_named_sibling(node); !ts_node_is_null(previous);
|
||||
previous = ts_node_prev_named_sibling(previous)) {
|
||||
TSNode tag = objectscript_routine_statement_tag(previous);
|
||||
if (!ts_node_is_null(tag)) {
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
return (TSNode){0};
|
||||
}
|
||||
|
||||
static bool push_pre_node_scope(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec,
|
||||
WalkState *state, uint32_t depth) {
|
||||
for (int i = 0; i < state->scope_top; i++) {
|
||||
if (state->scopes[i].kind == SCOPE_FUNC) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
TSNode label = {0};
|
||||
if (ctx->language == CBM_LANG_NASM && strcmp(ts_node_type(node), "actual_instruction") == 0) {
|
||||
label = nasm_preceding_label(node);
|
||||
} else if (ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE) {
|
||||
if (strcmp(ts_node_type(node), "tag_statement") == 0) {
|
||||
/* A parameterized flat tag owns its header as well as the following
|
||||
* sibling command statements. Anchor before descending so its
|
||||
* parameter_list records lexical bindings under the same tag QN. */
|
||||
label = cbm_find_child_by_kind(node, "tag");
|
||||
} else {
|
||||
label = objectscript_routine_preceding_tag(node);
|
||||
}
|
||||
}
|
||||
if (ts_node_is_null(label)) {
|
||||
return false;
|
||||
}
|
||||
const char *fqn = compute_func_qn(ctx, label, spec, state);
|
||||
if (!fqn) {
|
||||
return false;
|
||||
}
|
||||
uint32_t anchor_start = ts_node_start_byte(label);
|
||||
uint32_t anchor_end = ts_node_end_byte(label);
|
||||
bool same_flat_callable = state->flat_function_scope_id != 0 &&
|
||||
state->flat_anchor_start_byte == anchor_start &&
|
||||
state->flat_anchor_end_byte == anchor_end &&
|
||||
state->flat_function_qn && strcmp(state->flat_function_qn, fqn) == 0;
|
||||
bool pushed = same_flat_callable
|
||||
? push_existing_lexical_scope(state, SCOPE_FUNC, depth, fqn,
|
||||
state->flat_function_scope_id, node)
|
||||
: push_function_scope(state, depth, fqn, node);
|
||||
if (pushed && !same_flat_callable) {
|
||||
state->flat_function_scope_id = state->scopes[state->scope_top - SKIP_ONE].lexical_scope_id;
|
||||
state->flat_anchor_start_byte = anchor_start;
|
||||
state->flat_anchor_end_byte = anchor_end;
|
||||
state->flat_function_qn = fqn;
|
||||
}
|
||||
return pushed;
|
||||
}
|
||||
|
||||
static bool unified_kind_in_set(const char *kind, const char *const *set) {
|
||||
for (int i = 0; kind && set && set[i]; i++) {
|
||||
if (strcmp(kind, set[i]) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool lexical_boundary_kind(TSNode node, CBMLexicalScopeKind *out_kind) {
|
||||
const char *kind = ts_node_type(node);
|
||||
static const char *const block_kinds[] = {"block",
|
||||
"statement_block",
|
||||
"compound_statement",
|
||||
"switch_body",
|
||||
"catch_clause",
|
||||
"except_clause",
|
||||
"finally_clause",
|
||||
"for_statement",
|
||||
"for_in_statement",
|
||||
"for_of_statement",
|
||||
"enhanced_for_statement",
|
||||
"foreach_statement",
|
||||
"foreach_clause",
|
||||
"loop_expression",
|
||||
"match_block",
|
||||
"case_block",
|
||||
"script_block",
|
||||
"do_block",
|
||||
NULL};
|
||||
static const char *const comprehension_kinds[] = {
|
||||
"list_comprehension", "set_comprehension", "dictionary_comprehension",
|
||||
"generator_expression", "comprehension_expression", NULL};
|
||||
static const char *const anonymous_function_kinds[] = {"lambda",
|
||||
"lambda_expression",
|
||||
"anonymous_function",
|
||||
"anonymous_function_creation_expression",
|
||||
"anonymous_method_expression",
|
||||
"arrow_function",
|
||||
"closure_expression",
|
||||
"func_literal",
|
||||
"function_expression",
|
||||
"function_literal",
|
||||
NULL};
|
||||
/* Rust inline modules own independent item/import namespaces. Without a
|
||||
* concrete module scope, `mod inner { use ... as x; }` leaks x into every
|
||||
* sibling function in the file. */
|
||||
if (strcmp(kind, "mod_item") == 0) {
|
||||
*out_kind = CBM_LEXICAL_SCOPE_MODULE;
|
||||
return true;
|
||||
}
|
||||
if (unified_kind_in_set(kind, comprehension_kinds)) {
|
||||
*out_kind = CBM_LEXICAL_SCOPE_COMPREHENSION;
|
||||
return true;
|
||||
}
|
||||
if (unified_kind_in_set(kind, anonymous_function_kinds)) {
|
||||
*out_kind = CBM_LEXICAL_SCOPE_FUNCTION;
|
||||
return true;
|
||||
}
|
||||
if (unified_kind_in_set(kind, block_kinds)) {
|
||||
*out_kind = CBM_LEXICAL_SCOPE_BLOCK;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool node_already_has_lexical_scope(const WalkState *state, TSNode node) {
|
||||
uint32_t start = ts_node_start_byte(node);
|
||||
uint32_t end = ts_node_end_byte(node);
|
||||
for (int i = state->scope_top - 1; i >= 0; i--) {
|
||||
const CBMLexicalScope *scope =
|
||||
lexical_scope_by_id(state, state->scopes[i].lexical_scope_id);
|
||||
if (scope && scope->start_byte == start && scope->end_byte == end) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void push_lexical_boundary(TSNode node, WalkState *state, uint32_t depth) {
|
||||
CBMLexicalScopeKind kind;
|
||||
if (!node_already_has_lexical_scope(state, node) && lexical_boundary_kind(node, &kind)) {
|
||||
(void)push_lexical_scope(state, SCOPE_LEXICAL, depth, NULL, node, kind);
|
||||
}
|
||||
}
|
||||
|
||||
// Push scope markers for function, class, call, and import boundary nodes.
|
||||
static void push_boundary_scopes(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec,
|
||||
WalkState *state, uint32_t depth) {
|
||||
WalkState *state, uint32_t depth,
|
||||
const CBMInvocationDescriptor *invocation) {
|
||||
if (spec->function_node_types && cbm_kind_in_set(node, spec->function_node_types)) {
|
||||
/* OCaml: a nested local `let x = e in ...` is itself a value_definition,
|
||||
* but the def walk does not descend into function bodies, so it emits no
|
||||
@@ -1362,8 +1852,20 @@ static void push_boundary_scopes(CBMExtractCtx *ctx, TSNode node, const CBMLangS
|
||||
}
|
||||
if (!skip_nested) {
|
||||
const char *fqn = compute_func_qn(ctx, node, spec, state);
|
||||
if (fqn) {
|
||||
push_scope(state, SCOPE_FUNC, depth, fqn);
|
||||
if (fqn && push_function_scope(state, depth, fqn, node)) {
|
||||
const char *node_kind = ts_node_type(node);
|
||||
bool split_signature = (ctx->language == CBM_LANG_DART &&
|
||||
(strcmp(node_kind, "function_signature") == 0 ||
|
||||
strcmp(node_kind, "method_signature") == 0)) ||
|
||||
(ctx->language == CBM_LANG_LLVM_IR &&
|
||||
strcmp(node_kind, "function_header") == 0);
|
||||
if (split_signature) {
|
||||
state->split_function_scope_id =
|
||||
state->scopes[state->scope_top - SKIP_ONE].lexical_scope_id;
|
||||
state->split_signature_start_byte = ts_node_start_byte(node);
|
||||
state->split_signature_end_byte = ts_node_end_byte(node);
|
||||
state->split_function_qn = fqn;
|
||||
}
|
||||
// ObjectScript: entering a method resets local var types (keeping
|
||||
// class-level property types) and seeds the declared parameter types.
|
||||
if (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL ||
|
||||
@@ -1407,10 +1909,16 @@ static void push_boundary_scopes(CBMExtractCtx *ctx, TSNode node, const CBMLangS
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (cbm_is_namespace_scope_kind(ctx->language, ts_node_type(node))) {
|
||||
const char *namespace_qn = compute_class_qn(ctx, node, state);
|
||||
if (namespace_qn) {
|
||||
push_lexical_scope(state, SCOPE_NAMESPACE, depth, namespace_qn, node,
|
||||
CBM_LEXICAL_SCOPE_MODULE);
|
||||
}
|
||||
} else if (spec->class_node_types && cbm_kind_in_set(node, spec->class_node_types)) {
|
||||
const char *cqn = compute_class_qn(ctx, node, state);
|
||||
if (cqn) {
|
||||
push_scope(state, SCOPE_CLASS, depth, cqn);
|
||||
push_lexical_scope(state, SCOPE_CLASS, depth, cqn, node, CBM_LEXICAL_SCOPE_CLASS);
|
||||
// ObjectScript: a new class clears the type map entirely.
|
||||
if (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL ||
|
||||
ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE) {
|
||||
@@ -1425,7 +1933,7 @@ static void push_boundary_scopes(CBMExtractCtx *ctx, TSNode node, const CBMLangS
|
||||
if (type_name && type_name[0]) {
|
||||
const char *tqn =
|
||||
cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, type_name);
|
||||
push_scope(state, SCOPE_CLASS, depth, tqn);
|
||||
push_lexical_scope(state, SCOPE_CLASS, depth, tqn, node, CBM_LEXICAL_SCOPE_CLASS);
|
||||
}
|
||||
}
|
||||
} else if (ctx->language == CBM_LANG_DART && strcmp(ts_node_type(node), "function_body") == 0) {
|
||||
@@ -1442,16 +1950,54 @@ static void push_boundary_scopes(CBMExtractCtx *ctx, TSNode node, const CBMLangS
|
||||
if (!ts_node_is_null(prev)) {
|
||||
const char *fqn = compute_func_qn(ctx, prev, spec, state);
|
||||
if (fqn) {
|
||||
push_scope(state, SCOPE_FUNC, depth, fqn);
|
||||
bool exact_pending =
|
||||
state->split_function_scope_id != 0 && state->split_function_qn &&
|
||||
strcmp(state->split_function_qn, fqn) == 0 &&
|
||||
state->split_signature_start_byte == ts_node_start_byte(prev) &&
|
||||
state->split_signature_end_byte == ts_node_end_byte(prev);
|
||||
if (exact_pending) {
|
||||
push_existing_lexical_scope(state, SCOPE_FUNC, depth, fqn,
|
||||
state->split_function_scope_id, node);
|
||||
} else {
|
||||
push_function_scope(state, depth, fqn, node);
|
||||
}
|
||||
state->split_function_scope_id = 0;
|
||||
state->split_function_qn = NULL;
|
||||
}
|
||||
}
|
||||
} else if (ctx->language == CBM_LANG_LLVM_IR &&
|
||||
strcmp(ts_node_type(node), "function_body") == 0) {
|
||||
/* LLVM's function_header and function_body are siblings under
|
||||
* fn_define. Re-anchor the already-extracted header definition across
|
||||
* its body so instruction_call nodes inherit that routine. */
|
||||
TSNode previous = ts_node_prev_sibling(node);
|
||||
while (!ts_node_is_null(previous) &&
|
||||
strcmp(ts_node_type(previous), "function_header") != 0) {
|
||||
previous = ts_node_prev_sibling(previous);
|
||||
}
|
||||
if (!ts_node_is_null(previous)) {
|
||||
const char *fqn = compute_func_qn(ctx, previous, spec, state);
|
||||
if (fqn) {
|
||||
bool exact_pending =
|
||||
state->split_function_scope_id != 0 && state->split_function_qn &&
|
||||
strcmp(state->split_function_qn, fqn) == 0 &&
|
||||
state->split_signature_start_byte == ts_node_start_byte(previous) &&
|
||||
state->split_signature_end_byte == ts_node_end_byte(previous);
|
||||
if (exact_pending) {
|
||||
push_existing_lexical_scope(state, SCOPE_FUNC, depth, fqn,
|
||||
state->split_function_scope_id, node);
|
||||
} else {
|
||||
push_function_scope(state, depth, fqn, node);
|
||||
}
|
||||
state->split_function_scope_id = 0;
|
||||
state->split_function_qn = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (spec->call_node_types && cbm_kind_in_set(node, spec->call_node_types)) {
|
||||
push_scope(state, SCOPE_CALL, depth, NULL);
|
||||
}
|
||||
if (spec->import_node_types && cbm_kind_in_set(node, spec->import_node_types) &&
|
||||
!is_export_of_declaration(node)) {
|
||||
push_lexical_boundary(node, state, depth);
|
||||
push_call_scope(state, depth, invocation);
|
||||
if (is_actual_import_boundary(ctx, node, spec) && !is_export_of_declaration(node)) {
|
||||
push_scope(state, SCOPE_IMPORT, depth, NULL);
|
||||
}
|
||||
/* Loop / branch nesting for bottleneck metrics. Loops are gated on named
|
||||
@@ -1472,33 +2018,56 @@ void cbm_extract_unified(CBMExtractCtx *ctx) {
|
||||
}
|
||||
|
||||
TSTreeCursor cursor = ts_tree_cursor_new(ctx->root);
|
||||
TSTreeCursor occurrence_cursor = ts_tree_cursor_copy(&cursor);
|
||||
WalkState state;
|
||||
memset(&state, 0, sizeof(state));
|
||||
state.current_cursor = &cursor;
|
||||
state.occurrence_cursor = &occurrence_cursor;
|
||||
state.arena = ctx->arena;
|
||||
state.language = ctx->language;
|
||||
state.scopes = state.inline_scopes;
|
||||
state.scope_capacity = MAX_SCOPES;
|
||||
state.lexical_scopes = state.inline_lexical_scopes;
|
||||
state.lexical_scope_capacity = INLINE_LEXICAL_SCOPES;
|
||||
state.usage_start_index = ctx->result->usages.count;
|
||||
state.root_lexical_scope_id = add_lexical_scope(&state, ctx->root, CBM_LEXICAL_SCOPE_MODULE);
|
||||
|
||||
uint32_t depth = 0;
|
||||
|
||||
for (;;) {
|
||||
TSNode node = ts_tree_cursor_current_node(&cursor);
|
||||
bool trivia = is_unified_trivia_node(node);
|
||||
if (!trivia) {
|
||||
/* Trivia consumes no semantic state. Scope expiry may be deferred
|
||||
* until the next code-bearing node, which recomputes before use. */
|
||||
pop_expired_scopes(&state, depth);
|
||||
recompute_state(&state, ctx->module_qn);
|
||||
if (push_pre_node_scope(ctx, node, spec, &state, depth)) {
|
||||
recompute_state(&state, ctx->module_qn);
|
||||
}
|
||||
|
||||
pop_expired_scopes(&state, depth);
|
||||
recompute_state(&state, ctx->module_qn);
|
||||
handle_string_constants(ctx, node, &state);
|
||||
handle_objectscript_type_map(ctx, node, &state);
|
||||
CBMInvocationDescriptor invocation = handle_calls(ctx, node, spec, &state);
|
||||
handle_usages(ctx, node, spec, &state);
|
||||
handle_throws(ctx, node, spec, &state);
|
||||
handle_readwrites(ctx, node, spec, &state);
|
||||
handle_type_refs(ctx, node, spec, &state);
|
||||
handle_env_accesses(ctx, node, spec, &state);
|
||||
handle_type_assigns(ctx, node, spec, &state);
|
||||
handle_string_refs(ctx, node, &state);
|
||||
handle_yaml_nested(ctx, node);
|
||||
scan_infra_bindings(ctx, node);
|
||||
|
||||
handle_string_constants(ctx, node, &state);
|
||||
handle_objectscript_type_map(ctx, node, &state);
|
||||
handle_calls(ctx, node, spec, &state);
|
||||
handle_usages(ctx, node, spec, &state);
|
||||
handle_throws(ctx, node, spec, &state);
|
||||
handle_readwrites(ctx, node, spec, &state);
|
||||
handle_type_refs(ctx, node, spec, &state);
|
||||
handle_env_accesses(ctx, node, spec, &state);
|
||||
handle_type_assigns(ctx, node, spec, &state);
|
||||
handle_string_refs(ctx, node, &state);
|
||||
handle_yaml_nested(ctx, node);
|
||||
scan_infra_bindings(ctx, node);
|
||||
push_boundary_scopes(ctx, node, spec, &state, depth, &invocation);
|
||||
}
|
||||
|
||||
push_boundary_scopes(ctx, node, spec, &state, depth);
|
||||
|
||||
if (ts_tree_cursor_goto_first_child(&cursor)) {
|
||||
/* Lexer-terminal trivia has no semantic work and no descendants. Avoid
|
||||
* asking the cursor to construct a child iterator for every one of
|
||||
* hundreds of thousands of flat comment siblings. Structured extras
|
||||
* still descend normally. */
|
||||
if ((!trivia || ts_node_child_count(node) > 0) &&
|
||||
ts_tree_cursor_goto_first_child(&cursor)) {
|
||||
depth++;
|
||||
continue;
|
||||
}
|
||||
@@ -1518,5 +2087,7 @@ void cbm_extract_unified(CBMExtractCtx *ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
cbm_finalize_lexical_usages(ctx, &state);
|
||||
ts_tree_cursor_delete(&occurrence_cursor);
|
||||
ts_tree_cursor_delete(&cursor);
|
||||
}
|
||||
|
||||
+118
-15
@@ -11,8 +11,13 @@
|
||||
#define SCOPE_IMPORT 4
|
||||
#define SCOPE_LOOP 5
|
||||
#define SCOPE_BRANCH 6
|
||||
#define SCOPE_LEXICAL 7
|
||||
#define SCOPE_NAMESPACE 8
|
||||
|
||||
#define MAX_SCOPES 64
|
||||
#define INLINE_LEXICAL_SCOPES 64
|
||||
#define INLINE_LEXICAL_BINDINGS 64
|
||||
#define INLINE_PYTHON_DIRECTIVES 16
|
||||
|
||||
// ObjectScript type map: variable name → class name (for instance_method_call
|
||||
// resolution). Stack-allocated, per-method scope. Overflow is silent (no crash).
|
||||
@@ -28,30 +33,128 @@ typedef struct {
|
||||
int class_base_count; // entries [0,class_base_count) survive method-scope resets
|
||||
} os_type_map_t;
|
||||
|
||||
// WalkState tracks scope context during the unified cursor walk.
|
||||
// Replaces parent-chain walks for enclosing_func_qn, inside_call, etc.
|
||||
typedef struct {
|
||||
const char *enclosing_func_qn; // current function QN (module_qn at top level)
|
||||
const char *enclosing_class_qn; // current class QN (NULL outside class)
|
||||
bool inside_call; // within a call_node_types subtree
|
||||
bool inside_import; // within an import_node_types subtree
|
||||
int loop_depth; // count of enclosing loop scopes (for bottleneck metrics)
|
||||
int branch_depth; // count of enclosing branch scopes
|
||||
// A call consumes only the exact AST occurrence that denotes its callee. The
|
||||
// rest of the call subtree (receiver, computed key, arguments, callback body)
|
||||
// remains ordinary expression input and is eligible for USAGE extraction.
|
||||
typedef enum {
|
||||
CBM_INVOCATION_NONE = 0,
|
||||
CBM_INVOCATION_PRIMARY,
|
||||
CBM_INVOCATION_CALLABLE_REFERENCE,
|
||||
} CBMInvocationKind;
|
||||
|
||||
struct {
|
||||
const char *qn;
|
||||
uint32_t depth;
|
||||
uint8_t kind;
|
||||
} scopes[MAX_SCOPES];
|
||||
typedef struct {
|
||||
CBMInvocationKind kind;
|
||||
TSNode site;
|
||||
TSNode callee_expr;
|
||||
TSNode callee_leaf;
|
||||
const char *callee_name;
|
||||
bool raw_call_emitted;
|
||||
} CBMInvocationDescriptor;
|
||||
|
||||
typedef struct {
|
||||
const char *qn;
|
||||
uint32_t depth;
|
||||
uint32_t lexical_scope_id;
|
||||
uint8_t kind;
|
||||
CBMInvocationKind invocation_kind;
|
||||
TSNode callee_expr;
|
||||
TSNode callee_leaf;
|
||||
} CBMWalkScope;
|
||||
|
||||
typedef enum {
|
||||
CBM_LEXICAL_SCOPE_MODULE = 0,
|
||||
CBM_LEXICAL_SCOPE_CLASS,
|
||||
CBM_LEXICAL_SCOPE_FUNCTION,
|
||||
CBM_LEXICAL_SCOPE_BLOCK,
|
||||
CBM_LEXICAL_SCOPE_COMPREHENSION,
|
||||
} CBMLexicalScopeKind;
|
||||
|
||||
/* Concrete AST scope identity. QNs remain graph-attribution metadata only;
|
||||
* overloads, lambdas and sibling blocks therefore never share binding facts. */
|
||||
typedef struct {
|
||||
uint32_t id;
|
||||
uint32_t parent_id;
|
||||
uint32_t lookup_parent_id;
|
||||
uint32_t start_byte;
|
||||
uint32_t end_byte;
|
||||
uint8_t kind;
|
||||
} CBMLexicalScope;
|
||||
|
||||
/* Deferred binding event. Applying these after the walk represents hoisted
|
||||
* and whole-scope rules without depending on traversal order. */
|
||||
typedef struct {
|
||||
uint32_t scope_id;
|
||||
uint32_t active_start;
|
||||
uint32_t active_end;
|
||||
const char *name;
|
||||
} CBMLexicalBinding;
|
||||
|
||||
typedef enum {
|
||||
CBM_PYTHON_DIRECTIVE_GLOBAL = 1,
|
||||
CBM_PYTHON_DIRECTIVE_NONLOCAL,
|
||||
} CBMPythonDirectiveKind;
|
||||
|
||||
typedef struct {
|
||||
uint32_t function_scope_id;
|
||||
const char *name;
|
||||
uint8_t kind;
|
||||
} CBMPythonDirective;
|
||||
|
||||
// WalkState tracks scope context during the unified cursor walk.
|
||||
// Replaces parent-chain walks for enclosing_func_qn, import context, etc.
|
||||
typedef struct {
|
||||
const char *enclosing_func_qn; // current function QN (module_qn at top level)
|
||||
const char *enclosing_class_qn; // current class QN (NULL outside class)
|
||||
const TSTreeCursor *current_cursor; // unified walk cursor at the current node
|
||||
TSTreeCursor *occurrence_cursor; // reusable parent-preserving classifier cursor
|
||||
CBMInvocationKind invocation_kind; // exact active invocation/reference role
|
||||
TSNode callee_expr; // exact active callee expression, if any
|
||||
TSNode callee_leaf; // exact active terminal callee, if any
|
||||
bool inside_import; // within an import_node_types subtree
|
||||
int loop_depth; // count of enclosing loop scopes (for bottleneck metrics)
|
||||
int branch_depth; // count of enclosing branch scopes
|
||||
|
||||
CBMArena *arena;
|
||||
CBMWalkScope *scopes;
|
||||
CBMWalkScope inline_scopes[MAX_SCOPES];
|
||||
int scope_capacity;
|
||||
int scope_top;
|
||||
|
||||
CBMLexicalScope *lexical_scopes;
|
||||
CBMLexicalScope inline_lexical_scopes[INLINE_LEXICAL_SCOPES];
|
||||
int lexical_scope_capacity;
|
||||
int lexical_scope_count;
|
||||
uint32_t root_lexical_scope_id;
|
||||
uint32_t split_function_scope_id;
|
||||
uint32_t split_signature_start_byte;
|
||||
uint32_t split_signature_end_byte;
|
||||
const char *split_function_qn;
|
||||
uint32_t flat_function_scope_id;
|
||||
uint32_t flat_anchor_start_byte;
|
||||
uint32_t flat_anchor_end_byte;
|
||||
const char *flat_function_qn;
|
||||
|
||||
CBMLexicalBinding *lexical_bindings;
|
||||
CBMLexicalBinding inline_lexical_bindings[INLINE_LEXICAL_BINDINGS];
|
||||
int lexical_binding_capacity;
|
||||
int lexical_binding_count;
|
||||
int usage_start_index;
|
||||
bool lexical_binding_tracking_failed;
|
||||
CBMPythonDirective *python_directives;
|
||||
CBMPythonDirective inline_python_directives[INLINE_PYTHON_DIRECTIVES];
|
||||
int python_directive_capacity;
|
||||
int python_directive_count;
|
||||
CBMLanguage language;
|
||||
|
||||
os_type_map_t os_type_map; // ObjectScript variable → type mapping
|
||||
} WalkState;
|
||||
|
||||
// Per-node handler prototypes. Each is called once per node during the
|
||||
// unified cursor walk, replacing the old recursive walk_* functions.
|
||||
void handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state);
|
||||
CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec,
|
||||
WalkState *state);
|
||||
void handle_usages(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state);
|
||||
void cbm_finalize_lexical_usages(CBMExtractCtx *ctx, WalkState *state);
|
||||
void handle_throws(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state);
|
||||
void handle_readwrites(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state);
|
||||
void handle_type_refs(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state);
|
||||
|
||||
+2376
-39
File diff suppressed because it is too large
Load Diff
+15
-1
@@ -460,6 +460,19 @@ bool cbm_kind_in_set(TSNode node, const char **types) {
|
||||
return kind_in_set_strcmp(node, (const char *const *)types);
|
||||
}
|
||||
|
||||
bool cbm_is_namespace_scope_kind(CBMLanguage lang, const char *kind) {
|
||||
if (!kind) {
|
||||
return false;
|
||||
}
|
||||
if (lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA) {
|
||||
return strcmp(kind, "namespace_definition") == 0;
|
||||
}
|
||||
if (lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX) {
|
||||
return strcmp(kind, "internal_module") == 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Free the calling thread's node-type bitset cache (the calloc'd `bits` arrays
|
||||
* that cbm_kind_in_set builds lazily). The cache is thread-local, so each worker
|
||||
* thread and the main thread must call this at teardown (worker exit / process
|
||||
@@ -917,7 +930,8 @@ const char *cbm_enclosing_func_qn(CBMArena *a, TSNode node, CBMLanguage lang, co
|
||||
const char *class_chain = NULL;
|
||||
for (TSNode cur = ts_node_parent(func_node); !ts_node_is_null(cur);
|
||||
cur = ts_node_parent(cur)) {
|
||||
if (!cbm_kind_in_set(cur, spec->class_node_types)) {
|
||||
if (!cbm_kind_in_set(cur, spec->class_node_types) &&
|
||||
!cbm_is_namespace_scope_kind(lang, ts_node_type(cur))) {
|
||||
continue;
|
||||
}
|
||||
TSNode class_name = ts_node_child_by_field_name(cur, TS_FIELD("name"));
|
||||
|
||||
@@ -86,6 +86,11 @@ TSNode cbm_find_child_by_kind(TSNode parent, const char *kind);
|
||||
// Check if node kind matches a set of types (NULL-terminated array of strings).
|
||||
bool cbm_kind_in_set(TSNode node, const char **types);
|
||||
|
||||
/* Namespace/module declarations that extend a qualified-name scope without
|
||||
* turning their children into class methods. Shared by definition and unified
|
||||
* walks so TS/TSX scope attribution cannot drift. */
|
||||
bool cbm_is_namespace_scope_kind(CBMLanguage lang, const char *kind);
|
||||
|
||||
// Free the calling thread's cbm_kind_in_set bitset cache (call at thread/process
|
||||
// teardown so the thread-local cache is not reported as a leak).
|
||||
void cbm_kind_in_set_free_cache(void);
|
||||
|
||||
+31
-36
@@ -198,9 +198,10 @@ static const char *go_assign_types[] = {"assignment_statement", "short_var_decla
|
||||
static const char *py_func_types[] = {"function_definition", NULL};
|
||||
static const char *py_class_types[] = {"class_definition", NULL};
|
||||
static const char *py_module_types[] = {"module", NULL};
|
||||
static const char *py_call_types[] = {"call", "with_statement", NULL};
|
||||
static const char *py_call_types[] = {"call", NULL};
|
||||
static const char *py_import_types[] = {"import_statement", "import", NULL};
|
||||
static const char *py_import_from_types[] = {"import_from_statement", NULL};
|
||||
static const char *py_import_from_types[] = {"import_from_statement", "future_import_statement",
|
||||
NULL};
|
||||
static const char *py_branch_types[] = {
|
||||
"if_statement", "for_statement", "while_statement", "try_statement",
|
||||
"except_clause", "with_statement", "elif_clause", NULL};
|
||||
@@ -215,13 +216,8 @@ static const char *js_func_types[] = {"function_declaration", "generator_functio
|
||||
static const char *js_class_types[] = {"class_declaration", "class", NULL};
|
||||
static const char *js_module_types[] = {"program", NULL};
|
||||
static const char *js_call_types[] = {"call_expression", "new_expression", NULL};
|
||||
static const char *js_import_types[] = {"import_statement",
|
||||
"lexical_declaration",
|
||||
"export_statement",
|
||||
"import",
|
||||
"extends",
|
||||
"require",
|
||||
NULL};
|
||||
static const char *js_import_types[] = {"import_statement", "export_statement", "import",
|
||||
"extends", "require", NULL};
|
||||
static const char *js_branch_types[] = {"if_statement",
|
||||
"for_statement",
|
||||
"for_in_statement",
|
||||
@@ -343,12 +339,8 @@ static const char *cpp_class_types[] = {"class_specifier", "struct_specifier", "
|
||||
static const char *cpp_field_types[] = {"field_declaration", NULL};
|
||||
static const char *cpp_module_types[] = {"translation_unit", "namespace_definition",
|
||||
"linkage_specification", "declaration", NULL};
|
||||
static const char *cpp_call_types[] = {
|
||||
"call_expression", "field_expression", "subscript_expression",
|
||||
"new_expression", "delete_expression", "binary_expression",
|
||||
"unary_expression", "update_expression", NULL};
|
||||
static const char *cpp_import_types[] = {"preproc_include", "template_function", "declaration",
|
||||
NULL};
|
||||
static const char *cpp_call_types[] = {"call_expression", "new_expression", NULL};
|
||||
static const char *cpp_import_types[] = {"preproc_include", NULL};
|
||||
static const char *cpp_branch_types[] = {"if_statement", "for_statement", "for_range_loop",
|
||||
"while_statement", "switch_statement", "case_statement",
|
||||
"try_statement", "catch_clause", NULL};
|
||||
@@ -422,8 +414,7 @@ static const char *scala_class_types[] = {"class_definition", "object_definition
|
||||
"trait_definition", "enum_definition",
|
||||
"type_definition", NULL};
|
||||
static const char *scala_module_types[] = {"compilation_unit", NULL};
|
||||
static const char *scala_call_types[] = {"call_expression", "generic_function",
|
||||
"field_expression", "infix_expression",
|
||||
static const char *scala_call_types[] = {"call_expression", "infix_expression",
|
||||
"instance_expression", NULL};
|
||||
static const char *scala_import_types[] = {"import_declaration", "extends", "import",
|
||||
"using_directive", NULL};
|
||||
@@ -442,7 +433,7 @@ static const char *kotlin_func_types[] = {"function_declaration", "secondary_con
|
||||
static const char *kotlin_class_types[] = {"class_declaration", "object_declaration",
|
||||
"companion_object", "type_alias", NULL};
|
||||
static const char *kotlin_module_types[] = {"source_file", NULL};
|
||||
static const char *kotlin_call_types[] = {"call_expression", "navigation_expression", NULL};
|
||||
static const char *kotlin_call_types[] = {"call_expression", NULL};
|
||||
static const char *kotlin_import_types[] = {"import", NULL};
|
||||
static const char *kotlin_branch_types[] = {
|
||||
"if_expression", "for_statement", "while_statement", "when_expression",
|
||||
@@ -456,7 +447,7 @@ static const char *kotlin_decorator_types[] = {"annotation", NULL};
|
||||
static const char *ruby_func_types[] = {"method", "singleton_method", NULL};
|
||||
static const char *ruby_class_types[] = {"class", "module", NULL};
|
||||
static const char *ruby_module_types[] = {"program", NULL};
|
||||
static const char *ruby_call_types[] = {"call", "command_call", NULL};
|
||||
static const char *ruby_call_types[] = {"call", NULL};
|
||||
static const char *ruby_import_types[] = {"call", NULL};
|
||||
static const char *ruby_branch_types[] = {"if", "unless", "while", "until", "for",
|
||||
"case", "when", "rescue", "elsif", NULL};
|
||||
@@ -507,7 +498,7 @@ static const char *zig_assign_types[] = {"assignment_expression", NULL};
|
||||
// ==================== ELIXIR ====================
|
||||
static const char *elixir_func_types[] = {"call", "anonymous_function", NULL};
|
||||
static const char *elixir_module_types[] = {"source", NULL};
|
||||
static const char *elixir_call_types[] = {"call", "dot", "binary_operator", NULL};
|
||||
static const char *elixir_call_types[] = {"call", "binary_operator", NULL};
|
||||
static const char *elixir_import_types[] = {"call", NULL};
|
||||
static const char *elixir_branch_types[] = {"call", NULL};
|
||||
static const char *elixir_var_types[] = {"binary_operator", NULL};
|
||||
@@ -562,7 +553,7 @@ static const char *swift_class_types[] = {"class_declaration", "protocol_declara
|
||||
static const char *swift_field_types[] = {"property_declaration", NULL};
|
||||
static const char *swift_module_types[] = {"source_file", NULL};
|
||||
static const char *swift_call_types[] = {"call_expression", "constructor_expression",
|
||||
"macro_invocation", "navigation_expression", NULL};
|
||||
"macro_invocation", NULL};
|
||||
static const char *swift_import_types[] = {"import_declaration", "import", NULL};
|
||||
static const char *swift_branch_types[] = {"if_statement", "guard_statement", "for_statement",
|
||||
"while_statement", "switch_statement", NULL};
|
||||
@@ -669,7 +660,7 @@ static const char *sql_func_types[] = {"create_function", "function_declaration"
|
||||
static const char *sql_field_types[] = {"column_definition", NULL};
|
||||
static const char *sql_class_types[] = {"custom_type", NULL};
|
||||
static const char *sql_module_types[] = {"program", NULL};
|
||||
static const char *sql_call_types[] = {"function_call", "invocation", "command", NULL};
|
||||
static const char *sql_call_types[] = {"invocation", NULL};
|
||||
static const char *sql_branch_types[] = {"if_statement", "case_expression", NULL};
|
||||
static const char *sql_var_types[] = {"create_table", "create_view", NULL};
|
||||
|
||||
@@ -717,7 +708,7 @@ static const char *fsharp_func_types[] = {"function_declaration", "value_declara
|
||||
"function_or_value_defn", NULL};
|
||||
static const char *fsharp_class_types[] = {"type_definition", "exception_definition", NULL};
|
||||
static const char *fsharp_module_types[] = {"file", NULL};
|
||||
static const char *fsharp_call_types[] = {"application_expression", "dot_expression", NULL};
|
||||
static const char *fsharp_call_types[] = {"application_expression", NULL};
|
||||
static const char *fsharp_import_types[] = {"import_decl", "open_expression", "instance", NULL};
|
||||
static const char *fsharp_branch_types[] = {"if_expression", "for_expression",
|
||||
"while_expression", "match_expression",
|
||||
@@ -770,7 +761,7 @@ static const char *elm_func_types[] = {"value_declaration", "function_declaratio
|
||||
static const char *elm_class_types[] = {"type_declaration", "type_alias_declaration",
|
||||
"module_declaration", NULL};
|
||||
static const char *elm_module_types[] = {"file", NULL};
|
||||
static const char *elm_call_types[] = {"function_call", "function_call_expr", NULL};
|
||||
static const char *elm_call_types[] = {"function_call_expr", NULL};
|
||||
static const char *elm_import_types[] = {"import", NULL};
|
||||
static const char *elm_branch_types[] = {"case_of_expr", "if_else_expr", NULL};
|
||||
|
||||
@@ -781,7 +772,7 @@ static const char *fortran_func_types[] = {"function", "subroutine", "function_s
|
||||
"subroutine_statement", NULL};
|
||||
static const char *fortran_class_types[] = {"derived_type_definition", "enum_statement", NULL};
|
||||
static const char *fortran_module_types[] = {"translation_unit", NULL};
|
||||
static const char *fortran_call_types[] = {"call_expression", "keyword_argument", "call", NULL};
|
||||
static const char *fortran_call_types[] = {"call_expression", "subroutine_call", NULL};
|
||||
static const char *fortran_import_types[] = {"use_statement", "include_statement", "extends",
|
||||
"import", "import_statement", "include",
|
||||
"open_statement", "preproc_include", NULL};
|
||||
@@ -926,7 +917,7 @@ static const char *matlab_var_types[] = {"assignment", NULL};
|
||||
static const char *lean_func_types[] = {"def", "theorem", "instance", "abbrev", NULL};
|
||||
static const char *lean_class_types[] = {"structure", "class_inductive", "inductive", NULL};
|
||||
static const char *lean_module_types[] = {"module", NULL};
|
||||
static const char *lean_call_types[] = {"apply", "command", NULL};
|
||||
static const char *lean_call_types[] = {"apply", NULL};
|
||||
static const char *lean_import_types[] = {"import", "extends", "instance", NULL};
|
||||
static const char *lean_branch_types[] = {"if", "match", "do", NULL};
|
||||
|
||||
@@ -973,8 +964,7 @@ static const char *d_class_types[] = {"class_declaration",
|
||||
"module_def",
|
||||
NULL};
|
||||
static const char *d_field_types[] = {"variable_declaration", NULL};
|
||||
static const char *d_call_types[] = {"call_expression", "function_call_expression",
|
||||
"new_expression", NULL};
|
||||
static const char *d_call_types[] = {"call_expression", "new_expression", NULL};
|
||||
static const char *d_import_types[] = {"import_declaration", "import", NULL};
|
||||
static const char *d_branch_types[] = {"if_statement", "for_statement", "foreach_statement",
|
||||
"while_statement", "do_statement", "switch_statement",
|
||||
@@ -1094,7 +1084,7 @@ static const char *fish_module_types[] = {"program", NULL};
|
||||
* called by name, so a call inside a rule is legitimately Module-sourced, and a
|
||||
* rule must NOT be treated as a function boundary. */
|
||||
static const char *awk_func_types[] = {"func_def", NULL};
|
||||
static const char *awk_call_types[] = {"func_call", "command", NULL};
|
||||
static const char *awk_call_types[] = {"func_call", NULL};
|
||||
static const char *awk_branch_types[] = {"if_statement",
|
||||
"for_statement",
|
||||
"for_in_statement",
|
||||
@@ -1309,7 +1299,7 @@ static const char *prisma_call_types[] = {"call_expression", NULL};
|
||||
static const char *prisma_module_types[] = {"program", NULL};
|
||||
static const char *hyprlang_module_types[] = {"source_file", NULL};
|
||||
static const char *dotenv_module_types[] = {"source_file", NULL};
|
||||
static const char *diff_call_types[] = {"command", NULL};
|
||||
static const char *diff_call_types[] = {NULL};
|
||||
static const char *diff_module_types[] = {"source", NULL};
|
||||
static const char *wgsl_func_types[] = {"function_declaration", NULL};
|
||||
static const char *wgsl_class_types[] = {"struct_declaration", "type_alias_declaration",
|
||||
@@ -1352,7 +1342,7 @@ static const char *properties_var_types[] = {"property", NULL};
|
||||
/* tree-sitter-properties roots the tree at `file`, not `source_file`. */
|
||||
static const char *properties_module_types[] = {"file", "source_file", NULL};
|
||||
static const char *sshconfig_module_types[] = {"source_file", NULL};
|
||||
static const char *bibtex_call_types[] = {"command", NULL};
|
||||
static const char *bibtex_call_types[] = {NULL};
|
||||
static const char *bibtex_module_types[] = {"document", NULL};
|
||||
static const char *starlark_func_types[] = {"function_definition", "lambda", NULL};
|
||||
static const char *starlark_call_types[] = {"call", NULL};
|
||||
@@ -1423,7 +1413,7 @@ static const char *devicetree_call_types[] = {"call_expression", NULL};
|
||||
static const char *devicetree_import_types[] = {"dtsi_include", "preproc_include", NULL};
|
||||
static const char *devicetree_module_types[] = {"document", NULL};
|
||||
static const char *linkerscript_call_types[] = {"call_expression", NULL};
|
||||
static const char *linkerscript_module_types[] = {"source_file", NULL};
|
||||
static const char *linkerscript_module_types[] = {"linkerscript", NULL};
|
||||
static const char *gn_call_types[] = {"call_expression", NULL};
|
||||
static const char *gn_import_types[] = {"import_statement", "import", NULL};
|
||||
static const char *gn_branch_types[] = {"if_statement", "foreach_statement", NULL};
|
||||
@@ -1510,8 +1500,7 @@ static const char *mermaid_module_types[] = {"source_file", NULL};
|
||||
static const char *puppet_func_types[] = {"function_declaration", "lambda", NULL};
|
||||
static const char *puppet_class_types[] = {"class_definition", "node_definition",
|
||||
"resource_declaration", "type_declaration", NULL};
|
||||
static const char *puppet_call_types[] = {"function_call", "resource_declaration",
|
||||
"include_statement", NULL};
|
||||
static const char *puppet_call_types[] = {"function_call", "include_statement", NULL};
|
||||
static const char *puppet_import_types[] = {"include_statement", "require_statement", "include",
|
||||
"require", NULL};
|
||||
static const char *puppet_branch_types[] = {"if_statement", "unless_statement", "case_statement",
|
||||
@@ -1628,7 +1617,9 @@ static const char *objectscript_udl_module_types[] = {"source_file", NULL};
|
||||
static const char *objectscript_udl_branch_types[] = {
|
||||
"command_if", "command_for", "command_while", "elseif_block", "catch_block", NULL};
|
||||
|
||||
static const char *objectscript_routine_func_types[] = {"tag", NULL};
|
||||
/* Parameterized entries are `procedure` nodes whose direct `tag` child names
|
||||
* the callable; bare labels remain standalone `tag` nodes. */
|
||||
static const char *objectscript_routine_func_types[] = {"procedure", "tag", NULL};
|
||||
static const char *objectscript_routine_call_types[] = {"extrinsic_function", "routine_tag_call",
|
||||
NULL};
|
||||
static const char *objectscript_routine_module_types[] = {"source_file", NULL};
|
||||
@@ -2645,7 +2636,11 @@ const CBMLangSpec *cbm_lang_spec(CBMLanguage lang) {
|
||||
if (lang < 0 || lang >= CBM_LANG_COUNT) {
|
||||
return NULL;
|
||||
}
|
||||
return &lang_specs[lang];
|
||||
const CBMLangSpec *spec = &lang_specs[lang];
|
||||
if (spec->language != lang || !spec->function_node_types) {
|
||||
return NULL;
|
||||
}
|
||||
return spec;
|
||||
}
|
||||
|
||||
const TSLanguage *cbm_ts_language(CBMLanguage lang) {
|
||||
|
||||
+669
-153
File diff suppressed because it is too large
Load Diff
@@ -60,10 +60,14 @@ typedef struct {
|
||||
|
||||
// Output
|
||||
CBMResolvedCallArray *resolved_calls;
|
||||
CBMSourceOrigin source_origin; // source buffer represented by emitted occurrence spans
|
||||
|
||||
// Function pointer targets: var_name -> target function QN
|
||||
// Function pointer targets: lexical binding -> exact target function QN.
|
||||
// A NULL target is an explicitly unknown/ambiguous binding and must shadow
|
||||
// any outer exact target of the same name.
|
||||
const char **fp_var_names;
|
||||
const char **fp_target_qns;
|
||||
const CBMScope **fp_binding_scopes;
|
||||
int fp_count;
|
||||
int fp_cap;
|
||||
|
||||
@@ -93,6 +97,7 @@ typedef struct {
|
||||
int eval_depth; // recursion depth for c_eval_expr_type (crash guard)
|
||||
int eval_steps; // total expression eval calls for current file (hang guard)
|
||||
int walk_depth; // c_resolve_calls_in_node self-recursion (AST nesting)
|
||||
int control_flow_depth; // if/loop/switch/catch nesting; assignments merge fail-closed
|
||||
} CLSPContext;
|
||||
|
||||
// --- API ---
|
||||
@@ -120,7 +125,7 @@ const CBMType *c_simplify_type(CLSPContext *ctx, const CBMType *t, bool unwrap_p
|
||||
|
||||
// Single-file LSP: build registry from file defs + stdlib, run resolution.
|
||||
void cbm_run_c_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len,
|
||||
TSNode root, bool cpp_mode);
|
||||
TSNode root, bool cpp_mode, CBMSourceOrigin source_origin);
|
||||
|
||||
// Cross-file LSP: build registry from defs + stdlib, re-parse and resolve.
|
||||
void cbm_run_c_lsp_cross(CBMArena *arena, const char *source, int source_len, const char *module_qn,
|
||||
|
||||
+469
-56
@@ -73,6 +73,7 @@ static void cs_register_type_decls(CSLSPContext *ctx, CBMTypeRegistry *reg, TSNo
|
||||
static char *cs_node_text_cached(CSLSPContext *ctx, TSNode node);
|
||||
static const CBMType *cs_unwrap_task(CSLSPContext *ctx, const CBMType *t);
|
||||
static const CBMType *cs_unwrap_nullable(const CBMType *t);
|
||||
static const char *cs_resolve_callable_value(CSLSPContext *ctx, TSNode node, TSNode *callable_leaf);
|
||||
|
||||
/* ── small helpers ──────────────────────────────────────────────── */
|
||||
|
||||
@@ -499,6 +500,38 @@ const CBMRegisteredFunc *cs_lookup_method(CSLSPContext *ctx, const char *type_qn
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* A static import is precise only when every matching directive reaches the
|
||||
* same method. Repeated imports of one type are harmless, but selecting the
|
||||
* first of two distinct targets would make resolution declaration-order
|
||||
* dependent. */
|
||||
static const CBMRegisteredFunc *cs_lookup_using_static_method(CSLSPContext *ctx,
|
||||
const char *method_name,
|
||||
bool *ambiguous) {
|
||||
if (ambiguous)
|
||||
*ambiguous = false;
|
||||
if (!ctx || !method_name)
|
||||
return NULL;
|
||||
|
||||
const CBMRegisteredFunc *only = NULL;
|
||||
for (int i = 0; i < ctx->using_count; i++) {
|
||||
const CBMCSUsing *u = &ctx->usings[i];
|
||||
if (u->kind != CBM_CS_USING_STATIC)
|
||||
continue;
|
||||
const char *host = cs_resolve_type_name(ctx, u->target_qn);
|
||||
const CBMRegisteredFunc *candidate =
|
||||
cs_lookup_method(ctx, host ? host : u->target_qn, method_name);
|
||||
if (!candidate || !candidate->qualified_name)
|
||||
continue;
|
||||
if (only && strcmp(only->qualified_name, candidate->qualified_name) != 0) {
|
||||
if (ambiguous)
|
||||
*ambiguous = true;
|
||||
return NULL;
|
||||
}
|
||||
only = candidate;
|
||||
}
|
||||
return only;
|
||||
}
|
||||
|
||||
/* ── extension methods ──────────────────────────────────────────── */
|
||||
|
||||
/* Search for a static extension method `M(this U self, ...)` accessible
|
||||
@@ -1041,15 +1074,13 @@ static const CBMType *cs_eval_invocation_type(CSLSPContext *ctx, TSNode call) {
|
||||
return f->signature->data.func.return_types[0];
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < ctx->using_count; i++) {
|
||||
const CBMCSUsing *u = &ctx->usings[i];
|
||||
if (u->kind != CBM_CS_USING_STATIC) continue;
|
||||
const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", u->target_qn, bare);
|
||||
const CBMRegisteredFunc *f = cbm_registry_lookup_func(ctx->registry, qn);
|
||||
if (f && f->signature && f->signature->data.func.return_types &&
|
||||
f->signature->data.func.return_types[0]) {
|
||||
return f->signature->data.func.return_types[0];
|
||||
}
|
||||
bool ambiguous = false;
|
||||
const CBMRegisteredFunc *f = cs_lookup_using_static_method(ctx, bare, &ambiguous);
|
||||
if (ambiguous)
|
||||
return cbm_type_unknown();
|
||||
if (f && f->signature && f->signature->data.func.return_types &&
|
||||
f->signature->data.func.return_types[0]) {
|
||||
return f->signature->data.func.return_types[0];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1255,6 +1286,166 @@ static void cs_bind_parameters(CSLSPContext *ctx, TSNode params_node, bool is_ex
|
||||
|
||||
/* ── statement processing ───────────────────────────────────────── */
|
||||
|
||||
/* Resolve a source expression only when it denotes one exact callable value.
|
||||
* This deliberately accepts just direct method groups (bare or member access):
|
||||
* conditional/switch/lambda expressions remain ordinary usages because they
|
||||
* do not prove one target at this occurrence. */
|
||||
static const char *cs_resolve_callable_name(CSLSPContext *ctx, const char *name) {
|
||||
if (!ctx || !name || !name[0])
|
||||
return NULL;
|
||||
|
||||
/* A lexical binding always wins, including an UNKNOWN ordinary binding:
|
||||
* a nearer non-callable value must suppress a same-named class method. */
|
||||
if (cbm_scope_contains(ctx->current_scope, name)) {
|
||||
return cbm_scope_lookup_callable(ctx->current_scope, name);
|
||||
}
|
||||
|
||||
if (ctx->enclosing_class_qn) {
|
||||
const CBMRegisteredFunc *f = cs_lookup_method(ctx, ctx->enclosing_class_qn, name);
|
||||
if (f && f->qualified_name)
|
||||
return f->qualified_name;
|
||||
}
|
||||
if (ctx->enclosing_base_qn) {
|
||||
const CBMRegisteredFunc *f = cs_lookup_method(ctx, ctx->enclosing_base_qn, name);
|
||||
if (f && f->qualified_name)
|
||||
return f->qualified_name;
|
||||
}
|
||||
|
||||
bool using_static_ambiguous = false;
|
||||
const CBMRegisteredFunc *using_static =
|
||||
cs_lookup_using_static_method(ctx, name, &using_static_ambiguous);
|
||||
if (using_static_ambiguous)
|
||||
return NULL;
|
||||
if (using_static)
|
||||
return using_static->qualified_name;
|
||||
|
||||
const char *ns = cs_namespace_qn(ctx);
|
||||
if (ns && ns[0]) {
|
||||
const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ns, name);
|
||||
const CBMRegisteredFunc *f = cbm_registry_lookup_func(ctx->registry, qn);
|
||||
if (f && f->qualified_name)
|
||||
return f->qualified_name;
|
||||
}
|
||||
|
||||
/* C# has no true namespace-level functions, but error-tolerant parses and
|
||||
* generated fixtures can register receiverless callables. Only accept a
|
||||
* unique short-name match; ambiguity is intentionally not guessed. */
|
||||
const CBMRegisteredFunc *only = NULL;
|
||||
for (int i = 0; ctx->registry && i < ctx->registry->func_count; i++) {
|
||||
const CBMRegisteredFunc *candidate = &ctx->registry->funcs[i];
|
||||
if (candidate->receiver_type || !candidate->short_name ||
|
||||
strcmp(candidate->short_name, name) != 0) {
|
||||
continue;
|
||||
}
|
||||
if (only && strcmp(only->qualified_name, candidate->qualified_name) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
only = candidate;
|
||||
}
|
||||
return only ? only->qualified_name : NULL;
|
||||
}
|
||||
|
||||
static TSNode cs_callable_name_leaf(TSNode node) {
|
||||
if (ts_node_is_null(node) || strcmp(ts_node_type(node), "generic_name") != 0) {
|
||||
return node;
|
||||
}
|
||||
|
||||
TSNode leaf = ts_node_child_by_field_name(node, "name", 4);
|
||||
if (!ts_node_is_null(leaf))
|
||||
return leaf;
|
||||
uint32_t nc = ts_node_child_count(node);
|
||||
for (uint32_t i = 0; i < nc; i++) {
|
||||
TSNode child = ts_node_child(node, i);
|
||||
if (!ts_node_is_null(child) && ts_node_is_named(child) &&
|
||||
strcmp(ts_node_type(child), "identifier") == 0) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
static const char *cs_resolve_callable_value(CSLSPContext *ctx, TSNode node,
|
||||
TSNode *callable_leaf) {
|
||||
if (callable_leaf)
|
||||
memset(callable_leaf, 0, sizeof(*callable_leaf));
|
||||
if (!ctx || ts_node_is_null(node))
|
||||
return NULL;
|
||||
const char *kind = ts_node_type(node);
|
||||
|
||||
if (strcmp(kind, "argument") == 0 || strcmp(kind, "parenthesized_expression") == 0 ||
|
||||
strcmp(kind, "equals_value_clause") == 0) {
|
||||
TSNode inner = ts_node_child_by_field_name(node, "expression", 10);
|
||||
if (ts_node_is_null(inner))
|
||||
inner = ts_node_child_by_field_name(node, "value", 5);
|
||||
if (ts_node_is_null(inner))
|
||||
inner = cs_first_named_child(node);
|
||||
return ts_node_is_null(inner) ? NULL : cs_resolve_callable_value(ctx, inner, callable_leaf);
|
||||
}
|
||||
|
||||
if (strcmp(kind, "identifier") == 0 || strcmp(kind, "generic_name") == 0) {
|
||||
char *raw = cs_node_text(ctx, node);
|
||||
char *bare = raw ? cs_strip_generic_args(ctx->arena, raw) : NULL;
|
||||
const char *target = bare ? cs_resolve_callable_name(ctx, bare) : NULL;
|
||||
if (target && callable_leaf)
|
||||
*callable_leaf = cs_callable_name_leaf(node);
|
||||
return target;
|
||||
}
|
||||
|
||||
if (strcmp(kind, "member_access_expression") != 0)
|
||||
return NULL;
|
||||
TSNode receiver = ts_node_child_by_field_name(node, "expression", 10);
|
||||
TSNode member = ts_node_child_by_field_name(node, "name", 4);
|
||||
if (ts_node_is_null(receiver))
|
||||
receiver = cs_first_named_child(node);
|
||||
if (ts_node_is_null(member)) {
|
||||
uint32_t nc = ts_node_child_count(node);
|
||||
for (uint32_t i = 0; i < nc; i++) {
|
||||
TSNode child = ts_node_child(node, i);
|
||||
if (ts_node_is_null(child) || !ts_node_is_named(child) || ts_node_eq(child, receiver)) {
|
||||
continue;
|
||||
}
|
||||
const char *child_kind = ts_node_type(child);
|
||||
if (strcmp(child_kind, "identifier") == 0 || strcmp(child_kind, "generic_name") == 0) {
|
||||
member = child;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ts_node_is_null(receiver) || ts_node_is_null(member))
|
||||
return NULL;
|
||||
|
||||
char *member_raw = cs_node_text(ctx, member);
|
||||
char *member_name = member_raw ? cs_strip_generic_args(ctx->arena, member_raw) : NULL;
|
||||
if (!member_name)
|
||||
return NULL;
|
||||
|
||||
char *receiver_text = cs_node_text(ctx, receiver);
|
||||
if (receiver_text) {
|
||||
const char *type_qn = cs_resolve_type_name(ctx, receiver_text);
|
||||
if (type_qn && cs_lookup_type_qn(ctx, type_qn)) {
|
||||
const CBMRegisteredFunc *f = cs_lookup_method(ctx, type_qn, member_name);
|
||||
if (f && callable_leaf)
|
||||
*callable_leaf = cs_callable_name_leaf(member);
|
||||
return f ? f->qualified_name : NULL;
|
||||
}
|
||||
}
|
||||
|
||||
const CBMType *receiver_type = cs_unwrap_nullable(cs_eval_expr_type(ctx, receiver));
|
||||
const char *type_qn = NULL;
|
||||
if (receiver_type && receiver_type->kind == CBM_TYPE_NAMED) {
|
||||
type_qn = receiver_type->data.named.qualified_name;
|
||||
} else if (receiver_type && receiver_type->kind == CBM_TYPE_TEMPLATE) {
|
||||
type_qn = receiver_type->data.template_type.template_name;
|
||||
}
|
||||
if (!type_qn)
|
||||
return NULL;
|
||||
const CBMRegisteredFunc *f = cs_lookup_method(ctx, type_qn, member_name);
|
||||
if (!f)
|
||||
f = cs_lookup_extension(ctx, type_qn, member_name);
|
||||
if (f && callable_leaf)
|
||||
*callable_leaf = cs_callable_name_leaf(member);
|
||||
return f ? f->qualified_name : NULL;
|
||||
}
|
||||
|
||||
static void cs_process_local_decl(CSLSPContext *ctx, TSNode node) {
|
||||
/* local_declaration_statement -> variable_declaration */
|
||||
TSNode vd = cs_child_named_kind(node, "variable_declaration");
|
||||
@@ -1291,12 +1482,18 @@ static void cs_process_local_decl(CSLSPContext *ctx, TSNode node) {
|
||||
* variants. Try the field-name path first, then fall back to
|
||||
* walking named children for any expression after the identifier. */
|
||||
TSNode init = ts_node_child_by_field_name(c, "value", 5);
|
||||
TSNode rhs_node;
|
||||
memset(&rhs_node, 0, sizeof(rhs_node));
|
||||
const CBMType *rhs_t = NULL;
|
||||
if (!ts_node_is_null(init)) {
|
||||
if (strcmp(ts_node_type(init), "equals_value_clause") == 0) {
|
||||
TSNode rhs = cs_first_named_child(init);
|
||||
if (!ts_node_is_null(rhs)) rhs_t = cs_eval_expr_type(ctx, rhs);
|
||||
if (!ts_node_is_null(rhs)) {
|
||||
rhs_node = rhs;
|
||||
rhs_t = cs_eval_expr_type(ctx, rhs);
|
||||
}
|
||||
} else {
|
||||
rhs_node = init;
|
||||
rhs_t = cs_eval_expr_type(ctx, init);
|
||||
}
|
||||
}
|
||||
@@ -1313,6 +1510,7 @@ static void cs_process_local_decl(CSLSPContext *ctx, TSNode node) {
|
||||
if (strcmp(ck, "equals_value_clause") == 0) {
|
||||
TSNode rhs = cs_first_named_child(cn);
|
||||
if (!ts_node_is_null(rhs)) {
|
||||
rhs_node = rhs;
|
||||
const CBMType *t = cs_eval_expr_type(ctx, rhs);
|
||||
if (t && t->kind != CBM_TYPE_UNKNOWN) rhs_t = t;
|
||||
}
|
||||
@@ -1323,6 +1521,7 @@ static void cs_process_local_decl(CSLSPContext *ctx, TSNode node) {
|
||||
continue; /* the identifier name */
|
||||
}
|
||||
if (seen_named >= 1) {
|
||||
rhs_node = cn;
|
||||
const CBMType *t = cs_eval_expr_type(ctx, cn);
|
||||
if (t && t->kind != CBM_TYPE_UNKNOWN) {
|
||||
rhs_t = t;
|
||||
@@ -1333,7 +1532,13 @@ static void cs_process_local_decl(CSLSPContext *ctx, TSNode node) {
|
||||
}
|
||||
const CBMType *bind = is_var ? (rhs_t ? rhs_t : declared_type) : declared_type;
|
||||
if (!bind) bind = cbm_type_unknown();
|
||||
cbm_scope_bind(ctx->current_scope, vname, bind);
|
||||
const char *callable_qn =
|
||||
ts_node_is_null(rhs_node) ? NULL : cs_resolve_callable_value(ctx, rhs_node, NULL);
|
||||
if (callable_qn) {
|
||||
cbm_scope_bind_callable(ctx->current_scope, vname, bind, callable_qn);
|
||||
} else {
|
||||
cbm_scope_bind(ctx->current_scope, vname, bind);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1398,28 +1603,163 @@ static void cs_process_assignment(CSLSPContext *ctx, TSNode node) {
|
||||
if (!cs_node_is(lhs, "identifier")) return;
|
||||
char *vname = cs_node_text(ctx, lhs);
|
||||
if (!vname) return;
|
||||
const CBMType *t = cs_eval_expr_type(ctx, rhs);
|
||||
if (!t || t->kind == CBM_TYPE_UNKNOWN) return;
|
||||
/* Don't override more-specific bindings with weaker ones. */
|
||||
const CBMType *existing = cbm_scope_lookup(ctx->current_scope, vname);
|
||||
if (existing && existing->kind == CBM_TYPE_NAMED && t->kind == CBM_TYPE_UNKNOWN) return;
|
||||
cbm_scope_bind(ctx->current_scope, vname, t);
|
||||
const char *callable_qn = cs_resolve_callable_value(ctx, rhs, NULL);
|
||||
(void)cs_eval_expr_type(ctx, rhs);
|
||||
if (callable_qn) {
|
||||
cbm_scope_update_callable(ctx->current_scope, vname, callable_qn);
|
||||
} else {
|
||||
/* An ordinary or ambiguous reassignment clears stale callable identity
|
||||
* while retaining the best type already known for the variable. */
|
||||
cbm_scope_update_callable(ctx->current_scope, vname, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── call resolution + emit ─────────────────────────────────────── */
|
||||
|
||||
static void cs_emit_resolved(CSLSPContext *ctx, const char *callee_qn, const char *strategy,
|
||||
float confidence) {
|
||||
static void cs_invalidate_conditionally_assigned_callables(CSLSPContext *ctx, TSNode node) {
|
||||
if (!ctx || ts_node_is_null(node))
|
||||
return;
|
||||
const char *kind = ts_node_type(node);
|
||||
if (strcmp(kind, "method_declaration") == 0 || strcmp(kind, "local_function_statement") == 0 ||
|
||||
strcmp(kind, "lambda_expression") == 0 ||
|
||||
strcmp(kind, "anonymous_method_expression") == 0 ||
|
||||
strcmp(kind, "class_declaration") == 0 || strcmp(kind, "struct_declaration") == 0) {
|
||||
return;
|
||||
}
|
||||
if (strcmp(kind, "assignment_expression") == 0) {
|
||||
TSNode lhs = ts_node_child_by_field_name(node, "left", 4);
|
||||
if (ts_node_is_null(lhs))
|
||||
lhs = cs_first_named_child(node);
|
||||
if (cs_node_is(lhs, "identifier")) {
|
||||
char *name = cs_node_text(ctx, lhs);
|
||||
if (name)
|
||||
cbm_scope_update_callable(ctx->current_scope, name, NULL);
|
||||
}
|
||||
return;
|
||||
}
|
||||
uint32_t nc = ts_node_child_count(node);
|
||||
for (uint32_t i = 0; i < nc; i++) {
|
||||
TSNode child = ts_node_child(node, i);
|
||||
if (!ts_node_is_null(child) && ts_node_is_named(child)) {
|
||||
cs_invalidate_conditionally_assigned_callables(ctx, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void cs_emit_resolved_kind_reason(CSLSPContext *ctx, const char *callee_qn,
|
||||
const char *strategy, float confidence,
|
||||
CBMResolvedKind kind, const char *reason) {
|
||||
if (!ctx->resolved_calls || !callee_qn || !ctx->enclosing_func_qn) return;
|
||||
CBMResolvedCall rc;
|
||||
CBMResolvedCall rc = {0};
|
||||
rc.caller_qn = ctx->enclosing_func_qn;
|
||||
rc.callee_qn = callee_qn;
|
||||
rc.strategy = strategy;
|
||||
rc.confidence = confidence;
|
||||
rc.reason = NULL;
|
||||
rc.reason = reason;
|
||||
rc.kind = kind;
|
||||
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
|
||||
}
|
||||
|
||||
static void cs_emit_resolved_kind(CSLSPContext *ctx, const char *callee_qn, const char *strategy,
|
||||
float confidence, CBMResolvedKind kind) {
|
||||
cs_emit_resolved_kind_reason(ctx, callee_qn, strategy, confidence, kind, NULL);
|
||||
}
|
||||
|
||||
static void cs_emit_resolved(CSLSPContext *ctx, const char *callee_qn, const char *strategy,
|
||||
float confidence) {
|
||||
cs_emit_resolved_kind(ctx, callee_qn, strategy, confidence, CBM_RESOLVED_INVOCATION);
|
||||
}
|
||||
|
||||
static void cs_emit_resolved_reason(CSLSPContext *ctx, const char *callee_qn, const char *strategy,
|
||||
float confidence, const char *reason) {
|
||||
cs_emit_resolved_kind_reason(ctx, callee_qn, strategy, confidence, CBM_RESOLVED_INVOCATION,
|
||||
reason);
|
||||
}
|
||||
|
||||
static void cs_stamp_resolved_site(CSLSPContext *ctx, int first, TSNode site) {
|
||||
if (!ctx->resolved_calls || ts_node_is_null(site) || first < 0)
|
||||
return;
|
||||
for (int i = first; i < ctx->resolved_calls->count; i++) {
|
||||
ctx->resolved_calls->items[i].site_start_byte = ts_node_start_byte(site);
|
||||
ctx->resolved_calls->items[i].site_end_byte = ts_node_end_byte(site);
|
||||
}
|
||||
}
|
||||
|
||||
static void cs_emit_resolved_kind_reason_at(CSLSPContext *ctx, const char *callee_qn,
|
||||
const char *strategy, float confidence,
|
||||
CBMResolvedKind kind, const char *reason, TSNode site) {
|
||||
int first = ctx->resolved_calls ? ctx->resolved_calls->count : -1;
|
||||
cs_emit_resolved_kind_reason(ctx, callee_qn, strategy, confidence, kind, reason);
|
||||
cs_stamp_resolved_site(ctx, first, site);
|
||||
}
|
||||
|
||||
static void cs_retarget_callable_carrier(CSLSPContext *ctx, TSNode site, const char *source_name,
|
||||
const char *target_qn) {
|
||||
if (!ctx || !ctx->call_carriers || ts_node_is_null(site) || !source_name || !target_qn ||
|
||||
!ctx->enclosing_func_qn) {
|
||||
return;
|
||||
}
|
||||
const char *target_name = cs_short_name(target_qn);
|
||||
uint32_t start = ts_node_start_byte(site);
|
||||
uint32_t end = ts_node_end_byte(site);
|
||||
for (int i = 0; i < ctx->call_carriers->count; i++) {
|
||||
CBMCall *call = &ctx->call_carriers->items[i];
|
||||
if (!call->callee_name || !call->enclosing_func_qn ||
|
||||
strcmp(call->callee_name, source_name) != 0 ||
|
||||
strcmp(call->enclosing_func_qn, ctx->enclosing_func_qn) != 0 ||
|
||||
call->site_start_byte < start || call->site_end_byte > end) {
|
||||
continue;
|
||||
}
|
||||
call->callee_name = cbm_arena_strdup(ctx->arena, target_name);
|
||||
call->requires_lsp_resolution = true;
|
||||
}
|
||||
}
|
||||
|
||||
/* Promote only direct argument expressions whose value resolves to one exact
|
||||
* method group. Wrappers are peeled, but conditionals/switches/lambdas are not
|
||||
* traversed, so complex values retain parser-produced USAGE edges. */
|
||||
static void cs_resolve_callable_arguments(CSLSPContext *ctx, TSNode call) {
|
||||
TSNode args = ts_node_child_by_field_name(call, "arguments", 9);
|
||||
if (ts_node_is_null(args))
|
||||
args = cs_child_named_kind(call, "argument_list");
|
||||
if (ts_node_is_null(args))
|
||||
return;
|
||||
|
||||
uint32_t nc = ts_node_child_count(args);
|
||||
for (uint32_t i = 0; i < nc; i++) {
|
||||
TSNode argument = ts_node_child(args, i);
|
||||
if (ts_node_is_null(argument) || !ts_node_is_named(argument) ||
|
||||
strcmp(ts_node_type(argument), "argument") != 0) {
|
||||
continue;
|
||||
}
|
||||
TSNode value = ts_node_child_by_field_name(argument, "expression", 10);
|
||||
if (ts_node_is_null(value))
|
||||
value = ts_node_child_by_field_name(argument, "value", 5);
|
||||
if (ts_node_is_null(value)) {
|
||||
uint32_t ac = ts_node_child_count(argument);
|
||||
for (uint32_t j = ac; j > 0; j--) {
|
||||
TSNode child = ts_node_child(argument, j - 1);
|
||||
if (!ts_node_is_null(child) && ts_node_is_named(child)) {
|
||||
value = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ts_node_is_null(value))
|
||||
continue;
|
||||
TSNode callable_leaf;
|
||||
memset(&callable_leaf, 0, sizeof(callable_leaf));
|
||||
const char *target = cs_resolve_callable_value(ctx, value, &callable_leaf);
|
||||
if (!target)
|
||||
continue;
|
||||
if (ts_node_is_null(callable_leaf))
|
||||
continue;
|
||||
char *source_name = cs_node_text(ctx, callable_leaf);
|
||||
cs_emit_resolved_kind_reason_at(ctx, target, "lsp_callable_value_reference", 0.95f,
|
||||
CBM_RESOLVED_CALL_REFERENCE, source_name, callable_leaf);
|
||||
}
|
||||
}
|
||||
|
||||
static void cs_resolve_invocation(CSLSPContext *ctx, TSNode call) {
|
||||
TSNode fn = ts_node_child_by_field_name(call, "function", 8);
|
||||
if (ts_node_is_null(fn)) {
|
||||
@@ -1516,6 +1856,18 @@ static void cs_resolve_invocation(CSLSPContext *ctx, TSNode call) {
|
||||
if (!fname) return;
|
||||
char *bare = cs_strip_generic_args(ctx->arena, fname);
|
||||
|
||||
/* Callable-valued locals and parameters shadow class/static methods.
|
||||
* An exact alias carries its real target; an ordinary lexical value
|
||||
* suppresses all same-name fallbacks so we never fabricate a call. */
|
||||
if (cbm_scope_contains(ctx->current_scope, bare)) {
|
||||
const char *target = cbm_scope_lookup_callable(ctx->current_scope, bare);
|
||||
if (target) {
|
||||
cs_emit_resolved_reason(ctx, target, "lsp_callable_alias", 0.98f, bare);
|
||||
cs_retarget_callable_carrier(ctx, call, bare, target);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* Try enclosing class member. cs_lookup_method walks the base chain, so
|
||||
* a bare call may resolve to an INHERITED method. Distinguish, exactly
|
||||
* as the instance-call path does: a method actually declared on the
|
||||
@@ -1547,15 +1899,14 @@ static void cs_resolve_invocation(CSLSPContext *ctx, TSNode call) {
|
||||
* the file-stem QN ("proj.Client.MathUtil"); resolve the target through
|
||||
* the type-name resolver (its short-name fallback bridges the two)
|
||||
* before the method lookup. */
|
||||
for (int i = 0; i < ctx->using_count; i++) {
|
||||
const CBMCSUsing *u = &ctx->usings[i];
|
||||
if (u->kind != CBM_CS_USING_STATIC) continue;
|
||||
const char *host = cs_resolve_type_name(ctx, u->target_qn);
|
||||
const CBMRegisteredFunc *f = cs_lookup_method(ctx, host ? host : u->target_qn, bare);
|
||||
if (f) {
|
||||
cs_emit_resolved(ctx, f->qualified_name, "cs_using_static", 0.90f);
|
||||
return;
|
||||
}
|
||||
bool using_static_ambiguous = false;
|
||||
const CBMRegisteredFunc *using_static =
|
||||
cs_lookup_using_static_method(ctx, bare, &using_static_ambiguous);
|
||||
if (using_static_ambiguous)
|
||||
return;
|
||||
if (using_static) {
|
||||
cs_emit_resolved(ctx, using_static->qualified_name, "cs_using_static", 0.90f);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Free function in current namespace. */
|
||||
@@ -1656,9 +2007,14 @@ static void cs_resolve_calls_in_node(CSLSPContext *ctx, TSNode node) {
|
||||
} else if (strcmp(kind, "assignment_expression") == 0) {
|
||||
cs_process_assignment(ctx, node);
|
||||
} else if (strcmp(kind, "invocation_expression") == 0) {
|
||||
int first = ctx->resolved_calls ? ctx->resolved_calls->count : -1;
|
||||
cs_resolve_invocation(ctx, node);
|
||||
cs_stamp_resolved_site(ctx, first, node);
|
||||
cs_resolve_callable_arguments(ctx, node);
|
||||
} else if (strcmp(kind, "object_creation_expression") == 0) {
|
||||
int first = ctx->resolved_calls ? ctx->resolved_calls->count : -1;
|
||||
cs_resolve_object_creation(ctx, node);
|
||||
cs_stamp_resolved_site(ctx, first, node);
|
||||
}
|
||||
|
||||
/* Recurse into children. We do NOT pre-bind anything that would only be
|
||||
@@ -1692,6 +2048,13 @@ static void cs_resolve_calls_in_node(CSLSPContext *ctx, TSNode node) {
|
||||
cs_resolve_calls_in_node(ctx, c);
|
||||
} while (ts_tree_cursor_goto_next_sibling(&cursor));
|
||||
ts_tree_cursor_delete(&cursor);
|
||||
if (strcmp(kind, "if_statement") == 0 || strcmp(kind, "switch_statement") == 0 ||
|
||||
strcmp(kind, "switch_expression") == 0 || strcmp(kind, "for_statement") == 0 ||
|
||||
strcmp(kind, "for_each_statement") == 0 || strcmp(kind, "foreach_statement") == 0 ||
|
||||
strcmp(kind, "while_statement") == 0 || strcmp(kind, "do_statement") == 0 ||
|
||||
strcmp(kind, "try_statement") == 0) {
|
||||
cs_invalidate_conditionally_assigned_callables(ctx, node);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── method/constructor processing ──────────────────────────────── */
|
||||
@@ -2690,6 +3053,34 @@ static void cs_collect_method_return_types(CSLSPContext *ctx, TSNode root,
|
||||
}
|
||||
}
|
||||
|
||||
void cbm_cs_refine_ast_return_types(CSLSPContext *ctx, CBMTypeRegistry *reg, TSNode root) {
|
||||
if (!ctx || !reg || ts_node_is_null(root))
|
||||
return;
|
||||
|
||||
cs_method_rt_table_t mtab = {0};
|
||||
cs_collect_method_return_types(ctx, root, &mtab);
|
||||
for (int i = 0; i < mtab.count; i++) {
|
||||
const char *qn = mtab.items[i].qn;
|
||||
const CBMType *rt = mtab.items[i].rt;
|
||||
bool patched = false;
|
||||
for (int j = 0; j < reg->func_count; j++) {
|
||||
if (strcmp(reg->funcs[j].qualified_name, qn) == 0) {
|
||||
const CBMType **rets =
|
||||
(const CBMType **)cbm_arena_alloc(ctx->arena, 2 * sizeof(const CBMType *));
|
||||
if (rets) {
|
||||
rets[0] = rt;
|
||||
rets[1] = NULL;
|
||||
reg->funcs[j].signature =
|
||||
cbm_type_func_replace_returns(ctx->arena, reg->funcs[j].signature, rets);
|
||||
}
|
||||
patched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
(void)patched;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── parse return type from CBMDefinition.return_type ──────────── */
|
||||
|
||||
static const CBMType *cs_parse_return_type_text(CSLSPContext *ctx, const char *text) {
|
||||
@@ -2709,6 +3100,13 @@ static const CBMType *cs_parse_return_type_text(CSLSPContext *ctx, const char *t
|
||||
return cbm_type_named(ctx->arena, resolved);
|
||||
}
|
||||
|
||||
static const CBMType *cs_signature_param_type_adapter(CBMArena *arena, const char *text,
|
||||
void *parser_ctx) {
|
||||
(void)arena;
|
||||
CSLSPContext *ctx = (CSLSPContext *)parser_ctx;
|
||||
return ctx ? cs_parse_return_type_text(ctx, text) : cbm_type_unknown();
|
||||
}
|
||||
|
||||
/* ── single-file entry: cbm_run_cs_lsp ──────────────────────────── */
|
||||
|
||||
void cbm_run_cs_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len,
|
||||
@@ -2785,10 +3183,37 @@ void cbm_run_cs_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
|
||||
/* Phase C: build context for type resolution + run the resolver. */
|
||||
CSLSPContext ctx;
|
||||
cs_lsp_init(&ctx, arena, source, source_len, ®, module_qn, &result->resolved_calls);
|
||||
ctx.call_carriers = &result->calls;
|
||||
|
||||
/* Pass 1: collect imports + namespaces (for type-resolution context). */
|
||||
cs_collect_imports(&ctx, root);
|
||||
|
||||
/* The counted carrier is authoritative even when count is zero. Install it
|
||||
* only after imports are available so textual types use the same local
|
||||
* resolution context as declared returns. A truly absent carrier keeps the
|
||||
* legacy zero-parameter behavior. */
|
||||
for (int i = 0; i < result->defs.count; i++) {
|
||||
CBMDefinition *d = &result->defs.items[i];
|
||||
if (!d->qualified_name || !d->label ||
|
||||
(strcmp(d->label, "Function") != 0 && strcmp(d->label, "Method") != 0) ||
|
||||
(!d->signature_param_types && d->signature_param_count <= 0)) {
|
||||
continue;
|
||||
}
|
||||
const CBMType **param_types = cbm_type_materialize_signature_params(
|
||||
arena, d->signature_param_types, d->signature_param_count,
|
||||
cs_signature_param_type_adapter, &ctx);
|
||||
for (int j = 0; j < reg.func_count; j++) {
|
||||
if (strcmp(reg.funcs[j].qualified_name, d->qualified_name) != 0)
|
||||
continue;
|
||||
const CBMType **return_types = NULL;
|
||||
if (reg.funcs[j].signature && reg.funcs[j].signature->kind == CBM_TYPE_FUNC) {
|
||||
return_types = reg.funcs[j].signature->data.func.return_types;
|
||||
}
|
||||
reg.funcs[j].signature = cbm_type_func(arena, NULL, param_types, return_types);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Now refine return types of registered funcs using ctx (which has using
|
||||
* directives). Also refine field types via collect_class_fields. */
|
||||
for (int i = 0; i < result->defs.count; i++) {
|
||||
@@ -2804,7 +3229,8 @@ void cbm_run_cs_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
|
||||
if (rets) {
|
||||
rets[0] = rt;
|
||||
rets[1] = NULL;
|
||||
reg.funcs[j].signature = cbm_type_func(arena, NULL, NULL, rets);
|
||||
reg.funcs[j].signature =
|
||||
cbm_type_func_replace_returns(arena, reg.funcs[j].signature, rets);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -2814,29 +3240,7 @@ void cbm_run_cs_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
|
||||
/* Phase B.1.5: AST-driven return-type patch. The unified extractor
|
||||
* leaves CBMDefinition.return_type NULL for C# methods, so we walk the
|
||||
* AST ourselves to recover declared return types. */
|
||||
{
|
||||
cs_method_rt_table_t mtab = {0};
|
||||
cs_collect_method_return_types(&ctx, root, &mtab);
|
||||
for (int i = 0; i < mtab.count; i++) {
|
||||
const char *qn = mtab.items[i].qn;
|
||||
const CBMType *rt = mtab.items[i].rt;
|
||||
bool patched = false;
|
||||
for (int j = 0; j < reg.func_count; j++) {
|
||||
if (strcmp(reg.funcs[j].qualified_name, qn) == 0) {
|
||||
const CBMType **rets =
|
||||
(const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(const CBMType *));
|
||||
if (rets) {
|
||||
rets[0] = rt;
|
||||
rets[1] = NULL;
|
||||
reg.funcs[j].signature = cbm_type_func(arena, NULL, NULL, rets);
|
||||
}
|
||||
patched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
(void)patched;
|
||||
}
|
||||
}
|
||||
cbm_cs_refine_ast_return_types(&ctx, ®, root);
|
||||
|
||||
/* Phase B.1: collect typed properties + fields. */
|
||||
{
|
||||
@@ -2928,13 +3332,22 @@ static void cs_register_lsp_defs(CBMArena *arena, CBMTypeRegistry *reg,
|
||||
if (strcmp(d->label, "Method") == 0 && d->receiver_type) {
|
||||
rf.receiver_type = d->receiver_type;
|
||||
}
|
||||
CSLSPContext parser_ctx = {0};
|
||||
parser_ctx.arena = arena;
|
||||
parser_ctx.registry = reg;
|
||||
parser_ctx.module_qn = d->def_module_qn;
|
||||
const CBMType **rets =
|
||||
(const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(const CBMType *));
|
||||
if (rets) {
|
||||
rets[0] = cbm_type_unknown();
|
||||
rets[0] = d->return_types && d->return_types[0]
|
||||
? cs_parse_return_type_text(&parser_ctx, d->return_types)
|
||||
: cbm_type_unknown();
|
||||
rets[1] = NULL;
|
||||
}
|
||||
rf.signature = cbm_type_func(arena, NULL, NULL, rets);
|
||||
const CBMType **param_types = cbm_type_materialize_signature_params(
|
||||
arena, d->signature_param_types, d->signature_param_count,
|
||||
cs_signature_param_type_adapter, &parser_ctx);
|
||||
rf.signature = cbm_type_func(arena, NULL, param_types, rets);
|
||||
cbm_registry_add_func(reg, rf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,10 @@ typedef struct {
|
||||
|
||||
/* Output: resolved calls accumulate here. */
|
||||
CBMResolvedCallArray *resolved_calls;
|
||||
/* Existing parser call carriers for this file. Exact callable aliases
|
||||
* retarget their occurrence and require the semantic result, preventing a
|
||||
* same-named definition from winning through textual fallback. */
|
||||
CBMCallArray *call_carriers;
|
||||
|
||||
/* Active type-parameter substitution map (for generic methods/types).
|
||||
* Parallel arrays. NULL-terminated. */
|
||||
@@ -138,6 +142,11 @@ const char *cs_resolve_type_name(CSLSPContext *ctx, const char *name);
|
||||
const CBMRegisteredFunc *cs_lookup_method(CSLSPContext *ctx, const char *type_qn,
|
||||
const char *method_name);
|
||||
|
||||
/* Apply the local AST-declared return-type refinement to an already populated
|
||||
* registry. Kept as a production-used seam so post-registration signature
|
||||
* preservation can be regression-tested directly. */
|
||||
void cbm_cs_refine_ast_return_types(CSLSPContext *ctx, CBMTypeRegistry *reg, TSNode root);
|
||||
|
||||
/* Single-file entry: build registry from file defs + stdlib, run resolution. */
|
||||
void cbm_run_cs_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len,
|
||||
TSNode root);
|
||||
|
||||
+493
-92
@@ -22,7 +22,9 @@ static void resolve_calls_in_node(GoLSPContext* ctx, TSNode node) {
|
||||
resolve_calls_in_node_inner(ctx, node);
|
||||
ctx->walk_depth--;
|
||||
}
|
||||
static void emit_resolved_call(GoLSPContext* ctx, const char* callee_qn, const char* strategy, float confidence);
|
||||
static void emit_resolved_call(GoLSPContext *ctx, const char *callee_qn, const char *strategy,
|
||||
float confidence, TSNode site);
|
||||
static const char *go_exact_callable_target(GoLSPContext *ctx, TSNode node);
|
||||
static const CBMType* go_lookup_field(GoLSPContext* ctx, const char* type_qn, const char* field_name, int depth);
|
||||
static void extract_type_params_from_ast(CBMArena* arena, CBMTypeRegistry* reg,
|
||||
TSNode root, const char* source, const char* module_qn);
|
||||
@@ -336,7 +338,8 @@ const CBMType* go_eval_expr_type(GoLSPContext* ctx, TSNode node) {
|
||||
|
||||
// Check scope first
|
||||
const CBMType* t = cbm_scope_lookup(ctx->current_scope, name);
|
||||
if (!cbm_type_is_unknown(t)) return t;
|
||||
if (cbm_scope_contains(ctx->current_scope, name))
|
||||
return t ? t : cbm_type_unknown();
|
||||
|
||||
// Check if it's a package-level function
|
||||
const CBMRegisteredFunc* f = cbm_registry_lookup_symbol(ctx->registry, ctx->package_qn, name);
|
||||
@@ -929,6 +932,76 @@ const CBMRegisteredFunc* go_lookup_field_or_method(GoLSPContext* ctx,
|
||||
return go_lookup_field_or_method_depth(ctx, type_qn, member_name, 0);
|
||||
}
|
||||
|
||||
/* Resolve only source forms that prove one callable identity. This is
|
||||
* intentionally narrower than go_eval_expr_type: conditional expressions,
|
||||
* indexed dispatch tables, interfaces with several implementers, and unknown
|
||||
* locals remain ordinary values. */
|
||||
static const char *go_exact_callable_target(GoLSPContext *ctx, TSNode node) {
|
||||
if (!ctx || ts_node_is_null(node))
|
||||
return NULL;
|
||||
const char *kind = ts_node_type(node);
|
||||
|
||||
if (strcmp(kind, "parenthesized_expression") == 0 && ts_node_named_child_count(node) == 1) {
|
||||
return go_exact_callable_target(ctx, ts_node_named_child(node, 0));
|
||||
}
|
||||
|
||||
if (strcmp(kind, "identifier") == 0) {
|
||||
char *name = lsp_node_text(ctx, node);
|
||||
if (!name)
|
||||
return NULL;
|
||||
/* A local value shadows the package declaration even when its type is
|
||||
* UNKNOWN. Only an explicitly tracked callable alias may pass. */
|
||||
if (cbm_scope_contains(ctx->current_scope, name)) {
|
||||
return cbm_scope_lookup_callable(ctx->current_scope, name);
|
||||
}
|
||||
const CBMRegisteredFunc *f =
|
||||
cbm_registry_lookup_symbol(ctx->registry, ctx->package_qn, name);
|
||||
return f ? f->qualified_name : NULL;
|
||||
}
|
||||
|
||||
if (strcmp(kind, "selector_expression") == 0) {
|
||||
TSNode operand = ts_node_child_by_field_name(node, "operand", 7);
|
||||
TSNode field = ts_node_child_by_field_name(node, "field", 5);
|
||||
if (ts_node_is_null(operand) || ts_node_is_null(field))
|
||||
return NULL;
|
||||
char *member = lsp_node_text(ctx, field);
|
||||
if (!member)
|
||||
return NULL;
|
||||
|
||||
if (strcmp(ts_node_type(operand), "identifier") == 0) {
|
||||
char *local = lsp_node_text(ctx, operand);
|
||||
const char *package_qn = local ? resolve_import(ctx, local) : NULL;
|
||||
if (package_qn) {
|
||||
const CBMRegisteredFunc *f =
|
||||
cbm_registry_lookup_symbol(ctx->registry, package_qn, member);
|
||||
return f ? f->qualified_name : NULL;
|
||||
}
|
||||
}
|
||||
|
||||
const CBMType *recv = go_eval_expr_type(ctx, operand);
|
||||
if (recv && recv->kind == CBM_TYPE_POINTER)
|
||||
recv = cbm_type_deref(recv);
|
||||
if (recv && recv->kind == CBM_TYPE_NAMED) {
|
||||
/* A direct method name is unique in a valid Go method set.
|
||||
* Promoted lookup through embedded types can be ambiguous; without
|
||||
* full selector-depth resolution, fail closed and retain USAGE. */
|
||||
const CBMRegisteredFunc *method =
|
||||
cbm_registry_lookup_method(ctx->registry, recv->data.named.qualified_name, member);
|
||||
return method ? method->qualified_name : NULL;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void go_scope_bind_value(GoLSPContext *ctx, const char *name, const CBMType *type,
|
||||
const char *callable_qn) {
|
||||
if (callable_qn) {
|
||||
cbm_scope_bind_callable(ctx->current_scope, name, type, callable_qn);
|
||||
} else {
|
||||
cbm_scope_bind(ctx->current_scope, name, type);
|
||||
}
|
||||
}
|
||||
|
||||
// --- go_process_statement: bind variables from statements ---
|
||||
|
||||
void go_process_statement(GoLSPContext* ctx, TSNode node) {
|
||||
@@ -942,17 +1015,22 @@ void go_process_statement(GoLSPContext* ctx, TSNode node) {
|
||||
if (ts_node_is_null(left) || ts_node_is_null(right)) return;
|
||||
|
||||
const CBMType* rhs_type = NULL;
|
||||
TSNode single_rhs = {0};
|
||||
|
||||
// Check if RHS is an expression_list (multiple values)
|
||||
if (strcmp(ts_node_type(right), "expression_list") == 0) {
|
||||
uint32_t rhs_count = ts_node_named_child_count(right);
|
||||
if (rhs_count == 1) {
|
||||
// Single expression that might return a tuple (multi-return)
|
||||
rhs_type = go_eval_expr_type(ctx, ts_node_named_child(right, 0));
|
||||
single_rhs = ts_node_named_child(right, 0);
|
||||
rhs_type = go_eval_expr_type(ctx, single_rhs);
|
||||
}
|
||||
} else {
|
||||
single_rhs = right;
|
||||
rhs_type = go_eval_expr_type(ctx, right);
|
||||
}
|
||||
const char *rhs_callable =
|
||||
ts_node_is_null(single_rhs) ? NULL : go_exact_callable_target(ctx, single_rhs);
|
||||
|
||||
// Bind left-hand side variables
|
||||
if (strcmp(ts_node_type(left), "expression_list") == 0) {
|
||||
@@ -971,23 +1049,67 @@ void go_process_statement(GoLSPContext* ctx, TSNode node) {
|
||||
var_type = rhs_type;
|
||||
}
|
||||
}
|
||||
cbm_scope_bind(ctx->current_scope, var_name, var_type);
|
||||
go_scope_bind_value(ctx, var_name, var_type, i == 0 ? rhs_callable : NULL);
|
||||
}
|
||||
} else if (strcmp(ts_node_type(left), "identifier") == 0) {
|
||||
char* var_name = lsp_node_text(ctx, left);
|
||||
if (var_name && strcmp(var_name, "_") != 0 && rhs_type) {
|
||||
cbm_scope_bind(ctx->current_scope, var_name, rhs_type);
|
||||
go_scope_bind_value(ctx, var_name, rhs_type, rhs_callable);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* Reassignment must update (or clear) callable identity just like :=.
|
||||
* Without this, `alias := target; alias = dynamic; alias()` would retain a
|
||||
* stale exact target and fabricate a call. */
|
||||
if (strcmp(kind, "assignment_statement") == 0) {
|
||||
TSNode left = ts_node_child_by_field_name(node, "left", 4);
|
||||
TSNode right = ts_node_child_by_field_name(node, "right", 5);
|
||||
if (ts_node_is_null(left) || ts_node_is_null(right))
|
||||
return;
|
||||
uint32_t lhs_count = strcmp(ts_node_type(left), "expression_list") == 0
|
||||
? ts_node_named_child_count(left)
|
||||
: 1;
|
||||
uint32_t rhs_count = strcmp(ts_node_type(right), "expression_list") == 0
|
||||
? ts_node_named_child_count(right)
|
||||
: 1;
|
||||
for (uint32_t i = 0; i < lhs_count; i++) {
|
||||
TSNode lhs = lhs_count == 1 && strcmp(ts_node_type(left), "expression_list") != 0
|
||||
? left
|
||||
: ts_node_named_child(left, i);
|
||||
if (ts_node_is_null(lhs) || strcmp(ts_node_type(lhs), "identifier") != 0)
|
||||
continue;
|
||||
char *name = lsp_node_text(ctx, lhs);
|
||||
if (!name || strcmp(name, "_") == 0)
|
||||
continue;
|
||||
TSNode rhs = {0};
|
||||
if (rhs_count == lhs_count) {
|
||||
rhs = rhs_count == 1 && strcmp(ts_node_type(right), "expression_list") != 0
|
||||
? right
|
||||
: ts_node_named_child(right, i);
|
||||
}
|
||||
const CBMType *type =
|
||||
ts_node_is_null(rhs) ? cbm_type_unknown() : go_eval_expr_type(ctx, rhs);
|
||||
const char *callable = ts_node_is_null(rhs) ? NULL : go_exact_callable_target(ctx, rhs);
|
||||
go_scope_bind_value(ctx, name, type, callable);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// var_spec: var x Type = expr OR var a, b, c Type = expr
|
||||
if (strcmp(kind, "var_spec") == 0) {
|
||||
TSNode type_node = ts_node_child_by_field_name(node, "type", 4);
|
||||
TSNode value_node = ts_node_child_by_field_name(node, "value", 5);
|
||||
|
||||
const CBMType* var_type = cbm_type_unknown();
|
||||
TSNode callable_value = value_node;
|
||||
if (!ts_node_is_null(value_node) &&
|
||||
strcmp(ts_node_type(value_node), "expression_list") == 0) {
|
||||
callable_value = ts_node_named_child_count(value_node) == 1
|
||||
? ts_node_named_child(value_node, 0)
|
||||
: (TSNode){0};
|
||||
}
|
||||
if (!ts_node_is_null(type_node)) {
|
||||
var_type = go_parse_type_node(ctx, type_node);
|
||||
} else if (!ts_node_is_null(value_node)) {
|
||||
@@ -998,6 +1120,8 @@ void go_process_statement(GoLSPContext* ctx, TSNode node) {
|
||||
var_type = go_eval_expr_type(ctx, value_node);
|
||||
}
|
||||
}
|
||||
const char *callable_qn =
|
||||
ts_node_is_null(callable_value) ? NULL : go_exact_callable_target(ctx, callable_value);
|
||||
|
||||
// Bind all name identifiers (handles: var a, b, c int)
|
||||
uint32_t vnc = ts_node_child_count(node);
|
||||
@@ -1007,7 +1131,7 @@ void go_process_statement(GoLSPContext* ctx, TSNode node) {
|
||||
if (strcmp(ts_node_type(ch), "identifier") == 0) {
|
||||
char* var_name = lsp_node_text(ctx, ch);
|
||||
if (var_name && strcmp(var_name, "_") != 0) {
|
||||
cbm_scope_bind(ctx->current_scope, var_name, var_type);
|
||||
go_scope_bind_value(ctx, var_name, var_type, callable_qn);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1022,6 +1146,13 @@ void go_process_statement(GoLSPContext* ctx, TSNode node) {
|
||||
if (ts_node_is_null(name_node)) return;
|
||||
|
||||
const CBMType* const_type = cbm_type_unknown();
|
||||
TSNode callable_value = value_node;
|
||||
if (!ts_node_is_null(value_node) &&
|
||||
strcmp(ts_node_type(value_node), "expression_list") == 0) {
|
||||
callable_value = ts_node_named_child_count(value_node) == 1
|
||||
? ts_node_named_child(value_node, 0)
|
||||
: (TSNode){0};
|
||||
}
|
||||
if (!ts_node_is_null(type_node)) {
|
||||
const_type = go_parse_type_node(ctx, type_node);
|
||||
} else if (!ts_node_is_null(value_node)) {
|
||||
@@ -1033,11 +1164,13 @@ void go_process_statement(GoLSPContext* ctx, TSNode node) {
|
||||
const_type = go_eval_expr_type(ctx, value_node);
|
||||
}
|
||||
}
|
||||
const char *callable_qn =
|
||||
ts_node_is_null(callable_value) ? NULL : go_exact_callable_target(ctx, callable_value);
|
||||
|
||||
if (strcmp(ts_node_type(name_node), "identifier") == 0) {
|
||||
char* name = lsp_node_text(ctx, name_node);
|
||||
if (name && strcmp(name, "_") != 0)
|
||||
cbm_scope_bind(ctx->current_scope, name, const_type);
|
||||
go_scope_bind_value(ctx, name, const_type, callable_qn);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1094,33 +1227,151 @@ void go_process_statement(GoLSPContext* ctx, TSNode node) {
|
||||
// (needs per-case scope narrowing, not just variable binding)
|
||||
}
|
||||
|
||||
// --- Emit a resolved call ---
|
||||
// --- Emit a resolved call/reference ---
|
||||
|
||||
static void emit_resolved_call(GoLSPContext* ctx, const char* callee_qn, const char* strategy, float confidence) {
|
||||
static void go_emit_resolved_kind(GoLSPContext *ctx, const char *callee_qn, const char *strategy,
|
||||
float confidence, const char *source_name, CBMResolvedKind kind,
|
||||
TSNode site) {
|
||||
if (!ctx->resolved_calls || !callee_qn || !ctx->enclosing_func_qn) return;
|
||||
|
||||
CBMResolvedCall rc;
|
||||
CBMResolvedCall rc = {0};
|
||||
rc.caller_qn = ctx->enclosing_func_qn;
|
||||
rc.callee_qn = callee_qn;
|
||||
rc.strategy = strategy;
|
||||
rc.confidence = confidence;
|
||||
rc.reason = NULL;
|
||||
/* When a callable alias has a different textual leaf, retain that source
|
||||
* name for the exact-site join. */
|
||||
rc.reason = source_name;
|
||||
rc.kind = kind;
|
||||
if (!ts_node_is_null(site)) {
|
||||
rc.site_start_byte = ts_node_start_byte(site);
|
||||
rc.site_end_byte = ts_node_end_byte(site);
|
||||
}
|
||||
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
|
||||
}
|
||||
|
||||
static void emit_resolved_call(GoLSPContext *ctx, const char *callee_qn, const char *strategy,
|
||||
float confidence, TSNode site) {
|
||||
go_emit_resolved_kind(ctx, callee_qn, strategy, confidence, NULL, CBM_RESOLVED_INVOCATION,
|
||||
site);
|
||||
}
|
||||
|
||||
static void emit_resolved_alias_call(GoLSPContext *ctx, const char *callee_qn,
|
||||
const char *source_name, TSNode site) {
|
||||
go_emit_resolved_kind(ctx, callee_qn, "lsp_callable_alias", 0.97f, source_name,
|
||||
CBM_RESOLVED_INVOCATION, site);
|
||||
}
|
||||
|
||||
static void emit_resolved_reference(GoLSPContext *ctx, const char *callee_qn,
|
||||
const char *source_name, TSNode site) {
|
||||
const char *leaf = strrchr(callee_qn, '.');
|
||||
leaf = leaf ? leaf + 1 : callee_qn;
|
||||
const char *join_name = source_name && strcmp(source_name, leaf) != 0 ? source_name : NULL;
|
||||
go_emit_resolved_kind(ctx, callee_qn, "lsp_callable_value_reference", 0.97f, join_name,
|
||||
CBM_RESOLVED_CALL_REFERENCE, site);
|
||||
}
|
||||
|
||||
static void emit_unresolved_reference(GoLSPContext *ctx, const char *source_name, TSNode site) {
|
||||
if (!ctx || !ctx->resolved_calls || !ctx->enclosing_func_qn || !source_name ||
|
||||
ts_node_is_null(site)) {
|
||||
return;
|
||||
}
|
||||
CBMResolvedCall rc = {0};
|
||||
rc.caller_qn = ctx->enclosing_func_qn;
|
||||
rc.callee_qn = source_name;
|
||||
rc.strategy = "lsp_unresolved";
|
||||
rc.confidence = 0.0f;
|
||||
rc.reason = "callable_value_not_in_registry";
|
||||
rc.kind = CBM_RESOLVED_CALL_REFERENCE;
|
||||
rc.site_start_byte = ts_node_start_byte(site);
|
||||
rc.site_end_byte = ts_node_end_byte(site);
|
||||
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
|
||||
}
|
||||
|
||||
// Emit a diagnostic for an unresolved call (confidence 0.0).
|
||||
static void emit_unresolved_call(GoLSPContext* ctx, const char* expr_text, const char* reason) {
|
||||
static void emit_unresolved_call(GoLSPContext *ctx, const char *expr_text, const char *reason,
|
||||
TSNode site) {
|
||||
if (!ctx->resolved_calls || !ctx->enclosing_func_qn) return;
|
||||
|
||||
CBMResolvedCall rc;
|
||||
CBMResolvedCall rc = {0};
|
||||
rc.caller_qn = ctx->enclosing_func_qn;
|
||||
rc.callee_qn = expr_text ? expr_text : "?";
|
||||
rc.strategy = "lsp_unresolved";
|
||||
rc.confidence = 0.0f;
|
||||
rc.reason = reason;
|
||||
rc.kind = CBM_RESOLVED_INVOCATION;
|
||||
if (!ts_node_is_null(site)) {
|
||||
rc.site_start_byte = ts_node_start_byte(site);
|
||||
rc.site_end_byte = ts_node_end_byte(site);
|
||||
}
|
||||
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
|
||||
}
|
||||
|
||||
/* Only direct argument expressions are candidates. Descending into a
|
||||
* conditional/map/index expression would promote its constituent names even
|
||||
* though the runtime callable is not statically unique. */
|
||||
static void go_resolve_value_references_at(GoLSPContext *ctx, TSNode call) {
|
||||
TSNode args = ts_node_child_by_field_name(call, "arguments", 9);
|
||||
if (ts_node_is_null(args))
|
||||
return;
|
||||
uint32_t count = ts_node_named_child_count(args);
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
TSNode arg = ts_node_named_child(args, i);
|
||||
const char *kind = ts_node_type(arg);
|
||||
if (strcmp(kind, "identifier") != 0 && strcmp(kind, "selector_expression") != 0 &&
|
||||
strcmp(kind, "parenthesized_expression") != 0)
|
||||
continue;
|
||||
const char *target = go_exact_callable_target(ctx, arg);
|
||||
char *source_name = lsp_node_text(ctx, arg);
|
||||
if (target) {
|
||||
emit_resolved_reference(ctx, target, source_name, arg);
|
||||
} else if (strcmp(kind, "identifier") == 0 && source_name &&
|
||||
!cbm_scope_contains(ctx->current_scope, source_name)) {
|
||||
/* The per-file registry cannot see an unqualified function from
|
||||
* another file in the same Go package. Preserve the exact source
|
||||
* occurrence for the metadata-only project pass. */
|
||||
emit_unresolved_reference(ctx, source_name, arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Any assignment that is guarded by a branch/loop makes the post-construct
|
||||
* callable identity a join of at least two paths. Clear the outer exact
|
||||
* identity before walking the construct; assignments inside its temporary
|
||||
* scope may still be exact locally, but cannot leak past the join. */
|
||||
static void go_invalidate_control_flow_aliases(GoLSPContext *ctx, TSNode node, int depth) {
|
||||
if (!ctx || ts_node_is_null(node) || depth > 64)
|
||||
return;
|
||||
const char *kind = ts_node_type(node);
|
||||
if (strcmp(kind, "func_literal") == 0)
|
||||
return;
|
||||
if (strcmp(kind, "assignment_statement") == 0) {
|
||||
TSNode left = ts_node_child_by_field_name(node, "left", 4);
|
||||
if (!ts_node_is_null(left)) {
|
||||
uint32_t count = strcmp(ts_node_type(left), "expression_list") == 0
|
||||
? ts_node_named_child_count(left)
|
||||
: 1;
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
TSNode target = count == 1 && strcmp(ts_node_type(left), "expression_list") != 0
|
||||
? left
|
||||
: ts_node_named_child(left, i);
|
||||
if (ts_node_is_null(target) || strcmp(ts_node_type(target), "identifier") != 0) {
|
||||
continue;
|
||||
}
|
||||
char *name = lsp_node_text(ctx, target);
|
||||
if (name && cbm_scope_lookup_callable(ctx->current_scope, name)) {
|
||||
cbm_scope_update_callable(ctx->current_scope, name, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
uint32_t count = ts_node_named_child_count(node);
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
go_invalidate_control_flow_aliases(ctx, ts_node_named_child(node, i), depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Walk call expressions and resolve them ---
|
||||
|
||||
static void resolve_calls_in_node_inner(GoLSPContext* ctx, TSNode node) {
|
||||
@@ -1130,8 +1381,15 @@ static void resolve_calls_in_node_inner(GoLSPContext* ctx, TSNode node) {
|
||||
// Process statements to build scope
|
||||
go_process_statement(ctx, node);
|
||||
|
||||
if (strcmp(kind, "if_statement") == 0 || strcmp(kind, "for_statement") == 0 ||
|
||||
strcmp(kind, "expression_switch_statement") == 0 ||
|
||||
strcmp(kind, "type_switch_statement") == 0 || strcmp(kind, "select_statement") == 0) {
|
||||
go_invalidate_control_flow_aliases(ctx, node, 0);
|
||||
}
|
||||
|
||||
// Resolve call expressions
|
||||
if (strcmp(kind, "call_expression") == 0) {
|
||||
go_resolve_value_references_at(ctx, node);
|
||||
TSNode func_node = ts_node_child_by_field_name(node, "function", 8);
|
||||
if (!ts_node_is_null(func_node)) {
|
||||
const char* fk = ts_node_type(func_node);
|
||||
@@ -1151,13 +1409,15 @@ static void resolve_calls_in_node_inner(GoLSPContext* ctx, TSNode node) {
|
||||
if (pkg_qn && field_name) {
|
||||
const CBMRegisteredFunc* f = cbm_registry_lookup_symbol(ctx->registry, pkg_qn, field_name);
|
||||
if (f) {
|
||||
emit_resolved_call(ctx, f->qualified_name, "lsp_direct", 0.95f);
|
||||
emit_resolved_call(ctx, f->qualified_name, "lsp_direct", 0.95f,
|
||||
node);
|
||||
goto recurse;
|
||||
}
|
||||
// Package found but symbol not in registry
|
||||
emit_unresolved_call(ctx,
|
||||
emit_unresolved_call(
|
||||
ctx,
|
||||
cbm_arena_sprintf(ctx->arena, "%s.%s", pkg_name, field_name),
|
||||
"symbol_not_in_registry");
|
||||
"symbol_not_in_registry", node);
|
||||
goto recurse;
|
||||
}
|
||||
}
|
||||
@@ -1170,16 +1430,26 @@ static void resolve_calls_in_node_inner(GoLSPContext* ctx, TSNode node) {
|
||||
if (base && base->kind == CBM_TYPE_POINTER) base = cbm_type_deref(base);
|
||||
|
||||
if (base && base->kind == CBM_TYPE_NAMED) {
|
||||
const CBMRegisteredFunc* method = go_lookup_field_or_method(ctx,
|
||||
base->data.named.qualified_name, field_name);
|
||||
if (method) {
|
||||
const char* strategy = "lsp_type_dispatch";
|
||||
if (method->receiver_type &&
|
||||
strcmp(method->receiver_type, base->data.named.qualified_name) != 0) {
|
||||
strategy = "lsp_embed_dispatch";
|
||||
const CBMRegisteredType *receiver_type = cbm_registry_lookup_type(
|
||||
ctx->registry, base->data.named.qualified_name);
|
||||
/* Registered interface receivers must reach the
|
||||
* interface-resolution branch below. Their semantic
|
||||
* method registrations are signatures, not concrete
|
||||
* dispatch targets. */
|
||||
if (!receiver_type || !receiver_type->is_interface) {
|
||||
const CBMRegisteredFunc *method = go_lookup_field_or_method(
|
||||
ctx, base->data.named.qualified_name, field_name);
|
||||
if (method) {
|
||||
const char *strategy = "lsp_type_dispatch";
|
||||
if (method->receiver_type &&
|
||||
strcmp(method->receiver_type,
|
||||
base->data.named.qualified_name) != 0) {
|
||||
strategy = "lsp_embed_dispatch";
|
||||
}
|
||||
emit_resolved_call(ctx, method->qualified_name, strategy, 0.95f,
|
||||
node);
|
||||
goto recurse;
|
||||
}
|
||||
emit_resolved_call(ctx, method->qualified_name, strategy, 0.95f);
|
||||
goto recurse;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1243,17 +1513,20 @@ static void resolve_calls_in_node_inner(GoLSPContext* ctx, TSNode node) {
|
||||
// `Type.method` wins over the interface-method type_dispatch
|
||||
// for the same call site.
|
||||
emit_resolved_call(ctx, concrete_method->qualified_name,
|
||||
"lsp_interface_resolve", 0.95f);
|
||||
"lsp_interface_resolve", 0.95f,
|
||||
node);
|
||||
goto recurse;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: generic interface dispatch
|
||||
emit_resolved_call(ctx,
|
||||
emit_resolved_call(
|
||||
ctx,
|
||||
cbm_arena_sprintf(ctx->arena, "%s.%s",
|
||||
iface_qn ? iface_qn : "interface", field_name),
|
||||
"lsp_interface_dispatch", 0.85f);
|
||||
iface_qn ? iface_qn : "interface",
|
||||
field_name),
|
||||
"lsp_interface_dispatch", 0.85f, node);
|
||||
goto recurse;
|
||||
}
|
||||
}
|
||||
@@ -1261,15 +1534,17 @@ static void resolve_calls_in_node_inner(GoLSPContext* ctx, TSNode node) {
|
||||
// Type resolved to NAMED but neither method nor interface matched
|
||||
if (base && base->kind == CBM_TYPE_NAMED) {
|
||||
emit_unresolved_call(ctx,
|
||||
cbm_arena_sprintf(ctx->arena, "%s.%s",
|
||||
base->data.named.qualified_name, field_name),
|
||||
"method_not_found");
|
||||
cbm_arena_sprintf(ctx->arena, "%s.%s",
|
||||
base->data.named.qualified_name,
|
||||
field_name),
|
||||
"method_not_found", node);
|
||||
} else if (cbm_type_is_unknown(recv_type)) {
|
||||
char* operand_text = lsp_node_text(ctx, operand);
|
||||
emit_unresolved_call(ctx,
|
||||
emit_unresolved_call(
|
||||
ctx,
|
||||
cbm_arena_sprintf(ctx->arena, "%s.%s",
|
||||
operand_text ? operand_text : "?", field_name),
|
||||
"unknown_receiver_type");
|
||||
operand_text ? operand_text : "?", field_name),
|
||||
"unknown_receiver_type", node);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1279,12 +1554,23 @@ static void resolve_calls_in_node_inner(GoLSPContext* ctx, TSNode node) {
|
||||
if (strcmp(fk, "identifier") == 0) {
|
||||
char* name = lsp_node_text(ctx, func_node);
|
||||
if (name && !is_go_builtin_func(name)) {
|
||||
if (cbm_scope_contains(ctx->current_scope, name)) {
|
||||
const char *alias_target =
|
||||
cbm_scope_lookup_callable(ctx->current_scope, name);
|
||||
if (alias_target) {
|
||||
emit_resolved_alias_call(ctx, alias_target, name, node);
|
||||
} else {
|
||||
emit_unresolved_call(ctx, name, "lexical_value_not_exact_callable",
|
||||
node);
|
||||
}
|
||||
goto recurse;
|
||||
}
|
||||
// Package-local function
|
||||
const CBMRegisteredFunc* f = cbm_registry_lookup_symbol(ctx->registry, ctx->package_qn, name);
|
||||
if (f) {
|
||||
emit_resolved_call(ctx, f->qualified_name, "lsp_direct", 0.95f);
|
||||
emit_resolved_call(ctx, f->qualified_name, "lsp_direct", 0.95f, node);
|
||||
} else {
|
||||
emit_unresolved_call(ctx, name, "function_not_in_registry");
|
||||
emit_unresolved_call(ctx, name, "function_not_in_registry", node);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1715,6 +2001,17 @@ const CBMType* cbm_parse_return_type_text(CBMArena* a, const char* text, const c
|
||||
return cbm_type_named(a, cbm_arena_sprintf(a, "%s.%s", module_qn, text));
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const char *module_qn;
|
||||
} CBMGoLSPSignatureParserContext;
|
||||
|
||||
static const CBMType *cbm_go_lsp_parse_signature_param_text(CBMArena *arena, const char *text,
|
||||
void *parser_ctx) {
|
||||
const CBMGoLSPSignatureParserContext *ctx = (const CBMGoLSPSignatureParserContext *)parser_ctx;
|
||||
const char *module_qn = ctx && ctx->module_qn ? ctx->module_qn : "";
|
||||
return cbm_parse_return_type_text(arena, text, module_qn);
|
||||
}
|
||||
|
||||
// --- Entry point: build registry from file defs + run LSP ---
|
||||
|
||||
void cbm_run_go_lsp(CBMArena* arena, CBMFileResult* result,
|
||||
@@ -1790,9 +2087,21 @@ void cbm_run_go_lsp(CBMArena* arena, CBMFileResult* result,
|
||||
ret_types[1] = NULL;
|
||||
}
|
||||
|
||||
// Build param types from param_types array
|
||||
/* The occurrence-preserving carrier is authoritative whenever it
|
||||
* is present, including a non-NULL carrier with an explicit zero
|
||||
* count. Its slots deliberately have no names because legacy name
|
||||
* extraction is not guaranteed to have the same multiplicity. */
|
||||
const CBMType** param_types_arr = NULL;
|
||||
if (d->param_types) {
|
||||
const char **param_names_arr = NULL;
|
||||
bool has_ordered_params =
|
||||
d->signature_param_types != NULL || d->signature_param_count > 0;
|
||||
CBMGoLSPSignatureParserContext param_parser_ctx = {module_qn};
|
||||
if (has_ordered_params) {
|
||||
param_types_arr = cbm_type_materialize_signature_params(
|
||||
arena, d->signature_param_types, d->signature_param_count,
|
||||
cbm_go_lsp_parse_signature_param_text, ¶m_parser_ctx);
|
||||
} else if (d->param_types) {
|
||||
param_names_arr = d->param_names;
|
||||
int count = 0;
|
||||
while (d->param_types[count]) count++;
|
||||
if (count > 0) {
|
||||
@@ -1802,37 +2111,44 @@ void cbm_run_go_lsp(CBMArena* arena, CBMFileResult* result,
|
||||
}
|
||||
param_types_arr[count] = NULL;
|
||||
}
|
||||
} else {
|
||||
param_names_arr = d->param_names;
|
||||
}
|
||||
|
||||
rf.signature = cbm_type_func(arena, d->param_names, param_types_arr, ret_types);
|
||||
rf.signature = cbm_type_func(arena, param_names_arr, param_types_arr, ret_types);
|
||||
|
||||
// For methods, extract receiver type from receiver text
|
||||
if (strcmp(d->label, "Method") == 0 && d->receiver && d->receiver[0]) {
|
||||
// Receiver format: "(name *Type)" or "(name Type)"
|
||||
// Extract type name: skip parens, skip first identifier, strip *
|
||||
const char* r = d->receiver;
|
||||
while (*r == '(' || *r == ' ') r++;
|
||||
// Skip receiver name
|
||||
while (*r && *r != ' ' && *r != '*') r++;
|
||||
while (*r == ' ' || *r == '*') r++;
|
||||
// Now r points to the type name
|
||||
const char* end = r;
|
||||
while (*end && *end != ')' && *end != ' ') end++;
|
||||
if (end > r) {
|
||||
char* type_name = cbm_arena_strndup(arena, r, end - r);
|
||||
rf.receiver_type = cbm_arena_sprintf(arena, "%s.%s", module_qn, type_name);
|
||||
|
||||
// Also register this method under the type
|
||||
const CBMRegisteredType* existing = cbm_registry_lookup_type(®, rf.receiver_type);
|
||||
if (!existing) {
|
||||
// Auto-create the type entry
|
||||
CBMRegisteredType auto_type;
|
||||
memset(&auto_type, 0, sizeof(auto_type));
|
||||
auto_type.qualified_name = rf.receiver_type;
|
||||
auto_type.short_name = type_name;
|
||||
cbm_registry_add_type(®, auto_type);
|
||||
// Prefer the extractor's semantic receiver QN. Raw receiver text is
|
||||
// retained only as a compatibility fallback for hand-built defs;
|
||||
// reparsing it alone loses valid unnamed receivers such as `(Right)`.
|
||||
if (strcmp(d->label, "Method") == 0) {
|
||||
if (d->parent_class && d->parent_class[0]) {
|
||||
rf.receiver_type = d->parent_class;
|
||||
} else if (d->receiver && d->receiver[0]) {
|
||||
// Legacy named receiver formats: "(name *Type)" / "(name Type)".
|
||||
const char *r = d->receiver;
|
||||
while (*r == '(' || *r == ' ')
|
||||
r++;
|
||||
while (*r && *r != ' ' && *r != '*')
|
||||
r++;
|
||||
while (*r == ' ' || *r == '*')
|
||||
r++;
|
||||
const char *end = r;
|
||||
while (*end && *end != ')' && *end != ' ')
|
||||
end++;
|
||||
if (end > r) {
|
||||
char *type_name = cbm_arena_strndup(arena, r, (size_t)(end - r));
|
||||
rf.receiver_type = cbm_arena_sprintf(arena, "%s.%s", module_qn, type_name);
|
||||
}
|
||||
}
|
||||
|
||||
if (rf.receiver_type && !cbm_registry_lookup_type(®, rf.receiver_type)) {
|
||||
const char *dot = strrchr(rf.receiver_type, '.');
|
||||
CBMRegisteredType auto_type;
|
||||
memset(&auto_type, 0, sizeof(auto_type));
|
||||
auto_type.qualified_name = rf.receiver_type;
|
||||
auto_type.short_name = dot ? dot + 1 : rf.receiver_type;
|
||||
cbm_registry_add_type(®, auto_type);
|
||||
}
|
||||
}
|
||||
|
||||
cbm_registry_add_func(®, rf);
|
||||
@@ -2343,6 +2659,42 @@ static const CBMType* parse_type_node_with_params(CBMArena* arena, TSNode node,
|
||||
return cbm_type_unknown();
|
||||
}
|
||||
|
||||
/* Go permits one AST parameter declaration to introduce several call
|
||||
* positions: `first, second T`. Count direct names without exposing or
|
||||
* rewriting the legacy public param_names projection. */
|
||||
static int cbm_go_lsp_signature_param_multiplicity(TSNode param) {
|
||||
const char *kind = ts_node_type(param);
|
||||
if (strcmp(kind, "parameter_declaration") != 0 &&
|
||||
strcmp(kind, "variadic_parameter_declaration") != 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int field_names = 0;
|
||||
int identifier_names = 0;
|
||||
uint32_t child_count = ts_node_child_count(param);
|
||||
for (uint32_t i = 0; i < child_count; i++) {
|
||||
TSNode child = ts_node_child(param, i);
|
||||
if (ts_node_is_null(child) || !ts_node_is_named(child))
|
||||
continue;
|
||||
const char *field = ts_node_field_name_for_child(param, i);
|
||||
if (field && strcmp(field, "name") == 0)
|
||||
field_names++;
|
||||
if (strcmp(ts_node_type(child), "identifier") == 0)
|
||||
identifier_names++;
|
||||
}
|
||||
|
||||
int names = field_names > 0 ? field_names : identifier_names;
|
||||
return names > 0 ? names : 1;
|
||||
}
|
||||
|
||||
static const CBMType *cbm_go_lsp_keep_known_refinement(const CBMType *candidate,
|
||||
const CBMType *existing) {
|
||||
if (cbm_type_is_unknown(candidate) && !cbm_type_is_unknown(existing)) {
|
||||
return existing;
|
||||
}
|
||||
return candidate ? candidate : cbm_type_unknown();
|
||||
}
|
||||
|
||||
// Scan AST for generic function declarations and set type_param_names + re-parse
|
||||
// return types on matching registered functions.
|
||||
static void extract_type_params_from_ast(CBMArena* arena, CBMTypeRegistry* reg,
|
||||
@@ -2450,20 +2802,32 @@ static void extract_type_params_from_ast(CBMArena* arena, CBMTypeRegistry* reg,
|
||||
if (ts_node_is_null(rchild) || !ts_node_is_named(rchild)) continue;
|
||||
TSNode rtype = ts_node_child_by_field_name(rchild, "type", 4);
|
||||
if (ts_node_is_null(rtype)) rtype = rchild;
|
||||
new_rets[idx++] = parse_type_node_with_params(arena,
|
||||
rtype, source, module_qn, tp_names);
|
||||
const CBMType *parsed = parse_type_node_with_params(
|
||||
arena, rtype, source, module_qn, tp_names);
|
||||
new_rets[idx] =
|
||||
cbm_go_lsp_keep_known_refinement(parsed, old_rets[idx]);
|
||||
idx++;
|
||||
}
|
||||
new_rets[idx] = NULL;
|
||||
while (idx < ret_count) {
|
||||
new_rets[idx] = old_rets[idx];
|
||||
idx++;
|
||||
}
|
||||
new_rets[ret_count] = NULL;
|
||||
} else {
|
||||
new_rets[0] = parse_type_node_with_params(arena,
|
||||
result_node, source, module_qn, tp_names);
|
||||
new_rets[1] = NULL;
|
||||
const CBMType *parsed = parse_type_node_with_params(
|
||||
arena, result_node, source, module_qn, tp_names);
|
||||
new_rets[0] = cbm_go_lsp_keep_known_refinement(parsed, old_rets[0]);
|
||||
for (int idx = 1; idx < ret_count; idx++) {
|
||||
new_rets[idx] = old_rets[idx];
|
||||
}
|
||||
new_rets[ret_count] = NULL;
|
||||
}
|
||||
|
||||
CBMType* new_sig = (CBMType*)cbm_arena_alloc(arena, sizeof(CBMType));
|
||||
*new_sig = *(CBMType*)reg->funcs[fi].signature;
|
||||
new_sig->data.func.return_types = new_rets;
|
||||
reg->funcs[fi].signature = new_sig;
|
||||
const CBMType *new_sig = cbm_type_func_replace_returns(
|
||||
arena, reg->funcs[fi].signature, new_rets);
|
||||
if (new_sig && new_sig->kind == CBM_TYPE_FUNC) {
|
||||
reg->funcs[fi].signature = new_sig;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2480,26 +2844,40 @@ static void extract_type_params_from_ast(CBMArena* arena, CBMTypeRegistry* reg,
|
||||
const CBMType** new_params = (const CBMType**)cbm_arena_alloc(arena,
|
||||
(pc + 1) * sizeof(const CBMType*));
|
||||
int idx = 0;
|
||||
bool overflow = false;
|
||||
uint32_t pnc = ts_node_child_count(params_node);
|
||||
for (uint32_t pi = 0; pi < pnc && idx < pc; pi++) {
|
||||
for (uint32_t pi = 0; pi < pnc && !overflow; pi++) {
|
||||
TSNode pchild = ts_node_child(params_node, pi);
|
||||
if (ts_node_is_null(pchild) || !ts_node_is_named(pchild)) continue;
|
||||
if (strcmp(ts_node_type(pchild), "parameter_declaration") != 0) continue;
|
||||
const char *param_kind = ts_node_type(pchild);
|
||||
if (strcmp(param_kind, "parameter_declaration") != 0 &&
|
||||
strcmp(param_kind, "variadic_parameter_declaration") != 0) {
|
||||
continue;
|
||||
}
|
||||
TSNode ptype = ts_node_child_by_field_name(pchild, "type", 4);
|
||||
if (ts_node_is_null(ptype)) continue;
|
||||
new_params[idx++] = parse_type_node_with_params(arena,
|
||||
ptype, source, module_qn, tp_names);
|
||||
const CBMType *parsed =
|
||||
ts_node_is_null(ptype)
|
||||
? cbm_type_unknown()
|
||||
: parse_type_node_with_params(arena, ptype, source, module_qn,
|
||||
tp_names);
|
||||
int multiplicity = cbm_go_lsp_signature_param_multiplicity(pchild);
|
||||
for (int occurrence = 0; occurrence < multiplicity; occurrence++) {
|
||||
if (idx >= pc) {
|
||||
overflow = true;
|
||||
break;
|
||||
}
|
||||
new_params[idx] =
|
||||
cbm_go_lsp_keep_known_refinement(parsed, old_params[idx]);
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
new_params[idx] = NULL;
|
||||
if (idx > 0) {
|
||||
CBMType* sig = (CBMType*)reg->funcs[fi].signature;
|
||||
if (sig != reg->funcs[fi].signature) {
|
||||
// Already rebuilt — update in place
|
||||
((CBMType*)reg->funcs[fi].signature)->data.func.param_types = new_params;
|
||||
} else {
|
||||
CBMType* new_sig = (CBMType*)cbm_arena_alloc(arena, sizeof(CBMType));
|
||||
*new_sig = *sig;
|
||||
new_sig->data.func.param_types = new_params;
|
||||
if (!overflow && idx == pc) {
|
||||
const CBMType *old_sig = reg->funcs[fi].signature;
|
||||
const CBMType *new_sig =
|
||||
cbm_type_func(arena, old_sig->data.func.param_names, new_params,
|
||||
old_sig->data.func.return_types);
|
||||
if (new_sig && new_sig->kind == CBM_TYPE_FUNC) {
|
||||
reg->funcs[fi].signature = new_sig;
|
||||
}
|
||||
}
|
||||
@@ -2587,7 +2965,11 @@ void cbm_run_go_lsp_cross(
|
||||
|
||||
// Build FUNC type from return_types text
|
||||
const CBMType** ret_types = split_pipe_types(arena, d->return_types, def_mod);
|
||||
rf.signature = cbm_type_func(arena, NULL, NULL, ret_types);
|
||||
CBMGoLSPSignatureParserContext param_parser_ctx = {def_mod};
|
||||
const CBMType **param_types = cbm_type_materialize_signature_params(
|
||||
arena, d->signature_param_types, d->signature_param_count,
|
||||
cbm_go_lsp_parse_signature_param_text, ¶m_parser_ctx);
|
||||
rf.signature = cbm_type_func(arena, NULL, param_types, ret_types);
|
||||
|
||||
// Method receiver
|
||||
if (strcmp(d->label, "Method") == 0 && d->receiver_type && d->receiver_type[0]) {
|
||||
@@ -2833,7 +3215,11 @@ CBMTypeRegistry* cbm_go_build_cross_registry(
|
||||
rf.qualified_name = d->qualified_name; /* borrowed */
|
||||
rf.short_name = d->short_name;
|
||||
const CBMType** ret_types = split_pipe_types(arena, d->return_types, def_mod);
|
||||
rf.signature = cbm_type_func(arena, NULL, NULL, ret_types);
|
||||
CBMGoLSPSignatureParserContext param_parser_ctx = {def_mod};
|
||||
const CBMType **param_types = cbm_type_materialize_signature_params(
|
||||
arena, d->signature_param_types, d->signature_param_count,
|
||||
cbm_go_lsp_parse_signature_param_text, ¶m_parser_ctx);
|
||||
rf.signature = cbm_type_func(arena, NULL, param_types, ret_types);
|
||||
if (strcmp(d->label, "Method") == 0 && d->receiver_type && d->receiver_type[0]) {
|
||||
rf.receiver_type = d->receiver_type;
|
||||
if (!cbm_registry_lookup_type(reg, rf.receiver_type)) {
|
||||
@@ -2972,17 +3358,28 @@ int cbm_go_fast_resolve_qualified_calls(
|
||||
}
|
||||
}
|
||||
|
||||
/* Case 3: an unqualified function value from another source file in
|
||||
* the same Go package. Go module QNs are directory-based, so this is
|
||||
* an exact package-local lookup rather than a project-wide short-name
|
||||
* guess. */
|
||||
if (!f && !strchr(uc->callee_qn, '.') && result->module_qn) {
|
||||
f = cbm_registry_lookup_symbol(reg, result->module_qn, uc->callee_qn);
|
||||
}
|
||||
|
||||
if (!f) continue;
|
||||
|
||||
/* Emit a resolved entry. cbm_pipeline_find_lsp_resolution
|
||||
* picks the highest-confidence match, so the unresolved entry
|
||||
* stays (harmless duplicate) but our resolved entry wins. */
|
||||
CBMResolvedCall rc;
|
||||
CBMResolvedCall rc = {0};
|
||||
rc.caller_qn = uc->caller_qn;
|
||||
rc.callee_qn = f->qualified_name; /* borrowed from pipeline arena */
|
||||
rc.strategy = "lsp_strategy_cross_file";
|
||||
rc.confidence = 0.92f;
|
||||
rc.reason = NULL;
|
||||
rc.kind = uc->kind;
|
||||
rc.site_start_byte = uc->site_start_byte;
|
||||
rc.site_end_byte = uc->site_end_byte;
|
||||
cbm_resolvedcall_push(&result->resolved_calls, &result->arena, rc);
|
||||
newly_resolved++;
|
||||
}
|
||||
@@ -3034,11 +3431,15 @@ void cbm_batch_go_lsp_cross(
|
||||
for (int j = 0; j < file_out.count; j++) {
|
||||
CBMResolvedCall* src = &file_out.items[j];
|
||||
CBMResolvedCall* dst = &out[f].items[j];
|
||||
memset(dst, 0, sizeof(*dst));
|
||||
dst->caller_qn = src->caller_qn ? cbm_arena_strdup(arena, src->caller_qn) : NULL;
|
||||
dst->callee_qn = src->callee_qn ? cbm_arena_strdup(arena, src->callee_qn) : NULL;
|
||||
dst->strategy = src->strategy ? cbm_arena_strdup(arena, src->strategy) : NULL;
|
||||
dst->confidence = src->confidence;
|
||||
dst->reason = src->reason ? cbm_arena_strdup(arena, src->reason) : NULL;
|
||||
dst->kind = src->kind;
|
||||
dst->site_start_byte = src->site_start_byte;
|
||||
dst->site_end_byte = src->site_end_byte;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -84,9 +84,21 @@ typedef struct {
|
||||
const char* embedded_types; // "|"-separated embedded type QNs (for struct embedding)
|
||||
const char* field_defs; // "|"-separated "name:type" pairs (for struct fields, e.g. "Binder:Binder|Name:string")
|
||||
const char* method_names_str; // "|"-separated method names for interfaces (e.g. "Get|Put|Delete")
|
||||
const char **signature_param_types; // borrowed ordered parameter type texts; "?" means unknown
|
||||
int signature_param_count; // number of positional entries above
|
||||
bool is_interface;
|
||||
CBMLanguage lang; // language of the file that defined this — used by Tier 2 per-language registry build to filter all_defs
|
||||
const char* namespace_name; // declared namespace/package for source-root-independent JVM filtering
|
||||
/* Rust-only raw trait spelling. The Rust registry canonicalizes this
|
||||
* only when it has one exact/unique trait target. */
|
||||
const char *trait_qn;
|
||||
bool is_rust_impl_relation; // independent type-level impl record (empty impls survive)
|
||||
bool is_abstract; // Rust required trait method; false for defaults
|
||||
/* Python-only raw decorator syntax, borrowed from CBMDefinition. The
|
||||
* resolver must retain it across fused/cross-file registry construction:
|
||||
* a decorator rebinds the function name, so the undecorated definition is
|
||||
* not automatically an exact callable-value target. */
|
||||
const char **decorators;
|
||||
} CBMLSPDef;
|
||||
|
||||
// Parse source, build registry from defs + stdlib, run LSP.
|
||||
|
||||
+217
-72
@@ -1632,6 +1632,24 @@ static const char *class_super_qn(JavaLSPContext *ctx, TSNode class_node) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Java enum constants are direct enum_body children, while declarations after
|
||||
* the semicolon live in enum_body_declarations. LSP class processing only
|
||||
* wants declarations, so expose the nested container for enums and the normal
|
||||
* body for every other type declaration. */
|
||||
static TSNode java_type_declaration_body(TSNode node) {
|
||||
TSNode body = ts_node_child_by_field_name(node, "body", 4);
|
||||
if (ts_node_is_null(body) || strcmp(ts_node_type(body), "enum_body") != 0)
|
||||
return body;
|
||||
|
||||
uint32_t count = ts_node_named_child_count(body);
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
TSNode child = ts_node_named_child(body, i);
|
||||
if (strcmp(ts_node_type(child), "enum_body_declarations") == 0)
|
||||
return child;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
static void java_process_class_decl(JavaLSPContext *ctx, TSNode node) {
|
||||
TSNode name_node = ts_node_child_by_field_name(node, "name", 4);
|
||||
if (ts_node_is_null(name_node))
|
||||
@@ -1664,7 +1682,7 @@ static void java_process_class_decl(JavaLSPContext *ctx, TSNode node) {
|
||||
ctx->enclosing_super_qn = super_qn;
|
||||
ctx->enclosing_class_short = cname;
|
||||
|
||||
TSNode body = ts_node_child_by_field_name(node, "body", 4);
|
||||
TSNode body = java_type_declaration_body(node);
|
||||
if (!ts_node_is_null(body)) {
|
||||
CBMScope *saved_scope = ctx->current_scope;
|
||||
ctx->current_scope = cbm_scope_push(ctx->arena, saved_scope);
|
||||
@@ -1779,40 +1797,87 @@ void java_lsp_process_file(JavaLSPContext *ctx, TSNode root) {
|
||||
|
||||
/* ── Call-edge resolution ─────────────────────────────────────────── */
|
||||
|
||||
static void java_emit_resolved_orig(JavaLSPContext *ctx, const char *callee_qn, const char *orig,
|
||||
const char *strategy, float confidence) {
|
||||
static void java_emit_resolved_orig_kind(JavaLSPContext *ctx, const char *callee_qn,
|
||||
const char *orig, const char *strategy, float confidence,
|
||||
CBMResolvedKind kind) {
|
||||
if (!ctx->resolved_calls || !ctx->enclosing_method_qn || !callee_qn)
|
||||
return;
|
||||
CBMResolvedCall rc;
|
||||
CBMResolvedCall rc = {0};
|
||||
rc.caller_qn = ctx->enclosing_method_qn;
|
||||
rc.callee_qn = callee_qn;
|
||||
rc.strategy = strategy;
|
||||
rc.confidence = confidence;
|
||||
// For a data-flow resolution (constructor reference `Lhs::new` resolved to
|
||||
// the Lhs class), `reason` carries the ORIGINAL textual callee (`new`) so the
|
||||
// pipeline join can match the textual call site even though the resolved
|
||||
// callee_qn's short name differs. NULL/unread for normal resolved calls.
|
||||
rc.kind = kind;
|
||||
// For data-flow resolutions, `reason` may carry the original textual
|
||||
// callee when it differs from the resolved target. NULL for normal calls.
|
||||
rc.reason = orig;
|
||||
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
|
||||
}
|
||||
|
||||
static void java_emit_resolved_orig(JavaLSPContext *ctx, const char *callee_qn, const char *orig,
|
||||
const char *strategy, float confidence) {
|
||||
java_emit_resolved_orig_kind(ctx, callee_qn, orig, strategy, confidence,
|
||||
CBM_RESOLVED_INVOCATION);
|
||||
}
|
||||
|
||||
static void java_emit_resolved(JavaLSPContext *ctx, const char *callee_qn, const char *strategy,
|
||||
float confidence) {
|
||||
java_emit_resolved_orig(ctx, callee_qn, NULL, strategy, confidence);
|
||||
}
|
||||
|
||||
static void java_emit_unresolved(JavaLSPContext *ctx, const char *expr_text, const char *reason) {
|
||||
static void java_emit_resolved_reference(JavaLSPContext *ctx, const char *callee_qn,
|
||||
const char *strategy, float confidence) {
|
||||
java_emit_resolved_orig_kind(ctx, callee_qn, NULL, strategy, confidence,
|
||||
CBM_RESOLVED_CALL_REFERENCE);
|
||||
}
|
||||
|
||||
static void java_stamp_resolved_site(JavaLSPContext *ctx, int first, TSNode site) {
|
||||
if (!ctx->resolved_calls || ts_node_is_null(site) || first < 0) {
|
||||
return;
|
||||
}
|
||||
for (int i = first; i < ctx->resolved_calls->count; i++) {
|
||||
ctx->resolved_calls->items[i].site_start_byte = ts_node_start_byte(site);
|
||||
ctx->resolved_calls->items[i].site_end_byte = ts_node_end_byte(site);
|
||||
}
|
||||
}
|
||||
|
||||
static void java_emit_resolved_reference_at(JavaLSPContext *ctx, const char *callee_qn,
|
||||
const char *strategy, float confidence, TSNode site) {
|
||||
int first = ctx->resolved_calls ? ctx->resolved_calls->count : -1;
|
||||
java_emit_resolved_reference(ctx, callee_qn, strategy, confidence);
|
||||
java_stamp_resolved_site(ctx, first, site);
|
||||
}
|
||||
|
||||
static void java_emit_unresolved_kind(JavaLSPContext *ctx, const char *expr_text,
|
||||
const char *reason, CBMResolvedKind kind) {
|
||||
if (!ctx->resolved_calls || !ctx->enclosing_method_qn)
|
||||
return;
|
||||
CBMResolvedCall rc;
|
||||
CBMResolvedCall rc = {0};
|
||||
rc.caller_qn = ctx->enclosing_method_qn;
|
||||
rc.callee_qn = expr_text ? expr_text : "?";
|
||||
rc.strategy = "lsp_unresolved";
|
||||
rc.confidence = 0.0f;
|
||||
rc.reason = reason;
|
||||
rc.kind = kind;
|
||||
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
|
||||
}
|
||||
|
||||
static void java_emit_unresolved(JavaLSPContext *ctx, const char *expr_text, const char *reason) {
|
||||
java_emit_unresolved_kind(ctx, expr_text, reason, CBM_RESOLVED_INVOCATION);
|
||||
}
|
||||
|
||||
static void java_emit_unresolved_reference(JavaLSPContext *ctx, const char *expr_text,
|
||||
const char *reason) {
|
||||
java_emit_unresolved_kind(ctx, expr_text, reason, CBM_RESOLVED_CALL_REFERENCE);
|
||||
}
|
||||
|
||||
static void java_emit_unresolved_reference_at(JavaLSPContext *ctx, const char *expr_text,
|
||||
const char *reason, TSNode site) {
|
||||
int first = ctx->resolved_calls ? ctx->resolved_calls->count : -1;
|
||||
java_emit_unresolved_reference(ctx, expr_text, reason);
|
||||
java_stamp_resolved_site(ctx, first, site);
|
||||
}
|
||||
|
||||
/* Find a sole concrete in-project implementer of interface `iface_qn` that
|
||||
* declares method `mname`. Returns the implementer's QN when exactly ONE
|
||||
* exists (else NULL), and sets *out_count to the number found (capped at 2,
|
||||
@@ -2590,8 +2655,9 @@ static void resolve_method_reference(JavaLSPContext *ctx, TSNode mref,
|
||||
/* Try the last named child first; if it's the same as the LHS (only one
|
||||
* named child total), we have a constructor ref where `new` is unnamed. */
|
||||
char *mname = NULL;
|
||||
TSNode name_node = {0};
|
||||
if (nc_named >= 2) {
|
||||
TSNode name_node = ts_node_named_child(mref, nc_named - 1);
|
||||
name_node = ts_node_named_child(mref, nc_named - 1);
|
||||
mname = java_node_text(ctx, name_node);
|
||||
}
|
||||
if (!mname || !mname[0]) {
|
||||
@@ -2603,6 +2669,10 @@ static void resolve_method_reference(JavaLSPContext *ctx, TSNode mref,
|
||||
const char *ck = ts_node_type(c);
|
||||
if (strcmp(ck, "new") == 0) {
|
||||
mname = "new";
|
||||
/* Usage extraction represents `Type::new` by its type leaf,
|
||||
* because `new` is an unnamed grammar token. Keep the LSP
|
||||
* occurrence on that same leaf so the two passes join. */
|
||||
name_node = lhs;
|
||||
break;
|
||||
}
|
||||
if (strcmp(ck, "identifier") == 0) {
|
||||
@@ -2610,8 +2680,10 @@ static void resolve_method_reference(JavaLSPContext *ctx, TSNode mref,
|
||||
if (ts_node_eq(c, lhs))
|
||||
continue;
|
||||
mname = java_node_text(ctx, c);
|
||||
if (mname && mname[0])
|
||||
if (mname && mname[0]) {
|
||||
name_node = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2667,7 +2739,7 @@ static void resolve_method_reference(JavaLSPContext *ctx, TSNode mref,
|
||||
type_qn = lhs_t->data.template_type.template_name;
|
||||
}
|
||||
if (!type_qn) {
|
||||
java_emit_unresolved(ctx, mname, "method_reference_unknown_lhs");
|
||||
java_emit_unresolved_reference_at(ctx, mname, "method_reference_unknown_lhs", name_node);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2677,15 +2749,17 @@ static void resolve_method_reference(JavaLSPContext *ctx, TSNode mref,
|
||||
short_name = short_name ? short_name + 1 : type_qn;
|
||||
const CBMRegisteredFunc *cf =
|
||||
cbm_registry_lookup_method(ctx->registry, type_qn, short_name);
|
||||
// A `ClassName::new` reference constructs ClassName: resolve to the
|
||||
// ClassName CLASS node (which the textual extractor stored), not the
|
||||
// synthetic constructor QN that has no graph node. orig=mname ("new")
|
||||
// lets the join match the textual `new` call site (the constructor
|
||||
// reference is extracted as a call to `new`). cf distinguishes an
|
||||
// indexed constructor (higher confidence) from a synthesized one.
|
||||
java_emit_resolved_orig(ctx, type_qn, mname,
|
||||
cf ? "lsp_method_ref_ctor" : "lsp_method_ref_ctor_synth",
|
||||
cf ? 0.90f : 0.80f);
|
||||
/* A constructor reference is precise only when a concrete constructor
|
||||
* callable exists in the registry. Never relabel the class node itself
|
||||
* as a callable target; if the constructor is absent/unmaterialized,
|
||||
* the usage pass will retain the type occurrence as ordinary USAGE. */
|
||||
if (cf && cf->qualified_name) {
|
||||
java_emit_resolved_reference_at(ctx, cf->qualified_name, "lsp_method_ref_ctor", 0.90f,
|
||||
name_node);
|
||||
} else {
|
||||
java_emit_unresolved_reference_at(
|
||||
ctx, type_qn, "method_reference_constructor_not_materialized", name_node);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2704,10 +2778,11 @@ static void resolve_method_reference(JavaLSPContext *ctx, TSNode mref,
|
||||
if (!m)
|
||||
m = java_lookup_method(ctx, type_qn, mname, 0);
|
||||
if (m) {
|
||||
java_emit_resolved(ctx, m->qualified_name, "lsp_method_ref", 0.90f);
|
||||
java_emit_resolved_reference_at(ctx, m->qualified_name, "lsp_method_ref", 0.90f, name_node);
|
||||
} else {
|
||||
java_emit_unresolved(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", type_qn, mname),
|
||||
"method_reference_no_match");
|
||||
java_emit_unresolved_reference_at(ctx,
|
||||
cbm_arena_sprintf(ctx->arena, "%s.%s", type_qn, mname),
|
||||
"method_reference_no_match", name_node);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2825,7 +2900,13 @@ static void java_resolve_calls_in_node_inner(JavaLSPContext *ctx, TSNode node) {
|
||||
}
|
||||
|
||||
if (strcmp(kind, "method_invocation") == 0) {
|
||||
int first_resolution = ctx->resolved_calls ? ctx->resolved_calls->count : -1;
|
||||
resolve_method_call(ctx, node);
|
||||
/* Raw CALL extraction records the full method_invocation span. Stamp
|
||||
* the semantic record with that identical occurrence so two calls to
|
||||
* the same method name in one caller retain receiver identity at the
|
||||
* pipeline join (for example alpha.handle(); beta.handle();). */
|
||||
java_stamp_resolved_site(ctx, first_resolution, node);
|
||||
|
||||
/* Lambda / method-reference args: re-walk with the SAM-bound scope.
|
||||
* This is the central piece that turns chained streams + forEach
|
||||
@@ -2859,6 +2940,7 @@ static void java_resolve_calls_in_node_inner(JavaLSPContext *ctx, TSNode node) {
|
||||
}
|
||||
} else if (strcmp(kind, "object_creation_expression") == 0) {
|
||||
/* Resolve constructor as a method. */
|
||||
int first_resolution = ctx->resolved_calls ? ctx->resolved_calls->count : -1;
|
||||
TSNode type_node = ts_node_child_by_field_name(node, "type", 4);
|
||||
if (!ts_node_is_null(type_node)) {
|
||||
const CBMType *t = java_parse_type_node(ctx, type_node);
|
||||
@@ -2889,6 +2971,7 @@ static void java_resolve_calls_in_node_inner(JavaLSPContext *ctx, TSNode node) {
|
||||
}
|
||||
}
|
||||
}
|
||||
java_stamp_resolved_site(ctx, first_resolution, node);
|
||||
}
|
||||
|
||||
/* catch_clause: push a fresh scope so the bound exception variable is
|
||||
@@ -3095,6 +3178,19 @@ static const CBMType *parse_param_text(CBMArena *a, const char *text, const char
|
||||
return parse_param_text_full(a, text, NULL, module_qn, NULL);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const char *parent_class;
|
||||
const char *module_qn;
|
||||
const CBMTypeRegistry *registry;
|
||||
} JavaSignatureParseContext;
|
||||
|
||||
static const CBMType *java_signature_parse_type(CBMArena *arena, const char *text,
|
||||
void *parser_ctx) {
|
||||
const JavaSignatureParseContext *ctx = (const JavaSignatureParseContext *)parser_ctx;
|
||||
return parse_param_text_full(arena, text, ctx ? ctx->parent_class : NULL,
|
||||
ctx ? ctx->module_qn : NULL, ctx ? ctx->registry : NULL);
|
||||
}
|
||||
|
||||
static void register_local_func_or_type_from_file(JavaLSPContext *ctx, CBMTypeRegistry *reg,
|
||||
CBMFileResult *result) {
|
||||
CBMArena *a = ctx->arena;
|
||||
@@ -3259,17 +3355,35 @@ static void register_local_func_or_type_from_file(JavaLSPContext *ctx, CBMTypeRe
|
||||
* not "test.Main.Box". */
|
||||
const char *parent = d->parent_class;
|
||||
const CBMType **ptypes = NULL;
|
||||
if (d->param_types) {
|
||||
int pc = 0;
|
||||
while (d->param_types[pc])
|
||||
pc++;
|
||||
if (pc > 0) {
|
||||
ptypes = (const CBMType **)cbm_arena_alloc(a, (size_t)(pc + 1) * sizeof(*ptypes));
|
||||
for (int j = 0; j < pc; j++) {
|
||||
ptypes[j] = parse_param_text_full(a, d->param_types[j], parent, module_qn, reg);
|
||||
const char **param_names = NULL;
|
||||
if (d->signature_param_types || d->signature_param_count > 0) {
|
||||
JavaSignatureParseContext parse_ctx = {
|
||||
.parent_class = parent,
|
||||
.module_qn = module_qn,
|
||||
.registry = reg,
|
||||
};
|
||||
ptypes = cbm_type_materialize_signature_params(a, d->signature_param_types,
|
||||
d->signature_param_count,
|
||||
java_signature_parse_type, &parse_ctx);
|
||||
} else if (!d->signature_param_types && d->signature_param_count == 0) {
|
||||
/* Compatibility for callers that still construct CBMDefinition by
|
||||
* hand. The legacy projection is deduplicated/non-positional, so
|
||||
* it is consulted only when the counted carrier is wholly absent. */
|
||||
if (d->param_types) {
|
||||
int pc = 0;
|
||||
while (d->param_types[pc])
|
||||
pc++;
|
||||
if (pc > 0) {
|
||||
ptypes =
|
||||
(const CBMType **)cbm_arena_alloc(a, (size_t)(pc + 1) * sizeof(*ptypes));
|
||||
for (int j = 0; j < pc; j++) {
|
||||
ptypes[j] =
|
||||
parse_param_text_full(a, d->param_types[j], parent, module_qn, reg);
|
||||
}
|
||||
ptypes[pc] = NULL;
|
||||
}
|
||||
ptypes[pc] = NULL;
|
||||
}
|
||||
param_names = d->param_names;
|
||||
}
|
||||
/* Build return types — single return only in Java. */
|
||||
const CBMType **rtypes = NULL;
|
||||
@@ -3282,7 +3396,7 @@ static void register_local_func_or_type_from_file(JavaLSPContext *ctx, CBMTypeRe
|
||||
rtypes[0] = parse_param_text_full(a, d->return_types[0], parent, module_qn, reg);
|
||||
rtypes[1] = NULL;
|
||||
}
|
||||
rf.signature = cbm_type_func(a, d->param_names, ptypes, rtypes);
|
||||
rf.signature = cbm_type_func(a, param_names, ptypes, rtypes);
|
||||
if (d->parent_class) {
|
||||
rf.receiver_type = d->parent_class;
|
||||
}
|
||||
@@ -3316,29 +3430,17 @@ static void patch_one_method(JavaLSPContext *ctx, CBMTypeRegistry *reg, TSNode m
|
||||
return;
|
||||
char *method_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", class_qn, mname);
|
||||
|
||||
/* Find the registered func by exact QN. */
|
||||
CBMRegisteredFunc *slot = NULL;
|
||||
for (int fi = 0; fi < reg->func_count; fi++) {
|
||||
if (reg->funcs[fi].qualified_name &&
|
||||
strcmp(reg->funcs[fi].qualified_name, method_qn) == 0) {
|
||||
slot = ®->funcs[fi];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!slot)
|
||||
return;
|
||||
|
||||
/* Re-extract param types from the AST. */
|
||||
TSNode params = ts_node_child_by_field_name(method_node, "parameters", 10);
|
||||
if (ts_node_is_null(params))
|
||||
return;
|
||||
uint32_t pn = ts_node_named_child_count(params);
|
||||
if (pn == 0)
|
||||
return;
|
||||
const CBMType **ptypes =
|
||||
(const CBMType **)cbm_arena_alloc(ctx->arena, (size_t)(pn + 1) * sizeof(*ptypes));
|
||||
if (!ptypes)
|
||||
return;
|
||||
const CBMType **ptypes = NULL;
|
||||
if (pn > 0) {
|
||||
ptypes = (const CBMType **)cbm_arena_alloc(ctx->arena, (size_t)(pn + 1) * sizeof(*ptypes));
|
||||
if (!ptypes)
|
||||
return;
|
||||
}
|
||||
int idx = 0;
|
||||
for (uint32_t i = 0; i < pn; i++) {
|
||||
TSNode p = ts_node_named_child(params, i);
|
||||
@@ -3355,7 +3457,37 @@ static void patch_one_method(JavaLSPContext *ctx, CBMTypeRegistry *reg, TSNode m
|
||||
}
|
||||
ptypes[idx++] = pt;
|
||||
}
|
||||
ptypes[idx] = NULL;
|
||||
if (ptypes)
|
||||
ptypes[idx] = NULL;
|
||||
|
||||
/* Java overloads share a graph QN. Select the registry slot whose counted
|
||||
* signature has the same arity as this AST declaration; falling back to the
|
||||
* first QN match keeps the legacy no-signature path best-effort. */
|
||||
CBMRegisteredFunc *slot = NULL;
|
||||
CBMRegisteredFunc *first_qn_match = NULL;
|
||||
for (int fi = 0; fi < reg->func_count; fi++) {
|
||||
CBMRegisteredFunc *candidate = ®->funcs[fi];
|
||||
if (!candidate->qualified_name || strcmp(candidate->qualified_name, method_qn) != 0)
|
||||
continue;
|
||||
if (!first_qn_match)
|
||||
first_qn_match = candidate;
|
||||
if (!candidate->signature || candidate->signature->kind != CBM_TYPE_FUNC)
|
||||
continue;
|
||||
int candidate_arity = 0;
|
||||
const CBMType *const *candidate_params = candidate->signature->data.func.param_types;
|
||||
if (candidate_params) {
|
||||
while (candidate_params[candidate_arity])
|
||||
candidate_arity++;
|
||||
}
|
||||
if (candidate_arity == idx) {
|
||||
slot = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!slot)
|
||||
slot = first_qn_match;
|
||||
if (!slot)
|
||||
return;
|
||||
|
||||
/* Re-extract return type from AST (only for methods, not constructors). */
|
||||
const CBMType **rtypes = NULL;
|
||||
@@ -3401,7 +3533,7 @@ static void patch_method_signatures_from_ast(JavaLSPContext *ctx, CBMTypeRegistr
|
||||
class_qn = cbm_arena_strdup(ctx->arena, cname);
|
||||
}
|
||||
|
||||
TSNode body = ts_node_child_by_field_name(class_node, "body", 4);
|
||||
TSNode body = java_type_declaration_body(class_node);
|
||||
if (ts_node_is_null(body))
|
||||
return;
|
||||
|
||||
@@ -3495,7 +3627,7 @@ static void populate_class_fields_from_ast(JavaLSPContext *ctx, CBMTypeRegistry
|
||||
class_qn = cbm_arena_strdup(ctx->arena, cname);
|
||||
}
|
||||
|
||||
TSNode body = ts_node_child_by_field_name(class_node, "body", 4);
|
||||
TSNode body = java_type_declaration_body(class_node);
|
||||
if (ts_node_is_null(body))
|
||||
return;
|
||||
/* Snapshot enclosing-class stack so java_parse_type_node sees inner-class
|
||||
@@ -3595,20 +3727,10 @@ void cbm_run_java_lsp(CBMArena *arena, CBMFileResult *result, const char *source
|
||||
|
||||
/* ── Cross-file LSP ───────────────────────────────────────────────── */
|
||||
|
||||
void cbm_run_java_lsp_cross(CBMArena *arena, const char *source, int source_len,
|
||||
const char *module_qn, CBMLSPDef *defs, int def_count,
|
||||
const char **import_names, const char **import_qns, int import_count,
|
||||
TSTree *cached_tree, CBMResolvedCallArray *out) {
|
||||
if (!arena || !source)
|
||||
return;
|
||||
|
||||
CBMTypeRegistry reg;
|
||||
cbm_registry_init(®, arena);
|
||||
cbm_java_stdlib_register(®, arena);
|
||||
|
||||
/* Register cross-file defs. */
|
||||
void cbm_java_register_lsp_defs(CBMArena *arena, CBMTypeRegistry *reg, const CBMLSPDef *defs,
|
||||
int def_count) {
|
||||
for (int i = 0; i < def_count; i++) {
|
||||
CBMLSPDef *d = &defs[i];
|
||||
const CBMLSPDef *d = &defs[i];
|
||||
if (!d->qualified_name || !d->short_name || !d->label)
|
||||
continue;
|
||||
if (strcmp(d->label, "Class") == 0 || strcmp(d->label, "Interface") == 0 ||
|
||||
@@ -3642,7 +3764,7 @@ void cbm_run_java_lsp_cross(CBMArena *arena, const char *source, int source_len,
|
||||
emb[idx] = NULL;
|
||||
rt.embedded_types = emb;
|
||||
}
|
||||
cbm_registry_add_type(®, rt);
|
||||
cbm_registry_add_type(reg, rt);
|
||||
} else if (strcmp(d->label, "Method") == 0 || strcmp(d->label, "Function") == 0 ||
|
||||
strcmp(d->label, "Constructor") == 0) {
|
||||
CBMRegisteredFunc rf;
|
||||
@@ -3651,6 +3773,14 @@ void cbm_run_java_lsp_cross(CBMArena *arena, const char *source, int source_len,
|
||||
rf.short_name = d->short_name;
|
||||
rf.min_params = -1;
|
||||
rf.receiver_type = d->receiver_type;
|
||||
JavaSignatureParseContext parse_ctx = {
|
||||
.parent_class = d->receiver_type,
|
||||
.module_qn = d->def_module_qn,
|
||||
.registry = reg,
|
||||
};
|
||||
const CBMType **ptypes = cbm_type_materialize_signature_params(
|
||||
arena, d->signature_param_types, d->signature_param_count,
|
||||
java_signature_parse_type, &parse_ctx);
|
||||
/* Build single-return signature from return_types text. */
|
||||
const CBMType *ret = NULL;
|
||||
if (d->return_types && d->return_types[0]) {
|
||||
@@ -3670,10 +3800,25 @@ void cbm_run_java_lsp_cross(CBMArena *arena, const char *source, int source_len,
|
||||
rtypes[0] = ret;
|
||||
rtypes[1] = NULL;
|
||||
}
|
||||
rf.signature = cbm_type_func(arena, NULL, NULL, rtypes);
|
||||
cbm_registry_add_func(®, rf);
|
||||
rf.signature = cbm_type_func(arena, NULL, ptypes, rtypes);
|
||||
cbm_registry_add_func(reg, rf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void cbm_run_java_lsp_cross(CBMArena *arena, const char *source, int source_len,
|
||||
const char *module_qn, CBMLSPDef *defs, int def_count,
|
||||
const char **import_names, const char **import_qns, int import_count,
|
||||
TSTree *cached_tree, CBMResolvedCallArray *out) {
|
||||
if (!arena || !source)
|
||||
return;
|
||||
|
||||
CBMTypeRegistry reg;
|
||||
cbm_registry_init(®, arena);
|
||||
cbm_java_stdlib_register(®, arena);
|
||||
|
||||
/* Register cross-file defs. */
|
||||
cbm_java_register_lsp_defs(arena, ®, defs, def_count);
|
||||
|
||||
/* Build the hash indexes: without this every lookup_type/func/method in
|
||||
* the walk below is a LINEAR scan over the whole cross registry —
|
||||
|
||||
@@ -139,6 +139,9 @@ void cbm_run_java_lsp(CBMArena *arena, CBMFileResult *result, const char *source
|
||||
|
||||
/* Cross-file LSP: build registry from defs + stdlib, re-parse if needed,
|
||||
* walk and resolve. defs include both local + cross-file definitions. */
|
||||
void cbm_java_register_lsp_defs(CBMArena *arena, CBMTypeRegistry *reg, const CBMLSPDef *defs,
|
||||
int def_count);
|
||||
|
||||
void cbm_run_java_lsp_cross(CBMArena *arena, const char *source, int source_len,
|
||||
const char *module_qn, CBMLSPDef *defs, int def_count,
|
||||
const char **import_names, const char **import_qns, int import_count,
|
||||
|
||||
+1145
-216
File diff suppressed because it is too large
Load Diff
@@ -65,6 +65,14 @@ typedef enum {
|
||||
CBM_KT_USE_WILDCARD, /* import a.b.* — local_name is a.b prefix */
|
||||
} CBMKotlinUseKind;
|
||||
|
||||
typedef struct {
|
||||
const CBMScope *scope; /* exact lexical declaration frame, NULL for members */
|
||||
const char *owner_qn; /* class/object QN for a member, NULL for lexical values */
|
||||
const char *name;
|
||||
const char *getter_qn;
|
||||
const char *setter_qn;
|
||||
} CBMKotlinDelegateBinding;
|
||||
|
||||
/* KotlinLSPContext — per-file state for Kotlin call resolution. */
|
||||
typedef struct KotlinLSPContext {
|
||||
CBMArena *arena;
|
||||
@@ -107,6 +115,14 @@ typedef struct KotlinLSPContext {
|
||||
/* Output: resolved calls accumulate here. */
|
||||
CBMResolvedCallArray *resolved_calls;
|
||||
|
||||
/* Access-level delegated-property semantics. The declaration records the
|
||||
* proven getValue/setValue targets; actual reads/writes inject exact call
|
||||
* carriers and resolutions at their source occurrences. */
|
||||
CBMCallArray *synthetic_calls;
|
||||
CBMKotlinDelegateBinding *delegate_bindings;
|
||||
int delegate_count;
|
||||
int delegate_cap;
|
||||
|
||||
/* Recursion guard for kotlin_eval_expr_type. */
|
||||
int eval_depth;
|
||||
|
||||
@@ -167,6 +183,9 @@ void cbm_run_kotlin_lsp(CBMArena *arena, CBMFileResult *result, const char *sour
|
||||
* Mirrors cbm_run_java_lsp_cross. `defs` carries the graph QNs of every
|
||||
* project definition so a bare top-level call in file B resolves to the
|
||||
* definition node living in file A. Output is appended to `out`. */
|
||||
void cbm_kotlin_register_lsp_defs(CBMArena *arena, CBMTypeRegistry *reg, const CBMLSPDef *defs,
|
||||
int def_count);
|
||||
|
||||
void cbm_run_kotlin_lsp_cross(CBMArena *arena, const char *source, int source_len,
|
||||
const char *module_qn, CBMLSPDef *defs, int def_count,
|
||||
const char **import_names, const char **import_qns, int import_count,
|
||||
|
||||
+160
-8
@@ -85,7 +85,8 @@ static const CBMType *perl_eval_function_call_type(PerlLSPContext *ctx, TSNode n
|
||||
static const CBMType *perl_eval_method_call_type(PerlLSPContext *ctx, TSNode node);
|
||||
static const CBMType *perl_eval_new_type(PerlLSPContext *ctx, TSNode node);
|
||||
static void perl_emit_resolved(PerlLSPContext *ctx, const char *callee_qn, const char *strategy,
|
||||
float confidence);
|
||||
float confidence, TSNode site);
|
||||
static void perl_resolve_direct_coderef_arguments(PerlLSPContext *ctx, TSNode call);
|
||||
|
||||
/* ── helpers ────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -625,23 +626,168 @@ static const CBMType *perl_eval_method_call_type(PerlLSPContext *ctx, TSNode nod
|
||||
/* ── emit ───────────────────────────────────────────────────────── */
|
||||
|
||||
static void perl_emit_resolved(PerlLSPContext *ctx, const char *callee_qn, const char *strategy,
|
||||
float confidence) {
|
||||
float confidence, TSNode site) {
|
||||
if (!ctx->resolved_calls || !callee_qn || !ctx->enclosing_func_qn)
|
||||
return;
|
||||
CBMResolvedCall rc;
|
||||
CBMResolvedCall rc = {0};
|
||||
rc.caller_qn = ctx->enclosing_func_qn;
|
||||
rc.callee_qn = callee_qn;
|
||||
rc.strategy = strategy;
|
||||
rc.confidence = confidence;
|
||||
rc.reason = NULL;
|
||||
rc.kind = CBM_RESOLVED_INVOCATION;
|
||||
if (!ts_node_is_null(site)) {
|
||||
rc.site_start_byte = ts_node_start_byte(site);
|
||||
rc.site_end_byte = ts_node_end_byte(site);
|
||||
}
|
||||
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
|
||||
}
|
||||
|
||||
static void perl_emit_reference(PerlLSPContext *ctx, const char *callee_qn, TSNode site) {
|
||||
if (!ctx || !ctx->resolved_calls || !callee_qn || !ctx->enclosing_func_qn ||
|
||||
ts_node_is_null(site)) {
|
||||
return;
|
||||
}
|
||||
CBMResolvedCall rc = {0};
|
||||
rc.caller_qn = ctx->enclosing_func_qn;
|
||||
rc.callee_qn = callee_qn;
|
||||
rc.strategy = "perl_coderef";
|
||||
rc.confidence = PERL_CONF_LITERAL;
|
||||
rc.kind = CBM_RESOLVED_CALL_REFERENCE;
|
||||
rc.site_start_byte = ts_node_start_byte(site);
|
||||
rc.site_end_byte = ts_node_end_byte(site);
|
||||
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
|
||||
}
|
||||
|
||||
/* Accept only the exact coderef spelling represented by one direct
|
||||
* refgen_expression. Conditional/list/map expressions are not examined, so
|
||||
* their constituent \&names remain ordinary usages. */
|
||||
static char *perl_exact_coderef_name(PerlLSPContext *ctx, TSNode refgen) {
|
||||
if (ts_node_is_null(refgen) || strcmp(ts_node_type(refgen), "refgen_expression") != 0)
|
||||
return NULL;
|
||||
char *text = perl_node_text(ctx, refgen);
|
||||
if (!text)
|
||||
return NULL;
|
||||
const char *p = text;
|
||||
while (isspace((unsigned char)*p))
|
||||
p++;
|
||||
if (p[0] != '\\' || p[1] != '&')
|
||||
return NULL;
|
||||
p += 2;
|
||||
const char *start = p;
|
||||
if (!(isalpha((unsigned char)*p) || *p == '_'))
|
||||
return NULL;
|
||||
while (isalnum((unsigned char)*p) || *p == '_' || *p == ':')
|
||||
p++;
|
||||
const char *end = p;
|
||||
while (isspace((unsigned char)*p))
|
||||
p++;
|
||||
if (*p != '\0' || end == start)
|
||||
return NULL;
|
||||
return cbm_arena_strndup(ctx->arena, start, (size_t)(end - start));
|
||||
}
|
||||
|
||||
static const CBMRegisteredFunc *perl_resolve_coderef_target(PerlLSPContext *ctx, char *name) {
|
||||
if (!ctx || !name || !name[0])
|
||||
return NULL;
|
||||
char *colons = NULL;
|
||||
for (char *p = strstr(name, "::"); p; p = strstr(p + 2, "::"))
|
||||
colons = p;
|
||||
if (colons) {
|
||||
char *pkg = cbm_arena_strndup(ctx->arena, name, (size_t)(colons - name));
|
||||
const char *short_name = colons + 2;
|
||||
const CBMRegisteredFunc *f = perl_lookup_method(ctx, pkg, short_name);
|
||||
return f ? f : cbm_registry_lookup_symbol(ctx->registry, pkg, short_name);
|
||||
}
|
||||
const char *import_qn = perl_find_import(ctx, name);
|
||||
if (import_qn) {
|
||||
const CBMRegisteredFunc *f = cbm_registry_lookup_func(ctx->registry, import_qn);
|
||||
if (f)
|
||||
return f;
|
||||
}
|
||||
return cbm_registry_lookup_symbol(ctx->registry, ctx->module_qn, name);
|
||||
}
|
||||
|
||||
static void perl_resolve_one_coderef(PerlLSPContext *ctx, TSNode refgen) {
|
||||
char *name = perl_exact_coderef_name(ctx, refgen);
|
||||
const CBMRegisteredFunc *target = name ? perl_resolve_coderef_target(ctx, name) : NULL;
|
||||
if (target && target->qualified_name)
|
||||
perl_emit_reference(ctx, target->qualified_name, refgen);
|
||||
}
|
||||
|
||||
static void perl_resolve_direct_coderef_arguments(PerlLSPContext *ctx, TSNode call) {
|
||||
TSNode args = ts_node_child_by_field_name(call, "arguments", 9);
|
||||
if (ts_node_is_null(args))
|
||||
return;
|
||||
if (strcmp(ts_node_type(args), "refgen_expression") == 0) {
|
||||
perl_resolve_one_coderef(ctx, args);
|
||||
return;
|
||||
}
|
||||
/* Multiple arguments are exposed through a flat list_expression. Do not
|
||||
* recurse into any other wrapper: in particular, a conditional argument
|
||||
* may contain refgen descendants but does not select one exact target. */
|
||||
if (strcmp(ts_node_type(args), "list_expression") != 0)
|
||||
return;
|
||||
uint32_t count = ts_node_named_child_count(args);
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
TSNode arg = ts_node_named_child(args, i);
|
||||
if (strcmp(ts_node_type(arg), "refgen_expression") == 0)
|
||||
perl_resolve_one_coderef(ctx, arg);
|
||||
}
|
||||
}
|
||||
|
||||
static bool perl_is_direct_coderef_argument(TSNode refgen) {
|
||||
TSNode container = ts_node_parent(refgen);
|
||||
if (ts_node_is_null(container))
|
||||
return false;
|
||||
if (strcmp(ts_node_type(container), "list_expression") != 0)
|
||||
container = refgen;
|
||||
TSNode call = ts_node_parent(container);
|
||||
if (ts_node_is_null(call))
|
||||
return false;
|
||||
const char *kind = ts_node_type(call);
|
||||
if (strcmp(kind, "function_call_expression") != 0 &&
|
||||
strcmp(kind, "ambiguous_function_call_expression") != 0) {
|
||||
return false;
|
||||
}
|
||||
TSNode args = ts_node_child_by_field_name(call, "arguments", 9);
|
||||
return !ts_node_is_null(args) && ts_node_eq(args, container);
|
||||
}
|
||||
|
||||
static void perl_emit_non_direct_coderef_usage(PerlLSPContext *ctx, TSNode refgen) {
|
||||
if (!ctx || !ctx->usages || !ctx->enclosing_func_qn ||
|
||||
perl_is_direct_coderef_argument(refgen)) {
|
||||
return;
|
||||
}
|
||||
char *name = perl_exact_coderef_name(ctx, refgen);
|
||||
if (!name)
|
||||
return;
|
||||
uint32_t start = ts_node_start_byte(refgen);
|
||||
uint32_t end = ts_node_end_byte(refgen);
|
||||
for (int i = 0; i < ctx->usages->count; i++) {
|
||||
const CBMUsage *existing = &ctx->usages->items[i];
|
||||
if (existing->kind == CBM_USAGE_VALUE && existing->ref_name &&
|
||||
existing->enclosing_func_qn && strcmp(existing->ref_name, name) == 0 &&
|
||||
strcmp(existing->enclosing_func_qn, ctx->enclosing_func_qn) == 0 &&
|
||||
existing->site_start_byte == start && existing->site_end_byte == end) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
CBMUsage usage = {0};
|
||||
usage.ref_name = name;
|
||||
usage.enclosing_func_qn = ctx->enclosing_func_qn;
|
||||
usage.kind = CBM_USAGE_VALUE;
|
||||
usage.site_start_byte = start;
|
||||
usage.site_end_byte = end;
|
||||
cbm_usages_push(ctx->usages, ctx->arena, usage);
|
||||
}
|
||||
|
||||
/* ── call/method dispatch (emit edges) ──────────────────────────── */
|
||||
|
||||
/* Resolve a function/static call and emit an edge if it lands on a registered
|
||||
* sub. Bare func(), Exporter func(), and Package::func() static calls. */
|
||||
static void perl_resolve_function_call(PerlLSPContext *ctx, TSNode call) {
|
||||
perl_resolve_direct_coderef_arguments(ctx, call);
|
||||
TSNode fn = ts_node_child_by_field_name(call, "function", 8);
|
||||
if (ts_node_is_null(fn))
|
||||
return;
|
||||
@@ -669,7 +815,7 @@ static void perl_resolve_function_call(PerlLSPContext *ctx, TSNode call) {
|
||||
if (!f)
|
||||
f = cbm_registry_lookup_symbol(ctx->registry, pkg, shortn);
|
||||
if (f) {
|
||||
perl_emit_resolved(ctx, f->qualified_name, "perl_static_call", PERL_CONF_LITERAL);
|
||||
perl_emit_resolved(ctx, f->qualified_name, "perl_static_call", PERL_CONF_LITERAL, call);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
@@ -678,13 +824,14 @@ static void perl_resolve_function_call(PerlLSPContext *ctx, TSNode call) {
|
||||
f = cbm_registry_lookup_func(ctx->registry, imp);
|
||||
if (f) {
|
||||
perl_emit_resolved(ctx, f->qualified_name, "perl_imported_function",
|
||||
PERL_CONF_LITERAL);
|
||||
PERL_CONF_LITERAL, call);
|
||||
return;
|
||||
}
|
||||
}
|
||||
f = cbm_registry_lookup_symbol(ctx->registry, ctx->module_qn, name);
|
||||
if (f) {
|
||||
perl_emit_resolved(ctx, f->qualified_name, "perl_function_local", PERL_CONF_LITERAL);
|
||||
perl_emit_resolved(ctx, f->qualified_name, "perl_function_local", PERL_CONF_LITERAL,
|
||||
call);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -718,7 +865,8 @@ static void perl_resolve_method_call(PerlLSPContext *ctx, TSNode call) {
|
||||
return;
|
||||
const CBMRegisteredFunc *sf = perl_lookup_method(ctx, parent_qn, super_method);
|
||||
if (sf)
|
||||
perl_emit_resolved(ctx, sf->qualified_name, "perl_method_super", PERL_CONF_LITERAL);
|
||||
perl_emit_resolved(ctx, sf->qualified_name, "perl_method_super", PERL_CONF_LITERAL,
|
||||
call);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -747,7 +895,7 @@ static void perl_resolve_method_call(PerlLSPContext *ctx, TSNode call) {
|
||||
const char *strat = (f->receiver_type && strcmp(f->receiver_type, class_qn) == 0)
|
||||
? strategy
|
||||
: "perl_method_inherited";
|
||||
perl_emit_resolved(ctx, f->qualified_name, strat, PERL_CONF_LITERAL);
|
||||
perl_emit_resolved(ctx, f->qualified_name, strat, PERL_CONF_LITERAL, call);
|
||||
return;
|
||||
}
|
||||
/* Receiver typed but method not found in the indexed inheritance chain.
|
||||
@@ -823,6 +971,9 @@ static void perl_resolve_calls_in_node_inner(PerlLSPContext *ctx, TSNode node) {
|
||||
perl_process_assignment(ctx, assign);
|
||||
}
|
||||
|
||||
if (strcmp(k, "refgen_expression") == 0)
|
||||
perl_emit_non_direct_coderef_usage(ctx, node);
|
||||
|
||||
/* Call-resolution dispatch. */
|
||||
if (strcmp(k, "function_call_expression") == 0 ||
|
||||
strcmp(k, "ambiguous_function_call_expression") == 0) {
|
||||
@@ -1529,6 +1680,7 @@ void cbm_run_perl_lsp(CBMArena *arena, CBMFileResult *result, const char *source
|
||||
* finished (const) registry. */
|
||||
PerlLSPContext ctx;
|
||||
perl_lsp_init(&ctx, arena, source, source_len, ®, module_qn, &result->resolved_calls);
|
||||
ctx.usages = &result->usages;
|
||||
|
||||
ctx.current_package_qn = "";
|
||||
ctx.enclosing_package_qn = "";
|
||||
|
||||
@@ -61,6 +61,7 @@ typedef struct {
|
||||
|
||||
/* Output: resolved calls accumulate here. */
|
||||
CBMResolvedCallArray *resolved_calls;
|
||||
CBMUsageArray *usages;
|
||||
|
||||
/* Recursion guard for perl_eval_expr_type. */
|
||||
int eval_depth;
|
||||
|
||||
+326
-74
@@ -825,6 +825,33 @@ static char *fetch_leading_phpdoc(PHPLSPContext *ctx, TSNode node) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* PHP 8.1 first-class callable syntax reuses call-expression nodes but its
|
||||
* argument list contains only a `variadic_placeholder`: `target(...)`,
|
||||
* `$obj->method(...)`, or `Type::method(...)`. It creates a Closure; it does
|
||||
* not invoke the target. */
|
||||
static bool php_is_first_class_callable(TSNode node) {
|
||||
TSNode args = ts_node_child_by_field_name(node, "arguments", 9);
|
||||
if (ts_node_is_null(args))
|
||||
return false;
|
||||
|
||||
bool saw_placeholder = false;
|
||||
int semantic_children = 0;
|
||||
uint32_t child_count = ts_node_child_count(args);
|
||||
for (uint32_t i = 0; i < child_count; i++) {
|
||||
TSNode child = ts_node_child(args, i);
|
||||
if (ts_node_is_null(child) || !ts_node_is_named(child))
|
||||
continue;
|
||||
const char *kind = ts_node_type(child);
|
||||
if (strcmp(kind, "comment") == 0)
|
||||
continue;
|
||||
semantic_children++;
|
||||
if (strcmp(kind, "variadic_placeholder") != 0)
|
||||
return false;
|
||||
saw_placeholder = true;
|
||||
}
|
||||
return saw_placeholder && semantic_children == 1;
|
||||
}
|
||||
|
||||
/* ── expression evaluation ──────────────────────────────────────── */
|
||||
|
||||
/* Returns the type of a tree-sitter PHP expression node.
|
||||
@@ -868,6 +895,12 @@ const CBMType *php_eval_expr_type(PHPLSPContext *ctx, TSNode node) {
|
||||
}
|
||||
} else if (strcmp(kind, "object_creation_expression") == 0) {
|
||||
result = eval_object_creation_type(ctx, node);
|
||||
} else if ((strcmp(kind, "function_call_expression") == 0 ||
|
||||
strcmp(kind, "member_call_expression") == 0 ||
|
||||
strcmp(kind, "nullsafe_member_call_expression") == 0 ||
|
||||
strcmp(kind, "scoped_call_expression") == 0) &&
|
||||
php_is_first_class_callable(node)) {
|
||||
result = cbm_type_named(ctx->arena, "Closure");
|
||||
} else if (strcmp(kind, "member_call_expression") == 0 ||
|
||||
strcmp(kind, "nullsafe_member_call_expression") == 0 ||
|
||||
strcmp(kind, "scoped_call_expression") == 0) {
|
||||
@@ -1250,28 +1283,137 @@ static const CBMType *eval_member_call_type(PHPLSPContext *ctx, TSNode call_node
|
||||
|
||||
/* ── emit ───────────────────────────────────────────────────────── */
|
||||
|
||||
static void emit_resolved_reason(PHPLSPContext *ctx, const char *callee_qn, const char *strategy,
|
||||
float confidence, const char *reason) {
|
||||
static void emit_resolved_reason_kind(PHPLSPContext *ctx, const char *callee_qn,
|
||||
const char *strategy, float confidence, const char *reason,
|
||||
CBMResolvedKind kind) {
|
||||
if (!ctx->resolved_calls || !callee_qn || !ctx->enclosing_func_qn)
|
||||
return;
|
||||
CBMResolvedCall rc;
|
||||
CBMResolvedCall rc = {0};
|
||||
rc.caller_qn = ctx->enclosing_func_qn;
|
||||
rc.callee_qn = callee_qn;
|
||||
rc.strategy = strategy;
|
||||
rc.confidence = confidence;
|
||||
rc.reason = reason;
|
||||
rc.kind = kind;
|
||||
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
|
||||
}
|
||||
|
||||
static void emit_resolved(PHPLSPContext *ctx, const char *callee_qn, const char *strategy,
|
||||
float confidence) {
|
||||
emit_resolved_reason(ctx, callee_qn, strategy, confidence, NULL);
|
||||
static void emit_resolved_kind(PHPLSPContext *ctx, const char *callee_qn, const char *strategy,
|
||||
float confidence, CBMResolvedKind kind) {
|
||||
emit_resolved_reason_kind(ctx, callee_qn, strategy, confidence, NULL, kind);
|
||||
}
|
||||
|
||||
static bool php_dynamic_reference_expr(PHPLSPContext *ctx, TSNode expr) {
|
||||
if (ts_node_is_null(expr)) {
|
||||
return false;
|
||||
}
|
||||
const char *kind = ts_node_type(expr);
|
||||
if (strstr(kind, "subscript") || strstr(kind, "element_access") ||
|
||||
strstr(kind, "index_expression") || strstr(kind, "computed")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static const char *const terminal_fields[] = {"property", "field", "attribute",
|
||||
"name", "method", NULL};
|
||||
for (int i = 0; terminal_fields[i]; i++) {
|
||||
TSNode terminal = ts_node_child_by_field_name(expr, terminal_fields[i],
|
||||
(uint32_t)strlen(terminal_fields[i]));
|
||||
if (ts_node_is_null(terminal)) {
|
||||
continue;
|
||||
}
|
||||
uint32_t start = ts_node_start_byte(terminal);
|
||||
uint32_t expr_start = ts_node_start_byte(expr);
|
||||
while (start > expr_start && isspace((unsigned char)ctx->source[start - 1])) {
|
||||
start--;
|
||||
}
|
||||
return start > expr_start && ctx->source[start - 1] == '[';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool php_callable_reference_has_dynamic_member_name(PHPLSPContext *ctx, TSNode call) {
|
||||
const char *call_kind = ts_node_type(call);
|
||||
if (strcmp(call_kind, "member_call_expression") != 0 &&
|
||||
strcmp(call_kind, "nullsafe_member_call_expression") != 0 &&
|
||||
strcmp(call_kind, "scoped_call_expression") != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
TSNode name = ts_node_child_by_field_name(call, "name", 4);
|
||||
if (ts_node_is_null(name)) {
|
||||
return false;
|
||||
}
|
||||
const char *name_kind = ts_node_type(name);
|
||||
return strcmp(name_kind, "variable_name") == 0 ||
|
||||
strcmp(name_kind, "dynamic_variable_name") == 0 || php_dynamic_reference_expr(ctx, name);
|
||||
}
|
||||
|
||||
/* Mirror extract_calls.c's terminal_callee_leaf policy so the PHP LSP and
|
||||
* USAGE passes attach the same exact token occurrence to a first-class
|
||||
* callable, including qualified free-function references. */
|
||||
static TSNode php_reference_callee_leaf(PHPLSPContext *ctx, TSNode expr) {
|
||||
if (ts_node_is_null(expr) || php_dynamic_reference_expr(ctx, expr)) {
|
||||
return (TSNode){0};
|
||||
}
|
||||
|
||||
static const char *const terminal_fields[] = {"property", "field", "attribute",
|
||||
"method", "name", NULL};
|
||||
for (int i = 0; terminal_fields[i]; i++) {
|
||||
TSNode terminal = ts_node_child_by_field_name(expr, terminal_fields[i],
|
||||
(uint32_t)strlen(terminal_fields[i]));
|
||||
if (!ts_node_is_null(terminal)) {
|
||||
return php_reference_callee_leaf(ctx, terminal);
|
||||
}
|
||||
}
|
||||
|
||||
const char *kind = ts_node_type(expr);
|
||||
if (strcmp(kind, "identifier") == 0 || strcmp(kind, "simple_identifier") == 0 ||
|
||||
strcmp(kind, "type_identifier") == 0 || strcmp(kind, "field_identifier") == 0 ||
|
||||
strcmp(kind, "property_identifier") == 0 || strcmp(kind, "name") == 0 ||
|
||||
strcmp(kind, "variable_name") == 0 || strcmp(kind, "attribute") == 0 ||
|
||||
strcmp(kind, "symbol") == 0 || strcmp(kind, "sym_lit") == 0 ||
|
||||
strcmp(kind, "user_symbol") == 0) {
|
||||
return expr;
|
||||
}
|
||||
|
||||
uint32_t count = ts_node_named_child_count(expr);
|
||||
if (count == 0) {
|
||||
return (TSNode){0};
|
||||
}
|
||||
bool terminal_is_last = strstr(kind, "qualified") || strstr(kind, "scoped") ||
|
||||
strstr(kind, "member") || strstr(kind, "selector") ||
|
||||
strstr(kind, "navigation") || strstr(kind, "field_expression");
|
||||
return php_reference_callee_leaf(ctx,
|
||||
ts_node_named_child(expr, terminal_is_last ? count - 1 : 0));
|
||||
}
|
||||
|
||||
static TSNode php_callable_reference_leaf(PHPLSPContext *ctx, TSNode call, const char *kind) {
|
||||
if (php_callable_reference_has_dynamic_member_name(ctx, call)) {
|
||||
return (TSNode){0};
|
||||
}
|
||||
TSNode expr = {0};
|
||||
if (strcmp(kind, "function_call_expression") == 0) {
|
||||
expr = ts_node_child_by_field_name(call, "function", 8);
|
||||
} else {
|
||||
expr = ts_node_child_by_field_name(call, "name", 4);
|
||||
}
|
||||
return php_reference_callee_leaf(ctx, expr);
|
||||
}
|
||||
|
||||
static void php_stamp_resolved_site(PHPLSPContext *ctx, int first, TSNode site) {
|
||||
if (!ctx->resolved_calls || ts_node_is_null(site) || first < 0) {
|
||||
return;
|
||||
}
|
||||
for (int i = first; i < ctx->resolved_calls->count; i++) {
|
||||
ctx->resolved_calls->items[i].site_start_byte = ts_node_start_byte(site);
|
||||
ctx->resolved_calls->items[i].site_end_byte = ts_node_end_byte(site);
|
||||
}
|
||||
}
|
||||
|
||||
static void emit_unresolved(PHPLSPContext *ctx, const char *expr_text, const char *reason) {
|
||||
if (!ctx->resolved_calls || !ctx->enclosing_func_qn)
|
||||
return;
|
||||
CBMResolvedCall rc;
|
||||
CBMResolvedCall rc = {0};
|
||||
rc.caller_qn = ctx->enclosing_func_qn;
|
||||
rc.callee_qn = expr_text ? expr_text : "?";
|
||||
rc.strategy = "lsp_unresolved";
|
||||
@@ -1449,7 +1591,7 @@ static void process_catch(PHPLSPContext *ctx, TSNode node) {
|
||||
|
||||
/* ── call resolution ────────────────────────────────────────────── */
|
||||
|
||||
static void resolve_function_call(PHPLSPContext *ctx, TSNode call) {
|
||||
static void resolve_function_call(PHPLSPContext *ctx, TSNode call, CBMResolvedKind kind) {
|
||||
TSNode fn = ts_node_child_by_field_name(call, "function", 8);
|
||||
if (ts_node_is_null(fn))
|
||||
return;
|
||||
@@ -1457,12 +1599,34 @@ static void resolve_function_call(PHPLSPContext *ctx, TSNode call) {
|
||||
if (!name)
|
||||
return;
|
||||
|
||||
/* In PHP, `$handler(...)` is syntactically a function-call expression but
|
||||
* a typed object value dispatches to its materialized `__invoke` method.
|
||||
* This is precise only for a first-class callable and a receiver type whose
|
||||
* exact method exists in the registry; otherwise leave it unresolved so
|
||||
* the usage pipeline can retain ordinary value semantics. */
|
||||
if (kind == CBM_RESOLVED_CALL_REFERENCE && strcmp(ts_node_type(fn), "variable_name") == 0) {
|
||||
const CBMType *receiver = php_eval_expr_type(ctx, fn);
|
||||
const char *class_qn = NULL;
|
||||
if (receiver && receiver->kind == CBM_TYPE_NAMED) {
|
||||
class_qn = receiver->data.named.qualified_name;
|
||||
} else if (receiver && receiver->kind == CBM_TYPE_TEMPLATE) {
|
||||
class_qn = receiver->data.template_type.template_name;
|
||||
}
|
||||
const CBMRegisteredFunc *invoke =
|
||||
class_qn ? php_lookup_method(ctx, class_qn, "__invoke") : NULL;
|
||||
if (invoke && invoke->qualified_name) {
|
||||
emit_resolved_reason_kind(ctx, invoke->qualified_name, "php_invokable_reference", 0.95f,
|
||||
name, kind);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* `use function Foo\\bar;` mapping. */
|
||||
const char *target = find_use(ctx, name, CBM_PHP_USE_FUNCTION);
|
||||
if (target) {
|
||||
const CBMRegisteredFunc *f = cbm_registry_lookup_func(ctx->registry, target);
|
||||
if (f) {
|
||||
emit_resolved(ctx, f->qualified_name, "php_function_namespaced", 0.95f);
|
||||
emit_resolved_kind(ctx, f->qualified_name, "php_function_namespaced", 0.95f, kind);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1472,7 +1636,7 @@ static void resolve_function_call(PHPLSPContext *ctx, TSNode call) {
|
||||
const char *ns_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->current_namespace_qn, name);
|
||||
const CBMRegisteredFunc *f = cbm_registry_lookup_func(ctx->registry, ns_qn);
|
||||
if (f) {
|
||||
emit_resolved(ctx, f->qualified_name, "php_function_namespaced", 0.95f);
|
||||
emit_resolved_kind(ctx, f->qualified_name, "php_function_namespaced", 0.95f, kind);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1504,7 +1668,7 @@ static void resolve_function_call(PHPLSPContext *ctx, TSNode call) {
|
||||
}
|
||||
}
|
||||
if (best) {
|
||||
emit_resolved(ctx, best->qualified_name, "php_function_global_fallback", 0.70f);
|
||||
emit_resolved_kind(ctx, best->qualified_name, "php_function_global_fallback", 0.70f, kind);
|
||||
return;
|
||||
}
|
||||
/* Unresolved — but DO NOT emit unresolved noise here; the unified
|
||||
@@ -1513,11 +1677,15 @@ static void resolve_function_call(PHPLSPContext *ctx, TSNode call) {
|
||||
(void)emit_unresolved;
|
||||
}
|
||||
|
||||
static void resolve_member_call(PHPLSPContext *ctx, TSNode call) {
|
||||
static void resolve_member_call(PHPLSPContext *ctx, TSNode call, CBMResolvedKind kind) {
|
||||
TSNode obj = ts_node_child_by_field_name(call, "object", 6);
|
||||
TSNode name_node = ts_node_child_by_field_name(call, "name", 4);
|
||||
if (ts_node_is_null(name_node))
|
||||
return;
|
||||
if (kind == CBM_RESOLVED_CALL_REFERENCE &&
|
||||
php_callable_reference_has_dynamic_member_name(ctx, call)) {
|
||||
return;
|
||||
}
|
||||
char *method_name = php_node_text(ctx, name_node);
|
||||
if (!method_name)
|
||||
return;
|
||||
@@ -1541,7 +1709,7 @@ static void resolve_member_call(PHPLSPContext *ctx, TSNode call) {
|
||||
const char *strategy = (f->receiver_type && strcmp(f->receiver_type, class_qn) == 0)
|
||||
? "php_method_typed"
|
||||
: "php_method_inherited";
|
||||
emit_resolved(ctx, f->qualified_name, strategy, 0.95f);
|
||||
emit_resolved_kind(ctx, f->qualified_name, strategy, 0.95f, kind);
|
||||
return;
|
||||
}
|
||||
/* Receiver known but method missing — magic __call? The call dispatches to
|
||||
@@ -1550,8 +1718,8 @@ static void resolve_member_call(PHPLSPContext *ctx, TSNode call) {
|
||||
* so stash it in reason for the join (lsp_resolve.h, php_method_dynamic).
|
||||
* Emit above the join's confidence floor — dispatch to __call is certain. */
|
||||
if (class_has_magic_call(ctx, class_qn, false)) {
|
||||
emit_resolved_reason(ctx, cbm_arena_sprintf(ctx->arena, "%s.__call", class_qn),
|
||||
"php_method_dynamic", 0.85f, method_name);
|
||||
emit_resolved_reason_kind(ctx, cbm_arena_sprintf(ctx->arena, "%s.__call", class_qn),
|
||||
"php_method_dynamic", 0.85f, method_name, kind);
|
||||
return;
|
||||
}
|
||||
/* Receiver known but class not in registry (e.g. vendor type not indexed,
|
||||
@@ -1559,15 +1727,19 @@ static void resolve_member_call(PHPLSPContext *ctx, TSNode call) {
|
||||
* at "<class_qn>.<method>" so the pipeline bridge can BLOCK the unified
|
||||
* extractor's likely-incorrect short-name fallback. The bridge filters
|
||||
* unknown targets and yields no edge — better than a wrong edge. */
|
||||
emit_resolved(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", class_qn, method_name),
|
||||
"php_method_typed_unindexed", 0.55f);
|
||||
emit_resolved_kind(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", class_qn, method_name),
|
||||
"php_method_typed_unindexed", 0.55f, kind);
|
||||
}
|
||||
|
||||
static void resolve_static_call(PHPLSPContext *ctx, TSNode call) {
|
||||
static void resolve_static_call(PHPLSPContext *ctx, TSNode call, CBMResolvedKind kind) {
|
||||
TSNode scope = ts_node_child_by_field_name(call, "scope", 5);
|
||||
TSNode name_node = ts_node_child_by_field_name(call, "name", 4);
|
||||
if (ts_node_is_null(name_node))
|
||||
return;
|
||||
if (kind == CBM_RESOLVED_CALL_REFERENCE &&
|
||||
php_callable_reference_has_dynamic_member_name(ctx, call)) {
|
||||
return;
|
||||
}
|
||||
char *method_name = php_node_text(ctx, name_node);
|
||||
if (!method_name)
|
||||
return;
|
||||
@@ -1593,20 +1765,30 @@ static void resolve_static_call(PHPLSPContext *ctx, TSNode call) {
|
||||
|
||||
const CBMRegisteredFunc *f = php_lookup_method(ctx, class_qn, method_name);
|
||||
if (f) {
|
||||
emit_resolved(ctx, f->qualified_name, strategy, 0.95f);
|
||||
emit_resolved_kind(ctx, f->qualified_name, strategy, 0.95f, kind);
|
||||
return;
|
||||
}
|
||||
/* A first-class reference to an absent static method has an exact target
|
||||
* only when a materialized `__callStatic` exists. Preserve the source name
|
||||
* as the occurrence-join reason, but point at the real method node. Normal
|
||||
* facade invocations retain their existing low-confidence behavior. */
|
||||
const CBMRegisteredFunc *magic_static = php_lookup_method(ctx, class_qn, "__callStatic");
|
||||
if (magic_static && kind == CBM_RESOLVED_CALL_REFERENCE) {
|
||||
emit_resolved_reason_kind(ctx, magic_static->qualified_name, "php_static_magic_reference",
|
||||
0.95f, method_name, kind);
|
||||
return;
|
||||
}
|
||||
/* Facade detection: class has __callStatic in its chain. */
|
||||
if (class_has_magic_call(ctx, class_qn, true)) {
|
||||
emit_resolved(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", class_qn, method_name),
|
||||
"php_dynamic_unresolved", 0.10f);
|
||||
if (magic_static) {
|
||||
emit_resolved_kind(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", class_qn, method_name),
|
||||
"php_dynamic_unresolved", 0.10f, kind);
|
||||
return;
|
||||
}
|
||||
/* Class resolved (e.g., from a `use` clause), method unknown — emit
|
||||
* synthetic resolved call so the pipeline bridge can suppress the
|
||||
* unified extractor's name-fallback misroute. */
|
||||
emit_resolved(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", class_qn, method_name),
|
||||
"php_static_unindexed", 0.55f);
|
||||
emit_resolved_kind(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", class_qn, method_name),
|
||||
"php_static_unindexed", 0.55f, kind);
|
||||
}
|
||||
|
||||
/* ── type narrowing ─────────────────────────────────────────────────
|
||||
@@ -2158,14 +2340,32 @@ static void php_resolve_calls_in_node_inner(PHPLSPContext *ctx, TSNode node) {
|
||||
return; /* process_if_statement already walked body under narrowing */
|
||||
}
|
||||
|
||||
/* Call-resolution dispatch. */
|
||||
/* Call-resolution dispatch. A first-class callable has the same outer AST
|
||||
* node as an invocation, so carry the semantic kind alongside resolution
|
||||
* and skip invocation-only narrowing/binding effects. */
|
||||
bool is_callable_reference = php_is_first_class_callable(node);
|
||||
CBMResolvedKind resolved_kind =
|
||||
is_callable_reference ? CBM_RESOLVED_CALL_REFERENCE : CBM_RESOLVED_INVOCATION;
|
||||
if (strcmp(kind, "function_call_expression") == 0) {
|
||||
resolve_function_call(ctx, node);
|
||||
int first = ctx->resolved_calls ? ctx->resolved_calls->count : -1;
|
||||
resolve_function_call(ctx, node, resolved_kind);
|
||||
if (is_callable_reference) {
|
||||
php_stamp_resolved_site(ctx, first, php_callable_reference_leaf(ctx, node, kind));
|
||||
} else {
|
||||
php_stamp_resolved_site(ctx, first, node);
|
||||
}
|
||||
/* assert(...) in current scope narrows sequentially. */
|
||||
apply_assert_narrowing(ctx, node);
|
||||
if (!is_callable_reference)
|
||||
apply_assert_narrowing(ctx, node);
|
||||
} else if (strcmp(kind, "member_call_expression") == 0 ||
|
||||
strcmp(kind, "nullsafe_member_call_expression") == 0) {
|
||||
resolve_member_call(ctx, node);
|
||||
int first = ctx->resolved_calls ? ctx->resolved_calls->count : -1;
|
||||
resolve_member_call(ctx, node, resolved_kind);
|
||||
if (is_callable_reference) {
|
||||
php_stamp_resolved_site(ctx, first, php_callable_reference_leaf(ctx, node, kind));
|
||||
} else {
|
||||
php_stamp_resolved_site(ctx, first, node);
|
||||
}
|
||||
/* Closure binding via $f->bindTo($obj). When $f is an inline
|
||||
* closure literal AND the bindTo call's first arg is a typed
|
||||
* variable, walk the closure body with $this rebound. We only
|
||||
@@ -2174,7 +2374,7 @@ static void php_resolve_calls_in_node_inner(PHPLSPContext *ctx, TSNode node) {
|
||||
* value-typing is Phase 6+ infrastructure). */
|
||||
TSNode obj = ts_node_child_by_field_name(node, "object", 6);
|
||||
TSNode mname = ts_node_child_by_field_name(node, "name", 4);
|
||||
if (!ts_node_is_null(obj) && !ts_node_is_null(mname)) {
|
||||
if (!is_callable_reference && !ts_node_is_null(obj) && !ts_node_is_null(mname)) {
|
||||
char *mn = php_node_text(ctx, mname);
|
||||
const char *ok = ts_node_type(obj);
|
||||
if (mn && strcmp(mn, "bindTo") == 0 &&
|
||||
@@ -2212,13 +2412,19 @@ static void php_resolve_calls_in_node_inner(PHPLSPContext *ctx, TSNode node) {
|
||||
}
|
||||
}
|
||||
} else if (strcmp(kind, "scoped_call_expression") == 0) {
|
||||
resolve_static_call(ctx, node);
|
||||
int first = ctx->resolved_calls ? ctx->resolved_calls->count : -1;
|
||||
resolve_static_call(ctx, node, resolved_kind);
|
||||
if (is_callable_reference) {
|
||||
php_stamp_resolved_site(ctx, first, php_callable_reference_leaf(ctx, node, kind));
|
||||
} else {
|
||||
php_stamp_resolved_site(ctx, first, node);
|
||||
}
|
||||
/* Closure binding via Closure::bind($f, $obj). Same shape as
|
||||
* bindTo: rebind $this when the first arg is a closure literal
|
||||
* and the second arg has a known class. */
|
||||
TSNode scope = ts_node_child_by_field_name(node, "scope", 5);
|
||||
TSNode mname2 = ts_node_child_by_field_name(node, "name", 4);
|
||||
if (!ts_node_is_null(scope) && !ts_node_is_null(mname2)) {
|
||||
if (!is_callable_reference && !ts_node_is_null(scope) && !ts_node_is_null(mname2)) {
|
||||
char *cls = php_node_text(ctx, scope);
|
||||
char *mn = php_node_text(ctx, mname2);
|
||||
bool is_closure_bind = mn && strcmp(mn, "bind") == 0 && cls &&
|
||||
@@ -2782,6 +2988,22 @@ static const CBMType *parse_return_type_text(CBMArena *arena, const char *text,
|
||||
return cbm_type_named(arena, php_ns_to_dot(arena, first));
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const char *module_qn;
|
||||
const char *namespace_qn;
|
||||
const char **use_local_names;
|
||||
const char **use_target_qns;
|
||||
int use_count;
|
||||
} PHPSignatureParseContext;
|
||||
|
||||
static const CBMType *php_signature_parse_type(CBMArena *arena, const char *text,
|
||||
void *parser_ctx) {
|
||||
const PHPSignatureParseContext *ctx = (const PHPSignatureParseContext *)parser_ctx;
|
||||
return parse_return_type_text(arena, text, ctx ? ctx->module_qn : NULL,
|
||||
ctx ? ctx->namespace_qn : NULL, ctx ? ctx->use_local_names : NULL,
|
||||
ctx ? ctx->use_target_qns : NULL, ctx ? ctx->use_count : 0);
|
||||
}
|
||||
|
||||
/* ── property type extraction ───────────────────────────────────────
|
||||
*
|
||||
* Walks the AST top-level for class/trait/interface bodies and registers
|
||||
@@ -3514,7 +3736,7 @@ static void flatten_trait_into_class(PHPLSPContext *ctx, CBMTypeRegistry *reg, c
|
||||
if (rets) {
|
||||
rets[0] = cbm_type_named(ctx->arena, class_qn);
|
||||
rets[1] = NULL;
|
||||
new_sig = cbm_type_func(ctx->arena, NULL, NULL, rets);
|
||||
new_sig = cbm_type_func_replace_returns(ctx->arena, src->signature, rets);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3815,7 +4037,8 @@ static void process_class_for_fields(PHPLSPContext *ctx, CBMTypeRegistry *reg, T
|
||||
if (rets) {
|
||||
rets[0] = parsed;
|
||||
rets[1] = NULL;
|
||||
rf->signature = cbm_type_func(ctx->arena, NULL, NULL, rets);
|
||||
rf->signature =
|
||||
cbm_type_func_replace_returns(ctx->arena, rf->signature, rets);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -3892,7 +4115,8 @@ static void process_class_for_fields(PHPLSPContext *ctx, CBMTypeRegistry *reg, T
|
||||
if (rets) {
|
||||
rets[0] = ret_t;
|
||||
rets[1] = NULL;
|
||||
rf->signature = cbm_type_func(ctx->arena, NULL, NULL, rets);
|
||||
rf->signature = cbm_type_func_replace_returns(
|
||||
ctx->arena, rf->signature, rets);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -3944,6 +4168,26 @@ static void php_lsp_collect_class_fields(PHPLSPContext *ctx, CBMTypeRegistry *re
|
||||
}
|
||||
}
|
||||
|
||||
void cbm_php_refine_lsp_registry(PHPLSPContext *ctx, CBMTypeRegistry *reg, TSNode root) {
|
||||
if (!ctx || !reg || ts_node_is_null(root))
|
||||
return;
|
||||
|
||||
php_class_field_table_t tab = {0};
|
||||
php_lsp_collect_class_fields(ctx, reg, root, &tab);
|
||||
for (int i = 0; i < tab.count; i++) {
|
||||
php_class_fields_t *fields = &tab.items[i];
|
||||
if (fields->count == 0)
|
||||
continue;
|
||||
for (int t = 0; t < reg->type_count; t++) {
|
||||
if (strcmp(reg->types[t].qualified_name, fields->class_qn) == 0) {
|
||||
reg->types[t].field_names = fields->field_names;
|
||||
reg->types[t].field_types = fields->field_types;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── entry: cbm_run_php_lsp ─────────────────────────────────────── */
|
||||
|
||||
void cbm_run_php_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len,
|
||||
@@ -4015,7 +4259,29 @@ void cbm_run_php_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
|
||||
if (strcmp(d->label, "Method") == 0 && d->parent_class) {
|
||||
rf.receiver_type = d->parent_class;
|
||||
}
|
||||
/* Build a minimal signature with just the return type. */
|
||||
PHPSignatureParseContext parse_ctx = {
|
||||
.module_qn = module_qn,
|
||||
.namespace_qn = NULL,
|
||||
.use_local_names = NULL,
|
||||
.use_target_qns = NULL,
|
||||
.use_count = 0,
|
||||
};
|
||||
const CBMType **param_types = NULL;
|
||||
if (d->signature_param_types || d->signature_param_count > 0) {
|
||||
param_types = cbm_type_materialize_signature_params(
|
||||
arena, d->signature_param_types, d->signature_param_count,
|
||||
php_signature_parse_type, &parse_ctx);
|
||||
} else if (!d->signature_param_types && d->signature_param_count == 0 &&
|
||||
d->param_types) {
|
||||
/* Legacy CBMDefinition callers have no counted carrier. Keep
|
||||
* their historical projection as a local-only fallback. */
|
||||
int legacy_count = 0;
|
||||
while (d->param_types[legacy_count])
|
||||
legacy_count++;
|
||||
param_types = cbm_type_materialize_signature_params(
|
||||
arena, d->param_types, legacy_count, php_signature_parse_type, &parse_ctx);
|
||||
}
|
||||
/* Build a signature carrying ordered params plus the return type. */
|
||||
const CBMType *ret_t =
|
||||
d->return_type
|
||||
? parse_return_type_text(arena, d->return_type, module_qn, NULL, NULL, NULL, 0)
|
||||
@@ -4026,7 +4292,7 @@ void cbm_run_php_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
|
||||
rets[0] = ret_t;
|
||||
rets[1] = NULL;
|
||||
}
|
||||
rf.signature = cbm_type_func(arena, NULL, NULL, rets);
|
||||
rf.signature = cbm_type_func(arena, NULL, param_types, rets);
|
||||
cbm_registry_add_func(®, rf);
|
||||
}
|
||||
}
|
||||
@@ -4040,7 +4306,6 @@ void cbm_run_php_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
|
||||
* file defs are registered but before the call walk so eval_expr_type
|
||||
* for $this->prop and $obj->prop sees the field types. */
|
||||
{
|
||||
php_class_field_table_t tab = {0};
|
||||
/* First pass to populate the use map (so type names in property
|
||||
* declarations resolve correctly). */
|
||||
/* Collect top-level children once (O(n)); ts_node_child(root,i) is O(i) → O(n²). */
|
||||
@@ -4055,25 +4320,10 @@ void cbm_run_php_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
|
||||
collect_use_declaration(&ctx, c);
|
||||
}
|
||||
}
|
||||
php_lsp_collect_class_fields(&ctx, ®, root, &tab);
|
||||
cbm_php_refine_lsp_registry(&ctx, ®, root);
|
||||
/* Reset namespace/use state — process_file will re-collect. */
|
||||
ctx.current_namespace_qn = "";
|
||||
ctx.use_count = 0;
|
||||
|
||||
for (int i = 0; i < tab.count; i++) {
|
||||
php_class_fields_t *f = &tab.items[i];
|
||||
if (f->count == 0)
|
||||
continue;
|
||||
/* Mutate the existing entry — registered types are stored by
|
||||
* value in reg.types[], find the index and update it. */
|
||||
for (int t = 0; t < reg.type_count; t++) {
|
||||
if (strcmp(reg.types[t].qualified_name, f->class_qn) == 0) {
|
||||
reg.types[t].field_names = f->field_names;
|
||||
reg.types[t].field_types = f->field_types;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
php_lsp_process_file(&ctx, root);
|
||||
@@ -4131,8 +4381,8 @@ static const char **php_split_pipe(CBMArena *arena, const char *text) {
|
||||
* functions. Return type strings are parsed via parse_return_type_text using
|
||||
* each def's def_module_qn so bare type names qualify against the module
|
||||
* where the def lives, not the importer's module. */
|
||||
static void php_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeRegistry *reg,
|
||||
CBMLSPDef *defs, int def_count) {
|
||||
void cbm_php_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeRegistry *reg,
|
||||
const CBMLSPDef *defs, int def_count) {
|
||||
/* Pass 1: types only. The method pass below probes the registry per
|
||||
* Method def (receiver auto-registration); doing that against an
|
||||
* unfinalized registry is a LINEAR scan over the growing type table —
|
||||
@@ -4141,7 +4391,7 @@ static void php_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeR
|
||||
* the hash, then register funcs/methods with O(1) lookups (post-finalize
|
||||
* stub additions stay visible via the registry's tail-scan). */
|
||||
for (int i = 0; i < def_count; i++) {
|
||||
CBMLSPDef *d = &defs[i];
|
||||
const CBMLSPDef *d = &defs[i];
|
||||
if (!d->qualified_name || !d->short_name || !d->label)
|
||||
continue;
|
||||
|
||||
@@ -4168,7 +4418,7 @@ static void php_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeR
|
||||
|
||||
/* Pass 2: functions and methods. */
|
||||
for (int i = 0; i < def_count; i++) {
|
||||
CBMLSPDef *d = &defs[i];
|
||||
const CBMLSPDef *d = &defs[i];
|
||||
if (!d->qualified_name || !d->short_name || !d->label)
|
||||
continue;
|
||||
|
||||
@@ -4178,6 +4428,18 @@ static void php_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeR
|
||||
rf.qualified_name = d->qualified_name; /* borrowed */
|
||||
rf.short_name = d->short_name;
|
||||
|
||||
const char *def_mod = d->def_module_qn ? d->def_module_qn : "";
|
||||
PHPSignatureParseContext parse_ctx = {
|
||||
.module_qn = def_mod,
|
||||
.namespace_qn = d->namespace_name,
|
||||
.use_local_names = NULL,
|
||||
.use_target_qns = NULL,
|
||||
.use_count = 0,
|
||||
};
|
||||
const CBMType **param_types = cbm_type_materialize_signature_params(
|
||||
arena, d->signature_param_types, d->signature_param_count, php_signature_parse_type,
|
||||
&parse_ctx);
|
||||
|
||||
/* Build FUNC type from "|"-separated return-type texts. Each piece
|
||||
* is the unqualified return-type expression as it appears in the
|
||||
* def-defining module's source. parse_return_type_text qualifies
|
||||
@@ -4192,7 +4454,6 @@ static void php_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeR
|
||||
ret_types = (const CBMType **)cbm_arena_alloc(
|
||||
arena, (size_t)(n + 1) * sizeof(const CBMType *));
|
||||
if (ret_types) {
|
||||
const char *def_mod = d->def_module_qn ? d->def_module_qn : "";
|
||||
for (int j = 0; j < n; j++) {
|
||||
ret_types[j] = parse_return_type_text(arena, ret_strs[j], def_mod, NULL,
|
||||
NULL, NULL, 0);
|
||||
@@ -4201,7 +4462,7 @@ static void php_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeR
|
||||
}
|
||||
}
|
||||
}
|
||||
rf.signature = cbm_type_func(arena, NULL, NULL, ret_types);
|
||||
rf.signature = cbm_type_func(arena, NULL, param_types, ret_types);
|
||||
|
||||
if (strcmp(d->label, "Method") == 0 && d->receiver_type && d->receiver_type[0]) {
|
||||
rf.receiver_type = d->receiver_type; /* borrowed */
|
||||
@@ -4254,7 +4515,7 @@ void cbm_run_php_lsp_cross(CBMArena *arena, const char *source, int source_len,
|
||||
* accumulate GBs across a large repo. The scratch dies with this call. */
|
||||
CBMArena idx_arena;
|
||||
cbm_arena_init(&idx_arena);
|
||||
php_register_lsp_defs(arena, &idx_arena, ®, defs, def_count);
|
||||
cbm_php_register_lsp_defs(arena, &idx_arena, ®, defs, def_count);
|
||||
|
||||
/* Finalize registry — O(1) lookups. See go_lsp.c "3c. Finalize"
|
||||
* comment for the rationale. */
|
||||
@@ -4275,7 +4536,6 @@ void cbm_run_php_lsp_cross(CBMArena *arena, const char *source, int source_len,
|
||||
* to populate typed-property field maps so $this->prop and $obj->prop
|
||||
* resolve to the right type during the call walk. */
|
||||
{
|
||||
php_class_field_table_t tab = {0};
|
||||
/* Collect top-level children once (O(n)); ts_node_child(root,i) is O(i) → O(n²). */
|
||||
uint32_t pkn = 0;
|
||||
TSNode *pkids = cbm_lsp_collect_children(ctx.arena, root, &pkn);
|
||||
@@ -4288,22 +4548,10 @@ void cbm_run_php_lsp_cross(CBMArena *arena, const char *source, int source_len,
|
||||
collect_use_declaration(&ctx, c);
|
||||
}
|
||||
}
|
||||
php_lsp_collect_class_fields(&ctx, ®, root, &tab);
|
||||
cbm_php_refine_lsp_registry(&ctx, ®, root);
|
||||
ctx.current_namespace_qn = "";
|
||||
/* Reset to caller-supplied uses only — process_file re-adds AST uses. */
|
||||
ctx.use_count = import_count;
|
||||
for (int i = 0; i < tab.count; i++) {
|
||||
php_class_fields_t *f = &tab.items[i];
|
||||
if (f->count == 0)
|
||||
continue;
|
||||
for (int t = 0; t < reg.type_count; t++) {
|
||||
if (strcmp(reg.types[t].qualified_name, f->class_qn) == 0) {
|
||||
reg.types[t].field_names = f->field_names;
|
||||
reg.types[t].field_types = f->field_types;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
php_lsp_process_file(&ctx, root);
|
||||
@@ -4344,6 +4592,7 @@ void cbm_batch_php_lsp_cross(CBMArena *arena, CBMBatchPHPLSPFile *files, int fil
|
||||
for (int j = 0; j < file_out.count; j++) {
|
||||
CBMResolvedCall *src = &file_out.items[j];
|
||||
CBMResolvedCall *dst = &out[f].items[j];
|
||||
memset(dst, 0, sizeof(*dst));
|
||||
dst->caller_qn =
|
||||
src->caller_qn ? cbm_arena_strdup(arena, src->caller_qn) : NULL;
|
||||
dst->callee_qn =
|
||||
@@ -4351,6 +4600,9 @@ void cbm_batch_php_lsp_cross(CBMArena *arena, CBMBatchPHPLSPFile *files, int fil
|
||||
dst->strategy = src->strategy ? cbm_arena_strdup(arena, src->strategy) : NULL;
|
||||
dst->confidence = src->confidence;
|
||||
dst->reason = src->reason ? cbm_arena_strdup(arena, src->reason) : NULL;
|
||||
dst->kind = src->kind;
|
||||
dst->site_start_byte = src->site_start_byte;
|
||||
dst->site_end_byte = src->site_end_byte;
|
||||
}
|
||||
} else {
|
||||
out[f].count = 0;
|
||||
|
||||
@@ -71,6 +71,10 @@ void php_lsp_add_use(PHPLSPContext *ctx, const char *local_name, const char *tar
|
||||
/* Process a file's AST: walk top-level decls, then function/method bodies. */
|
||||
void php_lsp_process_file(PHPLSPContext *ctx, TSNode root);
|
||||
|
||||
/* Refine an already-populated registry from local class ASTs: declared and
|
||||
* PHPDoc returns, trait flattening, and typed fields. */
|
||||
void cbm_php_refine_lsp_registry(PHPLSPContext *ctx, CBMTypeRegistry *reg, TSNode root);
|
||||
|
||||
/* Evaluate a PHP expression's type. May return NULL / CBM_TYPE_UNKNOWN. */
|
||||
const CBMType *php_eval_expr_type(PHPLSPContext *ctx, TSNode node);
|
||||
|
||||
@@ -100,6 +104,9 @@ void cbm_php_stdlib_register(CBMTypeRegistry *reg, CBMArena *arena);
|
||||
* `use` declarations from the AST are layered on top by process_file.
|
||||
*
|
||||
* Reuses go_lsp.h's CBMLSPDef so cross-language registration is uniform. */
|
||||
void cbm_php_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeRegistry *reg,
|
||||
const CBMLSPDef *defs, int def_count);
|
||||
|
||||
void cbm_run_php_lsp_cross(
|
||||
CBMArena *arena,
|
||||
const char *source, int source_len,
|
||||
|
||||
+723
-101
File diff suppressed because it is too large
Load Diff
@@ -50,6 +50,7 @@ typedef struct {
|
||||
// Built from CBMFileResult.imports.
|
||||
const char **import_local_names;
|
||||
const char **import_module_qns;
|
||||
unsigned char *import_kinds; // internal PyDirectImportKind, classified once from the AST
|
||||
int import_count;
|
||||
|
||||
// Current function/class context for resolving `self`/`cls` and emitting
|
||||
@@ -69,8 +70,9 @@ typedef struct {
|
||||
// appears in result->calls because a `subscript`/`binary_operator` is not
|
||||
// a `call` node, so the syntactic extractor produced no call. When this is
|
||||
// non-NULL, py_emit_dunder_call injects a matching synthetic CBMCall so the
|
||||
// recovered dunder call becomes a real CALLS edge. NULL in the cross-file
|
||||
// path (no result->calls available there). Mirrors RustLSPContext.syn_calls.
|
||||
// recovered dunder call becomes a real CALLS edge. Cross-file callers use
|
||||
// a separate arena-owned output and merge it into the file result after
|
||||
// resolution. Mirrors RustLSPContext.syn_calls.
|
||||
CBMCallArray *syn_calls;
|
||||
|
||||
// Lambda registry: simple linear list, looked up by name.
|
||||
@@ -155,7 +157,7 @@ void cbm_run_py_lsp_cross(CBMArena *arena, const char *source, int source_len,
|
||||
const char *module_qn, CBMLSPDef *defs, int def_count,
|
||||
const char **import_names, const char **import_qns, int import_count,
|
||||
TSTree *cached_tree, // NULL = parse internally
|
||||
CBMResolvedCallArray *out);
|
||||
CBMResolvedCallArray *out, CBMCallArray *synthetic_calls);
|
||||
|
||||
/* Tier 2: pre-built per-language registry (mirrors Go pilot).
|
||||
* Filters all_defs to Python entries, builds + finalizes once. */
|
||||
@@ -166,7 +168,7 @@ void cbm_run_py_lsp_cross_with_registry(CBMArena *arena, const char *source, int
|
||||
CBMTypeRegistry *reg, // pre-built, finalized, READ-ONLY
|
||||
const char **import_names, const char **import_qns,
|
||||
int import_count, TSTree *cached_tree,
|
||||
CBMResolvedCallArray *out);
|
||||
CBMResolvedCallArray *out, CBMCallArray *synthetic_calls);
|
||||
|
||||
// --- Batch cross-file LSP ---
|
||||
|
||||
|
||||
+1086
-278
File diff suppressed because it is too large
Load Diff
+49
-20
@@ -55,9 +55,13 @@ typedef struct {
|
||||
CBMArena *arena;
|
||||
const char *source;
|
||||
int source_len;
|
||||
const char *root_source; /* immutable original file buffer */
|
||||
|
||||
const CBMTypeRegistry *registry;
|
||||
CBMScope *current_scope;
|
||||
/* Alias assignments beneath a branch/loop/match/closure cannot establish
|
||||
* one unconditional post-region target, so they clear callable identity. */
|
||||
int callable_control_flow_depth;
|
||||
|
||||
/* Negative-lookup memo for the registry-pure resolve cascades (trait
|
||||
* method / sole-trait-impl / free-func fallback). Active ONLY when the
|
||||
@@ -75,6 +79,11 @@ typedef struct {
|
||||
const char **glob_module_qns;
|
||||
int glob_count;
|
||||
|
||||
/* Exact names from top-level out-of-line `mod name;` declarations. A
|
||||
* relative scoped type may be project-qualified only when its head is in
|
||||
* this set; coincidentally same-named indexed modules stay external. */
|
||||
CBMIdxMemo declared_modules;
|
||||
|
||||
/* Module-qualified name for this file (e.g. "<project>.<crate>.foo"). */
|
||||
const char *module_qn;
|
||||
|
||||
@@ -107,6 +116,13 @@ typedef struct {
|
||||
struct RustMacroRule **macro_rules_arr;
|
||||
int macro_rules_count;
|
||||
|
||||
/* Original-file occurrence that established the lexical environment for
|
||||
* the current macro expansion. Nested macro invocations are parsed from a
|
||||
* synthetic transcriber buffer, so their tree-sitter offsets cannot be
|
||||
* used to decide which source-level macro_rules! definition is visible. */
|
||||
uint32_t macro_origin_byte;
|
||||
bool macro_origin_valid;
|
||||
|
||||
/* Recursion guard for macro expansion. Real macro_rules! can be
|
||||
* recursive; we cap at 8 nested expansions to keep the walker
|
||||
* bounded. */
|
||||
@@ -162,8 +178,9 @@ typedef struct {
|
||||
* calls hidden inside macro token-trees (`format!("{}", d.label())`) —
|
||||
* never appear in result->calls because the syntactic extractor cannot
|
||||
* see them. When this is non-NULL the resolver injects matching synthetic
|
||||
* CBMCall entries so those recovered calls become real edges. NULL in the
|
||||
* cross-file path (no result available). */
|
||||
* CBMCall entries so those recovered calls become real edges. Cross-file
|
||||
* callers provide a separate output array and merge it into the owning
|
||||
* file result with explicit arena-aware copying. */
|
||||
CBMCallArray *syn_calls;
|
||||
|
||||
/* While >0, rust_emit_resolved_call also injects a matching synthetic
|
||||
@@ -172,6 +189,19 @@ typedef struct {
|
||||
* produced a call node. */
|
||||
int inject_syn_calls;
|
||||
|
||||
/* Current invocation occurrence in ORIGINAL-file coordinates. Ordinary
|
||||
* source calls use their tree-sitter node directly. Calls recovered by
|
||||
* reparsing built-in macro arguments use the affine map below to translate
|
||||
* the synthetic wrapper's offsets back to the copied token-tree span.
|
||||
* macro_rules! transcriber text is intentionally left unmapped because
|
||||
* substitution is not position-preserving. */
|
||||
uint32_t emit_site_start_byte;
|
||||
uint32_t emit_site_end_byte;
|
||||
bool site_map_active;
|
||||
uint32_t site_map_synthetic_start_byte;
|
||||
uint32_t site_map_synthetic_end_byte;
|
||||
uint32_t site_map_original_start_byte;
|
||||
|
||||
/* CBM_LSP_DEBUG=1 in env enables verbose stderr trace. */
|
||||
bool debug;
|
||||
|
||||
@@ -235,6 +265,12 @@ void rust_process_statement(RustLSPContext *ctx, TSNode node);
|
||||
const CBMRegisteredFunc *rust_lookup_method(RustLSPContext *ctx, const char *type_qn,
|
||||
const char *member_name);
|
||||
|
||||
/* Populate the same per-file registry used by cbm_run_rust_lsp, including
|
||||
* AST-declared return-type refinements. Exposed as a production-used seam for
|
||||
* structural registry regression tests. */
|
||||
void cbm_rust_build_local_registry(CBMArena *arena, CBMTypeRegistry *reg, CBMFileResult *result,
|
||||
const char *module_qn, TSNode root, const char *source);
|
||||
|
||||
/* Entry point — called from `cbm_extract_file()` after the unified
|
||||
* extractor has filled `result->defs`, `result->imports`, and
|
||||
* `result->impl_traits`. Builds a per-file registry, parses the `use`
|
||||
@@ -242,19 +278,6 @@ const CBMRegisteredFunc *rust_lookup_method(RustLSPContext *ctx, const char *typ
|
||||
void cbm_run_rust_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len,
|
||||
TSNode root);
|
||||
|
||||
/* Synthesize call edges for curated attribute proc-macros.
|
||||
*
|
||||
* For each `CBMDefinition` whose decorators include a known
|
||||
* attribute proc-macro (`#[tokio::main]`, `#[tracing::instrument]`,
|
||||
* `#[async_trait]`, …), emit synthetic edges from that definition
|
||||
* to the helper functions the macro would have injected (e.g.
|
||||
* `tokio::runtime::Runtime::new` + `block_on`). The edges carry
|
||||
* strategy `lsp_proc_macro` so consumers can distinguish them from
|
||||
* direct call attributions.
|
||||
*
|
||||
* Called automatically from `cbm_run_rust_lsp_with_manifest`. */
|
||||
void cbm_rust_synth_proc_macro_edges(CBMArena *arena, CBMFileResult *result);
|
||||
|
||||
/* Same as `cbm_run_rust_lsp`, but accepts an optional Cargo manifest
|
||||
* so the resolver can route paths whose head is a workspace member /
|
||||
* declared dependency. Pass NULL to fall back to the manifest-free
|
||||
@@ -293,8 +316,12 @@ typedef struct {
|
||||
const char *embedded_types; /* "|"-separated embedded type QNs */
|
||||
const char *field_defs; /* "|"-separated "name:type" pairs */
|
||||
const char *method_names_str; /* "|"-separated method names for traits */
|
||||
const char *trait_qn; /* impl Trait for Type → set on Method defs */
|
||||
const char **signature_param_types; /* borrowed ordered parameter texts */
|
||||
int signature_param_count; /* positional entries; "?" is unknown */
|
||||
const char *trait_qn; /* raw impl-trait spelling; uniquely canonicalized */
|
||||
bool is_interface; /* true for traits */
|
||||
bool is_rust_impl_relation; /* independent type-level impl record */
|
||||
bool is_abstract; /* required trait declaration (no default) */
|
||||
} CBMRustLSPDef;
|
||||
|
||||
/* Run cross-file resolution on a single file. */
|
||||
@@ -314,7 +341,7 @@ void cbm_run_rust_lsp_cross_with_manifest(CBMArena *arena, const char *source, i
|
||||
const char **import_names, const char **import_qns,
|
||||
int import_count, TSTree *cached_tree,
|
||||
const struct CBMCargoManifest *manifest,
|
||||
CBMResolvedCallArray *out);
|
||||
CBMResolvedCallArray *out, CBMCallArray *synthetic_calls);
|
||||
|
||||
/* Tier-2: build the project-wide Rust cross registry ONCE from all defs, finalize,
|
||||
* and seal read-only. Shared across every Rust file's resolve (mirrors C/py/cs/ts).
|
||||
@@ -323,14 +350,16 @@ CBMTypeRegistry *cbm_rust_build_cross_registry(CBMArena *arena, CBMLSPDef *defs,
|
||||
|
||||
/* Cross-file Rust resolve using a pre-built shared registry (Tier-2). Skips the
|
||||
* per-file registry build; just parse + resolve. `manifest` = the same Cargo manifest
|
||||
* the per-file path uses (cross-crate #56). `result` may be NULL (the cross path does
|
||||
* not synthesise proc-macro edges). Mirrors cbm_run_c_lsp_cross_with_registry. */
|
||||
* the per-file path uses (cross-crate #56). `synthetic_calls` is an optional,
|
||||
* arena-owned output for exact carriers recovered from non-call syntax. Attribute
|
||||
* proc-macros are handled by the graph's DECORATES + USAGE semantic passes, not
|
||||
* by this resolver. */
|
||||
void cbm_run_rust_lsp_cross_with_registry(CBMArena *arena, const char *source, int source_len,
|
||||
const char *module_qn, const CBMTypeRegistry *reg,
|
||||
const char **import_names, const char **import_qns,
|
||||
int import_count, TSTree *cached_tree,
|
||||
const struct CBMCargoManifest *manifest,
|
||||
CBMResolvedCallArray *out, CBMFileResult *result);
|
||||
CBMResolvedCallArray *out, CBMCallArray *synthetic_calls);
|
||||
|
||||
/* Per-file input for batch cross-file Rust LSP processing. */
|
||||
typedef struct {
|
||||
|
||||
@@ -1,145 +1,11 @@
|
||||
/* rust_proc_macros.c — Curated attribute proc-macro expansion.
|
||||
/* Rust procedural-macro policy boundary.
|
||||
*
|
||||
* RUST_LSP_FOLLOWUP §A1 + improvement Option B: extend derive synthesis
|
||||
* (already done for #[derive(...)]) to *attribute*-style proc-macros.
|
||||
* Attribute and derive macros are represented by the generic graph semantic
|
||||
* passes as DECORATES plus USAGE. This file intentionally emits no
|
||||
* CBMResolvedCall records: without executing rustc and the proc-macro, helper
|
||||
* invocations such as Runtime::block_on or Span::enter are not source-proven
|
||||
* calls and must not be fabricated.
|
||||
*
|
||||
* We can't run proc-macros (would need rustc). But for the ~20
|
||||
* most-used attribute proc-macros we KNOW the expansion shape and can
|
||||
* synthesize equivalent calls/impls so the resolver doesn't go blind.
|
||||
*
|
||||
* Covered:
|
||||
* - `#[tokio::main]` / `#[tokio::test]` — wrap fn body in
|
||||
* `tokio::runtime::Runtime::new().unwrap().block_on(async { ... })`
|
||||
* - `#[async_std::main]` — same shape via async_std executor
|
||||
* - `#[async_trait::async_trait]` — methods on the trait become
|
||||
* `Pin<Box<dyn Future>>` returning shapes; we just register the
|
||||
* method bodies normally (the resolver already handles `async fn`)
|
||||
* - `#[derive(thiserror::Error)]` — synthesise `source`, `description`
|
||||
* (we already synth via the curated derive table; this just adds
|
||||
* `thiserror::Error` to the alias chain)
|
||||
* - `#[derive(serde::Serialize|Deserialize)]` — already covered by
|
||||
* the curated derive table (alias `serde::Serialize` to
|
||||
* `serde.Serialize`)
|
||||
* - `#[clap(...)]` field attributes — no synthesis needed; the
|
||||
* `#[derive(clap::Parser)]` outer macro handles the impl
|
||||
* - `#[tracing::instrument]` — wraps the function body in a span;
|
||||
* we synthesise a call to `tracing::span::Span::enter`
|
||||
* - `#[rocket::main]`/`#[actix_web::main]` — same shape as tokio
|
||||
* - `#[test]` / `#[bench]` — no synthesis; the function body is
|
||||
* attributed as-is
|
||||
*
|
||||
* For each, we register a synthetic "wrapper call" QN per
|
||||
* decorator → resolved-call edge so the project's tracing/dependency
|
||||
* analyses see what the proc-macro injected. */
|
||||
|
||||
#include "../arena.h"
|
||||
#include "../cbm.h"
|
||||
#include "rust_lsp.h"
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/* The attribute-decorator table. For each pattern in `decorator` text
|
||||
* we emit a *synthetic call edge* attributed to the wrapped function
|
||||
* so tracing tools can see the implicit dependency. The synthesis is
|
||||
* conservative: we emit only when the decorator text matches one of
|
||||
* the curated tokens — anything else is left alone. */
|
||||
typedef struct {
|
||||
const char* match; /* substring to find in the decorator text */
|
||||
const char* edges[6]; /* synthesized callee QNs (NULL-terminated) */
|
||||
} RustAttrSynth;
|
||||
|
||||
static const RustAttrSynth ATTR_SYNTH[] = {
|
||||
{"tokio::main", {
|
||||
"tokio.runtime.Runtime.new",
|
||||
"tokio.runtime.Runtime.block_on",
|
||||
NULL
|
||||
}},
|
||||
{"tokio::test", {
|
||||
"tokio.runtime.Runtime.new",
|
||||
"tokio.runtime.Runtime.block_on",
|
||||
NULL
|
||||
}},
|
||||
{"async_std::main", {
|
||||
"async_std.task.block_on",
|
||||
NULL
|
||||
}},
|
||||
{"actix_web::main", {
|
||||
"actix_web.rt.System.new",
|
||||
"actix_web.rt.System.block_on",
|
||||
NULL
|
||||
}},
|
||||
{"actix_rt::main", {
|
||||
"actix_rt.System.new",
|
||||
"actix_rt.System.block_on",
|
||||
NULL
|
||||
}},
|
||||
{"rocket::main", {
|
||||
"rocket.async_main",
|
||||
NULL
|
||||
}},
|
||||
{"rocket::launch", {
|
||||
"rocket.launch",
|
||||
NULL
|
||||
}},
|
||||
{"tracing::instrument", {
|
||||
"tracing.span.Span.enter",
|
||||
NULL
|
||||
}},
|
||||
{"async_trait", {
|
||||
/* The macro wraps each method's return in
|
||||
* `Pin<Box<dyn Future<Output = R> + Send>>` — no method-name
|
||||
* synthesis needed, just record the boxed-future bridging
|
||||
* call so analyses see the async-trait shape. */
|
||||
"alloc.boxed.Box.new",
|
||||
"core.pin.Pin.new",
|
||||
NULL
|
||||
}},
|
||||
{"wasm_bindgen", {
|
||||
"wasm_bindgen.JsValue.from",
|
||||
NULL
|
||||
}},
|
||||
{"napi", {
|
||||
"napi.Env.create",
|
||||
NULL
|
||||
}},
|
||||
{"pyo3::pyfunction", {
|
||||
"pyo3.Python.with_gil",
|
||||
NULL
|
||||
}},
|
||||
{"pyo3::pymethods", {
|
||||
"pyo3.Python.with_gil",
|
||||
NULL
|
||||
}},
|
||||
};
|
||||
|
||||
#define ATTR_SYNTH_COUNT (int)(sizeof(ATTR_SYNTH) / sizeof(ATTR_SYNTH[0]))
|
||||
|
||||
/* Inspect a definition's decorators and, if any matches a curated
|
||||
* proc-macro attribute, emit synthetic edges from that definition's
|
||||
* QN to the wrapper functions the macro would have injected. The
|
||||
* edges are best-effort and tagged `lsp_proc_macro` so consumers can
|
||||
* distinguish them from direct calls. */
|
||||
void cbm_rust_synth_proc_macro_edges(CBMArena* arena, CBMFileResult* result) {
|
||||
if (!arena || !result) return;
|
||||
for (int i = 0; i < result->defs.count; i++) {
|
||||
CBMDefinition* d = &result->defs.items[i];
|
||||
if (!d->decorators || !d->qualified_name) continue;
|
||||
for (int di = 0; d->decorators[di]; di++) {
|
||||
const char* dec = d->decorators[di];
|
||||
for (int t = 0; t < ATTR_SYNTH_COUNT; t++) {
|
||||
const RustAttrSynth* s = &ATTR_SYNTH[t];
|
||||
if (!strstr(dec, s->match)) continue;
|
||||
/* Emit one resolved call per edge. */
|
||||
for (int e = 0; s->edges[e]; e++) {
|
||||
CBMResolvedCall rc;
|
||||
memset(&rc, 0, sizeof(rc));
|
||||
rc.caller_qn = d->qualified_name;
|
||||
rc.callee_qn = cbm_arena_strdup(arena, s->edges[e]);
|
||||
rc.strategy = "lsp_proc_macro";
|
||||
rc.confidence = 0.78f; /* high-but-not-direct */
|
||||
cbm_resolvedcall_push(&result->resolved_calls, arena, rc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
* Kept as a separate compilation unit because lsp_all.c includes the historic
|
||||
* module directly; the empty policy boundary also makes accidental synthesis
|
||||
* conspicuous in review. */
|
||||
|
||||
@@ -33,7 +33,8 @@ static CBMScopeChunk* alloc_chunk(CBMScope* scope) {
|
||||
return c;
|
||||
}
|
||||
|
||||
void cbm_scope_bind(CBMScope* scope, const char* name, const CBMType* type) {
|
||||
static void cbm_scope_bind_value(CBMScope *scope, const char *name, const CBMType *type,
|
||||
const char *callable_qn) {
|
||||
if (!scope || !name) {
|
||||
return;
|
||||
}
|
||||
@@ -41,6 +42,7 @@ void cbm_scope_bind(CBMScope* scope, const char* name, const CBMType* type) {
|
||||
for (int i = 0; i < c->used; i++) {
|
||||
if (c->bindings[i].name && strcmp(c->bindings[i].name, name) == 0) {
|
||||
c->bindings[i].type = type;
|
||||
c->bindings[i].callable_qn = callable_qn;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -54,9 +56,19 @@ void cbm_scope_bind(CBMScope* scope, const char* name, const CBMType* type) {
|
||||
}
|
||||
head->bindings[head->used].name = name;
|
||||
head->bindings[head->used].type = type;
|
||||
head->bindings[head->used].callable_qn = callable_qn;
|
||||
head->used++;
|
||||
}
|
||||
|
||||
void cbm_scope_bind(CBMScope *scope, const char *name, const CBMType *type) {
|
||||
cbm_scope_bind_value(scope, name, type, NULL);
|
||||
}
|
||||
|
||||
void cbm_scope_bind_callable(CBMScope *scope, const char *name, const CBMType *type,
|
||||
const char *callable_qn) {
|
||||
cbm_scope_bind_value(scope, name, type, callable_qn);
|
||||
}
|
||||
|
||||
const CBMType* cbm_scope_lookup(const CBMScope* scope, const char* name) {
|
||||
if (!name) {
|
||||
return cbm_type_unknown();
|
||||
@@ -72,3 +84,52 @@ const CBMType* cbm_scope_lookup(const CBMScope* scope, const char* name) {
|
||||
}
|
||||
return cbm_type_unknown();
|
||||
}
|
||||
|
||||
bool cbm_scope_contains(const CBMScope *scope, const char *name) {
|
||||
if (!name) {
|
||||
return false;
|
||||
}
|
||||
for (const CBMScope *s = scope; s != NULL; s = s->parent) {
|
||||
for (const CBMScopeChunk *c = s->chunks; c != NULL; c = c->next) {
|
||||
for (int i = 0; i < c->used; i++) {
|
||||
if (c->bindings[i].name && strcmp(c->bindings[i].name, name) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *cbm_scope_lookup_callable(const CBMScope *scope, const char *name) {
|
||||
if (!name) {
|
||||
return NULL;
|
||||
}
|
||||
for (const CBMScope *s = scope; s != NULL; s = s->parent) {
|
||||
for (const CBMScopeChunk *c = s->chunks; c != NULL; c = c->next) {
|
||||
for (int i = 0; i < c->used; i++) {
|
||||
if (c->bindings[i].name && strcmp(c->bindings[i].name, name) == 0) {
|
||||
return c->bindings[i].callable_qn;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool cbm_scope_update_callable(CBMScope *scope, const char *name, const char *callable_qn) {
|
||||
if (!name) {
|
||||
return false;
|
||||
}
|
||||
for (CBMScope *s = scope; s != NULL; s = s->parent) {
|
||||
for (CBMScopeChunk *c = s->chunks; c != NULL; c = c->next) {
|
||||
for (int i = 0; i < c->used; i++) {
|
||||
if (c->bindings[i].name && strcmp(c->bindings[i].name, name) == 0) {
|
||||
c->bindings[i].callable_qn = callable_qn;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
typedef struct {
|
||||
const char* name;
|
||||
const CBMType* type;
|
||||
/* Exact callable value carried by this lexical binding, or NULL when the
|
||||
* binding is not proven to denote one callable. This is deliberately
|
||||
* identity metadata rather than another CBMType kind: aliases need both
|
||||
* their ordinary type and the graph QN of the value they reference. */
|
||||
const char *callable_qn;
|
||||
} CBMVarBinding;
|
||||
|
||||
#define CBM_SCOPE_CHUNK_BINDINGS 16
|
||||
@@ -56,6 +61,20 @@ static inline int cbm_lsp_max_walk_depth(void) {
|
||||
CBMScope* cbm_scope_push(CBMArena* a, CBMScope* current);
|
||||
CBMScope* cbm_scope_pop(CBMScope* scope);
|
||||
void cbm_scope_bind(CBMScope* scope, const char* name, const CBMType* type);
|
||||
/* Bind a value whose identity is one exact callable. A later ordinary
|
||||
* cbm_scope_bind of the same name clears this identity, so reassignment fails
|
||||
* closed instead of leaking a stale alias target. */
|
||||
void cbm_scope_bind_callable(CBMScope *scope, const char *name, const CBMType *type,
|
||||
const char *callable_qn);
|
||||
const CBMType* cbm_scope_lookup(const CBMScope* scope, const char* name);
|
||||
/* True when any lexical frame contains name, even when its type is UNKNOWN. */
|
||||
bool cbm_scope_contains(const CBMScope *scope, const char *name);
|
||||
/* Return the exact callable QN from the nearest binding. A nearer ordinary
|
||||
* binding shadows a parent's callable and therefore returns NULL. */
|
||||
const char *cbm_scope_lookup_callable(const CBMScope *scope, const char *name);
|
||||
/* Replace (or clear with NULL) callable identity on the nearest existing
|
||||
* lexical binding. Returns false when name is unbound. This is for assignment;
|
||||
* declarations should continue to use cbm_scope_bind[_callable]. */
|
||||
bool cbm_scope_update_callable(CBMScope *scope, const char *name, const char *callable_qn);
|
||||
|
||||
#endif // CBM_LSP_SCOPE_H
|
||||
|
||||
+677
-131
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,10 @@ typedef struct {
|
||||
|
||||
// Output: resolved calls accumulate here.
|
||||
CBMResolvedCallArray *resolved_calls;
|
||||
// Optional raw usages owned by the per-file extraction result. The scope
|
||||
// pass marks occurrence-exact module/import shadows without emitting a
|
||||
// semantic reference record for them.
|
||||
CBMUsageArray *usages;
|
||||
|
||||
// Type-parameter scope (innermost generic function/class).
|
||||
// type_param_constraints may be NULL or shorter — entries default to "any".
|
||||
@@ -51,6 +55,7 @@ typedef struct {
|
||||
bool js_mode;
|
||||
bool jsx_mode;
|
||||
bool dts_mode;
|
||||
bool cross_file_mode; // project-wide registry is available for exact reference proof
|
||||
bool strict; // tsconfig "strict": true → fewer implicit-any fallbacks
|
||||
bool debug; // CBM_LSP_DEBUG env
|
||||
|
||||
|
||||
@@ -286,6 +286,58 @@ int cbm_free_func_iter_next(CBMFreeFuncIter *it) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
void cbm_registry_methods(const CBMTypeRegistry *reg, const char *receiver_qn,
|
||||
const char *method_name, CBMMethodIter *out) {
|
||||
memset(out, 0, sizeof(*out));
|
||||
out->reg = reg;
|
||||
out->receiver_qn = receiver_qn;
|
||||
out->method_name = method_name;
|
||||
out->chain_idx = -1;
|
||||
if (!reg || !receiver_qn || !method_name) {
|
||||
return;
|
||||
}
|
||||
out->hash = fnv1a_pair(receiver_qn, method_name);
|
||||
if (reg->method_buckets && reg->method_bucket_count > 0) {
|
||||
int slot = (int)(out->hash & (uint64_t)(reg->method_bucket_count - 1));
|
||||
out->chain_idx = reg->method_buckets[slot];
|
||||
out->tail_i = reg->func_qn_entry_count;
|
||||
out->tail_end = reg->func_count;
|
||||
} else {
|
||||
out->tail_i = 0;
|
||||
out->tail_end = reg->func_count;
|
||||
}
|
||||
}
|
||||
|
||||
int cbm_method_iter_next(CBMMethodIter *it) {
|
||||
if (!it || !it->reg || !it->receiver_qn || !it->method_name) {
|
||||
return -1;
|
||||
}
|
||||
const CBMTypeRegistry *reg = it->reg;
|
||||
while (it->chain_idx >= 0) {
|
||||
const CBMRegistryHashEntry *e = ®->method_entries[it->chain_idx];
|
||||
int p = e->payload_index;
|
||||
uint64_t h = e->hash;
|
||||
it->chain_idx = e->next_index;
|
||||
if (h != it->hash) {
|
||||
continue;
|
||||
}
|
||||
const CBMRegisteredFunc *f = ®->funcs[p];
|
||||
if (f->receiver_type && f->short_name && strcmp(f->receiver_type, it->receiver_qn) == 0 &&
|
||||
strcmp(f->short_name, it->method_name) == 0) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
while (it->tail_i < it->tail_end) {
|
||||
int p = it->tail_i++;
|
||||
const CBMRegisteredFunc *f = ®->funcs[p];
|
||||
if (f->receiver_type && f->short_name && strcmp(f->receiver_type, it->receiver_qn) == 0 &&
|
||||
strcmp(f->short_name, it->method_name) == 0) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void cbm_registry_finalize_into(CBMTypeRegistry *reg, CBMArena *idx_arena) {
|
||||
if (!reg || !idx_arena)
|
||||
return;
|
||||
@@ -604,6 +656,27 @@ const CBMRegisteredFunc *cbm_registry_lookup_method_by_args(const CBMTypeRegistr
|
||||
range_pi = pi;
|
||||
}
|
||||
}
|
||||
/* Mirror lookup_method_self's post-finalize visibility contract. The
|
||||
* hash chain covers the finalize-time prefix only; overloads added
|
||||
* later must participate in the same exact/range/first ordering. */
|
||||
for (int pi = reg->func_qn_entry_count; pi < reg->func_count; pi++) {
|
||||
const CBMRegisteredFunc *f = ®->funcs[pi];
|
||||
if (!f->receiver_type || !f->short_name || strcmp(f->receiver_type, receiver_qn) != 0 ||
|
||||
strcmp(f->short_name, method_name) != 0)
|
||||
continue;
|
||||
if (first_pi < 0 || pi < first_pi)
|
||||
first_pi = pi;
|
||||
int pc = count_func_params(f);
|
||||
if (pc == arg_count) {
|
||||
if (exact_pi < 0 || pi < exact_pi)
|
||||
exact_pi = pi;
|
||||
continue;
|
||||
}
|
||||
int min_pc = (f->min_params >= 0) ? f->min_params : pc;
|
||||
if (arg_count >= min_pc && arg_count <= pc && (range_pi < 0 || pi < range_pi)) {
|
||||
range_pi = pi;
|
||||
}
|
||||
}
|
||||
int sel = exact_pi >= 0 ? exact_pi : (range_pi >= 0 ? range_pi : first_pi);
|
||||
if (sel >= 0)
|
||||
return ®->funcs[sel];
|
||||
@@ -776,6 +849,19 @@ const CBMRegisteredFunc *cbm_registry_lookup_method_by_types(const CBMTypeRegist
|
||||
best_pi = pi;
|
||||
}
|
||||
}
|
||||
for (int pi = reg->func_qn_entry_count; pi < reg->func_count; pi++) {
|
||||
const CBMRegisteredFunc *f = ®->funcs[pi];
|
||||
if (!f->receiver_type || !f->short_name || strcmp(f->receiver_type, receiver_qn) != 0 ||
|
||||
strcmp(f->short_name, method_name) != 0)
|
||||
continue;
|
||||
if (first_pi < 0 || pi < first_pi)
|
||||
first_pi = pi;
|
||||
int s = score_overload_match(f, arg_types, arg_count);
|
||||
if (s > best_score || (s == best_score && best_pi >= 0 && pi < best_pi)) {
|
||||
best_score = s;
|
||||
best_pi = pi;
|
||||
}
|
||||
}
|
||||
// best_score starts at 0; a score of 0 means "wrong arg count" → no
|
||||
// match, exactly as the linear scan (which never sets best on s==0).
|
||||
int sel = (best_pi >= 0 && best_score > 0) ? best_pi : first_pi;
|
||||
@@ -857,6 +943,18 @@ const CBMRegisteredFunc *cbm_registry_lookup_symbol_by_types(const CBMTypeRegist
|
||||
best_pi = pi;
|
||||
}
|
||||
}
|
||||
for (int pi = reg->func_qn_entry_count; pi < reg->func_count; pi++) {
|
||||
const CBMRegisteredFunc *f = ®->funcs[pi];
|
||||
if (!f->qualified_name || strcmp(f->qualified_name, buf) != 0)
|
||||
continue;
|
||||
if (first_pi < 0 || pi < first_pi)
|
||||
first_pi = pi;
|
||||
int s = score_overload_match(f, arg_types, arg_count);
|
||||
if (s > best_score || (s == best_score && best_pi >= 0 && pi < best_pi)) {
|
||||
best_score = s;
|
||||
best_pi = pi;
|
||||
}
|
||||
}
|
||||
int sel = (best_pi >= 0 && best_score > 0) ? best_pi : first_pi;
|
||||
if (sel >= 0)
|
||||
return ®->funcs[sel];
|
||||
@@ -935,6 +1033,23 @@ const CBMRegisteredFunc *cbm_registry_lookup_symbol_by_args(const CBMTypeRegistr
|
||||
range_pi = pi;
|
||||
}
|
||||
}
|
||||
for (int pi = reg->func_qn_entry_count; pi < reg->func_count; pi++) {
|
||||
const CBMRegisteredFunc *f = ®->funcs[pi];
|
||||
if (!f->qualified_name || strcmp(f->qualified_name, buf) != 0)
|
||||
continue;
|
||||
if (first_pi < 0 || pi < first_pi)
|
||||
first_pi = pi;
|
||||
int pc = count_func_params(f);
|
||||
if (pc == arg_count) {
|
||||
if (exact_pi < 0 || pi < exact_pi)
|
||||
exact_pi = pi;
|
||||
continue;
|
||||
}
|
||||
int min_pc = (f->min_params >= 0) ? f->min_params : pc;
|
||||
if (arg_count >= min_pc && arg_count <= pc && (range_pi < 0 || pi < range_pi)) {
|
||||
range_pi = pi;
|
||||
}
|
||||
}
|
||||
int sel = exact_pi >= 0 ? exact_pi : (range_pi >= 0 ? range_pi : first_pi);
|
||||
if (sel >= 0)
|
||||
return ®->funcs[sel];
|
||||
|
||||
@@ -5,18 +5,24 @@
|
||||
#include "../arena.h"
|
||||
#include <stdbool.h>
|
||||
|
||||
// Decorator-derived flags (Python). Added at struct tail so existing
|
||||
// Language-specific function metadata. Added at struct tail so existing
|
||||
// callers that memset to zero before populating other fields keep working.
|
||||
typedef enum {
|
||||
CBM_FUNC_FLAG_NONE = 0,
|
||||
CBM_FUNC_FLAG_PROPERTY = 1 << 0, // @property -> obj.attr returns getter return
|
||||
CBM_FUNC_FLAG_CLASSMETHOD = 1 << 1, // @classmethod -> first arg is cls (the class)
|
||||
CBM_FUNC_FLAG_STATICMETHOD = 1 << 2, // @staticmethod -> no implicit self/cls
|
||||
CBM_FUNC_FLAG_ABSTRACTMETHOD = 1 << 3, // @abstractmethod -> still callable for resolution
|
||||
CBM_FUNC_FLAG_OVERLOAD = 1 << 4, // @overload entry — non-implementation stub
|
||||
CBM_FUNC_FLAG_ASYNC = 1 << 5, // async def — return is Coroutine[..., T]
|
||||
CBM_FUNC_FLAG_GENERATOR = 1 << 6, // contains yield — return is Generator[T, ...]
|
||||
CBM_FUNC_FLAG_FINAL = 1 << 7, // @final — overrides not allowed
|
||||
CBM_FUNC_FLAG_PROPERTY = 1 << 0, // @property -> obj.attr returns getter return
|
||||
CBM_FUNC_FLAG_CLASSMETHOD = 1 << 1, // @classmethod -> first arg is cls (the class)
|
||||
CBM_FUNC_FLAG_STATICMETHOD = 1 << 2, // @staticmethod -> no implicit self/cls
|
||||
CBM_FUNC_FLAG_ABSTRACTMETHOD = 1 << 3, // @abstractmethod -> still callable for resolution
|
||||
CBM_FUNC_FLAG_OVERLOAD = 1 << 4, // @overload entry — non-implementation stub
|
||||
CBM_FUNC_FLAG_ASYNC = 1 << 5, // async def — return is Coroutine[..., T]
|
||||
CBM_FUNC_FLAG_GENERATOR = 1 << 6, // contains yield — return is Generator[T, ...]
|
||||
CBM_FUNC_FLAG_FINAL = 1 << 7, // @final — overrides not allowed
|
||||
CBM_FUNC_FLAG_RUST_TRAIT_IMPL = 1 << 8, // exact method from impl Trait for Type
|
||||
CBM_FUNC_FLAG_RUST_ABSTRACT = 1 << 9, // required trait method without a default body
|
||||
/* Python only: more than one registered definition has this exact QN.
|
||||
* Ordinary call resolution keeps its historical language-specific choice,
|
||||
* but a function value cannot name one materialized definition exactly. */
|
||||
CBM_FUNC_FLAG_AMBIGUOUS_BINDING = 1 << 10,
|
||||
} CBMFuncFlags;
|
||||
|
||||
// Registered function/method with full type signature.
|
||||
@@ -27,9 +33,13 @@ typedef struct {
|
||||
const CBMType *signature; // FUNC type with param/return types
|
||||
const char **type_param_names; // NULL-terminated, e.g., ["T", "R", NULL] for generics
|
||||
int min_params; // Minimum required params (excluding defaulted). -1 = unknown.
|
||||
int flags; // CBM_FUNC_FLAG_* bitfield (Python decorator info; 0 elsewhere)
|
||||
int flags; // CBM_FUNC_FLAG_* bitfield
|
||||
const char **decorator_qns; // NULL-terminated decorator QNs (Python only); used for
|
||||
// user-decorator return-type substitution.
|
||||
/* Rust only: canonical trait QN for a concrete trait-impl method. It may
|
||||
* remain NULL when raw cross-file provenance is ambiguous; the Rust trait
|
||||
* flag still prevents that method from being mistaken for inherent. */
|
||||
const char *impl_trait_qn;
|
||||
} CBMRegisteredFunc;
|
||||
|
||||
// Registered type with fields and method names.
|
||||
@@ -242,6 +252,24 @@ void cbm_registry_free_funcs_by_short_name(const CBMTypeRegistry *reg, const cha
|
||||
CBMFreeFuncIter *out);
|
||||
int cbm_free_func_iter_next(CBMFreeFuncIter *it);
|
||||
|
||||
// Iterate function indices for one exact (receiver QN, method name) key. This
|
||||
// exposes the existing finalized method bucket without making Rust scan the
|
||||
// project-wide func array merely to distinguish inherent and trait-impl
|
||||
// entries that intentionally share the same source-level QN. The caller may
|
||||
// filter on language-specific flags. Read-only and allocation-free.
|
||||
typedef struct {
|
||||
const CBMTypeRegistry *reg;
|
||||
const char *receiver_qn;
|
||||
const char *method_name;
|
||||
uint64_t hash;
|
||||
int chain_idx;
|
||||
int tail_i;
|
||||
int tail_end;
|
||||
} CBMMethodIter;
|
||||
void cbm_registry_methods(const CBMTypeRegistry *reg, const char *receiver_qn,
|
||||
const char *method_name, CBMMethodIter *out);
|
||||
int cbm_method_iter_next(CBMMethodIter *it);
|
||||
|
||||
// --- TS-specific helpers (return NULL for types without these signatures) ---
|
||||
|
||||
// If the type has a call signature (e.g., `interface F { (x:number): string }`), return
|
||||
|
||||
@@ -115,6 +115,41 @@ const CBMType *cbm_type_func(CBMArena *a, const char **param_names, const CBMTyp
|
||||
return t;
|
||||
}
|
||||
|
||||
const CBMType **cbm_type_materialize_signature_params(CBMArena *a, const char *const *type_texts,
|
||||
int count, CBMTypeTextParser parser,
|
||||
void *parser_ctx) {
|
||||
if (count <= 0)
|
||||
return NULL;
|
||||
|
||||
const CBMType **types =
|
||||
(const CBMType **)cbm_arena_alloc(a, ((size_t)count + 1) * sizeof(const CBMType *));
|
||||
if (!types)
|
||||
return NULL;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
const char *text = type_texts ? type_texts[i] : NULL;
|
||||
if (!text || text[0] == '\0' || strcmp(text, "?") == 0) {
|
||||
types[i] = cbm_type_unknown();
|
||||
continue;
|
||||
}
|
||||
|
||||
const CBMType *parsed = parser ? parser(a, text, parser_ctx) : NULL;
|
||||
types[i] = parsed ? parsed : cbm_type_unknown();
|
||||
}
|
||||
types[count] = NULL;
|
||||
return types;
|
||||
}
|
||||
|
||||
const CBMType *cbm_type_func_replace_returns(CBMArena *a, const CBMType *old_signature,
|
||||
const CBMType *const *new_return_types) {
|
||||
if (!old_signature || old_signature->kind != CBM_TYPE_FUNC)
|
||||
return cbm_type_unknown();
|
||||
|
||||
/* cbm_type_func only reads and then copies this vector into arena memory. */
|
||||
return cbm_type_func(a, old_signature->data.func.param_names,
|
||||
old_signature->data.func.param_types, (const CBMType **)new_return_types);
|
||||
}
|
||||
|
||||
const CBMType *cbm_type_builtin(CBMArena *a, const char *name) {
|
||||
CBMType *t = (CBMType *)cbm_arena_alloc(a, sizeof(CBMType));
|
||||
if (!t)
|
||||
|
||||
@@ -45,6 +45,9 @@ typedef enum {
|
||||
// Forward declaration
|
||||
typedef struct CBMType CBMType;
|
||||
|
||||
// Language-specific adapter used to parse one ordered signature type spelling.
|
||||
typedef const CBMType *(*CBMTypeTextParser)(CBMArena *arena, const char *text, void *parser_ctx);
|
||||
|
||||
// CBMTypeParam represents a generic type parameter with optional constraint.
|
||||
typedef struct {
|
||||
const char* name; // "T", "K", "V"
|
||||
@@ -153,6 +156,14 @@ const CBMType* cbm_type_slice(CBMArena* a, const CBMType* elem);
|
||||
const CBMType* cbm_type_map(CBMArena* a, const CBMType* key, const CBMType* value);
|
||||
const CBMType* cbm_type_channel(CBMArena* a, const CBMType* elem, int direction);
|
||||
const CBMType* cbm_type_func(CBMArena* a, const char** param_names, const CBMType** param_types, const CBMType** return_types);
|
||||
// Materialize exactly count positional parameter slots. NULL, empty, exact "?",
|
||||
// and parser failures become UNKNOWN; the returned vector is NULL-terminated.
|
||||
const CBMType **cbm_type_materialize_signature_params(CBMArena *a, const char *const *type_texts,
|
||||
int count, CBMTypeTextParser parser,
|
||||
void *parser_ctx);
|
||||
// Rebuild a FUNC with new returns while preserving its parameter names/types.
|
||||
const CBMType *cbm_type_func_replace_returns(CBMArena *a, const CBMType *old_signature,
|
||||
const CBMType *const *new_return_types);
|
||||
const CBMType* cbm_type_builtin(CBMArena* a, const char* name);
|
||||
const CBMType* cbm_type_tuple(CBMArena* a, const CBMType** elems, int count);
|
||||
const CBMType* cbm_type_type_param(CBMArena* a, const char* name);
|
||||
|
||||
+41
-13
@@ -1,10 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# repro.sh — Build + run the cumulative BUG-REPRODUCTION suite (test-repro).
|
||||
#
|
||||
# Unlike test.sh (the gating suite, must be GREEN), this suite is RED by design:
|
||||
# every case reproduces an open bug. So we distinguish two outcomes:
|
||||
# Unlike test.sh (the gating suite, must be GREEN), this suite tracks open RED
|
||||
# reproductions alongside GREEN controls and regression guards. We distinguish:
|
||||
# - BUILD/LINK failure → real breakage → exit non-zero (fail the CI job).
|
||||
# - Test redness → EXPECTED → report the count, exit 0 (green board).
|
||||
# - Reproduced RED cases → expected board data → report the count, exit 0.
|
||||
# - Any skipped case → incomplete board → report it, exit non-zero.
|
||||
#
|
||||
# Usage: scripts/repro.sh [CC=clang] [CXX=clang++] [--arch arm64|x86_64]
|
||||
set -uo pipefail
|
||||
@@ -37,7 +38,12 @@ done
|
||||
print_env "repro.sh"
|
||||
verify_compiler "$CC"
|
||||
|
||||
OUT="$ROOT/repro-out.txt"
|
||||
OUT="$(mktemp "${TMPDIR:-/tmp}/cbm-repro-out.XXXXXX")"
|
||||
if [[ -z "$OUT" ]]; then
|
||||
echo "::error::unable to create bug-repro transcript"
|
||||
exit 1
|
||||
fi
|
||||
trap 'rm -f "$OUT"' EXIT
|
||||
# A RED reproduction fails its assertion and returns EARLY — before any cleanup —
|
||||
# so LeakSanitizer would flag benign harness leaks on every red store-level test
|
||||
# and abort. The board's signal is the FAIL rows, not leak-cleanliness (the leak
|
||||
@@ -50,23 +56,45 @@ set +e
|
||||
make -j"$NPROC" -f Makefile.cbm test-repro $MAKE_ARGS 2>&1 | tee "$OUT"
|
||||
set -e
|
||||
|
||||
# The runner prints a "<N> passed[, <M> failed]" summary line only if it actually
|
||||
# ran. No summary line ⇒ the build/link failed ⇒ real breakage.
|
||||
if ! grep -qE '[0-9]+ passed' "$OUT"; then
|
||||
# The runner's final aggregate is the only line that starts with the count.
|
||||
# Per-suite lines also contain "N passed, M failed", so selecting the first
|
||||
# match undercounts every multi-suite run.
|
||||
summary_line="$(grep -E '^[[:space:]]*[0-9]+ passed(, [0-9]+ failed)?(, [0-9]+ skipped)?[[:space:]]*$' "$OUT" | tail -1 || true)"
|
||||
if [[ -z "$summary_line" ]]; then
|
||||
echo "::error::bug-repro runner did not execute — build or link failure"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
reproduced=$(grep -oE '[0-9]+ failed' "$OUT" | head -1 | grep -oE '[0-9]+' || echo 0)
|
||||
green=$(grep -oE '[0-9]+ passed' "$OUT" | head -1 | grep -oE '[0-9]+' || echo 0)
|
||||
green="$(printf '%s\n' "$summary_line" | grep -oE '[0-9]+ passed' | grep -oE '[0-9]+')"
|
||||
reproduced="$(printf '%s\n' "$summary_line" | grep -oE '[0-9]+ failed' | grep -oE '[0-9]+' || echo 0)"
|
||||
skipped="$(printf '%s\n' "$summary_line" | grep -oE '[0-9]+ skipped' | grep -oE '[0-9]+' || echo 0)"
|
||||
|
||||
{
|
||||
if ((green + reproduced == 0)); then
|
||||
echo "::error::bug-repro runner executed zero tests"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
emit_board_summary() {
|
||||
echo "## Bug-reproduction board — ${OS:-$(uname -s)} ${ARCH:-}"
|
||||
echo ""
|
||||
echo "- **${reproduced}** open bug(s) still reproduced (RED — expected)"
|
||||
echo "- **${green}** case(s) PASSING — candidate-fixed → verify + close the issue + promote the guard to the gating suite"
|
||||
} >> "${GITHUB_STEP_SUMMARY:-/dev/stderr}"
|
||||
echo "- **${green}** case(s) passing control(s) or candidate-fix candidate(s); verify each RED→GREEN transition before closing an issue"
|
||||
if ((skipped > 0)); then
|
||||
echo "- **${skipped}** case(s) SKIPPED — board is incomplete"
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then
|
||||
emit_board_summary >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
emit_board_summary >&2
|
||||
fi
|
||||
|
||||
echo "=== bug-repro board: ${reproduced} reproduced (RED), ${green} passing control(s) or candidate-fix candidate(s) ==="
|
||||
if ((skipped > 0)); then
|
||||
echo "::warning::bug-repro board incomplete — ${skipped} case(s) skipped"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== bug-repro board: ${reproduced} reproduced (RED), ${green} passing (candidate-fixed) ==="
|
||||
# Green board: the suite ran. Redness is the data, not a job failure.
|
||||
exit 0
|
||||
|
||||
+1
-1
@@ -576,7 +576,7 @@ static const char skill_content[] =
|
||||
"\n"
|
||||
"## Edge Types\n"
|
||||
"CALLS, HTTP_CALLS, ASYNC_CALLS, DATA_FLOWS, IMPORTS, DEFINES, DEFINES_METHOD,\n"
|
||||
"HANDLES, IMPLEMENTS, OVERRIDE, USAGE, CONFIGURES, FILE_CHANGES_WITH,\n"
|
||||
"HANDLES, IMPLEMENTS, OVERRIDE, USAGE, CALL_REFERENCE, CONFIGURES, FILE_CHANGES_WITH,\n"
|
||||
"SIMILAR_TO, SEMANTICALLY_RELATED, CONTAINS_FILE, CONTAINS_FOLDER,\n"
|
||||
"CONTAINS_PACKAGE\n"
|
||||
"\n"
|
||||
|
||||
@@ -48,7 +48,7 @@ static const char *ALWAYS_SKIP_DIRS[] = {
|
||||
/* Deploy */
|
||||
".vercel", ".netlify", "deploy", "deployed",
|
||||
/* Misc */
|
||||
".qdrant_code_embeddings", ".tmp", "vendor", "vendored", NULL};
|
||||
".codebase-memory", ".qdrant_code_embeddings", ".tmp", "vendor", "vendored", NULL};
|
||||
|
||||
static const char *FAST_SKIP_DIRS[] = {
|
||||
"generated", "gen", "auto-generated", "fixtures", "testdata", "test_data",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "foundation/constants.h"
|
||||
#include "foundation/platform.h" /* cbm_safe_getenv */
|
||||
#include "foundation/compat_fs.h"
|
||||
#include "foundation/sha256.h"
|
||||
|
||||
enum { MAX_CONFIG_SIZE = 65536 };
|
||||
#include "foundation/log.h"
|
||||
@@ -29,6 +30,26 @@ enum { MAX_CONFIG_SIZE = 65536 };
|
||||
|
||||
static const cbm_userconfig_t *g_userconfig = NULL;
|
||||
|
||||
static void userconfig_source_digest(const char *state, const void *bytes, size_t len,
|
||||
char out[CBM_SHA256_HEX_LEN + 1]) {
|
||||
static const char domain[] = "cbm-userconfig-source-v1";
|
||||
cbm_sha256_ctx sha;
|
||||
cbm_sha256_init(&sha);
|
||||
cbm_sha256_update(&sha, domain, sizeof(domain));
|
||||
cbm_sha256_update(&sha, state, strlen(state) + 1);
|
||||
if (bytes && len > 0) {
|
||||
cbm_sha256_update(&sha, bytes, len);
|
||||
}
|
||||
uint8_t digest[CBM_SHA256_DIGEST_LEN];
|
||||
cbm_sha256_final(&sha, digest);
|
||||
static const char hex[] = "0123456789abcdef";
|
||||
for (int i = 0; i < CBM_SHA256_DIGEST_LEN; i++) {
|
||||
out[i * 2] = hex[digest[i] >> 4];
|
||||
out[i * 2 + 1] = hex[digest[i] & 0x0f];
|
||||
}
|
||||
out[CBM_SHA256_HEX_LEN] = '\0';
|
||||
}
|
||||
|
||||
void cbm_set_user_lang_config(const cbm_userconfig_t *cfg) {
|
||||
g_userconfig = cfg;
|
||||
}
|
||||
@@ -232,7 +253,9 @@ static int parse_extra_extensions(yyjson_val *root, cbm_userext_t **entries, int
|
||||
* Silently ignores missing files. Logs warnings for corrupt JSON.
|
||||
* Returns 0 on success (or absent file), -1 on alloc failure.
|
||||
*/
|
||||
static int load_config_file(const char *path, cbm_userext_t **entries, int *count) {
|
||||
static int load_config_file(const char *path, cbm_userext_t **entries, int *count,
|
||||
char source_sha256[CBM_SHA256_HEX_LEN + 1]) {
|
||||
userconfig_source_digest("missing-or-unreadable", NULL, 0, source_sha256);
|
||||
FILE *f = cbm_fopen(path, "rb");
|
||||
if (!f) {
|
||||
return 0; /* file absent — silently ignore */
|
||||
@@ -240,11 +263,13 @@ static int load_config_file(const char *path, cbm_userext_t **entries, int *coun
|
||||
|
||||
if (fseek(f, 0, SEEK_END) != 0) {
|
||||
(void)fclose(f);
|
||||
userconfig_source_digest("seek-error", NULL, 0, source_sha256);
|
||||
return 0;
|
||||
}
|
||||
long len = ftell(f);
|
||||
if (fseek(f, 0, SEEK_SET) != 0) {
|
||||
(void)fclose(f);
|
||||
userconfig_source_digest("seek-error", NULL, 0, source_sha256);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -252,6 +277,9 @@ static int load_config_file(const char *path, cbm_userext_t **entries, int *coun
|
||||
(void)fclose(f);
|
||||
if (len > MAX_CONFIG_SIZE) {
|
||||
cbm_log_warn("userconfig.file_too_large", "path", path);
|
||||
userconfig_source_digest("oversized", NULL, 0, source_sha256);
|
||||
} else {
|
||||
userconfig_source_digest("empty", NULL, 0, source_sha256);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -268,6 +296,7 @@ static int load_config_file(const char *path, cbm_userext_t **entries, int *coun
|
||||
nread = (size_t)len;
|
||||
}
|
||||
buf[nread] = '\0';
|
||||
userconfig_source_digest("present", buf, nread, source_sha256);
|
||||
|
||||
yyjson_doc *doc = yyjson_read(buf, nread, 0);
|
||||
free(buf);
|
||||
@@ -301,7 +330,7 @@ cbm_userconfig_t *cbm_userconfig_load(const char *repo_path) {
|
||||
char global_path[PATH_BUF_SZ];
|
||||
snprintf(global_path, sizeof(global_path), "%s/codebase-memory-mcp/config.json", cfg_fallback);
|
||||
|
||||
if (load_config_file(global_path, &entries, &count) != 0) {
|
||||
if (load_config_file(global_path, &entries, &count, cfg->global_source_sha256) != 0) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
free(entries[i].ext);
|
||||
}
|
||||
@@ -313,11 +342,12 @@ cbm_userconfig_t *cbm_userconfig_load(const char *repo_path) {
|
||||
int global_count = count; /* entries[0..global_count) are from global */
|
||||
|
||||
/* ── Step 2: Load project config ── */
|
||||
userconfig_source_digest("not-applicable", NULL, 0, cfg->project_source_sha256);
|
||||
if (repo_path && repo_path[0]) {
|
||||
char project_path[PATH_BUF_SZ];
|
||||
snprintf(project_path, sizeof(project_path), "%s/.codebase-memory.json", repo_path);
|
||||
|
||||
if (load_config_file(project_path, &entries, &count) != 0) {
|
||||
if (load_config_file(project_path, &entries, &count, cfg->project_source_sha256) != 0) {
|
||||
/* Free already-allocated entries */
|
||||
for (int i = 0; i < count; i++) {
|
||||
free(entries[i].ext);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#define CBM_USERCONFIG_H
|
||||
|
||||
#include "cbm.h" /* CBMLanguage */
|
||||
#include "foundation/sha256.h"
|
||||
|
||||
/* ── Types ──────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -29,6 +30,9 @@ typedef struct {
|
||||
typedef struct {
|
||||
cbm_userext_t *entries; /* heap-allocated array */
|
||||
int count; /* number of entries */
|
||||
/* Digests of the exact bytes/state consumed by cbm_userconfig_load(). */
|
||||
char global_source_sha256[CBM_SHA256_HEX_LEN + 1];
|
||||
char project_source_sha256[CBM_SHA256_HEX_LEN + 1];
|
||||
} cbm_userconfig_t;
|
||||
|
||||
/* ── API ────────────────────────────────────────────────────────── */
|
||||
|
||||
+125
-8
@@ -11,6 +11,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
@@ -118,6 +119,40 @@ cbm_dirent_t *cbm_readdir(cbm_dir_t *d) {
|
||||
return &d->entry;
|
||||
}
|
||||
|
||||
int cbm_path_info_utf8(const char *path, cbm_path_info_t *out) {
|
||||
if (!path || !out) {
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
wchar_t *wpath = cbm_utf8_to_wide(path);
|
||||
if (!wpath) {
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
WIN32_FILE_ATTRIBUTE_DATA data;
|
||||
BOOL ok = GetFileAttributesExW(wpath, GetFileExInfoStandard, &data);
|
||||
free(wpath);
|
||||
if (!ok) {
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
memset(out, 0, sizeof(*out));
|
||||
out->is_directory = (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
||||
out->is_symlink = (data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0;
|
||||
out->is_regular = !out->is_directory && !out->is_symlink;
|
||||
ULARGE_INTEGER file_size;
|
||||
file_size.LowPart = data.nFileSizeLow;
|
||||
file_size.HighPart = data.nFileSizeHigh;
|
||||
out->size = (int64_t)file_size.QuadPart;
|
||||
ULARGE_INTEGER written;
|
||||
written.LowPart = data.ftLastWriteTime.dwLowDateTime;
|
||||
written.HighPart = data.ftLastWriteTime.dwHighDateTime;
|
||||
enum { NANOSECONDS_PER_WINDOWS_TICK = 100 };
|
||||
const uint64_t windows_to_unix_ticks = UINT64_C(116444736000000000);
|
||||
out->mtime_ns =
|
||||
written.QuadPart >= windows_to_unix_ticks
|
||||
? (int64_t)((written.QuadPart - windows_to_unix_ticks) * NANOSECONDS_PER_WINDOWS_TICK)
|
||||
: 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void cbm_closedir(cbm_dir_t *d) {
|
||||
if (d) {
|
||||
if (d->find_handle != INVALID_HANDLE_VALUE) {
|
||||
@@ -658,13 +693,51 @@ cbm_dirent_t *cbm_readdir(cbm_dir_t *d) {
|
||||
}
|
||||
memcpy(d->entry.name, de->d_name, nlen);
|
||||
d->entry.name[nlen] = '\0';
|
||||
d->entry.is_dir = (de->d_type == DT_DIR);
|
||||
d->entry.d_type = de->d_type;
|
||||
unsigned char type = de->d_type;
|
||||
#if defined(DT_UNKNOWN) && defined(AT_SYMLINK_NOFOLLOW)
|
||||
if (type == DT_UNKNOWN) {
|
||||
struct stat state;
|
||||
if (fstatat(dirfd(d->dir), de->d_name, &state, AT_SYMLINK_NOFOLLOW) == 0) {
|
||||
if (S_ISDIR(state.st_mode)) {
|
||||
type = DT_DIR;
|
||||
} else if (S_ISREG(state.st_mode)) {
|
||||
type = DT_REG;
|
||||
} else if (S_ISLNK(state.st_mode)) {
|
||||
type = DT_LNK;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
d->entry.is_dir = (type == DT_DIR);
|
||||
d->entry.d_type = type;
|
||||
return &d->entry;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int cbm_path_info_utf8(const char *path, cbm_path_info_t *out) {
|
||||
if (!path || !out) {
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
struct stat state;
|
||||
if (lstat(path, &state) != 0) {
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
memset(out, 0, sizeof(*out));
|
||||
out->is_regular = S_ISREG(state.st_mode);
|
||||
out->is_directory = S_ISDIR(state.st_mode);
|
||||
out->is_symlink = S_ISLNK(state.st_mode);
|
||||
out->size = (int64_t)state.st_size;
|
||||
#ifdef __APPLE__
|
||||
out->mtime_ns = ((int64_t)state.st_mtimespec.tv_sec * INT64_C(1000000000)) +
|
||||
(int64_t)state.st_mtimespec.tv_nsec;
|
||||
#else
|
||||
out->mtime_ns =
|
||||
((int64_t)state.st_mtim.tv_sec * INT64_C(1000000000)) + (int64_t)state.st_mtim.tv_nsec;
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
void cbm_closedir(cbm_dir_t *d) {
|
||||
if (d) {
|
||||
if (d->dir) {
|
||||
@@ -871,23 +944,67 @@ int cbm_rename_replace(const char *src, const char *dst) {
|
||||
#endif
|
||||
}
|
||||
|
||||
int cbm_rename_noreplace(const char *src, const char *dst) {
|
||||
if (!src || !dst || !src[0] || !dst[0]) {
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
wchar_t *wsrc = cbm_utf8_to_wide(src);
|
||||
wchar_t *wdst = cbm_utf8_to_wide(dst);
|
||||
int ret = CBM_NOT_FOUND;
|
||||
if (wsrc && wdst) {
|
||||
ret = MoveFileExW(wsrc, wdst, MOVEFILE_COPY_ALLOWED | MOVEFILE_WRITE_THROUGH)
|
||||
? 0
|
||||
: CBM_NOT_FOUND;
|
||||
}
|
||||
free(wsrc);
|
||||
free(wdst);
|
||||
return ret;
|
||||
#else
|
||||
/* link()+unlink() provides no-overwrite semantics portably (including
|
||||
* macOS, where renameat2(RENAME_NOREPLACE) is unavailable). Both paths
|
||||
* are adjacent database files and therefore on the same filesystem. */
|
||||
if (link(src, dst) != 0) {
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
if (unlink(src) != 0) {
|
||||
int saved_errno = errno;
|
||||
(void)unlink(dst);
|
||||
errno = saved_errno;
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Remove a SQLite database's -wal/-shm sidecars (both platforms). Any code
|
||||
* path that installs a FRESH database file where a previous generation lived
|
||||
* must remove them before the new generation can be opened: SQLite decides
|
||||
* whether to replay a WAL purely from the sidecar's own header/checksums, so
|
||||
* a leftover WAL can splice old-generation pages into the new file (#897). */
|
||||
void cbm_remove_db_sidecars(const char *db_path) {
|
||||
int cbm_remove_db_sidecars_checked(const char *db_path) {
|
||||
if (!db_path || !db_path[0]) {
|
||||
return;
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
enum { SIDECAR_PATH_MAX = 4096 };
|
||||
char side[SIDECAR_PATH_MAX];
|
||||
int n = snprintf(side, sizeof(side), "%s-wal", db_path);
|
||||
if (n > 0 && (size_t)n < sizeof(side)) {
|
||||
(void)cbm_unlink(side);
|
||||
if (n <= 0 || (size_t)n >= sizeof(side)) {
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
if (cbm_unlink(side) != 0 && errno != ENOENT) {
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
n = snprintf(side, sizeof(side), "%s-shm", db_path);
|
||||
if (n > 0 && (size_t)n < sizeof(side)) {
|
||||
(void)cbm_unlink(side);
|
||||
if (n <= 0 || (size_t)n >= sizeof(side)) {
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
if (cbm_unlink(side) != 0 && errno != ENOENT) {
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void cbm_remove_db_sidecars(const char *db_path) {
|
||||
(void)cbm_remove_db_sidecars_checked(db_path);
|
||||
}
|
||||
|
||||
@@ -9,12 +9,13 @@
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/* ── Directory iteration ──────────────────────────────────────── */
|
||||
|
||||
/* Max filename length (MAX_PATH on Windows, NAME_MAX on POSIX). */
|
||||
#define CBM_DIRENT_NAME_MAX 260
|
||||
#define CBM_DIRENT_NAME_MAX 1024
|
||||
|
||||
typedef struct cbm_dir cbm_dir_t;
|
||||
|
||||
@@ -24,6 +25,20 @@ typedef struct {
|
||||
unsigned char d_type; /* DT_REG, DT_DIR, DT_LNK, etc. (POSIX only, 0 on Windows) */
|
||||
} cbm_dirent_t;
|
||||
|
||||
/* Locale-independent metadata for a UTF-8 path. Symlinks/reparse points are
|
||||
* reported rather than followed so semantic-input walkers cannot leave the
|
||||
* repository through an alias. mtime_ns is Unix-epoch nanoseconds. */
|
||||
typedef struct {
|
||||
bool is_regular;
|
||||
bool is_directory;
|
||||
bool is_symlink;
|
||||
int64_t size;
|
||||
int64_t mtime_ns;
|
||||
} cbm_path_info_t;
|
||||
|
||||
/* Returns 0 on success and -1 when the path cannot be inspected. */
|
||||
int cbm_path_info_utf8(const char *path, cbm_path_info_t *out);
|
||||
|
||||
/* Open a directory for iteration. Returns NULL on error. */
|
||||
cbm_dir_t *cbm_opendir(const char *path);
|
||||
|
||||
@@ -50,9 +65,15 @@ int cbm_unlink(const char *path);
|
||||
* do this before the new generation can be opened; a leftover WAL is
|
||||
* otherwise replayed on top of the new file (#897). */
|
||||
void cbm_remove_db_sidecars(const char *db_path);
|
||||
/* Checked form used at an atomic replacement boundary. Missing sidecars are
|
||||
* success; any other deletion or path-construction failure is reported. */
|
||||
int cbm_remove_db_sidecars_checked(const char *db_path);
|
||||
/* rename() that replaces an existing destination on every platform
|
||||
* (Windows rename fails with EEXIST; this uses write-through MoveFileExW). */
|
||||
int cbm_rename_replace(const char *src, const char *dst);
|
||||
/* Move a regular file only when dst does not exist. Never overwrites dst.
|
||||
* Used for collision-safe evidence quarantine beside a database. */
|
||||
int cbm_rename_noreplace(const char *src, const char *dst);
|
||||
/* Canonicalize an EXISTING path (realpath / wide GetFullPathNameW). Locale-
|
||||
* independent on Windows — never routes UTF-8 through the ANSI CRT (#973).
|
||||
* out must be >= 4096 bytes. Returns 1 on success, 0 otherwise. */
|
||||
|
||||
+2
-1
@@ -375,7 +375,8 @@ static const tool_def_t TOOLS[] = {
|
||||
"RESPONSE: prefix-grouped tree rows by default — a shared (qn-prefix, file) group "
|
||||
"header printed once, then `name label lines in out` per row (full qn = group prefix "
|
||||
"+ dot + name). in/out = TOTAL degree across ALL edge types (DEFINES, "
|
||||
"USAGE, CALLS, ...), NOT caller/callee counts — use trace_path for callers. Add per-node "
|
||||
"USAGE, CALLS, CALL_REFERENCE, ...), NOT caller/callee counts — use trace_path for callers. "
|
||||
"Add per-node "
|
||||
"property columns via "
|
||||
"fields (e.g. [\"complexity\",\"signature\",\"docstring\"]); format=\"json\" returns "
|
||||
"the SAME tree model as structured JSON. "
|
||||
|
||||
@@ -574,7 +574,7 @@ int cbm_artifact_export(const char *db_path, const char *repo_path, const char *
|
||||
/* Get node/edge counts for metadata */
|
||||
int nodes = 0;
|
||||
int edges = 0;
|
||||
cbm_store_t *count_store = cbm_store_open_path(db_path);
|
||||
cbm_store_t *count_store = cbm_store_open_path_query(db_path);
|
||||
if (count_store) {
|
||||
nodes = cbm_store_count_nodes(count_store, project_name);
|
||||
edges = cbm_store_count_edges(count_store, project_name);
|
||||
|
||||
+706
-66
@@ -25,7 +25,9 @@
|
||||
#include "graph_buffer/graph_buffer.h"
|
||||
#include "foundation/constants.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* Confidence floor below which LSP-resolved calls are ignored and the
|
||||
@@ -35,6 +37,16 @@
|
||||
* (Go, C/C++, Python, PHP). */
|
||||
#define CBM_LSP_CONFIDENCE_FLOOR 0.6f
|
||||
|
||||
#if defined(CBM_CALL_REFERENCE_LOOKUP_TEST_API) && CBM_CALL_REFERENCE_LOOKUP_TEST_API
|
||||
/* Test-build-only operation counter implemented by pass_usages.c. Keeping the
|
||||
* increment behind this seam lets the regression measure the real sequential
|
||||
* and fused-parallel materialization helper without retaining instrumentation
|
||||
* in production builds. */
|
||||
void cbm_pipeline_lsp_reference_lookup_test_note_row(void);
|
||||
#else
|
||||
static inline void cbm_pipeline_lsp_reference_lookup_test_note_row(void) {}
|
||||
#endif
|
||||
|
||||
/* Bare last segment of a (possibly qualified) name, splitting on the LAST
|
||||
* member/scope separator. C++ textual callees carry `::` (Class::method,
|
||||
* Ns::f) and `->` (p->run), while the LSP records dotted internal QNs
|
||||
@@ -61,6 +73,9 @@ static inline const char *cbm_lsp_bare_segment(const char *name) {
|
||||
seg = p + SKIP_ONE;
|
||||
}
|
||||
}
|
||||
while (*seg && isspace((unsigned char)*seg)) {
|
||||
seg++;
|
||||
}
|
||||
return seg;
|
||||
}
|
||||
|
||||
@@ -105,11 +120,238 @@ static inline bool cbm_pipeline_lsp_allow_tail_match(CBMLanguage lang) {
|
||||
return lang == CBM_LANG_JAVA || lang == CBM_LANG_KOTLIN;
|
||||
}
|
||||
|
||||
/* When a JVM callable-reference occurrence has no exact semantic target, its
|
||||
* ordinary-USAGE fallback must still respect package/import reachability.
|
||||
* `unique_name` and `suffix_match` are project-wide guesses: admitting either
|
||||
* would bind an unresolved `::handler` in one package to an unrelated callable
|
||||
* in another. Other languages retain their legacy value-usage fallback. */
|
||||
static inline bool cbm_pipeline_call_reference_usage_fallback_allowed(CBMLanguage lang,
|
||||
const char *strategy) {
|
||||
if (lang != CBM_LANG_JAVA && lang != CBM_LANG_KOTLIN) {
|
||||
return true;
|
||||
}
|
||||
if (!strategy || !strategy[0]) {
|
||||
return false;
|
||||
}
|
||||
return strcmp(strategy, "unique_name") != 0 && strcmp(strategy, "suffix_match") != 0;
|
||||
}
|
||||
|
||||
static inline bool cbm_pipeline_reference_candidate_fallback_allowed(
|
||||
CBMLanguage lang, const CBMUsage *usage, const char *strategy, const char *const *import_names,
|
||||
int import_count) {
|
||||
if (!usage) {
|
||||
return false;
|
||||
}
|
||||
bool reference_candidate =
|
||||
usage->kind == CBM_USAGE_CALL_REFERENCE ||
|
||||
(lang == CBM_LANG_KOTLIN && usage->kind == CBM_USAGE_VALUE && usage->may_be_call_reference);
|
||||
if (!reference_candidate ||
|
||||
cbm_pipeline_call_reference_usage_fallback_allowed(lang, strategy)) {
|
||||
return true;
|
||||
}
|
||||
/* Kotlin's project import map currently names the imported module rather
|
||||
* than the member. A syntactically explicit, locally imported `::name`
|
||||
* may therefore resolve by unique name after LSP proves it is not a
|
||||
* callable. Admit that ordinary USAGE only when the import map confirms
|
||||
* the local spelling; an unimported same-name symbol remains unreachable. */
|
||||
if (lang == CBM_LANG_KOTLIN && usage->ref_name && import_names) {
|
||||
for (int i = 0; i < import_count; i++) {
|
||||
if (import_names[i] && strcmp(import_names[i], usage->ref_name) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Explicit reference syntax always participates in the semantic join. An
|
||||
* ordinary value participates only when extraction marked the narrow direct
|
||||
* argument shape in a language whose resolver can prove callable identity;
|
||||
* without an exact LSP record it remains USAGE. */
|
||||
static inline bool cbm_pipeline_usage_semantic_reference_candidate(const CBMUsage *usage) {
|
||||
if (!usage) {
|
||||
return false;
|
||||
}
|
||||
return usage->kind == CBM_USAGE_CALL_REFERENCE ||
|
||||
(usage->kind == CBM_USAGE_VALUE && usage->may_be_call_reference);
|
||||
}
|
||||
|
||||
/* Lexical extraction has whole-scope information that a source-order semantic
|
||||
* walk can lack. In Python and JS/TS, a later local binding makes an earlier
|
||||
* same-named value local too; an LSP row that still points at the module
|
||||
* function must not override that proof. A callable-alias row is different:
|
||||
* it explicitly resolves the local binding itself to its assigned callable. */
|
||||
static inline bool cbm_pipeline_usage_allows_semantic_reference(const CBMUsage *usage,
|
||||
const CBMResolvedCall *resolved) {
|
||||
if (!usage || !resolved) {
|
||||
return false;
|
||||
}
|
||||
if (usage->kind != CBM_USAGE_VALUE || !usage->semantic_reference_blocked ||
|
||||
!usage->semantic_reference_local_shadow) {
|
||||
return true;
|
||||
}
|
||||
return resolved->strategy && strcmp(resolved->strategy, "lsp_callable_alias") == 0;
|
||||
}
|
||||
|
||||
/* A blocked module value can still retain an ordinary edge to a same-named
|
||||
* non-callable declaration, but it must never fall back to an executable
|
||||
* declaration. A local lexical shadow rejects every raw-name fallback because
|
||||
* even a same-named module variable is a different binding. Exact semantic
|
||||
* rows are joined before either rule. This label check keeps sequential and
|
||||
* fused resolution aligned. */
|
||||
static inline bool cbm_pipeline_node_is_callable_target(const cbm_gbuf_node_t *node) {
|
||||
if (!node || !node->label) {
|
||||
return false;
|
||||
}
|
||||
return strcmp(node->label, "Function") == 0 || strcmp(node->label, "Method") == 0 ||
|
||||
strcmp(node->label, "Constructor") == 0 || strcmp(node->label, "Class") == 0;
|
||||
}
|
||||
|
||||
static inline int cbm_pipeline_qn_class_method_tail_eq(const char *qn, const char *tail) {
|
||||
const char *qt = cbm_pipeline_qn_class_method_tail(qn);
|
||||
return qt && tail && strcmp(qt, tail) == 0;
|
||||
}
|
||||
|
||||
static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_target_node_policy(
|
||||
const cbm_gbuf_t *gbuf, const char *project_name, const char *callee_qn, bool allow_tail_match,
|
||||
bool allow_unique_callable_fallback);
|
||||
|
||||
/* Local and cross passes may spell one exact invocation target with and without
|
||||
* the project prefix. Collapse that cross-language case only when exact graph
|
||||
* lookup maps both spellings to one materialized node. JVM passes additionally
|
||||
* reconcile file/project and declared-package QNs below; Class.method-tail
|
||||
* equality alone is insufficient because two packages can declare the same
|
||||
* class and method. */
|
||||
static inline bool cbm_pipeline_invocation_targets_equal(const char *left_qn, const char *right_qn,
|
||||
const cbm_gbuf_t *gbuf,
|
||||
const char *project_name,
|
||||
bool allow_tail_match) {
|
||||
if (!left_qn || !right_qn) {
|
||||
return false;
|
||||
}
|
||||
if (strcmp(left_qn, right_qn) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (!gbuf || !project_name) {
|
||||
return false;
|
||||
}
|
||||
const cbm_gbuf_node_t *left_exact =
|
||||
cbm_pipeline_lsp_target_node_policy(gbuf, project_name, left_qn, false, false);
|
||||
const cbm_gbuf_node_t *right_exact =
|
||||
cbm_pipeline_lsp_target_node_policy(gbuf, project_name, right_qn, false, false);
|
||||
if (left_exact && right_exact) {
|
||||
return left_exact->id == right_exact->id;
|
||||
}
|
||||
if (!allow_tail_match) {
|
||||
return false;
|
||||
}
|
||||
const char *left_tail = cbm_pipeline_qn_class_method_tail(left_qn);
|
||||
const char *right_tail = cbm_pipeline_qn_class_method_tail(right_qn);
|
||||
if (!left_tail || !right_tail || strcmp(left_tail, right_tail) != 0) {
|
||||
return false;
|
||||
}
|
||||
/* Two package-shaped rows that both reach a node only through the tail
|
||||
* fallback are still distinct semantic claims. Require at least one row
|
||||
* to name the materialized node exactly (or by project-prefix retry). */
|
||||
if (!left_exact && !right_exact) {
|
||||
return false;
|
||||
}
|
||||
const cbm_gbuf_node_t *left =
|
||||
left_exact ? left_exact
|
||||
: cbm_pipeline_lsp_target_node_policy(gbuf, project_name, left_qn, true, true);
|
||||
const cbm_gbuf_node_t *right =
|
||||
right_exact ? right_exact
|
||||
: cbm_pipeline_lsp_target_node_policy(gbuf, project_name, right_qn, true, true);
|
||||
return left && right && left->id == right->id;
|
||||
}
|
||||
|
||||
/* A semantic join is occurrence-exact only when both producers recorded the
|
||||
* same non-empty source span. Byte spans, unlike line numbers, distinguish
|
||||
* repeated same-named references/operators on one line. */
|
||||
static inline bool cbm_pipeline_source_site_present(uint32_t start, uint32_t end) {
|
||||
return end > start;
|
||||
}
|
||||
|
||||
static inline bool cbm_pipeline_source_site_legacy(uint32_t start, uint32_t end) {
|
||||
return start == 0 && end == 0;
|
||||
}
|
||||
|
||||
static inline bool cbm_pipeline_source_site_eq(uint32_t lhs_start, uint32_t lhs_end,
|
||||
uint32_t rhs_start, uint32_t rhs_end) {
|
||||
return cbm_pipeline_source_site_present(lhs_start, lhs_end) &&
|
||||
cbm_pipeline_source_site_present(rhs_start, rhs_end) && lhs_start == rhs_start &&
|
||||
lhs_end == rhs_end;
|
||||
}
|
||||
|
||||
static inline bool cbm_pipeline_source_occurrence_eq(uint32_t lhs_start, uint32_t lhs_end,
|
||||
CBMSourceOrigin lhs_origin, uint32_t rhs_start,
|
||||
uint32_t rhs_end, CBMSourceOrigin rhs_origin) {
|
||||
return lhs_origin == rhs_origin &&
|
||||
cbm_pipeline_source_site_eq(lhs_start, lhs_end, rhs_start, rhs_end);
|
||||
}
|
||||
|
||||
/* Rank an invocation record for one parser carrier. Exact occurrence identity
|
||||
* is stronger than the legacy 0:0 compatibility join. Partial/reversed spans
|
||||
* are corrupt metadata and never participate in either class. */
|
||||
static inline int cbm_pipeline_invocation_site_rank(const CBMResolvedCall *resolved,
|
||||
const CBMCall *call) {
|
||||
if (!resolved || !call) {
|
||||
return 0;
|
||||
}
|
||||
bool call_has_site =
|
||||
cbm_pipeline_source_site_present(call->site_start_byte, call->site_end_byte);
|
||||
bool call_is_legacy =
|
||||
cbm_pipeline_source_site_legacy(call->site_start_byte, call->site_end_byte);
|
||||
if (!call_has_site && !call_is_legacy) {
|
||||
return 0;
|
||||
}
|
||||
if (cbm_pipeline_source_site_present(resolved->site_start_byte, resolved->site_end_byte)) {
|
||||
return cbm_pipeline_source_occurrence_eq(call->site_start_byte, call->site_end_byte,
|
||||
call->source_origin, resolved->site_start_byte,
|
||||
resolved->site_end_byte, resolved->source_origin)
|
||||
? 2
|
||||
: 0;
|
||||
}
|
||||
if (!cbm_pipeline_source_site_legacy(resolved->site_start_byte, resolved->site_end_byte)) {
|
||||
return 0;
|
||||
}
|
||||
return call->requires_lsp_resolution || call->source_origin != resolved->source_origin ? 0 : 1;
|
||||
}
|
||||
|
||||
static inline bool cbm_pipeline_invocation_reason_join_strategy(const char *strategy) {
|
||||
return strategy &&
|
||||
(strcmp(strategy, "lsp_func_ptr") == 0 || strcmp(strategy, "lsp_callable_alias") == 0 ||
|
||||
strcmp(strategy, "lsp_dll_resolve") == 0 ||
|
||||
strcmp(strategy, "lsp_method_ref_ctor") == 0 ||
|
||||
strcmp(strategy, "lsp_method_ref_ctor_synth") == 0 ||
|
||||
strcmp(strategy, "lsp_dict_dispatch") == 0 ||
|
||||
strcmp(strategy, "lsp_import_alias") == 0 ||
|
||||
strcmp(strategy, "php_method_dynamic") == 0);
|
||||
}
|
||||
|
||||
static inline bool cbm_pipeline_invocation_leaf_matches(const CBMResolvedCall *resolved,
|
||||
const CBMCall *call, int site_rank) {
|
||||
if (!resolved || !resolved->callee_qn || !call || !call->callee_name) {
|
||||
return false;
|
||||
}
|
||||
const char *resolved_leaf = cbm_lsp_bare_segment(resolved->callee_qn);
|
||||
const char *call_leaf = cbm_lsp_bare_segment(call->callee_name);
|
||||
if (resolved_leaf && call_leaf && strcmp(resolved_leaf, call_leaf) == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Destructors intentionally join by their exact delete-expression
|
||||
* occurrence because the parser carrier starts as a neutral "~" marker. */
|
||||
if (site_rank == 2 && call->requires_lsp_resolution && resolved->strategy &&
|
||||
strcmp(resolved->strategy, "lsp_destructor") == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Indirect resolvers retain the textual source leaf in reason. */
|
||||
return cbm_pipeline_invocation_reason_join_strategy(resolved->strategy) && resolved->reason &&
|
||||
call_leaf && strcmp(cbm_lsp_bare_segment(resolved->reason), call_leaf) == 0;
|
||||
}
|
||||
|
||||
/* Look up the highest-confidence LSP-resolved call entry whose caller QN
|
||||
* matches the textual call's enclosing function and whose callee QN
|
||||
* short-name matches the textual callee. Returns a pointer into `arr`
|
||||
@@ -119,8 +361,9 @@ static inline int cbm_pipeline_qn_class_method_tail_eq(const char *qn, const cha
|
||||
* 1. exact caller_qn + callee short-name match wins first;
|
||||
* 2. if no exact caller match exists AND allow_tail_match is set
|
||||
* (JVM callers only, see cbm_pipeline_lsp_allow_tail_match), a
|
||||
* unique Class.method tail match between rc->caller_qn and
|
||||
* call->enclosing_func_qn may win;
|
||||
* unique Class.method tail match may win; an occurrence-exact row may
|
||||
* also reconcile Kotlin's package-vs-file top-level owner QNs when their
|
||||
* bare caller leaves agree;
|
||||
* 3. ambiguous tails return NULL so the registry fallback stays in
|
||||
* control.
|
||||
*
|
||||
@@ -129,8 +372,9 @@ static inline int cbm_pipeline_qn_class_method_tail_eq(const char *qn, const cha
|
||||
*
|
||||
* The pointer returned aliases into `arr` and stays valid as long as the
|
||||
* underlying CBMFileResult is alive. */
|
||||
static inline const CBMResolvedCall *cbm_pipeline_find_lsp_resolution(
|
||||
const CBMResolvedCallArray *arr, const CBMCall *call, bool allow_tail_match) {
|
||||
static inline const CBMResolvedCall *cbm_pipeline_find_lsp_resolution_in_graph(
|
||||
const CBMResolvedCallArray *arr, const CBMCall *call, bool allow_tail_match,
|
||||
const cbm_gbuf_t *gbuf, const char *project_name) {
|
||||
if (!arr || arr->count == 0 || !call) {
|
||||
return NULL;
|
||||
}
|
||||
@@ -138,9 +382,14 @@ static inline const CBMResolvedCall *cbm_pipeline_find_lsp_resolution(
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const CBMResolvedCall *best_exact = NULL;
|
||||
const CBMResolvedCall *best_exact_caller = NULL;
|
||||
int best_exact_caller_rank = 0;
|
||||
bool exact_caller_ambiguous = false;
|
||||
for (int i = 0; i < arr->count; i++) {
|
||||
const CBMResolvedCall *rc = &arr->items[i];
|
||||
if (rc->kind != CBM_RESOLVED_INVOCATION) {
|
||||
continue;
|
||||
}
|
||||
if (!rc->caller_qn || !rc->callee_qn) {
|
||||
continue;
|
||||
}
|
||||
@@ -150,77 +399,445 @@ static inline const CBMResolvedCall *cbm_pipeline_find_lsp_resolution(
|
||||
if (strcmp(rc->caller_qn, call->enclosing_func_qn) != 0) {
|
||||
continue;
|
||||
}
|
||||
const char *short_name = cbm_lsp_bare_segment(rc->callee_qn);
|
||||
/* The call's callee_name is receiver-qualified for method/qualified
|
||||
* calls ("c.inc", "A.Helper", "Math::square", "p->run"); the LSP
|
||||
* records the resolved class-qualified callee_qn ("Class.inc"). Compare
|
||||
* the bare last segment on BOTH sides so method-dispatch resolutions
|
||||
* join — the LSP already did the receiver->type resolution, and matching
|
||||
* the full "c.inc" against "inc" would always miss, silently dropping the
|
||||
* type-aware LSP strategy to the weaker textual registry. Free-function
|
||||
* calls (bare callee_name) are unaffected. */
|
||||
const char *call_short = cbm_lsp_bare_segment(call->callee_name);
|
||||
if (strcmp(short_name, call_short) != 0) {
|
||||
/* Indirect/implicit resolution: the textual callee differs from the
|
||||
* resolved callee_qn's short name. A function-pointer / DLL call's
|
||||
* callee is the pointer name (`fp`); a C++ destructor's only textual
|
||||
* anchor is the deleted operand (`p`, vs. the `T.~T` callee QN). In
|
||||
* both the LSP stashed the original textual name in `reason`. Match
|
||||
* the call site on that name, gated to those strategies so `reason`
|
||||
* is never misread as an unresolved-call diagnostic. */
|
||||
if (!(rc->reason && rc->strategy &&
|
||||
(strcmp(rc->strategy, "lsp_func_ptr") == 0 ||
|
||||
strcmp(rc->strategy, "lsp_dll_resolve") == 0 ||
|
||||
strcmp(rc->strategy, "lsp_method_ref_ctor") == 0 ||
|
||||
strcmp(rc->strategy, "lsp_method_ref_ctor_synth") == 0 ||
|
||||
strcmp(rc->strategy, "lsp_dict_dispatch") == 0 ||
|
||||
strcmp(rc->strategy, "lsp_import_alias") == 0 ||
|
||||
strcmp(rc->strategy, "lsp_destructor") == 0 ||
|
||||
strcmp(rc->strategy, "php_method_dynamic") == 0) &&
|
||||
strcmp(cbm_lsp_bare_segment(rc->reason), call_short) == 0)) {
|
||||
continue;
|
||||
}
|
||||
int site_rank = cbm_pipeline_invocation_site_rank(rc, call);
|
||||
if (site_rank == 0 || !cbm_pipeline_invocation_leaf_matches(rc, call, site_rank)) {
|
||||
continue;
|
||||
}
|
||||
if (!best_exact || rc->confidence > best_exact->confidence) {
|
||||
best_exact = rc;
|
||||
if (!best_exact_caller || site_rank > best_exact_caller_rank) {
|
||||
best_exact_caller = rc;
|
||||
best_exact_caller_rank = site_rank;
|
||||
exact_caller_ambiguous = false;
|
||||
continue;
|
||||
}
|
||||
if (site_rank < best_exact_caller_rank) {
|
||||
continue;
|
||||
}
|
||||
if (!cbm_pipeline_invocation_targets_equal(best_exact_caller->callee_qn, rc->callee_qn,
|
||||
gbuf, project_name, allow_tail_match)) {
|
||||
exact_caller_ambiguous = true;
|
||||
continue;
|
||||
}
|
||||
if (rc->confidence > best_exact_caller->confidence) {
|
||||
best_exact_caller = rc;
|
||||
}
|
||||
}
|
||||
if (best_exact) {
|
||||
return best_exact;
|
||||
if (best_exact_caller) {
|
||||
return exact_caller_ambiguous ? NULL : best_exact_caller;
|
||||
}
|
||||
if (!allow_tail_match) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *call_tail = cbm_pipeline_qn_class_method_tail(call->enclosing_func_qn);
|
||||
if (!call_tail) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const CBMResolvedCall *best_tail = NULL;
|
||||
const CBMResolvedCall *best_tail_exact = NULL;
|
||||
const CBMResolvedCall *best_tail_legacy = NULL;
|
||||
bool tail_exact_ambiguous = false;
|
||||
bool tail_legacy_ambiguous = false;
|
||||
for (int i = 0; i < arr->count; i++) {
|
||||
const CBMResolvedCall *rc = &arr->items[i];
|
||||
if (rc->kind != CBM_RESOLVED_INVOCATION) {
|
||||
continue;
|
||||
}
|
||||
if (!rc->caller_qn || !rc->callee_qn) {
|
||||
continue;
|
||||
}
|
||||
if (rc->confidence < CBM_LSP_CONFIDENCE_FLOOR) {
|
||||
continue;
|
||||
}
|
||||
const char *short_name = strrchr(rc->callee_qn, '.');
|
||||
short_name = short_name ? short_name + SKIP_ONE : rc->callee_qn;
|
||||
const char *call_leaf = cbm_pipeline_call_callee_leaf(call->callee_name);
|
||||
if (!call_leaf || strcmp(short_name, call_leaf) != 0) {
|
||||
int site_rank = cbm_pipeline_invocation_site_rank(rc, call);
|
||||
if (site_rank == 0) {
|
||||
continue;
|
||||
}
|
||||
if (!cbm_pipeline_qn_class_method_tail_eq(rc->caller_qn, call_tail)) {
|
||||
bool caller_matches =
|
||||
call_tail && cbm_pipeline_qn_class_method_tail_eq(rc->caller_qn, call_tail);
|
||||
if (!caller_matches && site_rank == 2) {
|
||||
const char *resolved_caller = cbm_lsp_bare_segment(rc->caller_qn);
|
||||
const char *source_caller = cbm_lsp_bare_segment(call->enclosing_func_qn);
|
||||
caller_matches =
|
||||
resolved_caller && source_caller && strcmp(resolved_caller, source_caller) == 0;
|
||||
}
|
||||
if (!caller_matches || !cbm_pipeline_invocation_leaf_matches(rc, call, site_rank)) {
|
||||
continue;
|
||||
}
|
||||
if (best_tail) {
|
||||
return NULL;
|
||||
|
||||
const CBMResolvedCall **best = site_rank == 2 ? &best_tail_exact : &best_tail_legacy;
|
||||
bool *ambiguous = site_rank == 2 ? &tail_exact_ambiguous : &tail_legacy_ambiguous;
|
||||
if (!*best) {
|
||||
*best = rc;
|
||||
continue;
|
||||
}
|
||||
if (!cbm_pipeline_invocation_targets_equal((*best)->callee_qn, rc->callee_qn, gbuf,
|
||||
project_name, allow_tail_match)) {
|
||||
*ambiguous = true;
|
||||
continue;
|
||||
}
|
||||
if (rc->confidence > (*best)->confidence) {
|
||||
*best = rc;
|
||||
}
|
||||
best_tail = rc;
|
||||
}
|
||||
return best_tail;
|
||||
if (best_tail_exact) {
|
||||
return tail_exact_ambiguous ? NULL : best_tail_exact;
|
||||
}
|
||||
return tail_legacy_ambiguous ? NULL : best_tail_legacy;
|
||||
}
|
||||
|
||||
/* Graph-free compatibility entry point for resolver unit tests. Raw distinct
|
||||
* targets remain ambiguous because no materialized-node identity is available. */
|
||||
static inline const CBMResolvedCall *cbm_pipeline_find_lsp_resolution(
|
||||
const CBMResolvedCallArray *arr, const CBMCall *call, bool allow_tail_match) {
|
||||
return cbm_pipeline_find_lsp_resolution_in_graph(arr, call, allow_tail_match, NULL, NULL);
|
||||
}
|
||||
|
||||
static inline bool cbm_pipeline_reference_reason_matches(const CBMResolvedCall *resolved,
|
||||
const char *reference_leaf) {
|
||||
if (!resolved || !resolved->strategy || !resolved->reason || !reference_leaf) {
|
||||
return false;
|
||||
}
|
||||
bool reason_join_strategy = strcmp(resolved->strategy, "php_method_dynamic") == 0 ||
|
||||
strcmp(resolved->strategy, "php_static_magic_reference") == 0 ||
|
||||
strcmp(resolved->strategy, "php_invokable_reference") == 0 ||
|
||||
strcmp(resolved->strategy, "lsp_callable_alias") == 0 ||
|
||||
strcmp(resolved->strategy, "lsp_callable_value") == 0 ||
|
||||
strcmp(resolved->strategy, "lsp_callable_value_reference") == 0;
|
||||
return reason_join_strategy &&
|
||||
strcmp(cbm_lsp_bare_segment(resolved->reason), reference_leaf) == 0;
|
||||
}
|
||||
|
||||
static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_target_node(const cbm_gbuf_t *gbuf,
|
||||
const char *project_name,
|
||||
const char *callee_qn,
|
||||
bool allow_tail_match) {
|
||||
return cbm_pipeline_lsp_target_node_policy(gbuf, project_name, callee_qn, allow_tail_match,
|
||||
true);
|
||||
}
|
||||
|
||||
/* External Kotlin protocol carriers must resolve to the exact semantic target.
|
||||
* A project-qualified retry is still exact, but Class.method and unique-leaf
|
||||
* fallbacks could bind `kotlin.IntArray.iterator` to an unrelated project
|
||||
* method and are therefore deliberately disabled. */
|
||||
static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_target_node_strict(const cbm_gbuf_t *gbuf,
|
||||
const char *project_name,
|
||||
const char *callee_qn,
|
||||
bool allow_tail_match) {
|
||||
(void)allow_tail_match;
|
||||
return cbm_pipeline_lsp_target_node_policy(gbuf, project_name, callee_qn, false, false);
|
||||
}
|
||||
|
||||
/* Keep exact-only lookup scoped to known external Kotlin namespaces. Project
|
||||
* callable aliases and operators still need the normal JVM QN reconciliation
|
||||
* path, while unresolved synthetic carriers remain fail-closed at the caller. */
|
||||
static inline bool cbm_pipeline_kotlin_external_target(CBMLanguage lang, const char *callee_qn) {
|
||||
if (lang != CBM_LANG_KOTLIN || !callee_qn) {
|
||||
return false;
|
||||
}
|
||||
return strncmp(callee_qn, "kotlin.", strlen("kotlin.")) == 0 ||
|
||||
strncmp(callee_qn, "java.", strlen("java.")) == 0 ||
|
||||
strncmp(callee_qn, "javax.", strlen("javax.")) == 0;
|
||||
}
|
||||
|
||||
/* Resolvers may report one graph target both project-relative and already
|
||||
* project-prefixed. Raw string inequality is not ambiguity when both spellings
|
||||
* resolve to the same materialized node; conversely, if both nodes exist they
|
||||
* remain distinct and the reference must fail closed. */
|
||||
static inline bool cbm_pipeline_reference_targets_equal(const char *left_qn, const char *right_qn,
|
||||
const cbm_gbuf_t *gbuf,
|
||||
const char *project_name,
|
||||
bool allow_tail_match) {
|
||||
if (!left_qn || !right_qn) {
|
||||
return false;
|
||||
}
|
||||
if (strcmp(left_qn, right_qn) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (!gbuf || !project_name) {
|
||||
return false;
|
||||
}
|
||||
/* Caller-tail fallback helps JVM rows find a materialized target, but it is
|
||||
* not proof that two distinct semantic target QNs denote the same symbol.
|
||||
* Equivalence is deliberately stricter: collapse only direct and
|
||||
* project-prefix spellings of one graph node, otherwise stay ambiguous. */
|
||||
(void)allow_tail_match;
|
||||
const cbm_gbuf_node_t *left = cbm_pipeline_lsp_target_node(gbuf, project_name, left_qn, false);
|
||||
const cbm_gbuf_node_t *right =
|
||||
cbm_pipeline_lsp_target_node(gbuf, project_name, right_qn, false);
|
||||
return left && right && left->id == right->id;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const CBMResolvedCall *row;
|
||||
int original_index;
|
||||
} cbm_pipeline_lsp_reference_index_entry_t;
|
||||
|
||||
typedef struct {
|
||||
cbm_pipeline_lsp_reference_index_entry_t *entries;
|
||||
int count;
|
||||
} cbm_pipeline_lsp_reference_index_t;
|
||||
|
||||
/* Sort by exact occurrence while retaining extraction order within one site.
|
||||
* The final ordinal key is intentional: confidence ties keep the first row,
|
||||
* so an unstable qsort tie must not silently change semantic selection. */
|
||||
static inline int cbm_pipeline_lsp_reference_index_entry_cmp(const void *left_ptr,
|
||||
const void *right_ptr) {
|
||||
const cbm_pipeline_lsp_reference_index_entry_t *left =
|
||||
(const cbm_pipeline_lsp_reference_index_entry_t *)left_ptr;
|
||||
const cbm_pipeline_lsp_reference_index_entry_t *right =
|
||||
(const cbm_pipeline_lsp_reference_index_entry_t *)right_ptr;
|
||||
if (left->row->source_origin != right->row->source_origin) {
|
||||
return left->row->source_origin < right->row->source_origin ? -1 : 1;
|
||||
}
|
||||
if (left->row->site_start_byte != right->row->site_start_byte) {
|
||||
return left->row->site_start_byte < right->row->site_start_byte ? -1 : 1;
|
||||
}
|
||||
if (left->row->site_end_byte != right->row->site_end_byte) {
|
||||
return left->row->site_end_byte < right->row->site_end_byte ? -1 : 1;
|
||||
}
|
||||
if (left->original_index == right->original_index) {
|
||||
return 0;
|
||||
}
|
||||
return left->original_index < right->original_index ? -1 : 1;
|
||||
}
|
||||
|
||||
/* Build one compact pointer index per extracted file. Invocation rows cannot
|
||||
* participate in CALL_REFERENCE matching and are excluded. False means only
|
||||
* allocation failure; callers must then use the unchanged linear matcher. */
|
||||
static inline bool cbm_pipeline_lsp_reference_index_build(
|
||||
const CBMResolvedCallArray *arr, cbm_pipeline_lsp_reference_index_t *index) {
|
||||
if (!index) {
|
||||
return false;
|
||||
}
|
||||
index->entries = NULL;
|
||||
index->count = 0;
|
||||
if (!arr || arr->count <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int reference_count = 0;
|
||||
for (int i = 0; i < arr->count; i++) {
|
||||
if (arr->items[i].kind == CBM_RESOLVED_CALL_REFERENCE) {
|
||||
reference_count++;
|
||||
}
|
||||
}
|
||||
if (reference_count == 0) {
|
||||
return true;
|
||||
}
|
||||
if ((size_t)reference_count > SIZE_MAX / sizeof(cbm_pipeline_lsp_reference_index_entry_t)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
cbm_pipeline_lsp_reference_index_entry_t *entries =
|
||||
(cbm_pipeline_lsp_reference_index_entry_t *)malloc((size_t)reference_count *
|
||||
sizeof(*entries));
|
||||
if (!entries) {
|
||||
return false;
|
||||
}
|
||||
int next = 0;
|
||||
for (int i = 0; i < arr->count; i++) {
|
||||
if (arr->items[i].kind != CBM_RESOLVED_CALL_REFERENCE) {
|
||||
continue;
|
||||
}
|
||||
entries[next].row = &arr->items[i];
|
||||
entries[next].original_index = i;
|
||||
next++;
|
||||
}
|
||||
qsort(entries, (size_t)reference_count, sizeof(*entries),
|
||||
cbm_pipeline_lsp_reference_index_entry_cmp);
|
||||
index->entries = entries;
|
||||
index->count = reference_count;
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline void cbm_pipeline_lsp_reference_index_free(
|
||||
cbm_pipeline_lsp_reference_index_t *index) {
|
||||
if (!index) {
|
||||
return;
|
||||
}
|
||||
free(index->entries);
|
||||
index->entries = NULL;
|
||||
index->count = 0;
|
||||
}
|
||||
|
||||
static inline int cbm_pipeline_lsp_reference_site_cmp_usage(const CBMResolvedCall *row,
|
||||
const CBMUsage *usage) {
|
||||
if (row->source_origin != usage->source_origin) {
|
||||
return row->source_origin < usage->source_origin ? -1 : 1;
|
||||
}
|
||||
if (row->site_start_byte != usage->site_start_byte) {
|
||||
return row->site_start_byte < usage->site_start_byte ? -1 : 1;
|
||||
}
|
||||
if (row->site_end_byte != usage->site_end_byte) {
|
||||
return row->site_end_byte < usage->site_end_byte ? -1 : 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline const CBMResolvedCall *cbm_pipeline_lsp_reference_view_row(
|
||||
const CBMResolvedCallArray *arr, const cbm_pipeline_lsp_reference_index_entry_t *entries,
|
||||
int first, int offset) {
|
||||
return entries ? entries[first + offset].row : &arr->items[first + offset];
|
||||
}
|
||||
|
||||
/* Look up the highest-confidence LSP resolution for a callable reference.
|
||||
* Reference records intentionally use a separate kind from invocations: an
|
||||
* explicit `Type::method`/`obj::method`, PHP first-class callable, or an exact
|
||||
* TS function-value argument denotes a function value and must never enter
|
||||
* the CALLS pipeline.
|
||||
*
|
||||
* Exact enclosing-function matches win. The unique Class.method caller-tail
|
||||
* fallback is admitted only when the caller opted into the existing JVM-only
|
||||
* rule via `allow_tail_match`; ambiguity returns NULL so the usage pipeline can
|
||||
* fall back to the textual registry without fabricating a semantic match.
|
||||
*
|
||||
* `entries` is either NULL for the original full-array fallback or a sorted
|
||||
* occurrence slice. The matching body is deliberately shared so indexing
|
||||
* cannot diverge on caller, reason, ambiguity, target identity, or confidence. */
|
||||
static inline const CBMResolvedCall *cbm_pipeline_find_lsp_reference_view_in_graph(
|
||||
const CBMResolvedCallArray *arr, const cbm_pipeline_lsp_reference_index_entry_t *entries,
|
||||
int first, int row_count, const CBMUsage *usage, bool allow_tail_match, const cbm_gbuf_t *gbuf,
|
||||
const char *project_name) {
|
||||
if ((!entries && !arr) || row_count <= 0 || !usage || !usage->enclosing_func_qn ||
|
||||
!usage->ref_name) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *ref_leaf = cbm_lsp_bare_segment(usage->ref_name);
|
||||
if (!ref_leaf || !ref_leaf[0]) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const CBMResolvedCall *best_exact = NULL;
|
||||
bool exact_ambiguous = false;
|
||||
for (int i = 0; i < row_count; i++) {
|
||||
cbm_pipeline_lsp_reference_lookup_test_note_row();
|
||||
const CBMResolvedCall *rc = cbm_pipeline_lsp_reference_view_row(arr, entries, first, i);
|
||||
if (rc->kind != CBM_RESOLVED_CALL_REFERENCE || !rc->caller_qn || !rc->callee_qn ||
|
||||
rc->confidence < CBM_LSP_CONFIDENCE_FLOOR) {
|
||||
continue;
|
||||
}
|
||||
if (strcmp(rc->caller_qn, usage->enclosing_func_qn) != 0 ||
|
||||
!cbm_pipeline_source_occurrence_eq(usage->site_start_byte, usage->site_end_byte,
|
||||
usage->source_origin, rc->site_start_byte,
|
||||
rc->site_end_byte, rc->source_origin)) {
|
||||
continue;
|
||||
}
|
||||
bool name_matches = strcmp(cbm_lsp_bare_segment(rc->callee_qn), ref_leaf) == 0;
|
||||
bool semantic_reason_matches = cbm_pipeline_reference_reason_matches(rc, ref_leaf);
|
||||
if (!name_matches && !semantic_reason_matches) {
|
||||
continue;
|
||||
}
|
||||
if (!best_exact) {
|
||||
best_exact = rc;
|
||||
continue;
|
||||
}
|
||||
if (!cbm_pipeline_reference_targets_equal(best_exact->callee_qn, rc->callee_qn, gbuf,
|
||||
project_name, allow_tail_match)) {
|
||||
exact_ambiguous = true;
|
||||
continue;
|
||||
}
|
||||
if (rc->confidence > best_exact->confidence) {
|
||||
best_exact = rc;
|
||||
}
|
||||
}
|
||||
if (best_exact || !allow_tail_match) {
|
||||
return exact_ambiguous ? NULL : best_exact;
|
||||
}
|
||||
|
||||
const char *caller_tail = cbm_pipeline_qn_class_method_tail(usage->enclosing_func_qn);
|
||||
if (!caller_tail) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const CBMResolvedCall *best_tail = NULL;
|
||||
bool tail_ambiguous = false;
|
||||
for (int i = 0; i < row_count; i++) {
|
||||
cbm_pipeline_lsp_reference_lookup_test_note_row();
|
||||
const CBMResolvedCall *rc = cbm_pipeline_lsp_reference_view_row(arr, entries, first, i);
|
||||
if (rc->kind != CBM_RESOLVED_CALL_REFERENCE || !rc->caller_qn || !rc->callee_qn ||
|
||||
rc->confidence < CBM_LSP_CONFIDENCE_FLOOR) {
|
||||
continue;
|
||||
}
|
||||
if (!cbm_pipeline_qn_class_method_tail_eq(rc->caller_qn, caller_tail) ||
|
||||
!cbm_pipeline_source_occurrence_eq(usage->site_start_byte, usage->site_end_byte,
|
||||
usage->source_origin, rc->site_start_byte,
|
||||
rc->site_end_byte, rc->source_origin)) {
|
||||
continue;
|
||||
}
|
||||
bool name_matches = strcmp(cbm_lsp_bare_segment(rc->callee_qn), ref_leaf) == 0;
|
||||
bool semantic_reason_matches = cbm_pipeline_reference_reason_matches(rc, ref_leaf);
|
||||
if (!name_matches && !semantic_reason_matches) {
|
||||
continue;
|
||||
}
|
||||
if (!best_tail) {
|
||||
best_tail = rc;
|
||||
continue;
|
||||
}
|
||||
if (!cbm_pipeline_reference_targets_equal(best_tail->callee_qn, rc->callee_qn, gbuf,
|
||||
project_name, allow_tail_match)) {
|
||||
tail_ambiguous = true;
|
||||
continue;
|
||||
}
|
||||
if (rc->confidence > best_tail->confidence) {
|
||||
best_tail = rc;
|
||||
}
|
||||
}
|
||||
return tail_ambiguous ? NULL : best_tail;
|
||||
}
|
||||
|
||||
/* Full-array compatibility/fallback path. Allocation failure in either
|
||||
* materializer deliberately lands here rather than dropping semantic edges. */
|
||||
static inline const CBMResolvedCall *cbm_pipeline_find_lsp_reference_in_graph(
|
||||
const CBMResolvedCallArray *arr, const CBMUsage *usage, bool allow_tail_match,
|
||||
const cbm_gbuf_t *gbuf, const char *project_name) {
|
||||
int count = arr ? arr->count : 0;
|
||||
return cbm_pipeline_find_lsp_reference_view_in_graph(arr, NULL, 0, count, usage,
|
||||
allow_tail_match, gbuf, project_name);
|
||||
}
|
||||
|
||||
/* Binary-search the exact (origin,start,end) occurrence, then run the shared
|
||||
* matcher only over that stable-order slice. A NULL index means construction
|
||||
* failed and selects the original linear fallback above. */
|
||||
static inline const CBMResolvedCall *cbm_pipeline_find_lsp_reference_indexed_in_graph(
|
||||
const CBMResolvedCallArray *arr, const cbm_pipeline_lsp_reference_index_t *index,
|
||||
const CBMUsage *usage, bool allow_tail_match, const cbm_gbuf_t *gbuf,
|
||||
const char *project_name) {
|
||||
if (!index) {
|
||||
return cbm_pipeline_find_lsp_reference_in_graph(arr, usage, allow_tail_match, gbuf,
|
||||
project_name);
|
||||
}
|
||||
if (!usage || !usage->enclosing_func_qn || !usage->ref_name || index->count <= 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int low = 0;
|
||||
int high = index->count;
|
||||
while (low < high) {
|
||||
int middle = low + (high - low) / 2;
|
||||
if (cbm_pipeline_lsp_reference_site_cmp_usage(index->entries[middle].row, usage) < 0) {
|
||||
low = middle + 1;
|
||||
} else {
|
||||
high = middle;
|
||||
}
|
||||
}
|
||||
int first = low;
|
||||
high = index->count;
|
||||
while (low < high) {
|
||||
int middle = low + (high - low) / 2;
|
||||
if (cbm_pipeline_lsp_reference_site_cmp_usage(index->entries[middle].row, usage) <= 0) {
|
||||
low = middle + 1;
|
||||
} else {
|
||||
high = middle;
|
||||
}
|
||||
}
|
||||
int row_count = low - first;
|
||||
return cbm_pipeline_find_lsp_reference_view_in_graph(
|
||||
arr, index->entries, first, row_count, usage, allow_tail_match, gbuf, project_name);
|
||||
}
|
||||
|
||||
/* String-strict helper retained for unit tests and callers without a graph.
|
||||
* Production edge materialization uses the graph-aware variant above. */
|
||||
static inline const CBMResolvedCall *cbm_pipeline_find_lsp_reference(
|
||||
const CBMResolvedCallArray *arr, const CBMUsage *usage, bool allow_tail_match) {
|
||||
return cbm_pipeline_find_lsp_reference_in_graph(arr, usage, allow_tail_match, NULL, NULL);
|
||||
}
|
||||
|
||||
/* Resolve an LSP-emitted callee_qn to a graph-buffer node.
|
||||
@@ -234,13 +851,14 @@ static inline const CBMResolvedCall *cbm_pipeline_find_lsp_resolution(
|
||||
* cbm_pipeline_lsp_allow_tail_match), use the exact node-name index
|
||||
* to narrow candidates by short method name and accept exactly one
|
||||
* Function/Method whose qualified_name has the same Class.method
|
||||
* tail.
|
||||
* tail;
|
||||
* 4. when a package-shaped top-level JVM QN cannot share a tail with the
|
||||
* graph's file-shaped QN, accept one globally unique callable short name.
|
||||
*
|
||||
* Returns the matching node, or NULL if neither lookup hits. */
|
||||
static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_target_node(const cbm_gbuf_t *gbuf,
|
||||
const char *project_name,
|
||||
const char *callee_qn,
|
||||
bool allow_tail_match) {
|
||||
static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_target_node_policy(
|
||||
const cbm_gbuf_t *gbuf, const char *project_name, const char *callee_qn, bool allow_tail_match,
|
||||
bool allow_unique_callable_fallback) {
|
||||
if (!gbuf || !callee_qn) {
|
||||
return NULL;
|
||||
}
|
||||
@@ -251,12 +869,19 @@ static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_target_node(const cbm_gbuf
|
||||
if (project_name && project_name[0]) {
|
||||
size_t proj_len = strlen(project_name);
|
||||
if (!(strncmp(callee_qn, project_name, proj_len) == 0 && callee_qn[proj_len] == '.')) {
|
||||
char buf[CBM_SZ_1K];
|
||||
int written = snprintf(buf, sizeof(buf), "%s.%s", project_name, callee_qn);
|
||||
if (written > 0 && (size_t)written < sizeof(buf)) {
|
||||
const cbm_gbuf_node_t *prefixed = cbm_gbuf_find_by_qn(gbuf, buf);
|
||||
if (prefixed) {
|
||||
return prefixed;
|
||||
size_t callee_len = strlen(callee_qn);
|
||||
if (proj_len <= SIZE_MAX - callee_len - 2U) {
|
||||
size_t prefixed_len = proj_len + 1U + callee_len;
|
||||
char *prefixed_qn = (char *)malloc(prefixed_len + 1U);
|
||||
if (prefixed_qn) {
|
||||
memcpy(prefixed_qn, project_name, proj_len);
|
||||
prefixed_qn[proj_len] = '.';
|
||||
memcpy(prefixed_qn + proj_len + 1U, callee_qn, callee_len + 1U);
|
||||
const cbm_gbuf_node_t *prefixed = cbm_gbuf_find_by_qn(gbuf, prefixed_qn);
|
||||
free(prefixed_qn);
|
||||
if (prefixed) {
|
||||
return prefixed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -278,6 +903,9 @@ static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_target_node(const cbm_gbuf
|
||||
}
|
||||
|
||||
const cbm_gbuf_node_t *match = NULL;
|
||||
const cbm_gbuf_node_t *unique_callable = NULL;
|
||||
bool tail_ambiguous = false;
|
||||
bool callable_ambiguous = false;
|
||||
for (int i = 0; i < hit_count; i++) {
|
||||
const cbm_gbuf_node_t *cand = hits[i];
|
||||
if (!cand || !cand->label || !cand->qualified_name) {
|
||||
@@ -286,15 +914,27 @@ static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_target_node(const cbm_gbuf
|
||||
if (strcmp(cand->label, "Function") != 0 && strcmp(cand->label, "Method") != 0) {
|
||||
continue;
|
||||
}
|
||||
if (!unique_callable) {
|
||||
unique_callable = cand;
|
||||
} else if (unique_callable->id != cand->id) {
|
||||
callable_ambiguous = true;
|
||||
}
|
||||
if (!cbm_pipeline_qn_class_method_tail_eq(cand->qualified_name, callee_tail)) {
|
||||
continue;
|
||||
}
|
||||
if (match) {
|
||||
return NULL;
|
||||
if (match && match->id != cand->id) {
|
||||
tail_ambiguous = true;
|
||||
continue;
|
||||
}
|
||||
match = cand;
|
||||
}
|
||||
return match;
|
||||
if (match) {
|
||||
return tail_ambiguous ? NULL : match;
|
||||
}
|
||||
if (!allow_unique_callable_fallback) {
|
||||
return NULL;
|
||||
}
|
||||
return callable_ambiguous ? NULL : unique_callable;
|
||||
}
|
||||
|
||||
#endif /* CBM_PIPELINE_LSP_RESOLVE_H */
|
||||
|
||||
@@ -467,10 +467,16 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
|
||||
/* LSP-resolved calls take precedence over registry-textual matching.
|
||||
* Unique-tail fallbacks are JVM-only (see cbm_pipeline_lsp_allow_tail_match). */
|
||||
bool allow_tail = cbm_pipeline_lsp_allow_tail_match(lang);
|
||||
const CBMResolvedCall *lsp = cbm_pipeline_find_lsp_resolution(lsp_calls, call, allow_tail);
|
||||
const CBMResolvedCall *lsp = cbm_pipeline_find_lsp_resolution_in_graph(
|
||||
lsp_calls, call, allow_tail, ctx->gbuf, ctx->project_name);
|
||||
if (lsp) {
|
||||
bool exact_external_target = call->requires_lsp_resolution &&
|
||||
cbm_pipeline_kotlin_external_target(lang, lsp->callee_qn);
|
||||
const cbm_gbuf_node_t *target_node =
|
||||
cbm_pipeline_lsp_target_node(ctx->gbuf, ctx->project_name, lsp->callee_qn, allow_tail);
|
||||
exact_external_target ? cbm_pipeline_lsp_target_node_strict(
|
||||
ctx->gbuf, ctx->project_name, lsp->callee_qn, allow_tail)
|
||||
: cbm_pipeline_lsp_target_node(ctx->gbuf, ctx->project_name,
|
||||
lsp->callee_qn, allow_tail);
|
||||
if (target_node && source_node->id != target_node->id) {
|
||||
cbm_resolution_t res = {0};
|
||||
/* Use the gbuf node's QN so downstream edge props show the canonical
|
||||
@@ -485,6 +491,15 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
|
||||
}
|
||||
}
|
||||
|
||||
/* Synthetic semantic candidates (currently implicit C++ operators) are
|
||||
* valid calls only when the language resolver identifies a concrete
|
||||
* invocation target. Textual registry/service fallbacks would bind an
|
||||
* unresolved primitive expression such as `int + int` to an unrelated
|
||||
* project `operator+`, fabricating a CALLS edge. */
|
||||
if (call->requires_lsp_resolution) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Service-pattern HTTP/ASYNC client call (`requests.get(url)`): the service
|
||||
* signal lives in the callee_name. The registry can mis-resolve such a call
|
||||
* to a spurious builtin short-name match (e.g. `requests.get` ->
|
||||
|
||||
+193
-40
@@ -405,8 +405,8 @@ static void create_channel_edges_for_file(cbm_pipeline_ctx_t *ctx, const CBMFile
|
||||
* env key and link the enclosing function (or the file node) CONFIGURES-> it,
|
||||
* so environment-driven configuration is visible even when the accessor is a
|
||||
* stdlib symbol that never resolves to an in-graph callee. */
|
||||
static int create_env_configures_for_file(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result,
|
||||
const char *rel) {
|
||||
int cbm_pipeline_create_env_configures_for_file(cbm_pipeline_ctx_t *ctx,
|
||||
const CBMFileResult *result, const char *rel) {
|
||||
int count = 0;
|
||||
char *file_qn = NULL;
|
||||
const cbm_gbuf_node_t *file_node = NULL;
|
||||
@@ -481,6 +481,186 @@ static int create_import_edges_for_file(cbm_pipeline_ctx_t *ctx, const CBMFileRe
|
||||
return count;
|
||||
}
|
||||
|
||||
static bool objectscript_export_append_strings(CBMArena *arena, const char ***dst,
|
||||
const char *const *src) {
|
||||
if (!src) {
|
||||
return true;
|
||||
}
|
||||
int old_count = 0;
|
||||
int add_count = 0;
|
||||
while (*dst && (*dst)[old_count]) {
|
||||
old_count++;
|
||||
}
|
||||
while (src[add_count]) {
|
||||
add_count++;
|
||||
}
|
||||
const char **items = (const char **)cbm_arena_alloc(arena, (size_t)(old_count + add_count + 1) *
|
||||
sizeof(const char *));
|
||||
if (!items) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < old_count; i++) {
|
||||
items[i] = (*dst)[i];
|
||||
}
|
||||
for (int i = 0; i < add_count; i++) {
|
||||
items[old_count + i] = src[i];
|
||||
}
|
||||
items[old_count + add_count] = NULL;
|
||||
*dst = items;
|
||||
return true;
|
||||
}
|
||||
|
||||
#define OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, field, push_fn) \
|
||||
do { \
|
||||
int expected_count = (aggregate)->field.count + (part)->field.count; \
|
||||
for (int item_i = 0; item_i < (part)->field.count; item_i++) { \
|
||||
push_fn(&(aggregate)->field, &(aggregate)->arena, (part)->field.items[item_i]); \
|
||||
} \
|
||||
if ((aggregate)->field.count != expected_count) { \
|
||||
return false; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static bool objectscript_export_append_primary_arrays(CBMFileResult *aggregate,
|
||||
const CBMFileResult *part) {
|
||||
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, defs, cbm_defs_push);
|
||||
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, calls, cbm_calls_push);
|
||||
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, imports, cbm_imports_push);
|
||||
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, usages, cbm_usages_push);
|
||||
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, throws, cbm_throws_push);
|
||||
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, rw, cbm_rw_push);
|
||||
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, type_refs, cbm_typerefs_push);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool objectscript_export_append_secondary_arrays(CBMFileResult *aggregate,
|
||||
const CBMFileResult *part) {
|
||||
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, env_accesses, cbm_envaccess_push);
|
||||
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, type_assigns, cbm_typeassign_push);
|
||||
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, impl_traits, cbm_impltrait_push);
|
||||
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, resolved_calls, cbm_resolvedcall_push);
|
||||
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, string_refs, cbm_stringref_push);
|
||||
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, infra_bindings, cbm_infrabinding_push);
|
||||
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, channels, cbm_channels_push);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Preserve every generated class's parse diagnostics. The generated UDL
|
||||
* snippets all map back to one physical Studio Export file, so their compact
|
||||
* range lists can be concatenated using the ordinary comma separator. */
|
||||
static bool objectscript_export_append_error_ranges(CBMFileResult *aggregate,
|
||||
const CBMFileResult *part) {
|
||||
aggregate->parse_incomplete = aggregate->parse_incomplete || part->parse_incomplete;
|
||||
aggregate->error_region_count += part->error_region_count;
|
||||
if (!part->error_ranges || !part->error_ranges[0]) {
|
||||
return true;
|
||||
}
|
||||
const char *combined = NULL;
|
||||
if (aggregate->error_ranges && aggregate->error_ranges[0]) {
|
||||
combined = cbm_arena_sprintf(&aggregate->arena, "%s,%s", aggregate->error_ranges,
|
||||
part->error_ranges);
|
||||
} else {
|
||||
combined = cbm_arena_strdup(&aggregate->arena, part->error_ranges);
|
||||
}
|
||||
if (!combined) {
|
||||
return false;
|
||||
}
|
||||
aggregate->error_ranges = combined;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Studio Export files may contain multiple <Class> elements, while the
|
||||
* pipeline cache has one slot per physical file. Extract each generated UDL
|
||||
* class independently (preserving the upstream parser behavior), then compose
|
||||
* every extracted carrier into one result for the normal registry/call/usage/
|
||||
* semantic passes. */
|
||||
CBMFileResult *cbm_pipeline_extract_objectscript_export(
|
||||
const char *source, int source_len, const char *project_name, const char *rel_path,
|
||||
const CBMMacroTable *macro_table, const CBMReturnTypeTable *return_type_table) {
|
||||
CBMArena export_arena;
|
||||
cbm_arena_init(&export_arena);
|
||||
int class_count = 0;
|
||||
char **udl_strings = cbm_iris_export_to_udl(&export_arena, source, source_len, &class_count);
|
||||
if (!udl_strings || class_count <= 0) {
|
||||
cbm_arena_destroy(&export_arena);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
CBMFileResult *aggregate = (CBMFileResult *)calloc(1, sizeof(CBMFileResult));
|
||||
if (!aggregate) {
|
||||
cbm_arena_destroy(&export_arena);
|
||||
return NULL;
|
||||
}
|
||||
cbm_arena_init(&aggregate->arena);
|
||||
if (aggregate->arena.nblocks == 0) {
|
||||
cbm_free_result(aggregate);
|
||||
cbm_arena_destroy(&export_arena);
|
||||
return NULL;
|
||||
}
|
||||
aggregate->owned_results =
|
||||
(CBMFileResult **)calloc((size_t)class_count, sizeof(CBMFileResult *));
|
||||
if (!aggregate->owned_results) {
|
||||
cbm_free_result(aggregate);
|
||||
cbm_arena_destroy(&export_arena);
|
||||
return NULL;
|
||||
}
|
||||
aggregate->cached_lang = CBM_LANG_OBJECTSCRIPT_UDL;
|
||||
|
||||
for (int ci = 0; ci < class_count; ci++) {
|
||||
CBMFileResult *part = cbm_extract_file_ex(
|
||||
udl_strings[ci], (int)strlen(udl_strings[ci]), CBM_LANG_OBJECTSCRIPT_UDL, project_name,
|
||||
rel_path, CBM_EXTRACT_BUDGET, NULL, NULL, macro_table, return_type_table);
|
||||
if (!part) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* The aggregate has no single parse tree. Later ObjectScript Export
|
||||
* passes consume extracted carriers, not a raw-XML tree. */
|
||||
cbm_free_tree(part);
|
||||
if (!objectscript_export_append_primary_arrays(aggregate, part) ||
|
||||
!objectscript_export_append_secondary_arrays(aggregate, part) ||
|
||||
!objectscript_export_append_strings(&aggregate->arena, &aggregate->exports,
|
||||
part->exports) ||
|
||||
!objectscript_export_append_strings(&aggregate->arena, &aggregate->constants,
|
||||
part->constants) ||
|
||||
!objectscript_export_append_strings(&aggregate->arena, &aggregate->global_vars,
|
||||
part->global_vars) ||
|
||||
!objectscript_export_append_strings(&aggregate->arena, &aggregate->macros,
|
||||
part->macros) ||
|
||||
!objectscript_export_append_error_ranges(aggregate, part)) {
|
||||
goto merge_failed;
|
||||
}
|
||||
|
||||
if (!aggregate->module_qn) {
|
||||
aggregate->module_qn = part->module_qn;
|
||||
}
|
||||
if (!aggregate->namespace_name) {
|
||||
aggregate->namespace_name = part->namespace_name;
|
||||
}
|
||||
if (part->has_error) {
|
||||
aggregate->has_error = true;
|
||||
if (!aggregate->error_msg) {
|
||||
aggregate->error_msg = part->error_msg;
|
||||
}
|
||||
}
|
||||
aggregate->is_test_file = aggregate->is_test_file || part->is_test_file;
|
||||
aggregate->owned_results[aggregate->owned_result_count++] = part;
|
||||
continue;
|
||||
|
||||
merge_failed:
|
||||
cbm_free_result(part);
|
||||
cbm_free_result(aggregate);
|
||||
cbm_arena_destroy(&export_arena);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
aggregate->imports_count = aggregate->imports.count;
|
||||
cbm_arena_destroy(&export_arena);
|
||||
return aggregate;
|
||||
}
|
||||
|
||||
#undef OBJECTSCRIPT_EXPORT_APPEND_ARRAY
|
||||
|
||||
int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
|
||||
int file_count) {
|
||||
cbm_log_info("pass.start", "pass", "definitions", "files", itoa_log(file_count));
|
||||
@@ -568,42 +748,15 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
|
||||
continue;
|
||||
}
|
||||
|
||||
/* ObjectScript Studio Export XML: transcode each <Class> to UDL and
|
||||
* extract it as CBM_LANG_OBJECTSCRIPT_UDL. The XML→UDL mapping is 1:1,
|
||||
* so the same UDL extractor handles the result. These files are not
|
||||
* cached (their defs/edges are emitted directly here). */
|
||||
if (lang == CBM_LANG_OBJECTSCRIPT_EXPORT) {
|
||||
CBMArena export_arena;
|
||||
cbm_arena_init(&export_arena);
|
||||
int class_count = 0;
|
||||
char **udl_strings =
|
||||
cbm_iris_export_to_udl(&export_arena, source, source_len, &class_count);
|
||||
free(source);
|
||||
for (int ci = 0; ci < class_count; ci++) {
|
||||
CBMFileResult *xr = cbm_extract_file_ex(
|
||||
udl_strings[ci], (int)strlen(udl_strings[ci]), CBM_LANG_OBJECTSCRIPT_UDL,
|
||||
ctx->project_name, rel, CBM_EXTRACT_BUDGET, NULL, NULL, ctx->macro_table, NULL);
|
||||
if (!xr) {
|
||||
continue;
|
||||
}
|
||||
for (int d = 0; d < xr->defs.count; d++) {
|
||||
process_def(ctx, &xr->defs.items[d], rel);
|
||||
total_defs++;
|
||||
}
|
||||
total_calls += xr->calls.count;
|
||||
total_imports += create_import_edges_for_file(ctx, xr, rel, NULL);
|
||||
create_channel_edges_for_file(ctx, xr, rel);
|
||||
create_env_configures_for_file(ctx, xr, rel);
|
||||
cbm_free_result(xr);
|
||||
}
|
||||
cbm_arena_destroy(&export_arena);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Extract */
|
||||
CBMFileResult *result = cbm_extract_file_ex(
|
||||
source, source_len, lang, ctx->project_name, rel, CBM_EXTRACT_BUDGET, NULL,
|
||||
NULL /* no extra defines or include paths */, ctx->macro_table, NULL);
|
||||
/* Studio Export XML is transformed to one cacheable aggregate so later
|
||||
* passes see the same calls/usages/semantic carriers as native UDL. */
|
||||
CBMFileResult *result =
|
||||
lang == CBM_LANG_OBJECTSCRIPT_EXPORT
|
||||
? cbm_pipeline_extract_objectscript_export(source, source_len, ctx->project_name,
|
||||
rel, ctx->macro_table, NULL)
|
||||
: cbm_extract_file_ex(
|
||||
source, source_len, lang, ctx->project_name, rel, CBM_EXTRACT_BUDGET, NULL,
|
||||
NULL /* no extra defines or include paths */, ctx->macro_table, NULL);
|
||||
free(source);
|
||||
|
||||
if (!result) {
|
||||
@@ -647,7 +800,7 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
|
||||
* map is available without the cache (single-file scope). */
|
||||
total_imports += create_import_edges_for_file(ctx, result, rel, NULL);
|
||||
create_channel_edges_for_file(ctx, result, rel);
|
||||
create_env_configures_for_file(ctx, result, rel);
|
||||
cbm_pipeline_create_env_configures_for_file(ctx, result, rel);
|
||||
cbm_free_result(result);
|
||||
}
|
||||
}
|
||||
@@ -680,7 +833,7 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
|
||||
total_imports +=
|
||||
create_import_edges_for_file(ctx, result, files[i].rel_path, namespace_map);
|
||||
create_channel_edges_for_file(ctx, result, files[i].rel_path);
|
||||
create_env_configures_for_file(ctx, result, files[i].rel_path);
|
||||
cbm_pipeline_create_env_configures_for_file(ctx, result, files[i].rel_path);
|
||||
}
|
||||
cbm_pipeline_namespace_map_free(namespace_map);
|
||||
if (owns_local_cache) {
|
||||
|
||||
+508
-72
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
#include "pipeline/pass_lsp_cross.h"
|
||||
#include "pipeline/pipeline_internal.h"
|
||||
#include "pipeline/lsp_resolve.h"
|
||||
#include "lsp/go_lsp.h"
|
||||
#include "lsp/c_lsp.h"
|
||||
#include "lsp/py_lsp.h"
|
||||
@@ -31,6 +32,7 @@
|
||||
#include "foundation/compat_fs.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -234,6 +236,19 @@ static const char *pxc_infer_jvm_namespace(CBMArena *arena, const char *rel_path
|
||||
return ns;
|
||||
}
|
||||
|
||||
static const char *pxc_qn_leaf(const char *name) {
|
||||
if (!name) {
|
||||
return NULL;
|
||||
}
|
||||
const char *leaf = name;
|
||||
for (const char *p = name; *p; p++) {
|
||||
if (*p == '.' || *p == ':' || *p == '/' || *p == '\\') {
|
||||
leaf = p + 1;
|
||||
}
|
||||
}
|
||||
return leaf;
|
||||
}
|
||||
|
||||
/* Convert one CBMDefinition into a CBMLSPDef. Returns 0 on success, -1
|
||||
* to skip (unsupported label or missing required field). dst gets borrowed
|
||||
* pointers into src and into `arena` for synthesised composites. */
|
||||
@@ -260,7 +275,39 @@ static int pxc_build_lsp_def(CBMArena *arena, const CBMDefinition *src, const ch
|
||||
* piece, which is what's already stored. */
|
||||
dst->return_types = src->return_type;
|
||||
dst->embedded_types = pxc_join_pipe(arena, src->base_classes);
|
||||
dst->signature_param_types = src->signature_param_types;
|
||||
dst->signature_param_count = src->signature_param_count;
|
||||
dst->lang = lang;
|
||||
dst->decorators = src->decorators;
|
||||
if (lang == CBM_LANG_RUST) {
|
||||
/* Exact impl-block provenance is captured while the Rust impl node is
|
||||
* still on hand. Do not reconstruct it later from leaf names. */
|
||||
dst->trait_qn = src->impl_trait;
|
||||
dst->is_abstract = src->is_abstract;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Carry one Rust type-level impl independently of any method definition.
|
||||
* `impl Trait for Type {}` is semantically meaningful even when the block is
|
||||
* empty (the trait may provide defaults), so attaching the relation only to
|
||||
* concrete method records is lossy. */
|
||||
static int pxc_build_rust_impl_relation(CBMArena *arena, const CBMImplTrait *impl,
|
||||
const char *project_name, const char *rel_path,
|
||||
const char *module_qn, CBMLSPDef *dst) {
|
||||
if (!arena || !impl || !impl->trait_name || !impl->struct_name || !impl->struct_qn) {
|
||||
return -1;
|
||||
}
|
||||
const char *receiver_qn = impl->struct_qn;
|
||||
memset(dst, 0, sizeof(*dst));
|
||||
dst->qualified_name = receiver_qn;
|
||||
dst->short_name = pxc_qn_leaf(receiver_qn);
|
||||
dst->label = "RustImpl";
|
||||
dst->receiver_type = receiver_qn;
|
||||
dst->def_module_qn = module_qn;
|
||||
dst->trait_qn = impl->trait_name; /* raw; canonicalized by Rust registry */
|
||||
dst->lang = CBM_LANG_RUST;
|
||||
dst->is_rust_impl_relation = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -272,8 +319,12 @@ CBMLSPDef *cbm_pxc_collect_all_defs(CBMFileResult **cache, const cbm_file_info_t
|
||||
int *out_count) {
|
||||
int total = 0;
|
||||
for (int i = 0; i < file_count; i++) {
|
||||
if (cache[i])
|
||||
if (cache[i]) {
|
||||
total += cache[i]->defs.count;
|
||||
if (files[i].language == CBM_LANG_RUST) {
|
||||
total += cache[i]->impl_traits.count;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (total == 0) {
|
||||
*out_count = 0;
|
||||
@@ -306,39 +357,198 @@ CBMLSPDef *cbm_pxc_collect_all_defs(CBMFileResult **cache, const cbm_file_info_t
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
if (files[fi].language == CBM_LANG_RUST) {
|
||||
for (int ii = 0; ii < cache[fi]->impl_traits.count; ii++) {
|
||||
if (pxc_build_rust_impl_relation(
|
||||
&cache[fi]->arena, &cache[fi]->impl_traits.items[ii], project_name,
|
||||
files[fi].rel_path, def_modules[fi], &defs[idx]) == 0) {
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*out_count = idx;
|
||||
return defs;
|
||||
}
|
||||
|
||||
/* Build per-file import map (local_name -> resolved module QN) from gbuf
|
||||
* IMPORTS edges. Mirrors build_import_map() in pass_parallel.c. Returns 0
|
||||
* with *out_count = 0 when the file has no IMPORTS edges. Caller frees keys
|
||||
* with pxc_free_import_map. */
|
||||
static int pxc_build_import_map(const cbm_gbuf_t *gbuf, const char *project_name,
|
||||
const char *rel_path, const char ***out_keys,
|
||||
const char ***out_vals, int *out_count) {
|
||||
/* Return the one source import path bound to `local_name`, or NULL when the
|
||||
* extraction metadata is absent or two distinct imports bind the same local.
|
||||
* The latter is deliberately fail-closed: choosing either path would turn an
|
||||
* ambiguous Python value into a fabricated CALL_REFERENCE. */
|
||||
static const char *pxc_unique_import_path(const CBMFileResult *result, const char *local_name,
|
||||
bool *out_ambiguous) {
|
||||
if (out_ambiguous)
|
||||
*out_ambiguous = false;
|
||||
if (!result || !local_name)
|
||||
return NULL;
|
||||
const char *path = NULL;
|
||||
for (int i = 0; i < result->imports.count; i++) {
|
||||
const CBMImport *imp = &result->imports.items[i];
|
||||
if (!imp->local_name || !imp->module_path || strcmp(imp->local_name, local_name) != 0) {
|
||||
continue;
|
||||
}
|
||||
if (path && strcmp(path, imp->module_path) != 0) {
|
||||
if (out_ambiguous)
|
||||
*out_ambiguous = true;
|
||||
return NULL;
|
||||
}
|
||||
path = imp->module_path;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
static const char *pxc_import_leaf(const char *path) {
|
||||
if (!path)
|
||||
return NULL;
|
||||
const char *leaf = path;
|
||||
for (const char *p = path; *p; p++) {
|
||||
if (*p == '.' || *p == '/' || *p == '\\' || *p == ':')
|
||||
leaf = p + 1;
|
||||
}
|
||||
return leaf;
|
||||
}
|
||||
|
||||
static char *pxc_kotlin_import_from_metadata(const CBMFileResult *result, const char *local_name) {
|
||||
bool ambiguous = false;
|
||||
const char *path = pxc_unique_import_path(result, local_name, &ambiguous);
|
||||
const char *leaf = pxc_import_leaf(path);
|
||||
if (ambiguous || !path || !path[0] || path[0] == '.' || !leaf ||
|
||||
strcmp(leaf, local_name) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
/* Keep the source package/member spelling. Kotlin cross registries are
|
||||
* package-qualified (target.actual), not path/project-qualified. The
|
||||
* resolver still requires a registered function/type before emitting an
|
||||
* exact semantic relationship. */
|
||||
return strdup(path);
|
||||
}
|
||||
|
||||
/* IMPORTS edges correctly target dependency modules, but Python from-imports
|
||||
* also carry a source-level member: `from target import handler` is extracted
|
||||
* as module_path=`target.handler` while its edge targets Module `P.target`.
|
||||
* Reattach that member only when the raw metadata proves one unique,
|
||||
* non-aliased path. The shared Python registry must still materialize the
|
||||
* resulting exact QN before it earns CALL_REFERENCE. */
|
||||
static char *pxc_import_value_qn(CBMLanguage lang, const CBMFileResult *result,
|
||||
const char *local_name, const cbm_gbuf_node_t *target) {
|
||||
if (!target || !target->qualified_name)
|
||||
return NULL;
|
||||
if (lang == CBM_LANG_KOTLIN)
|
||||
return pxc_kotlin_import_from_metadata(result, local_name);
|
||||
if (lang != CBM_LANG_PYTHON)
|
||||
return strdup(target->qualified_name);
|
||||
|
||||
bool ambiguous = false;
|
||||
const char *path = pxc_unique_import_path(result, local_name, &ambiguous);
|
||||
if (ambiguous)
|
||||
return NULL;
|
||||
const char *source_leaf = pxc_import_leaf(path);
|
||||
const char *target_leaf = pxc_import_leaf(target->qualified_name);
|
||||
if (!path || !source_leaf || !target_leaf || strcmp(source_leaf, local_name) != 0 ||
|
||||
strcmp(source_leaf, target_leaf) == 0 || !target->label ||
|
||||
(strcmp(target->label, "Module") != 0 && strcmp(target->label, "Folder") != 0)) {
|
||||
return strdup(target->qualified_name);
|
||||
}
|
||||
|
||||
size_t need = strlen(target->qualified_name) + 1 + strlen(source_leaf) + 1;
|
||||
char *qualified = (char *)malloc(need);
|
||||
if (!qualified)
|
||||
return NULL;
|
||||
snprintf(qualified, need, "%s.%s", target->qualified_name, source_leaf);
|
||||
return qualified;
|
||||
}
|
||||
|
||||
static bool pxc_import_map_has_local(const char *const *keys, int count, const char *local_name) {
|
||||
if (!keys || !local_name)
|
||||
return false;
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (keys[i] && strcmp(keys[i], local_name) == 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Recover a Python import directly from extraction metadata when graph import
|
||||
* materialization produced no edge. Prefer an already materialized exact node
|
||||
* (including the shared project-prefix rule); otherwise accept only the
|
||||
* ordinary non-aliased dotted spelling and let the sealed Python registry
|
||||
* decide whether that QN denotes a class/function. A wrong candidate cannot
|
||||
* earn a semantic edge because the resolver requires registry materialization. */
|
||||
static char *pxc_python_import_from_metadata(const cbm_gbuf_t *gbuf, const char *project_name,
|
||||
const char *local_name, const char *module_path) {
|
||||
if (!gbuf || !local_name || !module_path)
|
||||
return NULL;
|
||||
const char *source_leaf = pxc_import_leaf(module_path);
|
||||
if (!source_leaf || strcmp(source_leaf, local_name) != 0 || module_path[0] == '.')
|
||||
return NULL;
|
||||
|
||||
const cbm_gbuf_node_t *exact =
|
||||
cbm_pipeline_lsp_target_node(gbuf, project_name, module_path, false);
|
||||
if (exact && exact->qualified_name)
|
||||
return strdup(exact->qualified_name);
|
||||
|
||||
const char *last_dot = strrchr(module_path, '.');
|
||||
if (last_dot && last_dot > module_path) {
|
||||
size_t module_len = (size_t)(last_dot - module_path);
|
||||
char *module = (char *)malloc(module_len + 1);
|
||||
if (!module)
|
||||
return NULL;
|
||||
memcpy(module, module_path, module_len);
|
||||
module[module_len] = '\0';
|
||||
const cbm_gbuf_node_t *module_node =
|
||||
cbm_pipeline_lsp_target_node(gbuf, project_name, module, false);
|
||||
free(module);
|
||||
if (module_node && module_node->qualified_name && module_node->label &&
|
||||
(strcmp(module_node->label, "Module") == 0 ||
|
||||
strcmp(module_node->label, "Folder") == 0)) {
|
||||
size_t need = strlen(module_node->qualified_name) + 1 + strlen(source_leaf) + 1;
|
||||
char *qualified = (char *)malloc(need);
|
||||
if (!qualified)
|
||||
return NULL;
|
||||
snprintf(qualified, need, "%s.%s", module_node->qualified_name, source_leaf);
|
||||
return qualified;
|
||||
}
|
||||
}
|
||||
|
||||
if (!project_name || !project_name[0])
|
||||
return strdup(module_path);
|
||||
size_t need = strlen(project_name) + 1 + strlen(module_path) + 1;
|
||||
char *qualified = (char *)malloc(need);
|
||||
if (!qualified)
|
||||
return NULL;
|
||||
snprintf(qualified, need, "%s.%s", project_name, module_path);
|
||||
return qualified;
|
||||
}
|
||||
|
||||
/* Build per-file import map (local_name -> semantic import QN) from gbuf
|
||||
* IMPORTS edges. Both pipeline drivers call this implementation. Returns 0
|
||||
* with *out_count = 0 when the file has no IMPORTS edges. */
|
||||
int cbm_pxc_build_import_map(const cbm_gbuf_t *gbuf, const char *project_name, const char *rel_path,
|
||||
CBMLanguage lang, const CBMFileResult *result, const char ***out_keys,
|
||||
const char ***out_vals, int *out_count) {
|
||||
*out_keys = NULL;
|
||||
*out_vals = NULL;
|
||||
*out_count = 0;
|
||||
|
||||
char *file_qn = cbm_pipeline_fqn_compute(project_name, rel_path, "__file__");
|
||||
if (!file_qn)
|
||||
return 0;
|
||||
const cbm_gbuf_node_t *file_node = cbm_gbuf_find_by_qn(gbuf, file_qn);
|
||||
free(file_qn);
|
||||
if (!file_node)
|
||||
return 0;
|
||||
|
||||
const cbm_gbuf_edge_t **edges = NULL;
|
||||
int edge_count = 0;
|
||||
int rc =
|
||||
cbm_gbuf_find_edges_by_source_type(gbuf, file_node->id, "IMPORTS", &edges, &edge_count);
|
||||
if (rc != 0 || edge_count == 0)
|
||||
char *file_qn = cbm_pipeline_fqn_compute(project_name, rel_path, "__file__");
|
||||
const cbm_gbuf_node_t *file_node = file_qn ? cbm_gbuf_find_by_qn(gbuf, file_qn) : NULL;
|
||||
free(file_qn);
|
||||
if (file_node && cbm_gbuf_find_edges_by_source_type(gbuf, file_node->id, "IMPORTS", &edges,
|
||||
&edge_count) != 0) {
|
||||
edges = NULL;
|
||||
edge_count = 0;
|
||||
}
|
||||
|
||||
int metadata_count =
|
||||
(lang == CBM_LANG_PYTHON || lang == CBM_LANG_KOTLIN) && result ? result->imports.count : 0;
|
||||
if (edge_count == 0 && metadata_count == 0)
|
||||
return 0;
|
||||
|
||||
const char **keys = (const char **)calloc((size_t)edge_count, sizeof(const char *));
|
||||
const char **vals = (const char **)calloc((size_t)edge_count, sizeof(const char *));
|
||||
size_t capacity = (size_t)edge_count + (size_t)metadata_count;
|
||||
const char **keys = (const char **)calloc(capacity, sizeof(const char *));
|
||||
const char **vals = (const char **)calloc(capacity, sizeof(const char *));
|
||||
if (!keys || !vals) {
|
||||
free(keys);
|
||||
free(vals);
|
||||
@@ -363,8 +573,43 @@ static int pxc_build_import_map(const cbm_gbuf_t *gbuf, const char *project_name
|
||||
continue;
|
||||
memcpy(local, start, n);
|
||||
local[n] = '\0';
|
||||
char *value = pxc_import_value_qn(lang, result, local, target);
|
||||
if (!value) {
|
||||
free(local);
|
||||
continue;
|
||||
}
|
||||
keys[count] = local;
|
||||
vals[count] = target->qualified_name; /* borrowed from gbuf */
|
||||
vals[count] = value;
|
||||
count++;
|
||||
}
|
||||
|
||||
/* IMPORTS edges are best-effort graph relationships. The cross-LSP still
|
||||
* has the authoritative extraction metadata in both sequential and
|
||||
* parallel caches, so a missing edge must not erase an otherwise exact
|
||||
* Python import. Add only locals not already represented by an edge. */
|
||||
for (int i = 0; i < metadata_count; i++) {
|
||||
const CBMImport *imp = &result->imports.items[i];
|
||||
if (!imp->local_name || !imp->local_name[0] || !imp->module_path ||
|
||||
pxc_import_map_has_local(keys, count, imp->local_name)) {
|
||||
continue;
|
||||
}
|
||||
bool ambiguous = false;
|
||||
const char *path = pxc_unique_import_path(result, imp->local_name, &ambiguous);
|
||||
if (ambiguous || !path)
|
||||
continue;
|
||||
char *value =
|
||||
lang == CBM_LANG_KOTLIN
|
||||
? pxc_kotlin_import_from_metadata(result, imp->local_name)
|
||||
: pxc_python_import_from_metadata(gbuf, project_name, imp->local_name, path);
|
||||
if (!value)
|
||||
continue;
|
||||
char *local = strdup(imp->local_name);
|
||||
if (!local) {
|
||||
free(value);
|
||||
continue;
|
||||
}
|
||||
keys[count] = local;
|
||||
vals[count] = value;
|
||||
count++;
|
||||
}
|
||||
*out_keys = keys;
|
||||
@@ -373,13 +618,17 @@ static int pxc_build_import_map(const cbm_gbuf_t *gbuf, const char *project_name
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void pxc_free_import_map(const char **keys, const char **vals, int count) {
|
||||
void cbm_pxc_free_import_map(const char **keys, const char **vals, int count) {
|
||||
if (keys) {
|
||||
for (int i = 0; i < count; i++)
|
||||
free((void *)keys[i]);
|
||||
free((void *)keys);
|
||||
}
|
||||
free((void *)vals); /* vals strings borrowed from gbuf — don't free elements */
|
||||
if (vals) {
|
||||
for (int i = 0; i < count; i++)
|
||||
free((void *)vals[i]);
|
||||
free((void *)vals);
|
||||
}
|
||||
}
|
||||
|
||||
/* Detect TS dialect flags from a relative path. */
|
||||
@@ -424,10 +673,14 @@ bool cbm_pxc_has_cross_lsp(CBMLanguage lang) {
|
||||
/* Append cross-file results from `src_out` (allocated in a scratch arena
|
||||
* about to be destroyed) into `dst_calls` (lives in cache_entry->arena),
|
||||
* copying every string field into dst_arena. Skips entries whose
|
||||
* (caller_qn, callee_qn) is already present — avoids inflating the array with
|
||||
* cross-file duplicates of per-file LSP output.
|
||||
* (kind, caller_qn, callee_qn, source span) is already present — avoids
|
||||
* inflating the array with cross-file duplicates without conflating distinct
|
||||
* same-named occurrences. For an exact duplicate, retain the higher-confidence
|
||||
* record.
|
||||
*
|
||||
* Dedup uses a hash set keyed on "caller\x1fcallee", giving O(1) membership.
|
||||
* Dedup uses a hash table keyed on
|
||||
* "kind\x1fcaller\x1fcallee\x1fstart:end\x1forigin", giving O(1)
|
||||
* membership.
|
||||
* The previous linear strcmp scan made each append O(n), so a file that
|
||||
* resolved very many cross-calls turned the whole append into O(n^2) and could
|
||||
* peg a core for minutes (observed: an index hung in pxc_append_results/strcmp).
|
||||
@@ -444,9 +697,16 @@ static void pxc_append_results(CBMArena *dst_arena, CBMResolvedCallArray *dst_ca
|
||||
for (int i = 0; i < dst_calls->count; i++) {
|
||||
const CBMResolvedCall *rc = &dst_calls->items[i];
|
||||
if (rc->caller_qn && rc->callee_qn) {
|
||||
char *k = cbm_arena_sprintf(&keys, "%s\x1f%s", rc->caller_qn, rc->callee_qn);
|
||||
char *k = cbm_arena_sprintf(&keys, "%u\x1f%s\x1f%s\x1f%u:%u\x1f%u", (unsigned)rc->kind,
|
||||
rc->caller_qn, rc->callee_qn, rc->site_start_byte,
|
||||
rc->site_end_byte, (unsigned)rc->source_origin);
|
||||
if (k) {
|
||||
cbm_ht_set(seen, k, (void *)1);
|
||||
void *prior = cbm_ht_get(seen, k);
|
||||
if (!prior ||
|
||||
rc->confidence > dst_calls->items[(int)(uintptr_t)prior - 1].confidence) {
|
||||
const char *stored = cbm_ht_get_key(seen, k);
|
||||
cbm_ht_set(seen, stored ? stored : k, (void *)(uintptr_t)(i + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -455,20 +715,104 @@ static void pxc_append_results(CBMArena *dst_arena, CBMResolvedCallArray *dst_ca
|
||||
const CBMResolvedCall *src = &src_out->items[j];
|
||||
if (!src->caller_qn || !src->callee_qn)
|
||||
continue;
|
||||
char *k = cbm_arena_sprintf(&keys, "%s\x1f%s", src->caller_qn, src->callee_qn);
|
||||
if (k && cbm_ht_has(seen, k))
|
||||
char *k = cbm_arena_sprintf(&keys, "%u\x1f%s\x1f%s\x1f%u:%u\x1f%u", (unsigned)src->kind,
|
||||
src->caller_qn, src->callee_qn, src->site_start_byte,
|
||||
src->site_end_byte, (unsigned)src->source_origin);
|
||||
void *prior = k ? cbm_ht_get(seen, k) : NULL;
|
||||
if (prior) {
|
||||
CBMResolvedCall *dst = &dst_calls->items[(int)(uintptr_t)prior - 1];
|
||||
if (src->confidence > dst->confidence) {
|
||||
dst->caller_qn = cbm_arena_strdup(dst_arena, src->caller_qn);
|
||||
dst->callee_qn = cbm_arena_strdup(dst_arena, src->callee_qn);
|
||||
dst->strategy = src->strategy ? cbm_arena_strdup(dst_arena, src->strategy) : NULL;
|
||||
dst->confidence = src->confidence;
|
||||
dst->reason = src->reason ? cbm_arena_strdup(dst_arena, src->reason) : NULL;
|
||||
dst->kind = src->kind;
|
||||
dst->site_start_byte = src->site_start_byte;
|
||||
dst->site_end_byte = src->site_end_byte;
|
||||
dst->source_origin = src->source_origin;
|
||||
}
|
||||
continue;
|
||||
if (k) {
|
||||
cbm_ht_set(seen, k, (void *)1);
|
||||
}
|
||||
CBMResolvedCall dst;
|
||||
memset(&dst, 0, sizeof(dst));
|
||||
CBMResolvedCall dst = {0};
|
||||
dst.caller_qn = cbm_arena_strdup(dst_arena, src->caller_qn);
|
||||
dst.callee_qn = cbm_arena_strdup(dst_arena, src->callee_qn);
|
||||
dst.strategy = src->strategy ? cbm_arena_strdup(dst_arena, src->strategy) : NULL;
|
||||
dst.confidence = src->confidence;
|
||||
dst.reason = src->reason ? cbm_arena_strdup(dst_arena, src->reason) : NULL;
|
||||
dst.kind = src->kind;
|
||||
dst.site_start_byte = src->site_start_byte;
|
||||
dst.site_end_byte = src->site_end_byte;
|
||||
dst.source_origin = src->source_origin;
|
||||
cbm_resolvedcall_push(dst_calls, dst_arena, dst);
|
||||
if (k) {
|
||||
cbm_ht_set(seen, k, (void *)(uintptr_t)dst_calls->count);
|
||||
}
|
||||
}
|
||||
|
||||
cbm_ht_free(seen);
|
||||
cbm_arena_destroy(&keys);
|
||||
}
|
||||
|
||||
/* Merge exact synthetic call carriers produced by a cross-LSP resolver. The
|
||||
* source may live in the per-file result arena or in cbm_pxc_run_one's scratch
|
||||
* arena, so every pointer-bearing field is copied explicitly. Invalid/legacy
|
||||
* zero-span carriers are rejected: a requires-LSP carrier is safe only when it
|
||||
* can join the semantic record for the same source occurrence. */
|
||||
static void pxc_append_synthetic_calls(CBMArena *dst_arena, CBMCallArray *dst_calls,
|
||||
const CBMCallArray *src_calls) {
|
||||
if (!dst_arena || !dst_calls || !src_calls || src_calls->count <= 0)
|
||||
return;
|
||||
|
||||
CBMArena keys;
|
||||
cbm_arena_init(&keys);
|
||||
CBMHashTable *seen = cbm_ht_create((uint32_t)(dst_calls->count + src_calls->count + 1));
|
||||
|
||||
for (int i = 0; i < dst_calls->count; i++) {
|
||||
const CBMCall *call = &dst_calls->items[i];
|
||||
if (!call->callee_name || !call->enclosing_func_qn ||
|
||||
call->site_end_byte <= call->site_start_byte) {
|
||||
continue;
|
||||
}
|
||||
char *key = cbm_arena_sprintf(
|
||||
&keys, "%s\x1f%s\x1f%u:%u\x1f%u\x1f%d:%d", call->enclosing_func_qn, call->callee_name,
|
||||
call->site_start_byte, call->site_end_byte, (unsigned)call->source_origin,
|
||||
call->requires_lsp_resolution ? 1 : 0, call->is_method ? 1 : 0);
|
||||
if (key && !cbm_ht_get(seen, key))
|
||||
cbm_ht_set(seen, key, (void *)(uintptr_t)(i + 1));
|
||||
}
|
||||
|
||||
for (int i = 0; i < src_calls->count; i++) {
|
||||
const CBMCall *src = &src_calls->items[i];
|
||||
if (!src->requires_lsp_resolution || !src->callee_name || !src->enclosing_func_qn ||
|
||||
src->site_end_byte <= src->site_start_byte) {
|
||||
continue;
|
||||
}
|
||||
char *key = cbm_arena_sprintf(
|
||||
&keys, "%s\x1f%s\x1f%u:%u\x1f%u\x1f%d:%d", src->enclosing_func_qn, src->callee_name,
|
||||
src->site_start_byte, src->site_end_byte, (unsigned)src->source_origin,
|
||||
src->requires_lsp_resolution ? 1 : 0, src->is_method ? 1 : 0);
|
||||
if (key && cbm_ht_get(seen, key))
|
||||
continue;
|
||||
|
||||
CBMCall dst = *src;
|
||||
dst.callee_name = cbm_arena_strdup(dst_arena, src->callee_name);
|
||||
dst.enclosing_func_qn = cbm_arena_strdup(dst_arena, src->enclosing_func_qn);
|
||||
dst.first_string_arg =
|
||||
src->first_string_arg ? cbm_arena_strdup(dst_arena, src->first_string_arg) : NULL;
|
||||
dst.second_arg_name =
|
||||
src->second_arg_name ? cbm_arena_strdup(dst_arena, src->second_arg_name) : NULL;
|
||||
for (int ai = 0; ai < CBM_MAX_CALL_ARGS; ai++) {
|
||||
dst.args[ai].expr =
|
||||
src->args[ai].expr ? cbm_arena_strdup(dst_arena, src->args[ai].expr) : NULL;
|
||||
dst.args[ai].value =
|
||||
src->args[ai].value ? cbm_arena_strdup(dst_arena, src->args[ai].value) : NULL;
|
||||
dst.args[ai].keyword =
|
||||
src->args[ai].keyword ? cbm_arena_strdup(dst_arena, src->args[ai].keyword) : NULL;
|
||||
}
|
||||
cbm_calls_push(dst_calls, dst_arena, dst);
|
||||
if (key)
|
||||
cbm_ht_set(seen, key, (void *)(uintptr_t)dst_calls->count);
|
||||
}
|
||||
|
||||
cbm_ht_free(seen);
|
||||
@@ -498,12 +842,8 @@ const struct CBMCargoManifest *cbm_pxc_get_rust_manifest(void) {
|
||||
|
||||
/* Convert a CBMLSPDef array (the pipeline's lingua franca, go_lsp.h:73)
|
||||
* into a CBMRustLSPDef array (rust_lsp.h) inside `arena`. The two structs
|
||||
* share their first 9 string fields; CBMRustLSPDef adds `trait_qn` before
|
||||
* `is_interface` whereas CBMLSPDef has `is_interface` followed by `lang`,
|
||||
* so a memcpy is unsafe — copy field-by-field. trait_qn is left NULL
|
||||
* because the pipeline's collect-all-defs step does not carry the
|
||||
* impl-Trait-for-Type linkage; the resolver still recovers trait dispatch
|
||||
* from the in-file walk (the cross-file path only needs receiver_type). */
|
||||
* have similar fields but different layouts; CBMLSPDef adds
|
||||
* language/namespace metadata, so a memcpy is unsafe — copy field-by-field. */
|
||||
static CBMRustLSPDef *pxc_lspdefs_to_rust(CBMArena *arena, const CBMLSPDef *defs, int def_count) {
|
||||
if (!defs || def_count <= 0)
|
||||
return NULL;
|
||||
@@ -521,8 +861,12 @@ static CBMRustLSPDef *pxc_lspdefs_to_rust(CBMArena *arena, const CBMLSPDef *defs
|
||||
out[i].embedded_types = defs[i].embedded_types;
|
||||
out[i].field_defs = defs[i].field_defs;
|
||||
out[i].method_names_str = defs[i].method_names_str;
|
||||
out[i].trait_qn = NULL;
|
||||
out[i].signature_param_types = defs[i].signature_param_types;
|
||||
out[i].signature_param_count = defs[i].signature_param_count;
|
||||
out[i].trait_qn = defs[i].trait_qn;
|
||||
out[i].is_interface = defs[i].is_interface;
|
||||
out[i].is_rust_impl_relation = defs[i].is_rust_impl_relation;
|
||||
out[i].is_abstract = defs[i].is_abstract;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -543,6 +887,8 @@ void cbm_pxc_run_one(CBMLanguage lang, CBMFileResult *r, const char *source, int
|
||||
cbm_arena_init(&scratch);
|
||||
CBMResolvedCallArray out;
|
||||
memset(&out, 0, sizeof(out));
|
||||
CBMCallArray synthetic_calls;
|
||||
memset(&synthetic_calls, 0, sizeof(synthetic_calls));
|
||||
|
||||
switch (lang) {
|
||||
case CBM_LANG_GO:
|
||||
@@ -563,7 +909,7 @@ void cbm_pxc_run_one(CBMLanguage lang, CBMFileResult *r, const char *source, int
|
||||
}
|
||||
case CBM_LANG_PYTHON:
|
||||
cbm_run_py_lsp_cross(&scratch, source, source_len, module_qn, defs, def_count, imp_names,
|
||||
imp_qns, imp_count, tree, &out);
|
||||
imp_qns, imp_count, tree, &out, &synthetic_calls);
|
||||
break;
|
||||
case CBM_LANG_PHP:
|
||||
cbm_run_php_lsp_cross(&scratch, source, source_len, module_qn, defs, def_count, imp_names,
|
||||
@@ -586,7 +932,7 @@ void cbm_pxc_run_one(CBMLanguage lang, CBMFileResult *r, const char *source, int
|
||||
CBMRustLSPDef *rdefs = pxc_lspdefs_to_rust(&scratch, defs, def_count);
|
||||
cbm_run_rust_lsp_cross_with_manifest(&scratch, source, source_len, module_qn, rdefs,
|
||||
def_count, imp_names, imp_qns, imp_count, tree,
|
||||
g_pxc_rust_manifest, &out);
|
||||
g_pxc_rust_manifest, &out, &synthetic_calls);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -594,6 +940,7 @@ void cbm_pxc_run_one(CBMLanguage lang, CBMFileResult *r, const char *source, int
|
||||
}
|
||||
|
||||
pxc_append_results(&r->arena, &r->resolved_calls, &out);
|
||||
pxc_append_synthetic_calls(&r->arena, &r->calls, &synthetic_calls);
|
||||
cbm_arena_destroy(&scratch);
|
||||
}
|
||||
|
||||
@@ -655,12 +1002,20 @@ void cbm_pxc_dispatch_file(CBMLanguage lang, CBMFileResult *result, const char *
|
||||
cbm_go_fast_resolve_qualified_calls(result, prebuilt, imp_keys, imp_vals, imp_count);
|
||||
used_prebuilt = true;
|
||||
break;
|
||||
case CBM_LANG_PYTHON:
|
||||
cbm_run_py_lsp_cross_with_registry(&result->arena, source, source_len, def_module,
|
||||
prebuilt, imp_keys, imp_vals, imp_count,
|
||||
result->cached_tree, &result->resolved_calls);
|
||||
case CBM_LANG_PYTHON: {
|
||||
CBMArena scratch;
|
||||
cbm_arena_init(&scratch);
|
||||
CBMResolvedCallArray out = {0};
|
||||
CBMCallArray synthetic_calls = {0};
|
||||
cbm_run_py_lsp_cross_with_registry(&scratch, source, source_len, def_module, prebuilt,
|
||||
imp_keys, imp_vals, imp_count, result->cached_tree,
|
||||
&out, &synthetic_calls);
|
||||
pxc_append_results(&result->arena, &result->resolved_calls, &out);
|
||||
pxc_append_synthetic_calls(&result->arena, &result->calls, &synthetic_calls);
|
||||
cbm_arena_destroy(&scratch);
|
||||
used_prebuilt = true;
|
||||
break;
|
||||
}
|
||||
case CBM_LANG_C:
|
||||
case CBM_LANG_CPP:
|
||||
case CBM_LANG_CUDA:
|
||||
@@ -690,10 +1045,11 @@ void cbm_pxc_dispatch_file(CBMLanguage lang, CBMFileResult *result, const char *
|
||||
CBMLSPDef *ts_filtered = NULL;
|
||||
if (module_def_index) {
|
||||
int fc = 0;
|
||||
ts_filtered = cbm_pxc_filter_defs_for_file(module_def_index, all_defs, lang,
|
||||
result->namespace_name, def_module,
|
||||
imp_vals, imp_count, &fc);
|
||||
if (ts_filtered) {
|
||||
bool filter_succeeded = false;
|
||||
ts_filtered = cbm_pxc_filter_defs_for_file(
|
||||
module_def_index, all_defs, lang, result->namespace_name, def_module, imp_vals,
|
||||
imp_count, &fc, &filter_succeeded);
|
||||
if (filter_succeeded) {
|
||||
ts_defs = ts_filtered;
|
||||
ts_def_count = fc;
|
||||
}
|
||||
@@ -728,10 +1084,11 @@ void cbm_pxc_dispatch_file(CBMLanguage lang, CBMFileResult *result, const char *
|
||||
int file_def_count = all_def_count;
|
||||
if (module_def_index && lang != CBM_LANG_RUST) {
|
||||
int filtered_count = 0;
|
||||
filtered =
|
||||
cbm_pxc_filter_defs_for_file(module_def_index, all_defs, lang, result->namespace_name,
|
||||
def_module, imp_vals, imp_count, &filtered_count);
|
||||
if (filtered) {
|
||||
bool filter_succeeded = false;
|
||||
filtered = cbm_pxc_filter_defs_for_file(module_def_index, all_defs, lang,
|
||||
result->namespace_name, def_module, imp_vals,
|
||||
imp_count, &filtered_count, &filter_succeeded);
|
||||
if (filter_succeeded) {
|
||||
file_defs = filtered;
|
||||
file_def_count = filtered_count;
|
||||
}
|
||||
@@ -739,11 +1096,16 @@ void cbm_pxc_dispatch_file(CBMLanguage lang, CBMFileResult *result, const char *
|
||||
if (lang == CBM_LANG_RUST) {
|
||||
CBMTypeRegistry *shared = rust_shared_get ? rust_shared_get(rust_shared_ctx) : NULL;
|
||||
if (shared) {
|
||||
cbm_run_rust_lsp_cross_with_registry(&result->arena, source, source_len, def_module,
|
||||
shared, imp_keys, imp_vals, imp_count,
|
||||
result->cached_tree, cbm_pxc_get_rust_manifest(),
|
||||
&result->resolved_calls,
|
||||
/*result=*/NULL);
|
||||
CBMArena scratch;
|
||||
cbm_arena_init(&scratch);
|
||||
CBMResolvedCallArray out = {0};
|
||||
CBMCallArray synthetic_calls = {0};
|
||||
cbm_run_rust_lsp_cross_with_registry(
|
||||
&scratch, source, source_len, def_module, shared, imp_keys, imp_vals, imp_count,
|
||||
result->cached_tree, cbm_pxc_get_rust_manifest(), &out, &synthetic_calls);
|
||||
pxc_append_results(&result->arena, &result->resolved_calls, &out);
|
||||
pxc_append_synthetic_calls(&result->arena, &result->calls, &synthetic_calls);
|
||||
cbm_arena_destroy(&scratch);
|
||||
} else {
|
||||
cbm_pxc_run_one(lang, result, source, source_len, def_module, file_defs, file_def_count,
|
||||
imp_keys, imp_vals, imp_count);
|
||||
@@ -877,8 +1239,8 @@ int cbm_pipeline_pass_lsp_cross(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *
|
||||
const char **imp_keys = NULL;
|
||||
const char **imp_vals = NULL;
|
||||
int imp_count = 0;
|
||||
pxc_build_import_map(ctx->gbuf, ctx->project_name, files[i].rel_path, &imp_keys, &imp_vals,
|
||||
&imp_count);
|
||||
cbm_pxc_build_import_map(ctx->gbuf, ctx->project_name, files[i].rel_path, lang, cache[i],
|
||||
&imp_keys, &imp_vals, &imp_count);
|
||||
|
||||
/* Journal around the resolve: a hang here must be attributed to THIS
|
||||
* file, not to a stale extraction marker (the innocent-quarantine
|
||||
@@ -891,7 +1253,7 @@ int cbm_pipeline_pass_lsp_cross(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *
|
||||
per_lang_calls++;
|
||||
processed++;
|
||||
|
||||
pxc_free_import_map(imp_keys, imp_vals, imp_count);
|
||||
cbm_pxc_free_import_map(imp_keys, imp_vals, imp_count);
|
||||
free(source);
|
||||
}
|
||||
|
||||
@@ -1003,6 +1365,15 @@ static bool pxc_is_jvm_lang(CBMLanguage lang) {
|
||||
return lang == CBM_LANG_JAVA || lang == CBM_LANG_KOTLIN;
|
||||
}
|
||||
|
||||
/* Java and Kotlin files without a package declaration still share one JVM
|
||||
* default package. The hash table intentionally rejects empty keys, so use an
|
||||
* internal-only sentinel to make that package selectable across files. */
|
||||
static const char *pxc_namespace_index_key(const char *namespace_name) {
|
||||
return namespace_name && namespace_name[0] ? namespace_name
|
||||
: "\x1f"
|
||||
"cbm-jvm-default-package";
|
||||
}
|
||||
|
||||
static bool pxc_def_lang_matches(CBMLanguage caller_lang, CBMLanguage def_lang) {
|
||||
if (pxc_is_jvm_lang(caller_lang)) {
|
||||
return pxc_is_jvm_lang(def_lang);
|
||||
@@ -1023,6 +1394,65 @@ static void pxc_mark_module_defs(const CBMModuleDefIndex *idx, bool *selected,
|
||||
}
|
||||
}
|
||||
|
||||
/* Import maps bind source locals to semantic targets. A from-import therefore
|
||||
* carries a symbol QN (`project.module.Class` / `project.module.function`),
|
||||
* while the def index is keyed by the declaring file module
|
||||
* (`project.module`). Select the nearest materialized module prefix; the
|
||||
* language registry still has to prove the full target QN before an edge is
|
||||
* emitted, so this broadens the candidate set without weakening precision. */
|
||||
static void pxc_mark_import_defs(const CBMModuleDefIndex *idx, bool *selected,
|
||||
const CBMLSPDef *all_defs, CBMLanguage caller_lang,
|
||||
const char *import_qn, int *total) {
|
||||
if (!idx || !idx->ht || !import_qn || !import_qn[0]) {
|
||||
return;
|
||||
}
|
||||
char *candidate = strdup(import_qn);
|
||||
if (!candidate) {
|
||||
return;
|
||||
}
|
||||
bool matched = false;
|
||||
for (;;) {
|
||||
pxc_module_entry_t *entry = (pxc_module_entry_t *)cbm_ht_get(idx->ht, candidate);
|
||||
if (entry) {
|
||||
int added = pxc_mark_entry_defs(selected, entry, all_defs, caller_lang);
|
||||
if (total) {
|
||||
*total += added;
|
||||
}
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
char *dot = strrchr(candidate, '.');
|
||||
if (!dot) {
|
||||
break;
|
||||
}
|
||||
*dot = '\0';
|
||||
}
|
||||
/* Kotlin/Java source imports are package-qualified while file modules are
|
||||
* path-qualified. If no module prefix matched, walk the declared-package
|
||||
* index from the full import toward its package. This only selects a small
|
||||
* candidate registry; the language resolver must still prove the symbol. */
|
||||
if (!matched && pxc_is_jvm_lang(caller_lang) && idx->namespace_ht) {
|
||||
strcpy(candidate, import_qn);
|
||||
for (;;) {
|
||||
pxc_module_entry_t *entry = (pxc_module_entry_t *)cbm_ht_get(
|
||||
idx->namespace_ht, pxc_namespace_index_key(candidate));
|
||||
if (entry) {
|
||||
int added = pxc_mark_entry_defs(selected, entry, all_defs, caller_lang);
|
||||
if (total) {
|
||||
*total += added;
|
||||
}
|
||||
break;
|
||||
}
|
||||
char *dot = strrchr(candidate, '.');
|
||||
if (!dot) {
|
||||
break;
|
||||
}
|
||||
*dot = '\0';
|
||||
}
|
||||
}
|
||||
free(candidate);
|
||||
}
|
||||
|
||||
CBMModuleDefIndex *cbm_pxc_build_module_def_index(CBMLSPDef *all_defs, int def_count) {
|
||||
if (!all_defs || def_count <= 0) {
|
||||
return NULL;
|
||||
@@ -1043,7 +1473,9 @@ CBMModuleDefIndex *cbm_pxc_build_module_def_index(CBMLSPDef *all_defs, int def_c
|
||||
pxc_module_entry_add_index(pxc_module_entry_get_or_create(ht, all_defs[i].def_module_qn),
|
||||
i);
|
||||
pxc_module_entry_add_index(
|
||||
pxc_module_entry_get_or_create(namespace_ht, all_defs[i].namespace_name), i);
|
||||
pxc_module_entry_get_or_create(namespace_ht,
|
||||
pxc_namespace_index_key(all_defs[i].namespace_name)),
|
||||
i);
|
||||
}
|
||||
|
||||
CBMModuleDefIndex *idx = (CBMModuleDefIndex *)calloc(1, sizeof(*idx));
|
||||
@@ -1078,11 +1510,14 @@ void cbm_pxc_free_module_def_index(CBMModuleDefIndex *idx) {
|
||||
CBMLSPDef *cbm_pxc_filter_defs_for_file(const CBMModuleDefIndex *idx, CBMLSPDef *all_defs,
|
||||
CBMLanguage caller_lang, const char *caller_namespace,
|
||||
const char *own_module, const char *const *imp_qns,
|
||||
int imp_count, int *out_count) {
|
||||
int imp_count, int *out_count, bool *out_success) {
|
||||
if (out_count) {
|
||||
*out_count = 0;
|
||||
}
|
||||
if (!idx || !idx->ht || !all_defs || !out_count || idx->def_count <= 0) {
|
||||
if (out_success) {
|
||||
*out_success = false;
|
||||
}
|
||||
if (!idx || !idx->ht || !all_defs || !out_count || !out_success || idx->def_count <= 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -1094,17 +1529,17 @@ CBMLSPDef *cbm_pxc_filter_defs_for_file(const CBMModuleDefIndex *idx, CBMLSPDef
|
||||
int total = 0;
|
||||
pxc_mark_module_defs(idx, selected, all_defs, caller_lang, own_module, &total);
|
||||
for (int i = 0; i < imp_count; i++) {
|
||||
pxc_mark_module_defs(idx, selected, all_defs, caller_lang, imp_qns[i], &total);
|
||||
pxc_mark_import_defs(idx, selected, all_defs, caller_lang, imp_qns[i], &total);
|
||||
}
|
||||
if (pxc_is_jvm_lang(caller_lang) && caller_namespace && caller_namespace[0] &&
|
||||
idx->namespace_ht) {
|
||||
pxc_module_entry_t *e =
|
||||
(pxc_module_entry_t *)cbm_ht_get(idx->namespace_ht, caller_namespace);
|
||||
if (pxc_is_jvm_lang(caller_lang) && idx->namespace_ht) {
|
||||
const char *namespace_key = pxc_namespace_index_key(caller_namespace);
|
||||
pxc_module_entry_t *e = (pxc_module_entry_t *)cbm_ht_get(idx->namespace_ht, namespace_key);
|
||||
total += pxc_mark_entry_defs(selected, e, all_defs, caller_lang);
|
||||
}
|
||||
|
||||
if (total == 0) {
|
||||
free(selected);
|
||||
*out_success = true;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -1121,6 +1556,7 @@ CBMLSPDef *cbm_pxc_filter_defs_for_file(const CBMModuleDefIndex *idx, CBMLSPDef
|
||||
}
|
||||
}
|
||||
*out_count = n;
|
||||
*out_success = true;
|
||||
free(selected);
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -58,6 +58,17 @@ CBMLSPDef *cbm_pxc_collect_all_defs(CBMFileResult **cache, const cbm_file_info_t
|
||||
void cbm_pxc_ts_modes(CBMLanguage lang, const char *rel_path, bool *out_js, bool *out_jsx,
|
||||
bool *out_dts);
|
||||
|
||||
/* Build the local-name -> semantic import-QN map consumed by cross-file LSPs.
|
||||
* Both sequential and parallel drivers use this exact helper so import
|
||||
* metadata cannot diverge between pipelines. Values are owned by the returned
|
||||
* map (not borrowed from gbuf); release both arrays with
|
||||
* cbm_pxc_free_import_map(). */
|
||||
int cbm_pxc_build_import_map(const cbm_gbuf_t *gbuf, const char *project_name, const char *rel_path,
|
||||
CBMLanguage lang, const CBMFileResult *result, const char ***out_keys,
|
||||
const char ***out_vals, int *out_count);
|
||||
|
||||
void cbm_pxc_free_import_map(const char **keys, const char **vals, int count);
|
||||
|
||||
/* ── Per-module def index (the gopls "package summary" pattern) ──
|
||||
*
|
||||
* The hot path used to register ALL all_defs[] into a fresh registry
|
||||
@@ -87,11 +98,13 @@ void cbm_pxc_free_module_def_index(CBMModuleDefIndex *idx);
|
||||
* `src/main/kotlin` roots without import statements. String fields inside
|
||||
* each entry are borrowed from the original all_defs[] arena (caller keeps
|
||||
* it alive). Caller frees the returned array with free(). Writes the entry
|
||||
* count to *out_count. Returns NULL if no matches (with *out_count = 0). */
|
||||
* count to *out_count and sets *out_success on every valid selection. A valid
|
||||
* empty selection returns NULL with *out_count = 0 and *out_success = true;
|
||||
* NULL with *out_success = false means invalid input or allocation failure. */
|
||||
CBMLSPDef *cbm_pxc_filter_defs_for_file(const CBMModuleDefIndex *idx, CBMLSPDef *all_defs,
|
||||
CBMLanguage caller_lang, const char *caller_namespace,
|
||||
const char *own_module, const char *const *imp_qns,
|
||||
int imp_count, int *out_count);
|
||||
int imp_count, int *out_count, bool *out_success);
|
||||
|
||||
/* ── Tier 2 full: pre-built per-language cross-LSP registries ─────
|
||||
*
|
||||
|
||||
+386
-166
@@ -89,7 +89,6 @@ enum { PP_CSHARP_M_PREFIX_LEN = 2 };
|
||||
#include "cbm.h"
|
||||
#include "arena.h"
|
||||
#include "macro_table.h"
|
||||
#include "iris_export_xml.h"
|
||||
#include "simhash/minhash.h"
|
||||
#include "semantic/ast_profile.h"
|
||||
|
||||
@@ -106,6 +105,7 @@ enum { PP_CSHARP_M_PREFIX_LEN = 2 };
|
||||
* not re-pay the full nap tax on every file pull when napping cannot reclaim
|
||||
* memory (the resident floor, not in-flight transients, holds the budget). */
|
||||
static _Atomic long g_bp_nap_cycles = 0;
|
||||
static _Atomic uint64_t g_lsp_linear_fallback_rows = 0;
|
||||
|
||||
long cbm_pp_bp_nap_cycles(void) {
|
||||
return atomic_load_explicit(&g_bp_nap_cycles, memory_order_relaxed);
|
||||
@@ -115,6 +115,14 @@ void cbm_pp_bp_nap_cycles_reset(void) {
|
||||
atomic_store_explicit(&g_bp_nap_cycles, 0, memory_order_relaxed);
|
||||
}
|
||||
|
||||
uint64_t cbm_pp_lsp_linear_fallback_rows(void) {
|
||||
return atomic_load_explicit(&g_lsp_linear_fallback_rows, memory_order_relaxed);
|
||||
}
|
||||
|
||||
void cbm_pp_lsp_linear_fallback_rows_reset(void) {
|
||||
atomic_store_explicit(&g_lsp_linear_fallback_rows, 0, memory_order_relaxed);
|
||||
}
|
||||
|
||||
/* Parse a positive MB-valued retention env knob (CBM_RETAIN_*_MB) into bytes.
|
||||
* Follows the limits.c strtol convention: unset / unparseable / non-positive
|
||||
* → return 0 so the caller keeps its derived default. */
|
||||
@@ -528,68 +536,6 @@ static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def)
|
||||
}
|
||||
}
|
||||
|
||||
/* Build import map from graph buffer IMPORTS edges (read-only access to gbuf). */
|
||||
static int build_import_map(const cbm_gbuf_t *gbuf, const char *project_name, const char *rel_path,
|
||||
const char ***out_keys, const char ***out_vals, int *out_count) {
|
||||
*out_keys = NULL;
|
||||
*out_vals = NULL;
|
||||
*out_count = 0;
|
||||
|
||||
char *file_qn = cbm_pipeline_fqn_compute(project_name, rel_path, "__file__");
|
||||
const cbm_gbuf_node_t *file_node = cbm_gbuf_find_by_qn(gbuf, file_qn);
|
||||
free(file_qn);
|
||||
if (!file_node) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const cbm_gbuf_edge_t **edges = NULL;
|
||||
int edge_count = 0;
|
||||
int rc =
|
||||
cbm_gbuf_find_edges_by_source_type(gbuf, file_node->id, "IMPORTS", &edges, &edge_count);
|
||||
if (rc != 0 || edge_count == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char **keys = calloc(edge_count, sizeof(const char *));
|
||||
const char **vals = calloc(edge_count, sizeof(const char *));
|
||||
int count = 0;
|
||||
|
||||
for (int i = 0; i < edge_count; i++) {
|
||||
const cbm_gbuf_edge_t *e = edges[i];
|
||||
const cbm_gbuf_node_t *target = cbm_gbuf_find_by_id(gbuf, e->target_id);
|
||||
if (!target || !e->properties_json) {
|
||||
continue;
|
||||
}
|
||||
const char *start = strstr(e->properties_json, "\"local_name\":\"");
|
||||
if (start) {
|
||||
start += strlen("\"local_name\":\"");
|
||||
const char *end = strchr(start, '"');
|
||||
if (end && end > start) {
|
||||
keys[count] = cbm_strndup(start, end - start);
|
||||
vals[count] = target->qualified_name;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*out_keys = keys;
|
||||
*out_vals = vals;
|
||||
*out_count = count;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void free_import_map(const char **keys, const char **vals, int count) {
|
||||
if (keys) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
free((void *)keys[i]);
|
||||
}
|
||||
free((void *)keys);
|
||||
}
|
||||
if (vals) {
|
||||
free((void *)vals);
|
||||
}
|
||||
}
|
||||
|
||||
/* True for languages whose module QN derives from the CONTAINING DIRECTORY
|
||||
* (Java/Go package). MUST match cbm_lang_module_is_dir() (internal/cbm/helpers.c)
|
||||
* and pxc_module_is_dir() (pass_lsp_cross.c) so same-module callee resolution
|
||||
@@ -634,11 +580,14 @@ static void extract_decorator_func(const char *dec, char *out, size_t outsz) {
|
||||
return;
|
||||
}
|
||||
const char *start = dec;
|
||||
if (*start == '@') {
|
||||
while (*start == '@' || *start == '#' || *start == '[' || *start == ' ') {
|
||||
start++;
|
||||
}
|
||||
const char *paren = strchr(start, '(');
|
||||
size_t len = paren ? (size_t)(paren - start) : strlen(start);
|
||||
size_t len = 0;
|
||||
while (start[len] && start[len] != '(' && start[len] != '[' && start[len] != ']' &&
|
||||
start[len] != ' ' && start[len] != ',') {
|
||||
len++;
|
||||
}
|
||||
if (len == 0 || len >= outsz) {
|
||||
return;
|
||||
}
|
||||
@@ -886,38 +835,17 @@ static void extract_worker(int worker_id, void *ctx_ptr) {
|
||||
|
||||
uint64_t file_t0 = extract_now_ns();
|
||||
|
||||
/* ObjectScript Studio Export XML: transcode each <Class> to UDL and
|
||||
* extract directly into the local gbuf (the per-file cache holds a single
|
||||
* result, so multi-class Export files are processed inline here). */
|
||||
if (fi->language == CBM_LANG_OBJECTSCRIPT_EXPORT) {
|
||||
CBMArena ea;
|
||||
cbm_arena_init(&ea);
|
||||
int cc = 0;
|
||||
char **udls = cbm_iris_export_to_udl(&ea, source, source_len, &cc);
|
||||
for (int ci = 0; ci < cc; ci++) {
|
||||
CBMFileResult *xr =
|
||||
cbm_extract_file_ex(udls[ci], (int)strlen(udls[ci]), CBM_LANG_OBJECTSCRIPT_UDL,
|
||||
ec->project_name, fi->rel_path, CBM_EXTRACT_BUDGET, NULL,
|
||||
NULL, ec->macro_table, ec->return_type_table);
|
||||
if (!xr) {
|
||||
continue;
|
||||
}
|
||||
for (int d = 0; d < xr->defs.count; d++) {
|
||||
CBMDefinition *def = &xr->defs.items[d];
|
||||
if (def->qualified_name && def->name) {
|
||||
insert_def_into_gbuf(ws, fi, def);
|
||||
}
|
||||
}
|
||||
cbm_free_result(xr);
|
||||
}
|
||||
cbm_arena_destroy(&ea);
|
||||
free_source(source);
|
||||
continue;
|
||||
}
|
||||
|
||||
CBMFileResult *result = cbm_extract_file_ex(
|
||||
source, source_len, fi->language, ec->project_name, fi->rel_path, CBM_EXTRACT_BUDGET,
|
||||
NULL, NULL, ec->macro_table, ec->return_type_table);
|
||||
/* Export XML uses the same cache slot as every physical file, so its
|
||||
* generated classes are composed before entering the common registry
|
||||
* and resolution lifecycle. */
|
||||
CBMFileResult *result =
|
||||
fi->language == CBM_LANG_OBJECTSCRIPT_EXPORT
|
||||
? cbm_pipeline_extract_objectscript_export(source, source_len, ec->project_name,
|
||||
fi->rel_path, ec->macro_table,
|
||||
ec->return_type_table)
|
||||
: cbm_extract_file_ex(source, source_len, fi->language, ec->project_name,
|
||||
fi->rel_path, CBM_EXTRACT_BUDGET, NULL, NULL, ec->macro_table,
|
||||
ec->return_type_table);
|
||||
|
||||
uint64_t file_elapsed_ms = (extract_now_ns() - file_t0) / PP_USEC_PER_MS;
|
||||
|
||||
@@ -1395,6 +1323,7 @@ int cbm_build_registry_from_cache(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
|
||||
|
||||
imports_edges += create_imports_edges(ctx, result, rel, namespace_map);
|
||||
create_channel_edges(ctx, result, rel);
|
||||
cbm_pipeline_create_env_configures_for_file(ctx, result, rel);
|
||||
}
|
||||
|
||||
cbm_pipeline_namespace_map_free(namespace_map);
|
||||
@@ -2182,46 +2111,205 @@ static void lsp_idx_free_key(const char *key, void *value, void *ud) {
|
||||
free((char *)key);
|
||||
}
|
||||
|
||||
/* A hash key may have multiple semantic rows. Keep a permanent tombstone for
|
||||
* distinct targets so O(1) lookup cannot turn ambiguity into a confidence
|
||||
* contest; duplicate rows for the same target may still keep the best score. */
|
||||
static CBMResolvedCall *const LSP_IDX_AMBIGUOUS = (CBMResolvedCall *)(uintptr_t)1;
|
||||
|
||||
static bool lsp_idx_keep_best(CBMHashTable *index, const char *key, CBMResolvedCall *candidate,
|
||||
const cbm_gbuf_t *gbuf, const char *project_name,
|
||||
bool allow_tail_match) {
|
||||
if (!index || !key || !candidate) {
|
||||
return false;
|
||||
}
|
||||
CBMResolvedCall *existing = (CBMResolvedCall *)cbm_ht_get(index, key);
|
||||
if (!existing) {
|
||||
char *owned_key = strdup(key);
|
||||
if (owned_key) {
|
||||
cbm_ht_set(index, owned_key, candidate);
|
||||
if (cbm_ht_get(index, key) == candidate) {
|
||||
return true;
|
||||
}
|
||||
free(owned_key);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (existing == LSP_IDX_AMBIGUOUS) {
|
||||
return true;
|
||||
}
|
||||
if (!cbm_pipeline_invocation_targets_equal(existing->callee_qn, candidate->callee_qn, gbuf,
|
||||
project_name, allow_tail_match)) {
|
||||
const char *stored_key = cbm_ht_get_key(index, key);
|
||||
if (stored_key) {
|
||||
cbm_ht_set(index, stored_key, LSP_IDX_AMBIGUOUS);
|
||||
}
|
||||
return cbm_ht_get(index, key) != NULL;
|
||||
}
|
||||
if (candidate->confidence > existing->confidence) {
|
||||
const char *stored_key = cbm_ht_get_key(index, key);
|
||||
if (stored_key) {
|
||||
cbm_ht_set(index, stored_key, candidate);
|
||||
}
|
||||
}
|
||||
return cbm_ht_get(index, key) != NULL;
|
||||
}
|
||||
|
||||
/* Caller QNs are user-controlled graph identifiers and can legitimately exceed
|
||||
* the old 1 KiB stack buffer. Build occurrence keys at their exact length so an
|
||||
* exact semantic row can never disappear merely because its caller is long. */
|
||||
static char *lsp_idx_key(const char *caller_qn, const char *leaf, bool exact_site,
|
||||
uint32_t site_start_byte, uint32_t site_end_byte,
|
||||
CBMSourceOrigin source_origin) {
|
||||
if (!caller_qn || !leaf) {
|
||||
return NULL;
|
||||
}
|
||||
size_t caller_len = strlen(caller_qn);
|
||||
size_t leaf_len = strlen(leaf);
|
||||
size_t suffix_len = exact_site ? 48U : 16U;
|
||||
if (caller_len > SIZE_MAX - leaf_len - suffix_len - 2U) {
|
||||
return NULL;
|
||||
}
|
||||
size_t cap = caller_len + 1U + leaf_len + suffix_len + 1U;
|
||||
char *key = (char *)malloc(cap);
|
||||
if (!key) {
|
||||
return NULL;
|
||||
}
|
||||
int written = exact_site
|
||||
? snprintf(key, cap, "%s|%s|%u:%u|%u", caller_qn, leaf, site_start_byte,
|
||||
site_end_byte, (unsigned)source_origin)
|
||||
: snprintf(key, cap, "%s|%s|%u", caller_qn, leaf, (unsigned)source_origin);
|
||||
if (written <= 0 || (size_t)written >= cap) {
|
||||
free(key);
|
||||
return NULL;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
static bool lsp_idx_insert_leaf(CBMHashTable *index, CBMResolvedCall *candidate, const char *leaf,
|
||||
bool exact_site, const cbm_gbuf_t *gbuf, const char *project_name,
|
||||
bool allow_tail_match) {
|
||||
if (!index || !candidate || !candidate->caller_qn || !leaf || !leaf[0]) {
|
||||
return false;
|
||||
}
|
||||
char *key = lsp_idx_key(candidate->caller_qn, leaf, exact_site, candidate->site_start_byte,
|
||||
candidate->site_end_byte, candidate->source_origin);
|
||||
if (!key) {
|
||||
return false;
|
||||
}
|
||||
bool inserted = lsp_idx_keep_best(index, key, candidate, gbuf, project_name, allow_tail_match);
|
||||
free(key);
|
||||
return inserted;
|
||||
}
|
||||
|
||||
static const CBMResolvedCall *lsp_idx_lookup(const CBMHashTable *index, const CBMCall *call,
|
||||
bool exact_site, bool *key_built, bool *ambiguous) {
|
||||
if (key_built) {
|
||||
*key_built = false;
|
||||
}
|
||||
if (ambiguous) {
|
||||
*ambiguous = false;
|
||||
}
|
||||
if (!index || !call || !call->enclosing_func_qn || !call->callee_name) {
|
||||
return NULL;
|
||||
}
|
||||
const char *leaf = cbm_pipeline_call_callee_leaf(call->callee_name);
|
||||
if (!leaf || !leaf[0]) {
|
||||
return NULL;
|
||||
}
|
||||
char *key = lsp_idx_key(call->enclosing_func_qn, leaf, exact_site, call->site_start_byte,
|
||||
call->site_end_byte, call->source_origin);
|
||||
if (!key) {
|
||||
return NULL;
|
||||
}
|
||||
if (key_built) {
|
||||
*key_built = true;
|
||||
}
|
||||
const CBMResolvedCall *candidate = (const CBMResolvedCall *)cbm_ht_get(index, key);
|
||||
free(key);
|
||||
if (candidate == LSP_IDX_AMBIGUOUS) {
|
||||
if (ambiguous) {
|
||||
*ambiguous = true;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
if (!candidate || candidate->kind != CBM_RESOLVED_INVOCATION || !candidate->caller_qn ||
|
||||
strcmp(candidate->caller_qn, call->enclosing_func_qn) != 0 ||
|
||||
candidate->confidence < CBM_LSP_CONFIDENCE_FLOOR) {
|
||||
return NULL;
|
||||
}
|
||||
int site_rank = cbm_pipeline_invocation_site_rank(candidate, call);
|
||||
int expected_rank = exact_site ? 2 : 1;
|
||||
return site_rank == expected_rank &&
|
||||
cbm_pipeline_invocation_leaf_matches(candidate, call, site_rank)
|
||||
? candidate
|
||||
: NULL;
|
||||
}
|
||||
|
||||
/* Resolve calls for one file and emit CALLS/HTTP_CALLS/ASYNC_CALLS edges. */
|
||||
static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CBMFileResult *result,
|
||||
const char *rel, const char *module_qn, const char **imp_keys,
|
||||
const char **imp_vals, int imp_count, CBMLanguage lang) {
|
||||
/* Build a per-file hash index of resolved_calls keyed by
|
||||
* "caller_qn|callee_short" for O(1) lookup. cbm_pipeline_find_lsp_
|
||||
* resolution would otherwise do an O(N) linear scan over
|
||||
* resolved_calls for EACH of result->calls.count calls — the
|
||||
* dominant cost in parallel_resolve on kubernetes (~50s of pure
|
||||
* scanning). On insert, keep the highest-confidence entry per key
|
||||
* (matches the original "best" tie-break). Skip the build entirely
|
||||
* when there are no calls (nothing to look up) or no resolved
|
||||
* entries (lookups would all miss). */
|
||||
CBMHashTable *lsp_idx = NULL;
|
||||
/* Two occurrence-aware indexes preserve the authoritative matcher's
|
||||
* primary ordering without restoring its O(calls × resolutions) scan:
|
||||
* exact caller+leaf+span first, then the legacy caller+leaf fallback.
|
||||
* Repeated same-leaf calls therefore remain O(1), and a high-confidence
|
||||
* 0:0 record can never hide an exact source occurrence. */
|
||||
CBMHashTable *lsp_exact_idx = NULL;
|
||||
CBMHashTable *lsp_legacy_idx = NULL;
|
||||
bool lsp_exact_idx_complete = true;
|
||||
bool lsp_legacy_idx_complete = true;
|
||||
bool allow_tail = cbm_pipeline_lsp_allow_tail_match(lang);
|
||||
if (result->calls.count > 0 && result->resolved_calls.count > 0) {
|
||||
lsp_idx = cbm_ht_create((uint32_t)result->resolved_calls.count * 2u + 16u);
|
||||
if (lsp_idx) {
|
||||
uint32_t capacity = (uint32_t)result->resolved_calls.count * 2u + 16u;
|
||||
lsp_exact_idx = cbm_ht_create(capacity);
|
||||
lsp_legacy_idx = cbm_ht_create(capacity);
|
||||
if (!lsp_exact_idx) {
|
||||
lsp_exact_idx_complete = false;
|
||||
}
|
||||
if (!lsp_legacy_idx) {
|
||||
lsp_legacy_idx_complete = false;
|
||||
}
|
||||
if (lsp_exact_idx || lsp_legacy_idx) {
|
||||
for (int i = 0; i < result->resolved_calls.count; i++) {
|
||||
CBMResolvedCall *rc_e = &result->resolved_calls.items[i];
|
||||
if (!rc_e->caller_qn || !rc_e->callee_qn ||
|
||||
if (rc_e->kind != CBM_RESOLVED_INVOCATION || !rc_e->caller_qn || !rc_e->callee_qn ||
|
||||
rc_e->confidence < CBM_LSP_CONFIDENCE_FLOOR) {
|
||||
continue;
|
||||
}
|
||||
const char *short_name = strrchr(rc_e->callee_qn, '.');
|
||||
short_name = short_name ? short_name + 1 : rc_e->callee_qn;
|
||||
char key[1024];
|
||||
int kn = snprintf(key, sizeof(key), "%s|%s", rc_e->caller_qn, short_name);
|
||||
if (kn <= 0 || kn >= (int)sizeof(key))
|
||||
bool exact_site =
|
||||
cbm_pipeline_source_site_present(rc_e->site_start_byte, rc_e->site_end_byte);
|
||||
bool legacy_site =
|
||||
cbm_pipeline_source_site_legacy(rc_e->site_start_byte, rc_e->site_end_byte);
|
||||
if (!exact_site && !legacy_site) {
|
||||
continue;
|
||||
CBMResolvedCall *existing = (CBMResolvedCall *)cbm_ht_get(lsp_idx, key);
|
||||
if (!existing) {
|
||||
/* New entry — strdup so the key outlives the loop body. */
|
||||
char *kdup = strdup(key);
|
||||
if (kdup)
|
||||
cbm_ht_set(lsp_idx, kdup, rc_e);
|
||||
} else if (rc_e->confidence > existing->confidence) {
|
||||
/* Update value; reuse stored key pointer to avoid leak. */
|
||||
const char *skey = cbm_ht_get_key(lsp_idx, key);
|
||||
if (skey)
|
||||
cbm_ht_set(lsp_idx, skey, rc_e);
|
||||
}
|
||||
CBMHashTable *index = exact_site ? lsp_exact_idx : lsp_legacy_idx;
|
||||
bool inserted =
|
||||
lsp_idx_insert_leaf(index, rc_e, cbm_lsp_bare_segment(rc_e->callee_qn),
|
||||
exact_site, rc->main_gbuf, rc->project_name, allow_tail);
|
||||
if (!inserted) {
|
||||
if (exact_site) {
|
||||
lsp_exact_idx_complete = false;
|
||||
} else {
|
||||
lsp_legacy_idx_complete = false;
|
||||
}
|
||||
}
|
||||
if (rc_e->reason && cbm_pipeline_invocation_reason_join_strategy(rc_e->strategy)) {
|
||||
inserted = lsp_idx_insert_leaf(index, rc_e, cbm_lsp_bare_segment(rc_e->reason),
|
||||
exact_site, rc->main_gbuf, rc->project_name,
|
||||
allow_tail);
|
||||
if (!inserted) {
|
||||
if (exact_site) {
|
||||
lsp_exact_idx_complete = false;
|
||||
} else {
|
||||
lsp_legacy_idx_complete = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (exact_site && rc_e->strategy && strcmp(rc_e->strategy, "lsp_destructor") == 0 &&
|
||||
!lsp_idx_insert_leaf(index, rc_e, "~", true, rc->main_gbuf, rc->project_name,
|
||||
allow_tail)) {
|
||||
lsp_exact_idx_complete = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2247,25 +2335,37 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
|
||||
* LSP overrides so a project doesn't get different attributions
|
||||
* depending on whether parallel mode kicked in. Unique-tail
|
||||
* fallbacks are JVM-only (see cbm_pipeline_lsp_allow_tail_match). */
|
||||
bool allow_tail = cbm_pipeline_lsp_allow_tail_match(lang);
|
||||
cbm_resolution_t res = {0};
|
||||
const CBMResolvedCall *lsp = NULL;
|
||||
bool exact_key_built = true;
|
||||
bool exact_key_ambiguous = false;
|
||||
bool legacy_key_built = true;
|
||||
_rc_t0 = extract_now_ns();
|
||||
if (lsp_idx && call->enclosing_func_qn) {
|
||||
const char *call_leaf = cbm_pipeline_call_callee_leaf(call->callee_name);
|
||||
char key[1024];
|
||||
int kn = call_leaf
|
||||
? snprintf(key, sizeof(key), "%s|%s", call->enclosing_func_qn, call_leaf)
|
||||
: -1;
|
||||
if (kn > 0 && kn < (int)sizeof(key)) {
|
||||
lsp = (const CBMResolvedCall *)cbm_ht_get(lsp_idx, key);
|
||||
}
|
||||
if (cbm_pipeline_source_site_present(call->site_start_byte, call->site_end_byte)) {
|
||||
lsp = lsp_idx_lookup(lsp_exact_idx, call, true, &exact_key_built, &exact_key_ambiguous);
|
||||
}
|
||||
if (!lsp) {
|
||||
bool exact_index_authoritative = lsp_exact_idx_complete && exact_key_built;
|
||||
if (!lsp && !exact_index_authoritative) {
|
||||
/* An incomplete exact index must fail closed: consult the
|
||||
* authoritative matcher before a legacy row can win. */
|
||||
atomic_fetch_add_explicit(&g_lsp_linear_fallback_rows,
|
||||
(uint64_t)result->resolved_calls.count, memory_order_relaxed);
|
||||
lsp = cbm_pipeline_find_lsp_resolution_in_graph(
|
||||
&result->resolved_calls, call, allow_tail, rc->main_gbuf, rc->project_name);
|
||||
}
|
||||
if (!lsp && !call->requires_lsp_resolution && exact_index_authoritative &&
|
||||
!exact_key_ambiguous) {
|
||||
lsp = lsp_idx_lookup(lsp_legacy_idx, call, false, &legacy_key_built, NULL);
|
||||
}
|
||||
if (!lsp && (!lsp_legacy_idx_complete || !legacy_key_built || allow_tail ||
|
||||
call->requires_lsp_resolution)) {
|
||||
/* Fallback to the linear scan for edge cases the index may
|
||||
* miss (e.g. callee_name that wasn't the registered short
|
||||
* name). Keeps semantics identical. */
|
||||
lsp = cbm_pipeline_find_lsp_resolution(&result->resolved_calls, call, allow_tail);
|
||||
atomic_fetch_add_explicit(&g_lsp_linear_fallback_rows,
|
||||
(uint64_t)result->resolved_calls.count, memory_order_relaxed);
|
||||
lsp = cbm_pipeline_find_lsp_resolution_in_graph(
|
||||
&result->resolved_calls, call, allow_tail, rc->main_gbuf, rc->project_name);
|
||||
}
|
||||
atomic_fetch_add_explicit(&rc->time_ns_rc_lsp_lookup, extract_now_ns() - _rc_t0,
|
||||
memory_order_relaxed);
|
||||
@@ -2275,8 +2375,13 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
|
||||
/* Canonicalise to the gbuf node's QN so res.qualified_name matches
|
||||
* the gbuf even when the cross-file fallback had to prefix the
|
||||
* project name. */
|
||||
lsp_target = cbm_pipeline_lsp_target_node(rc->main_gbuf, rc->project_name,
|
||||
lsp->callee_qn, allow_tail);
|
||||
bool exact_external_target = call->requires_lsp_resolution &&
|
||||
cbm_pipeline_kotlin_external_target(lang, lsp->callee_qn);
|
||||
lsp_target = exact_external_target
|
||||
? cbm_pipeline_lsp_target_node_strict(rc->main_gbuf, rc->project_name,
|
||||
lsp->callee_qn, allow_tail)
|
||||
: cbm_pipeline_lsp_target_node(rc->main_gbuf, rc->project_name,
|
||||
lsp->callee_qn, allow_tail);
|
||||
if (lsp_target) {
|
||||
res.qualified_name = lsp_target->qualified_name;
|
||||
res.strategy = lsp->strategy ? lsp->strategy : "lsp_override";
|
||||
@@ -2295,14 +2400,24 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
|
||||
* losing every alias-imported JSX component edge on the parallel path
|
||||
* (~21% of a Next.js call graph) while the sequential pass, which falls
|
||||
* THROUGH to the registry here, kept them. This restores seq/parallel
|
||||
* parity via the import_map / unique_name resolution. */
|
||||
if (!res.qualified_name || !res.qualified_name[0]) {
|
||||
* parity via the import_map / unique_name resolution. Synthetic
|
||||
* semantic candidates are deliberately excluded: they require an
|
||||
* exact LSP target and must fail closed rather than accepting a textual
|
||||
* registry match. */
|
||||
if ((!res.qualified_name || !res.qualified_name[0]) && !call->requires_lsp_resolution) {
|
||||
res = cbm_registry_resolve(rc->registry, call->callee_name, module_qn, imp_keys,
|
||||
imp_vals, imp_count);
|
||||
}
|
||||
atomic_fetch_add_explicit(&rc->time_ns_rc_resolve, extract_now_ns() - _rc_t0,
|
||||
memory_order_relaxed);
|
||||
|
||||
/* A synthetic semantic candidate is an invocation only when the LSP
|
||||
* resolved it to a concrete graph node. Never let registry, field-name,
|
||||
* route, or service heuristics manufacture a target for it. */
|
||||
if (call->requires_lsp_resolution && !lsp_target) {
|
||||
continue;
|
||||
}
|
||||
|
||||
_rc_t0 = extract_now_ns();
|
||||
try_field_type_hint(rc, &res, call->callee_name, source_node->id);
|
||||
atomic_fetch_add_explicit(&rc->time_ns_rc_hint, extract_now_ns() - _rc_t0,
|
||||
@@ -2428,16 +2543,24 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
|
||||
memory_order_relaxed);
|
||||
ws->calls_resolved++;
|
||||
}
|
||||
if (lsp_idx) {
|
||||
cbm_ht_foreach(lsp_idx, lsp_idx_free_key, NULL);
|
||||
cbm_ht_free(lsp_idx);
|
||||
if (lsp_exact_idx) {
|
||||
cbm_ht_foreach(lsp_exact_idx, lsp_idx_free_key, NULL);
|
||||
cbm_ht_free(lsp_exact_idx);
|
||||
}
|
||||
if (lsp_legacy_idx) {
|
||||
cbm_ht_foreach(lsp_legacy_idx, lsp_idx_free_key, NULL);
|
||||
cbm_ht_free(lsp_legacy_idx);
|
||||
}
|
||||
}
|
||||
|
||||
/* Resolve usages for one file. */
|
||||
static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
|
||||
CBMFileResult *result, const char *rel, const char *module_qn,
|
||||
const char **imp_keys, const char **imp_vals, int imp_count) {
|
||||
const char **imp_keys, const char **imp_vals, int imp_count,
|
||||
CBMLanguage lang) {
|
||||
cbm_pipeline_lsp_reference_index_t reference_index = {0};
|
||||
bool reference_index_ready =
|
||||
cbm_pipeline_lsp_reference_index_build(&result->resolved_calls, &reference_index);
|
||||
for (int u = 0; u < result->usages.count; u++) {
|
||||
CBMUsage *usage = &result->usages.items[u];
|
||||
if (!usage->ref_name) {
|
||||
@@ -2448,12 +2571,45 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
|
||||
if (!src) {
|
||||
continue;
|
||||
}
|
||||
cbm_resolution_t res = cbm_registry_resolve(rc->registry, usage->ref_name, module_qn,
|
||||
imp_keys, imp_vals, imp_count);
|
||||
if (!res.qualified_name || res.qualified_name[0] == '\0') {
|
||||
continue;
|
||||
const cbm_gbuf_node_t *tgt = NULL;
|
||||
bool precise_call_reference = false;
|
||||
const CBMResolvedCall *semantic_reference = NULL;
|
||||
if (cbm_pipeline_usage_semantic_reference_candidate(usage)) {
|
||||
bool allow_tail = cbm_pipeline_lsp_allow_tail_match(lang);
|
||||
semantic_reference = cbm_pipeline_find_lsp_reference_indexed_in_graph(
|
||||
&result->resolved_calls, reference_index_ready ? &reference_index : NULL, usage,
|
||||
allow_tail, rc->main_gbuf, rc->project_name);
|
||||
if (semantic_reference &&
|
||||
!cbm_pipeline_usage_allows_semantic_reference(usage, semantic_reference)) {
|
||||
semantic_reference = NULL;
|
||||
}
|
||||
if (semantic_reference) {
|
||||
tgt = cbm_pipeline_lsp_target_node(rc->main_gbuf, rc->project_name,
|
||||
semantic_reference->callee_qn, allow_tail);
|
||||
precise_call_reference = cbm_pipeline_node_is_callable_target(tgt);
|
||||
}
|
||||
}
|
||||
if (!tgt) {
|
||||
/* Exact semantic ownership beats textual name fallback even when
|
||||
* the semantic target is not materialized in this graph. */
|
||||
if (semantic_reference) {
|
||||
continue;
|
||||
}
|
||||
cbm_resolution_t res = cbm_registry_resolve(rc->registry, usage->ref_name, module_qn,
|
||||
imp_keys, imp_vals, imp_count);
|
||||
if (!res.qualified_name || res.qualified_name[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
if (!cbm_pipeline_reference_candidate_fallback_allowed(lang, usage, res.strategy,
|
||||
imp_keys, imp_count)) {
|
||||
continue;
|
||||
}
|
||||
tgt = cbm_gbuf_find_by_qn(rc->main_gbuf, res.qualified_name);
|
||||
if (usage->semantic_reference_blocked && (usage->semantic_reference_local_shadow ||
|
||||
cbm_pipeline_node_is_callable_target(tgt))) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const cbm_gbuf_node_t *tgt = cbm_gbuf_find_by_qn(rc->main_gbuf, res.qualified_name);
|
||||
if (!tgt || src->id == tgt->id) {
|
||||
continue;
|
||||
}
|
||||
@@ -2461,9 +2617,11 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
|
||||
char esc_ref[CBM_SZ_256]; /* sliced source text: escape quotes/newlines */
|
||||
cbm_json_escape(esc_ref, sizeof(esc_ref), usage->ref_name);
|
||||
snprintf(uprops, sizeof(uprops), "{\"callee\":\"%s\"}", esc_ref);
|
||||
cbm_gbuf_insert_edge(ws->local_edge_buf, src->id, tgt->id, "USAGE", uprops);
|
||||
const char *edge_type = precise_call_reference ? "CALL_REFERENCE" : "USAGE";
|
||||
cbm_gbuf_insert_edge(ws->local_edge_buf, src->id, tgt->id, edge_type, uprops);
|
||||
ws->usages_resolved++;
|
||||
}
|
||||
cbm_pipeline_lsp_reference_index_free(&reference_index);
|
||||
}
|
||||
|
||||
/* Resolve throws/raises for one file. */
|
||||
@@ -2572,6 +2730,14 @@ static void resolve_def_decorators(resolve_ctx_t *rc, resolve_worker_state_t *ws
|
||||
if (res.qualified_name && res.qualified_name[0] != '\0') {
|
||||
dn = cbm_gbuf_find_by_qn(rc->main_gbuf, res.qualified_name);
|
||||
}
|
||||
/* Qualified Rust proc-macro paths must not degrade to the decorated
|
||||
* function itself through the registry's same-module suffix fallback
|
||||
* (`#[tokio::main]` on local `main`). Preserve the full external path
|
||||
* through the synthetic Decorator node instead. */
|
||||
if (dn && dn->id == node->id && def->decorators[dc][0] == '#' &&
|
||||
def->decorators[dc][1] == '[' && strstr(fn, "::")) {
|
||||
dn = NULL;
|
||||
}
|
||||
int64_t dn_id = 0;
|
||||
if (dn) {
|
||||
dn_id = dn->id;
|
||||
@@ -2678,6 +2844,55 @@ static CBMTypeRegistry *pp_rust_shared_registry_get(void *ctx) {
|
||||
return pp_rust_shared_registry((resolve_ctx_t *)ctx);
|
||||
}
|
||||
|
||||
static int pp_call_reference_site_count(const CBMFileResult *result, CBMLanguage lang) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < result->usages.count; i++) {
|
||||
if (cbm_pipeline_usage_semantic_reference_candidate(&result->usages.items[i])) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static int pp_qualified_lsp_site_count(const CBMFileResult *result) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < result->resolved_calls.count; i++) {
|
||||
const CBMResolvedCall *resolved = &result->resolved_calls.items[i];
|
||||
if ((resolved->kind == CBM_RESOLVED_INVOCATION ||
|
||||
resolved->kind == CBM_RESOLVED_CALL_REFERENCE) &&
|
||||
resolved->caller_qn && resolved->callee_qn &&
|
||||
resolved->confidence >= CBM_LSP_CONFIDENCE_FLOOR) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/* A per-file semantic walk can discover a faithful source occurrence that the
|
||||
* syntax extractor cannot represent as a CBMCall yet: Python operators and
|
||||
* calls hidden in Rust built-in macro token trees are the canonical examples.
|
||||
* An exact unresolved/low-confidence record is therefore an explicit request
|
||||
* for the project-registry pass, even when the parser-backed site count is
|
||||
* zero or already fully qualified. Zero-span generated semantics are excluded:
|
||||
* without source provenance they cannot safely join a graph carrier. */
|
||||
static bool pp_has_pending_lsp_site(const CBMFileResult *result) {
|
||||
for (int i = 0; i < result->resolved_calls.count; i++) {
|
||||
const CBMResolvedCall *resolved = &result->resolved_calls.items[i];
|
||||
if (resolved->kind != CBM_RESOLVED_INVOCATION &&
|
||||
resolved->kind != CBM_RESOLVED_CALL_REFERENCE) {
|
||||
continue;
|
||||
}
|
||||
if (!resolved->caller_qn || !resolved->callee_qn ||
|
||||
resolved->site_end_byte <= resolved->site_start_byte) {
|
||||
continue;
|
||||
}
|
||||
if (resolved->reason || resolved->confidence < CBM_LSP_CONFIDENCE_FLOOR) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void resolve_worker(int worker_id, void *ctx_ptr) {
|
||||
resolve_ctx_t *rc = ctx_ptr;
|
||||
resolve_worker_state_t *ws = &rc->workers[worker_id];
|
||||
@@ -2737,19 +2952,23 @@ static void resolve_worker(int worker_id, void *ctx_ptr) {
|
||||
}
|
||||
|
||||
/* Cross-file LSP is a per-file tree-sitter re-parse + AST walk +
|
||||
* registry lookups — ~50-150ms per file. It can ONLY find calls
|
||||
* that exist in the AST. If the per-file extract found zero calls,
|
||||
* cross-LSP will too: the AST is the same. For non-JVM languages,
|
||||
* skip when per-file LSP already produced at least as many resolved
|
||||
* entries as textual calls. Java/Kotlin per-file LSP can fill the
|
||||
* count with constructors or same-file calls while a mixed-source-root
|
||||
* Java↔Kotlin call remains unresolved, so JVM callers run whenever
|
||||
* calls exist. */
|
||||
* registry lookups — ~50-150ms per file. It can only resolve semantic
|
||||
* sites present in that AST: invocations plus explicit callable
|
||||
* references. A file containing only a callable reference still needs
|
||||
* this pass. For non-JVM languages, skip when per-file LSP already
|
||||
* produced at least as many qualifying typed resolutions as sites.
|
||||
* Java/Kotlin per-file LSP can fill the count with same-file sites while
|
||||
* a mixed-source-root Java↔Kotlin site remains unresolved, so JVM
|
||||
* callers run whenever either kind of site exists. */
|
||||
bool jvm_cross_lsp = (lang == CBM_LANG_JAVA || lang == CBM_LANG_KOTLIN);
|
||||
int call_reference_sites = pp_call_reference_site_count(result, lang);
|
||||
int semantic_sites = result->calls.count + call_reference_sites;
|
||||
int qualified_lsp_sites = pp_qualified_lsp_site_count(result);
|
||||
bool pending_lsp_site = pp_has_pending_lsp_site(result);
|
||||
bool cross_lsp_eligible =
|
||||
(rc->all_defs && rc->def_count > 0 && cbm_pxc_has_cross_lsp(lang) &&
|
||||
result->calls.count > 0 &&
|
||||
(jvm_cross_lsp || result->resolved_calls.count < result->calls.count) &&
|
||||
(semantic_sites > 0 || pending_lsp_site) &&
|
||||
(jvm_cross_lsp || pending_lsp_site || qualified_lsp_sites < semantic_sites) &&
|
||||
!is_generated);
|
||||
|
||||
/* Skip files with nothing else to resolve and no cross-LSP work. */
|
||||
@@ -2766,7 +2985,8 @@ static void resolve_worker(int worker_id, void *ctx_ptr) {
|
||||
const char **imp_vals = NULL;
|
||||
int imp_count = 0;
|
||||
uint64_t _imp_t0 = extract_now_ns();
|
||||
build_import_map(rc->main_gbuf, rc->project_name, rel, &imp_keys, &imp_vals, &imp_count);
|
||||
cbm_pxc_build_import_map(rc->main_gbuf, rc->project_name, rel, lang, result, &imp_keys,
|
||||
&imp_vals, &imp_count);
|
||||
atomic_fetch_add_explicit(&rc->time_ns_import_map, extract_now_ns() - _imp_t0,
|
||||
memory_order_relaxed);
|
||||
|
||||
@@ -2882,7 +3102,7 @@ static void resolve_worker(int worker_id, void *ctx_ptr) {
|
||||
|
||||
/* ── USAGE resolution ──────────────────────────────────── */
|
||||
_ph_t0 = extract_now_ns();
|
||||
resolve_file_usages(rc, ws, result, rel, module_qn, imp_keys, imp_vals, imp_count);
|
||||
resolve_file_usages(rc, ws, result, rel, module_qn, imp_keys, imp_vals, imp_count, lang);
|
||||
atomic_fetch_add_explicit(&rc->time_ns_usages, extract_now_ns() - _ph_t0,
|
||||
memory_order_relaxed);
|
||||
|
||||
@@ -2908,7 +3128,7 @@ static void resolve_worker(int worker_id, void *ctx_ptr) {
|
||||
cbm_registry_resolve_cache_end();
|
||||
|
||||
free(module_qn);
|
||||
free_import_map(imp_keys, imp_vals, imp_count);
|
||||
cbm_pxc_free_import_map(imp_keys, imp_vals, imp_count);
|
||||
|
||||
atomic_fetch_add_explicit(&rc->time_ns_total_loop, extract_now_ns() - _loop_t0,
|
||||
memory_order_relaxed);
|
||||
|
||||
@@ -400,6 +400,15 @@ static void resolve_decorator(cbm_pipeline_ctx_t *ctx, const cbm_gbuf_node_t *no
|
||||
if (res.qualified_name && res.qualified_name[0] != '\0') {
|
||||
dec = cbm_gbuf_find_by_qn(ctx->gbuf, res.qualified_name);
|
||||
}
|
||||
/* A qualified Rust proc-macro path can collide with the decorated
|
||||
* function's own name: resolving `#[tokio::main]` from module `main`
|
||||
* may fall back from `tokio::main` to same-module `main`. That is not a
|
||||
* local decorator target (and the self-edge is discarded below), so keep
|
||||
* the full external spelling by materialising the synthetic decorator. */
|
||||
if (dec && dec->id == node->id && decorator[0] == '#' && decorator[1] == '[' &&
|
||||
strstr(func_name, "::")) {
|
||||
dec = NULL;
|
||||
}
|
||||
if (!dec) {
|
||||
/* The decorator target is not a local symbol (external attribute /
|
||||
* stdlib annotation / proc-macro derive). Materialise a synthetic
|
||||
|
||||
+83
-109
@@ -2,7 +2,8 @@
|
||||
* pass_usages.c — Resolve usages, throws, and read/write edges.
|
||||
*
|
||||
* For each file, re-extracts and resolves:
|
||||
* - USAGE edges: identifier references (not calls) to registered symbols
|
||||
* - USAGE edges: ordinary identifier references to registered symbols
|
||||
* - CALL_REFERENCE edges: explicit callable-reference syntax (not invocations)
|
||||
* - THROWS/RAISES edges: exception types
|
||||
* - READS/WRITES edges: variable read/write access patterns
|
||||
*
|
||||
@@ -15,6 +16,8 @@
|
||||
#include "foundation/str_util.h" // cbm_json_escape
|
||||
#include "pipeline/pipeline.h"
|
||||
#include "pipeline/pipeline_internal.h"
|
||||
#include "pipeline/lsp_resolve.h"
|
||||
#include "pipeline/pass_lsp_cross.h"
|
||||
#include "graph_buffer/graph_buffer.h"
|
||||
#include "foundation/log.h"
|
||||
#include "foundation/compat.h"
|
||||
@@ -26,6 +29,24 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#if defined(CBM_CALL_REFERENCE_LOOKUP_TEST_API) && CBM_CALL_REFERENCE_LOOKUP_TEST_API
|
||||
#include <stdatomic.h>
|
||||
|
||||
static _Atomic uint64_t g_lsp_reference_lookup_rows_examined = 0;
|
||||
|
||||
void cbm_pipeline_lsp_reference_lookup_test_note_row(void) {
|
||||
atomic_fetch_add_explicit(&g_lsp_reference_lookup_rows_examined, 1, memory_order_relaxed);
|
||||
}
|
||||
|
||||
void cbm_pipeline_lsp_reference_lookup_test_reset(void) {
|
||||
atomic_store_explicit(&g_lsp_reference_lookup_rows_examined, 0, memory_order_relaxed);
|
||||
}
|
||||
|
||||
uint64_t cbm_pipeline_lsp_reference_lookup_test_rows_examined(void) {
|
||||
return atomic_load_explicit(&g_lsp_reference_lookup_rows_examined, memory_order_relaxed);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* True for languages whose module QN derives from the CONTAINING DIRECTORY
|
||||
* (Java/Go package). MUST match cbm_lang_module_is_dir() (internal/cbm/helpers.c)
|
||||
* so same-module resolution keys against the directory-based def-node QNs. */
|
||||
@@ -87,108 +108,18 @@ static bool is_checked_exception(const char *name) {
|
||||
return true; /* Default: treat as checked */
|
||||
}
|
||||
|
||||
/* Build import map from cached extraction result (fast path). */
|
||||
static int build_import_map_from_cache(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result,
|
||||
const char ***out_keys, const char ***out_vals,
|
||||
int *out_count) {
|
||||
const char **keys = calloc((size_t)result->imports.count, sizeof(const char *));
|
||||
const char **vals = calloc((size_t)result->imports.count, sizeof(const char *));
|
||||
int count = 0;
|
||||
|
||||
for (int i = 0; i < result->imports.count; i++) {
|
||||
const CBMImport *imp = &result->imports.items[i];
|
||||
if (!imp->local_name || !imp->local_name[0] || !imp->module_path) {
|
||||
continue;
|
||||
}
|
||||
char *target_qn = cbm_pipeline_fqn_module(ctx->project_name, imp->module_path);
|
||||
const cbm_gbuf_node_t *target = cbm_gbuf_find_by_qn(ctx->gbuf, target_qn);
|
||||
free(target_qn);
|
||||
if (!target) {
|
||||
continue;
|
||||
}
|
||||
keys[count] = strdup(imp->local_name);
|
||||
vals[count] = target->qualified_name;
|
||||
count++;
|
||||
}
|
||||
|
||||
*out_keys = keys;
|
||||
*out_vals = vals;
|
||||
*out_count = count;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Build import map from graph buffer IMPORTS edges (slow path). */
|
||||
static int build_import_map_from_edges(cbm_pipeline_ctx_t *ctx, const char *rel_path,
|
||||
const char ***out_keys, const char ***out_vals,
|
||||
int *out_count) {
|
||||
char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel_path, "__file__");
|
||||
const cbm_gbuf_node_t *file_node = cbm_gbuf_find_by_qn(ctx->gbuf, file_qn);
|
||||
free(file_qn);
|
||||
if (!file_node) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const cbm_gbuf_edge_t **edges = NULL;
|
||||
int edge_count = 0;
|
||||
int rc = cbm_gbuf_find_edges_by_source_type(ctx->gbuf, file_node->id, "IMPORTS", &edges,
|
||||
&edge_count);
|
||||
if (rc != 0 || edge_count == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char **keys = calloc(edge_count, sizeof(const char *));
|
||||
const char **vals = calloc(edge_count, sizeof(const char *));
|
||||
int count = 0;
|
||||
|
||||
for (int i = 0; i < edge_count; i++) {
|
||||
const cbm_gbuf_edge_t *e = edges[i];
|
||||
const cbm_gbuf_node_t *target = cbm_gbuf_find_by_id(ctx->gbuf, e->target_id);
|
||||
if (!target || !e->properties_json) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char *start = strstr(e->properties_json, "\"local_name\":\"");
|
||||
if (start) {
|
||||
start += strlen("\"local_name\":\"");
|
||||
const char *end = strchr(start, '"');
|
||||
if (end && end > start) {
|
||||
keys[count] = cbm_strndup(start, end - start);
|
||||
vals[count] = target->qualified_name;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*out_keys = keys;
|
||||
*out_vals = vals;
|
||||
*out_count = count;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Build per-file import map from cached extraction result or graph buffer edges. */
|
||||
/* Build the same semantic import map used by cross-LSP and parallel resolution.
|
||||
* The old sequential-only shortcut treated raw paths such as `./types` as QNs,
|
||||
* so duplicate symbol names fell through to an ambiguous unique-name lookup. */
|
||||
static int build_import_map(cbm_pipeline_ctx_t *ctx, const char *rel_path,
|
||||
const CBMFileResult *result, const char ***out_keys,
|
||||
const char ***out_vals, int *out_count) {
|
||||
*out_keys = NULL;
|
||||
*out_vals = NULL;
|
||||
*out_count = 0;
|
||||
|
||||
if (result && result->imports.count > 0) {
|
||||
return build_import_map_from_cache(ctx, result, out_keys, out_vals, out_count);
|
||||
}
|
||||
return build_import_map_from_edges(ctx, rel_path, out_keys, out_vals, out_count);
|
||||
const char ***out_vals, int *out_count, CBMLanguage lang) {
|
||||
return cbm_pxc_build_import_map(ctx->gbuf, ctx->project_name, rel_path, lang, result, out_keys,
|
||||
out_vals, out_count);
|
||||
}
|
||||
|
||||
static void free_import_map(const char **keys, const char **vals, int count) {
|
||||
if (keys) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
free((void *)keys[i]);
|
||||
}
|
||||
free((void *)keys);
|
||||
}
|
||||
if (vals) {
|
||||
free((void *)vals);
|
||||
}
|
||||
cbm_pxc_free_import_map(keys, vals, count);
|
||||
}
|
||||
|
||||
/* Find the graph buffer node for an enclosing function QN, falling back to file node. */
|
||||
@@ -215,26 +146,67 @@ static const cbm_gbuf_node_t *find_enclosing_node(cbm_pipeline_ctx_t *ctx, const
|
||||
/* Resolve USAGE edges for one file's extracted usages. */
|
||||
static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result,
|
||||
const char *rel, const char *module_qn, const char **imp_keys,
|
||||
const char **imp_vals, int imp_count) {
|
||||
const char **imp_vals, int imp_count, CBMLanguage lang) {
|
||||
int resolved = 0;
|
||||
cbm_pipeline_lsp_reference_index_t reference_index = {0};
|
||||
bool reference_index_ready =
|
||||
cbm_pipeline_lsp_reference_index_build(&result->resolved_calls, &reference_index);
|
||||
for (int u = 0; u < result->usages.count; u++) {
|
||||
CBMUsage *usage = &result->usages.items[u];
|
||||
if (!usage->ref_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const cbm_gbuf_node_t *src = find_enclosing_node(ctx, usage->enclosing_func_qn, rel);
|
||||
if (!src) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cbm_resolution_t res = cbm_registry_resolve(ctx->registry, usage->ref_name, module_qn,
|
||||
imp_keys, imp_vals, imp_count);
|
||||
if (!res.qualified_name || res.qualified_name[0] == '\0') {
|
||||
continue;
|
||||
const cbm_gbuf_node_t *tgt = NULL;
|
||||
bool precise_call_reference = false;
|
||||
const CBMResolvedCall *semantic_reference = NULL;
|
||||
if (cbm_pipeline_usage_semantic_reference_candidate(usage)) {
|
||||
bool allow_tail = cbm_pipeline_lsp_allow_tail_match(lang);
|
||||
semantic_reference = cbm_pipeline_find_lsp_reference_indexed_in_graph(
|
||||
&result->resolved_calls, reference_index_ready ? &reference_index : NULL, usage,
|
||||
allow_tail, ctx->gbuf, ctx->project_name);
|
||||
if (semantic_reference &&
|
||||
!cbm_pipeline_usage_allows_semantic_reference(usage, semantic_reference)) {
|
||||
semantic_reference = NULL;
|
||||
}
|
||||
if (semantic_reference) {
|
||||
tgt = cbm_pipeline_lsp_target_node(ctx->gbuf, ctx->project_name,
|
||||
semantic_reference->callee_qn, allow_tail);
|
||||
precise_call_reference = cbm_pipeline_node_is_callable_target(tgt);
|
||||
}
|
||||
}
|
||||
|
||||
const cbm_gbuf_node_t *tgt = cbm_gbuf_find_by_qn(ctx->gbuf, res.qualified_name);
|
||||
/* A syntactically explicit reference remains useful when exact semantic
|
||||
* resolution is unavailable, but the textual registry fallback proves
|
||||
* only value use—not an occurrence-exact callable target. Emit USAGE in
|
||||
* that case; CALL_REFERENCE is reserved for the exact LSP join. */
|
||||
if (!tgt) {
|
||||
/* An occurrence-exact semantic record owns this reference even when
|
||||
* its target is not materialized in the graph (for example, a
|
||||
* Kotlin local function). Falling back by raw name here would bind
|
||||
* an unrelated same-named declaration. */
|
||||
if (semantic_reference) {
|
||||
continue;
|
||||
}
|
||||
cbm_resolution_t res = cbm_registry_resolve(ctx->registry, usage->ref_name, module_qn,
|
||||
imp_keys, imp_vals, imp_count);
|
||||
if (!res.qualified_name || res.qualified_name[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
if (!cbm_pipeline_reference_candidate_fallback_allowed(lang, usage, res.strategy,
|
||||
imp_keys, imp_count)) {
|
||||
continue;
|
||||
}
|
||||
tgt = cbm_gbuf_find_by_qn(ctx->gbuf, res.qualified_name);
|
||||
if (usage->semantic_reference_blocked && (usage->semantic_reference_local_shadow ||
|
||||
cbm_pipeline_node_is_callable_target(tgt))) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!tgt || src->id == tgt->id) {
|
||||
continue;
|
||||
}
|
||||
@@ -245,9 +217,11 @@ static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res
|
||||
cbm_json_escape(esc_ref, sizeof(esc_ref), usage->ref_name);
|
||||
char uprops[CBM_SZ_512];
|
||||
snprintf(uprops, sizeof(uprops), "{\"callee\":\"%s\"}", esc_ref);
|
||||
cbm_gbuf_insert_edge(ctx->gbuf, src->id, tgt->id, "USAGE", uprops);
|
||||
const char *edge_type = precise_call_reference ? "CALL_REFERENCE" : "USAGE";
|
||||
cbm_gbuf_insert_edge(ctx->gbuf, src->id, tgt->id, edge_type, uprops);
|
||||
resolved++;
|
||||
}
|
||||
cbm_pipeline_lsp_reference_index_free(&reference_index);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
@@ -368,13 +342,13 @@ int cbm_pipeline_pass_usages(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *fil
|
||||
const char **imp_keys = NULL;
|
||||
const char **imp_vals = NULL;
|
||||
int imp_count = 0;
|
||||
build_import_map(ctx, rel, result, &imp_keys, &imp_vals, &imp_count);
|
||||
build_import_map(ctx, rel, result, &imp_keys, &imp_vals, &imp_count, files[i].language);
|
||||
|
||||
char *module_qn = cbm_pipeline_fqn_module_dir(ctx->project_name, rel,
|
||||
pu_module_is_dir(files[i].language));
|
||||
|
||||
usage_resolved +=
|
||||
resolve_usage_edges(ctx, result, rel, module_qn, imp_keys, imp_vals, imp_count);
|
||||
usage_resolved += resolve_usage_edges(ctx, result, rel, module_qn, imp_keys, imp_vals,
|
||||
imp_count, files[i].language);
|
||||
throw_resolved +=
|
||||
resolve_throw_edges(ctx, result, rel, module_qn, imp_keys, imp_vals, imp_count);
|
||||
rw_resolved += resolve_rw_edges(ctx, result, rel, module_qn, imp_keys, imp_vals, imp_count);
|
||||
|
||||
+35
-13
@@ -289,24 +289,29 @@ static cbm_path_alias_map_t *load_tsconfig_file(const char *abs_path, const char
|
||||
|
||||
/* ── Public API ────────────────────────────────────────────────── */
|
||||
|
||||
static void free_alias_map(cbm_path_alias_map_t *map) {
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
for (int j = 0; j < map->count; j++) {
|
||||
free(map->entries[j].alias_prefix);
|
||||
free(map->entries[j].alias_suffix);
|
||||
free(map->entries[j].target_prefix);
|
||||
free(map->entries[j].target_suffix);
|
||||
}
|
||||
free(map->entries);
|
||||
free(map->base_url);
|
||||
free(map);
|
||||
}
|
||||
|
||||
void cbm_path_alias_collection_free(cbm_path_alias_collection_t *coll) {
|
||||
if (!coll) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < coll->count; i++) {
|
||||
free(coll->scopes[i].dir_prefix);
|
||||
if (coll->scopes[i].map) {
|
||||
cbm_path_alias_map_t *map = coll->scopes[i].map;
|
||||
for (int j = 0; j < map->count; j++) {
|
||||
free(map->entries[j].alias_prefix);
|
||||
free(map->entries[j].alias_suffix);
|
||||
free(map->entries[j].target_prefix);
|
||||
free(map->entries[j].target_suffix);
|
||||
}
|
||||
free(map->entries);
|
||||
free(map->base_url);
|
||||
free(map);
|
||||
}
|
||||
free(coll->scopes[i].source_rel_path);
|
||||
free_alias_map(coll->scopes[i].map);
|
||||
}
|
||||
free(coll->scopes);
|
||||
free(coll);
|
||||
@@ -472,7 +477,24 @@ cbm_path_alias_collection_t *cbm_load_path_aliases_excluded(const char *repo_pat
|
||||
if (!map) {
|
||||
continue;
|
||||
}
|
||||
coll->scopes[coll->count].dir_prefix = strdup(hits[i].rel);
|
||||
const char *base = strrchr(hits[i].abs, '/');
|
||||
base = base ? base + 1 : hits[i].abs;
|
||||
char selected_rel[CBM_SZ_512];
|
||||
if (hits[i].rel[0]) {
|
||||
snprintf(selected_rel, sizeof(selected_rel), "%s/%s", hits[i].rel, base);
|
||||
} else {
|
||||
snprintf(selected_rel, sizeof(selected_rel), "%s", base);
|
||||
}
|
||||
char *dir_prefix = strdup(hits[i].rel);
|
||||
char *source_rel_path = strdup(selected_rel);
|
||||
if (!dir_prefix || !source_rel_path) {
|
||||
free(dir_prefix);
|
||||
free(source_rel_path);
|
||||
free_alias_map(map);
|
||||
continue;
|
||||
}
|
||||
coll->scopes[coll->count].dir_prefix = dir_prefix;
|
||||
coll->scopes[coll->count].source_rel_path = source_rel_path;
|
||||
coll->scopes[coll->count].map = map;
|
||||
coll->count++;
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ typedef struct {
|
||||
* pick up the map of the nearest ancestor scope. */
|
||||
typedef struct {
|
||||
char *dir_prefix;
|
||||
char *source_rel_path; /* selected config file (tsconfig beats jsconfig) */
|
||||
cbm_path_alias_map_t *map;
|
||||
} cbm_path_alias_scope_t;
|
||||
|
||||
|
||||
+623
-247
@@ -43,6 +43,13 @@ enum { CBM_DIR_PERMS = 0755, PL_RING = 4, PL_RING_MASK = 3, PL_SEQ_PASSES = 6, P
|
||||
#include <stdatomic.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#ifdef _WIN32
|
||||
#include <process.h>
|
||||
#define cbm_pipeline_getpid _getpid
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#define cbm_pipeline_getpid getpid
|
||||
#endif
|
||||
|
||||
static inline void *intptr_to_ptr(intptr_t v) {
|
||||
void *p;
|
||||
@@ -55,6 +62,68 @@ static inline void *intptr_to_ptr(intptr_t v) {
|
||||
* Atomic spinlock: 0 = free, 1 = locked. */
|
||||
static atomic_int g_pipeline_busy = 0;
|
||||
|
||||
#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API
|
||||
static atomic_bool g_persist_test_fail_after_stage_dump = false;
|
||||
static atomic_bool g_persist_test_cancel_after_predump = false;
|
||||
static atomic_bool g_persist_test_cancel_after_destination_prepare = false;
|
||||
static atomic_bool g_persist_test_fail_adr_capture = false;
|
||||
static cbm_pipeline_test_hook_fn g_persist_test_before_final_manifest = NULL;
|
||||
static void *g_persist_test_before_final_manifest_userdata = NULL;
|
||||
|
||||
void cbm_pipeline_incremental_test_fail_after_stage_dump_once(void) {
|
||||
atomic_store(&g_persist_test_fail_after_stage_dump, true);
|
||||
}
|
||||
|
||||
void cbm_pipeline_incremental_test_cancel_after_predump_once(void) {
|
||||
atomic_store(&g_persist_test_cancel_after_predump, true);
|
||||
}
|
||||
|
||||
void cbm_pipeline_incremental_test_cancel_after_destination_prepare_once(void) {
|
||||
atomic_store(&g_persist_test_cancel_after_destination_prepare, true);
|
||||
}
|
||||
|
||||
void cbm_pipeline_incremental_test_fail_adr_capture_once(void) {
|
||||
atomic_store(&g_persist_test_fail_adr_capture, true);
|
||||
}
|
||||
|
||||
void cbm_pipeline_incremental_test_before_final_manifest_once(cbm_pipeline_test_hook_fn hook,
|
||||
void *userdata) {
|
||||
g_persist_test_before_final_manifest = hook;
|
||||
g_persist_test_before_final_manifest_userdata = userdata;
|
||||
}
|
||||
|
||||
void cbm_pipeline_persist_test_run_before_final_manifest(void) {
|
||||
cbm_pipeline_test_hook_fn hook = g_persist_test_before_final_manifest;
|
||||
void *userdata = g_persist_test_before_final_manifest_userdata;
|
||||
g_persist_test_before_final_manifest = NULL;
|
||||
g_persist_test_before_final_manifest_userdata = NULL;
|
||||
if (hook) {
|
||||
hook(userdata);
|
||||
}
|
||||
}
|
||||
|
||||
bool cbm_pipeline_persist_test_take_failure_after_stage_dump(void) {
|
||||
return atomic_exchange(&g_persist_test_fail_after_stage_dump, false);
|
||||
}
|
||||
|
||||
bool cbm_pipeline_persist_test_take_cancel_after_predump(void) {
|
||||
return atomic_exchange(&g_persist_test_cancel_after_predump, false);
|
||||
}
|
||||
|
||||
bool cbm_pipeline_persist_test_take_cancel_after_destination_prepare(void) {
|
||||
return atomic_exchange(&g_persist_test_cancel_after_destination_prepare, false);
|
||||
}
|
||||
|
||||
void cbm_pipeline_persist_test_reset_faults(void) {
|
||||
atomic_store(&g_persist_test_fail_after_stage_dump, false);
|
||||
atomic_store(&g_persist_test_cancel_after_predump, false);
|
||||
atomic_store(&g_persist_test_cancel_after_destination_prepare, false);
|
||||
atomic_store(&g_persist_test_fail_adr_capture, false);
|
||||
g_persist_test_before_final_manifest = NULL;
|
||||
g_persist_test_before_final_manifest_userdata = NULL;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool cbm_pipeline_try_lock(void) {
|
||||
return atomic_exchange(&g_pipeline_busy, 1) == 0;
|
||||
}
|
||||
@@ -188,6 +257,24 @@ cbm_pipeline_t *cbm_pipeline_new(const char *repo_path, const char *db_path,
|
||||
return p;
|
||||
}
|
||||
|
||||
static int pipeline_refresh_git_context(cbm_pipeline_t *p) {
|
||||
cbm_git_context_t fresh = {0};
|
||||
if (!p || cbm_git_context_resolve(p->repo_path, &fresh) != 0) {
|
||||
cbm_git_context_free(&fresh);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
char *fresh_branch_qn = cbm_git_context_branch_qn(p->project_name, &fresh);
|
||||
if (!fresh_branch_qn) {
|
||||
cbm_git_context_free(&fresh);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
cbm_git_context_free(&p->git_ctx);
|
||||
free(p->branch_qn);
|
||||
p->git_ctx = fresh;
|
||||
p->branch_qn = fresh_branch_qn;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void cbm_pipeline_set_persistence(cbm_pipeline_t *p, bool enabled) {
|
||||
if (p) {
|
||||
p->persistence = enabled;
|
||||
@@ -979,6 +1066,9 @@ static int run_parallel_pipeline(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx,
|
||||
cbm_log_info("pass.timing", "pass", "parallel_extract", "elapsed_ms",
|
||||
itoa_buf((int)elapsed_ms(*t)));
|
||||
if (rc != 0 || check_cancel(p)) {
|
||||
for (int i = 0; i < file_count; i++) {
|
||||
cbm_free_result(cache[i]);
|
||||
}
|
||||
free(cache);
|
||||
return rc != 0 ? rc : CBM_NOT_FOUND;
|
||||
}
|
||||
@@ -1005,6 +1095,15 @@ static int run_parallel_pipeline(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx,
|
||||
free(cache);
|
||||
return rc != 0 ? rc : CBM_NOT_FOUND;
|
||||
}
|
||||
/* Registry consumers may materialize serial nodes (Channel, EnvVar, and
|
||||
* future carrier-derived resources) after parallel extraction established
|
||||
* the shared allocator watermark. Advance the atomic allocator before
|
||||
* resolve workers resume; otherwise their IDs and the later next-id reset
|
||||
* can collide with those nodes and orphan freshly inserted edges. */
|
||||
int64_t registry_next_id = cbm_gbuf_next_id(p->gbuf);
|
||||
if (registry_next_id > atomic_load(&shared_ids)) {
|
||||
atomic_store(&shared_ids, registry_next_id);
|
||||
}
|
||||
/* Cross-file LSP precondition: build a project-wide CBMLSPDef[]
|
||||
* once. The fused resolve_worker invokes cbm_pxc_run_one(_ts) per
|
||||
* file using these defs + the file's IMPORTS map, so cross-file
|
||||
@@ -1100,77 +1199,76 @@ static int run_parallel_pipeline(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx,
|
||||
return check_cancel(p) ? CBM_NOT_FOUND : 0;
|
||||
}
|
||||
|
||||
/* Try incremental pipeline or delete old DB for reindex.
|
||||
* Returns >= 0 if incremental was used (the return code), or -1 to proceed with full. */
|
||||
static int try_incremental_or_delete_db(cbm_pipeline_t *p, cbm_file_info_t *files, int file_count) {
|
||||
static int capture_existing_adr(cbm_pipeline_t *p, const char *db_path) {
|
||||
#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API
|
||||
if (atomic_exchange(&g_persist_test_fail_adr_capture, false)) {
|
||||
return CBM_PIPELINE_ABORT_PRESERVE_DB;
|
||||
}
|
||||
#endif
|
||||
cbm_store_t *adr_store = cbm_store_open_path_query(db_path);
|
||||
if (!adr_store) {
|
||||
return CBM_PIPELINE_ABORT_PRESERVE_DB;
|
||||
}
|
||||
cbm_adr_t existing = {0};
|
||||
int adr_rc = cbm_store_adr_get(adr_store, p->project_name, &existing);
|
||||
if (adr_rc == CBM_STORE_NOT_FOUND) {
|
||||
cbm_store_close(adr_store);
|
||||
free(p->saved_adr);
|
||||
p->saved_adr = NULL;
|
||||
return 0;
|
||||
}
|
||||
if (adr_rc != CBM_STORE_OK || !existing.content) {
|
||||
cbm_store_adr_free(&existing);
|
||||
cbm_store_close(adr_store);
|
||||
return CBM_PIPELINE_ABORT_PRESERVE_DB;
|
||||
}
|
||||
char *saved = strdup(existing.content);
|
||||
cbm_store_adr_free(&existing);
|
||||
cbm_store_close(adr_store);
|
||||
if (!saved) {
|
||||
return CBM_PIPELINE_ABORT_PRESERVE_DB;
|
||||
}
|
||||
free(p->saved_adr);
|
||||
p->saved_adr = saved;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Route an existing generation. Full rebuilds never delete the live DB here:
|
||||
* publication owns the eventual atomic replacement after every pass and
|
||||
* metadata write has succeeded. */
|
||||
static int try_incremental_or_delete_db(cbm_pipeline_t *p, cbm_file_info_t *files, int file_count,
|
||||
const cbm_file_hash_t *baseline_manifest,
|
||||
int baseline_count) {
|
||||
char *db_path = resolve_db_path(p);
|
||||
if (!db_path) {
|
||||
return CBM_NOT_FOUND;
|
||||
return CBM_PIPELINE_FORCE_FULL_REINDEX;
|
||||
}
|
||||
struct stat db_st;
|
||||
if (stat(db_path, &db_st) != 0) {
|
||||
free(db_path);
|
||||
return CBM_NOT_FOUND;
|
||||
return CBM_PIPELINE_FORCE_FULL_REINDEX;
|
||||
}
|
||||
cbm_store_t *check_store = cbm_store_open_path(db_path);
|
||||
if (check_store && cbm_store_check_integrity(check_store)) {
|
||||
cbm_file_hash_t *hashes = NULL;
|
||||
int hash_count = 0;
|
||||
cbm_store_get_file_hashes(check_store, p->project_name, &hashes, &hash_count);
|
||||
cbm_store_free_file_hashes(hashes, hash_count);
|
||||
cbm_store_close(check_store);
|
||||
if (hash_count > 0 && file_count <= hash_count + (hash_count / PAIR_LEN)) {
|
||||
cbm_log_info("pipeline.route", "path", "incremental", "stored_hashes",
|
||||
itoa_buf(hash_count));
|
||||
int rc = cbm_pipeline_run_incremental(p, db_path, files, file_count);
|
||||
free(db_path);
|
||||
return rc;
|
||||
}
|
||||
if (hash_count > 0) {
|
||||
cbm_log_info("pipeline.route", "path", "mode_change_reindex", "stored_hashes",
|
||||
itoa_buf(hash_count), "discovered", itoa_buf(file_count));
|
||||
}
|
||||
} else if (check_store) {
|
||||
cbm_store_t *check_store = cbm_store_open_path_query(db_path);
|
||||
bool valid = check_store && cbm_store_check_integrity(check_store);
|
||||
if (check_store) {
|
||||
cbm_store_close(check_store);
|
||||
}
|
||||
cbm_log_info("pipeline.route", "path", "reindex", "action", "deleting old db");
|
||||
/* Capture any ADR before deleting the DB so the full-reindex rebuild can
|
||||
* restore it (project_summaries is otherwise lost). Issue #516. */
|
||||
{
|
||||
cbm_store_t *adr_store = cbm_store_open_path(db_path);
|
||||
if (adr_store) {
|
||||
cbm_adr_t existing;
|
||||
if (cbm_store_adr_get(adr_store, p->project_name, &existing) == CBM_STORE_OK) {
|
||||
if (existing.content) {
|
||||
free(p->saved_adr);
|
||||
p->saved_adr = strdup(existing.content);
|
||||
}
|
||||
cbm_store_adr_free(&existing);
|
||||
}
|
||||
cbm_store_close(adr_store);
|
||||
if (!valid) {
|
||||
cbm_log_warn("pipeline.route", "path", "full", "reason", "invalid_existing_db");
|
||||
free(db_path);
|
||||
return CBM_PIPELINE_FORCE_FULL_REINDEX;
|
||||
}
|
||||
cbm_log_info("pipeline.route", "path", "incremental_manifest");
|
||||
int rc = cbm_pipeline_run_incremental(p, db_path, files, file_count, baseline_manifest,
|
||||
baseline_count);
|
||||
if (rc == CBM_PIPELINE_FORCE_FULL_REINDEX) {
|
||||
int adr_rc = capture_existing_adr(p, db_path);
|
||||
if (adr_rc != 0) {
|
||||
rc = adr_rc;
|
||||
}
|
||||
}
|
||||
cbm_unlink(db_path);
|
||||
char wal[PL_WAL_BUF];
|
||||
char shm[PL_WAL_BUF];
|
||||
snprintf(wal, sizeof(wal), "%s-wal", db_path);
|
||||
snprintf(shm, sizeof(shm), "%s-shm", db_path);
|
||||
cbm_unlink(wal);
|
||||
cbm_unlink(shm);
|
||||
free(db_path);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
/* Get platform-specific mtime in nanoseconds. */
|
||||
static int64_t stat_mtime_ns(const struct stat *fst) {
|
||||
#ifdef __APPLE__
|
||||
return ((int64_t)fst->st_mtimespec.tv_sec * PL_NSEC_PER_SEC) +
|
||||
(int64_t)fst->st_mtimespec.tv_nsec;
|
||||
#elif defined(_WIN32)
|
||||
return (int64_t)fst->st_mtime * 1000000000LL;
|
||||
#else
|
||||
return ((int64_t)fst->st_mtim.tv_sec * PL_NSEC_PER_SEC) + (int64_t)fst->st_mtim.tv_nsec;
|
||||
#endif
|
||||
return rc;
|
||||
}
|
||||
|
||||
static const char *pipeline_mode_name(cbm_index_mode_t mode) {
|
||||
@@ -1186,9 +1284,348 @@ static const char *pipeline_mode_name(cbm_index_mode_t mode) {
|
||||
}
|
||||
}
|
||||
|
||||
static int pipeline_mode_coverage_rank(cbm_index_mode_t mode) {
|
||||
switch (mode) {
|
||||
case CBM_MODE_FULL:
|
||||
return 3;
|
||||
case CBM_MODE_MODERATE:
|
||||
return 2;
|
||||
case CBM_MODE_FAST:
|
||||
return 1;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Index modes are additive: a cheaper run may refresh a fuller graph, but it
|
||||
* must never erase files that the cheaper discovery intentionally skips. The
|
||||
* exact-manifest pipeline therefore keeps the most comprehensive successfully
|
||||
* published mode and performs any changed rebuild at that coverage level. */
|
||||
static void promote_mode_to_existing_coverage(cbm_pipeline_t *p) {
|
||||
if (!p || !p->project_name) {
|
||||
return;
|
||||
}
|
||||
char *db_path = resolve_db_path(p);
|
||||
if (!db_path) {
|
||||
return;
|
||||
}
|
||||
cbm_store_t *store = cbm_store_open_path_query(db_path);
|
||||
free(db_path);
|
||||
if (!store) {
|
||||
return;
|
||||
}
|
||||
cbm_coverage_meta_t meta = {0};
|
||||
if (cbm_store_coverage_meta_get(store, p->project_name, &meta) == CBM_STORE_OK &&
|
||||
meta.index_mode) {
|
||||
cbm_index_mode_t stored_mode = p->mode;
|
||||
if (strcmp(meta.index_mode, "full") == 0) {
|
||||
stored_mode = CBM_MODE_FULL;
|
||||
} else if (strcmp(meta.index_mode, "moderate") == 0) {
|
||||
stored_mode = CBM_MODE_MODERATE;
|
||||
} else if (strcmp(meta.index_mode, "fast") == 0) {
|
||||
stored_mode = CBM_MODE_FAST;
|
||||
}
|
||||
if (pipeline_mode_coverage_rank(stored_mode) > pipeline_mode_coverage_rank(p->mode)) {
|
||||
cbm_log_info("pipeline.mode", "requested", pipeline_mode_name(p->mode), "effective",
|
||||
pipeline_mode_name(stored_mode), "reason", "preserve_existing_coverage");
|
||||
p->mode = stored_mode;
|
||||
}
|
||||
}
|
||||
cbm_store_coverage_meta_clear(&meta);
|
||||
cbm_store_close(store);
|
||||
}
|
||||
|
||||
int cbm_pipeline_refresh_artifact(cbm_pipeline_t *p, const char *db_path) {
|
||||
if (!p || !db_path || !p->repo_path || !p->project_name) {
|
||||
return 0;
|
||||
}
|
||||
bool existing = cbm_artifact_exists(p->repo_path);
|
||||
if (!p->persistence && !existing) {
|
||||
return 0;
|
||||
}
|
||||
int quality = p->persistence ? CBM_ARTIFACT_BEST : CBM_ARTIFACT_FAST;
|
||||
int rc = cbm_artifact_export(db_path, p->repo_path, p->project_name, quality);
|
||||
if (rc != 0) {
|
||||
const char *err = cbm_artifact_export_last_error();
|
||||
if (p->persistence) {
|
||||
cbm_log_error("pipeline.err", "phase", "artifact_export", "err", err ? err : "unknown");
|
||||
return rc;
|
||||
}
|
||||
cbm_log_warn("artifact.refresh_failed", "err", err ? err : "unknown");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static atomic_ullong g_generation_stage_counter = 0;
|
||||
|
||||
static void discard_generation_stage(const char *stage_path) {
|
||||
if (!stage_path) {
|
||||
return;
|
||||
}
|
||||
cbm_unlink(stage_path);
|
||||
cbm_remove_db_sidecars(stage_path);
|
||||
}
|
||||
|
||||
static int generation_rebuild_fts(cbm_store_t *store) {
|
||||
if (cbm_store_exec(store, "INSERT INTO nodes_fts(nodes_fts) VALUES('delete-all');") !=
|
||||
CBM_STORE_OK) {
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
if (cbm_store_exec(store,
|
||||
"INSERT INTO nodes_fts(rowid, name, qualified_name, label, file_path) "
|
||||
"SELECT id, cbm_camel_split(name), qualified_name, label, file_path "
|
||||
"FROM nodes;") == CBM_STORE_OK) {
|
||||
return CBM_STORE_OK;
|
||||
}
|
||||
return cbm_store_exec(store,
|
||||
"INSERT INTO nodes_fts(rowid, name, qualified_name, label, file_path) "
|
||||
"SELECT id, name, qualified_name, label, file_path FROM nodes;");
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
bool quarantined;
|
||||
char backup_path[CBM_SZ_4K];
|
||||
} cbm_replacement_prepare_t;
|
||||
|
||||
static int replacement_sidecar_path(char *out, size_t out_size, const char *base,
|
||||
const char *suffix) {
|
||||
int n = snprintf(out, out_size, "%s%s", base, suffix);
|
||||
return n > 0 && (size_t)n < out_size ? 0 : CBM_PIPELINE_PERSIST_FAILED;
|
||||
}
|
||||
|
||||
static bool replacement_path_exists(const char *path) {
|
||||
cbm_path_info_t info;
|
||||
return cbm_path_info_utf8(path, &info) == 0;
|
||||
}
|
||||
|
||||
static int rollback_quarantined_generation(const char *db_path,
|
||||
cbm_replacement_prepare_t *prepared) {
|
||||
if (!prepared || !prepared->quarantined) {
|
||||
return 0;
|
||||
}
|
||||
static const char *const suffixes[] = {"-wal", "-shm"};
|
||||
if (cbm_rename_noreplace(prepared->backup_path, db_path) != 0) {
|
||||
return CBM_PIPELINE_PERSIST_FAILED;
|
||||
}
|
||||
for (size_t i = 0; i < sizeof(suffixes) / sizeof(suffixes[0]); i++) {
|
||||
char source[CBM_SZ_4K];
|
||||
char destination[CBM_SZ_4K];
|
||||
if (replacement_sidecar_path(source, sizeof(source), prepared->backup_path, suffixes[i]) !=
|
||||
0 ||
|
||||
replacement_sidecar_path(destination, sizeof(destination), db_path, suffixes[i]) != 0) {
|
||||
return CBM_PIPELINE_PERSIST_FAILED;
|
||||
}
|
||||
if (replacement_path_exists(source) && cbm_rename_noreplace(source, destination) != 0) {
|
||||
return CBM_PIPELINE_PERSIST_FAILED;
|
||||
}
|
||||
}
|
||||
prepared->quarantined = false;
|
||||
prepared->backup_path[0] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int quarantine_existing_generation(const char *db_path,
|
||||
cbm_replacement_prepare_t *prepared) {
|
||||
if (!db_path || !prepared) {
|
||||
return CBM_PIPELINE_PERSIST_FAILED;
|
||||
}
|
||||
static const char *const suffixes[] = {"-wal", "-shm"};
|
||||
char candidate[CBM_SZ_4K];
|
||||
for (int attempt = 0; attempt < 10000; attempt++) {
|
||||
int n = attempt == 0
|
||||
? snprintf(candidate, sizeof(candidate), "%s.corrupt", db_path)
|
||||
: snprintf(candidate, sizeof(candidate), "%s.corrupt.%d", db_path, attempt);
|
||||
if (n <= 0 || (size_t)n >= sizeof(candidate)) {
|
||||
return CBM_PIPELINE_PERSIST_FAILED;
|
||||
}
|
||||
bool available = !replacement_path_exists(candidate);
|
||||
for (size_t i = 0; available && i < sizeof(suffixes) / sizeof(suffixes[0]); i++) {
|
||||
char candidate_sidecar[CBM_SZ_4K];
|
||||
if (replacement_sidecar_path(candidate_sidecar, sizeof(candidate_sidecar), candidate,
|
||||
suffixes[i]) != 0) {
|
||||
return CBM_PIPELINE_PERSIST_FAILED;
|
||||
}
|
||||
available = !replacement_path_exists(candidate_sidecar);
|
||||
}
|
||||
if (!available) {
|
||||
continue;
|
||||
}
|
||||
if (cbm_rename_noreplace(db_path, candidate) != 0) {
|
||||
if (replacement_path_exists(candidate)) {
|
||||
continue;
|
||||
}
|
||||
return CBM_PIPELINE_PERSIST_FAILED;
|
||||
}
|
||||
|
||||
snprintf(prepared->backup_path, sizeof(prepared->backup_path), "%s", candidate);
|
||||
prepared->quarantined = true;
|
||||
for (size_t i = 0; i < sizeof(suffixes) / sizeof(suffixes[0]); i++) {
|
||||
char source[CBM_SZ_4K];
|
||||
char destination[CBM_SZ_4K];
|
||||
if (replacement_sidecar_path(source, sizeof(source), db_path, suffixes[i]) != 0 ||
|
||||
replacement_sidecar_path(destination, sizeof(destination), candidate,
|
||||
suffixes[i]) != 0) {
|
||||
(void)rollback_quarantined_generation(db_path, prepared);
|
||||
return CBM_PIPELINE_PERSIST_FAILED;
|
||||
}
|
||||
if (replacement_path_exists(source) && cbm_rename_noreplace(source, destination) != 0) {
|
||||
(void)rollback_quarantined_generation(db_path, prepared);
|
||||
return CBM_PIPELINE_PERSIST_FAILED;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return CBM_PIPELINE_PERSIST_FAILED;
|
||||
}
|
||||
|
||||
static int prepare_existing_generation_for_replace(const char *db_path,
|
||||
cbm_replacement_prepare_t *prepared) {
|
||||
if (!prepared) {
|
||||
return CBM_PIPELINE_PERSIST_FAILED;
|
||||
}
|
||||
memset(prepared, 0, sizeof(*prepared));
|
||||
cbm_path_info_t info;
|
||||
if (cbm_path_info_utf8(db_path, &info) == 0) {
|
||||
if (!info.is_regular || info.is_symlink) {
|
||||
return CBM_PIPELINE_PERSIST_FAILED;
|
||||
}
|
||||
int seal_rc = cbm_store_seal_existing_path_for_replace(db_path);
|
||||
if (seal_rc == CBM_STORE_NOT_FOUND) {
|
||||
return quarantine_existing_generation(db_path, prepared);
|
||||
}
|
||||
if (seal_rc != CBM_STORE_OK) {
|
||||
return CBM_PIPELINE_PERSIST_FAILED;
|
||||
}
|
||||
}
|
||||
return cbm_remove_db_sidecars_checked(db_path) == 0 ? 0 : CBM_PIPELINE_PERSIST_FAILED;
|
||||
}
|
||||
|
||||
int cbm_pipeline_publish_generation(const cbm_pipeline_generation_t *generation) {
|
||||
if (!generation || !generation->gbuf || !generation->final_db_path || !generation->project ||
|
||||
generation->manifest_count < 0 ||
|
||||
(generation->manifest_count > 0 && !generation->manifest) ||
|
||||
generation->coverage_count < 0 ||
|
||||
(generation->coverage_count > 0 && !generation->coverage)) {
|
||||
return CBM_PIPELINE_PERSIST_FAILED;
|
||||
}
|
||||
if (generation->cancelled && atomic_load(generation->cancelled)) {
|
||||
return CBM_PIPELINE_ABORT_PRESERVE_DB;
|
||||
}
|
||||
|
||||
size_t stage_size = strlen(generation->final_db_path) + 96;
|
||||
char *stage_path = malloc(stage_size);
|
||||
if (!stage_path) {
|
||||
return CBM_PIPELINE_PERSIST_FAILED;
|
||||
}
|
||||
unsigned long long serial = atomic_fetch_add(&g_generation_stage_counter, 1) + 1;
|
||||
int path_n = snprintf(stage_path, stage_size, "%s.stage.%ld.%llu", generation->final_db_path,
|
||||
(long)cbm_pipeline_getpid(), serial);
|
||||
if (path_n < 0 || (size_t)path_n >= stage_size) {
|
||||
free(stage_path);
|
||||
return CBM_PIPELINE_PERSIST_FAILED;
|
||||
}
|
||||
discard_generation_stage(stage_path);
|
||||
|
||||
int dump_rc = cbm_gbuf_dump_to_sqlite(generation->gbuf, stage_path);
|
||||
if (dump_rc != 0) {
|
||||
discard_generation_stage(stage_path);
|
||||
free(stage_path);
|
||||
return CBM_PIPELINE_PERSIST_FAILED;
|
||||
}
|
||||
#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API
|
||||
if (cbm_pipeline_persist_test_take_failure_after_stage_dump()) {
|
||||
discard_generation_stage(stage_path);
|
||||
free(stage_path);
|
||||
return CBM_PIPELINE_PERSIST_FAILED;
|
||||
}
|
||||
#endif
|
||||
if (generation->cancelled && atomic_load(generation->cancelled)) {
|
||||
discard_generation_stage(stage_path);
|
||||
free(stage_path);
|
||||
return CBM_PIPELINE_ABORT_PRESERVE_DB;
|
||||
}
|
||||
|
||||
cbm_store_t *store = cbm_store_open_path(stage_path);
|
||||
if (!store) {
|
||||
discard_generation_stage(stage_path);
|
||||
free(stage_path);
|
||||
return CBM_PIPELINE_PERSIST_FAILED;
|
||||
}
|
||||
bool ok = cbm_store_exec(store, "PRAGMA synchronous=FULL;") == CBM_STORE_OK;
|
||||
ok = ok && cbm_store_delete_file_hashes(store, generation->project) == CBM_STORE_OK &&
|
||||
cbm_store_upsert_file_hash_batch(store, generation->manifest,
|
||||
generation->manifest_count) == CBM_STORE_OK;
|
||||
if (ok && generation->adr_content) {
|
||||
ok = cbm_store_adr_store(store, generation->project, generation->adr_content) ==
|
||||
CBM_STORE_OK;
|
||||
}
|
||||
|
||||
cbm_project_t project_info = {0};
|
||||
bool have_project_info =
|
||||
cbm_store_get_project(store, generation->project, &project_info) == CBM_STORE_OK;
|
||||
cbm_coverage_meta_t meta = generation->coverage_meta;
|
||||
meta.generation = have_project_info ? project_info.indexed_at : NULL;
|
||||
meta.coverage_version = CBM_SEMANTIC_INDEX_VERSION;
|
||||
meta.hash_records_complete = true;
|
||||
if (!have_project_info ||
|
||||
cbm_store_coverage_replace_ex(store, generation->project, generation->coverage,
|
||||
generation->coverage_count, &meta) != CBM_STORE_OK) {
|
||||
ok = false;
|
||||
}
|
||||
if (have_project_info) {
|
||||
cbm_project_free_fields(&project_info);
|
||||
}
|
||||
if (generation_rebuild_fts(store) != CBM_STORE_OK || !cbm_store_check_integrity(store)) {
|
||||
ok = false;
|
||||
}
|
||||
if (ok && cbm_store_seal_for_atomic_publish(store) != CBM_STORE_OK) {
|
||||
ok = false;
|
||||
}
|
||||
cbm_store_close(store);
|
||||
if (ok && cbm_remove_db_sidecars_checked(stage_path) != 0) {
|
||||
ok = false;
|
||||
}
|
||||
if (!ok) {
|
||||
discard_generation_stage(stage_path);
|
||||
free(stage_path);
|
||||
return CBM_PIPELINE_PERSIST_FAILED;
|
||||
}
|
||||
if (generation->cancelled && atomic_load(generation->cancelled)) {
|
||||
discard_generation_stage(stage_path);
|
||||
free(stage_path);
|
||||
return CBM_PIPELINE_ABORT_PRESERVE_DB;
|
||||
}
|
||||
cbm_replacement_prepare_t prepared = {0};
|
||||
if (prepare_existing_generation_for_replace(generation->final_db_path, &prepared) != 0) {
|
||||
discard_generation_stage(stage_path);
|
||||
free(stage_path);
|
||||
return CBM_PIPELINE_PERSIST_FAILED;
|
||||
}
|
||||
#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API
|
||||
if (cbm_pipeline_persist_test_take_cancel_after_destination_prepare() &&
|
||||
generation->cancelled) {
|
||||
atomic_store(generation->cancelled, true);
|
||||
}
|
||||
#endif
|
||||
if (generation->cancelled && atomic_load(generation->cancelled)) {
|
||||
int rollback_rc = rollback_quarantined_generation(generation->final_db_path, &prepared);
|
||||
discard_generation_stage(stage_path);
|
||||
free(stage_path);
|
||||
return rollback_rc == 0 ? CBM_PIPELINE_ABORT_PRESERVE_DB : CBM_PIPELINE_PERSIST_FAILED;
|
||||
}
|
||||
if (cbm_rename_replace(stage_path, generation->final_db_path) != 0) {
|
||||
(void)rollback_quarantined_generation(generation->final_db_path, &prepared);
|
||||
discard_generation_stage(stage_path);
|
||||
free(stage_path);
|
||||
return CBM_PIPELINE_PERSIST_FAILED;
|
||||
}
|
||||
free(stage_path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Dump graph to SQLite and persist file hashes for incremental indexing. */
|
||||
static int dump_and_persist_hashes(cbm_pipeline_t *p, const cbm_file_info_t *files, int file_count,
|
||||
struct timespec *t) {
|
||||
static int dump_and_persist_hashes(cbm_pipeline_t *p, const cbm_file_hash_t *baseline_manifest,
|
||||
int baseline_count, struct timespec *t) {
|
||||
cbm_clock_gettime(CLOCK_MONOTONIC, t);
|
||||
char db_path[CBM_SZ_1K];
|
||||
if (p->db_path) {
|
||||
@@ -1207,190 +1644,95 @@ static int dump_and_persist_hashes(cbm_pipeline_t *p, const cbm_file_info_t *fil
|
||||
*last_slash = '\0';
|
||||
cbm_mkdir_p(db_dir, CBM_DIR_PERMS);
|
||||
}
|
||||
/* Capture committed counts BEFORE the dump. cbm_gbuf_dump_to_sqlite calls
|
||||
* release_gbuf_indexes(), which frees node_by_qn (graph_buffer.c), after
|
||||
* which cbm_gbuf_node_count() returns 0. Reading these post-dump left
|
||||
* committed_nodes at 0, so the #334 plausibility gate never fired. */
|
||||
|
||||
cbm_file_hash_t *manifest = NULL;
|
||||
int manifest_count = 0;
|
||||
#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API
|
||||
cbm_pipeline_persist_test_run_before_final_manifest();
|
||||
#endif
|
||||
if (cbm_pipeline_build_fresh_semantic_manifest(p->project_name, p->repo_path, p->mode,
|
||||
&manifest, &manifest_count) != 0) {
|
||||
cbm_log_error("pipeline.err", "phase", "semantic_manifest");
|
||||
return CBM_PIPELINE_ABORT_PRESERVE_DB;
|
||||
}
|
||||
if (!cbm_pipeline_semantic_manifests_equal(baseline_manifest, baseline_count, manifest,
|
||||
manifest_count)) {
|
||||
cbm_log_warn("pipeline.abort", "reason", "semantic_inputs_changed");
|
||||
cbm_pipeline_free_semantic_manifest(manifest, manifest_count);
|
||||
return CBM_PIPELINE_ABORT_PRESERVE_DB;
|
||||
}
|
||||
|
||||
int cov_total = p->file_errors_count + p->excluded_count + p->ignored_count;
|
||||
cbm_coverage_row_t *cov = NULL;
|
||||
int cov_count = 0;
|
||||
bool coverage_rows_available = cov_total == 0;
|
||||
if (cov_total > 0) {
|
||||
cov = malloc((size_t)cov_total * sizeof(*cov));
|
||||
if (cov) {
|
||||
coverage_rows_available = true;
|
||||
for (int i = 0; i < p->file_errors_count; i++) {
|
||||
cov[cov_count++] = (cbm_coverage_row_t){.rel_path = p->file_errors[i].path,
|
||||
.kind = p->file_errors[i].phase,
|
||||
.detail = p->file_errors[i].reason};
|
||||
}
|
||||
for (int i = 0; i < p->excluded_count; i++) {
|
||||
cov[cov_count++] = (cbm_coverage_row_t){.rel_path = p->excluded_dirs[i],
|
||||
.kind = "not_indexed_dir",
|
||||
.detail = "excluded subtree"};
|
||||
}
|
||||
for (int i = 0; i < p->ignored_count; i++) {
|
||||
cov[cov_count++] = (cbm_coverage_row_t){.rel_path = p->ignored_files[i].rel_path,
|
||||
.kind = "not_indexed_file",
|
||||
.detail = p->ignored_files[i].reason};
|
||||
}
|
||||
}
|
||||
}
|
||||
cbm_pipeline_generation_t generation = {
|
||||
.gbuf = p->gbuf,
|
||||
.final_db_path = db_path,
|
||||
.project = p->project_name,
|
||||
.cancelled = &p->cancelled,
|
||||
.manifest = manifest,
|
||||
.manifest_count = manifest_count,
|
||||
.adr_content = p->saved_adr,
|
||||
.coverage = cov,
|
||||
.coverage_count = cov_count,
|
||||
.coverage_meta =
|
||||
{
|
||||
.index_mode = pipeline_mode_name(p->mode),
|
||||
.recording_status =
|
||||
!coverage_rows_available
|
||||
? "unavailable"
|
||||
: (p->ignored_total > p->ignored_count ? "truncated" : "complete"),
|
||||
.ignored_files_stored = p->ignored_count,
|
||||
.ignored_files_total = p->ignored_total,
|
||||
.coverage_version = CBM_SEMANTIC_INDEX_VERSION,
|
||||
.hash_records_complete = true,
|
||||
},
|
||||
};
|
||||
|
||||
/* Capture counts before the dump releases graph-buffer indexes. */
|
||||
p->committed_nodes = cbm_gbuf_node_count(p->gbuf);
|
||||
p->committed_edges = cbm_gbuf_edge_count(p->gbuf);
|
||||
int rc = cbm_gbuf_dump_to_sqlite(p->gbuf, db_path);
|
||||
int rc = cbm_pipeline_publish_generation(&generation);
|
||||
free(cov);
|
||||
cbm_pipeline_free_semantic_manifest(manifest, manifest_count);
|
||||
if (rc != 0) {
|
||||
cbm_log_error("pipeline.err", "phase", "dump");
|
||||
return rc;
|
||||
}
|
||||
cbm_log_info("pass.timing", "pass", "dump", "elapsed_ms", itoa_buf((int)elapsed_ms(*t)));
|
||||
/* Persist-tail spans (phase "persist"): attribute the ~60s that lands here
|
||||
* AFTER cbm_gbuf_dump_to_sqlite returns. Active only under CBM_PROFILE. */
|
||||
CBM_PROF_START(t_reopen);
|
||||
cbm_store_t *hash_store = cbm_store_open_path(db_path);
|
||||
CBM_PROF_END("persist", "1_reopen", t_reopen);
|
||||
if (hash_store) {
|
||||
bool hash_records_complete = true;
|
||||
CBM_PROF_START(t_delhash);
|
||||
if (cbm_store_delete_file_hashes(hash_store, p->project_name) != CBM_STORE_OK) {
|
||||
hash_records_complete = false;
|
||||
}
|
||||
CBM_PROF_END("persist", "2_delete_file_hashes", t_delhash);
|
||||
|
||||
/* Restore the ADR captured before the dump. Surface a failed restore
|
||||
* rather than silently dropping the ADR (the original #516 symptom). */
|
||||
CBM_PROF_START(t_adr);
|
||||
if (p->saved_adr) {
|
||||
if (cbm_store_adr_store(hash_store, p->project_name, p->saved_adr) != CBM_STORE_OK) {
|
||||
cbm_log_error("pipeline.err", "phase", "adr_restore", "project", p->project_name);
|
||||
}
|
||||
}
|
||||
CBM_PROF_END("persist", "3_adr_restore", t_adr);
|
||||
|
||||
/* Batch the per-file hash upserts into ONE transaction. The per-file
|
||||
* cbm_store_upsert_file_hash path autocommits, i.e. file_count fsyncs
|
||||
* (~89k on the kernel); cbm_store_upsert_file_hash_batch wraps the same
|
||||
* cached INSERT ... ON CONFLICT upsert in a single begin/commit. Same
|
||||
* (project, rel_path, sha256="", mtime_ns, size) tuples, same replace
|
||||
* semantics — only the transaction boundary changes. */
|
||||
CBM_PROF_START(t_fh);
|
||||
cbm_file_hash_t *fhashes = (cbm_file_hash_t *)malloc(
|
||||
(size_t)(file_count > 0 ? file_count : 1) * sizeof(cbm_file_hash_t));
|
||||
if (fhashes) {
|
||||
int fh_n = 0;
|
||||
for (int i = 0; i < file_count; i++) {
|
||||
struct stat fst;
|
||||
if (stat(files[i].path, &fst) == 0) {
|
||||
fhashes[fh_n].project = p->project_name;
|
||||
fhashes[fh_n].rel_path = files[i].rel_path;
|
||||
fhashes[fh_n].sha256 = "";
|
||||
fhashes[fh_n].mtime_ns = stat_mtime_ns(&fst);
|
||||
fhashes[fh_n].size = fst.st_size;
|
||||
fh_n++;
|
||||
} else {
|
||||
hash_records_complete = false;
|
||||
}
|
||||
}
|
||||
if (cbm_store_upsert_file_hash_batch(hash_store, fhashes, fh_n) != CBM_STORE_OK) {
|
||||
cbm_log_error("pipeline.err", "phase", "persist_file_hashes", "project",
|
||||
p->project_name);
|
||||
hash_records_complete = false;
|
||||
}
|
||||
free(fhashes);
|
||||
} else {
|
||||
/* OOM fallback: the original per-file path (identical result, slower). */
|
||||
for (int i = 0; i < file_count; i++) {
|
||||
struct stat fst;
|
||||
if (stat(files[i].path, &fst) == 0) {
|
||||
if (cbm_store_upsert_file_hash(hash_store, p->project_name, files[i].rel_path,
|
||||
"", stat_mtime_ns(&fst),
|
||||
fst.st_size) != CBM_STORE_OK) {
|
||||
hash_records_complete = false;
|
||||
}
|
||||
} else {
|
||||
hash_records_complete = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
CBM_PROF_END_N("persist", "4_file_hashes", t_fh, file_count);
|
||||
|
||||
/* Coverage rows (#963): a full run's file_errors plus the by-design
|
||||
* discovery exclusions are the complete coverage truth for the
|
||||
* project. The dump recreated the DB file, so the separate
|
||||
* index_coverage table starts empty — write only when there is
|
||||
* something to record (AFTER hashes, so the deleted-file prune inside
|
||||
* replace sees the live file set; not_indexed_* kinds are exempt from
|
||||
* that prune — deliberately-unindexed paths have no hash rows). */
|
||||
int cov_total = p->file_errors_count + p->excluded_count + p->ignored_count;
|
||||
cbm_coverage_row_t *cov = NULL;
|
||||
int cn = 0;
|
||||
bool coverage_rows_available = cov_total == 0;
|
||||
if (cov_total > 0) {
|
||||
cov = (cbm_coverage_row_t *)malloc((size_t)cov_total * sizeof(*cov));
|
||||
if (cov) {
|
||||
coverage_rows_available = true;
|
||||
for (int i = 0; i < p->file_errors_count; i++) {
|
||||
cov[cn].rel_path = p->file_errors[i].path;
|
||||
cov[cn].kind = p->file_errors[i].phase;
|
||||
cov[cn].detail = p->file_errors[i].reason;
|
||||
cn++;
|
||||
}
|
||||
for (int i = 0; i < p->excluded_count; i++) {
|
||||
cov[cn].rel_path = p->excluded_dirs[i];
|
||||
cov[cn].kind = "not_indexed_dir";
|
||||
cov[cn].detail = "excluded subtree";
|
||||
cn++;
|
||||
}
|
||||
for (int i = 0; i < p->ignored_count; i++) {
|
||||
cov[cn].rel_path = p->ignored_files[i].rel_path;
|
||||
cov[cn].kind = "not_indexed_file";
|
||||
cov[cn].detail = p->ignored_files[i].reason;
|
||||
cn++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cbm_project_t project_info = {0};
|
||||
bool have_project_info =
|
||||
cbm_store_get_project(hash_store, p->project_name, &project_info) == CBM_STORE_OK;
|
||||
const char *recording_status =
|
||||
!coverage_rows_available
|
||||
? "unavailable"
|
||||
: (p->ignored_total > p->ignored_count ? "truncated" : "complete");
|
||||
cbm_coverage_meta_t coverage_meta = {
|
||||
.generation = have_project_info ? project_info.indexed_at : NULL,
|
||||
.index_mode = pipeline_mode_name(p->mode),
|
||||
.recording_status = recording_status,
|
||||
.ignored_files_stored = p->ignored_count,
|
||||
.ignored_files_total = p->ignored_total,
|
||||
.coverage_version = 1,
|
||||
.hash_records_complete = hash_records_complete,
|
||||
};
|
||||
if (cbm_store_coverage_replace_ex(hash_store, p->project_name, cov, cn, &coverage_meta) !=
|
||||
CBM_STORE_OK) {
|
||||
cbm_log_error("pipeline.err", "phase", "persist_coverage", "project", p->project_name);
|
||||
}
|
||||
free(cov);
|
||||
if (have_project_info) {
|
||||
cbm_project_free_fields(&project_info);
|
||||
}
|
||||
if (p->ignored_total > p->ignored_count) {
|
||||
cbm_log_warn("index.ignored_capped", "stored", itoa_buf(p->ignored_count), "total",
|
||||
itoa_buf(p->ignored_total));
|
||||
}
|
||||
|
||||
/* FTS5 backfill: populate nodes_fts with camelCase-split names.
|
||||
* Contentless FTS5 requires the special 'delete-all' command instead of
|
||||
* DELETE FROM to wipe prior rows (there's no underlying content table).
|
||||
* Falls back to plain names if cbm_camel_split is unavailable (which
|
||||
* shouldn't happen because we always register it, but we stay defensive). */
|
||||
CBM_PROF_START(t_fts);
|
||||
cbm_store_exec(hash_store, "INSERT INTO nodes_fts(nodes_fts) VALUES('delete-all');");
|
||||
if (cbm_store_exec(hash_store,
|
||||
"INSERT INTO nodes_fts(rowid, name, qualified_name, label, file_path) "
|
||||
"SELECT id, cbm_camel_split(name), qualified_name, label, file_path "
|
||||
"FROM nodes;") != CBM_STORE_OK) {
|
||||
cbm_store_exec(hash_store,
|
||||
"INSERT INTO nodes_fts(rowid, name, qualified_name, label, file_path) "
|
||||
"SELECT id, name, qualified_name, label, file_path FROM nodes;");
|
||||
}
|
||||
CBM_PROF_END("persist", "5_fts_backfill", t_fts);
|
||||
|
||||
cbm_store_close(hash_store);
|
||||
cbm_log_info("pass.timing", "pass", "persist_hashes", "files", itoa_buf(file_count));
|
||||
cbm_log_info("pass.timing", "pass", "dump_and_persist", "elapsed_ms",
|
||||
itoa_buf((int)elapsed_ms(*t)), "files", itoa_buf(manifest_count));
|
||||
if (p->ignored_total > p->ignored_count) {
|
||||
cbm_log_warn("index.ignored_capped", "stored", itoa_buf(p->ignored_count), "total",
|
||||
itoa_buf(p->ignored_total));
|
||||
}
|
||||
free(p->saved_adr);
|
||||
p->saved_adr = NULL;
|
||||
|
||||
/* Export persistent artifact if enabled */
|
||||
if (p->persistence) {
|
||||
CBM_PROF_START(t_art);
|
||||
int arc = cbm_artifact_export(db_path, p->repo_path, p->project_name, CBM_ARTIFACT_BEST);
|
||||
CBM_PROF_END("persist", "6_artifact_export", t_art);
|
||||
if (arc != 0) {
|
||||
const char *err = cbm_artifact_export_last_error();
|
||||
cbm_log_error("pipeline.err", "phase", "artifact_export", "err", err ? err : "unknown");
|
||||
/* A failed persistence export intentionally fails the run; this used to be ignored. */
|
||||
return arc;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
/* The SQLite generation is the commit point. Automatic refresh of an
|
||||
* existing artifact is best-effort, but an explicitly requested artifact
|
||||
* is caller-visible and must report an export failure. */
|
||||
return cbm_pipeline_refresh_artifact(p, db_path);
|
||||
}
|
||||
|
||||
/* Run githistory pass. */
|
||||
@@ -1459,7 +1801,8 @@ static int run_tests_and_history(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx,
|
||||
|
||||
/* Run tests, git history, predump passes, and dump+persist. */
|
||||
static int run_post_extraction(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx,
|
||||
const cbm_file_info_t *files, int file_count) {
|
||||
const cbm_file_info_t *files, int file_count,
|
||||
const cbm_file_hash_t *baseline_manifest, int baseline_count) {
|
||||
int rc = run_tests_and_history(p, ctx, files, file_count);
|
||||
if (rc != 0) {
|
||||
return rc;
|
||||
@@ -1469,12 +1812,20 @@ static int run_post_extraction(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx,
|
||||
run_predump_passes(p, ctx);
|
||||
CBM_PROF_END("pipeline", "3_predump_passes_total", t_predump);
|
||||
|
||||
if (!check_cancel(p)) {
|
||||
struct timespec t;
|
||||
CBM_PROF_START(t_dump);
|
||||
rc = dump_and_persist_hashes(p, files, file_count, &t);
|
||||
CBM_PROF_END("pipeline", "4_dump_and_persist", t_dump);
|
||||
#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API
|
||||
if (cbm_pipeline_persist_test_take_cancel_after_predump()) {
|
||||
atomic_store(&p->cancelled, 1);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (check_cancel(p)) {
|
||||
return CBM_PIPELINE_ABORT_PRESERVE_DB;
|
||||
}
|
||||
|
||||
struct timespec t;
|
||||
CBM_PROF_START(t_dump);
|
||||
rc = dump_and_persist_hashes(p, baseline_manifest, baseline_count, &t);
|
||||
CBM_PROF_END("pipeline", "4_dump_and_persist", t_dump);
|
||||
return rc;
|
||||
}
|
||||
|
||||
@@ -1514,6 +1865,17 @@ int cbm_pipeline_run(cbm_pipeline_t *p) {
|
||||
struct timespec t0;
|
||||
cbm_clock_gettime(CLOCK_MONOTONIC, &t0);
|
||||
cbm_path_alias_collection_t *path_aliases = NULL;
|
||||
cbm_file_hash_t *baseline_manifest = NULL;
|
||||
int baseline_count = 0;
|
||||
|
||||
promote_mode_to_existing_coverage(p);
|
||||
|
||||
/* cbm_pipeline_new() may precede the actual run by an arbitrary interval.
|
||||
* Refresh once here, then use this exact snapshot for both Branch graph
|
||||
* construction and the baseline semantic manifest. */
|
||||
if (pipeline_refresh_git_context(p) != 0) {
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
/* C/C++ #define Macro nodes (#375) dominate extraction on macro-dense repos
|
||||
* (≈49% of nodes on the Linux kernel), so gate them to full mode — moderate
|
||||
@@ -1560,11 +1922,24 @@ int cbm_pipeline_run(cbm_pipeline_t *p) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
/* Snapshot every semantic input once before routing/extraction. The same
|
||||
* bytes drive exact no-op comparison and are checked against a fresh
|
||||
* rediscovery immediately before any replacement is published. */
|
||||
rc = cbm_pipeline_build_semantic_manifest(p->project_name, p->repo_path, files, file_count,
|
||||
p->excluded_dirs, p->excluded_count, &p->git_ctx,
|
||||
p->userconfig, &baseline_manifest, &baseline_count);
|
||||
if (rc != 0) {
|
||||
rc = CBM_PIPELINE_ABORT_PRESERVE_DB;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
/* Check for existing DB → try incremental or delete for reindex */
|
||||
rc = try_incremental_or_delete_db(p, files, file_count);
|
||||
if (rc == CBM_PIPELINE_ABORT_PRESERVE_DB || rc >= 0) {
|
||||
cbm_discover_free(files, file_count);
|
||||
return rc;
|
||||
rc = try_incremental_or_delete_db(p, files, file_count, baseline_manifest, baseline_count);
|
||||
if (rc >= 0 || rc == CBM_PIPELINE_ABORT_PRESERVE_DB || rc == CBM_PIPELINE_PERSIST_FAILED) {
|
||||
goto cleanup;
|
||||
}
|
||||
if (rc != CBM_PIPELINE_FORCE_FULL_REINDEX) {
|
||||
goto cleanup;
|
||||
}
|
||||
cbm_log_info("pipeline.route", "path", "full");
|
||||
|
||||
@@ -1596,7 +1971,7 @@ int cbm_pipeline_run(cbm_pipeline_t *p) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
rc = run_post_extraction(p, &ctx, files, file_count);
|
||||
rc = run_post_extraction(p, &ctx, files, file_count, baseline_manifest, baseline_count);
|
||||
if (rc != 0) {
|
||||
goto cleanup;
|
||||
}
|
||||
@@ -1610,6 +1985,7 @@ cleanup:
|
||||
cbm_pkgmap_free(cbm_pipeline_get_pkgmap());
|
||||
cbm_pipeline_set_pkgmap(NULL);
|
||||
cbm_discover_free(files, file_count);
|
||||
cbm_pipeline_free_semantic_manifest(baseline_manifest, baseline_count);
|
||||
cbm_gbuf_free(p->gbuf);
|
||||
p->gbuf = NULL;
|
||||
cbm_registry_free(p->registry);
|
||||
|
||||
+777
-191
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,10 @@
|
||||
#include "pipeline/pipeline.h"
|
||||
#include "pipeline/path_alias.h"
|
||||
#include "graph_buffer/graph_buffer.h"
|
||||
#include "store/store.h"
|
||||
#include "discover/discover.h"
|
||||
#include "discover/userconfig.h"
|
||||
#include "git/git_context.h"
|
||||
#include "foundation/hash_table.h"
|
||||
#include "cbm.h"
|
||||
#include "lsp/go_lsp.h" /* CBMLSPDef for cbm_parallel_resolve cross-LSP inputs */
|
||||
@@ -30,6 +33,8 @@
|
||||
* Distinct from CBM_NOT_FOUND, which the orchestrator uses as the normal
|
||||
* "no incremental route; continue with a full index" sentinel. */
|
||||
#define CBM_PIPELINE_ABORT_PRESERVE_DB (-2)
|
||||
#define CBM_PIPELINE_FORCE_FULL_REINDEX (-3)
|
||||
#define CBM_PIPELINE_PERSIST_FAILED (-4)
|
||||
|
||||
/* Canonicalize route-path parameter placeholders (":id", "{id}", "<id>",
|
||||
* "${...}") to a single "{}" token so that client call sites and server
|
||||
@@ -125,6 +130,18 @@ typedef struct {
|
||||
const CBMReturnTypeTable *return_type_table;
|
||||
} cbm_pipeline_ctx_t;
|
||||
|
||||
/* Transcode an ObjectScript Studio Export XML file and compose every generated
|
||||
* UDL class into one cacheable result. The returned result owns all child
|
||||
* extraction arenas and is released with the ordinary cbm_free_result(). */
|
||||
CBMFileResult *cbm_pipeline_extract_objectscript_export(
|
||||
const char *source, int source_len, const char *project_name, const char *rel_path,
|
||||
const CBMMacroTable *macro_table, const CBMReturnTypeTable *return_type_table);
|
||||
|
||||
/* Materialize CONFIGURES edges from one extracted file's env-access carriers.
|
||||
* Shared by sequential definition processing and the parallel cache registry. */
|
||||
int cbm_pipeline_create_env_configures_for_file(cbm_pipeline_ctx_t *ctx,
|
||||
const CBMFileResult *result, const char *rel);
|
||||
|
||||
static inline int cbm_pipeline_relpath_is_excluded(const char *rel_path, char *const *excluded_dirs,
|
||||
int excluded_count) {
|
||||
if (!rel_path || rel_path[0] == '\0' || !excluded_dirs || excluded_count <= 0) {
|
||||
@@ -622,7 +639,56 @@ int cbm_scan_project_env_urls_excluded(const char *root_path, cbm_env_binding_t
|
||||
* Classifies files by mtime+size, deletes changed nodes, re-parses changed
|
||||
* files, merges into disk DB. Returns 0 on success. */
|
||||
int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_file_info_t *files,
|
||||
int file_count);
|
||||
int file_count, const cbm_file_hash_t *baseline_manifest,
|
||||
int baseline_count);
|
||||
|
||||
/* Exact semantic inputs for no-op/forced-full routing. The manifest contains
|
||||
* every discovered source plus repository controls actually consumed by
|
||||
* discovery, package mapping, path aliases, and extension overrides. */
|
||||
#define CBM_SEMANTIC_INPUT_PREFIX ".codebase-memory/.semantic-input/"
|
||||
#define CBM_SEMANTIC_INPUT_GIT_CONTEXT CBM_SEMANTIC_INPUT_PREFIX "git-context-v1"
|
||||
#define CBM_SEMANTIC_INPUT_GLOBAL_CONFIG CBM_SEMANTIC_INPUT_PREFIX "global-extension-config-v1"
|
||||
#define CBM_SEMANTIC_INPUT_PROJECT_CONFIG CBM_SEMANTIC_INPUT_PREFIX "project-extension-config-v1"
|
||||
|
||||
int cbm_pipeline_build_semantic_manifest(const char *project, const char *repo_path,
|
||||
const cbm_file_info_t *files, int file_count,
|
||||
char **excluded_dirs, int excluded_count,
|
||||
const cbm_git_context_t *git_ctx,
|
||||
const cbm_userconfig_t *userconfig, cbm_file_hash_t **out,
|
||||
int *out_count);
|
||||
void cbm_pipeline_free_semantic_manifest(cbm_file_hash_t *manifest, int count);
|
||||
bool cbm_pipeline_semantic_manifests_equal(const cbm_file_hash_t *left, int left_count,
|
||||
const cbm_file_hash_t *right, int right_count);
|
||||
/* Re-run discovery and hash its exact semantic inputs. Used at the publication
|
||||
* boundary so late additions/deletions cannot escape a frozen file list. */
|
||||
int cbm_pipeline_build_fresh_semantic_manifest(const char *project, const char *repo_path, int mode,
|
||||
cbm_file_hash_t **out, int *out_count);
|
||||
|
||||
/* Compatibility contract persisted in coverage metadata. Increment when a
|
||||
* graph/manifest semantic change makes prior exact-input indexes unsafe. */
|
||||
enum { CBM_SEMANTIC_INDEX_VERSION = 3 };
|
||||
|
||||
typedef struct {
|
||||
cbm_gbuf_t *gbuf;
|
||||
const char *final_db_path;
|
||||
const char *project;
|
||||
atomic_int *cancelled;
|
||||
const cbm_file_hash_t *manifest;
|
||||
int manifest_count;
|
||||
const char *adr_content;
|
||||
const cbm_coverage_row_t *coverage;
|
||||
int coverage_count;
|
||||
cbm_coverage_meta_t coverage_meta;
|
||||
} cbm_pipeline_generation_t;
|
||||
|
||||
/* Serialize and fully populate a sibling staging database, then atomically
|
||||
* replace final_db_path. The old generation is untouched on every failure or
|
||||
* cancellation observed before the rename commit point. */
|
||||
int cbm_pipeline_publish_generation(const cbm_pipeline_generation_t *generation);
|
||||
/* The SQLite generation is authoritative. An explicitly requested artifact is
|
||||
* part of the caller-visible operation and its export error is returned;
|
||||
* automatic refresh of an already-existing artifact remains best-effort. */
|
||||
int cbm_pipeline_refresh_artifact(cbm_pipeline_t *p, const char *db_path);
|
||||
|
||||
/* Pipeline accessors for incremental use */
|
||||
const char *cbm_pipeline_repo_path(const cbm_pipeline_t *p);
|
||||
@@ -645,4 +711,48 @@ bool extract_grpc_service_method(const char *callee, char *service, size_t srv_s
|
||||
long cbm_pp_bp_nap_cycles(void);
|
||||
void cbm_pp_bp_nap_cycles_reset(void);
|
||||
|
||||
/* Number of resolved-call rows handed to the parallel resolver's linear LSP
|
||||
* fallback since the last reset. Test observability for occurrence-index
|
||||
* coverage; deterministic and independent of wall-clock timing. */
|
||||
uint64_t cbm_pp_lsp_linear_fallback_rows(void);
|
||||
void cbm_pp_lsp_linear_fallback_rows_reset(void);
|
||||
|
||||
#if defined(CBM_CALL_REFERENCE_LOOKUP_TEST_API) && CBM_CALL_REFERENCE_LOOKUP_TEST_API
|
||||
/* Deterministic test-only operation count for the shared semantic-reference
|
||||
* matcher used by both sequential and fused-parallel usage materialization. */
|
||||
void cbm_pipeline_lsp_reference_lookup_test_reset(void);
|
||||
uint64_t cbm_pipeline_lsp_reference_lookup_test_rows_examined(void);
|
||||
#endif
|
||||
|
||||
#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API
|
||||
typedef enum {
|
||||
CBM_INCREMENTAL_ROUTE_NONE = 0,
|
||||
CBM_INCREMENTAL_ROUTE_NOOP,
|
||||
CBM_INCREMENTAL_ROUTE_FORCED_FULL,
|
||||
CBM_INCREMENTAL_ROUTE_LEGACY_PARTIAL,
|
||||
} cbm_incremental_route_t;
|
||||
|
||||
/* Deterministic one-shot fault injection for the incremental-parallel result
|
||||
* cache allocation. Reset explicitly so one test cannot affect another. */
|
||||
void cbm_pipeline_incremental_test_fail_result_cache_alloc_once(void);
|
||||
void cbm_pipeline_incremental_test_force_legacy_partial_once(void);
|
||||
void cbm_pipeline_incremental_test_fail_after_stage_dump_once(void);
|
||||
void cbm_pipeline_incremental_test_cancel_after_predump_once(void);
|
||||
void cbm_pipeline_incremental_test_cancel_after_destination_prepare_once(void);
|
||||
void cbm_pipeline_incremental_test_fail_adr_capture_once(void);
|
||||
typedef void (*cbm_pipeline_test_hook_fn)(void *userdata);
|
||||
void cbm_pipeline_incremental_test_before_final_manifest_once(cbm_pipeline_test_hook_fn hook,
|
||||
void *userdata);
|
||||
cbm_incremental_route_t cbm_pipeline_incremental_test_last_route(void);
|
||||
void cbm_pipeline_incremental_test_reset_faults(void);
|
||||
|
||||
/* Shared persistence-hook plumbing. Tests use the incremental facade above so
|
||||
* one reset covers route, extraction, and publication faults. */
|
||||
bool cbm_pipeline_persist_test_take_failure_after_stage_dump(void);
|
||||
bool cbm_pipeline_persist_test_take_cancel_after_predump(void);
|
||||
bool cbm_pipeline_persist_test_take_cancel_after_destination_prepare(void);
|
||||
void cbm_pipeline_persist_test_run_before_final_manifest(void);
|
||||
void cbm_pipeline_persist_test_reset_faults(void);
|
||||
#endif
|
||||
|
||||
#endif /* CBM_PIPELINE_INTERNAL_H */
|
||||
|
||||
+207
-21
@@ -437,15 +437,17 @@ static int configure_pragmas(cbm_store_t *s, bool in_memory, bool read_only) {
|
||||
return rc;
|
||||
}
|
||||
/* #1083: bound the WAL file so a checkpoint-starved log is physically
|
||||
* reclaimed the next time a checkpoint can reset it. Our checkpoints are
|
||||
* all PASSIVE (they never ftruncate — see cbm_store_checkpoint's SIGBUS
|
||||
* note), so without a size limit the -wal file only ever grows; a
|
||||
* journal_size_limit truncates it back to N bytes on the next successful
|
||||
* reclaimed the next time a checkpoint can reset it. Shared/live
|
||||
* checkpoints are PASSIVE (they never ftruncate — see
|
||||
* cbm_store_checkpoint's SIGBUS note), so without a size limit the -wal
|
||||
* file only ever grows; journal_size_limit truncates it back to N bytes
|
||||
* on the next successful
|
||||
* reset. N is far above the healthy WAL (~4 MiB under the default
|
||||
* 1000-page autocheckpoint), so normal indexing never triggers
|
||||
* truncate/regrow churn — it only fires after abnormal growth. We do NOT
|
||||
* use a TRUNCATE checkpoint: its ftruncate(fd,0) can raise SIGBUS in a
|
||||
* sibling process that has the DB mmap'd on macOS. */
|
||||
* truncate/regrow churn — it only fires after abnormal growth.
|
||||
* Shared/live paths do NOT use a TRUNCATE checkpoint: truncating the WAL
|
||||
* to zero can raise SIGBUS in a sibling process that has the DB mmap'd
|
||||
* on macOS. Exclusive staging publication seals separately below. */
|
||||
rc = exec_sql(s, "PRAGMA journal_size_limit = 268435456;"); /* 256 MiB */
|
||||
if (rc != CBM_STORE_OK) {
|
||||
return rc;
|
||||
@@ -1135,6 +1137,118 @@ int64_t cbm_store_journal_size_limit(cbm_store_t *s) {
|
||||
return limit;
|
||||
}
|
||||
|
||||
int cbm_store_seal_for_atomic_publish(cbm_store_t *s) {
|
||||
if (!s || !s->db) {
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
|
||||
/* Bulk indexing deliberately relaxes synchronous writes. Restore the
|
||||
* strongest durability before copying WAL pages into the database file.
|
||||
* This staging connection is exclusively owned by the publisher. */
|
||||
if (exec_sql(s, "PRAGMA synchronous = FULL;") != CBM_STORE_OK) {
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
|
||||
int log_frames = 0;
|
||||
int checkpointed_frames = 0;
|
||||
int rc = sqlite3_wal_checkpoint_v2(s->db, NULL, SQLITE_CHECKPOINT_TRUNCATE, &log_frames,
|
||||
&checkpointed_frames);
|
||||
if (rc != SQLITE_OK) {
|
||||
snprintf(s->errbuf, sizeof(s->errbuf), "seal checkpoint: %s (log=%d checkpointed=%d)",
|
||||
sqlite3_errstr(rc), log_frames, checkpointed_frames);
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
|
||||
/* PRAGMA journal_mode returns the mode SQLite actually entered. sqlite3_exec
|
||||
* would discard that result and could falsely report a successful seal. */
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
rc = sqlite3_prepare_v2(s->db, "PRAGMA journal_mode = DELETE;", CBM_NOT_FOUND, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
store_set_error_sqlite(s, "seal journal_mode prepare");
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
|
||||
rc = sqlite3_step(stmt);
|
||||
if (rc != SQLITE_ROW) {
|
||||
store_set_error_sqlite(s, "seal journal_mode step");
|
||||
sqlite3_finalize(stmt);
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
const char *mode = (const char *)sqlite3_column_text(stmt, 0);
|
||||
bool is_delete = mode && sqlite3_stricmp(mode, "delete") == 0;
|
||||
|
||||
rc = sqlite3_step(stmt);
|
||||
if (rc != SQLITE_DONE) {
|
||||
store_set_error_sqlite(s, "seal journal_mode completion");
|
||||
sqlite3_finalize(stmt);
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
rc = sqlite3_finalize(stmt);
|
||||
if (rc != SQLITE_OK) {
|
||||
store_set_error_sqlite(s, "seal journal_mode finalize");
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
if (!is_delete) {
|
||||
store_set_error(s, "seal journal_mode did not enter delete mode");
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
|
||||
return CBM_STORE_OK;
|
||||
}
|
||||
|
||||
int cbm_store_seal_existing_path_for_replace(const char *db_path) {
|
||||
if (!db_path || !db_path[0]) {
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
|
||||
cbm_store_t maintenance = {0};
|
||||
int rc = sqlite3_open_v2(db_path, &maintenance.db, SQLITE_OPEN_READWRITE, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
int primary = rc & 0xff;
|
||||
sqlite3_close(maintenance.db);
|
||||
return primary == SQLITE_CORRUPT || primary == SQLITE_NOTADB || primary == SQLITE_FORMAT
|
||||
? CBM_STORE_NOT_FOUND
|
||||
: CBM_STORE_ERR;
|
||||
}
|
||||
(void)sqlite3_busy_timeout(maintenance.db, 10000);
|
||||
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
rc = sqlite3_prepare_v2(maintenance.db, "PRAGMA quick_check(1);", CBM_NOT_FOUND, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
int primary = rc & 0xff;
|
||||
sqlite3_close(maintenance.db);
|
||||
return primary == SQLITE_BUSY || primary == SQLITE_LOCKED || primary == SQLITE_READONLY ||
|
||||
primary == SQLITE_CANTOPEN || primary == SQLITE_IOERR
|
||||
? CBM_STORE_ERR
|
||||
: CBM_STORE_NOT_FOUND;
|
||||
}
|
||||
rc = sqlite3_step(stmt);
|
||||
const char *result = rc == SQLITE_ROW ? (const char *)sqlite3_column_text(stmt, 0) : NULL;
|
||||
bool structurally_valid = result && strcmp(result, "ok") == 0;
|
||||
int finalize_rc = sqlite3_finalize(stmt);
|
||||
if (!structurally_valid || finalize_rc != SQLITE_OK) {
|
||||
int primary = rc & 0xff;
|
||||
sqlite3_close(maintenance.db);
|
||||
return primary == SQLITE_BUSY || primary == SQLITE_LOCKED || primary == SQLITE_READONLY ||
|
||||
primary == SQLITE_CANTOPEN || primary == SQLITE_IOERR
|
||||
? CBM_STORE_ERR
|
||||
: CBM_STORE_NOT_FOUND;
|
||||
}
|
||||
|
||||
/* A structurally valid SQLite file that is not recognizably a codebase
|
||||
* graph is incompatible at this destination and must be preserved before
|
||||
* replacement. Legacy graph schemas still pass this shallow identity
|
||||
* check and can be sealed without running init_schema(). */
|
||||
if (!cbm_store_check_integrity(&maintenance)) {
|
||||
sqlite3_close(maintenance.db);
|
||||
return CBM_STORE_NOT_FOUND;
|
||||
}
|
||||
|
||||
int seal_rc = cbm_store_seal_for_atomic_publish(&maintenance);
|
||||
sqlite3_close(maintenance.db);
|
||||
return seal_rc;
|
||||
}
|
||||
|
||||
/* ── Dump ───────────────────────────────────────────────────────── */
|
||||
|
||||
/* Dump entire in-memory database to a file via sqlite3_backup.
|
||||
@@ -1288,8 +1402,15 @@ int cbm_store_get_project(cbm_store_t *s, const char *name, cbm_project_t *out)
|
||||
out->name = heap_strdup((const char *)sqlite3_column_text(stmt, 0));
|
||||
out->indexed_at = heap_strdup((const char *)sqlite3_column_text(stmt, SKIP_ONE));
|
||||
out->root_path = heap_strdup((const char *)sqlite3_column_text(stmt, CBM_SZ_2));
|
||||
/* Cached SELECT statements must not remain parked on SQLITE_ROW.
|
||||
* Besides retaining a read transaction, an active reader prevents
|
||||
* publication from switching a fully-written staging database out of
|
||||
* WAL mode before the atomic rename. All result text is owned by the
|
||||
* caller at this point, so release the reader immediately. */
|
||||
sqlite3_reset(stmt);
|
||||
return CBM_STORE_OK;
|
||||
}
|
||||
sqlite3_reset(stmt);
|
||||
return CBM_STORE_NOT_FOUND;
|
||||
}
|
||||
|
||||
@@ -1438,8 +1559,10 @@ int cbm_store_find_node_by_id(cbm_store_t *s, int64_t id, cbm_node_t *out) {
|
||||
int rc = sqlite3_step(stmt);
|
||||
if (rc == SQLITE_ROW) {
|
||||
scan_node(stmt, out);
|
||||
sqlite3_reset(stmt);
|
||||
return CBM_STORE_OK;
|
||||
}
|
||||
sqlite3_reset(stmt);
|
||||
return CBM_STORE_NOT_FOUND;
|
||||
}
|
||||
|
||||
@@ -1462,8 +1585,10 @@ int cbm_store_find_node_by_qn(cbm_store_t *s, const char *project, const char *q
|
||||
int rc = sqlite3_step(stmt);
|
||||
if (rc == SQLITE_ROW) {
|
||||
scan_node(stmt, out);
|
||||
sqlite3_reset(stmt);
|
||||
return CBM_STORE_OK;
|
||||
}
|
||||
sqlite3_reset(stmt);
|
||||
return CBM_STORE_NOT_FOUND;
|
||||
}
|
||||
|
||||
@@ -1484,8 +1609,10 @@ int cbm_store_find_node_by_qn_any(cbm_store_t *s, const char *qn, cbm_node_t *ou
|
||||
int rc = sqlite3_step(stmt);
|
||||
if (rc == SQLITE_ROW) {
|
||||
scan_node(stmt, out);
|
||||
sqlite3_reset(stmt);
|
||||
return CBM_STORE_OK;
|
||||
}
|
||||
sqlite3_reset(stmt);
|
||||
return CBM_STORE_NOT_FOUND;
|
||||
}
|
||||
|
||||
@@ -1654,10 +1781,12 @@ int cbm_store_count_nodes(cbm_store_t *s, const char *project) {
|
||||
}
|
||||
|
||||
bind_text(stmt, SKIP_ONE, project);
|
||||
int count = 0;
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
return sqlite3_column_int(stmt, 0);
|
||||
count = sqlite3_column_int(stmt, 0);
|
||||
}
|
||||
return 0;
|
||||
sqlite3_reset(stmt);
|
||||
return count;
|
||||
}
|
||||
|
||||
int cbm_store_delete_nodes_by_project(cbm_store_t *s, const char *project) {
|
||||
@@ -1972,10 +2101,12 @@ int cbm_store_count_edges(cbm_store_t *s, const char *project) {
|
||||
}
|
||||
|
||||
bind_text(stmt, SKIP_ONE, project);
|
||||
int count = 0;
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
return sqlite3_column_int(stmt, 0);
|
||||
count = sqlite3_column_int(stmt, 0);
|
||||
}
|
||||
return 0;
|
||||
sqlite3_reset(stmt);
|
||||
return count;
|
||||
}
|
||||
|
||||
int cbm_store_count_edges_by_type(cbm_store_t *s, const char *project, const char *type) {
|
||||
@@ -1988,10 +2119,12 @@ int cbm_store_count_edges_by_type(cbm_store_t *s, const char *project, const cha
|
||||
|
||||
bind_text(stmt, SKIP_ONE, project);
|
||||
bind_text(stmt, ST_COL_2, type);
|
||||
int count = 0;
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
return sqlite3_column_int(stmt, 0);
|
||||
count = sqlite3_column_int(stmt, 0);
|
||||
}
|
||||
return 0;
|
||||
sqlite3_reset(stmt);
|
||||
return count;
|
||||
}
|
||||
|
||||
int cbm_store_delete_edges_by_project(cbm_store_t *s, const char *project) {
|
||||
@@ -3552,9 +3685,11 @@ int cbm_store_search(cbm_store_t *s, const cbm_search_params_t *params, cbm_sear
|
||||
const char *select_cols = "SELECT n.id, n.project, n.label, n.name, n.qualified_name, "
|
||||
"n.file_path, n.start_line, n.end_line, n.properties, "
|
||||
"(SELECT COUNT(*) FROM edges e WHERE e.target_id = n.id AND "
|
||||
"e.type IN ('CALLS', 'USAGE', 'INHERITS', 'IMPLEMENTS')) AS in_deg, "
|
||||
"e.type IN ('CALLS', 'USAGE', 'CALL_REFERENCE', 'INHERITS', "
|
||||
"'IMPLEMENTS')) AS in_deg, "
|
||||
"(SELECT COUNT(*) FROM edges e WHERE e.source_id = n.id AND "
|
||||
"e.type IN ('CALLS', 'USAGE', 'INHERITS', 'IMPLEMENTS')) AS out_deg ";
|
||||
"e.type IN ('CALLS', 'USAGE', 'CALL_REFERENCE', 'INHERITS', "
|
||||
"'IMPLEMENTS')) AS out_deg ";
|
||||
|
||||
char where[CBM_SZ_2K] = "";
|
||||
search_bind_t binds[ST_SEARCH_MAX_BINDS];
|
||||
@@ -7174,6 +7309,33 @@ int cbm_store_adr_store(cbm_store_t *s, const char *project, const char *content
|
||||
}
|
||||
|
||||
int cbm_store_adr_get(cbm_store_t *s, const char *project, cbm_adr_t *out) {
|
||||
if (!s || !s->db || !project || !out) {
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
memset(out, 0, sizeof(*out));
|
||||
|
||||
/* ADR storage was added after the original graph schema. A readable
|
||||
* legacy generation without project_summaries has no ADR to preserve; it
|
||||
* is not a read failure and must remain replaceable by a full reindex. */
|
||||
sqlite3_stmt *table_stmt = NULL;
|
||||
int rc = sqlite3_prepare_v2(
|
||||
s->db,
|
||||
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='project_summaries' LIMIT 1;",
|
||||
CBM_NOT_FOUND, &table_stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
store_set_error_sqlite(s, "adr_get table probe");
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
rc = sqlite3_step(table_stmt);
|
||||
if (rc == SQLITE_DONE) {
|
||||
sqlite3_finalize(table_stmt);
|
||||
return CBM_STORE_NOT_FOUND;
|
||||
}
|
||||
if (rc != SQLITE_ROW || sqlite3_finalize(table_stmt) != SQLITE_OK) {
|
||||
store_set_error_sqlite(s, "adr_get table probe step");
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
|
||||
const char *sql = "SELECT project, summary, created_at, updated_at FROM project_summaries "
|
||||
"WHERE project=?1";
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
@@ -7181,18 +7343,42 @@ int cbm_store_adr_get(cbm_store_t *s, const char *project, cbm_adr_t *out) {
|
||||
store_set_error_sqlite(s, "adr_get");
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
bind_text(stmt, SKIP_ONE, project);
|
||||
int rc = sqlite3_step(stmt);
|
||||
if (rc != SQLITE_ROW) {
|
||||
if (bind_text(stmt, SKIP_ONE, project) != SQLITE_OK) {
|
||||
store_set_error_sqlite(s, "adr_get bind");
|
||||
sqlite3_finalize(stmt);
|
||||
store_set_error(s, "no ADR found");
|
||||
return CBM_STORE_NOT_FOUND;
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
rc = sqlite3_step(stmt);
|
||||
if (rc != SQLITE_ROW) {
|
||||
if (rc != SQLITE_DONE) {
|
||||
store_set_error_sqlite(s, "adr_get step");
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
if (rc == SQLITE_DONE) {
|
||||
store_set_error(s, "no ADR found");
|
||||
return CBM_STORE_NOT_FOUND;
|
||||
}
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
out->project = heap_strdup((const char *)sqlite3_column_text(stmt, 0));
|
||||
out->content = heap_strdup((const char *)sqlite3_column_text(stmt, SKIP_ONE));
|
||||
out->created_at = heap_strdup((const char *)sqlite3_column_text(stmt, CBM_SZ_2));
|
||||
out->updated_at = heap_strdup((const char *)sqlite3_column_text(stmt, CBM_SZ_3));
|
||||
sqlite3_finalize(stmt);
|
||||
rc = sqlite3_finalize(stmt);
|
||||
if (rc != SQLITE_OK) {
|
||||
cbm_store_adr_free(out);
|
||||
store_set_error_sqlite(s, "adr_get finalize");
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
|
||||
/* Every selected column is NOT NULL in the schema. A NULL here therefore
|
||||
* means either allocation failure or corrupt data; never return a partial
|
||||
* ADR that a full rebuild could silently drop during publication. */
|
||||
if (!out->project || !out->content || !out->created_at || !out->updated_at) {
|
||||
cbm_store_adr_free(out);
|
||||
store_set_error(s, "adr_get: failed to copy complete ADR");
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
return CBM_STORE_OK;
|
||||
}
|
||||
|
||||
|
||||
@@ -207,6 +207,12 @@ cbm_store_t *cbm_store_open_path(const char *db_path);
|
||||
* exist — never creates a new .db file. */
|
||||
cbm_store_t *cbm_store_open_path_query(const char *db_path);
|
||||
|
||||
/* Validate and seal an existing DB for atomic replacement without creating or
|
||||
* migrating its schema. Returns OK when sealed, NOT_FOUND when the bytes are
|
||||
* definitely corrupt/incompatible and should be quarantined, or ERR when the
|
||||
* file is busy/unavailable and must be left in place. */
|
||||
int cbm_store_seal_existing_path_for_replace(const char *db_path);
|
||||
|
||||
/* On-disk path of a file-backed store, or NULL for an in-memory (:memory:)
|
||||
* store. The returned pointer is owned by the store. */
|
||||
const char *cbm_store_db_path(const cbm_store_t *s);
|
||||
@@ -271,6 +277,14 @@ int64_t cbm_store_journal_size_limit(cbm_store_t *s);
|
||||
* bumps on every index run. "legacy" for DBs predating store_meta. */
|
||||
int cbm_store_generation(cbm_store_t *s, char *buf, size_t bufsz);
|
||||
|
||||
/* Seal a fully-written staging database before atomic publication.
|
||||
* Raises synchronous to FULL, requires an exclusive TRUNCATE checkpoint to
|
||||
* complete, then leaves the database in verified DELETE journal mode so the
|
||||
* main file is self-contained (no required -wal/-shm sidecars). This is a
|
||||
* fail-closed operation: SQLITE_BUSY and an unconfirmed mode transition are
|
||||
* errors. The caller must own the staging database exclusively. */
|
||||
int cbm_store_seal_for_atomic_publish(cbm_store_t *s);
|
||||
|
||||
/* Resolve the mmap_size pragma value applied to on-disk stores from the
|
||||
* CBM_SQLITE_MMAP_SIZE environment variable. Defaults to 67108864 (64 MB)
|
||||
* when the variable is unset, malformed, or partially numeric. Negative
|
||||
|
||||
+16
-7
@@ -650,16 +650,24 @@ cbm_layout_result_t *cbm_layout_compute(cbm_store_t *store, const char *project,
|
||||
int64_t *node_ids = malloc((size_t)n * sizeof(int64_t));
|
||||
int *in_calls = calloc((size_t)n, sizeof(int));
|
||||
int *in_usage = calloc((size_t)n, sizeof(int));
|
||||
int *in_call_reference = calloc((size_t)n, sizeof(int));
|
||||
int *deg_dummy = calloc((size_t)n, sizeof(int));
|
||||
if (node_ids && in_calls && in_usage && deg_dummy) {
|
||||
bool dead_degrees_valid = node_ids && in_calls && in_usage && in_call_reference && deg_dummy;
|
||||
if (dead_degrees_valid) {
|
||||
for (int i = 0; i < n; i++)
|
||||
node_ids[i] = search_out.results[i].node.id;
|
||||
for (int off = 0; off < n; off += DEAD_DEGREE_CHUNK) {
|
||||
int cnt = (n - off < DEAD_DEGREE_CHUNK) ? (n - off) : DEAD_DEGREE_CHUNK;
|
||||
cbm_store_batch_count_degrees(store, node_ids + off, cnt, "CALLS", in_calls + off,
|
||||
deg_dummy + off);
|
||||
cbm_store_batch_count_degrees(store, node_ids + off, cnt, "USAGE", in_usage + off,
|
||||
deg_dummy + off);
|
||||
if (cbm_store_batch_count_degrees(store, node_ids + off, cnt, "CALLS", in_calls + off,
|
||||
deg_dummy + off) != CBM_STORE_OK ||
|
||||
cbm_store_batch_count_degrees(store, node_ids + off, cnt, "USAGE", in_usage + off,
|
||||
deg_dummy + off) != CBM_STORE_OK ||
|
||||
cbm_store_batch_count_degrees(store, node_ids + off, cnt, "CALL_REFERENCE",
|
||||
in_call_reference + off,
|
||||
deg_dummy + off) != CBM_STORE_OK) {
|
||||
dead_degrees_valid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -720,8 +728,8 @@ cbm_layout_result_t *cbm_layout_compute(cbm_store_t *store, const char *project,
|
||||
bool is_fn =
|
||||
sn->label && (strcmp(sn->label, "Function") == 0 || strcmp(sn->label, "Method") == 0);
|
||||
bool testish = nf.is_test || (sn->file_path && cbm_is_test_file_path(sn->file_path));
|
||||
int ic = in_calls ? in_calls[i] : 1;
|
||||
int iu = in_usage ? in_usage[i] : 1;
|
||||
int ic = dead_degrees_valid ? in_calls[i] : 1;
|
||||
int iu = dead_degrees_valid ? in_usage[i] + in_call_reference[i] : 1;
|
||||
const char *status;
|
||||
if (!is_fn)
|
||||
status = "structural";
|
||||
@@ -770,6 +778,7 @@ cbm_layout_result_t *cbm_layout_compute(cbm_store_t *store, const char *project,
|
||||
free(node_ids);
|
||||
free(in_calls);
|
||||
free(in_usage);
|
||||
free(in_call_reference);
|
||||
free(deg_dummy);
|
||||
free_edge_array(all_edges, mapped);
|
||||
cbm_store_search_free(&search_out);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,984 @@
|
||||
/*
|
||||
* repro_call_node_behaviors.c — raw CALL/USAGE ownership for overloaded and
|
||||
* call-shaped expressions.
|
||||
*
|
||||
* C++ and CUDA deliberately register several expression containers as call
|
||||
* nodes. A container may own one synthetic operator call, one semantic-only
|
||||
* implicit-call candidate, or no call at all; none of those roles allows it to erase
|
||||
* operand references. Scala and Elixir operator syntax follows the same rule.
|
||||
* Puppet resource declarations belong to the DSL extractor and must not be
|
||||
* reinterpreted as generic code calls.
|
||||
*
|
||||
* Every fixture has exactly one target AST node. Every raw count is exact and
|
||||
* joined to the exact qualified name of its caller (or exact module QN for the
|
||||
* top-level Puppet DSL case), so duplicate emissions and scope drift stay RED.
|
||||
*/
|
||||
#include "test_framework.h"
|
||||
#include "cbm.h"
|
||||
#include "lang_specs.h"
|
||||
#include "macro_table.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
const char *tag;
|
||||
CBMLanguage language;
|
||||
const char *filename;
|
||||
const char *source;
|
||||
const char *ast_kind;
|
||||
const char *reason;
|
||||
} BehaviorCase;
|
||||
|
||||
static int terminal_name_matches(const char *raw, const char *expected) {
|
||||
if (!raw || !expected)
|
||||
return 0;
|
||||
if (strcmp(raw, expected) == 0)
|
||||
return 1;
|
||||
|
||||
size_t raw_len = strlen(raw);
|
||||
size_t expected_len = strlen(expected);
|
||||
if (raw_len < expected_len)
|
||||
return 0;
|
||||
const char *tail = raw + raw_len - expected_len;
|
||||
if (strcmp(tail, expected) != 0)
|
||||
return 0;
|
||||
if (tail == raw)
|
||||
return 1;
|
||||
|
||||
unsigned char boundary = (unsigned char)tail[-1];
|
||||
return !isalnum(boundary) && boundary != '_';
|
||||
}
|
||||
|
||||
static void check_exact(const BehaviorCase *test_case, const char *invariant, int actual,
|
||||
int expected, int *failures) {
|
||||
if (actual == expected)
|
||||
return;
|
||||
fprintf(stderr, " [call-node-behavior] case=%s invariant=%s expected=%d actual=%d reason=%s\n",
|
||||
test_case->tag, invariant, expected, actual, test_case->reason);
|
||||
(*failures)++;
|
||||
}
|
||||
|
||||
static int ast_kind_count(TSNode node, const char *kind) {
|
||||
int count = strcmp(ts_node_type(node), kind) == 0 ? 1 : 0;
|
||||
uint32_t child_count = ts_node_child_count(node);
|
||||
for (uint32_t i = 0; i < child_count; i++)
|
||||
count += ast_kind_count(ts_node_child(node, i), kind);
|
||||
return count;
|
||||
}
|
||||
|
||||
static void check_exact_ast_kind(const BehaviorCase *test_case, int *failures) {
|
||||
const TSLanguage *language = cbm_ts_language(test_case->language);
|
||||
TSParser *parser = ts_parser_new();
|
||||
if (!language || !parser || !ts_parser_set_language(parser, language)) {
|
||||
fprintf(stderr,
|
||||
" [call-node-behavior] case=%s invariant=ast_parser_ready expected=1 "
|
||||
"actual=0 reason=%s\n",
|
||||
test_case->tag, test_case->reason);
|
||||
(*failures)++;
|
||||
if (parser)
|
||||
ts_parser_delete(parser);
|
||||
return;
|
||||
}
|
||||
|
||||
TSTree *tree = ts_parser_parse_string(parser, NULL, test_case->source,
|
||||
(uint32_t)strlen(test_case->source));
|
||||
int count = 0;
|
||||
int parse_error = 1;
|
||||
if (tree) {
|
||||
TSNode root = ts_tree_root_node(tree);
|
||||
parse_error = ts_node_has_error(root) ? 1 : 0;
|
||||
count = ast_kind_count(root, test_case->ast_kind);
|
||||
}
|
||||
check_exact(test_case, "ast_clean_parse", parse_error, 0, failures);
|
||||
check_exact(test_case, "exact_ast_kind_count", count, 1, failures);
|
||||
if (tree)
|
||||
ts_tree_delete(tree);
|
||||
ts_parser_delete(parser);
|
||||
}
|
||||
|
||||
static CBMFileResult *extract_case_with_macros(const BehaviorCase *test_case,
|
||||
const CBMMacroTable *macro_table, int *failures) {
|
||||
CBMFileResult *result =
|
||||
cbm_extract_file_ex(test_case->source, (int)strlen(test_case->source), test_case->language,
|
||||
"repro", test_case->filename, 0, NULL, NULL, macro_table, NULL);
|
||||
if (!result) {
|
||||
fprintf(stderr,
|
||||
" [call-node-behavior] case=%s invariant=extract_result expected=non-null "
|
||||
"reason=%s\n",
|
||||
test_case->tag, test_case->reason);
|
||||
(*failures)++;
|
||||
return NULL;
|
||||
}
|
||||
check_exact(test_case, "clean_parse", (result->has_error || result->parse_incomplete) ? 1 : 0,
|
||||
0, failures);
|
||||
return result;
|
||||
}
|
||||
|
||||
static CBMFileResult *extract_case(const BehaviorCase *test_case, int *failures) {
|
||||
return extract_case_with_macros(test_case, NULL, failures);
|
||||
}
|
||||
|
||||
static int call_count_exact(const CBMFileResult *result, const char *caller_qn,
|
||||
const char *callee) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < result->calls.count; i++) {
|
||||
const CBMCall *call = &result->calls.items[i];
|
||||
if (call->callee_name && call->enclosing_func_qn &&
|
||||
strcmp(call->callee_name, callee) == 0 &&
|
||||
strcmp(call->enclosing_func_qn, caller_qn) == 0)
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static int usage_count_exact(const CBMFileResult *result, const char *caller_qn,
|
||||
const char *reference) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < result->usages.count; i++) {
|
||||
const CBMUsage *usage = &result->usages.items[i];
|
||||
if (usage->ref_name && usage->enclosing_func_qn &&
|
||||
strcmp(usage->ref_name, reference) == 0 &&
|
||||
strcmp(usage->enclosing_func_qn, caller_qn) == 0)
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static int value_usage_count_exact(const CBMFileResult *result, const char *caller_qn,
|
||||
const char *reference) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < result->usages.count; i++) {
|
||||
const CBMUsage *usage = &result->usages.items[i];
|
||||
if (usage->kind == CBM_USAGE_VALUE && usage->ref_name && usage->enclosing_func_qn &&
|
||||
strcmp(usage->ref_name, reference) == 0 &&
|
||||
strcmp(usage->enclosing_func_qn, caller_qn) == 0) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static int callable_definition_count_exact(const CBMFileResult *result, const char *name,
|
||||
const char *qualified_name) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < result->defs.count; i++) {
|
||||
const CBMDefinition *definition = &result->defs.items[i];
|
||||
int callable_label = definition->label && (strcmp(definition->label, "Function") == 0 ||
|
||||
strcmp(definition->label, "Method") == 0);
|
||||
if (callable_label && definition->name && definition->qualified_name &&
|
||||
strcmp(definition->name, name) == 0 &&
|
||||
strcmp(definition->qualified_name, qualified_name) == 0) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static int call_count_in_routine(const CBMFileResult *result, const char *caller,
|
||||
const char *callee) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < result->calls.count; i++) {
|
||||
const CBMCall *call = &result->calls.items[i];
|
||||
if (call->callee_name && strcmp(call->callee_name, callee) == 0 &&
|
||||
terminal_name_matches(call->enclosing_func_qn, caller))
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static int exact_semantic_candidate_count_in_routine(const BehaviorCase *test_case,
|
||||
const CBMFileResult *result,
|
||||
const char *caller, const char *callee,
|
||||
const char *site_text) {
|
||||
int count = 0;
|
||||
size_t source_len = strlen(test_case->source);
|
||||
size_t site_len = strlen(site_text);
|
||||
for (int i = 0; i < result->calls.count; i++) {
|
||||
const CBMCall *call = &result->calls.items[i];
|
||||
if (!call->requires_lsp_resolution || !call->callee_name ||
|
||||
strcmp(call->callee_name, callee) != 0 ||
|
||||
!terminal_name_matches(call->enclosing_func_qn, caller) ||
|
||||
call->site_end_byte <= call->site_start_byte || call->site_end_byte > source_len ||
|
||||
(size_t)(call->site_end_byte - call->site_start_byte) != site_len ||
|
||||
strncmp(test_case->source + call->site_start_byte, site_text, site_len) != 0) {
|
||||
continue;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static int usage_count_in_routine(const CBMFileResult *result, const char *caller,
|
||||
const char *reference) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < result->usages.count; i++) {
|
||||
const CBMUsage *usage = &result->usages.items[i];
|
||||
if (usage->kind == CBM_USAGE_VALUE && usage->ref_name &&
|
||||
strcmp(usage->ref_name, reference) == 0 &&
|
||||
terminal_name_matches(usage->enclosing_func_qn, caller))
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static int callable_reference_count_in_routine(const CBMFileResult *result, const char *caller,
|
||||
const char *reference) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < result->usages.count; i++) {
|
||||
const CBMUsage *usage = &result->usages.items[i];
|
||||
if (usage->kind == CBM_USAGE_CALL_REFERENCE && usage->ref_name &&
|
||||
strcmp(usage->ref_name, reference) == 0 &&
|
||||
terminal_name_matches(usage->enclosing_func_qn, caller))
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static int semantic_reference_candidate_count_in_routine(const CBMFileResult *result,
|
||||
const char *caller,
|
||||
const char *reference) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < result->usages.count; i++) {
|
||||
const CBMUsage *usage = &result->usages.items[i];
|
||||
if (usage->kind == CBM_USAGE_VALUE && usage->may_be_call_reference && usage->ref_name &&
|
||||
strcmp(usage->ref_name, reference) == 0 &&
|
||||
terminal_name_matches(usage->enclosing_func_qn, caller)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static int resolved_call_count_in_routine(const CBMFileResult *result, const char *caller,
|
||||
const char *callee) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < result->resolved_calls.count; i++) {
|
||||
const CBMResolvedCall *call = &result->resolved_calls.items[i];
|
||||
if (call->kind == CBM_RESOLVED_INVOCATION &&
|
||||
terminal_name_matches(call->caller_qn, caller) &&
|
||||
terminal_name_matches(call->callee_qn, callee)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static int resolved_call_total_in_routine(const CBMFileResult *result, const char *caller) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < result->resolved_calls.count; i++) {
|
||||
if (result->resolved_calls.items[i].kind == CBM_RESOLVED_INVOCATION &&
|
||||
terminal_name_matches(result->resolved_calls.items[i].caller_qn, caller)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static int resolved_reference_count_in_routine(const CBMFileResult *result, const char *caller,
|
||||
const char *callee) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < result->resolved_calls.count; i++) {
|
||||
const CBMResolvedCall *reference = &result->resolved_calls.items[i];
|
||||
if (reference->kind == CBM_RESOLVED_CALL_REFERENCE &&
|
||||
terminal_name_matches(reference->caller_qn, caller) &&
|
||||
terminal_name_matches(reference->callee_qn, callee)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static void check_caller_definition(const BehaviorCase *test_case, const CBMFileResult *result,
|
||||
const char *caller, int *failures) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < result->defs.count; i++) {
|
||||
const CBMDefinition *definition = &result->defs.items[i];
|
||||
int callable_label = definition->label && (strcmp(definition->label, "Function") == 0 ||
|
||||
strcmp(definition->label, "Method") == 0);
|
||||
if (callable_label && definition->name && strcmp(definition->name, caller) == 0 &&
|
||||
terminal_name_matches(definition->qualified_name, caller)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
check_exact(test_case, "caller_callable_definition", count, 1, failures);
|
||||
}
|
||||
|
||||
static const char *checked_module_qn(const BehaviorCase *test_case, const CBMFileResult *result,
|
||||
int *failures) {
|
||||
if (result->module_qn && result->module_qn[0])
|
||||
return result->module_qn;
|
||||
fprintf(stderr,
|
||||
" [call-node-behavior] case=%s invariant=module_qn expected=non-empty reason=%s\n",
|
||||
test_case->tag, test_case->reason);
|
||||
(*failures)++;
|
||||
return "<missing-module-qn>";
|
||||
}
|
||||
|
||||
static int begin_routine_case(const BehaviorCase *test_case, CBMFileResult **result_out,
|
||||
int *failures) {
|
||||
check_exact_ast_kind(test_case, failures);
|
||||
CBMFileResult *result = extract_case(test_case, failures);
|
||||
*result_out = result;
|
||||
if (!result)
|
||||
return 0;
|
||||
check_caller_definition(test_case, result, "run", failures);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int finish_case(CBMFileResult *result, int failures) {
|
||||
if (result)
|
||||
cbm_free_result(result);
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
static int run_binary_behavior(const BehaviorCase *test_case) {
|
||||
int failures = 0;
|
||||
CBMFileResult *result = NULL;
|
||||
if (begin_routine_case(test_case, &result, &failures)) {
|
||||
check_exact(test_case, "operator_call_in_run",
|
||||
call_count_in_routine(result, "run", "operator+"), 1, &failures);
|
||||
check_exact(test_case, "lhs_usage_in_run", usage_count_in_routine(result, "run", "lhs"), 1,
|
||||
&failures);
|
||||
check_exact(test_case, "rhs_usage_in_run", usage_count_in_routine(result, "run", "rhs"), 1,
|
||||
&failures);
|
||||
check_exact(test_case, "lhs_not_call_in_run", call_count_in_routine(result, "run", "lhs"),
|
||||
0, &failures);
|
||||
check_exact(test_case, "rhs_not_call_in_run", call_count_in_routine(result, "run", "rhs"),
|
||||
0, &failures);
|
||||
check_exact(test_case, "total_calls", result->calls.count, 1, &failures);
|
||||
}
|
||||
return finish_case(result, failures);
|
||||
}
|
||||
|
||||
static int run_subscript_behavior(const BehaviorCase *test_case) {
|
||||
int failures = 0;
|
||||
CBMFileResult *result = NULL;
|
||||
if (begin_routine_case(test_case, &result, &failures)) {
|
||||
check_exact(test_case, "operator_call_in_run",
|
||||
call_count_in_routine(result, "run", "operator[]"), 1, &failures);
|
||||
check_exact(test_case, "receiver_usage_in_run",
|
||||
usage_count_in_routine(result, "run", "receiver"), 1, &failures);
|
||||
check_exact(test_case, "index_usage_in_run", usage_count_in_routine(result, "run", "index"),
|
||||
1, &failures);
|
||||
check_exact(test_case, "receiver_not_call_in_run",
|
||||
call_count_in_routine(result, "run", "receiver"), 0, &failures);
|
||||
check_exact(test_case, "index_not_call_in_run",
|
||||
call_count_in_routine(result, "run", "index"), 0, &failures);
|
||||
check_exact(test_case, "total_calls", result->calls.count, 1, &failures);
|
||||
}
|
||||
return finish_case(result, failures);
|
||||
}
|
||||
|
||||
static int run_unary_behavior(const BehaviorCase *test_case) {
|
||||
int failures = 0;
|
||||
CBMFileResult *result = NULL;
|
||||
if (begin_routine_case(test_case, &result, &failures)) {
|
||||
check_exact(test_case, "operator_call_in_run",
|
||||
call_count_in_routine(result, "run", "operator-"), 1, &failures);
|
||||
check_exact(test_case, "operand_usage_in_run",
|
||||
usage_count_in_routine(result, "run", "operand"), 1, &failures);
|
||||
check_exact(test_case, "operand_not_call_in_run",
|
||||
call_count_in_routine(result, "run", "operand"), 0, &failures);
|
||||
check_exact(test_case, "total_calls", result->calls.count, 1, &failures);
|
||||
}
|
||||
return finish_case(result, failures);
|
||||
}
|
||||
|
||||
static int run_update_behavior(const BehaviorCase *test_case) {
|
||||
int failures = 0;
|
||||
CBMFileResult *result = NULL;
|
||||
if (begin_routine_case(test_case, &result, &failures)) {
|
||||
check_exact(test_case, "operator_call_in_run",
|
||||
call_count_in_routine(result, "run", "operator++"), 1, &failures);
|
||||
check_exact(test_case, "operand_usage_in_run",
|
||||
usage_count_in_routine(result, "run", "operand"), 1, &failures);
|
||||
check_exact(test_case, "operand_not_call_in_run",
|
||||
call_count_in_routine(result, "run", "operand"), 0, &failures);
|
||||
check_exact(test_case, "total_calls", result->calls.count, 1, &failures);
|
||||
}
|
||||
return finish_case(result, failures);
|
||||
}
|
||||
|
||||
/* A delete expression has no textual `~Type` token. The raw call therefore uses
|
||||
* a non-textual `~` candidate at the exact expression span. It is semantic-only:
|
||||
* without a materialized destructor the LSP emits no target and the graph pass
|
||||
* drops the candidate instead of falling back to the operand's spelling. */
|
||||
static int run_delete_behavior(const BehaviorCase *test_case) {
|
||||
int failures = 0;
|
||||
CBMFileResult *result = NULL;
|
||||
if (begin_routine_case(test_case, &result, &failures)) {
|
||||
check_exact(test_case, "raw_delete_semantic_candidate_in_run",
|
||||
call_count_in_routine(result, "run", "~"), 1, &failures);
|
||||
check_exact(test_case, "raw_delete_candidate_exact_site",
|
||||
exact_semantic_candidate_count_in_routine(test_case, result, "run", "~",
|
||||
"delete victim"),
|
||||
1, &failures);
|
||||
check_exact(test_case, "operand_usage_in_run",
|
||||
usage_count_in_routine(result, "run", "victim"), 1, &failures);
|
||||
check_exact(test_case, "operand_not_call_in_run",
|
||||
call_count_in_routine(result, "run", "victim"), 0, &failures);
|
||||
check_exact(test_case, "semantic_operator_not_minted_raw",
|
||||
call_count_in_routine(result, "run", "operator delete"), 0, &failures);
|
||||
check_exact(test_case, "unmaterialized_destructor_not_resolved",
|
||||
resolved_call_count_in_routine(result, "run", "~Value"), 0, &failures);
|
||||
check_exact(test_case, "total_calls", result->calls.count, 1, &failures);
|
||||
}
|
||||
return finish_case(result, failures);
|
||||
}
|
||||
|
||||
static int run_field_behavior(const BehaviorCase *test_case) {
|
||||
int failures = 0;
|
||||
CBMFileResult *result = NULL;
|
||||
if (begin_routine_case(test_case, &result, &failures)) {
|
||||
check_exact(test_case, "receiver_usage_in_run",
|
||||
usage_count_in_routine(result, "run", "receiver"), 1, &failures);
|
||||
check_exact(test_case, "member_usage_in_run",
|
||||
usage_count_in_routine(result, "run", "member"), 1, &failures);
|
||||
check_exact(test_case, "receiver_not_call_in_run",
|
||||
call_count_in_routine(result, "run", "receiver"), 0, &failures);
|
||||
check_exact(test_case, "member_not_call_in_run",
|
||||
call_count_in_routine(result, "run", "member"), 0, &failures);
|
||||
check_exact(test_case, "total_calls", result->calls.count, 0, &failures);
|
||||
}
|
||||
return finish_case(result, failures);
|
||||
}
|
||||
|
||||
static int run_callable_value_behavior(const BehaviorCase *test_case,
|
||||
int expect_raw_typed_reference,
|
||||
int expect_semantic_reference) {
|
||||
int failures = 0;
|
||||
CBMFileResult *result = NULL;
|
||||
if (begin_routine_case(test_case, &result, &failures)) {
|
||||
check_caller_definition(test_case, result, "handler", &failures);
|
||||
check_exact(test_case, "outer_accept_call_in_run",
|
||||
call_count_in_routine(result, "run", "accept"), 1, &failures);
|
||||
check_exact(test_case, "handler_value_usage_in_run",
|
||||
usage_count_in_routine(result, "run", "handler"),
|
||||
expect_raw_typed_reference ? 0 : 1, &failures);
|
||||
check_exact(test_case, "handler_callable_reference_in_run",
|
||||
callable_reference_count_in_routine(result, "run", "handler"),
|
||||
expect_raw_typed_reference ? 1 : 0, &failures);
|
||||
check_exact(test_case, "handler_semantic_reference_candidate_in_run",
|
||||
semantic_reference_candidate_count_in_routine(result, "run", "handler"),
|
||||
!expect_raw_typed_reference && expect_semantic_reference ? 1 : 0, &failures);
|
||||
check_exact(test_case, "handler_value_not_invoked_in_run",
|
||||
call_count_in_routine(result, "run", "handler"), 0, &failures);
|
||||
check_exact(test_case, "total_calls", result->calls.count, 1, &failures);
|
||||
|
||||
if (expect_semantic_reference) {
|
||||
check_exact(test_case, "lsp_outer_accept_call_in_run",
|
||||
resolved_call_count_in_routine(result, "run", "accept"), 1, &failures);
|
||||
check_exact(test_case, "lsp_handler_value_not_invoked_in_run",
|
||||
resolved_call_count_in_routine(result, "run", "handler"), 0, &failures);
|
||||
check_exact(test_case, "lsp_handler_callable_reference_in_run",
|
||||
resolved_reference_count_in_routine(result, "run", "handler"), 1,
|
||||
&failures);
|
||||
check_exact(test_case, "lsp_total_calls_in_run",
|
||||
resolved_call_total_in_routine(result, "run"), 1, &failures);
|
||||
}
|
||||
}
|
||||
return finish_case(result, failures);
|
||||
}
|
||||
|
||||
static int run_arrow_behavior(const BehaviorCase *test_case) {
|
||||
int failures = 0;
|
||||
CBMFileResult *result = NULL;
|
||||
if (begin_routine_case(test_case, &result, &failures)) {
|
||||
check_exact(test_case, "operator_arrow_call_in_run",
|
||||
call_count_in_routine(result, "run", "operator->"), 1, &failures);
|
||||
check_exact(test_case, "receiver_usage_in_run",
|
||||
usage_count_in_routine(result, "run", "receiver"), 1, &failures);
|
||||
check_exact(test_case, "member_usage_in_run",
|
||||
usage_count_in_routine(result, "run", "member"), 1, &failures);
|
||||
check_exact(test_case, "receiver_not_call_in_run",
|
||||
call_count_in_routine(result, "run", "receiver"), 0, &failures);
|
||||
check_exact(test_case, "member_not_call_in_run",
|
||||
call_count_in_routine(result, "run", "member"), 0, &failures);
|
||||
check_exact(test_case, "total_calls", result->calls.count, 1, &failures);
|
||||
}
|
||||
return finish_case(result, failures);
|
||||
}
|
||||
|
||||
static int run_objectscript_call_behavior(const BehaviorCase *test_case, const char *caller_qn,
|
||||
const char *callee, const char *callee_reference,
|
||||
const CBMMacroTable *macro_table) {
|
||||
int failures = 0;
|
||||
CBMFileResult *result = NULL;
|
||||
check_exact_ast_kind(test_case, &failures);
|
||||
result = extract_case_with_macros(test_case, macro_table, &failures);
|
||||
if (result) {
|
||||
check_exact(test_case, "exact_caller_definition",
|
||||
callable_definition_count_exact(result, "run", caller_qn), 1, &failures);
|
||||
check_exact(test_case, "exact_call_owner_and_target",
|
||||
call_count_exact(result, caller_qn, callee), 1, &failures);
|
||||
check_exact(test_case, "argument_usage_in_exact_caller",
|
||||
value_usage_count_exact(result, caller_qn, "watched"), 1, &failures);
|
||||
check_exact(test_case, "argument_not_call_in_exact_caller",
|
||||
call_count_exact(result, caller_qn, "watched"), 0, &failures);
|
||||
check_exact(test_case, "callee_not_usage_in_exact_caller",
|
||||
value_usage_count_exact(result, caller_qn, callee_reference), 0, &failures);
|
||||
check_exact(test_case, "total_calls", result->calls.count, 1, &failures);
|
||||
}
|
||||
return finish_case(result, failures);
|
||||
}
|
||||
|
||||
static const char CPP_BINARY_SOURCE[] = "struct Value {};\n"
|
||||
"Value operator+(Value lhs, Value rhs) { return lhs; }\n"
|
||||
"Value run(Value lhs, Value rhs) { return lhs + rhs; }\n";
|
||||
|
||||
static const char CPP_SUBSCRIPT_SOURCE[] =
|
||||
"struct Buffer {\n"
|
||||
" int storage;\n"
|
||||
" int& operator[](int index) { return storage; }\n"
|
||||
"};\n"
|
||||
"int run(Buffer& receiver, int index) { return receiver[index]; }\n";
|
||||
|
||||
static const char CPP_UNARY_SOURCE[] = "struct Value {};\n"
|
||||
"Value operator-(Value operand) { return operand; }\n"
|
||||
"Value run(Value operand) { return -operand; }\n";
|
||||
|
||||
static const char CPP_UPDATE_SOURCE[] = "struct Counter {\n"
|
||||
" Counter& operator++() { return *this; }\n"
|
||||
"};\n"
|
||||
"Counter& run(Counter& operand) { return ++operand; }\n";
|
||||
|
||||
static const char CPP_DELETE_SOURCE[] = "struct Value {};\n"
|
||||
"void run(Value* victim) { delete victim; }\n";
|
||||
|
||||
static const char CPP_FIELD_SOURCE[] = "struct Record { int member; };\n"
|
||||
"int run(Record receiver) { return receiver.member; }\n";
|
||||
|
||||
static const char CPP_ARROW_SOURCE[] = "struct Target { int member; };\n"
|
||||
"struct Proxy {\n"
|
||||
" Target* target;\n"
|
||||
" Target* operator->() { return target; }\n"
|
||||
"};\n"
|
||||
"int run(Proxy receiver) { return receiver->member; }\n";
|
||||
|
||||
static const char CPP_DELETE_COLLISION_SOURCE[] = "struct Value { ~Value() {} };\n"
|
||||
"void victim() {}\n"
|
||||
"void run(Value* victim) { delete victim; }\n";
|
||||
|
||||
static const char JAVA_METHOD_REFERENCE_SOURCE[] = "interface Task { void invoke(); }\n"
|
||||
"class Sample {\n"
|
||||
" void handler() {}\n"
|
||||
" void accept(Task callback) {}\n"
|
||||
" void run() { accept(this::handler); }\n"
|
||||
"}\n";
|
||||
|
||||
static const char KOTLIN_CALLABLE_REFERENCE_SOURCE[] = "fun handler() {}\n"
|
||||
"fun accept(callback: () -> Unit) {}\n"
|
||||
"fun run() { accept(::handler) }\n";
|
||||
|
||||
static const char CSHARP_METHOD_GROUP_SOURCE[] = "delegate void Task();\n"
|
||||
"class Sample {\n"
|
||||
" static void handler() {}\n"
|
||||
" static void accept(Task callback) {}\n"
|
||||
" static void run() { accept(handler); }\n"
|
||||
"}\n";
|
||||
|
||||
static const char CPP_FUNCTION_VALUE_SOURCE[] = "void handler() {}\n"
|
||||
"void accept(void (*callback)()) {}\n"
|
||||
"void run() { accept(handler); }\n";
|
||||
|
||||
static const char RUST_FUNCTION_ITEM_SOURCE[] = "fn handler() {}\n"
|
||||
"fn accept(_callback: fn()) {}\n"
|
||||
"fn run() { accept(handler); }\n";
|
||||
|
||||
static const char OBJECTSCRIPT_CLASS_METHOD_SOURCE[] =
|
||||
"Class Sample.Behavior Extends %RegisteredObject\n"
|
||||
"{\n"
|
||||
"Method run(watched As %String) As %Status\n"
|
||||
"{\n"
|
||||
" Do ##class(Sample.Target).accept(watched)\n"
|
||||
" Quit\n"
|
||||
"}\n"
|
||||
"}\n";
|
||||
|
||||
static const char OBJECTSCRIPT_METHOD_SOURCE[] =
|
||||
"Class Sample.Behavior Extends %RegisteredObject\n"
|
||||
"{\n"
|
||||
"Method run(target As Sample.Target, watched As %String) As %Status\n"
|
||||
"{\n"
|
||||
" Do target.accept(watched)\n"
|
||||
" Quit\n"
|
||||
"}\n"
|
||||
"}\n";
|
||||
|
||||
static const char OBJECTSCRIPT_RELATIVE_DOT_METHOD_SOURCE[] =
|
||||
"Class Sample.Behavior Extends %RegisteredObject\n"
|
||||
"{\n"
|
||||
"Method accept(value As %String) As %Status\n"
|
||||
"{\n"
|
||||
" Quit\n"
|
||||
"}\n"
|
||||
"Method run(watched As %String) As %Status\n"
|
||||
"{\n"
|
||||
" Do ..accept(watched)\n"
|
||||
" Quit\n"
|
||||
"}\n"
|
||||
"}\n";
|
||||
|
||||
static const char OBJECTSCRIPT_MACRO_SOURCE[] = "Class Sample.Behavior Extends %RegisteredObject\n"
|
||||
"{\n"
|
||||
"Method run(watched As %Status) As %Status\n"
|
||||
"{\n"
|
||||
" Quit $$$ISERR(watched)\n"
|
||||
"}\n"
|
||||
"}\n";
|
||||
|
||||
static const char OBJECTSCRIPT_EXTRINSIC_SOURCE[] = "run(watched)\n"
|
||||
" Quit $$accept(watched)\n"
|
||||
"accept(value)\n"
|
||||
" Quit value\n";
|
||||
|
||||
static const char OBJECTSCRIPT_ROUTINE_TAG_SOURCE[] = "run(watched)\n"
|
||||
" Do accept(watched)\n"
|
||||
" Quit\n"
|
||||
"accept(value)\n"
|
||||
" Quit value\n";
|
||||
|
||||
static const char SCALA_INFIX_SOURCE[] = "class Box {\n"
|
||||
" def merge(other: Box): Box = this\n"
|
||||
"}\n"
|
||||
"object Sample {\n"
|
||||
" def run(lhs: Box, rhs: Box): Box = lhs merge rhs\n"
|
||||
"}\n";
|
||||
|
||||
static const char ELIXIR_BINARY_SOURCE[] = "defmodule Sample do\n"
|
||||
" def run(watched), do: watched + 1\n"
|
||||
"end\n";
|
||||
|
||||
static const char PUPPET_RESOURCE_SOURCE[] = "$watched = 'ready'\n"
|
||||
"notify { 'call-node-behavior':\n"
|
||||
" message => $watched,\n"
|
||||
"}\n";
|
||||
|
||||
#define DEFINE_CPP_CUDA_BEHAVIOR_TESTS(name, source_value, ast_value, runner, reason_value) \
|
||||
TEST(repro_call_node_behavior_cpp_##name) { \
|
||||
static const BehaviorCase test_case = {"cpp/" #name, CBM_LANG_CPP, "behavior.cpp", \
|
||||
source_value, ast_value, reason_value}; \
|
||||
return runner(&test_case); \
|
||||
} \
|
||||
TEST(repro_call_node_behavior_cuda_##name) { \
|
||||
static const BehaviorCase test_case = {"cuda/" #name, CBM_LANG_CUDA, "behavior.cu", \
|
||||
source_value, ast_value, reason_value}; \
|
||||
return runner(&test_case); \
|
||||
}
|
||||
|
||||
DEFINE_CPP_CUDA_BEHAVIOR_TESTS(
|
||||
binary, CPP_BINARY_SOURCE, "binary_expression", run_binary_behavior,
|
||||
"binary operator call exists, but operands remain independently visible usages")
|
||||
DEFINE_CPP_CUDA_BEHAVIOR_TESTS(
|
||||
subscript, CPP_SUBSCRIPT_SOURCE, "subscript_expression", run_subscript_behavior,
|
||||
"subscript operator needs one synthetic call without consuming receiver or index usages")
|
||||
DEFINE_CPP_CUDA_BEHAVIOR_TESTS(
|
||||
unary, CPP_UNARY_SOURCE, "unary_expression", run_unary_behavior,
|
||||
"unary overload needs its exact synthetic operator name and operand usage")
|
||||
DEFINE_CPP_CUDA_BEHAVIOR_TESTS(
|
||||
update, CPP_UPDATE_SOURCE, "update_expression", run_update_behavior,
|
||||
"update overload needs one operator++ call while retaining its operand usage")
|
||||
DEFINE_CPP_CUDA_BEHAVIOR_TESTS(
|
||||
delete, CPP_DELETE_SOURCE, "delete_expression", run_delete_behavior,
|
||||
"delete keeps an exact-span semantic candidate plus the independent operand usage")
|
||||
DEFINE_CPP_CUDA_BEHAVIOR_TESTS(
|
||||
field, CPP_FIELD_SOURCE, "field_expression", run_field_behavior,
|
||||
"ordinary member access is a callee wrapper, not a call, and both names are usages")
|
||||
DEFINE_CPP_CUDA_BEHAVIOR_TESTS(
|
||||
overloaded_arrow, CPP_ARROW_SOURCE, "field_expression", run_arrow_behavior,
|
||||
"overloaded operator-> is an actual invocation while receiver and member remain usages")
|
||||
|
||||
#undef DEFINE_CPP_CUDA_BEHAVIOR_TESTS
|
||||
|
||||
TEST(repro_call_node_behavior_objectscript_udl_class_method_call) {
|
||||
static const BehaviorCase test_case = {
|
||||
"objectscript-udl/class-method-call",
|
||||
CBM_LANG_OBJECTSCRIPT_UDL,
|
||||
"Behavior.cls",
|
||||
OBJECTSCRIPT_CLASS_METHOD_SOURCE,
|
||||
"class_method_call",
|
||||
"##class(Sample.Target).accept(watched) has one exact class-method callee while its "
|
||||
"argument remains a value usage"};
|
||||
return run_objectscript_call_behavior(&test_case, "repro.Behavior.Sample.Behavior.run",
|
||||
"Sample.Target.accept", "accept", NULL);
|
||||
}
|
||||
|
||||
TEST(repro_call_node_behavior_objectscript_udl_method_call) {
|
||||
static const BehaviorCase test_case = {
|
||||
"objectscript-udl/method-call",
|
||||
CBM_LANG_OBJECTSCRIPT_UDL,
|
||||
"Behavior.cls",
|
||||
OBJECTSCRIPT_METHOD_SOURCE,
|
||||
"method_call",
|
||||
"a typed ObjectScript receiver resolves one exact instance-method call without "
|
||||
"consuming its argument usage"};
|
||||
return run_objectscript_call_behavior(&test_case, "repro.Behavior.Sample.Behavior.run",
|
||||
"Sample.Target.accept", "accept", NULL);
|
||||
}
|
||||
|
||||
TEST(repro_call_node_behavior_objectscript_udl_relative_dot_method) {
|
||||
static const BehaviorCase test_case = {
|
||||
"objectscript-udl/relative-dot-method",
|
||||
CBM_LANG_OBJECTSCRIPT_UDL,
|
||||
"Behavior.cls",
|
||||
OBJECTSCRIPT_RELATIVE_DOT_METHOD_SOURCE,
|
||||
"relative_dot_method",
|
||||
"..accept(watched) resolves against the exact enclosing class while watched remains a "
|
||||
"value usage"};
|
||||
return run_objectscript_call_behavior(&test_case, "repro.Behavior.Sample.Behavior.run",
|
||||
"repro.Behavior.Sample.Behavior.accept", "accept", NULL);
|
||||
}
|
||||
|
||||
TEST(repro_call_node_behavior_objectscript_udl_macro) {
|
||||
static const BehaviorCase test_case = {
|
||||
"objectscript-udl/macro",
|
||||
CBM_LANG_OBJECTSCRIPT_UDL,
|
||||
"Behavior.cls",
|
||||
OBJECTSCRIPT_MACRO_SOURCE,
|
||||
"macro",
|
||||
"the system $$$ISERR(watched) macro maps to %SYSTEM.Status.IsError while watched "
|
||||
"remains a value usage"};
|
||||
CBMMacroTable *macro_table = calloc(1, sizeof(*macro_table));
|
||||
ASSERT_NOT_NULL(macro_table);
|
||||
cbm_macro_table_init_system(macro_table);
|
||||
int status = run_objectscript_call_behavior(&test_case, "repro.Behavior.Sample.Behavior.run",
|
||||
"%SYSTEM.Status.IsError", "ISERR", macro_table);
|
||||
cbm_macro_table_free(macro_table);
|
||||
return status;
|
||||
}
|
||||
|
||||
TEST(repro_call_node_behavior_objectscript_routine_extrinsic_function) {
|
||||
static const BehaviorCase test_case = {
|
||||
"objectscript-routine/extrinsic-function",
|
||||
CBM_LANG_OBJECTSCRIPT_ROUTINE,
|
||||
"Behavior.mac",
|
||||
OBJECTSCRIPT_EXTRINSIC_SOURCE,
|
||||
"extrinsic_function",
|
||||
"$$accept(watched) belongs to the exact run tag and retains watched as its argument "
|
||||
"usage"};
|
||||
return run_objectscript_call_behavior(&test_case, "repro.Behavior.run", "accept", "accept",
|
||||
NULL);
|
||||
}
|
||||
|
||||
TEST(repro_call_node_behavior_objectscript_routine_tag_call) {
|
||||
static const BehaviorCase test_case = {
|
||||
"objectscript-routine/routine-tag-call",
|
||||
CBM_LANG_OBJECTSCRIPT_ROUTINE,
|
||||
"Behavior.mac",
|
||||
OBJECTSCRIPT_ROUTINE_TAG_SOURCE,
|
||||
"routine_tag_call",
|
||||
"Do accept(watched) belongs to the exact run tag and retains watched as its argument "
|
||||
"usage"};
|
||||
return run_objectscript_call_behavior(&test_case, "repro.Behavior.run", "accept", "accept",
|
||||
NULL);
|
||||
}
|
||||
|
||||
TEST(repro_call_node_behavior_scala_user_infix) {
|
||||
static const BehaviorCase test_case = {
|
||||
"scala/user-infix",
|
||||
CBM_LANG_SCALA,
|
||||
"Behavior.scala",
|
||||
SCALA_INFIX_SOURCE,
|
||||
"infix_expression",
|
||||
"user-defined infix merge is the callee while lhs and rhs remain value usages"};
|
||||
int failures = 0;
|
||||
CBMFileResult *result = NULL;
|
||||
if (begin_routine_case(&test_case, &result, &failures)) {
|
||||
check_exact(&test_case, "merge_call_in_run", call_count_in_routine(result, "run", "merge"),
|
||||
1, &failures);
|
||||
check_exact(&test_case, "lhs_usage_in_run", usage_count_in_routine(result, "run", "lhs"), 1,
|
||||
&failures);
|
||||
check_exact(&test_case, "rhs_usage_in_run", usage_count_in_routine(result, "run", "rhs"), 1,
|
||||
&failures);
|
||||
check_exact(&test_case, "lhs_not_call_in_run", call_count_in_routine(result, "run", "lhs"),
|
||||
0, &failures);
|
||||
check_exact(&test_case, "rhs_not_call_in_run", call_count_in_routine(result, "run", "rhs"),
|
||||
0, &failures);
|
||||
check_exact(&test_case, "total_calls", result->calls.count, 1, &failures);
|
||||
}
|
||||
return finish_case(result, failures);
|
||||
}
|
||||
|
||||
TEST(repro_call_node_behavior_elixir_binary_plus) {
|
||||
static const BehaviorCase test_case = {
|
||||
"elixir/binary-plus",
|
||||
CBM_LANG_ELIXIR,
|
||||
"behavior.ex",
|
||||
ELIXIR_BINARY_SOURCE,
|
||||
"binary_operator",
|
||||
"binary + belongs to the operator callee, not to its watched operand"};
|
||||
int failures = 0;
|
||||
CBMFileResult *result = NULL;
|
||||
if (begin_routine_case(&test_case, &result, &failures)) {
|
||||
check_exact(&test_case, "plus_call_in_run", call_count_in_routine(result, "run", "+"), 1,
|
||||
&failures);
|
||||
check_exact(&test_case, "operand_usage_in_run",
|
||||
usage_count_in_routine(result, "run", "watched"), 1, &failures);
|
||||
check_exact(&test_case, "operand_not_call_in_run",
|
||||
call_count_in_routine(result, "run", "watched"), 0, &failures);
|
||||
check_exact(&test_case, "total_calls", result->calls.count, 1, &failures);
|
||||
}
|
||||
return finish_case(result, failures);
|
||||
}
|
||||
|
||||
TEST(repro_call_node_behavior_puppet_resource_ownership) {
|
||||
static const BehaviorCase test_case = {
|
||||
"puppet/resource",
|
||||
CBM_LANG_PUPPET,
|
||||
"behavior.pp",
|
||||
PUPPET_RESOURCE_SOURCE,
|
||||
"resource_declaration",
|
||||
"resource declarations are Puppet DSL ownership and must not fabricate generic calls"};
|
||||
int failures = 0;
|
||||
check_exact_ast_kind(&test_case, &failures);
|
||||
CBMFileResult *result = extract_case(&test_case, &failures);
|
||||
if (result) {
|
||||
const char *module_qn = checked_module_qn(&test_case, result, &failures);
|
||||
check_exact(&test_case, "resource_value_usage_in_module",
|
||||
usage_count_exact(result, module_qn, "$watched"), 1, &failures);
|
||||
check_exact(&test_case, "variable_not_call_in_module",
|
||||
call_count_exact(result, module_qn, "$watched"), 0, &failures);
|
||||
check_exact(&test_case, "total_calls", result->calls.count, 0, &failures);
|
||||
}
|
||||
return finish_case(result, failures);
|
||||
}
|
||||
|
||||
TEST(repro_call_node_behavior_java_method_reference_value) {
|
||||
static const BehaviorCase test_case = {
|
||||
"java/method-reference-value",
|
||||
CBM_LANG_JAVA,
|
||||
"Behavior.java",
|
||||
JAVA_METHOD_REFERENCE_SOURCE,
|
||||
"method_reference",
|
||||
"a Java method reference is a value, not an invocation at the reference site"};
|
||||
return run_callable_value_behavior(&test_case, 1, 1);
|
||||
}
|
||||
|
||||
TEST(repro_call_node_behavior_kotlin_callable_reference_value) {
|
||||
static const BehaviorCase test_case = {
|
||||
"kotlin/callable-reference-value",
|
||||
CBM_LANG_KOTLIN,
|
||||
"Behavior.kt",
|
||||
KOTLIN_CALLABLE_REFERENCE_SOURCE,
|
||||
"callable_reference",
|
||||
"a Kotlin callable reference is a value, not an invocation at the reference site"};
|
||||
return run_callable_value_behavior(&test_case, 0, 1);
|
||||
}
|
||||
|
||||
TEST(repro_call_node_behavior_csharp_method_group_value) {
|
||||
static const BehaviorCase test_case = {
|
||||
"csharp/method-group-value",
|
||||
CBM_LANG_CSHARP,
|
||||
"Behavior.cs",
|
||||
CSHARP_METHOD_GROUP_SOURCE,
|
||||
"invocation_expression",
|
||||
"a C# method group passed as an argument is a value, not a handler invocation"};
|
||||
return run_callable_value_behavior(&test_case, 0, 1);
|
||||
}
|
||||
|
||||
TEST(repro_call_node_behavior_cpp_function_value) {
|
||||
static const BehaviorCase test_case = {
|
||||
"cpp/function-value",
|
||||
CBM_LANG_CPP,
|
||||
"behavior.cpp",
|
||||
CPP_FUNCTION_VALUE_SOURCE,
|
||||
"call_expression",
|
||||
"a C++ function value passed as an argument is referenced but not invoked"};
|
||||
return run_callable_value_behavior(&test_case, 0, 1);
|
||||
}
|
||||
|
||||
TEST(repro_call_node_behavior_rust_function_item_value) {
|
||||
static const BehaviorCase test_case = {
|
||||
"rust/function-item-value",
|
||||
CBM_LANG_RUST,
|
||||
"behavior.rs",
|
||||
RUST_FUNCTION_ITEM_SOURCE,
|
||||
"call_expression",
|
||||
"a Rust function item passed as an argument is referenced but not invoked"};
|
||||
return run_callable_value_behavior(&test_case, 0, 1);
|
||||
}
|
||||
|
||||
TEST(repro_call_node_behavior_cpp_delete_name_collision) {
|
||||
static const BehaviorCase test_case = {
|
||||
"cpp/delete-name-collision",
|
||||
CBM_LANG_CPP,
|
||||
"behavior.cpp",
|
||||
CPP_DELETE_COLLISION_SOURCE,
|
||||
"delete_expression",
|
||||
"delete must key the implicit ~Value destructor, not operand text that collides with "
|
||||
"victim()"};
|
||||
int failures = 0;
|
||||
CBMFileResult *result = NULL;
|
||||
if (begin_routine_case(&test_case, &result, &failures)) {
|
||||
check_caller_definition(&test_case, result, "victim", &failures);
|
||||
check_exact(&test_case, "destructor_call_in_run",
|
||||
call_count_in_routine(result, "run", "~Value"), 1, &failures);
|
||||
check_exact(&test_case, "destructor_candidate_exact_site",
|
||||
exact_semantic_candidate_count_in_routine(&test_case, result, "run", "~Value",
|
||||
"delete victim"),
|
||||
1, &failures);
|
||||
check_exact(&test_case, "operand_usage_in_run",
|
||||
usage_count_in_routine(result, "run", "victim"), 1, &failures);
|
||||
check_exact(&test_case, "unrelated_victim_not_called_in_run",
|
||||
call_count_in_routine(result, "run", "victim"), 0, &failures);
|
||||
check_exact(&test_case, "lsp_destructor_call_in_run",
|
||||
resolved_call_count_in_routine(result, "run", "~Value"), 1, &failures);
|
||||
check_exact(&test_case, "lsp_unrelated_victim_not_called_in_run",
|
||||
resolved_call_count_in_routine(result, "run", "victim"), 0, &failures);
|
||||
check_exact(&test_case, "lsp_total_calls_in_run",
|
||||
resolved_call_total_in_routine(result, "run"), 1, &failures);
|
||||
check_exact(&test_case, "total_calls", result->calls.count, 1, &failures);
|
||||
}
|
||||
return finish_case(result, failures);
|
||||
}
|
||||
|
||||
enum {
|
||||
CPP_BEHAVIOR_COUNT = 9,
|
||||
CUDA_BEHAVIOR_COUNT = 7,
|
||||
OTHER_BEHAVIOR_COUNT = 7,
|
||||
OBJECTSCRIPT_BEHAVIOR_COUNT = 6,
|
||||
CALL_NODE_BEHAVIOR_COUNT = CPP_BEHAVIOR_COUNT + CUDA_BEHAVIOR_COUNT + OTHER_BEHAVIOR_COUNT +
|
||||
OBJECTSCRIPT_BEHAVIOR_COUNT,
|
||||
};
|
||||
|
||||
_Static_assert(CALL_NODE_BEHAVIOR_COUNT == 29,
|
||||
"call-node behavior suite must retain 23 contracts and add 6 ObjectScript "
|
||||
"call-role guards");
|
||||
|
||||
SUITE(repro_call_node_behaviors) {
|
||||
RUN_TEST(repro_call_node_behavior_cpp_binary);
|
||||
RUN_TEST(repro_call_node_behavior_cpp_subscript);
|
||||
RUN_TEST(repro_call_node_behavior_cpp_unary);
|
||||
RUN_TEST(repro_call_node_behavior_cpp_update);
|
||||
RUN_TEST(repro_call_node_behavior_cpp_delete);
|
||||
RUN_TEST(repro_call_node_behavior_cpp_field);
|
||||
RUN_TEST(repro_call_node_behavior_cpp_overloaded_arrow);
|
||||
RUN_TEST(repro_call_node_behavior_cpp_function_value);
|
||||
RUN_TEST(repro_call_node_behavior_cpp_delete_name_collision);
|
||||
|
||||
RUN_TEST(repro_call_node_behavior_cuda_binary);
|
||||
RUN_TEST(repro_call_node_behavior_cuda_subscript);
|
||||
RUN_TEST(repro_call_node_behavior_cuda_unary);
|
||||
RUN_TEST(repro_call_node_behavior_cuda_update);
|
||||
RUN_TEST(repro_call_node_behavior_cuda_delete);
|
||||
RUN_TEST(repro_call_node_behavior_cuda_field);
|
||||
RUN_TEST(repro_call_node_behavior_cuda_overloaded_arrow);
|
||||
|
||||
RUN_TEST(repro_call_node_behavior_objectscript_udl_class_method_call);
|
||||
RUN_TEST(repro_call_node_behavior_objectscript_udl_method_call);
|
||||
RUN_TEST(repro_call_node_behavior_objectscript_udl_relative_dot_method);
|
||||
RUN_TEST(repro_call_node_behavior_objectscript_udl_macro);
|
||||
RUN_TEST(repro_call_node_behavior_objectscript_routine_extrinsic_function);
|
||||
RUN_TEST(repro_call_node_behavior_objectscript_routine_tag_call);
|
||||
|
||||
RUN_TEST(repro_call_node_behavior_scala_user_infix);
|
||||
RUN_TEST(repro_call_node_behavior_elixir_binary_plus);
|
||||
RUN_TEST(repro_call_node_behavior_puppet_resource_ownership);
|
||||
RUN_TEST(repro_call_node_behavior_java_method_reference_value);
|
||||
RUN_TEST(repro_call_node_behavior_kotlin_callable_reference_value);
|
||||
RUN_TEST(repro_call_node_behavior_csharp_method_group_value);
|
||||
RUN_TEST(repro_call_node_behavior_rust_function_item_value);
|
||||
}
|
||||
@@ -0,0 +1,638 @@
|
||||
/*
|
||||
* Exhaustive contract for call-node metadata.
|
||||
*
|
||||
* This ledger is intentionally literal. Semantic role and extraction ownership
|
||||
* are independent review decisions; neither field supplies a default for the
|
||||
* other. Every row also states whether it belonged to the historical metadata
|
||||
* snapshot and whether it belongs in the primary call-node registry.
|
||||
*
|
||||
* This metadata contract is not proof of extraction behavior. Changes must also
|
||||
* pass the focused repro_call_scope_usages, repro_call_argument_matrix_a,
|
||||
* repro_call_argument_matrix_b, repro_lsp_c_cpp, repro_grammar_build, and
|
||||
* repro_grammar_shells suites before production metadata is changed.
|
||||
* C++/CUDA operator rows require raw-call-to-LSP assertions in repro_lsp_c_cpp;
|
||||
* Scala/Elixir direct operators require operand-scope assertions in the call
|
||||
* argument suites; Puppet resource ownership remains gated by repro_grammar_build.
|
||||
* TLA+ bound_op is historical direct-call metadata. Bounded quantification is
|
||||
* not a historical ledger row; repro_call_argument_matrix_b is its separate
|
||||
* negative behavior guard.
|
||||
*/
|
||||
#include "test_framework.h"
|
||||
#include "lang_specs.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef enum {
|
||||
DIRECT_CALL = 0,
|
||||
CONSTRUCTOR_CALL,
|
||||
OPERATOR_CALL,
|
||||
IMPLICIT_CALL,
|
||||
DSL_INVOCATION,
|
||||
BUILD_DEPENDENCY,
|
||||
CALLEE_WRAPPER,
|
||||
ARGUMENT_WRAPPER,
|
||||
CONTROL_OR_DOCUMENT_NONCALL,
|
||||
CALL_SEMANTIC_ROLE_COUNT
|
||||
} CallSemanticRole;
|
||||
|
||||
typedef enum {
|
||||
PRIMARY_EXTRACTOR = 0,
|
||||
SYNTHETIC_EXTRACTOR,
|
||||
NO_CALL_EDGE,
|
||||
CALL_EXTRACTION_OWNER_COUNT
|
||||
} CallExtractionOwner;
|
||||
|
||||
typedef struct {
|
||||
CBMLanguage language;
|
||||
const char *node_kind;
|
||||
CallSemanticRole semantic_role;
|
||||
CallExtractionOwner extraction_owner;
|
||||
bool historically_registered;
|
||||
bool expected_primary_registered;
|
||||
} CallNodeManifestEntry;
|
||||
|
||||
enum {
|
||||
EXPECTED_HISTORICAL_CALL_NODE_TOTAL = 219,
|
||||
EXPECTED_ACTIVE_PRIMARY_TOTAL = 189,
|
||||
EXPECTED_DIRECT_CALLS = 155,
|
||||
EXPECTED_CONSTRUCTOR_CALLS = 20,
|
||||
EXPECTED_OPERATOR_CALLS = 12,
|
||||
EXPECTED_IMPLICIT_CALLS = 2,
|
||||
EXPECTED_DSL_INVOCATIONS = 14,
|
||||
EXPECTED_BUILD_DEPENDENCIES = 1,
|
||||
EXPECTED_CALLEE_WRAPPERS = 8,
|
||||
EXPECTED_ARGUMENT_WRAPPERS = 1,
|
||||
EXPECTED_CONTROL_OR_DOCUMENT_NONCALLS = 6,
|
||||
EXPECTED_PRIMARY_OWNERS = 189,
|
||||
EXPECTED_SYNTHETIC_OWNERS = 10,
|
||||
EXPECTED_NO_CALL_EDGE_OWNERS = 20,
|
||||
EXPECTED_PRIMARY_OPERATOR_CALLS = 4,
|
||||
EXPECTED_SYNTHETIC_OPERATOR_CALLS = 8,
|
||||
};
|
||||
|
||||
#define ENTRY(language, kind, role, owner, historical, expected_primary) \
|
||||
{language, kind, role, owner, historical, expected_primary}
|
||||
|
||||
static const CallNodeManifestEntry CALL_NODE_MANIFEST[] = {
|
||||
ENTRY(CBM_LANG_GO, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_PYTHON, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_PYTHON, "with_statement", CONTROL_OR_DOCUMENT_NONCALL, NO_CALL_EDGE, true,
|
||||
false),
|
||||
ENTRY(CBM_LANG_JAVASCRIPT, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_JAVASCRIPT, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_TYPESCRIPT, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_TYPESCRIPT, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_TSX, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_TSX, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_RUST, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_RUST, "macro_invocation", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_JAVA, "method_invocation", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_JAVA, "object_creation_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true,
|
||||
true),
|
||||
ENTRY(CBM_LANG_CPP, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_CPP, "field_expression", CALLEE_WRAPPER, NO_CALL_EDGE, true, false),
|
||||
ENTRY(CBM_LANG_CPP, "subscript_expression", OPERATOR_CALL, SYNTHETIC_EXTRACTOR, true, false),
|
||||
ENTRY(CBM_LANG_CPP, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_CPP, "delete_expression", IMPLICIT_CALL, SYNTHETIC_EXTRACTOR, true, false),
|
||||
ENTRY(CBM_LANG_CPP, "binary_expression", OPERATOR_CALL, SYNTHETIC_EXTRACTOR, true, false),
|
||||
ENTRY(CBM_LANG_CPP, "unary_expression", OPERATOR_CALL, SYNTHETIC_EXTRACTOR, true, false),
|
||||
ENTRY(CBM_LANG_CPP, "update_expression", OPERATOR_CALL, SYNTHETIC_EXTRACTOR, true, false),
|
||||
ENTRY(CBM_LANG_CSHARP, "invocation_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_CSHARP, "object_creation_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true,
|
||||
true),
|
||||
ENTRY(CBM_LANG_PHP, "member_call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_PHP, "scoped_call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_PHP, "function_call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_PHP, "object_creation_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true,
|
||||
true),
|
||||
ENTRY(CBM_LANG_PHP, "nullsafe_member_call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true,
|
||||
true),
|
||||
ENTRY(CBM_LANG_LUA, "function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_SCALA, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_SCALA, "generic_function", CALLEE_WRAPPER, NO_CALL_EDGE, true, false),
|
||||
ENTRY(CBM_LANG_SCALA, "field_expression", CALLEE_WRAPPER, NO_CALL_EDGE, true, false),
|
||||
ENTRY(CBM_LANG_SCALA, "infix_expression", OPERATOR_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_SCALA, "instance_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_KOTLIN, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_KOTLIN, "navigation_expression", CALLEE_WRAPPER, NO_CALL_EDGE, true, false),
|
||||
ENTRY(CBM_LANG_RUBY, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_RUBY, "command_call", DIRECT_CALL, NO_CALL_EDGE, true, false),
|
||||
ENTRY(CBM_LANG_C, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_BASH, "command", DSL_INVOCATION, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_ZIG, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_ZIG, "builtin_function", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_ELIXIR, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_ELIXIR, "dot", CALLEE_WRAPPER, NO_CALL_EDGE, true, false),
|
||||
ENTRY(CBM_LANG_ELIXIR, "binary_operator", OPERATOR_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_HASKELL, "infix", OPERATOR_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_HASKELL, "apply", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_OCAML, "application_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_OCAML, "infix_expression", OPERATOR_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_OCAML, "method_invocation", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_OCAML, "module_application", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_OCAML, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_OBJC, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_OBJC, "message_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_SWIFT, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_SWIFT, "constructor_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true,
|
||||
true),
|
||||
ENTRY(CBM_LANG_SWIFT, "macro_invocation", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_SWIFT, "navigation_expression", CALLEE_WRAPPER, NO_CALL_EDGE, true, false),
|
||||
ENTRY(CBM_LANG_DART, "selector", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_DART, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_PERL, "ambiguous_function_call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true,
|
||||
true),
|
||||
ENTRY(CBM_LANG_PERL, "function_call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_PERL, "func1op_call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_PERL, "method_call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_GROOVY, "function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_GROOVY, "juxt_function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_ERLANG, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_R, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_CSS, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_SCSS, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_SCSS, "include_statement", DSL_INVOCATION, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_HCL, "function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_SQL, "function_call", DIRECT_CALL, NO_CALL_EDGE, true, false),
|
||||
ENTRY(CBM_LANG_SQL, "invocation", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_SQL, "command", CONTROL_OR_DOCUMENT_NONCALL, NO_CALL_EDGE, true, false),
|
||||
ENTRY(CBM_LANG_CLOJURE, "list_lit", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_FSHARP, "application_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_FSHARP, "dot_expression", CALLEE_WRAPPER, NO_CALL_EDGE, true, false),
|
||||
ENTRY(CBM_LANG_JULIA, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_JULIA, "broadcast_call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_VIMSCRIPT, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_VIMSCRIPT, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_VIMSCRIPT, "command", DSL_INVOCATION, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_NIX, "apply_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_COMMONLISP, "list_lit", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_ELM, "function_call", DIRECT_CALL, NO_CALL_EDGE, true, false),
|
||||
ENTRY(CBM_LANG_ELM, "function_call_expr", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_FORTRAN, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_FORTRAN, "keyword_argument", ARGUMENT_WRAPPER, NO_CALL_EDGE, true, false),
|
||||
ENTRY(CBM_LANG_FORTRAN, "subroutine_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_CUDA, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_CUDA, "field_expression", CALLEE_WRAPPER, NO_CALL_EDGE, true, false),
|
||||
ENTRY(CBM_LANG_CUDA, "subscript_expression", OPERATOR_CALL, SYNTHETIC_EXTRACTOR, true, false),
|
||||
ENTRY(CBM_LANG_CUDA, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_CUDA, "delete_expression", IMPLICIT_CALL, SYNTHETIC_EXTRACTOR, true, false),
|
||||
ENTRY(CBM_LANG_CUDA, "binary_expression", OPERATOR_CALL, SYNTHETIC_EXTRACTOR, true, false),
|
||||
ENTRY(CBM_LANG_CUDA, "unary_expression", OPERATOR_CALL, SYNTHETIC_EXTRACTOR, true, false),
|
||||
ENTRY(CBM_LANG_CUDA, "update_expression", OPERATOR_CALL, SYNTHETIC_EXTRACTOR, true, false),
|
||||
ENTRY(CBM_LANG_COBOL, "call_statement", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_VERILOG, "system_tf_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_VERILOG, "subroutine_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_VERILOG, "function_subroutine_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_VERILOG, "method_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_EMACSLISP, "list", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_MAKEFILE, "function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_MAKEFILE, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_MAKEFILE, "shell_function", DSL_INVOCATION, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_CMAKE, "normal_command", DSL_INVOCATION, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_MESON, "normal_command", DSL_INVOCATION, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_GLSL, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_MATLAB, "function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_MATLAB, "command", DSL_INVOCATION, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_LEAN, "apply", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_LEAN, "command", CONTROL_OR_DOCUMENT_NONCALL, NO_CALL_EDGE, true, false),
|
||||
ENTRY(CBM_LANG_FORM, "call_statement", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_MAGMA, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_WOLFRAM, "apply", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_SOLIDITY, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_SOLIDITY, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_SOLIDITY, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_TYPST, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_GDSCRIPT, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_GDSCRIPT, "attribute_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_GDSCRIPT, "base_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_QML, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_QML, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_CFSCRIPT, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_CFSCRIPT, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_CFML, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_GLEAM, "function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_POWERSHELL, "invokation_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_POWERSHELL, "command", DSL_INVOCATION, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_PASCAL, "exprCall", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_DLANG, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_DLANG, "function_call_expression", DIRECT_CALL, NO_CALL_EDGE, true, false),
|
||||
ENTRY(CBM_LANG_DLANG, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_SCHEME, "list", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_FENNEL, "list", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_FISH, "command", DSL_INVOCATION, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_AWK, "func_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_AWK, "command", CONTROL_OR_DOCUMENT_NONCALL, NO_CALL_EDGE, true, false),
|
||||
ENTRY(CBM_LANG_ZSH, "command", DSL_INVOCATION, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_ZSH, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_TCL, "command", DSL_INVOCATION, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_ADA, "function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_ADA, "procedure_call_statement", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_AGDA, "module_application", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_AGDA, "expr", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_RACKET, "list", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_ODIN, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_ODIN, "selector_call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_RESCRIPT, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_PURESCRIPT, "exp_apply", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_NICKEL, "applicative", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_CRYSTAL, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_CRYSTAL, "command", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_CRYSTAL, "implicit_object_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_TEAL, "function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_HARE, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_PONY, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_LUAU, "function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_SWAY, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_SWAY, "abi_call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_NASM, "call_syntax_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_NASM, "actual_instruction", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_JUST, "function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_JUST, "dependency", BUILD_DEPENDENCY, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_GOTEMPLATE, "function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_GOTEMPLATE, "method_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_GOTEMPLATE, "template_action", DSL_INVOCATION, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_TEMPL, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_PRISMA, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_DIFF, "command", CONTROL_OR_DOCUMENT_NONCALL, NO_CALL_EDGE, true, false),
|
||||
ENTRY(CBM_LANG_WGSL, "type_constructor_or_function_call_expression", DIRECT_CALL,
|
||||
PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_JSONNET, "functioncall", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_BIBTEX, "command", CONTROL_OR_DOCUMENT_NONCALL, NO_CALL_EDGE, true, false),
|
||||
ENTRY(CBM_LANG_STARLARK, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_BICEP, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_HLSL, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_HLSL, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_VHDL, "function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_VHDL, "procedure_call_statement", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_VHDL, "component_instantiation_statement", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR,
|
||||
true, true),
|
||||
ENTRY(CBM_LANG_VHDL, "parenthesis_group", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_SYSTEMVERILOG, "function_subroutine_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true,
|
||||
true),
|
||||
ENTRY(CBM_LANG_SYSTEMVERILOG, "system_tf_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_SYSTEMVERILOG, "method_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_DEVICETREE, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_LINKERSCRIPT, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_GN, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_BITBAKE, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_ISPC, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_ISPC, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_CAIRO, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_CAIRO, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_MOVE, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_SQUIRREL, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_FUNC, "method_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_FUNC, "function_application", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_PUPPET, "function_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_PUPPET, "resource_declaration", DSL_INVOCATION, NO_CALL_EDGE, true, false),
|
||||
ENTRY(CBM_LANG_PUPPET, "include_statement", DSL_INVOCATION, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_SLANG, "call_expression", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_SLANG, "new_expression", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_LLVM_IR, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_LLVM_IR, "invoke", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_LLVM_IR, "instruction_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_TLAPLUS, "function_evaluation", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_TLAPLUS, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_TLAPLUS, "bound_op", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_APEX, "method_invocation", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_PINE, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_MOJO, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_OBJECTSCRIPT_UDL, "class_method_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true,
|
||||
true),
|
||||
ENTRY(CBM_LANG_OBJECTSCRIPT_UDL, "method_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_OBJECTSCRIPT_UDL, "relative_dot_method", DIRECT_CALL, PRIMARY_EXTRACTOR, true,
|
||||
true),
|
||||
ENTRY(CBM_LANG_OBJECTSCRIPT_UDL, "macro", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
|
||||
ENTRY(CBM_LANG_OBJECTSCRIPT_ROUTINE, "extrinsic_function", DIRECT_CALL, PRIMARY_EXTRACTOR, true,
|
||||
true),
|
||||
ENTRY(CBM_LANG_OBJECTSCRIPT_ROUTINE, "routine_tag_call", DIRECT_CALL, PRIMARY_EXTRACTOR, true,
|
||||
true),
|
||||
};
|
||||
|
||||
#undef ENTRY
|
||||
|
||||
#define MANIFEST_COUNT (sizeof(CALL_NODE_MANIFEST) / sizeof(CALL_NODE_MANIFEST[0]))
|
||||
|
||||
_Static_assert(MANIFEST_COUNT == EXPECTED_HISTORICAL_CALL_NODE_TOTAL,
|
||||
"call-node manifest must contain exactly 219 entries");
|
||||
_Static_assert(EXPECTED_DIRECT_CALLS + EXPECTED_CONSTRUCTOR_CALLS + EXPECTED_OPERATOR_CALLS +
|
||||
EXPECTED_IMPLICIT_CALLS + EXPECTED_DSL_INVOCATIONS +
|
||||
EXPECTED_BUILD_DEPENDENCIES + EXPECTED_CALLEE_WRAPPERS +
|
||||
EXPECTED_ARGUMENT_WRAPPERS + EXPECTED_CONTROL_OR_DOCUMENT_NONCALLS ==
|
||||
EXPECTED_HISTORICAL_CALL_NODE_TOTAL,
|
||||
"semantic-role partition must cover the historical manifest");
|
||||
_Static_assert(EXPECTED_PRIMARY_OWNERS + EXPECTED_SYNTHETIC_OWNERS + EXPECTED_NO_CALL_EDGE_OWNERS ==
|
||||
EXPECTED_HISTORICAL_CALL_NODE_TOTAL,
|
||||
"extraction-owner partition must cover the historical manifest");
|
||||
_Static_assert(EXPECTED_PRIMARY_OWNERS == EXPECTED_ACTIVE_PRIMARY_TOTAL,
|
||||
"primary-owner and active-primary totals must agree");
|
||||
_Static_assert(EXPECTED_PRIMARY_OPERATOR_CALLS + EXPECTED_SYNTHETIC_OPERATOR_CALLS ==
|
||||
EXPECTED_OPERATOR_CALLS,
|
||||
"operator calls must have an explicit direct or synthetic owner");
|
||||
|
||||
static int manifest_match_count(CBMLanguage language, const char *node_kind) {
|
||||
int matches = 0;
|
||||
for (size_t i = 0; i < MANIFEST_COUNT; i++) {
|
||||
const CallNodeManifestEntry *entry = &CALL_NODE_MANIFEST[i];
|
||||
if (entry->language == language && strcmp(entry->node_kind, node_kind) == 0) {
|
||||
matches++;
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
static int spec_match_count(const CBMLangSpec *spec, const char *node_kind) {
|
||||
int matches = 0;
|
||||
if (!spec || !spec->call_node_types) {
|
||||
return 0;
|
||||
}
|
||||
for (int i = 0; spec->call_node_types[i]; i++) {
|
||||
if (strcmp(spec->call_node_types[i], node_kind) == 0) {
|
||||
matches++;
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
TEST(repro_call_node_manifest_is_unique_and_partitioned) {
|
||||
int failures = 0;
|
||||
int role_counts[CALL_SEMANTIC_ROLE_COUNT] = {0};
|
||||
int owner_counts[CALL_EXTRACTION_OWNER_COUNT] = {0};
|
||||
int historical_total = 0;
|
||||
int expected_primary_total = 0;
|
||||
int primary_operator_calls = 0;
|
||||
int synthetic_operator_calls = 0;
|
||||
|
||||
if (MANIFEST_COUNT != EXPECTED_HISTORICAL_CALL_NODE_TOTAL) {
|
||||
fprintf(stderr, " [call-node-manifest] invariant=table_total expected=%d actual=%zu\n",
|
||||
EXPECTED_HISTORICAL_CALL_NODE_TOTAL, MANIFEST_COUNT);
|
||||
failures++;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < MANIFEST_COUNT; i++) {
|
||||
const CallNodeManifestEntry *entry = &CALL_NODE_MANIFEST[i];
|
||||
if (entry->language < 0 || entry->language >= CBM_LANG_COUNT ||
|
||||
entry->language == CBM_LANG_NIM) {
|
||||
fprintf(stderr,
|
||||
" [call-node-manifest] invariant=invalid_language index=%zu language=%d\n", i,
|
||||
(int)entry->language);
|
||||
failures++;
|
||||
}
|
||||
if (!entry->node_kind || !entry->node_kind[0]) {
|
||||
fprintf(stderr,
|
||||
" [call-node-manifest] invariant=empty_node_kind index=%zu language=%d\n", i,
|
||||
(int)entry->language);
|
||||
failures++;
|
||||
}
|
||||
if (entry->semantic_role < DIRECT_CALL ||
|
||||
entry->semantic_role >= CALL_SEMANTIC_ROLE_COUNT) {
|
||||
fprintf(stderr,
|
||||
" [call-node-manifest] invariant=invalid_role index=%zu language=%d role=%d\n",
|
||||
i, (int)entry->language, (int)entry->semantic_role);
|
||||
failures++;
|
||||
} else {
|
||||
role_counts[entry->semantic_role]++;
|
||||
}
|
||||
if (entry->extraction_owner < PRIMARY_EXTRACTOR ||
|
||||
entry->extraction_owner >= CALL_EXTRACTION_OWNER_COUNT) {
|
||||
fprintf(stderr,
|
||||
" [call-node-manifest] invariant=invalid_owner index=%zu language=%d "
|
||||
"owner=%d\n",
|
||||
i, (int)entry->language, (int)entry->extraction_owner);
|
||||
failures++;
|
||||
} else {
|
||||
owner_counts[entry->extraction_owner]++;
|
||||
}
|
||||
|
||||
if (!entry->historically_registered) {
|
||||
fprintf(stderr,
|
||||
" [call-node-manifest] invariant=historical_snapshot_gap index=%zu "
|
||||
"language=%d node=%s\n",
|
||||
i, (int)entry->language, entry->node_kind);
|
||||
failures++;
|
||||
} else {
|
||||
historical_total++;
|
||||
}
|
||||
if (entry->expected_primary_registered) {
|
||||
expected_primary_total++;
|
||||
}
|
||||
|
||||
/*
|
||||
* Direct operator models are PRIMARY_EXTRACTOR entries. C++/CUDA
|
||||
* operator triggers owned by synthetic extraction must not also create
|
||||
* generic primary scopes or raw calls.
|
||||
*/
|
||||
if (entry->extraction_owner == SYNTHETIC_EXTRACTOR && entry->expected_primary_registered) {
|
||||
fprintf(stderr,
|
||||
" [call-node-manifest] invariant=synthetic_owner_registered_primary "
|
||||
"language=%d node=%s role=%d\n",
|
||||
(int)entry->language, entry->node_kind, (int)entry->semantic_role);
|
||||
failures++;
|
||||
}
|
||||
if (entry->extraction_owner == NO_CALL_EDGE && entry->expected_primary_registered) {
|
||||
fprintf(stderr,
|
||||
" [call-node-manifest] invariant=no_edge_owner_registered_primary "
|
||||
"language=%d node=%s role=%d\n",
|
||||
(int)entry->language, entry->node_kind, (int)entry->semantic_role);
|
||||
failures++;
|
||||
}
|
||||
if ((entry->extraction_owner == PRIMARY_EXTRACTOR) != entry->expected_primary_registered) {
|
||||
fprintf(stderr,
|
||||
" [call-node-manifest] invariant=primary_owner_membership_mismatch "
|
||||
"language=%d node=%s owner=%d expected_primary=%d\n",
|
||||
(int)entry->language, entry->node_kind, (int)entry->extraction_owner,
|
||||
entry->expected_primary_registered);
|
||||
failures++;
|
||||
}
|
||||
|
||||
if (entry->semantic_role == OPERATOR_CALL) {
|
||||
if (entry->extraction_owner == PRIMARY_EXTRACTOR) {
|
||||
primary_operator_calls++;
|
||||
} else if (entry->extraction_owner == SYNTHETIC_EXTRACTOR) {
|
||||
synthetic_operator_calls++;
|
||||
} else {
|
||||
fprintf(stderr,
|
||||
" [call-node-manifest] invariant=operator_without_extractor "
|
||||
"language=%d node=%s\n",
|
||||
(int)entry->language, entry->node_kind);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t j = i + 1; j < MANIFEST_COUNT; j++) {
|
||||
const CallNodeManifestEntry *other = &CALL_NODE_MANIFEST[j];
|
||||
if (entry->language == other->language && entry->node_kind && other->node_kind &&
|
||||
strcmp(entry->node_kind, other->node_kind) == 0) {
|
||||
fprintf(stderr,
|
||||
" [call-node-manifest] invariant=duplicate_table_entry language=%d "
|
||||
"node=%s first=%zu second=%zu\n",
|
||||
(int)entry->language, entry->node_kind, i, j);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (historical_total != EXPECTED_HISTORICAL_CALL_NODE_TOTAL ||
|
||||
expected_primary_total != EXPECTED_ACTIVE_PRIMARY_TOTAL) {
|
||||
fprintf(stderr,
|
||||
" [call-node-manifest] invariant=registration_partition historical=%d "
|
||||
"expected_primary=%d\n",
|
||||
historical_total, expected_primary_total);
|
||||
failures++;
|
||||
}
|
||||
|
||||
if (role_counts[DIRECT_CALL] != EXPECTED_DIRECT_CALLS ||
|
||||
role_counts[CONSTRUCTOR_CALL] != EXPECTED_CONSTRUCTOR_CALLS ||
|
||||
role_counts[OPERATOR_CALL] != EXPECTED_OPERATOR_CALLS ||
|
||||
role_counts[IMPLICIT_CALL] != EXPECTED_IMPLICIT_CALLS ||
|
||||
role_counts[DSL_INVOCATION] != EXPECTED_DSL_INVOCATIONS ||
|
||||
role_counts[BUILD_DEPENDENCY] != EXPECTED_BUILD_DEPENDENCIES ||
|
||||
role_counts[CALLEE_WRAPPER] != EXPECTED_CALLEE_WRAPPERS ||
|
||||
role_counts[ARGUMENT_WRAPPER] != EXPECTED_ARGUMENT_WRAPPERS ||
|
||||
role_counts[CONTROL_OR_DOCUMENT_NONCALL] != EXPECTED_CONTROL_OR_DOCUMENT_NONCALLS) {
|
||||
fprintf(stderr,
|
||||
" [call-node-manifest] invariant=semantic_role_partition direct=%d "
|
||||
"constructor=%d operator=%d implicit=%d dsl=%d build=%d callee_wrapper=%d "
|
||||
"argument_wrapper=%d control_or_document=%d\n",
|
||||
role_counts[DIRECT_CALL], role_counts[CONSTRUCTOR_CALL], role_counts[OPERATOR_CALL],
|
||||
role_counts[IMPLICIT_CALL], role_counts[DSL_INVOCATION],
|
||||
role_counts[BUILD_DEPENDENCY], role_counts[CALLEE_WRAPPER],
|
||||
role_counts[ARGUMENT_WRAPPER], role_counts[CONTROL_OR_DOCUMENT_NONCALL]);
|
||||
failures++;
|
||||
}
|
||||
|
||||
if (owner_counts[PRIMARY_EXTRACTOR] != EXPECTED_PRIMARY_OWNERS ||
|
||||
owner_counts[SYNTHETIC_EXTRACTOR] != EXPECTED_SYNTHETIC_OWNERS ||
|
||||
owner_counts[NO_CALL_EDGE] != EXPECTED_NO_CALL_EDGE_OWNERS) {
|
||||
fprintf(stderr,
|
||||
" [call-node-manifest] invariant=extraction_owner_partition primary=%d "
|
||||
"synthetic=%d no_call_edge=%d\n",
|
||||
owner_counts[PRIMARY_EXTRACTOR], owner_counts[SYNTHETIC_EXTRACTOR],
|
||||
owner_counts[NO_CALL_EDGE]);
|
||||
failures++;
|
||||
}
|
||||
|
||||
if (primary_operator_calls != EXPECTED_PRIMARY_OPERATOR_CALLS ||
|
||||
synthetic_operator_calls != EXPECTED_SYNTHETIC_OPERATOR_CALLS) {
|
||||
fprintf(stderr,
|
||||
" [call-node-manifest] invariant=operator_owner_partition primary=%d "
|
||||
"synthetic=%d\n",
|
||||
primary_operator_calls, synthetic_operator_calls);
|
||||
failures++;
|
||||
}
|
||||
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_call_node_manifest_live_specs_contain_only_expected_primary_entries) {
|
||||
int failures = 0;
|
||||
int runtime_total = 0;
|
||||
|
||||
for (int value = 0; value < CBM_LANG_COUNT; value++) {
|
||||
CBMLanguage language = (CBMLanguage)value;
|
||||
const CBMLangSpec *spec = cbm_lang_spec(language);
|
||||
|
||||
/* The registry reproduction owns Nim's stale enum and Studio Export's
|
||||
* transform-only, intentionally grammar-free classification. */
|
||||
if (language == CBM_LANG_NIM || language == CBM_LANG_OBJECTSCRIPT_EXPORT) {
|
||||
continue;
|
||||
}
|
||||
if (!spec || spec->language != language) {
|
||||
fprintf(stderr,
|
||||
" [call-node-manifest] invariant=live_spec_missing language=%d actual=%d\n",
|
||||
value, spec ? (int)spec->language : -1);
|
||||
failures++;
|
||||
continue;
|
||||
}
|
||||
if (!spec->call_node_types) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int i = 0; spec->call_node_types[i]; i++) {
|
||||
const char *node_kind = spec->call_node_types[i];
|
||||
runtime_total++;
|
||||
|
||||
int table_matches = manifest_match_count(language, node_kind);
|
||||
if (table_matches != 1) {
|
||||
fprintf(stderr,
|
||||
" [call-node-manifest] invariant=live_to_table language=%d node=%s "
|
||||
"matches=%d\n",
|
||||
value, node_kind, table_matches);
|
||||
failures++;
|
||||
} else {
|
||||
for (size_t manifest_index = 0; manifest_index < MANIFEST_COUNT; manifest_index++) {
|
||||
const CallNodeManifestEntry *entry = &CALL_NODE_MANIFEST[manifest_index];
|
||||
if (entry->language == language && strcmp(entry->node_kind, node_kind) == 0 &&
|
||||
!entry->expected_primary_registered) {
|
||||
fprintf(stderr,
|
||||
" [call-node-manifest] invariant=unexpected_primary_registration "
|
||||
"language=%d node=%s role=%d owner=%d\n",
|
||||
value, node_kind, (int)entry->semantic_role,
|
||||
(int)entry->extraction_owner);
|
||||
failures++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int j = 0; j < i; j++) {
|
||||
if (strcmp(spec->call_node_types[j], node_kind) == 0) {
|
||||
fprintf(stderr,
|
||||
" [call-node-manifest] invariant=duplicate_live_kind language=%d "
|
||||
"node=%s first=%d second=%d\n",
|
||||
value, node_kind, j, i);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (runtime_total != EXPECTED_ACTIVE_PRIMARY_TOTAL) {
|
||||
fprintf(stderr, " [call-node-manifest] invariant=runtime_total expected=%d actual=%d\n",
|
||||
EXPECTED_ACTIVE_PRIMARY_TOTAL, runtime_total);
|
||||
failures++;
|
||||
}
|
||||
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_call_node_manifest_live_membership_matches_expected_primary_boolean) {
|
||||
int failures = 0;
|
||||
|
||||
for (size_t i = 0; i < MANIFEST_COUNT; i++) {
|
||||
const CallNodeManifestEntry *entry = &CALL_NODE_MANIFEST[i];
|
||||
const CBMLangSpec *spec = cbm_lang_spec(entry->language);
|
||||
if (!spec || spec->language != entry->language) {
|
||||
fprintf(stderr,
|
||||
" [call-node-manifest] invariant=table_spec_missing index=%zu language=%d "
|
||||
"actual=%d\n",
|
||||
i, (int)entry->language, spec ? (int)spec->language : -1);
|
||||
failures++;
|
||||
continue;
|
||||
}
|
||||
|
||||
int live_matches = spec_match_count(spec, entry->node_kind);
|
||||
int expected_matches = entry->expected_primary_registered ? 1 : 0;
|
||||
if (live_matches != expected_matches) {
|
||||
fprintf(stderr,
|
||||
" [call-node-manifest] invariant=table_to_live index=%zu language=%d node=%s "
|
||||
"expected=%d matches=%d role=%d owner=%d historical=%d\n",
|
||||
i, (int)entry->language, entry->node_kind, expected_matches, live_matches,
|
||||
(int)entry->semantic_role, (int)entry->extraction_owner,
|
||||
entry->historically_registered);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(repro_call_node_manifest) {
|
||||
RUN_TEST(repro_call_node_manifest_is_unique_and_partitioned);
|
||||
RUN_TEST(repro_call_node_manifest_live_specs_contain_only_expected_primary_entries);
|
||||
RUN_TEST(repro_call_node_manifest_live_membership_matches_expected_primary_boolean);
|
||||
}
|
||||
@@ -0,0 +1,678 @@
|
||||
/*
|
||||
* repro_call_scope_usages.c — exact reference semantics inside call-shaped ASTs.
|
||||
*
|
||||
* A CALLS edge replaces only the exact invoked callee occurrence. Receivers,
|
||||
* computed keys, arguments, and nested callback bodies remain ordinary value
|
||||
* references. Several language specs also classify non-invocation containers
|
||||
* as call nodes; those containers must neither erase their whole subtree nor
|
||||
* fabricate calls from an arbitrary child. Every assertion uses exact counts
|
||||
* so duplicate CALLS/USAGE emissions cannot masquerade as a successful fix.
|
||||
*/
|
||||
#include "test_framework.h"
|
||||
#include "cbm.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static int qn_ends_with(const char *qn, const char *name) {
|
||||
if (!qn || !name)
|
||||
return 0;
|
||||
const char *last = strrchr(qn, '.');
|
||||
return strcmp(last ? last + 1 : qn, name) == 0;
|
||||
}
|
||||
|
||||
static const char *call_short_name(const char *name) {
|
||||
if (!name)
|
||||
return NULL;
|
||||
const char *short_name = strrchr(name, '.');
|
||||
short_name = short_name ? short_name + 1 : name;
|
||||
const char *scope = strrchr(short_name, ':');
|
||||
return scope ? scope + 1 : short_name;
|
||||
}
|
||||
|
||||
static int usage_count(const CBMFileResult *r, const char *caller, const char *target) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < r->usages.count; i++) {
|
||||
const CBMUsage *usage = &r->usages.items[i];
|
||||
if (usage->kind == CBM_USAGE_VALUE && usage->ref_name &&
|
||||
strcmp(usage->ref_name, target) == 0 &&
|
||||
(!caller || qn_ends_with(usage->enclosing_func_qn, caller))) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static int call_reference_count(const CBMFileResult *r, const char *caller, const char *target) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < r->usages.count; i++) {
|
||||
const CBMUsage *usage = &r->usages.items[i];
|
||||
if (usage->kind == CBM_USAGE_CALL_REFERENCE && usage->ref_name &&
|
||||
strcmp(usage->ref_name, target) == 0 &&
|
||||
(!caller || qn_ends_with(usage->enclosing_func_qn, caller))) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static int call_count(const CBMFileResult *r, const char *caller, const char *target) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < r->calls.count; i++) {
|
||||
const CBMCall *call = &r->calls.items[i];
|
||||
const char *callee = call_short_name(call->callee_name);
|
||||
if (callee && strcmp(callee, target) == 0 &&
|
||||
(!caller || qn_ends_with(call->enclosing_func_qn, caller))) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static int exact_usage_count(const CBMFileResult *r, const char *caller_qn, const char *target) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < r->usages.count; i++) {
|
||||
const CBMUsage *usage = &r->usages.items[i];
|
||||
if (usage->kind == CBM_USAGE_VALUE && usage->ref_name && usage->enclosing_func_qn &&
|
||||
strcmp(usage->ref_name, target) == 0 &&
|
||||
strcmp(usage->enclosing_func_qn, caller_qn) == 0) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static int exact_call_count(const CBMFileResult *r, const char *caller_qn, const char *callee) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < r->calls.count; i++) {
|
||||
const CBMCall *call = &r->calls.items[i];
|
||||
if (call->callee_name && call->enclosing_func_qn &&
|
||||
strcmp(call->callee_name, callee) == 0 &&
|
||||
strcmp(call->enclosing_func_qn, caller_qn) == 0) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static int exact_definition_count(const CBMFileResult *r, const char *name,
|
||||
const char *qualified_name) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < r->defs.count; i++) {
|
||||
const CBMDefinition *def = &r->defs.items[i];
|
||||
if (def->name && def->qualified_name && strcmp(def->name, name) == 0 &&
|
||||
strcmp(def->qualified_name, qualified_name) == 0) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static int definition_count(const CBMFileResult *r, const char *name) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < r->defs.count; i++) {
|
||||
const CBMDefinition *def = &r->defs.items[i];
|
||||
if (def->name && strcmp(def->name, name) == 0)
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static CBMFileResult *extract_case(const char *tag, const char *source, CBMLanguage language,
|
||||
const char *filename) {
|
||||
CBMFileResult *r =
|
||||
cbm_extract_file(source, (int)strlen(source), language, "repro", filename, 0, NULL, NULL);
|
||||
if (!r)
|
||||
fprintf(stderr, " [call-scope] case=%s invariant=extract_result expected=non-null\n", tag);
|
||||
return r;
|
||||
}
|
||||
|
||||
#define CHECK_COUNT(tag, invariant, expression, expected) \
|
||||
do { \
|
||||
int actual_count = (expression); \
|
||||
if (actual_count != (expected)) { \
|
||||
fprintf(stderr, " [call-scope] case=%s invariant=%s expected=%d actual=%d\n", (tag), \
|
||||
(invariant), (expected), actual_count); \
|
||||
failures++; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define CHECK_CLEAN(tag, result) \
|
||||
do { \
|
||||
if ((result)->has_error || (result)->parse_incomplete) { \
|
||||
fprintf(stderr, " [call-scope] case=%s invariant=valid_fixture expected=clean\n", \
|
||||
(tag)); \
|
||||
failures++; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* The two textual `foo` occurrences have different roles: the first is the
|
||||
* invoked callee, while the second is a function value passed as an argument. */
|
||||
TEST(repro_call_scope_same_spelling_callee_and_argument) {
|
||||
static const char source[] = "function foo() {}\n"
|
||||
"function run() { foo(foo); }\n";
|
||||
CBMFileResult *r = extract_case("same_spelling", source, CBM_LANG_JAVASCRIPT, "main.js");
|
||||
ASSERT_NOT_NULL(r);
|
||||
int failures = 0;
|
||||
CHECK_CLEAN("same_spelling", r);
|
||||
CHECK_COUNT("same_spelling", "foo_definition", definition_count(r, "foo"), 1);
|
||||
CHECK_COUNT("same_spelling", "callee_call", call_count(r, "run", "foo"), 1);
|
||||
CHECK_COUNT("same_spelling", "argument_usage_only", usage_count(r, "run", "foo"), 1);
|
||||
cbm_free_result(r);
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Nested calls need independent callee identity. The outer call must not hide
|
||||
* either its direct function-value argument or the inner call's value argument. */
|
||||
TEST(repro_call_scope_nested_calls_retain_each_argument) {
|
||||
static const char source[] = "const value = 1;\n"
|
||||
"function handler(): void {}\n"
|
||||
"function inner(input: number): number { return input; }\n"
|
||||
"function outer(cb: () => void, input: number): void {}\n"
|
||||
"function run(): void { outer(handler, inner(value)); }\n";
|
||||
CBMFileResult *r = extract_case("nested_calls", source, CBM_LANG_TYPESCRIPT, "main.ts");
|
||||
ASSERT_NOT_NULL(r);
|
||||
int failures = 0;
|
||||
CHECK_CLEAN("nested_calls", r);
|
||||
CHECK_COUNT("nested_calls", "outer_call", call_count(r, "run", "outer"), 1);
|
||||
CHECK_COUNT("nested_calls", "inner_call", call_count(r, "run", "inner"), 1);
|
||||
CHECK_COUNT("nested_calls", "handler_argument_usage", usage_count(r, "run", "handler"), 1);
|
||||
CHECK_COUNT("nested_calls", "value_argument_usage", usage_count(r, "run", "value"), 1);
|
||||
CHECK_COUNT("nested_calls", "outer_not_usage", usage_count(r, "run", "outer"), 0);
|
||||
CHECK_COUNT("nested_calls", "inner_not_usage", usage_count(r, "run", "inner"), 0);
|
||||
cbm_free_result(r);
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A callback body is executable code, not part of the registrar's callee. Its
|
||||
* references remain visible even though the arrow function is an argument. */
|
||||
TEST(repro_call_scope_inline_callback_body_usage) {
|
||||
static const char source[] = "const watched = 1;\n"
|
||||
"function register(callback) {}\n"
|
||||
"function run() { register(() => { watched; }); }\n";
|
||||
CBMFileResult *r = extract_case("inline_callback", source, CBM_LANG_JAVASCRIPT, "main.js");
|
||||
ASSERT_NOT_NULL(r);
|
||||
int failures = 0;
|
||||
CHECK_CLEAN("inline_callback", r);
|
||||
CHECK_COUNT("inline_callback", "registrar_call", call_count(r, "run", "register"), 1);
|
||||
CHECK_COUNT("inline_callback", "callback_body_usage", usage_count(r, NULL, "watched"), 1);
|
||||
CHECK_COUNT("inline_callback", "registrar_not_usage", usage_count(r, "run", "register"), 0);
|
||||
cbm_free_result(r);
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* For a member call only `method` is the direct callee. The receiver and
|
||||
* arguments are values. For a computed call, both receiver and key are values. */
|
||||
TEST(repro_call_scope_member_and_computed_components) {
|
||||
static const char source[] =
|
||||
"class Receiver { method(value: number): void {} }\n"
|
||||
"const receiver = new Receiver();\n"
|
||||
"const key = 'method';\n"
|
||||
"const payload = 1;\n"
|
||||
"function run(): void { receiver.method(payload); receiver[key](payload); }\n";
|
||||
CBMFileResult *r = extract_case("member_computed", source, CBM_LANG_TYPESCRIPT, "main.ts");
|
||||
ASSERT_NOT_NULL(r);
|
||||
int failures = 0;
|
||||
CHECK_CLEAN("member_computed", r);
|
||||
CHECK_COUNT("member_computed", "method_call", call_count(r, "run", "method"), 1);
|
||||
CHECK_COUNT("member_computed", "receiver_usages", usage_count(r, "run", "receiver"), 2);
|
||||
CHECK_COUNT("member_computed", "computed_key_usage", usage_count(r, "run", "key"), 1);
|
||||
CHECK_COUNT("member_computed", "argument_usages", usage_count(r, "run", "payload"), 2);
|
||||
CHECK_COUNT("member_computed", "terminal_method_not_usage", usage_count(r, "run", "method"), 0);
|
||||
cbm_free_result(r);
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Constructor and generic-call syntax must not change argument semantics. The
|
||||
* type argument belongs to type-reference extraction; both handler expressions
|
||||
* are ordinary value usages. */
|
||||
TEST(repro_call_scope_constructor_and_generic_arguments) {
|
||||
static const char source[] =
|
||||
"interface Payload {}\n"
|
||||
"class Box { constructor(callback: () => void) {} }\n"
|
||||
"function handler(): void {}\n"
|
||||
"function accept<T>(callback: () => void): void {}\n"
|
||||
"function run(): void { new Box(handler); accept<Payload>(handler); }\n";
|
||||
CBMFileResult *r = extract_case("constructor_generic", source, CBM_LANG_TYPESCRIPT, "main.ts");
|
||||
ASSERT_NOT_NULL(r);
|
||||
int failures = 0;
|
||||
CHECK_CLEAN("constructor_generic", r);
|
||||
CHECK_COUNT("constructor_generic", "constructor_call", call_count(r, "run", "Box"), 1);
|
||||
CHECK_COUNT("constructor_generic", "generic_call", call_count(r, "run", "accept"), 1);
|
||||
CHECK_COUNT("constructor_generic", "handler_argument_usages", usage_count(r, "run", "handler"),
|
||||
2);
|
||||
CHECK_COUNT("constructor_generic", "constructor_not_usage", usage_count(r, "run", "Box"), 0);
|
||||
CHECK_COUNT("constructor_generic", "generic_callee_not_usage", usage_count(r, "run", "accept"),
|
||||
0);
|
||||
cbm_free_result(r);
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_call_scope_c_function_pointer_invocation_and_arguments) {
|
||||
static const char source[] = "typedef void (*Callback)(void);\n"
|
||||
"void handler(void) {}\n"
|
||||
"void invoke(Callback first, Callback second) {}\n"
|
||||
"Callback shared = 0;\n"
|
||||
"void run(void) { invoke(shared, handler); shared(); }\n";
|
||||
CBMFileResult *r = extract_case("c_function_pointer", source, CBM_LANG_C, "main.c");
|
||||
ASSERT_NOT_NULL(r);
|
||||
int failures = 0;
|
||||
CHECK_CLEAN("c_function_pointer", r);
|
||||
CHECK_COUNT("c_function_pointer", "invoke_call", call_count(r, "run", "invoke"), 1);
|
||||
CHECK_COUNT("c_function_pointer", "pointer_call", call_count(r, "run", "shared"), 1);
|
||||
CHECK_COUNT("c_function_pointer", "pointer_argument_usage", usage_count(r, "run", "shared"), 1);
|
||||
CHECK_COUNT("c_function_pointer", "function_argument_usage", usage_count(r, "run", "handler"),
|
||||
1);
|
||||
CHECK_COUNT("c_function_pointer", "invoke_not_usage", usage_count(r, "run", "invoke"), 0);
|
||||
cbm_free_result(r);
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_call_scope_cpp_function_pointer_invocation_and_arguments) {
|
||||
static const char source[] = "using Callback = void (*)();\n"
|
||||
"void handler() {}\n"
|
||||
"void invoke(Callback first, Callback second) {}\n"
|
||||
"Callback shared = nullptr;\n"
|
||||
"void run() { invoke(shared, handler); shared(); }\n";
|
||||
CBMFileResult *r = extract_case("cpp_function_pointer", source, CBM_LANG_CPP, "main.cpp");
|
||||
ASSERT_NOT_NULL(r);
|
||||
int failures = 0;
|
||||
CHECK_CLEAN("cpp_function_pointer", r);
|
||||
CHECK_COUNT("cpp_function_pointer", "invoke_call", call_count(r, "run", "invoke"), 1);
|
||||
CHECK_COUNT("cpp_function_pointer", "pointer_call", call_count(r, "run", "shared"), 1);
|
||||
CHECK_COUNT("cpp_function_pointer", "pointer_argument_usage", usage_count(r, "run", "shared"),
|
||||
1);
|
||||
CHECK_COUNT("cpp_function_pointer", "function_argument_usage", usage_count(r, "run", "handler"),
|
||||
1);
|
||||
CHECK_COUNT("cpp_function_pointer", "invoke_not_usage", usage_count(r, "run", "invoke"), 0);
|
||||
cbm_free_result(r);
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_call_scope_python_keyword_argument) {
|
||||
static const char source[] = "def handler():\n pass\n"
|
||||
"def accept(callback=None):\n pass\n"
|
||||
"def run():\n accept(callback=handler)\n";
|
||||
CBMFileResult *r = extract_case("python_keyword", source, CBM_LANG_PYTHON, "main.py");
|
||||
ASSERT_NOT_NULL(r);
|
||||
int failures = 0;
|
||||
CHECK_CLEAN("python_keyword", r);
|
||||
CHECK_COUNT("python_keyword", "accept_call", call_count(r, "run", "accept"), 1);
|
||||
CHECK_COUNT("python_keyword", "keyword_value_usage", usage_count(r, "run", "handler"), 1);
|
||||
CHECK_COUNT("python_keyword", "keyword_label_not_usage", usage_count(r, "run", "callback"), 0);
|
||||
CHECK_COUNT("python_keyword", "callee_not_usage", usage_count(r, "run", "accept"), 0);
|
||||
cbm_free_result(r);
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Python registers `with_statement` as a call node. The actual manager()
|
||||
* invocation is a call, but the with body is not part of that callee. */
|
||||
TEST(repro_call_scope_python_with_body_usage) {
|
||||
static const char source[] = "watched = 1\n"
|
||||
"def manager():\n pass\n"
|
||||
"def run():\n with manager():\n watched\n";
|
||||
CBMFileResult *r = extract_case("python_with", source, CBM_LANG_PYTHON, "main.py");
|
||||
ASSERT_NOT_NULL(r);
|
||||
int failures = 0;
|
||||
CHECK_CLEAN("python_with", r);
|
||||
CHECK_COUNT("python_with", "run_definition", definition_count(r, "run"), 1);
|
||||
CHECK_COUNT("python_with", "manager_call", call_count(r, "run", "manager"), 1);
|
||||
CHECK_COUNT("python_with", "with_body_usage", usage_count(r, "run", "watched"), 1);
|
||||
CHECK_COUNT("python_with", "manager_not_usage", usage_count(r, "run", "manager"), 0);
|
||||
cbm_free_result(r);
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Rust macro_invocation is call-like, but its token-tree arguments remain
|
||||
* value references and must survive the macro callee's scope. */
|
||||
TEST(repro_call_scope_rust_macro_argument) {
|
||||
static const char source[] = "static WATCHED: i32 = 1;\n"
|
||||
"macro_rules! probe { ($value:expr) => { $value }; }\n"
|
||||
"fn run() { probe!(WATCHED); }\n";
|
||||
CBMFileResult *r = extract_case("rust_macro", source, CBM_LANG_RUST, "main.rs");
|
||||
ASSERT_NOT_NULL(r);
|
||||
int failures = 0;
|
||||
CHECK_CLEAN("rust_macro", r);
|
||||
CHECK_COUNT("rust_macro", "run_definition", definition_count(r, "run"), 1);
|
||||
CHECK_COUNT("rust_macro", "macro_call", call_count(r, "run", "probe"), 1);
|
||||
CHECK_COUNT("rust_macro", "macro_argument_usage", usage_count(r, "run", "WATCHED"), 1);
|
||||
CHECK_COUNT("rust_macro", "macro_callee_not_usage", usage_count(r, "run", "probe"), 0);
|
||||
cbm_free_result(r);
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Elixir binary_operator is registered as a call node, but `watched + 1` is
|
||||
* not a call to watched. This uses caller-independent counts because Elixir's
|
||||
* callable attribution has a separately documented Module-scope drift. */
|
||||
TEST(repro_call_scope_elixir_binary_operator_operand) {
|
||||
static const char source[] = "defmodule Sample do\n"
|
||||
" def run(), do: watched + 1\n"
|
||||
"end\n";
|
||||
CBMFileResult *r = extract_case("elixir_binary", source, CBM_LANG_ELIXIR, "sample.ex");
|
||||
ASSERT_NOT_NULL(r);
|
||||
int failures = 0;
|
||||
CHECK_CLEAN("elixir_binary", r);
|
||||
CHECK_COUNT("elixir_binary", "run_definition", definition_count(r, "run"), 1);
|
||||
CHECK_COUNT("elixir_binary", "operator_call", call_count(r, NULL, "+"), 1);
|
||||
CHECK_COUNT("elixir_binary", "operand_usage", usage_count(r, NULL, "watched"), 1);
|
||||
CHECK_COUNT("elixir_binary", "operand_not_call", call_count(r, NULL, "watched"), 0);
|
||||
cbm_free_result(r);
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Fortran keyword_argument is an argument container, not another invocation.
|
||||
* The keyword label is metadata; only its value is a usage. */
|
||||
TEST(repro_call_scope_fortran_keyword_argument) {
|
||||
static const char source[] = "module sample\n"
|
||||
" integer :: watched\n"
|
||||
"contains\n"
|
||||
" integer function consume(value)\n"
|
||||
" integer, intent(in) :: value\n"
|
||||
" consume = value\n"
|
||||
" end function consume\n"
|
||||
" integer function run()\n"
|
||||
" run = consume(value=watched)\n"
|
||||
" end function run\n"
|
||||
"end module sample\n";
|
||||
CBMFileResult *r = extract_case("fortran_keyword", source, CBM_LANG_FORTRAN, "main.f90");
|
||||
ASSERT_NOT_NULL(r);
|
||||
int failures = 0;
|
||||
CHECK_CLEAN("fortran_keyword", r);
|
||||
CHECK_COUNT("fortran_keyword", "run_definition", definition_count(r, "run"), 1);
|
||||
CHECK_COUNT("fortran_keyword", "consume_call", call_count(r, NULL, "consume"), 1);
|
||||
CHECK_COUNT("fortran_keyword", "keyword_value_usage", usage_count(r, "run", "watched"), 1);
|
||||
CHECK_COUNT("fortran_keyword", "keyword_label_not_usage", usage_count(r, "run", "value"), 0);
|
||||
CHECK_COUNT("fortran_keyword", "keyword_label_not_call", call_count(r, "run", "value"), 0);
|
||||
cbm_free_result(r);
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A separate gap discovered while building the keyword-argument control:
|
||||
* Fortran's `call` statement is registered as call metadata, yet the direct
|
||||
* subroutine invocation currently emits no CBMCall. Keep this independent from
|
||||
* keyword labels so its future fix cannot hide behind another emitted call. */
|
||||
TEST(repro_fortran_subroutine_call_is_extracted) {
|
||||
static const char source[] = "subroutine consume(value)\n"
|
||||
" integer :: value\n"
|
||||
"end subroutine consume\n"
|
||||
"subroutine run()\n"
|
||||
" call consume(watched)\n"
|
||||
"end subroutine run\n";
|
||||
CBMFileResult *r =
|
||||
extract_case("fortran_subroutine_call", source, CBM_LANG_FORTRAN, "main.f90");
|
||||
ASSERT_NOT_NULL(r);
|
||||
int failures = 0;
|
||||
CHECK_CLEAN("fortran_subroutine_call", r);
|
||||
CHECK_COUNT("fortran_subroutine_call", "consume_definition", definition_count(r, "consume"), 1);
|
||||
CHECK_COUNT("fortran_subroutine_call", "run_definition", definition_count(r, "run"), 1);
|
||||
CHECK_COUNT("fortran_subroutine_call", "total_calls", r->calls.count, 1);
|
||||
CHECK_COUNT("fortran_subroutine_call", "consume_call_from_run", call_count(r, "run", "consume"),
|
||||
1);
|
||||
cbm_free_result(r);
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Kotlin navigation_expression also represents ordinary property access. A
|
||||
* property read must retain both reference components and emit no call. */
|
||||
TEST(repro_call_scope_kotlin_property_navigation) {
|
||||
static const char source[] = "class Holder(val value: Int)\n"
|
||||
"val box = Holder(1)\n"
|
||||
"fun run(): Int = box.value\n";
|
||||
CBMFileResult *r = extract_case("kotlin_navigation", source, CBM_LANG_KOTLIN, "Main.kt");
|
||||
ASSERT_NOT_NULL(r);
|
||||
int failures = 0;
|
||||
CHECK_CLEAN("kotlin_navigation", r);
|
||||
CHECK_COUNT("kotlin_navigation", "run_definition", definition_count(r, "run"), 1);
|
||||
CHECK_COUNT("kotlin_navigation", "receiver_usage", usage_count(r, "run", "box"), 1);
|
||||
CHECK_COUNT("kotlin_navigation", "property_usage", usage_count(r, "run", "value"), 1);
|
||||
CHECK_COUNT("kotlin_navigation", "receiver_not_call", call_count(r, "run", "box"), 0);
|
||||
cbm_free_result(r);
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* PHP 8.1 first-class callable syntax evaluates a function reference without
|
||||
* invoking it. The pinned grammar exposes the ordinary bare, member,
|
||||
* scoped/static, and nullsafe call-expression nodes plus variadic_placeholder;
|
||||
* CHECK_CLEAN guards the combinations used below. A real variadic_unpacking
|
||||
* argument is still an invocation, so keep it as a positive discriminator. */
|
||||
TEST(repro_call_scope_php_first_class_callable) {
|
||||
static const char source[] = "<?php\n"
|
||||
"function bare_handler(): void {}\n"
|
||||
"function foo(...$values): void {}\n"
|
||||
"function accept(callable $callback): void {}\n"
|
||||
"class CallbackTarget {\n"
|
||||
" public function member_handler(): void {}\n"
|
||||
" public function nullable_handler(): void {}\n"
|
||||
" public static function static_handler(): void {}\n"
|
||||
"}\n"
|
||||
"function run($receiver, $nullable, array $args): void {\n"
|
||||
" accept(bare_handler(...));\n"
|
||||
" accept($receiver->member_handler(...));\n"
|
||||
" accept(CallbackTarget::static_handler(...));\n"
|
||||
" accept($nullable?->nullable_handler(...));\n"
|
||||
" foo(...$args);\n"
|
||||
"}\n";
|
||||
CBMFileResult *r = extract_case("php_first_class", source, CBM_LANG_PHP, "main.php");
|
||||
ASSERT_NOT_NULL(r);
|
||||
int failures = 0;
|
||||
CHECK_CLEAN("php_first_class", r);
|
||||
CHECK_COUNT("php_first_class", "total_calls", r->calls.count, 5);
|
||||
CHECK_COUNT("php_first_class", "accept_calls", call_count(r, "run", "accept"), 4);
|
||||
CHECK_COUNT("php_first_class", "bare_reference_usage", usage_count(r, "run", "bare_handler"),
|
||||
0);
|
||||
CHECK_COUNT("php_first_class", "bare_call_reference",
|
||||
call_reference_count(r, "run", "bare_handler"), 1);
|
||||
CHECK_COUNT("php_first_class", "bare_reference_not_invoked",
|
||||
call_count(r, "run", "bare_handler"), 0);
|
||||
CHECK_COUNT("php_first_class", "member_receiver_usage", usage_count(r, "run", "$receiver"), 1);
|
||||
CHECK_COUNT("php_first_class", "member_reference_usage",
|
||||
usage_count(r, "run", "member_handler"), 0);
|
||||
CHECK_COUNT("php_first_class", "member_call_reference",
|
||||
call_reference_count(r, "run", "member_handler"), 1);
|
||||
CHECK_COUNT("php_first_class", "member_reference_not_invoked",
|
||||
call_count(r, "run", "member_handler"), 0);
|
||||
CHECK_COUNT("php_first_class", "static_scope_usage", usage_count(r, "run", "CallbackTarget"),
|
||||
1);
|
||||
CHECK_COUNT("php_first_class", "static_reference_usage",
|
||||
usage_count(r, "run", "static_handler"), 0);
|
||||
CHECK_COUNT("php_first_class", "static_call_reference",
|
||||
call_reference_count(r, "run", "static_handler"), 1);
|
||||
CHECK_COUNT("php_first_class", "static_reference_not_invoked",
|
||||
call_count(r, "run", "static_handler"), 0);
|
||||
CHECK_COUNT("php_first_class", "nullsafe_receiver_usage", usage_count(r, "run", "$nullable"),
|
||||
1);
|
||||
CHECK_COUNT("php_first_class", "nullsafe_reference_usage",
|
||||
usage_count(r, "run", "nullable_handler"), 0);
|
||||
CHECK_COUNT("php_first_class", "nullsafe_call_reference",
|
||||
call_reference_count(r, "run", "nullable_handler"), 1);
|
||||
CHECK_COUNT("php_first_class", "nullsafe_reference_not_invoked",
|
||||
call_count(r, "run", "nullable_handler"), 0);
|
||||
CHECK_COUNT("php_first_class", "unpacking_argument_usage", usage_count(r, "run", "$args"), 1);
|
||||
CHECK_COUNT("php_first_class", "unpacking_is_invocation", call_count(r, "run", "foo"), 1);
|
||||
CHECK_COUNT("php_first_class", "invoked_callee_not_usage", usage_count(r, "run", "foo"), 0);
|
||||
cbm_free_result(r);
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* MAX_SCOPES is currently a fixed 64-entry stack. The 65th nested function is
|
||||
* silently discarded, so calls in its body inherit the wrong enclosing QN.
|
||||
* The later sibling is an independent pop-recovery control: it must remain
|
||||
* attributed to itself rather than inheriting any deep or stale scope. */
|
||||
TEST(repro_scope_stack_preserves_deepest_function) {
|
||||
enum { FUNCTION_DEPTH = 65, SOURCE_CAPACITY = 8192 };
|
||||
char *source = calloc(SOURCE_CAPACITY, 1);
|
||||
ASSERT_NOT_NULL(source);
|
||||
|
||||
size_t used = (size_t)snprintf(source, SOURCE_CAPACITY, "function target() {}\n");
|
||||
for (int depth = 0; depth < FUNCTION_DEPTH && used < SOURCE_CAPACITY; depth++) {
|
||||
used +=
|
||||
(size_t)snprintf(source + used, SOURCE_CAPACITY - used, "function f%d() {\n", depth);
|
||||
}
|
||||
used += (size_t)snprintf(source + used, SOURCE_CAPACITY - used, "target();\n");
|
||||
for (int depth = 0; depth < FUNCTION_DEPTH && used < SOURCE_CAPACITY; depth++) {
|
||||
used += (size_t)snprintf(source + used, SOURCE_CAPACITY - used, "}\n");
|
||||
}
|
||||
used += (size_t)snprintf(source + used, SOURCE_CAPACITY - used,
|
||||
"function sibling() { target(); }\n");
|
||||
ASSERT_TRUE(used < SOURCE_CAPACITY);
|
||||
|
||||
CBMFileResult *r = extract_case("deep_scope", source, CBM_LANG_JAVASCRIPT, "deep.js");
|
||||
free(source);
|
||||
ASSERT_NOT_NULL(r);
|
||||
int failures = 0;
|
||||
CHECK_CLEAN("deep_scope", r);
|
||||
CHECK_COUNT("deep_scope", "deepest_definition", definition_count(r, "f64"), 1);
|
||||
CHECK_COUNT("deep_scope", "sibling_definition", definition_count(r, "sibling"), 1);
|
||||
CHECK_COUNT("deep_scope", "target_call_total", call_count(r, NULL, "target"), 2);
|
||||
CHECK_COUNT("deep_scope", "penultimate_scope_not_inherited", call_count(r, "f63", "target"), 0);
|
||||
CHECK_COUNT("deep_scope", "deepest_call_attribution", call_count(r, "f64", "target"), 1);
|
||||
CHECK_COUNT("deep_scope", "sibling_pop_recovery_control", call_count(r, "sibling", "target"),
|
||||
1);
|
||||
cbm_free_result(r);
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Declarations and simple assignment targets are not references. True reads
|
||||
* remain usages, including the read side of compound assignments and updates.
|
||||
* Distinct names prevent one role from falsely satisfying another assertion. */
|
||||
TEST(repro_usage_declarations_are_not_references) {
|
||||
static const char source[] = "function run(unusedParameter: number,\n"
|
||||
" usedParameter: number): number {\n"
|
||||
" let assignmentOnly: number;\n"
|
||||
" assignmentOnly = 1;\n"
|
||||
" let localValue = usedParameter + usedParameter;\n"
|
||||
" let compoundValue = 0;\n"
|
||||
" compoundValue += 1;\n"
|
||||
" let updatedValue = 0;\n"
|
||||
" updatedValue++;\n"
|
||||
" return localValue + localValue;\n"
|
||||
"}\n";
|
||||
CBMFileResult *r = extract_case("declaration_usage", source, CBM_LANG_TYPESCRIPT, "main.ts");
|
||||
ASSERT_NOT_NULL(r);
|
||||
int failures = 0;
|
||||
CHECK_CLEAN("declaration_usage", r);
|
||||
CHECK_COUNT("declaration_usage", "run_definition", definition_count(r, "run"), 1);
|
||||
CHECK_COUNT("declaration_usage", "unused_parameter_not_usage",
|
||||
usage_count(r, "run", "unusedParameter"), 0);
|
||||
CHECK_COUNT("declaration_usage", "assignment_only_binding_not_usage",
|
||||
usage_count(r, "run", "assignmentOnly"), 0);
|
||||
CHECK_COUNT("declaration_usage", "used_parameter_reads", usage_count(r, "run", "usedParameter"),
|
||||
2);
|
||||
CHECK_COUNT("declaration_usage", "local_value_reads", usage_count(r, "run", "localValue"), 2);
|
||||
CHECK_COUNT("declaration_usage", "compound_assignment_reads_value",
|
||||
usage_count(r, "run", "compoundValue"), 1);
|
||||
CHECK_COUNT("declaration_usage", "update_reads_value", usage_count(r, "run", "updatedValue"),
|
||||
1);
|
||||
cbm_free_result(r);
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ObjectScript's typed receiver map must not change either ownership or
|
||||
* CALL/USAGE roles: the method is the exact callee and the parameter passed to
|
||||
* it remains one value usage in Sample.Behavior.run. */
|
||||
TEST(repro_call_scope_objectscript_typed_receiver_exact_owner) {
|
||||
static const char source[] =
|
||||
"Class Sample.Behavior Extends %RegisteredObject\n"
|
||||
"{\n"
|
||||
"Method run(target As Sample.Target, watched As %String) As %Status\n"
|
||||
"{\n"
|
||||
" Do target.accept(watched)\n"
|
||||
" Quit\n"
|
||||
"}\n"
|
||||
"}\n";
|
||||
static const char caller_qn[] = "repro.Behavior.Sample.Behavior.run";
|
||||
static const char callee[] = "Sample.Target.accept";
|
||||
CBMFileResult *r = extract_case("objectscript_typed_receiver", source,
|
||||
CBM_LANG_OBJECTSCRIPT_UDL, "Behavior.cls");
|
||||
ASSERT_NOT_NULL(r);
|
||||
int failures = 0;
|
||||
CHECK_CLEAN("objectscript_typed_receiver", r);
|
||||
CHECK_COUNT("objectscript_typed_receiver", "exact_run_definition",
|
||||
exact_definition_count(r, "run", caller_qn), 1);
|
||||
CHECK_COUNT("objectscript_typed_receiver", "exact_call_owner_and_target",
|
||||
exact_call_count(r, caller_qn, callee), 1);
|
||||
CHECK_COUNT("objectscript_typed_receiver", "argument_usage",
|
||||
exact_usage_count(r, caller_qn, "watched"), 1);
|
||||
CHECK_COUNT("objectscript_typed_receiver", "argument_not_call",
|
||||
exact_call_count(r, caller_qn, "watched"), 0);
|
||||
CHECK_COUNT("objectscript_typed_receiver", "callee_not_usage",
|
||||
exact_usage_count(r, caller_qn, "accept"), 0);
|
||||
CHECK_COUNT("objectscript_typed_receiver", "one_call", r->calls.count, 1);
|
||||
cbm_free_result(r);
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A parameterized ObjectScript routine is represented by a procedure wrapper
|
||||
* around its tag. Calls in the body still belong to the tag's Function node,
|
||||
* not to the module and not to the following tag. */
|
||||
TEST(repro_call_scope_objectscript_routine_tag_exact_owner) {
|
||||
static const char source[] = "run(watched)\n"
|
||||
" Do accept(watched)\n"
|
||||
" Quit\n"
|
||||
"accept(value)\n"
|
||||
" Quit value\n";
|
||||
static const char caller_qn[] = "repro.Behavior.run";
|
||||
CBMFileResult *r = extract_case("objectscript_routine_scope", source,
|
||||
CBM_LANG_OBJECTSCRIPT_ROUTINE, "Behavior.mac");
|
||||
ASSERT_NOT_NULL(r);
|
||||
int failures = 0;
|
||||
CHECK_CLEAN("objectscript_routine_scope", r);
|
||||
CHECK_COUNT("objectscript_routine_scope", "exact_run_definition",
|
||||
exact_definition_count(r, "run", caller_qn), 1);
|
||||
CHECK_COUNT("objectscript_routine_scope", "exact_call_owner_and_target",
|
||||
exact_call_count(r, caller_qn, "accept"), 1);
|
||||
CHECK_COUNT("objectscript_routine_scope", "argument_usage",
|
||||
exact_usage_count(r, caller_qn, "watched"), 1);
|
||||
CHECK_COUNT("objectscript_routine_scope", "argument_not_call",
|
||||
exact_call_count(r, caller_qn, "watched"), 0);
|
||||
CHECK_COUNT("objectscript_routine_scope", "callee_not_usage",
|
||||
exact_usage_count(r, caller_qn, "accept"), 0);
|
||||
CHECK_COUNT("objectscript_routine_scope", "one_call", r->calls.count, 1);
|
||||
cbm_free_result(r);
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(repro_call_scope_usages) {
|
||||
RUN_TEST(repro_call_scope_same_spelling_callee_and_argument);
|
||||
RUN_TEST(repro_call_scope_nested_calls_retain_each_argument);
|
||||
RUN_TEST(repro_call_scope_inline_callback_body_usage);
|
||||
RUN_TEST(repro_call_scope_member_and_computed_components);
|
||||
RUN_TEST(repro_call_scope_constructor_and_generic_arguments);
|
||||
RUN_TEST(repro_call_scope_c_function_pointer_invocation_and_arguments);
|
||||
RUN_TEST(repro_call_scope_cpp_function_pointer_invocation_and_arguments);
|
||||
RUN_TEST(repro_call_scope_python_keyword_argument);
|
||||
RUN_TEST(repro_call_scope_python_with_body_usage);
|
||||
RUN_TEST(repro_call_scope_rust_macro_argument);
|
||||
RUN_TEST(repro_call_scope_elixir_binary_operator_operand);
|
||||
RUN_TEST(repro_call_scope_fortran_keyword_argument);
|
||||
RUN_TEST(repro_fortran_subroutine_call_is_extracted);
|
||||
RUN_TEST(repro_call_scope_kotlin_property_navigation);
|
||||
RUN_TEST(repro_call_scope_php_first_class_callable);
|
||||
RUN_TEST(repro_scope_stack_preserves_deepest_function);
|
||||
RUN_TEST(repro_usage_declarations_are_not_references);
|
||||
RUN_TEST(repro_call_scope_objectscript_typed_receiver_exact_owner);
|
||||
RUN_TEST(repro_call_scope_objectscript_routine_tag_exact_owner);
|
||||
}
|
||||
|
||||
#undef CHECK_CLEAN
|
||||
#undef CHECK_COUNT
|
||||
@@ -59,9 +59,8 @@
|
||||
* languages with non-empty call_types AND a fixture that
|
||||
* produces a resolvable callee_name (TYPST call, QML JS
|
||||
* call_expression, PURESCRIPT exp_apply). BIBTEX/DIFF
|
||||
* have call_types ("command") but the nodes are not
|
||||
* function-application sites with a stable callee_name;
|
||||
* dim 6 is SKIPPED there and noted.
|
||||
* intentionally have no call metadata because their
|
||||
* `command` nodes are document records, not applications.
|
||||
*
|
||||
* FULL-PIPELINE (rh_index_files -> cbm_store_t*, via inv_count_* store helpers):
|
||||
* 7. callable-sourcing : inv_count_calls_by_source(store,project,&mod,&call).
|
||||
@@ -91,15 +90,15 @@
|
||||
* Dims 1-8. Dim 5 asserts "Function" (a let-bound lambda).
|
||||
* Dim 7 may RED if the lambda is anonymous and the enclosing-func
|
||||
* walk attributes the call at Module.
|
||||
* BIBTEX -- DOCS. call_types = {command} only; entries (@article{...}) are
|
||||
* BIBTEX -- DOCS. call_types is empty; entries (@article{...}) are
|
||||
* NOT mapped to any def label, and "command" nodes are LaTeX-style
|
||||
* commands, not callee-named application sites. Dims 1-4 + R
|
||||
* (dim 5 skipped -- no def types; dim 6 skipped -- no stable callee).
|
||||
* MERMAID -- structural-only. module_types only. Dims 1-4 + R.
|
||||
* PO -- DOCS/structural-only. module_types only (gettext msgid/msgstr
|
||||
* entries are not mapped to a def label). Dims 1-4 + R.
|
||||
* DIFF -- structural. call_types = {command} only (a "command" line in a
|
||||
* git-style diff header, not a function call); no def types.
|
||||
* DIFF -- structural. call_types is empty because a "command" line in a
|
||||
* git-style diff header is not a function call; no def types.
|
||||
* Dims 1-4 + R (dims 5-6 skipped).
|
||||
* REGEX -- structural-only. module_types = {pattern}. Dims 1-4 + R.
|
||||
* CAPNP -- SCHEMA. func_types = {method} -> "Function";
|
||||
@@ -518,28 +517,18 @@ TEST(repro_grammar_markup_rst) {
|
||||
* Dims asserted: 1-8 (full battery).
|
||||
* Dim 5 expected GREEN: "Function" def for the let-bound lambda.
|
||||
* Dim 6 expected GREEN: call to "greet" via the call node.
|
||||
* Dim 7 expected RED if the lambda binding name does not flow to the enclosing-
|
||||
* func walk and the call is attributed at Module. RED documents the gap.
|
||||
* Dim 7 expected GREEN: `render` calls `greet` from its function body, so the
|
||||
* call must source to the `render` Function rather than the Module.
|
||||
* Dim 8 expected GREEN: no dangling CALLS endpoints.
|
||||
*/
|
||||
TEST(repro_grammar_markup_typst) {
|
||||
/* DISABLED — RARE LANGUAGE (maintainer-approved, 2026-06-28): Typst (markup).
|
||||
* The `#greet("world")` is a genuinely top-level (module-level) application
|
||||
* that production CORRECTLY sources to the Module, but pipeline_battery counts
|
||||
* any non-Function-sourced edge as drift (the nix-pattern). A simple in-
|
||||
* function wrap conflicts with markup_callable_battery, which needs that very
|
||||
* call. Murky markup/fixture interaction in a niche language; deferred. */
|
||||
printf("%sSKIP%s rare language (Typst top-level-call sourcing)\n", tf_dim(), tf_reset());
|
||||
return -1; /* skip — not counted as pass or fail */
|
||||
static const char src[] =
|
||||
"#let title = \"Codebase Memory\"\n"
|
||||
"#let greet(name) = [Hello, #name!]\n"
|
||||
"\n"
|
||||
"= #title\n"
|
||||
"\n"
|
||||
"#greet(\"world\")\n"
|
||||
"\n"
|
||||
"Some body text with a #strong[bold] run.\n";
|
||||
static const char src[] = "#let title = \"Codebase Memory\"\n"
|
||||
"#let greet(name) = [Hello, #name!]\n"
|
||||
"#let render() = greet(\"world\")\n"
|
||||
"\n"
|
||||
"= #title\n"
|
||||
"\n"
|
||||
"Some body text with a #strong[bold] run.\n";
|
||||
static const char bad[] = "#let greet(name) = [Hello, #name";
|
||||
if (markup_callable_battery("Typst", src, CBM_LANG_TYPST, "doc.typ",
|
||||
"Function", "greet") != 0)
|
||||
@@ -551,14 +540,13 @@ TEST(repro_grammar_markup_typst) {
|
||||
|
||||
/* -- BIBTEX ------------------------------------------------------------------
|
||||
* Idiomatic BibTeX bibliography with an @article and an @book entry. The spec
|
||||
* has bibtex_module_types = {document} and bibtex_call_types = {command}; entry
|
||||
* declarations are NOT mapped to any def label, and "command" nodes are
|
||||
* LaTeX-style commands without a stable function callee_name.
|
||||
* has bibtex_module_types = {document} and an empty bibtex_call_types array;
|
||||
* entry declarations are NOT mapped to any def label, and "command" nodes are
|
||||
* LaTeX-style document commands rather than function applications.
|
||||
*
|
||||
* Dims asserted: 1-4 + R.
|
||||
* Dim 5 SKIPPED: no def-minting types (entries unmapped).
|
||||
* Dim 6 SKIPPED: call_types exists but "command" nodes have no resolvable
|
||||
* callee_name to assert against; asserting would be brittle.
|
||||
* Dim 6 SKIPPED: call_types is empty by design.
|
||||
* Dims 7-8 SKIPPED: no func_types to anchor a call.
|
||||
* Expected GREEN: dims 1-4. extract-clean RED would indicate the BibTeX grammar
|
||||
* misparses standard @entry{...} records.
|
||||
@@ -639,7 +627,7 @@ TEST(repro_grammar_markup_po) {
|
||||
|
||||
/* -- DIFF --------------------------------------------------------------------
|
||||
* Idiomatic unified diff (git-style) with file headers and a hunk. The spec has
|
||||
* diff_module_types = {source} and diff_call_types = {command}; there are no
|
||||
* diff_module_types = {source} and an empty diff_call_types array; there are no
|
||||
* def-minting types and "command" nodes are diff command lines, not function
|
||||
* application sites with a stable callee_name.
|
||||
*
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
* repro_grammar_misc.c -- FINAL per-grammar INVARIANT battery covering the
|
||||
* remaining MISCELLANEOUS language family (hardware-description, CFML dialects,
|
||||
* niche scripting, structural assembly/linker/tablegen/ledger/IaC). This file
|
||||
* completes the all-159-grammar reproduce-first coverage: every CBM_LANG_* now
|
||||
* has a per-language RED/GREEN row on the bug-repro board.
|
||||
* completes the all-161-grammar reproduce-first coverage: every grammar-backed
|
||||
* CBM_LANG_* now has a per-language RED/GREEN row on the bug-repro board.
|
||||
* CBM_LANG_OBJECTSCRIPT_EXPORT is intentionally excluded from the grammar
|
||||
* count because Studio Export XML is transformed to ObjectScript UDL first.
|
||||
*
|
||||
* One TEST() per language so per-language RED/GREEN shows on the board. Each
|
||||
* test runs the battery dimension appropriate to what the language's lang_spec
|
||||
@@ -31,6 +33,12 @@
|
||||
* call: call_expression)
|
||||
* SQL -> CBM_LANG_SQL (func: create_function; call:
|
||||
* function_call/invocation/command)
|
||||
* OBJECTSCRIPT_UDL -> CBM_LANG_OBJECTSCRIPT_UDL
|
||||
* (func: method/classmethod/query;
|
||||
* call: four registered UDL forms)
|
||||
* OBJECTSCRIPT_ROUTINE -> CBM_LANG_OBJECTSCRIPT_ROUTINE
|
||||
* (func: tag; call: extrinsic_function/
|
||||
* routine_tag_call)
|
||||
*
|
||||
* STRUCTURAL family (asm / linker / data / IaC) -> extract-clean +
|
||||
* labels/fqn/ranges valid + defs-present (the entities each should extract) +
|
||||
@@ -91,8 +99,9 @@
|
||||
*
|
||||
* HONEST RED CONTRACT (the point of this file): dimension 7 (callable-sourcing) is
|
||||
* expected RED for the non-LSP callable languages here. None of VERILOG /
|
||||
* SYSTEMVERILOG / VHDL / CFML / CFSCRIPT / RESCRIPT / SQUIRREL / PINE / TEMPL / SQL
|
||||
* has a dedicated cross-LSP rescue, so attribution depends solely on the
|
||||
* SYSTEMVERILOG / VHDL / CFML / CFSCRIPT / RESCRIPT / SQUIRREL / PINE / TEMPL / SQL /
|
||||
* OBJECTSCRIPT_UDL / OBJECTSCRIPT_ROUTINE has a dedicated cross-LSP rescue, so
|
||||
* attribution depends solely on the
|
||||
* tree-sitter enclosing-func walk (cbm_find_enclosing_func + func_kinds_for_lang in
|
||||
* helpers.c). When that mapping does not match the grammar's emitted func node
|
||||
* types, the in-body call falls back to the Module QN -- exactly the enclosing-func
|
||||
@@ -468,6 +477,67 @@ TEST(repro_grammar_misc_linkerscript) {
|
||||
return misc_robustness("LINKERSCRIPT", bad, CBM_LANG_LINKERSCRIPT, "link.ld");
|
||||
}
|
||||
|
||||
/* ── OBJECTSCRIPT UDL (callable) ─────────────────────────────────────────────
|
||||
* A class method calls another class method with a local value argument. The
|
||||
* call is the registered class_method_call form; the other three UDL call forms
|
||||
* remain enumerated by repro_call_node_manifest.
|
||||
*
|
||||
* Dims asserted: 1-8 + R. Studio Export XML is not a grammar row because it is
|
||||
* transformed to this UDL language before extraction.
|
||||
*/
|
||||
TEST(repro_grammar_misc_objectscript_udl) {
|
||||
static const char src[] = "Class Sample.Callbacks Extends %RegisteredObject\n"
|
||||
"{\n"
|
||||
"ClassMethod Accept(value As %String) As %String\n"
|
||||
"{\n"
|
||||
" Quit value\n"
|
||||
"}\n"
|
||||
"ClassMethod Run(watched As %String) As %String\n"
|
||||
"{\n"
|
||||
" Quit ##class(Sample.Callbacks).Accept(watched)\n"
|
||||
"}\n"
|
||||
"}\n";
|
||||
static const char bad[] = "Class Sample.Callbacks Extends %RegisteredObject\n"
|
||||
"{\n"
|
||||
"ClassMethod Run(watched As %String) As %String\n"
|
||||
"{\n"
|
||||
" Quit ##class(Sample.Callbacks).Accept(\n";
|
||||
if (misc_single_file_battery("OBJECTSCRIPT_UDL", src, CBM_LANG_OBJECTSCRIPT_UDL,
|
||||
"Callbacks.cls", "Method", NULL, "Accept") != 0)
|
||||
return 1;
|
||||
if (misc_robustness("OBJECTSCRIPT_UDL", bad, CBM_LANG_OBJECTSCRIPT_UDL, "Callbacks.cls") != 0)
|
||||
return 1;
|
||||
return misc_pipeline_battery("OBJECTSCRIPT_UDL", "Callbacks.cls", src);
|
||||
}
|
||||
|
||||
/* ── OBJECTSCRIPT ROUTINE (callable) ─────────────────────────────────────────
|
||||
* A parameterized routine tag invokes a sibling through the registered
|
||||
* extrinsic_function form. routine_tag_call membership is covered separately by
|
||||
* the literal call-node manifest.
|
||||
*
|
||||
* Dims asserted: 1-8 + R.
|
||||
*/
|
||||
TEST(repro_grammar_misc_objectscript_routine) {
|
||||
static const char src[] = "SAMPLE\n"
|
||||
" Quit\n"
|
||||
"Accept(value)\n"
|
||||
" Quit value\n"
|
||||
"Run(watched)\n"
|
||||
" Set result = $$Accept(watched)\n"
|
||||
" Quit result\n";
|
||||
static const char bad[] = "SAMPLE\n"
|
||||
" Quit\n"
|
||||
"Run(watched)\n"
|
||||
" Set result = $$Accept(\n";
|
||||
if (misc_single_file_battery("OBJECTSCRIPT_ROUTINE", src, CBM_LANG_OBJECTSCRIPT_ROUTINE,
|
||||
"Sample.mac", "Function", NULL, "Accept") != 0)
|
||||
return 1;
|
||||
if (misc_robustness("OBJECTSCRIPT_ROUTINE", bad, CBM_LANG_OBJECTSCRIPT_ROUTINE, "Sample.mac") !=
|
||||
0)
|
||||
return 1;
|
||||
return misc_pipeline_battery("OBJECTSCRIPT_ROUTINE", "Sample.mac", src);
|
||||
}
|
||||
|
||||
/* ── PINE (callable) ─────────────────────────────────────────────────────────
|
||||
* Idiomatic Pine Script v5 indicator: a user function `ema2` defined with
|
||||
* function_declaration_statement, and a call to the built-in `plot()` plus an
|
||||
@@ -790,6 +860,8 @@ SUITE(repro_grammar_misc) {
|
||||
RUN_TEST(repro_grammar_misc_cfml);
|
||||
RUN_TEST(repro_grammar_misc_cfscript);
|
||||
RUN_TEST(repro_grammar_misc_linkerscript);
|
||||
RUN_TEST(repro_grammar_misc_objectscript_udl);
|
||||
RUN_TEST(repro_grammar_misc_objectscript_routine);
|
||||
RUN_TEST(repro_grammar_misc_pine);
|
||||
RUN_TEST(repro_grammar_misc_rescript);
|
||||
RUN_TEST(repro_grammar_misc_sql);
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* Languages covered (12) and the CBM_LANG_* enum each uses (every enum verified
|
||||
* present in internal/cbm/cbm.h; none missing, none skipped):
|
||||
* Zig -> CBM_LANG_ZIG
|
||||
* Nim -> CBM_LANG_NIM
|
||||
* Nim -> CBM_LANG_NIM (explicitly unsupported; no grammar is vendored)
|
||||
* Crystal -> CBM_LANG_CRYSTAL
|
||||
* Hare -> CBM_LANG_HARE
|
||||
* Odin -> CBM_LANG_ODIN
|
||||
@@ -65,8 +65,8 @@
|
||||
* - Crystal (method_def), Odin (procedure_declaration), Pony (method),
|
||||
* Ada (subprogram_body), Fortran (function/subroutine),
|
||||
* COBOL (program_definition), Pascal (defProc), Move (function_item).
|
||||
* Nim has NO lang_spec / grammar entry at all, so it extracts zero defs and zero
|
||||
* calls today: dims 5/6/7 are RED for Nim and the fixture documents that gap.
|
||||
* Nim has no lang_spec / grammar entry. Its row is an explicit registry contract,
|
||||
* not a skipped extraction fixture.
|
||||
*
|
||||
* When a language extracts NO in-body call today, dimension 6 (calls-extracted)
|
||||
* is asserted anyway -- the language SHOULD capture the call -- so the RED row
|
||||
@@ -78,6 +78,7 @@
|
||||
*/
|
||||
|
||||
#include "test_framework.h"
|
||||
#include "lang_specs.h"
|
||||
#include "repro_invariant_lib.h"
|
||||
#include <store/store.h>
|
||||
|
||||
@@ -248,39 +249,13 @@ TEST(repro_grammar_systems_zig) {
|
||||
}
|
||||
|
||||
/* -- Nim --------------------------------------------------------------------
|
||||
* Idiomatic: import, an object type, two `proc`s with the callee called inside
|
||||
* the caller body. Nim has NO lang_spec row and NO grammar_nim.c -- there is no
|
||||
* func/class/call node-type table for it. Expected: dim 1 (extract-clean) GREEN
|
||||
* (cbm_extract_file returns a result), but dims 5/6 RED (zero defs, zero calls)
|
||||
* and dim 7 RED (zero CALLS edges to attribute). These RED rows document the
|
||||
* missing Nim support; the fixture asserts it SHOULD extract a "Function" and a
|
||||
* call to "add".
|
||||
* The enum is retained for API compatibility, but no Nim grammar is linked.
|
||||
* Unsupported must mean NULL; it must never alias the zero-initialized Go row.
|
||||
*/
|
||||
TEST(repro_grammar_systems_nim) {
|
||||
/* DISABLED — GRAMMAR ISSUE (maintainer-approved, 2026-06-28): extraction of
|
||||
* standard Nim (`proc add(a, b: int): int = ...`) fails extract-clean (NULL
|
||||
* result or has_error set) — tree-sitter-nim mis-parses the indentation-
|
||||
* sensitive layout (Nim was a deferred/problematic grammar in the sweep). A
|
||||
* grammar/parser defect, not a cbm extraction bug. Original assertions below
|
||||
* are preserved (unreachable) for re-enable when the grammar is fixed. */
|
||||
printf("%sSKIP%s grammar issue (tree-sitter-nim parse failure)\n", tf_dim(), tf_reset());
|
||||
return -1; /* skip — not counted as pass or fail */
|
||||
static const char src[] =
|
||||
"import std/strutils\n"
|
||||
"\n"
|
||||
"type\n"
|
||||
" Calc = object\n"
|
||||
" base: int\n"
|
||||
"\n"
|
||||
"proc add(a, b: int): int =\n"
|
||||
" return a + b\n"
|
||||
"\n"
|
||||
"proc compute(x: int): int =\n"
|
||||
" return add(x, 1)\n";
|
||||
if (single_file_battery("Nim", src, CBM_LANG_NIM, "calc.nim",
|
||||
"Function", NULL, "add") != 0)
|
||||
return 1;
|
||||
return pipeline_battery("Nim", "calc.nim", src);
|
||||
ASSERT_NULL(cbm_lang_spec(CBM_LANG_NIM));
|
||||
ASSERT_NULL(cbm_ts_language(CBM_LANG_NIM));
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* -- Crystal ----------------------------------------------------------------
|
||||
|
||||
+51
-23
@@ -31,6 +31,7 @@
|
||||
|
||||
typedef struct {
|
||||
char tmpdir[256];
|
||||
char cachedir[256];
|
||||
char dbpath[512];
|
||||
char *project;
|
||||
cbm_mcp_server_t *srv;
|
||||
@@ -54,25 +55,43 @@ static inline cbm_store_t *rh_open_indexed(RProj *lp) {
|
||||
lp->project = cbm_project_name_from_path(lp->tmpdir);
|
||||
if (!lp->project)
|
||||
return NULL;
|
||||
const char *home = getenv("HOME");
|
||||
if (!home)
|
||||
home = "/tmp";
|
||||
char cache_dir[512];
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/codebase-memory-mcp", home);
|
||||
cbm_mkdir(cache_dir);
|
||||
snprintf(lp->dbpath, sizeof(lp->dbpath), "%s/%s.db", cache_dir, lp->project);
|
||||
if (!lp->cachedir[0]) {
|
||||
snprintf(lp->cachedir, sizeof(lp->cachedir), "/tmp/cbm_repro_cache_XXXXXX");
|
||||
if (!cbm_mkdtemp(lp->cachedir))
|
||||
return NULL;
|
||||
rh_to_fwd_slashes(lp->cachedir);
|
||||
}
|
||||
snprintf(lp->dbpath, sizeof(lp->dbpath), "%s/%s.db", lp->cachedir, lp->project);
|
||||
unlink(lp->dbpath);
|
||||
|
||||
const char *prior_cache_dir = getenv("CBM_CACHE_DIR");
|
||||
char *saved_cache_dir = prior_cache_dir ? cbm_strdup(prior_cache_dir) : NULL;
|
||||
cbm_setenv("CBM_CACHE_DIR", lp->cachedir, 1);
|
||||
|
||||
cbm_store_t *store = NULL;
|
||||
lp->srv = cbm_mcp_server_new(NULL);
|
||||
if (!lp->srv)
|
||||
return NULL;
|
||||
char args[700];
|
||||
snprintf(args, sizeof(args), "{\"repo_path\":\"%s\"}", lp->tmpdir);
|
||||
char *resp = cbm_mcp_handle_tool(lp->srv, "index_repository", args);
|
||||
if (resp)
|
||||
free(resp);
|
||||
return cbm_store_open_path(lp->dbpath);
|
||||
if (lp->srv) {
|
||||
char args[700];
|
||||
snprintf(args, sizeof(args), "{\"repo_path\":\"%s\"}", lp->tmpdir);
|
||||
char *resp = cbm_mcp_handle_tool(lp->srv, "index_repository", args);
|
||||
if (resp)
|
||||
free(resp);
|
||||
/* Repro consumers only inspect the graph. Keep this connection query-only
|
||||
* so the harness does not mutate the publisher's sealed journal mode. */
|
||||
store = cbm_store_open_path_query(lp->dbpath);
|
||||
}
|
||||
|
||||
if (saved_cache_dir) {
|
||||
cbm_setenv("CBM_CACHE_DIR", saved_cache_dir, 1);
|
||||
free(saved_cache_dir);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_CACHE_DIR");
|
||||
}
|
||||
return store;
|
||||
}
|
||||
|
||||
static inline void rh_cleanup(RProj *lp, cbm_store_t *store);
|
||||
|
||||
/* Write each fixture file into a fresh temp project, index it via the MCP
|
||||
* production flow, and open the resulting graph DB. Returns store (NULL on fail). */
|
||||
static inline cbm_store_t *rh_index_files(RProj *lp, const RFile *files, int nfiles) {
|
||||
@@ -91,12 +110,18 @@ static inline cbm_store_t *rh_index_files(RProj *lp, const RFile *files, int nfi
|
||||
*slash = '/';
|
||||
}
|
||||
FILE *f = fopen(path, "wb"); /* binary: keep "\n" exact */
|
||||
if (!f)
|
||||
if (!f) {
|
||||
rh_cleanup(lp, NULL);
|
||||
return NULL;
|
||||
}
|
||||
fputs(files[i].content, f);
|
||||
fclose(f);
|
||||
}
|
||||
return rh_open_indexed(lp);
|
||||
cbm_store_t *store = rh_open_indexed(lp);
|
||||
if (!store) {
|
||||
rh_cleanup(lp, NULL);
|
||||
}
|
||||
return store;
|
||||
}
|
||||
|
||||
static inline cbm_store_t *rh_index(RProj *lp, const char *filename, const char *content) {
|
||||
@@ -114,12 +139,15 @@ static inline void rh_cleanup(RProj *lp, cbm_store_t *store) {
|
||||
free(lp->project);
|
||||
lp->project = NULL;
|
||||
th_rmtree(lp->tmpdir);
|
||||
unlink(lp->dbpath);
|
||||
char wal[600], shm[600];
|
||||
snprintf(wal, sizeof(wal), "%s-wal", lp->dbpath);
|
||||
unlink(wal);
|
||||
snprintf(shm, sizeof(shm), "%s-shm", lp->dbpath);
|
||||
unlink(shm);
|
||||
th_rmtree(lp->cachedir);
|
||||
if (lp->dbpath[0]) {
|
||||
unlink(lp->dbpath);
|
||||
char wal[600], shm[600];
|
||||
snprintf(wal, sizeof(wal), "%s-wal", lp->dbpath);
|
||||
unlink(wal);
|
||||
snprintf(shm, sizeof(shm), "%s-shm", lp->dbpath);
|
||||
unlink(shm);
|
||||
}
|
||||
}
|
||||
|
||||
/* Count edges of a given type in the project graph. Returns -1 on query error. */
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/* Reproduction for temporary directories leaked by repro_harness failures. */
|
||||
#include "test_framework.h"
|
||||
#include "repro_harness.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
static int cleanup_path_exists(const char *path) {
|
||||
struct stat st;
|
||||
return path && path[0] && stat(path, &st) == 0;
|
||||
}
|
||||
|
||||
static int cleanup_write_sentinel(const char *path) {
|
||||
FILE *file = cbm_fopen(path, "wb");
|
||||
if (!file) {
|
||||
return 0;
|
||||
}
|
||||
int wrote = fputs("sentinel\n", file) >= 0;
|
||||
int closed = fclose(file) == 0;
|
||||
return wrote && closed;
|
||||
}
|
||||
|
||||
TEST(repro_harness_cleans_temp_project_after_fixture_write_failure) {
|
||||
RProj project;
|
||||
const RFile invalid_file = {
|
||||
"", /* Resolves to the fresh project directory, so fopen must fail. */
|
||||
"fixture content",
|
||||
};
|
||||
|
||||
cbm_store_t *store = rh_index_files(&project, &invalid_file, 1);
|
||||
|
||||
char tmpdir[sizeof(project.tmpdir)];
|
||||
char cachedir[sizeof(project.cachedir)];
|
||||
snprintf(tmpdir, sizeof(tmpdir), "%s", project.tmpdir);
|
||||
snprintf(cachedir, sizeof(cachedir), "%s", project.cachedir);
|
||||
|
||||
int temp_project_was_created = tmpdir[0] != '\0';
|
||||
int tmpdir_remained = cleanup_path_exists(tmpdir);
|
||||
int cachedir_remained = cleanup_path_exists(cachedir);
|
||||
|
||||
/* This failure happens before dbpath is initialized. Keep rh_cleanup's WAL
|
||||
* cleanup absolute so the RED guard cannot unlink a relative workspace file. */
|
||||
if (!project.dbpath[0]) {
|
||||
snprintf(project.dbpath, sizeof(project.dbpath), "/tmp/cbm_repro_cleanup_guard_%ld.db",
|
||||
(long)getpid());
|
||||
}
|
||||
rh_cleanup(&project, store);
|
||||
|
||||
int explicit_cleanup_left_tmpdir = cleanup_path_exists(tmpdir);
|
||||
int explicit_cleanup_left_cachedir = cleanup_path_exists(cachedir);
|
||||
|
||||
if (store != NULL) {
|
||||
fprintf(stderr, " [repro-harness-cleanup] invariant=invalid_fixture_was_indexed\n");
|
||||
FAIL("invalid fixture path must make rh_index_files return NULL");
|
||||
}
|
||||
if (!temp_project_was_created) {
|
||||
fprintf(stderr,
|
||||
" [repro-harness-cleanup] invariant=temp_project_not_created_before_failure\n");
|
||||
FAIL("test did not reach the post-mkdtemp fixture-write failure path");
|
||||
}
|
||||
if (explicit_cleanup_left_tmpdir || explicit_cleanup_left_cachedir) {
|
||||
fprintf(stderr,
|
||||
" [repro-harness-cleanup] invariant=test_cleanup_failed tmpdir=%d cachedir=%d\n",
|
||||
explicit_cleanup_left_tmpdir, explicit_cleanup_left_cachedir);
|
||||
FAIL("rh_cleanup did not remove the test's temporary directories");
|
||||
}
|
||||
if (tmpdir_remained || cachedir_remained) {
|
||||
fprintf(stderr,
|
||||
" [repro-harness-cleanup] invariant=helper_returned_with_temp_state "
|
||||
"tmpdir=%d cachedir=%d\n",
|
||||
tmpdir_remained, cachedir_remained);
|
||||
FAIL("rh_index_files must clean temporary directories before returning NULL");
|
||||
}
|
||||
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_harness_cleanup_empty_dbpath_preserves_relative_sidecars) {
|
||||
/* Empty dbpath must be a no-op for sidecars, never a cwd-relative unlink. */
|
||||
char original_cwd[1024];
|
||||
char private_dir[256] = "/tmp/cbm_repro_empty_dbpath_XXXXXX";
|
||||
_Static_assert(sizeof(private_dir) >= 256,
|
||||
"cbm_mkdtemp requires a 256-byte caller buffer on Windows");
|
||||
int entered_private_dir = 0;
|
||||
int wal_created = 0;
|
||||
int shm_created = 0;
|
||||
int wal_survived = 0;
|
||||
int shm_survived = 0;
|
||||
|
||||
if (!getcwd(original_cwd, sizeof(original_cwd))) {
|
||||
FAIL("test could not record the original working directory");
|
||||
}
|
||||
if (!cbm_mkdtemp(private_dir)) {
|
||||
FAIL("test could not create a private temporary working directory");
|
||||
}
|
||||
|
||||
entered_private_dir = chdir(private_dir) == 0;
|
||||
if (entered_private_dir) {
|
||||
wal_created = cleanup_write_sentinel("-wal");
|
||||
shm_created = cleanup_write_sentinel("-shm");
|
||||
if (wal_created && shm_created) {
|
||||
RProj project = {0};
|
||||
rh_cleanup(&project, NULL);
|
||||
wal_survived = cleanup_path_exists("-wal");
|
||||
shm_survived = cleanup_path_exists("-shm");
|
||||
}
|
||||
}
|
||||
|
||||
int restored_cwd = chdir(original_cwd) == 0;
|
||||
int removed_private_dir = th_rmtree(private_dir) == 0;
|
||||
|
||||
if (!entered_private_dir || !wal_created || !shm_created) {
|
||||
fprintf(stderr,
|
||||
" [repro-harness-cleanup] invariant=sentinel_setup_failed entered=%d "
|
||||
"wal_created=%d shm_created=%d\n",
|
||||
entered_private_dir, wal_created, shm_created);
|
||||
FAIL("test could not establish the empty-dbpath sidecar preconditions");
|
||||
}
|
||||
if (!restored_cwd || !removed_private_dir) {
|
||||
fprintf(stderr,
|
||||
" [repro-harness-cleanup] invariant=test_cleanup_failed restored_cwd=%d "
|
||||
"removed_private_dir=%d\n",
|
||||
restored_cwd, removed_private_dir);
|
||||
FAIL("test did not restore and remove its private working directory");
|
||||
}
|
||||
if (!wal_survived || !shm_survived) {
|
||||
fprintf(stderr,
|
||||
" [repro-harness-cleanup] invariant=empty_dbpath_unlinked_relative_sidecar "
|
||||
"wal_survived=%d shm_survived=%d\n",
|
||||
wal_survived, shm_survived);
|
||||
FAIL("rh_cleanup with an empty dbpath must not unlink relative -wal/-shm files");
|
||||
}
|
||||
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(repro_harness_cleanup) {
|
||||
RUN_TEST(repro_harness_cleans_temp_project_after_fixture_write_failure);
|
||||
RUN_TEST(repro_harness_cleanup_empty_dbpath_preserves_relative_sidecars);
|
||||
}
|
||||
@@ -0,0 +1,574 @@
|
||||
/* Reproductions for CBMLanguage-to-spec registry invariants.
|
||||
*
|
||||
* This ledger classifies raw call-node metadata and generic identifier/reference
|
||||
* vocabulary. It does not claim that every call-capable language can produce an
|
||||
* exact graph CALL_REFERENCE edge; that narrower semantic contract is covered
|
||||
* by the reference-precision suites. */
|
||||
#include "test_framework.h"
|
||||
#include "lang_specs.h"
|
||||
#include "tree_sitter/api.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef enum {
|
||||
CAP_UNSET = 0,
|
||||
CAP_CALL_WITH_REFERENCE_VOCAB,
|
||||
CAP_CALL_WITHOUT_REFERENCE_VOCAB,
|
||||
CAP_NO_CALL,
|
||||
CAP_TRANSFORM_ONLY,
|
||||
CAP_UNSUPPORTED,
|
||||
CAP_COUNT
|
||||
} LanguageCapability;
|
||||
|
||||
typedef struct {
|
||||
LanguageCapability capability;
|
||||
const char *name;
|
||||
const char *reason;
|
||||
} LanguageCapabilityEntry;
|
||||
|
||||
size_t repro_call_argument_matrix_a_copy_language_ids(CBMLanguage *language_ids, size_t capacity);
|
||||
size_t repro_call_argument_matrix_b_copy_language_ids(CBMLanguage *language_ids, size_t capacity);
|
||||
|
||||
#define CALL_WITH_REFERENCE_VOCAB(lang) \
|
||||
[CBM_LANG_##lang] = {CAP_CALL_WITH_REFERENCE_VOCAB, #lang, \
|
||||
"call metadata and a currently recognized reference node"}
|
||||
#define CALL_WITHOUT_REFERENCE_VOCAB(lang) \
|
||||
[CBM_LANG_##lang] = {CAP_CALL_WITHOUT_REFERENCE_VOCAB, #lang, \
|
||||
"call metadata but no reference node recognized by extract_usages"}
|
||||
#define NO_CALL(lang) \
|
||||
[CBM_LANG_##lang] = {CAP_NO_CALL, #lang, "no call-node metadata; callback case inapplicable"}
|
||||
#define TRANSFORM_ONLY(lang) \
|
||||
[CBM_LANG_##lang] = {CAP_TRANSFORM_ONLY, #lang, \
|
||||
"source is transformed into another registered language first"}
|
||||
#define UNSUPPORTED(lang) \
|
||||
[CBM_LANG_##lang] = {CAP_UNSUPPORTED, #lang, "enum retained without a registered grammar"}
|
||||
|
||||
/* Explicit ledger: no default initializer is intentional. A new enum remains
|
||||
* CAP_UNSET until its call-metadata/reference-vocabulary behavior is audited. */
|
||||
static const LanguageCapabilityEntry LANGUAGE_CAPABILITIES[CBM_LANG_COUNT] = {
|
||||
CALL_WITH_REFERENCE_VOCAB(GO),
|
||||
CALL_WITH_REFERENCE_VOCAB(PYTHON),
|
||||
CALL_WITH_REFERENCE_VOCAB(JAVASCRIPT),
|
||||
CALL_WITH_REFERENCE_VOCAB(TYPESCRIPT),
|
||||
CALL_WITH_REFERENCE_VOCAB(TSX),
|
||||
CALL_WITH_REFERENCE_VOCAB(RUST),
|
||||
CALL_WITH_REFERENCE_VOCAB(JAVA),
|
||||
CALL_WITH_REFERENCE_VOCAB(CPP),
|
||||
CALL_WITH_REFERENCE_VOCAB(CSHARP),
|
||||
CALL_WITHOUT_REFERENCE_VOCAB(PHP),
|
||||
CALL_WITH_REFERENCE_VOCAB(LUA),
|
||||
CALL_WITH_REFERENCE_VOCAB(SCALA),
|
||||
CALL_WITH_REFERENCE_VOCAB(KOTLIN),
|
||||
CALL_WITH_REFERENCE_VOCAB(RUBY),
|
||||
CALL_WITH_REFERENCE_VOCAB(C),
|
||||
CALL_WITHOUT_REFERENCE_VOCAB(BASH),
|
||||
CALL_WITH_REFERENCE_VOCAB(ZIG),
|
||||
CALL_WITH_REFERENCE_VOCAB(ELIXIR),
|
||||
CALL_WITH_REFERENCE_VOCAB(HASKELL),
|
||||
CALL_WITH_REFERENCE_VOCAB(OCAML),
|
||||
CALL_WITH_REFERENCE_VOCAB(OBJC),
|
||||
CALL_WITH_REFERENCE_VOCAB(SWIFT),
|
||||
CALL_WITH_REFERENCE_VOCAB(DART),
|
||||
CALL_WITHOUT_REFERENCE_VOCAB(PERL),
|
||||
CALL_WITH_REFERENCE_VOCAB(GROOVY),
|
||||
CALL_WITH_REFERENCE_VOCAB(ERLANG),
|
||||
CALL_WITH_REFERENCE_VOCAB(R),
|
||||
NO_CALL(HTML),
|
||||
CALL_WITH_REFERENCE_VOCAB(CSS),
|
||||
CALL_WITH_REFERENCE_VOCAB(SCSS),
|
||||
NO_CALL(YAML),
|
||||
NO_CALL(TOML),
|
||||
CALL_WITH_REFERENCE_VOCAB(HCL),
|
||||
CALL_WITH_REFERENCE_VOCAB(SQL),
|
||||
NO_CALL(DOCKERFILE),
|
||||
CALL_WITHOUT_REFERENCE_VOCAB(CLOJURE),
|
||||
CALL_WITH_REFERENCE_VOCAB(FSHARP),
|
||||
CALL_WITH_REFERENCE_VOCAB(JULIA),
|
||||
CALL_WITH_REFERENCE_VOCAB(VIMSCRIPT),
|
||||
CALL_WITH_REFERENCE_VOCAB(NIX),
|
||||
CALL_WITHOUT_REFERENCE_VOCAB(COMMONLISP),
|
||||
CALL_WITHOUT_REFERENCE_VOCAB(ELM),
|
||||
CALL_WITH_REFERENCE_VOCAB(FORTRAN),
|
||||
CALL_WITH_REFERENCE_VOCAB(CUDA),
|
||||
CALL_WITHOUT_REFERENCE_VOCAB(COBOL),
|
||||
CALL_WITH_REFERENCE_VOCAB(VERILOG),
|
||||
CALL_WITHOUT_REFERENCE_VOCAB(EMACSLISP),
|
||||
NO_CALL(JSON),
|
||||
NO_CALL(XML),
|
||||
NO_CALL(MARKDOWN),
|
||||
CALL_WITHOUT_REFERENCE_VOCAB(MAKEFILE),
|
||||
CALL_WITH_REFERENCE_VOCAB(CMAKE),
|
||||
NO_CALL(PROTOBUF),
|
||||
NO_CALL(GRAPHQL),
|
||||
NO_CALL(VUE),
|
||||
NO_CALL(SVELTE),
|
||||
CALL_WITH_REFERENCE_VOCAB(MESON),
|
||||
CALL_WITH_REFERENCE_VOCAB(GLSL),
|
||||
NO_CALL(INI),
|
||||
CALL_WITH_REFERENCE_VOCAB(MATLAB),
|
||||
CALL_WITH_REFERENCE_VOCAB(LEAN),
|
||||
CALL_WITH_REFERENCE_VOCAB(FORM),
|
||||
CALL_WITH_REFERENCE_VOCAB(MAGMA),
|
||||
CALL_WITHOUT_REFERENCE_VOCAB(WOLFRAM),
|
||||
CALL_WITH_REFERENCE_VOCAB(SOLIDITY),
|
||||
CALL_WITHOUT_REFERENCE_VOCAB(TYPST),
|
||||
CALL_WITH_REFERENCE_VOCAB(GDSCRIPT),
|
||||
CALL_WITH_REFERENCE_VOCAB(GLEAM),
|
||||
CALL_WITHOUT_REFERENCE_VOCAB(POWERSHELL),
|
||||
CALL_WITH_REFERENCE_VOCAB(PASCAL),
|
||||
CALL_WITH_REFERENCE_VOCAB(DLANG),
|
||||
UNSUPPORTED(NIM),
|
||||
CALL_WITHOUT_REFERENCE_VOCAB(SCHEME),
|
||||
CALL_WITHOUT_REFERENCE_VOCAB(FENNEL),
|
||||
CALL_WITHOUT_REFERENCE_VOCAB(FISH),
|
||||
CALL_WITH_REFERENCE_VOCAB(AWK),
|
||||
CALL_WITHOUT_REFERENCE_VOCAB(ZSH),
|
||||
CALL_WITHOUT_REFERENCE_VOCAB(TCL),
|
||||
CALL_WITH_REFERENCE_VOCAB(ADA),
|
||||
CALL_WITHOUT_REFERENCE_VOCAB(AGDA),
|
||||
CALL_WITHOUT_REFERENCE_VOCAB(RACKET),
|
||||
CALL_WITH_REFERENCE_VOCAB(ODIN),
|
||||
CALL_WITH_REFERENCE_VOCAB(RESCRIPT),
|
||||
CALL_WITHOUT_REFERENCE_VOCAB(PURESCRIPT),
|
||||
CALL_WITHOUT_REFERENCE_VOCAB(NICKEL),
|
||||
CALL_WITH_REFERENCE_VOCAB(CRYSTAL),
|
||||
CALL_WITH_REFERENCE_VOCAB(TEAL),
|
||||
CALL_WITH_REFERENCE_VOCAB(HARE),
|
||||
CALL_WITH_REFERENCE_VOCAB(PONY),
|
||||
CALL_WITH_REFERENCE_VOCAB(LUAU),
|
||||
NO_CALL(JANET),
|
||||
CALL_WITH_REFERENCE_VOCAB(SWAY),
|
||||
CALL_WITHOUT_REFERENCE_VOCAB(NASM),
|
||||
NO_CALL(ASSEMBLY),
|
||||
NO_CALL(ASTRO),
|
||||
NO_CALL(BLADE),
|
||||
CALL_WITH_REFERENCE_VOCAB(JUST),
|
||||
CALL_WITH_REFERENCE_VOCAB(GOTEMPLATE),
|
||||
CALL_WITH_REFERENCE_VOCAB(TEMPL),
|
||||
NO_CALL(LIQUID),
|
||||
NO_CALL(JINJA2),
|
||||
CALL_WITH_REFERENCE_VOCAB(PRISMA),
|
||||
NO_CALL(HYPRLANG),
|
||||
NO_CALL(DOTENV),
|
||||
NO_CALL(DIFF),
|
||||
CALL_WITH_REFERENCE_VOCAB(WGSL),
|
||||
NO_CALL(KDL),
|
||||
NO_CALL(JSON5),
|
||||
CALL_WITH_REFERENCE_VOCAB(JSONNET),
|
||||
NO_CALL(RON),
|
||||
NO_CALL(THRIFT),
|
||||
NO_CALL(CAPNP),
|
||||
NO_CALL(PROPERTIES),
|
||||
NO_CALL(SSHCONFIG),
|
||||
NO_CALL(BIBTEX),
|
||||
CALL_WITH_REFERENCE_VOCAB(STARLARK),
|
||||
CALL_WITH_REFERENCE_VOCAB(BICEP),
|
||||
NO_CALL(CSV),
|
||||
NO_CALL(REQUIREMENTS),
|
||||
CALL_WITH_REFERENCE_VOCAB(HLSL),
|
||||
CALL_WITH_REFERENCE_VOCAB(VHDL),
|
||||
CALL_WITH_REFERENCE_VOCAB(SYSTEMVERILOG),
|
||||
CALL_WITH_REFERENCE_VOCAB(DEVICETREE),
|
||||
CALL_WITHOUT_REFERENCE_VOCAB(LINKERSCRIPT),
|
||||
CALL_WITH_REFERENCE_VOCAB(GN),
|
||||
NO_CALL(KCONFIG),
|
||||
CALL_WITH_REFERENCE_VOCAB(BITBAKE),
|
||||
NO_CALL(SMALI),
|
||||
NO_CALL(TABLEGEN),
|
||||
CALL_WITH_REFERENCE_VOCAB(ISPC),
|
||||
CALL_WITH_REFERENCE_VOCAB(CAIRO),
|
||||
CALL_WITH_REFERENCE_VOCAB(MOVE),
|
||||
CALL_WITH_REFERENCE_VOCAB(SQUIRREL),
|
||||
CALL_WITH_REFERENCE_VOCAB(FUNC),
|
||||
NO_CALL(REGEX),
|
||||
NO_CALL(JSDOC),
|
||||
NO_CALL(RST),
|
||||
NO_CALL(BEANCOUNT),
|
||||
NO_CALL(MERMAID),
|
||||
CALL_WITHOUT_REFERENCE_VOCAB(PUPPET),
|
||||
NO_CALL(PO),
|
||||
NO_CALL(GITATTRIBUTES),
|
||||
NO_CALL(GITIGNORE),
|
||||
CALL_WITH_REFERENCE_VOCAB(SLANG),
|
||||
CALL_WITHOUT_REFERENCE_VOCAB(LLVM_IR),
|
||||
NO_CALL(SMITHY),
|
||||
NO_CALL(WIT),
|
||||
CALL_WITH_REFERENCE_VOCAB(TLAPLUS),
|
||||
NO_CALL(PKL),
|
||||
NO_CALL(GOMOD),
|
||||
CALL_WITH_REFERENCE_VOCAB(APEX),
|
||||
NO_CALL(SOQL),
|
||||
NO_CALL(SOSL),
|
||||
NO_CALL(KUSTOMIZE),
|
||||
NO_CALL(K8S),
|
||||
CALL_WITH_REFERENCE_VOCAB(PINE),
|
||||
CALL_WITH_REFERENCE_VOCAB(QML),
|
||||
CALL_WITH_REFERENCE_VOCAB(CFSCRIPT),
|
||||
CALL_WITH_REFERENCE_VOCAB(CFML),
|
||||
CALL_WITH_REFERENCE_VOCAB(MOJO),
|
||||
CALL_WITH_REFERENCE_VOCAB(OBJECTSCRIPT_UDL),
|
||||
CALL_WITH_REFERENCE_VOCAB(OBJECTSCRIPT_ROUTINE),
|
||||
TRANSFORM_ONLY(OBJECTSCRIPT_EXPORT),
|
||||
};
|
||||
|
||||
#undef CALL_WITH_REFERENCE_VOCAB
|
||||
#undef CALL_WITHOUT_REFERENCE_VOCAB
|
||||
#undef NO_CALL
|
||||
#undef TRANSFORM_ONLY
|
||||
#undef UNSUPPORTED
|
||||
|
||||
_Static_assert(sizeof(LANGUAGE_CAPABILITIES) / sizeof(LANGUAGE_CAPABILITIES[0]) == CBM_LANG_COUNT,
|
||||
"language capability ledger must track CBM_LANG_COUNT");
|
||||
|
||||
static int grammar_has_symbol(const TSLanguage *language, const char *expected_name) {
|
||||
uint32_t symbol_count = ts_language_symbol_count(language);
|
||||
for (TSSymbol symbol = 0; symbol < symbol_count; symbol++) {
|
||||
const char *actual_name = ts_language_symbol_name(language, symbol);
|
||||
if (actual_name && strcmp(actual_name, expected_name) == 0) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Every enum value must either be unsupported (NULL) or map to a spec tagged
|
||||
* with that exact language. A missing designated initializer must never alias
|
||||
* the zero-initialized CBM_LANG_GO slot. */
|
||||
TEST(repro_language_specs_do_not_alias_other_languages) {
|
||||
int failures = 0;
|
||||
for (int value = 0; value < CBM_LANG_COUNT; value++) {
|
||||
CBMLanguage language = (CBMLanguage)value;
|
||||
const CBMLangSpec *spec = cbm_lang_spec(language);
|
||||
if (spec && spec->language != language) {
|
||||
fprintf(
|
||||
stderr,
|
||||
" [language-registry] invariant=spec_language_mismatch requested=%d actual=%d\n",
|
||||
value, (int)spec->language);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_language_capability_ledger_covers_every_enum) {
|
||||
int failures = 0;
|
||||
int counts[CAP_COUNT] = {0};
|
||||
|
||||
for (int value = 0; value < CBM_LANG_COUNT; value++) {
|
||||
const LanguageCapabilityEntry *entry = &LANGUAGE_CAPABILITIES[value];
|
||||
const CBMLangSpec *spec = cbm_lang_spec((CBMLanguage)value);
|
||||
if (entry->capability <= CAP_UNSET || entry->capability >= CAP_COUNT || !entry->name ||
|
||||
!entry->reason) {
|
||||
fprintf(stderr, " [language-registry] invariant=capability_unset requested=%d\n",
|
||||
value);
|
||||
failures++;
|
||||
continue;
|
||||
}
|
||||
counts[entry->capability]++;
|
||||
|
||||
if (entry->capability == CAP_UNSUPPORTED || entry->capability == CAP_TRANSFORM_ONLY) {
|
||||
if (spec) {
|
||||
fprintf(stderr,
|
||||
" [language-registry] lang=%s "
|
||||
"invariant=non_grammar_capability_spec_nonnull capability=%d actual=%d\n",
|
||||
entry->name, (int)entry->capability, (int)spec->language);
|
||||
failures++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!spec || spec->language != (CBMLanguage)value) {
|
||||
fprintf(stderr, " [language-registry] lang=%s invariant=registered_spec_missing\n",
|
||||
entry->name);
|
||||
failures++;
|
||||
continue;
|
||||
}
|
||||
|
||||
int has_call_metadata = spec->call_node_types && spec->call_node_types[0];
|
||||
int expects_call_metadata = entry->capability == CAP_CALL_WITH_REFERENCE_VOCAB ||
|
||||
entry->capability == CAP_CALL_WITHOUT_REFERENCE_VOCAB;
|
||||
if (has_call_metadata != expects_call_metadata) {
|
||||
fprintf(stderr,
|
||||
" [language-registry] lang=%s invariant=call_capability_mismatch expected=%d "
|
||||
"actual=%d\n",
|
||||
entry->name, expects_call_metadata, has_call_metadata);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
if (counts[CAP_CALL_WITH_REFERENCE_VOCAB] != 86 ||
|
||||
counts[CAP_CALL_WITHOUT_REFERENCE_VOCAB] != 25 || counts[CAP_NO_CALL] != 50 ||
|
||||
counts[CAP_TRANSFORM_ONLY] != 1 || counts[CAP_UNSUPPORTED] != 1) {
|
||||
fprintf(stderr,
|
||||
" [language-registry] invariant=capability_partition call_ref_vocab=%d ref_gap=%d "
|
||||
"no_call=%d transform_only=%d unsupported=%d\n",
|
||||
counts[CAP_CALL_WITH_REFERENCE_VOCAB], counts[CAP_CALL_WITHOUT_REFERENCE_VOCAB],
|
||||
counts[CAP_NO_CALL], counts[CAP_TRANSFORM_ONLY], counts[CAP_UNSUPPORTED]);
|
||||
failures++;
|
||||
}
|
||||
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_call_argument_matrices_equal_call_capability_ledger) {
|
||||
enum {
|
||||
EXPECTED_MATRIX_A_ROWS = 67,
|
||||
EXPECTED_MATRIX_B_ROWS = 46,
|
||||
EXPECTED_CALL_CAPABLE_LANGUAGES = 111,
|
||||
EXPECTED_NON_CALL_LANGUAGES = 52,
|
||||
EXPECTED_NON_CALL_DOMAIN_CONTROLS = 2,
|
||||
};
|
||||
CBMLanguage matrix_a_ids[CBM_LANG_COUNT];
|
||||
CBMLanguage matrix_b_ids[CBM_LANG_COUNT];
|
||||
unsigned int matrix_a_counts[CBM_LANG_COUNT] = {0};
|
||||
unsigned int matrix_b_counts[CBM_LANG_COUNT] = {0};
|
||||
size_t matrix_a_rows = repro_call_argument_matrix_a_copy_language_ids(
|
||||
matrix_a_ids, sizeof(matrix_a_ids) / sizeof(matrix_a_ids[0]));
|
||||
size_t matrix_b_rows = repro_call_argument_matrix_b_copy_language_ids(
|
||||
matrix_b_ids, sizeof(matrix_b_ids) / sizeof(matrix_b_ids[0]));
|
||||
int failures = 0;
|
||||
|
||||
if (matrix_a_rows != EXPECTED_MATRIX_A_ROWS) {
|
||||
fprintf(stderr,
|
||||
" [language-registry] matrix=A invariant=row_count expected=%d actual=%zu\n",
|
||||
EXPECTED_MATRIX_A_ROWS, matrix_a_rows);
|
||||
failures++;
|
||||
}
|
||||
if (matrix_b_rows != EXPECTED_MATRIX_B_ROWS) {
|
||||
fprintf(stderr,
|
||||
" [language-registry] matrix=B invariant=row_count expected=%d actual=%zu\n",
|
||||
EXPECTED_MATRIX_B_ROWS, matrix_b_rows);
|
||||
failures++;
|
||||
}
|
||||
|
||||
size_t matrix_a_copied =
|
||||
matrix_a_rows < CBM_LANG_COUNT ? matrix_a_rows : (size_t)CBM_LANG_COUNT;
|
||||
for (size_t row = 0; row < matrix_a_copied; row++) {
|
||||
int language_id = (int)matrix_a_ids[row];
|
||||
if (language_id < 0 || language_id >= CBM_LANG_COUNT) {
|
||||
fprintf(stderr,
|
||||
" [language-registry] matrix=A row=%zu invariant=language_id_range id=%d\n",
|
||||
row, language_id);
|
||||
failures++;
|
||||
continue;
|
||||
}
|
||||
matrix_a_counts[language_id]++;
|
||||
}
|
||||
|
||||
size_t matrix_b_copied =
|
||||
matrix_b_rows < CBM_LANG_COUNT ? matrix_b_rows : (size_t)CBM_LANG_COUNT;
|
||||
for (size_t row = 0; row < matrix_b_copied; row++) {
|
||||
int language_id = (int)matrix_b_ids[row];
|
||||
if (language_id < 0 || language_id >= CBM_LANG_COUNT) {
|
||||
fprintf(stderr,
|
||||
" [language-registry] matrix=B row=%zu invariant=language_id_range id=%d\n",
|
||||
row, language_id);
|
||||
failures++;
|
||||
continue;
|
||||
}
|
||||
matrix_b_counts[language_id]++;
|
||||
}
|
||||
|
||||
int call_capable_languages = 0;
|
||||
int covered_call_capable_languages = 0;
|
||||
int explicitly_non_call_languages = 0;
|
||||
int covered_non_call_domain_controls = 0;
|
||||
for (int language_id = 0; language_id < CBM_LANG_COUNT; language_id++) {
|
||||
const LanguageCapabilityEntry *entry = &LANGUAGE_CAPABILITIES[language_id];
|
||||
const char *language_name = entry->name ? entry->name : "<unclassified>";
|
||||
unsigned int matrix_a_count = matrix_a_counts[language_id];
|
||||
unsigned int matrix_b_count = matrix_b_counts[language_id];
|
||||
unsigned int total_count = matrix_a_count + matrix_b_count;
|
||||
int call_capable = entry->capability == CAP_CALL_WITH_REFERENCE_VOCAB ||
|
||||
entry->capability == CAP_CALL_WITHOUT_REFERENCE_VOCAB;
|
||||
|
||||
if (matrix_a_count > 1) {
|
||||
fprintf(stderr,
|
||||
" [language-registry] matrix=A lang=%s id=%d invariant=duplicate_language "
|
||||
"actual=%u\n",
|
||||
language_name, language_id, matrix_a_count);
|
||||
failures++;
|
||||
}
|
||||
if (matrix_b_count > 1) {
|
||||
fprintf(stderr,
|
||||
" [language-registry] matrix=B lang=%s id=%d invariant=duplicate_language "
|
||||
"actual=%u\n",
|
||||
language_name, language_id, matrix_b_count);
|
||||
failures++;
|
||||
}
|
||||
if (matrix_a_count && matrix_b_count) {
|
||||
fprintf(stderr,
|
||||
" [language-registry] lang=%s id=%d invariant=matrix_overlap matrix_a=%u "
|
||||
"matrix_b=%u\n",
|
||||
language_name, language_id, matrix_a_count, matrix_b_count);
|
||||
failures++;
|
||||
}
|
||||
|
||||
if (call_capable) {
|
||||
call_capable_languages++;
|
||||
if (total_count == 0) {
|
||||
fprintf(stderr,
|
||||
" [language-registry] lang=%s id=%d "
|
||||
"invariant=call_capability_missing_from_matrices\n",
|
||||
language_name, language_id);
|
||||
failures++;
|
||||
} else {
|
||||
covered_call_capable_languages++;
|
||||
if (total_count != 1) {
|
||||
fprintf(stderr,
|
||||
" [language-registry] lang=%s id=%d "
|
||||
"invariant=call_capability_matrix_count expected=1 actual=%u\n",
|
||||
language_name, language_id, total_count);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry->capability != CAP_NO_CALL && entry->capability != CAP_TRANSFORM_ONLY &&
|
||||
entry->capability != CAP_UNSUPPORTED) {
|
||||
fprintf(stderr,
|
||||
" [language-registry] lang=%s id=%d "
|
||||
"invariant=remaining_capability_explicit "
|
||||
"expected=NO_CALL_or_TRANSFORM_ONLY_or_UNSUPPORTED "
|
||||
"actual=%d\n",
|
||||
language_name, language_id, (int)entry->capability);
|
||||
failures++;
|
||||
} else {
|
||||
explicitly_non_call_languages++;
|
||||
}
|
||||
int expected_domain_control =
|
||||
language_id == CBM_LANG_DIFF || language_id == CBM_LANG_BIBTEX;
|
||||
if (expected_domain_control && total_count == 1) {
|
||||
covered_non_call_domain_controls++;
|
||||
} else if (total_count != 0 || expected_domain_control) {
|
||||
fprintf(stderr,
|
||||
" [language-registry] lang=%s id=%d "
|
||||
"invariant=non_call_domain_control_coverage expected=%d "
|
||||
"matrix_a=%u matrix_b=%u\n",
|
||||
language_name, language_id, expected_domain_control, matrix_a_count,
|
||||
matrix_b_count);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
if (call_capable_languages != EXPECTED_CALL_CAPABLE_LANGUAGES ||
|
||||
covered_call_capable_languages != EXPECTED_CALL_CAPABLE_LANGUAGES ||
|
||||
explicitly_non_call_languages != EXPECTED_NON_CALL_LANGUAGES ||
|
||||
covered_non_call_domain_controls != EXPECTED_NON_CALL_DOMAIN_CONTROLS) {
|
||||
fprintf(stderr,
|
||||
" [language-registry] invariant=matrix_ledger_partition "
|
||||
"call_capable_expected=%d call_capable_actual=%d covered_actual=%d "
|
||||
"non_call_expected=%d non_call_actual=%d "
|
||||
"non_call_controls_expected=%d non_call_controls_actual=%d\n",
|
||||
EXPECTED_CALL_CAPABLE_LANGUAGES, call_capable_languages,
|
||||
covered_call_capable_languages, EXPECTED_NON_CALL_LANGUAGES,
|
||||
explicitly_non_call_languages, EXPECTED_NON_CALL_DOMAIN_CONTROLS,
|
||||
covered_non_call_domain_controls);
|
||||
failures++;
|
||||
}
|
||||
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A call-node registration that does not exist in the compiled grammar can
|
||||
* never match an AST node. Check the compiled language instead of trusting
|
||||
* hand-maintained spec strings or the graph index. */
|
||||
TEST(repro_primary_call_metadata_names_exist_in_grammars) {
|
||||
int failures = 0;
|
||||
|
||||
for (int value = 0; value < CBM_LANG_COUNT; value++) {
|
||||
const LanguageCapabilityEntry *entry = &LANGUAGE_CAPABILITIES[value];
|
||||
const CBMLangSpec *spec = cbm_lang_spec((CBMLanguage)value);
|
||||
const TSLanguage *language = cbm_ts_language((CBMLanguage)value);
|
||||
if (!spec || !spec->call_node_types || !spec->call_node_types[0]) {
|
||||
continue;
|
||||
}
|
||||
if (!language) {
|
||||
fprintf(stderr,
|
||||
" [language-registry] lang=%s invariant=call_metadata_grammar_missing\n",
|
||||
entry->name);
|
||||
failures++;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int i = 0; spec->call_node_types[i]; i++) {
|
||||
const char *call_node_type = spec->call_node_types[i];
|
||||
if (!grammar_has_symbol(language, call_node_type)) {
|
||||
fprintf(stderr,
|
||||
" [language-registry] lang=%s "
|
||||
"invariant=call_node_type_exists node=%s\n",
|
||||
entry->name, call_node_type);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* LinkerScript's compiled grammar root is `linkerscript`, while the language
|
||||
* spec currently registers `source_file`. Path-derived module QNs hide the
|
||||
* mismatch, so exercise the AST-to-metadata contract directly. */
|
||||
TEST(repro_linkerscript_root_is_registered_as_module) {
|
||||
static const char source[] = "SECTIONS { .text : { *(.text) } }\n";
|
||||
const CBMLangSpec *spec = cbm_lang_spec(CBM_LANG_LINKERSCRIPT);
|
||||
const TSLanguage *language = cbm_ts_language(CBM_LANG_LINKERSCRIPT);
|
||||
TSParser *parser = ts_parser_new();
|
||||
int failures = 0;
|
||||
|
||||
if (!spec || !language || !parser || !ts_parser_set_language(parser, language)) {
|
||||
fprintf(stderr, " [language-registry] lang=LINKERSCRIPT invariant=parser_setup\n");
|
||||
failures++;
|
||||
} else {
|
||||
TSTree *tree = ts_parser_parse_string(parser, NULL, source, (uint32_t)strlen(source));
|
||||
if (!tree) {
|
||||
fprintf(stderr,
|
||||
" [language-registry] lang=LINKERSCRIPT invariant=parse_tree_nonnull\n");
|
||||
failures++;
|
||||
} else {
|
||||
TSNode root = ts_tree_root_node(tree);
|
||||
const char *root_kind = ts_node_type(root);
|
||||
int registered = 0;
|
||||
if (spec->module_node_types) {
|
||||
for (int i = 0; spec->module_node_types[i]; i++) {
|
||||
if (strcmp(spec->module_node_types[i], root_kind) == 0) {
|
||||
registered = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ts_node_has_error(root)) {
|
||||
fprintf(stderr,
|
||||
" [language-registry] lang=LINKERSCRIPT invariant=valid_fixture\n");
|
||||
failures++;
|
||||
}
|
||||
if (!registered) {
|
||||
fprintf(stderr,
|
||||
" [language-registry] lang=LINKERSCRIPT "
|
||||
"invariant=root_registered_as_module root=%s\n",
|
||||
root_kind);
|
||||
failures++;
|
||||
}
|
||||
ts_tree_delete(tree);
|
||||
}
|
||||
}
|
||||
if (parser) {
|
||||
ts_parser_delete(parser);
|
||||
}
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(repro_language_registry) {
|
||||
RUN_TEST(repro_language_specs_do_not_alias_other_languages);
|
||||
RUN_TEST(repro_language_capability_ledger_covers_every_enum);
|
||||
RUN_TEST(repro_call_argument_matrices_equal_call_capability_ledger);
|
||||
RUN_TEST(repro_primary_call_metadata_names_exist_in_grammars);
|
||||
RUN_TEST(repro_linkerscript_root_is_registered_as_module);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -57,9 +57,8 @@
|
||||
* lsp_static_call (1904) ClassName.staticMethod()
|
||||
* lsp_interface_resolve (1985) iface-typed call, SOLE concrete impl
|
||||
* lsp_interface_dispatch (1990) iface-typed call, no sole impl
|
||||
* lsp_method_ref_ctor (2591) ClassName::new, ctor indexed
|
||||
* lsp_method_ref_ctor_synth(2594) ClassName::new, ctor NOT in registry
|
||||
* lsp_method_ref (2614) Type::instanceMethod reference
|
||||
* lsp_method_ref_ctor (2758) ClassName::new, ctor indexed
|
||||
* lsp_method_ref (2783) Type::instanceMethod reference
|
||||
* lsp_constructor (2787) new Foo(), ctor indexed
|
||||
* lsp_constructor_synth (2792) new Foo(), ctor NOT in registry
|
||||
* lsp_unresolved (1801) fallback marker for an unresolved call
|
||||
@@ -153,6 +152,63 @@ static int assert_lsp_strategy(const char *filename, const char *src,
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int count_edges_from_callable(cbm_store_t *store, const char *project, const char *edge_type,
|
||||
const char *callable_name) {
|
||||
cbm_edge_t *edges = NULL;
|
||||
int edge_count = 0;
|
||||
if (cbm_store_find_edges_by_type(store, project, edge_type, &edges, &edge_count) !=
|
||||
CBM_STORE_OK) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int matches = 0;
|
||||
for (int i = 0; i < edge_count; i++) {
|
||||
cbm_node_t source = {0};
|
||||
if (cbm_store_find_node_by_id(store, edges[i].source_id, &source) == CBM_STORE_OK &&
|
||||
source.name && strcmp(source.name, callable_name) == 0 && source.label &&
|
||||
(strcmp(source.label, "Function") == 0 || strcmp(source.label, "Method") == 0)) {
|
||||
matches++;
|
||||
}
|
||||
cbm_node_free_fields(&source);
|
||||
}
|
||||
cbm_store_free_edges(edges, edge_count);
|
||||
return matches;
|
||||
}
|
||||
|
||||
static int assert_method_reference_semantics(const char *filename, const char *src,
|
||||
int target_is_materialized) {
|
||||
RProj project;
|
||||
cbm_store_t *store = rh_index(&project, filename, src);
|
||||
if (!store) {
|
||||
printf(" %sFAIL%s %s:%d: method-reference fixture failed to index\n", tf_red(), tf_reset(),
|
||||
__FILE__, __LINE__);
|
||||
rh_cleanup(&project, store);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int references = count_edges_from_callable(store, project.project, "CALL_REFERENCE", "run");
|
||||
int calls = count_edges_from_callable(store, project.project, "CALLS", "run");
|
||||
int usages = count_edges_from_callable(store, project.project, "USAGE", "run");
|
||||
int rc = 0;
|
||||
if (references != (target_is_materialized ? 1 : 0)) {
|
||||
printf(" %sFAIL%s %s:%d: method reference count expected=%d actual=%d\n", tf_red(),
|
||||
tf_reset(), __FILE__, __LINE__, target_is_materialized ? 1 : 0, references);
|
||||
rc = 1;
|
||||
}
|
||||
if (calls != 0) {
|
||||
printf(" %sFAIL%s %s:%d: method reference fabricated %d CALLS edges\n", tf_red(),
|
||||
tf_reset(), __FILE__, __LINE__, calls);
|
||||
rc = 1;
|
||||
}
|
||||
if (!target_is_materialized && usages < 1) {
|
||||
printf(" %sFAIL%s %s:%d: unmaterialized constructor reference lost its USAGE\n", tf_red(),
|
||||
tf_reset(), __FILE__, __LINE__);
|
||||
rc = 1;
|
||||
}
|
||||
rh_cleanup(&project, store);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/*
|
||||
* assert_no_resolvable_edge — the ACCURATE invariant for a call whose callee is
|
||||
* genuinely UNRESOLVABLE: undeclared (totallyUnknownFn), an external symbol
|
||||
@@ -336,9 +392,8 @@ static const char kJavaMethodRefCtor[] =
|
||||
" Maker run() { return Foo::new; }\n"
|
||||
"}\n";
|
||||
|
||||
/* lsp_method_ref_ctor_synth — a constructor reference ClassName::new whose ctor
|
||||
* is NOT in the registry, so the resolver synthesizes the ctor QN
|
||||
* (java_lsp.c:2592-2594). Foo declares no explicit constructor. */
|
||||
/* A constructor reference ClassName::new whose ctor is NOT materialized. The
|
||||
* reference must remain ordinary USAGE; a class node is not a callable target. */
|
||||
static const char kJavaMethodRefCtorSynth[] =
|
||||
"interface Maker {\n"
|
||||
" Foo make();\n"
|
||||
@@ -607,17 +662,15 @@ TEST(repro_lsp_java_interface_dispatch) {
|
||||
}
|
||||
|
||||
TEST(repro_lsp_java_method_ref_ctor) {
|
||||
return assert_lsp_strategy("Client.java", kJavaMethodRefCtor,
|
||||
"lsp_method_ref_ctor");
|
||||
return assert_method_reference_semantics("Client.java", kJavaMethodRefCtor, 1);
|
||||
}
|
||||
|
||||
TEST(repro_lsp_java_method_ref_ctor_synth) {
|
||||
return assert_lsp_strategy("Client.java", kJavaMethodRefCtorSynth,
|
||||
"lsp_method_ref_ctor_synth");
|
||||
return assert_method_reference_semantics("Client.java", kJavaMethodRefCtorSynth, 0);
|
||||
}
|
||||
|
||||
TEST(repro_lsp_java_method_ref) {
|
||||
return assert_lsp_strategy("Client.java", kJavaMethodRef, "lsp_method_ref");
|
||||
return assert_method_reference_semantics("Client.java", kJavaMethodRef, 1);
|
||||
}
|
||||
|
||||
TEST(repro_lsp_java_constructor) {
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
* same two invariants per (lang,strategy) — (a) inv_count_calls_by_source
|
||||
* module_sourced == 0 and a callable-sourced CALLS edge exists, and (b)
|
||||
* inv_edge_has_strategy(store, project, "<strategy>"). One TEST per
|
||||
* (lang,strategy); SUITE(repro_lsp_kt_php_rust) at the bottom.
|
||||
* (lang,strategy), plus two Rust dispatch-provenance guards;
|
||||
* SUITE(repro_lsp_kt_php_rust) is at the bottom.
|
||||
*
|
||||
* WHAT THIS ASSERTS — the LSP RESOLUTION CONTRACT, one invariant per strategy.
|
||||
* Each hybrid LSP resolves a call via a specific STRATEGY and tags the
|
||||
@@ -36,16 +37,14 @@
|
||||
* - RED = the strategy is dropped, lands Module-sourced, or never reaches
|
||||
* the graph. The TEST documents the exact gap for the fixer.
|
||||
*
|
||||
* RUST CROSS-LSP IS NOT WIRED (documented gap). src/pipeline/pass_lsp_cross.c
|
||||
* has NO CBM_LANG_RUST case in either cbm_pxc_has_cross_lsp (lines 282-298)
|
||||
* or the cbm_pxc_run_one dispatch (lines 372-407). Go/C/C++/Python/PHP/Java/
|
||||
* Kotlin are wired; Rust is absent. So rust_lsp.c can EMIT every strategy
|
||||
* below, but those resolved calls never reach pass_lsp_cross → never become
|
||||
* tagged CALLS edges in the graph. Every Rust strategy test is therefore
|
||||
* expected RED until rust_lsp.c is wired into the pipeline. We assert the
|
||||
* CORRECT (resolved) outcome anyway, per the reproduce-first contract: the
|
||||
* red test is the durable record of the gap and turns GREEN the moment Rust
|
||||
* is wired and resolving correctly.
|
||||
* RUST CROSS-LSP CONTRACT. src/pipeline/pass_lsp_cross.c dispatches
|
||||
* CBM_LANG_RUST through the project-wide Rust registry, including exact
|
||||
* impl-block provenance and independent `impl Trait for Type` relations for
|
||||
* default-only impls. The Rust cases below are intended GREEN regression
|
||||
* guards. Dispatch follows Rust precedence: an inherent method wins over a
|
||||
* same-named trait method; otherwise an exact trait override or unambiguous
|
||||
* trait default may resolve. Missing or conflicting provenance fails closed:
|
||||
* the resolver must not guess a trait or method from a shared leaf name.
|
||||
*
|
||||
* SKIPPED STRATEGIES (documented, not tested):
|
||||
* Kotlin:
|
||||
@@ -64,14 +63,15 @@
|
||||
* synthetic module scope (a `mod foo;` declaration has no
|
||||
* enclosing callable). It would violate invariant (a)
|
||||
* (module_sourced == 0) by construction, so the shared
|
||||
* runner cannot express it. Also blocked by the unwired-
|
||||
* Rust gap above.
|
||||
* runner cannot express it.
|
||||
* - lsp_deref_dispatch / lsp_bound_dispatch / lsp_prelude_trait /
|
||||
* lsp_short_name_unique / lsp_trait_ufcs_amb — emitted on harder-to-
|
||||
* fixture paths (Deref chains, type-param bounds, prelude best-effort,
|
||||
* crate-prefix short-name scan, multi-impl ambiguity). They are all also
|
||||
* blocked by the unwired-Rust gap, so adding fragile fixtures for them
|
||||
* buys nothing over the representative dispatch tests below; skipped.
|
||||
* lsp_short_name_unique — emitted on harder-to-fixture paths (Deref
|
||||
* chains, type-param bounds, prelude best-effort, crate-prefix short-name
|
||||
* scan). Representative exact-provenance dispatch is covered below;
|
||||
* these specialized strategies remain skipped. Multi-trait ambiguity is
|
||||
* deliberately not a resolved strategy: without exact qualification,
|
||||
* competing trait methods/defaults fail closed instead of emitting a
|
||||
* guessed CALLS edge.
|
||||
*
|
||||
* STRATEGY INVENTORIES — every strategy literal grepped from each source:
|
||||
* Kotlin (kotlin_lsp.c, grep '"lsp_kt_'):
|
||||
@@ -113,7 +113,7 @@
|
||||
* lsp_operator_trait (2443) a + b where T : Add (operator overload)
|
||||
* lsp_macro (3832) known std macro (println!/vec!/panic!)
|
||||
* lsp_deref_dispatch / lsp_bound_dispatch / lsp_prelude_trait /
|
||||
* lsp_short_name_unique / lsp_trait_ufcs_amb / lsp_mod_decl (skipped, see above)
|
||||
* lsp_short_name_unique / lsp_mod_decl (skipped, see above)
|
||||
* lsp_unresolved (3393) fallback marker
|
||||
*
|
||||
* NOTE: line comments only inside this header (no nested block comments, per
|
||||
@@ -188,6 +188,92 @@ static int assert_lsp_strategy(const char *filename, const char *src,
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Assert required/forbidden strategy provenance on the SAME Rust CALLS edge.
|
||||
* Restricting by caller and callee avoids a false green from an unrelated edge
|
||||
* elsewhere in a multi-file fixture (for example `mod contract;`). */
|
||||
static int assert_rust_dispatch_files(const RFile *files, int nfiles, const char *caller_substr,
|
||||
const char *callee_suffix, const char *required_strategy,
|
||||
const char *forbidden_strategy) {
|
||||
RProj lp;
|
||||
cbm_store_t *store = rh_index_files(&lp, files, nfiles);
|
||||
if (!store) {
|
||||
printf(" %sFAIL%s %s:%d: index failed for Rust %s -> %s dispatch\n", tf_red(), tf_reset(),
|
||||
__FILE__, __LINE__, caller_substr, callee_suffix);
|
||||
rh_cleanup(&lp, store);
|
||||
return 1;
|
||||
}
|
||||
|
||||
cbm_edge_t *edges = NULL;
|
||||
int edge_count = 0;
|
||||
if (cbm_store_find_edges_by_type(store, lp.project, "CALLS", &edges, &edge_count) !=
|
||||
CBM_STORE_OK) {
|
||||
printf(" %sFAIL%s %s:%d: CALLS query failed for Rust %s -> %s dispatch\n", tf_red(),
|
||||
tf_reset(), __FILE__, __LINE__, caller_substr, callee_suffix);
|
||||
rh_cleanup(&lp, store);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int matching_edges = 0;
|
||||
int required_edges = 0;
|
||||
int forbidden_edges = 0;
|
||||
const char *observed_properties = NULL;
|
||||
size_t suffix_len = strlen(callee_suffix);
|
||||
for (int i = 0; i < edge_count; i++) {
|
||||
cbm_node_t source;
|
||||
cbm_node_t target;
|
||||
if (cbm_store_find_node_by_id(store, edges[i].source_id, &source) != CBM_STORE_OK ||
|
||||
cbm_store_find_node_by_id(store, edges[i].target_id, &target) != CBM_STORE_OK) {
|
||||
continue;
|
||||
}
|
||||
const char *source_label = source.label ? source.label : "";
|
||||
if (strcmp(source_label, "Function") != 0 && strcmp(source_label, "Method") != 0) {
|
||||
continue;
|
||||
}
|
||||
if (!source.qualified_name || !strstr(source.qualified_name, caller_substr) ||
|
||||
!target.qualified_name) {
|
||||
continue;
|
||||
}
|
||||
size_t target_len = strlen(target.qualified_name);
|
||||
if (target_len < suffix_len ||
|
||||
strcmp(target.qualified_name + target_len - suffix_len, callee_suffix) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
matching_edges++;
|
||||
const char *properties = edges[i].properties_json ? edges[i].properties_json : "{}";
|
||||
if (!observed_properties) {
|
||||
observed_properties = properties;
|
||||
}
|
||||
if (strstr(properties, required_strategy)) {
|
||||
required_edges++;
|
||||
}
|
||||
if (forbidden_strategy && strstr(properties, forbidden_strategy)) {
|
||||
forbidden_edges++;
|
||||
}
|
||||
}
|
||||
|
||||
int rc = 0;
|
||||
if (matching_edges == 0 || required_edges == 0) {
|
||||
printf(" %sFAIL%s %s:%d: Rust %s -> *%s has %d matching CALLS edges, "
|
||||
"but none with required strategy %s (observed %s)\n",
|
||||
tf_red(), tf_reset(), __FILE__, __LINE__, caller_substr, callee_suffix,
|
||||
matching_edges, required_strategy,
|
||||
observed_properties ? observed_properties : "<none>");
|
||||
rc = 1;
|
||||
}
|
||||
if (forbidden_edges != 0) {
|
||||
printf(" %sFAIL%s %s:%d: Rust %s -> *%s has %d CALLS edges with "
|
||||
"forbidden strategy %s\n",
|
||||
tf_red(), tf_reset(), __FILE__, __LINE__, caller_substr, callee_suffix,
|
||||
forbidden_edges, forbidden_strategy);
|
||||
rc = 1;
|
||||
}
|
||||
|
||||
cbm_store_free_edges(edges, edge_count);
|
||||
rh_cleanup(&lp, store);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════════
|
||||
* KOTLIN FIXTURES (main.kt) — every fixture keeps the call inside a callable
|
||||
* (a top-level fun or a method) so callable-sourcing is testable, and the
|
||||
@@ -399,10 +485,9 @@ static const char kPhpSelfStatic[] =
|
||||
"}\n";
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════════
|
||||
* RUST FIXTURES (main.rs) — Rust cross-LSP is NOT wired into pass_lsp_cross
|
||||
* (see header), so ALL of these are expected RED until rust_lsp.c is wired.
|
||||
* Each fixture still exercises exactly the keyed construct so the test turns
|
||||
* GREEN the moment Rust resolution reaches the graph.
|
||||
* RUST FIXTURES (main.rs) — Rust cross-LSP is wired through pass_lsp_cross.
|
||||
* These are intended GREEN guards for callable attribution, strategy tags,
|
||||
* exact inherent/trait provenance, and default-only cross-file trait impls.
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* lsp_direct — plain free-function call (rust_lsp.c:3580: path resolves to a
|
||||
@@ -439,6 +524,35 @@ static const char kRustTraitDispatch[] =
|
||||
" d.speak(2)\n"
|
||||
"}\n";
|
||||
|
||||
/* Inherent methods take precedence over same-named trait methods. The shared
|
||||
* Rust registry must preserve that distinction even though both concrete
|
||||
* definitions have receiver S and the type also implements Pinger. */
|
||||
static const char kRustInherentTraitCollision[] = "trait Pinger {\n"
|
||||
" fn ping(&self) -> i32;\n"
|
||||
"}\n"
|
||||
"struct S;\n"
|
||||
"impl Pinger for S {\n"
|
||||
" fn ping(&self) -> i32 { 2 }\n"
|
||||
"}\n"
|
||||
"impl S {\n"
|
||||
" fn ping(&self) -> i32 { 1 }\n"
|
||||
"}\n"
|
||||
"fn caller(s: &S) -> i32 { s.ping() }\n";
|
||||
|
||||
/* Cross-file default-only trait dispatch. S has no concrete `ping` Method def;
|
||||
* the project-wide registry must therefore carry `impl Pinger for S` itself,
|
||||
* rather than relying on provenance attached to an overriding method. */
|
||||
static const RFile kRustDefaultTraitDispatch[] = {
|
||||
{"contract.rs", "pub trait Pinger {\n"
|
||||
" fn ping(&self) -> i32 { 7 }\n"
|
||||
"}\n"
|
||||
"pub struct S;\n"
|
||||
"impl Pinger for S {}\n"},
|
||||
{"main.rs", "mod contract;\n"
|
||||
"use contract::Pinger;\n"
|
||||
"fn caller(s: &contract::S) -> i32 { s.ping() }\n"},
|
||||
};
|
||||
|
||||
/* lsp_constructor — Type::new() UFCS constructor (rust_lsp.c:3607: UFCS head is
|
||||
* a type, short_name == "new"). */
|
||||
static const char kRustConstructor[] =
|
||||
@@ -594,7 +708,7 @@ TEST(repro_lsp_php_self_static) {
|
||||
return assert_lsp_strategy("main.php", kPhpSelfStatic, "php_self_static");
|
||||
}
|
||||
|
||||
/* Rust — all expected RED (cross-LSP not wired; see header). */
|
||||
/* Rust — intended GREEN, including exact dispatch-provenance guards. */
|
||||
TEST(repro_lsp_rust_direct) {
|
||||
return assert_lsp_strategy("main.rs", kRustDirect, "lsp_direct");
|
||||
}
|
||||
@@ -606,6 +720,17 @@ TEST(repro_lsp_rust_trait_dispatch) {
|
||||
return assert_lsp_strategy("main.rs", kRustTraitDispatch,
|
||||
"lsp_trait_dispatch");
|
||||
}
|
||||
TEST(repro_lsp_rust_inherent_trait_collision) {
|
||||
RFile file = {"main.rs", kRustInherentTraitCollision};
|
||||
return assert_rust_dispatch_files(&file, 1, "caller", ".ping", "lsp_method_dispatch",
|
||||
"lsp_trait_dispatch");
|
||||
}
|
||||
TEST(repro_lsp_rust_default_trait_dispatch_cross_file) {
|
||||
return assert_rust_dispatch_files(
|
||||
kRustDefaultTraitDispatch,
|
||||
(int)(sizeof(kRustDefaultTraitDispatch) / sizeof(kRustDefaultTraitDispatch[0])), "caller",
|
||||
".ping", "lsp_trait_dispatch", "lsp_method_dispatch");
|
||||
}
|
||||
TEST(repro_lsp_rust_constructor) {
|
||||
return assert_lsp_strategy("main.rs", kRustConstructor, "lsp_constructor");
|
||||
}
|
||||
@@ -673,10 +798,12 @@ SUITE(repro_lsp_kt_php_rust) {
|
||||
RUN_TEST(repro_lsp_php_static_resolved);
|
||||
RUN_TEST(repro_lsp_php_self_static);
|
||||
|
||||
/* Rust — expected RED (cross-LSP not wired). */
|
||||
/* Rust — eight strategy inventories plus two dispatch-provenance guards. */
|
||||
RUN_TEST(repro_lsp_rust_direct);
|
||||
RUN_TEST(repro_lsp_rust_method_dispatch);
|
||||
RUN_TEST(repro_lsp_rust_trait_dispatch);
|
||||
RUN_TEST(repro_lsp_rust_inherent_trait_collision);
|
||||
RUN_TEST(repro_lsp_rust_default_trait_dispatch_cross_file);
|
||||
RUN_TEST(repro_lsp_rust_constructor);
|
||||
RUN_TEST(repro_lsp_rust_ufcs);
|
||||
RUN_TEST(repro_lsp_rust_trait_ufcs);
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
/*
|
||||
* repro_lsp_ordered_local.c — Behaviour-level REDs for positional signatures
|
||||
* after a language's local/post-extraction registry refinement.
|
||||
*
|
||||
* These cases deliberately assert a downstream call target, not merely the
|
||||
* extracted metadata. A GREEN transition therefore means the ordered slots
|
||||
* survived far enough to change resolver selection/type inference. Strategy
|
||||
* and target checks prevent a raw unique-name fallback from satisfying the
|
||||
* oracle accidentally.
|
||||
*/
|
||||
|
||||
#include "test_framework.h"
|
||||
|
||||
#include "arena.h"
|
||||
#include "cbm.h"
|
||||
#include "lsp/java_lsp.h"
|
||||
#include "lsp/ts_lsp.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static bool ends_with(const char *text, const char *suffix) {
|
||||
if (!text || !suffix)
|
||||
return false;
|
||||
size_t text_len = strlen(text);
|
||||
size_t suffix_len = strlen(suffix);
|
||||
return suffix_len <= text_len && strcmp(text + text_len - suffix_len, suffix) == 0;
|
||||
}
|
||||
|
||||
static void print_resolved_calls(const CBMResolvedCallArray *calls) {
|
||||
if (!calls)
|
||||
return;
|
||||
for (int i = 0; i < calls->count; i++) {
|
||||
const CBMResolvedCall *call = &calls->items[i];
|
||||
printf(" %s -> %s [%s %.2f kind=%d]\n", call->caller_qn ? call->caller_qn : "(null)",
|
||||
call->callee_qn ? call->callee_qn : "(null)",
|
||||
call->strategy ? call->strategy : "(null)", call->confidence, (int)call->kind);
|
||||
}
|
||||
}
|
||||
|
||||
static bool has_resolved_suffix(const CBMResolvedCallArray *calls, const char *caller_sub,
|
||||
const char *callee_suffix, const char *strategy) {
|
||||
if (!calls)
|
||||
return false;
|
||||
for (int i = 0; i < calls->count; i++) {
|
||||
const CBMResolvedCall *call = &calls->items[i];
|
||||
if (call->kind != CBM_RESOLVED_INVOCATION || call->confidence < 0.5f || !call->caller_qn ||
|
||||
!strstr(call->caller_qn, caller_sub) || !ends_with(call->callee_qn, callee_suffix) ||
|
||||
!call->strategy || strcmp(call->strategy, strategy) != 0) {
|
||||
continue;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool has_resolved_exact(const CBMResolvedCallArray *calls, const char *caller_sub,
|
||||
const char *callee_qn, const char *strategy) {
|
||||
if (!calls)
|
||||
return false;
|
||||
for (int i = 0; i < calls->count; i++) {
|
||||
const CBMResolvedCall *call = &calls->items[i];
|
||||
if (call->kind == CBM_RESOLVED_INVOCATION && call->confidence >= 0.5f && call->caller_qn &&
|
||||
strstr(call->caller_qn, caller_sub) && call->callee_qn &&
|
||||
strcmp(call->callee_qn, callee_qn) == 0 && call->strategy &&
|
||||
strcmp(call->strategy, strategy) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static int assert_local_chained_target(CBMLanguage language, const char *rel_path,
|
||||
const char *source, const char *callee_suffix,
|
||||
const char *strategy) {
|
||||
CBMFileResult *result = cbm_extract_file(source, (int)strlen(source), language,
|
||||
"repro_ordered_local", rel_path, 0, NULL, NULL);
|
||||
if (!result) {
|
||||
printf(" ordered-local: extraction returned NULL for %s\n", rel_path);
|
||||
return 1;
|
||||
}
|
||||
if (result->has_error) {
|
||||
printf(" ordered-local: fixture parse failed for %s\n", rel_path);
|
||||
cbm_free_result(result);
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool found = has_resolved_suffix(&result->resolved_calls, "run", callee_suffix, strategy);
|
||||
if (!found) {
|
||||
printf(" ordered-local: missing run -> *%s via %s for %s (have %d)\n", callee_suffix,
|
||||
strategy, rel_path, result->resolved_calls.count);
|
||||
print_resolved_calls(&result->resolved_calls);
|
||||
}
|
||||
cbm_free_result(result);
|
||||
return found ? 0 : 1;
|
||||
}
|
||||
|
||||
/* Go's legacy type-usage projection stores [T,U] for `(first, second T,
|
||||
* last U)`. Correct implicit inference requires the ordered [T,T,U] carrier,
|
||||
* including preservation through the AST generic-refinement pass. */
|
||||
static const char kGoGroupedGeneric[] =
|
||||
"package main\n"
|
||||
"type Left struct{}\n"
|
||||
"type Right struct{}\n"
|
||||
"func (Right) RightOnly() {}\n"
|
||||
"func PickLast[T any, U any](first, second T, last U) U { return last }\n"
|
||||
"func run(left Left, right Right) {\n"
|
||||
" got := PickLast(left, left, right)\n"
|
||||
" got.RightOnly()\n"
|
||||
"}\n";
|
||||
|
||||
TEST(repro_lsp_ordered_local_go_grouped_generic) {
|
||||
/* Go method nodes intentionally use a flat `module.Method` graph QN;
|
||||
* receiver ownership is carried separately by receiver_type/parent_class.
|
||||
* Only Right defines RightOnly, and lsp_type_dispatch therefore proves the
|
||||
* positional generic result was inferred as Right. */
|
||||
return assert_local_chained_target(CBM_LANG_GO, "main.go", kGoGroupedGeneric, "RightOnly",
|
||||
"lsp_type_dispatch");
|
||||
}
|
||||
|
||||
/* A C++ reference return wraps the function declarator in a
|
||||
* reference_declarator. The local signature extractor must still reach the
|
||||
* nested parameter list; otherwise `operator[](int)` collapses to arity zero
|
||||
* and the exact-arity operator resolver correctly refuses it. */
|
||||
static const char kCppReferenceReturnOperator[] =
|
||||
"struct Item {};\n"
|
||||
"struct Vec {\n"
|
||||
" Item& operator[](int index) { static Item item; return item; }\n"
|
||||
"};\n"
|
||||
"void run(Vec& values) { values[0]; }\n";
|
||||
|
||||
static int assert_reference_return_operator_arity(CBMLanguage language, const char *rel_path,
|
||||
const char *language_name) {
|
||||
CBMFileResult *result =
|
||||
cbm_extract_file(kCppReferenceReturnOperator, (int)strlen(kCppReferenceReturnOperator),
|
||||
language, "repro_ordered_local", rel_path, 0, NULL, NULL);
|
||||
if (!result || result->has_error) {
|
||||
printf(" ordered-local: %s reference-return fixture extraction failed\n", language_name);
|
||||
if (result)
|
||||
cbm_free_result(result);
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool counted_signature = false;
|
||||
for (int i = 0; i < result->defs.count; i++) {
|
||||
const CBMDefinition *definition = &result->defs.items[i];
|
||||
if (definition->name && strcmp(definition->name, "operator[]") == 0 &&
|
||||
definition->signature_param_count == 1 && definition->signature_param_types &&
|
||||
definition->signature_param_types[0] &&
|
||||
strcmp(definition->signature_param_types[0], "int") == 0) {
|
||||
counted_signature = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
bool resolved =
|
||||
has_resolved_suffix(&result->resolved_calls, "run", "operator[]", "lsp_operator");
|
||||
if (!counted_signature || !resolved) {
|
||||
printf(" ordered-local: %s reference-return operator counted=%d resolved=%d\n",
|
||||
language_name, counted_signature, resolved);
|
||||
print_resolved_calls(&result->resolved_calls);
|
||||
}
|
||||
cbm_free_result(result);
|
||||
return counted_signature && resolved ? 0 : 1;
|
||||
}
|
||||
|
||||
TEST(repro_lsp_ordered_local_cpp_reference_return_operator_arity) {
|
||||
return assert_reference_return_operator_arity(CBM_LANG_CPP, "main.cpp", "C++");
|
||||
}
|
||||
|
||||
/* CUDA uses its own vendored grammar. It aliases this syntax to the same
|
||||
* reference_declarator node shape today, but must retain an independent guard
|
||||
* so a future grammar update cannot silently reintroduce the arity collapse. */
|
||||
TEST(repro_lsp_ordered_local_cuda_reference_return_operator_arity) {
|
||||
return assert_reference_return_operator_arity(CBM_LANG_CUDA, "main.cu", "CUDA");
|
||||
}
|
||||
|
||||
/* The shared C-family name resolver permits eight declarator wrappers, but the
|
||||
* parameter walk historically stopped after five. A five-level pointer return
|
||||
* therefore produced a named function whose signature silently had arity zero.
|
||||
* C, C++, and CUDA each get a real grammar guard; GLSL has no pointer syntax. */
|
||||
static const char kDeepPointerReturn[] = "int *****deep(int value) { return 0; }\n";
|
||||
|
||||
static int assert_deep_pointer_return_arity(CBMLanguage language, const char *rel_path,
|
||||
const char *language_name) {
|
||||
CBMFileResult *result =
|
||||
cbm_extract_file(kDeepPointerReturn, (int)strlen(kDeepPointerReturn), language,
|
||||
"repro_ordered_local", rel_path, 0, NULL, NULL);
|
||||
if (!result || result->has_error) {
|
||||
printf(" ordered-local: %s deep-pointer fixture extraction failed\n", language_name);
|
||||
if (result)
|
||||
cbm_free_result(result);
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool found = false;
|
||||
for (int i = 0; i < result->defs.count; i++) {
|
||||
const CBMDefinition *definition = &result->defs.items[i];
|
||||
if (definition->name && strcmp(definition->name, "deep") == 0 &&
|
||||
definition->signature_param_count == 1 && definition->signature_param_types &&
|
||||
definition->signature_param_types[0] &&
|
||||
strcmp(definition->signature_param_types[0], "int") == 0) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
printf(" ordered-local: %s deep-pointer return lost parameter arity\n", language_name);
|
||||
}
|
||||
cbm_free_result(result);
|
||||
return found ? 0 : 1;
|
||||
}
|
||||
|
||||
TEST(repro_lsp_ordered_local_c_deep_pointer_return_arity) {
|
||||
return assert_deep_pointer_return_arity(CBM_LANG_C, "main.c", "C");
|
||||
}
|
||||
|
||||
TEST(repro_lsp_ordered_local_cpp_deep_pointer_return_arity) {
|
||||
return assert_deep_pointer_return_arity(CBM_LANG_CPP, "main.cpp", "C++");
|
||||
}
|
||||
|
||||
TEST(repro_lsp_ordered_local_cuda_deep_pointer_return_arity) {
|
||||
return assert_deep_pointer_return_arity(CBM_LANG_CUDA, "main.cu", "CUDA");
|
||||
}
|
||||
|
||||
/* GREEN controls for the local TypeScript AST-rebuild path. The pseudo `this`
|
||||
* parameter is not an argument and is excluded from the ordered carrier. The
|
||||
* AST rebuild also skips it because it is not an identifier pattern, then
|
||||
* reconstructs [T,T,U], so these pass even before ordered-carrier transport is
|
||||
* adopted. They verify post-AST positional behavior, not transport itself. */
|
||||
static const char kTsThisGeneric[] =
|
||||
"class Left {}\n"
|
||||
"class Right { rightOnly(): void {} }\n"
|
||||
"function pickLast<T, U>(this: void, first: T, second: T, last: U): U {\n"
|
||||
" return last;\n"
|
||||
"}\n"
|
||||
"function run(left: Left, right: Right): void {\n"
|
||||
" const got = pickLast(left, left, right);\n"
|
||||
" got.rightOnly();\n"
|
||||
"}\n";
|
||||
|
||||
TEST(repro_lsp_ordered_local_control_typescript_this_receiver) {
|
||||
return assert_local_chained_target(CBM_LANG_TYPESCRIPT, "main.ts", kTsThisGeneric,
|
||||
"Right.rightOnly", "lsp_ts_method");
|
||||
}
|
||||
|
||||
TEST(repro_lsp_ordered_local_control_tsx_this_receiver) {
|
||||
return assert_local_chained_target(CBM_LANG_TSX, "main.tsx", kTsThisGeneric, "Right.rightOnly",
|
||||
"lsp_ts_method");
|
||||
}
|
||||
|
||||
/* Interface method signatures are synthesized by the TS AST shape sweep rather
|
||||
* than supplied as graph defs. Put the same two parameter types in the wrong
|
||||
* order first, then require the reversed two-argument overload plus a non-first
|
||||
* zero-argument overload. Arity alone cannot pass this: the selected edge and
|
||||
* return-chain type must agree on ordered parameter types, and the post-sweep
|
||||
* overloads must remain visible through a finalized registry. */
|
||||
static const char kTsInterfaceOverloadReturnChains[] =
|
||||
"interface Reversed { reversedOnly(): void; }\n"
|
||||
"interface Empty { emptyOnly(): void; }\n"
|
||||
"interface Pair { pairOnly(): void; }\n"
|
||||
"interface Factory {\n"
|
||||
" pick(s: string, n: number): Reversed;\n"
|
||||
" pick(): Empty;\n"
|
||||
" pick(n: number, s: string): Pair;\n"
|
||||
"}\n"
|
||||
"function run(f: Factory): void {\n"
|
||||
" f.pick().emptyOnly();\n"
|
||||
" f.pick(1, 'x').pairOnly();\n"
|
||||
"}\n";
|
||||
|
||||
static int assert_ts_interface_overload_return_chains(CBMLanguage language, bool jsx_mode,
|
||||
const char *language_name) {
|
||||
static const char *const type_names[] = {"Reversed", "Empty", "Pair", "Factory"};
|
||||
CBMLSPDef defs[5];
|
||||
memset(defs, 0, sizeof(defs));
|
||||
for (int i = 0; i < 4; i++) {
|
||||
defs[i].qualified_name = type_names[i];
|
||||
defs[i].short_name = type_names[i];
|
||||
defs[i].label = "Interface";
|
||||
defs[i].def_module_qn = "repro.ts";
|
||||
defs[i].is_interface = true;
|
||||
defs[i].lang = language;
|
||||
}
|
||||
defs[0].qualified_name = "repro.ts.Reversed";
|
||||
defs[1].qualified_name = "repro.ts.Empty";
|
||||
defs[2].qualified_name = "repro.ts.Pair";
|
||||
defs[3].qualified_name = "repro.ts.Factory";
|
||||
defs[4].qualified_name = "repro.ts.run";
|
||||
defs[4].short_name = "run";
|
||||
defs[4].label = "Function";
|
||||
defs[4].def_module_qn = "repro.ts";
|
||||
defs[4].return_types = "void";
|
||||
defs[4].lang = language;
|
||||
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
CBMTypeRegistry *registry = cbm_ts_build_cross_registry(&arena, defs, 5);
|
||||
CBMResolvedCallArray calls = {0};
|
||||
if (registry) {
|
||||
cbm_run_ts_lsp_cross_with_registry(
|
||||
&arena, kTsInterfaceOverloadReturnChains, (int)strlen(kTsInterfaceOverloadReturnChains),
|
||||
"repro.ts", false, jsx_mode, false, registry, defs, 5, NULL, NULL, 0, NULL, &calls);
|
||||
}
|
||||
|
||||
bool empty = has_resolved_suffix(&calls, "run", "Empty.emptyOnly", "lsp_ts_method");
|
||||
bool pair = has_resolved_suffix(&calls, "run", "Pair.pairOnly", "lsp_ts_method");
|
||||
if (!registry || !empty || !pair) {
|
||||
printf(" ordered-local: %s interface overload chains empty=%d pair=%d registry=%d\n",
|
||||
language_name, empty, pair, registry != NULL);
|
||||
print_resolved_calls(&calls);
|
||||
}
|
||||
cbm_arena_destroy(&arena);
|
||||
return registry && empty && pair ? 0 : 1;
|
||||
}
|
||||
|
||||
TEST(repro_lsp_ordered_local_typescript_interface_overload_return_chains) {
|
||||
return assert_ts_interface_overload_return_chains(CBM_LANG_TYPESCRIPT, false, "TypeScript");
|
||||
}
|
||||
|
||||
TEST(repro_lsp_ordered_local_tsx_interface_overload_return_chains) {
|
||||
return assert_ts_interface_overload_return_chains(CBM_LANG_TSX, true, "TSX");
|
||||
}
|
||||
|
||||
/* Java overloads normally share one graph QN, which hides which overload won.
|
||||
* The synthetic suffixes below make the registry choice observable while both
|
||||
* entries retain the source-level short name `pick`. With both signatures
|
||||
* collapsed to zero slots, the three-argument call falls back to the first
|
||||
* registration ($zero); materializing its counted carrier must select $three. */
|
||||
TEST(repro_lsp_ordered_local_java_overload_arity) {
|
||||
static const char source[] =
|
||||
"package demo;\n"
|
||||
"public class Caller {\n"
|
||||
" public void callZero(demo.Target t) { t.pick(); }\n"
|
||||
" public void callThree(demo.Target t) { t.pick(1, \"x\", true); }\n"
|
||||
"}\n";
|
||||
const char *three_params[] = {"int", "java.lang.String", "boolean", NULL};
|
||||
CBMLSPDef defs[3];
|
||||
memset(defs, 0, sizeof(defs));
|
||||
|
||||
defs[0].qualified_name = "demo.Target";
|
||||
defs[0].short_name = "Target";
|
||||
defs[0].label = "Class";
|
||||
defs[0].def_module_qn = "demo";
|
||||
defs[0].lang = CBM_LANG_JAVA;
|
||||
|
||||
defs[1].qualified_name = "demo.Target.pick$zero";
|
||||
defs[1].short_name = "pick";
|
||||
defs[1].label = "Method";
|
||||
defs[1].receiver_type = "demo.Target";
|
||||
defs[1].def_module_qn = "demo";
|
||||
defs[1].return_types = "void";
|
||||
defs[1].signature_param_types = NULL;
|
||||
defs[1].signature_param_count = 0;
|
||||
defs[1].lang = CBM_LANG_JAVA;
|
||||
|
||||
defs[2].qualified_name = "demo.Target.pick$three";
|
||||
defs[2].short_name = "pick";
|
||||
defs[2].label = "Method";
|
||||
defs[2].receiver_type = "demo.Target";
|
||||
defs[2].def_module_qn = "demo";
|
||||
defs[2].return_types = "void";
|
||||
defs[2].signature_param_types = three_params;
|
||||
defs[2].signature_param_count = 3;
|
||||
defs[2].lang = CBM_LANG_JAVA;
|
||||
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
CBMResolvedCallArray calls = {0};
|
||||
cbm_run_java_lsp_cross(&arena, source, (int)strlen(source), "demo", defs, 3, NULL, NULL, 0,
|
||||
NULL, &calls);
|
||||
|
||||
bool zero =
|
||||
has_resolved_exact(&calls, "callZero", "demo.Target.pick$zero", "lsp_type_dispatch");
|
||||
bool three =
|
||||
has_resolved_exact(&calls, "callThree", "demo.Target.pick$three", "lsp_type_dispatch");
|
||||
bool wrong =
|
||||
has_resolved_exact(&calls, "callThree", "demo.Target.pick$zero", "lsp_type_dispatch");
|
||||
if (!zero || !three || wrong) {
|
||||
printf(" ordered-local: Java overload selection zero=%d three=%d wrong-zero=%d\n", zero,
|
||||
three, wrong);
|
||||
print_resolved_calls(&calls);
|
||||
}
|
||||
cbm_arena_destroy(&arena);
|
||||
return zero && three && !wrong ? 0 : 1;
|
||||
}
|
||||
|
||||
SUITE(repro_lsp_ordered_local) {
|
||||
RUN_TEST(repro_lsp_ordered_local_go_grouped_generic);
|
||||
RUN_TEST(repro_lsp_ordered_local_cpp_reference_return_operator_arity);
|
||||
RUN_TEST(repro_lsp_ordered_local_cuda_reference_return_operator_arity);
|
||||
RUN_TEST(repro_lsp_ordered_local_c_deep_pointer_return_arity);
|
||||
RUN_TEST(repro_lsp_ordered_local_cpp_deep_pointer_return_arity);
|
||||
RUN_TEST(repro_lsp_ordered_local_cuda_deep_pointer_return_arity);
|
||||
RUN_TEST(repro_lsp_ordered_local_control_typescript_this_receiver);
|
||||
RUN_TEST(repro_lsp_ordered_local_control_tsx_this_receiver);
|
||||
RUN_TEST(repro_lsp_ordered_local_typescript_interface_overload_return_chains);
|
||||
RUN_TEST(repro_lsp_ordered_local_tsx_interface_overload_return_chains);
|
||||
RUN_TEST(repro_lsp_ordered_local_java_overload_arity);
|
||||
}
|
||||
@@ -0,0 +1,980 @@
|
||||
/*
|
||||
* repro_lsp_ordered_signatures.c — Counted positional signature transport into
|
||||
* the project-wide LSP registries.
|
||||
*
|
||||
* The public CBMDefinition.param_types array is a legacy type-usage projection:
|
||||
* it is not positional. Cross-LSP registration instead receives the internal
|
||||
* CBMLSPDef.signature_param_types + signature_param_count carrier. These tests
|
||||
* start at the production registry-population boundary so a GREEN result means
|
||||
* the registry consumer, not merely extraction, retained every argument slot.
|
||||
*
|
||||
* Contracts exercised per typed language:
|
||||
* - non-palindromic [integer, UNKNOWN, string] catches reordering;
|
||||
* - [integer, integer] catches accidental deduplication;
|
||||
* - [integer, string, UNKNOWN] catches NULL-termination at an unknown slot;
|
||||
* - only the exact text "?" means UNKNOWN; a language-valid nullable type is
|
||||
* still a real, non-UNKNOWN type;
|
||||
* - signature_param_count is authoritative. The non-palindromic carrier has
|
||||
* a fourth, out-of-count bool entry which must not be scanned or merged, and
|
||||
* a non-NULL carrier with count zero must materialize zero parameters.
|
||||
*
|
||||
* JavaScript has no source annotations in its primary carrier, so its contract
|
||||
* is three explicit UNKNOWN slots (rather than silently collapsing to zero).
|
||||
* Every listed frontend now consumes the counted carrier. These cases are
|
||||
* permanent gating guards against reordering, deduplication, or truncation.
|
||||
*/
|
||||
|
||||
#include "test_framework.h"
|
||||
|
||||
#include "arena.h"
|
||||
#include "cbm.h"
|
||||
#include "lang_specs.h"
|
||||
#include "lsp/c_lsp.h"
|
||||
#include "lsp/cs_lsp.h"
|
||||
#include "lsp/go_lsp.h"
|
||||
#include "lsp/java_lsp.h"
|
||||
#include "lsp/kotlin_lsp.h"
|
||||
#include "lsp/php_lsp.h"
|
||||
#include "lsp/py_lsp.h"
|
||||
#include "lsp/rust_lsp.h"
|
||||
#include "lsp/ts_lsp.h"
|
||||
#include "lsp/type_registry.h"
|
||||
#include "lsp/type_rep.h"
|
||||
#include "tree_sitter/api.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
typedef CBMTypeRegistry *(*CrossRegistryBuilder)(CBMArena *, CBMLSPDef *, int);
|
||||
|
||||
typedef struct {
|
||||
const char *tag;
|
||||
CBMLanguage lang;
|
||||
CrossRegistryBuilder build;
|
||||
const char *integer_text;
|
||||
const char *string_text;
|
||||
const char *nullable_text;
|
||||
const char *return_text;
|
||||
} OrderedSignatureLanguage;
|
||||
|
||||
static TSTree *parse_lsp_fixture(CBMLanguage language, const char *source) {
|
||||
const TSLanguage *grammar = cbm_ts_language(language);
|
||||
TSParser *parser = ts_parser_new();
|
||||
if (!grammar || !parser || !ts_parser_set_language(parser, grammar)) {
|
||||
if (parser)
|
||||
ts_parser_delete(parser);
|
||||
return NULL;
|
||||
}
|
||||
TSTree *tree = ts_parser_parse_string(parser, NULL, source, (uint32_t)strlen(source));
|
||||
ts_parser_delete(parser);
|
||||
return tree;
|
||||
}
|
||||
|
||||
static TSNode find_first_lsp_node_of_kind(TSNode node, const char *kind) {
|
||||
if (strcmp(ts_node_type(node), kind) == 0)
|
||||
return node;
|
||||
|
||||
uint32_t child_count = ts_node_child_count(node);
|
||||
for (uint32_t i = 0; i < child_count; i++) {
|
||||
TSNode found = find_first_lsp_node_of_kind(ts_node_child(node, i), kind);
|
||||
if (!ts_node_is_null(found))
|
||||
return found;
|
||||
}
|
||||
|
||||
TSNode missing = {0};
|
||||
return missing;
|
||||
}
|
||||
|
||||
static CBMTypeRegistry *alloc_test_registry(CBMArena *arena) {
|
||||
CBMTypeRegistry *registry = (CBMTypeRegistry *)cbm_arena_alloc(arena, sizeof(*registry));
|
||||
if (!registry)
|
||||
return NULL;
|
||||
cbm_registry_init(registry, arena);
|
||||
return registry;
|
||||
}
|
||||
|
||||
static CBMTypeRegistry *build_java_cross_registry_for_contract(CBMArena *arena, CBMLSPDef *defs,
|
||||
int def_count) {
|
||||
CBMTypeRegistry *registry = alloc_test_registry(arena);
|
||||
if (registry)
|
||||
cbm_java_register_lsp_defs(arena, registry, defs, def_count);
|
||||
return registry;
|
||||
}
|
||||
|
||||
static CBMTypeRegistry *build_kotlin_cross_registry_for_contract(CBMArena *arena, CBMLSPDef *defs,
|
||||
int def_count) {
|
||||
CBMTypeRegistry *registry = alloc_test_registry(arena);
|
||||
if (registry)
|
||||
cbm_kotlin_register_lsp_defs(arena, registry, defs, def_count);
|
||||
return registry;
|
||||
}
|
||||
|
||||
static CBMTypeRegistry *build_php_cross_registry_for_contract(CBMArena *arena, CBMLSPDef *defs,
|
||||
int def_count) {
|
||||
CBMTypeRegistry *registry = alloc_test_registry(arena);
|
||||
if (registry)
|
||||
cbm_php_register_lsp_defs(arena, NULL, registry, defs, def_count);
|
||||
return registry;
|
||||
}
|
||||
|
||||
static int func_param_count(const CBMType *signature) {
|
||||
if (!signature || signature->kind != CBM_TYPE_FUNC || !signature->data.func.param_types)
|
||||
return 0;
|
||||
int count = 0;
|
||||
while (signature->data.func.param_types[count])
|
||||
count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
static const char *last_type_component(const char *name) {
|
||||
if (!name)
|
||||
return NULL;
|
||||
const char *leaf = name;
|
||||
for (const char *cursor = name; *cursor; cursor++) {
|
||||
if (*cursor == '.' || *cursor == ':')
|
||||
leaf = cursor + 1;
|
||||
}
|
||||
return leaf;
|
||||
}
|
||||
|
||||
static const char *type_atom_name(const CBMType *type) {
|
||||
if (!type)
|
||||
return NULL;
|
||||
switch (type->kind) {
|
||||
case CBM_TYPE_BUILTIN:
|
||||
return type->data.builtin.name;
|
||||
case CBM_TYPE_NAMED:
|
||||
return type->data.named.qualified_name;
|
||||
case CBM_TYPE_TYPE_PARAM:
|
||||
return type->data.type_param.name;
|
||||
case CBM_TYPE_TEMPLATE:
|
||||
return type->data.template_type.template_name;
|
||||
case CBM_TYPE_ALIAS:
|
||||
return type->data.alias.alias_qn;
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static int param_matches(const CBMType *actual, const char *expected) {
|
||||
if (strcmp(expected, "?") == 0)
|
||||
return cbm_type_is_unknown(actual);
|
||||
if (!actual || cbm_type_is_unknown(actual))
|
||||
return 0;
|
||||
const char *name = type_atom_name(actual);
|
||||
const char *leaf = last_type_component(name);
|
||||
return leaf && strcmp(leaf, expected) == 0;
|
||||
}
|
||||
|
||||
static int check_signature(const CBMTypeRegistry *registry, const char *qualified_name,
|
||||
const char *case_name, const char *const *expected, int expected_count) {
|
||||
const CBMRegisteredFunc *func = cbm_registry_lookup_func(registry, qualified_name);
|
||||
if (!func) {
|
||||
fprintf(stderr, " [ordered-signature] case=%s missing function=%s\n", case_name,
|
||||
qualified_name);
|
||||
return 1;
|
||||
}
|
||||
if (!func->signature || func->signature->kind != CBM_TYPE_FUNC) {
|
||||
fprintf(stderr, " [ordered-signature] case=%s function=%s has no FUNC signature\n",
|
||||
case_name, qualified_name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int actual_count = func_param_count(func->signature);
|
||||
if (actual_count != expected_count) {
|
||||
fprintf(stderr,
|
||||
" [ordered-signature] case=%s function=%s expected-count=%d actual-count=%d\n",
|
||||
case_name, qualified_name, expected_count, actual_count);
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < expected_count; i++) {
|
||||
const CBMType *actual = func->signature->data.func.param_types[i];
|
||||
if (!param_matches(actual, expected[i])) {
|
||||
const char *actual_name = type_atom_name(actual);
|
||||
fprintf(stderr,
|
||||
" [ordered-signature] case=%s function=%s slot=%d expected=%s "
|
||||
"actual=%s kind=%d\n",
|
||||
case_name, qualified_name, i, expected[i],
|
||||
actual_name ? actual_name
|
||||
: (cbm_type_is_unknown(actual) ? "?" : "<structured>"),
|
||||
actual ? (int)actual->kind : -1);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int check_single_return(const CBMTypeRegistry *registry, const char *qualified_name,
|
||||
const char *case_name, const char *expected) {
|
||||
const CBMRegisteredFunc *func = cbm_registry_lookup_func(registry, qualified_name);
|
||||
if (!func || !func->signature || func->signature->kind != CBM_TYPE_FUNC ||
|
||||
!func->signature->data.func.return_types || !func->signature->data.func.return_types[0]) {
|
||||
fprintf(stderr, " [ordered-signature] case=%s function=%s missing return type\n",
|
||||
case_name, qualified_name);
|
||||
return 1;
|
||||
}
|
||||
const CBMType *actual = func->signature->data.func.return_types[0];
|
||||
if (!param_matches(actual, expected)) {
|
||||
const char *actual_name = type_atom_name(actual);
|
||||
fprintf(stderr,
|
||||
" [ordered-signature] case=%s function=%s expected-return=%s actual=%s kind=%d\n",
|
||||
case_name, qualified_name, expected,
|
||||
actual_name ? actual_name : (cbm_type_is_unknown(actual) ? "?" : "<structured>"),
|
||||
(int)actual->kind);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int check_nullable_signature(const CBMTypeRegistry *registry, const char *qualified_name,
|
||||
const char *case_name) {
|
||||
const CBMRegisteredFunc *func = cbm_registry_lookup_func(registry, qualified_name);
|
||||
if (!func || !func->signature || func->signature->kind != CBM_TYPE_FUNC) {
|
||||
fprintf(stderr, " [ordered-signature] case=%s missing FUNC signature=%s\n", case_name,
|
||||
qualified_name);
|
||||
return 1;
|
||||
}
|
||||
int actual_count = func_param_count(func->signature);
|
||||
if (actual_count != 1) {
|
||||
fprintf(stderr,
|
||||
" [ordered-signature] case=%s function=%s expected-count=1 actual-count=%d\n",
|
||||
case_name, qualified_name, actual_count);
|
||||
return 1;
|
||||
}
|
||||
if (cbm_type_is_unknown(func->signature->data.func.param_types[0])) {
|
||||
fprintf(stderr,
|
||||
" [ordered-signature] case=%s function=%s nullable type collapsed to UNKNOWN\n",
|
||||
case_name, qualified_name);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void init_function_def(CBMLSPDef *def, CBMArena *arena,
|
||||
const OrderedSignatureLanguage *language, const char *case_name,
|
||||
const char **param_types, int param_count) {
|
||||
memset(def, 0, sizeof(*def));
|
||||
def->qualified_name = cbm_arena_sprintf(arena, "cbm.ordered.%s.%s", language->tag, case_name);
|
||||
def->short_name = case_name;
|
||||
def->label = "Function";
|
||||
def->def_module_qn = cbm_arena_sprintf(arena, "cbm.ordered.%s", language->tag);
|
||||
def->return_types = language->return_text;
|
||||
def->signature_param_types = param_types;
|
||||
def->signature_param_count = param_count;
|
||||
def->lang = language->lang;
|
||||
}
|
||||
|
||||
static int run_typed_language_contract(const OrderedSignatureLanguage *language) {
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
|
||||
/* The out-of-count bool is deliberate: count=3, so it must never surface. */
|
||||
const char *nonpal_carrier[] = {language->integer_text, "?", language->string_text, "bool",
|
||||
NULL};
|
||||
const char *duplicate_carrier[] = {language->integer_text, language->integer_text, NULL};
|
||||
const char *trailing_carrier[] = {language->integer_text, language->string_text, "?", NULL};
|
||||
const char *nullable_carrier[] = {language->nullable_text, NULL};
|
||||
/* A stale pointer with authoritative count zero must not be scanned. */
|
||||
const char *count_zero_carrier[] = {language->integer_text, language->string_text, NULL};
|
||||
|
||||
CBMLSPDef defs[5];
|
||||
init_function_def(&defs[0], &arena, language, "nonpal", nonpal_carrier, 3);
|
||||
init_function_def(&defs[1], &arena, language, "duplicate", duplicate_carrier, 2);
|
||||
init_function_def(&defs[2], &arena, language, "trailing_unknown", trailing_carrier, 3);
|
||||
init_function_def(&defs[3], &arena, language, "nullable", nullable_carrier, 1);
|
||||
init_function_def(&defs[4], &arena, language, "count_zero", count_zero_carrier, 0);
|
||||
|
||||
CBMTypeRegistry *registry = language->build(&arena, defs, 5);
|
||||
if (!registry) {
|
||||
fprintf(stderr, " [ordered-signature] lang=%s registry build failed\n", language->tag);
|
||||
cbm_arena_destroy(&arena);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* C# deliberately canonicalizes source aliases in its semantic registry.
|
||||
* Keep feeding valid source spellings (`int`, `string`) while checking the
|
||||
* canonical leaf names, rather than weakening production parsing to satisfy
|
||||
* a spelling-only assertion. */
|
||||
const bool csharp_canonical = strcmp(language->tag, "csharp") == 0;
|
||||
const char *integer_expected = csharp_canonical ? "Int32" : language->integer_text;
|
||||
const char *string_expected = csharp_canonical ? "String" : language->string_text;
|
||||
const char *nonpal_expected[] = {integer_expected, "?", string_expected};
|
||||
const char *duplicate_expected[] = {integer_expected, integer_expected};
|
||||
const char *trailing_expected[] = {integer_expected, string_expected, "?"};
|
||||
int failures = 0;
|
||||
failures += check_signature(registry, defs[0].qualified_name, "nonpal", nonpal_expected, 3);
|
||||
failures +=
|
||||
check_signature(registry, defs[1].qualified_name, "duplicate", duplicate_expected, 2);
|
||||
failures +=
|
||||
check_signature(registry, defs[2].qualified_name, "trailing_unknown", trailing_expected, 3);
|
||||
failures += check_nullable_signature(registry, defs[3].qualified_name, "nullable");
|
||||
failures += check_signature(registry, defs[4].qualified_name, "count_zero", NULL, 0);
|
||||
|
||||
cbm_arena_destroy(&arena);
|
||||
return failures;
|
||||
}
|
||||
|
||||
static int run_javascript_unknown_contract(const OrderedSignatureLanguage *language) {
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
|
||||
const char *unknown_carrier[] = {"?", "?", "?", "number", NULL};
|
||||
const char *count_zero_carrier[] = {"number", NULL};
|
||||
CBMLSPDef defs[2];
|
||||
init_function_def(&defs[0], &arena, language, "three_unknowns", unknown_carrier, 3);
|
||||
init_function_def(&defs[1], &arena, language, "count_zero", count_zero_carrier, 0);
|
||||
|
||||
CBMTypeRegistry *registry = language->build(&arena, defs, 2);
|
||||
if (!registry) {
|
||||
fprintf(stderr, " [ordered-signature] lang=%s registry build failed\n", language->tag);
|
||||
cbm_arena_destroy(&arena);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char *expected[] = {"?", "?", "?"};
|
||||
int failures = check_signature(registry, defs[0].qualified_name, "three_unknowns", expected, 3);
|
||||
failures += check_signature(registry, defs[1].qualified_name, "count_zero", NULL, 0);
|
||||
cbm_arena_destroy(&arena);
|
||||
return failures;
|
||||
}
|
||||
|
||||
static const OrderedSignatureLanguage C_LANGUAGE = {
|
||||
"c", CBM_LANG_C, cbm_c_build_cross_registry, "int", "string", "Thing *", "void"};
|
||||
static const OrderedSignatureLanguage CPP_LANGUAGE = {
|
||||
"cpp", CBM_LANG_CPP, cbm_c_build_cross_registry, "int", "string", "std::optional<Thing>",
|
||||
"void"};
|
||||
static const OrderedSignatureLanguage CUDA_LANGUAGE = {
|
||||
"cuda", CBM_LANG_CUDA, cbm_c_build_cross_registry, "int", "string", "Thing *", "void"};
|
||||
static const OrderedSignatureLanguage GO_LANGUAGE = {
|
||||
"go", CBM_LANG_GO, cbm_go_build_cross_registry, "int", "string", "*Thing", "void"};
|
||||
static const OrderedSignatureLanguage JS_LANGUAGE = {
|
||||
"javascript", CBM_LANG_JAVASCRIPT, cbm_ts_build_cross_registry, NULL, NULL, NULL, "void"};
|
||||
static const OrderedSignatureLanguage TS_LANGUAGE = {"typescript",
|
||||
CBM_LANG_TYPESCRIPT,
|
||||
cbm_ts_build_cross_registry,
|
||||
"number",
|
||||
"string",
|
||||
"Thing | null",
|
||||
"void"};
|
||||
static const OrderedSignatureLanguage TSX_LANGUAGE = {
|
||||
"tsx", CBM_LANG_TSX, cbm_ts_build_cross_registry, "number", "string", "Thing | null", "void"};
|
||||
static const OrderedSignatureLanguage PYTHON_LANGUAGE = {
|
||||
"python", CBM_LANG_PYTHON, cbm_py_build_cross_registry, "int", "str", "Thing | None", "None"};
|
||||
static const OrderedSignatureLanguage CSHARP_LANGUAGE = {
|
||||
"csharp", CBM_LANG_CSHARP, cbm_cs_build_cross_registry, "int", "string", "Thing?", "void"};
|
||||
static const OrderedSignatureLanguage RUST_LANGUAGE = {
|
||||
"rust", CBM_LANG_RUST, cbm_rust_build_cross_registry, "i32", "String", "Option<Thing>", "()"};
|
||||
static const OrderedSignatureLanguage JAVA_LANGUAGE = {
|
||||
"java", CBM_LANG_JAVA, build_java_cross_registry_for_contract,
|
||||
"int", "String", "java.util.List<?>",
|
||||
"void"};
|
||||
static const OrderedSignatureLanguage KOTLIN_LANGUAGE = {
|
||||
"kotlin", CBM_LANG_KOTLIN, build_kotlin_cross_registry_for_contract, "Int", "String",
|
||||
"Thing?", "Unit"};
|
||||
static const OrderedSignatureLanguage PHP_LANGUAGE = {
|
||||
"php", CBM_LANG_PHP, build_php_cross_registry_for_contract, "int", "string", "?Thing", "void"};
|
||||
|
||||
static void add_fixture_type(CBMTypeRegistry *registry, const char *qualified_name,
|
||||
const char *short_name) {
|
||||
CBMRegisteredType type = {0};
|
||||
type.qualified_name = qualified_name;
|
||||
type.short_name = short_name;
|
||||
cbm_registry_add_type(registry, type);
|
||||
}
|
||||
|
||||
static void add_php_seeded_method(CBMArena *arena, CBMTypeRegistry *registry,
|
||||
const char *qualified_name, const char *short_name,
|
||||
const char *receiver_type, const char *thing_qn,
|
||||
const CBMType *return_type) {
|
||||
const char *param_names[] = {"first", "middle", "last", NULL};
|
||||
const CBMType *param_types[] = {cbm_type_builtin(arena, "int"), cbm_type_named(arena, thing_qn),
|
||||
cbm_type_builtin(arena, "int"), NULL};
|
||||
const CBMType *return_types[] = {return_type, NULL};
|
||||
CBMRegisteredFunc function = {0};
|
||||
function.qualified_name = qualified_name;
|
||||
function.short_name = short_name;
|
||||
function.receiver_type = receiver_type;
|
||||
function.min_params = 3;
|
||||
function.signature = cbm_type_func(arena, param_names, param_types, return_types);
|
||||
cbm_registry_add_func(registry, function);
|
||||
}
|
||||
|
||||
static void add_csharp_seeded_method(CBMArena *arena, CBMTypeRegistry *registry,
|
||||
const char *qualified_name, const char *short_name,
|
||||
const char *receiver_type, const char *thing_qn) {
|
||||
const CBMType *param_types[] = {cbm_type_builtin(arena, "int"), cbm_type_named(arena, thing_qn),
|
||||
cbm_type_builtin(arena, "int"), NULL};
|
||||
const CBMType *return_types[] = {cbm_type_unknown(), NULL};
|
||||
CBMRegisteredFunc function = {0};
|
||||
function.qualified_name = qualified_name;
|
||||
function.short_name = short_name;
|
||||
function.receiver_type = receiver_type;
|
||||
function.min_params = 3;
|
||||
function.signature = cbm_type_func(arena, NULL, param_types, return_types);
|
||||
cbm_registry_add_func(registry, function);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
int call_count;
|
||||
const char *seen[8];
|
||||
} SignatureParserProbe;
|
||||
|
||||
static const CBMType *parse_signature_probe_type(CBMArena *arena, const char *text,
|
||||
void *parser_ctx) {
|
||||
SignatureParserProbe *probe = (SignatureParserProbe *)parser_ctx;
|
||||
probe->seen[probe->call_count++] = text;
|
||||
if (strcmp(text, "parser-null") == 0)
|
||||
return NULL;
|
||||
return cbm_type_builtin(arena, text);
|
||||
}
|
||||
|
||||
TEST(repro_lsp_signature_param_materializer_contract) {
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
|
||||
SignatureParserProbe probe = {0};
|
||||
const char *ordered_texts[] = {"int", "?", "string", "outside-count"};
|
||||
const CBMType **ordered = cbm_type_materialize_signature_params(
|
||||
&arena, ordered_texts, 3, parse_signature_probe_type, &probe);
|
||||
ASSERT_NOT_NULL(ordered);
|
||||
ASSERT_EQ(probe.call_count, 2);
|
||||
ASSERT_STR_EQ(probe.seen[0], "int");
|
||||
ASSERT_STR_EQ(probe.seen[1], "string");
|
||||
ASSERT_EQ(ordered[0]->kind, CBM_TYPE_BUILTIN);
|
||||
ASSERT_STR_EQ(ordered[0]->data.builtin.name, "int");
|
||||
ASSERT_TRUE(cbm_type_is_unknown(ordered[1]));
|
||||
ASSERT_EQ(ordered[2]->kind, CBM_TYPE_BUILTIN);
|
||||
ASSERT_STR_EQ(ordered[2]->data.builtin.name, "string");
|
||||
ASSERT_NULL(ordered[3]);
|
||||
|
||||
memset(&probe, 0, sizeof(probe));
|
||||
const char *duplicate_texts[] = {"int", "int"};
|
||||
const CBMType **duplicates = cbm_type_materialize_signature_params(
|
||||
&arena, duplicate_texts, 2, parse_signature_probe_type, &probe);
|
||||
ASSERT_NOT_NULL(duplicates);
|
||||
ASSERT_EQ(probe.call_count, 2);
|
||||
ASSERT_STR_EQ(duplicates[0]->data.builtin.name, "int");
|
||||
ASSERT_STR_EQ(duplicates[1]->data.builtin.name, "int");
|
||||
ASSERT_NULL(duplicates[2]);
|
||||
|
||||
memset(&probe, 0, sizeof(probe));
|
||||
const char *trailing_texts[] = {"int", "string", "?"};
|
||||
const CBMType **trailing = cbm_type_materialize_signature_params(
|
||||
&arena, trailing_texts, 3, parse_signature_probe_type, &probe);
|
||||
ASSERT_NOT_NULL(trailing);
|
||||
ASSERT_EQ(probe.call_count, 2);
|
||||
ASSERT_STR_EQ(trailing[0]->data.builtin.name, "int");
|
||||
ASSERT_STR_EQ(trailing[1]->data.builtin.name, "string");
|
||||
ASSERT_TRUE(cbm_type_is_unknown(trailing[2]));
|
||||
ASSERT_NULL(trailing[3]);
|
||||
|
||||
memset(&probe, 0, sizeof(probe));
|
||||
const char *sentinel_texts[] = {"?", "?Foo", "Foo?", "parser-null", "", NULL};
|
||||
const CBMType **sentinels = cbm_type_materialize_signature_params(
|
||||
&arena, sentinel_texts, 6, parse_signature_probe_type, &probe);
|
||||
ASSERT_NOT_NULL(sentinels);
|
||||
ASSERT_EQ(probe.call_count, 3);
|
||||
ASSERT_STR_EQ(probe.seen[0], "?Foo");
|
||||
ASSERT_STR_EQ(probe.seen[1], "Foo?");
|
||||
ASSERT_STR_EQ(probe.seen[2], "parser-null");
|
||||
ASSERT_TRUE(cbm_type_is_unknown(sentinels[0]));
|
||||
ASSERT_EQ(sentinels[1]->kind, CBM_TYPE_BUILTIN);
|
||||
ASSERT_STR_EQ(sentinels[1]->data.builtin.name, "?Foo");
|
||||
ASSERT_EQ(sentinels[2]->kind, CBM_TYPE_BUILTIN);
|
||||
ASSERT_STR_EQ(sentinels[2]->data.builtin.name, "Foo?");
|
||||
ASSERT_TRUE(cbm_type_is_unknown(sentinels[3]));
|
||||
ASSERT_TRUE(cbm_type_is_unknown(sentinels[4]));
|
||||
ASSERT_TRUE(cbm_type_is_unknown(sentinels[5]));
|
||||
ASSERT_NULL(sentinels[6]);
|
||||
|
||||
memset(&probe, 0, sizeof(probe));
|
||||
const CBMType **missing_texts =
|
||||
cbm_type_materialize_signature_params(&arena, NULL, 3, parse_signature_probe_type, &probe);
|
||||
ASSERT_NOT_NULL(missing_texts);
|
||||
ASSERT_EQ(probe.call_count, 0);
|
||||
ASSERT_TRUE(cbm_type_is_unknown(missing_texts[0]));
|
||||
ASSERT_TRUE(cbm_type_is_unknown(missing_texts[1]));
|
||||
ASSERT_TRUE(cbm_type_is_unknown(missing_texts[2]));
|
||||
ASSERT_NULL(missing_texts[3]);
|
||||
ASSERT_NULL(cbm_type_materialize_signature_params(&arena, ordered_texts, 0,
|
||||
parse_signature_probe_type, &probe));
|
||||
|
||||
cbm_arena_destroy(&arena);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_lsp_func_replace_returns_contract) {
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
|
||||
const CBMType *integer = cbm_type_builtin(&arena, "int");
|
||||
const CBMType *string = cbm_type_builtin(&arena, "string");
|
||||
const CBMType *old_return = cbm_type_builtin(&arena, "old-return");
|
||||
const CBMType *new_return = cbm_type_builtin(&arena, "new-return");
|
||||
const char *param_names[] = {"first", "middle", "last", NULL};
|
||||
const CBMType *param_types[] = {integer, cbm_type_unknown(), string, NULL};
|
||||
const CBMType *old_returns[] = {old_return, NULL};
|
||||
const CBMType *old_signature = cbm_type_func(&arena, param_names, param_types, old_returns);
|
||||
ASSERT_NOT_NULL(old_signature);
|
||||
ASSERT_EQ(old_signature->kind, CBM_TYPE_FUNC);
|
||||
|
||||
const CBMType *new_returns[] = {new_return, NULL};
|
||||
const CBMType *replaced = cbm_type_func_replace_returns(&arena, old_signature, new_returns);
|
||||
ASSERT_NOT_NULL(replaced);
|
||||
ASSERT_EQ(replaced->kind, CBM_TYPE_FUNC);
|
||||
ASSERT(replaced != old_signature);
|
||||
ASSERT_STR_EQ(replaced->data.func.param_names[0], "first");
|
||||
ASSERT_STR_EQ(replaced->data.func.param_names[1], "middle");
|
||||
ASSERT_STR_EQ(replaced->data.func.param_names[2], "last");
|
||||
ASSERT_NULL(replaced->data.func.param_names[3]);
|
||||
ASSERT(replaced->data.func.param_types[0] == integer);
|
||||
ASSERT_TRUE(cbm_type_is_unknown(replaced->data.func.param_types[1]));
|
||||
ASSERT(replaced->data.func.param_types[2] == string);
|
||||
ASSERT_NULL(replaced->data.func.param_types[3]);
|
||||
ASSERT(replaced->data.func.return_types[0] == new_return);
|
||||
ASSERT_NULL(replaced->data.func.return_types[1]);
|
||||
|
||||
/* The source signature remains untouched. */
|
||||
ASSERT(old_signature->data.func.return_types[0] == old_return);
|
||||
ASSERT_NULL(old_signature->data.func.return_types[1]);
|
||||
ASSERT_STR_EQ(old_signature->data.func.param_names[1], "middle");
|
||||
ASSERT_TRUE(cbm_type_is_unknown(old_signature->data.func.param_types[1]));
|
||||
|
||||
cbm_arena_destroy(&arena);
|
||||
PASS();
|
||||
}
|
||||
|
||||
static int run_c_explicit_template_value_signature_contract(const char *source,
|
||||
const char *registered_qn,
|
||||
const char *function_node_kind) {
|
||||
TSTree *tree = parse_lsp_fixture(CBM_LANG_CPP, source);
|
||||
ASSERT_NOT_NULL(tree);
|
||||
|
||||
TSNode call = find_first_lsp_node_of_kind(ts_tree_root_node(tree), "call_expression");
|
||||
ASSERT_FALSE(ts_node_is_null(call));
|
||||
TSNode function = ts_node_child_by_field_name(call, "function", 8);
|
||||
ASSERT_FALSE(ts_node_is_null(function));
|
||||
ASSERT_STR_EQ(ts_node_type(function), function_node_kind);
|
||||
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
CBMTypeRegistry registry;
|
||||
cbm_registry_init(®istry, &arena);
|
||||
|
||||
const char *param_names[] = {"value", "enabled", NULL};
|
||||
const CBMType *param_types[] = {cbm_type_builtin(&arena, "long"),
|
||||
cbm_type_builtin(&arena, "bool"), NULL};
|
||||
const CBMType *return_types[] = {cbm_type_type_param(&arena, "T"), NULL};
|
||||
const char *type_params[] = {"T", NULL};
|
||||
CBMRegisteredFunc registered = {0};
|
||||
registered.qualified_name = registered_qn;
|
||||
registered.short_name = "materialize";
|
||||
registered.signature = cbm_type_func(&arena, param_names, param_types, return_types);
|
||||
registered.type_param_names = type_params;
|
||||
registered.min_params = 2;
|
||||
cbm_registry_add_func(®istry, registered);
|
||||
|
||||
CBMResolvedCallArray out = {0};
|
||||
CLSPContext ctx;
|
||||
c_lsp_init(&ctx, &arena, source, (int)strlen(source), ®istry, NULL,
|
||||
/*cpp_mode=*/true, &out);
|
||||
const CBMType *value_type = c_eval_expr_type(&ctx, function);
|
||||
|
||||
ASSERT_NOT_NULL(value_type);
|
||||
ASSERT_EQ(value_type->kind, CBM_TYPE_FUNC);
|
||||
ASSERT_NOT_NULL(value_type->data.func.return_types);
|
||||
ASSERT_NOT_NULL(value_type->data.func.return_types[0]);
|
||||
ASSERT_EQ(value_type->data.func.return_types[0]->kind, CBM_TYPE_BUILTIN);
|
||||
ASSERT_STR_EQ(value_type->data.func.return_types[0]->data.builtin.name, "int");
|
||||
|
||||
ASSERT_NOT_NULL(value_type->data.func.param_names);
|
||||
ASSERT_STR_EQ(value_type->data.func.param_names[0], "value");
|
||||
ASSERT_STR_EQ(value_type->data.func.param_names[1], "enabled");
|
||||
ASSERT_NULL(value_type->data.func.param_names[2]);
|
||||
ASSERT_NOT_NULL(value_type->data.func.param_types);
|
||||
ASSERT_EQ(value_type->data.func.param_types[0]->kind, CBM_TYPE_BUILTIN);
|
||||
ASSERT_STR_EQ(value_type->data.func.param_types[0]->data.builtin.name, "long");
|
||||
ASSERT_EQ(value_type->data.func.param_types[1]->kind, CBM_TYPE_BUILTIN);
|
||||
ASSERT_STR_EQ(value_type->data.func.param_types[1]->data.builtin.name, "bool");
|
||||
ASSERT_NULL(value_type->data.func.param_types[2]);
|
||||
|
||||
ts_tree_delete(tree);
|
||||
cbm_arena_destroy(&arena);
|
||||
return 0;
|
||||
}
|
||||
|
||||
TEST(repro_lsp_c_unqualified_explicit_template_value_preserves_signature) {
|
||||
return run_c_explicit_template_value_signature_contract(
|
||||
"void caller(){ materialize<int>(1L, true); }", "materialize", "template_function");
|
||||
}
|
||||
|
||||
TEST(repro_lsp_c_qualified_explicit_template_value_preserves_signature) {
|
||||
return run_c_explicit_template_value_signature_contract(
|
||||
"void caller(){ ns::materialize<int>(1L, true); }", "ns.materialize",
|
||||
"qualified_identifier");
|
||||
}
|
||||
|
||||
TEST(repro_lsp_ordered_signatures_c_control) {
|
||||
ASSERT_EQ(run_typed_language_contract(&C_LANGUAGE), 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_lsp_ordered_signatures_cpp_control) {
|
||||
ASSERT_EQ(run_typed_language_contract(&CPP_LANGUAGE), 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_lsp_ordered_signatures_cuda_control) {
|
||||
ASSERT_EQ(run_typed_language_contract(&CUDA_LANGUAGE), 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_lsp_ordered_signatures_go) {
|
||||
ASSERT_EQ(run_typed_language_contract(&GO_LANGUAGE), 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_lsp_ordered_signatures_javascript) {
|
||||
ASSERT_EQ(run_javascript_unknown_contract(&JS_LANGUAGE), 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_lsp_ordered_signatures_typescript) {
|
||||
ASSERT_EQ(run_typed_language_contract(&TS_LANGUAGE), 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_lsp_ordered_signatures_tsx) {
|
||||
ASSERT_EQ(run_typed_language_contract(&TSX_LANGUAGE), 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_lsp_ordered_signatures_python) {
|
||||
ASSERT_EQ(run_typed_language_contract(&PYTHON_LANGUAGE), 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_lsp_ordered_signatures_csharp) {
|
||||
ASSERT_EQ(run_typed_language_contract(&CSHARP_LANGUAGE), 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_lsp_ordered_signatures_csharp_cross_return) {
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
|
||||
CBMLSPDef def;
|
||||
init_function_def(&def, &arena, &CSHARP_LANGUAGE, "cross_return", NULL, 0);
|
||||
def.return_types = "System.String";
|
||||
CBMTypeRegistry *registry = cbm_cs_build_cross_registry(&arena, &def, 1);
|
||||
int failures = registry ? check_single_return(registry, def.qualified_name,
|
||||
"csharp-cross-return", "String")
|
||||
: 1;
|
||||
|
||||
cbm_arena_destroy(&arena);
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_lsp_ordered_signatures_rust) {
|
||||
ASSERT_EQ(run_typed_language_contract(&RUST_LANGUAGE), 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_lsp_ordered_signatures_java) {
|
||||
ASSERT_EQ(run_typed_language_contract(&JAVA_LANGUAGE), 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_lsp_ordered_signatures_kotlin) {
|
||||
ASSERT_EQ(run_typed_language_contract(&KOTLIN_LANGUAGE), 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_lsp_ordered_signatures_php) {
|
||||
ASSERT_EQ(run_typed_language_contract(&PHP_LANGUAGE), 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_lsp_ordered_signatures_csharp_ast_return_refinement) {
|
||||
const char *module_qn = "cbm.ordered.csharp.local";
|
||||
const char *subject_qn = "cbm.ordered.csharp.local.Subject";
|
||||
const char *thing_qn = "cbm.ordered.csharp.local.Thing";
|
||||
const char *result_qn = "cbm.ordered.csharp.local.Result";
|
||||
const char *method_qn = "cbm.ordered.csharp.local.Subject.method";
|
||||
const char *source =
|
||||
"class Thing {}\n"
|
||||
"class Result {}\n"
|
||||
"class Subject {\n"
|
||||
" Result method(int first, Thing middle, int last) { return new Result(); }\n"
|
||||
"}\n";
|
||||
TSTree *tree = parse_lsp_fixture(CBM_LANG_CSHARP, source);
|
||||
ASSERT_NOT_NULL(tree);
|
||||
TSNode root = ts_tree_root_node(tree);
|
||||
ASSERT_FALSE(ts_node_has_error(root));
|
||||
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
CBMTypeRegistry registry;
|
||||
cbm_registry_init(®istry, &arena);
|
||||
add_fixture_type(®istry, thing_qn, "Thing");
|
||||
add_fixture_type(®istry, result_qn, "Result");
|
||||
add_fixture_type(®istry, subject_qn, "Subject");
|
||||
add_csharp_seeded_method(&arena, ®istry, method_qn, "method", subject_qn, thing_qn);
|
||||
|
||||
CBMResolvedCallArray out = {0};
|
||||
CSLSPContext ctx;
|
||||
cs_lsp_init(&ctx, &arena, source, (int)strlen(source), ®istry, module_qn, &out);
|
||||
cbm_cs_refine_ast_return_types(&ctx, ®istry, root);
|
||||
|
||||
const char *expected[] = {"int", "Thing", "int"};
|
||||
int failures =
|
||||
check_signature(®istry, method_qn, "csharp-ast-return-refinement", expected, 3);
|
||||
failures += check_single_return(®istry, method_qn, "csharp-ast-return-refinement", "Result");
|
||||
|
||||
ts_tree_delete(tree);
|
||||
cbm_arena_destroy(&arena);
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_lsp_ordered_signatures_rust_ast_return_refinement) {
|
||||
const char *module_qn = "cbm.ordered.rust.local";
|
||||
const char *thing_qn = "cbm.ordered.rust.local.Thing";
|
||||
const char *result_qn = "cbm.ordered.rust.local.Result";
|
||||
const char *subject_qn = "cbm.ordered.rust.local.Subject";
|
||||
const char *top_qn = "cbm.ordered.rust.local.top";
|
||||
const char *method_qn = "cbm.ordered.rust.local.Subject.method";
|
||||
const char *source =
|
||||
"struct Thing;\n"
|
||||
"struct Result;\n"
|
||||
"struct Subject;\n"
|
||||
"fn top(first: i32, middle: Thing, last: i32) -> Result { Result }\n"
|
||||
"impl Subject {\n"
|
||||
" fn method(&self, first: i32, middle: Thing, last: i32) -> Result { Result }\n"
|
||||
"}\n";
|
||||
TSTree *tree = parse_lsp_fixture(CBM_LANG_RUST, source);
|
||||
ASSERT_NOT_NULL(tree);
|
||||
TSNode root = ts_tree_root_node(tree);
|
||||
ASSERT_FALSE(ts_node_has_error(root));
|
||||
|
||||
const char *params[] = {"i32", "Thing", "i32", NULL};
|
||||
CBMDefinition defs[5] = {0};
|
||||
defs[0].name = "Thing";
|
||||
defs[0].qualified_name = thing_qn;
|
||||
defs[0].label = "Struct";
|
||||
defs[1].name = "Result";
|
||||
defs[1].qualified_name = result_qn;
|
||||
defs[1].label = "Struct";
|
||||
defs[2].name = "Subject";
|
||||
defs[2].qualified_name = subject_qn;
|
||||
defs[2].label = "Struct";
|
||||
defs[3].name = "top";
|
||||
defs[3].qualified_name = top_qn;
|
||||
defs[3].label = "Function";
|
||||
defs[3].param_types = params;
|
||||
defs[3].signature_param_types = params;
|
||||
defs[3].signature_param_count = 3;
|
||||
defs[4].name = "method";
|
||||
defs[4].qualified_name = method_qn;
|
||||
defs[4].label = "Method";
|
||||
defs[4].parent_class = subject_qn;
|
||||
defs[4].param_types = params;
|
||||
defs[4].signature_param_types = params;
|
||||
defs[4].signature_param_count = 3;
|
||||
|
||||
CBMFileResult result = {0};
|
||||
result.module_qn = module_qn;
|
||||
result.defs.items = defs;
|
||||
result.defs.count = 5;
|
||||
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
CBMTypeRegistry registry;
|
||||
cbm_rust_build_local_registry(&arena, ®istry, &result, module_qn, root, source);
|
||||
|
||||
const char *expected[] = {"i32", "Thing", "i32"};
|
||||
int failures = check_signature(®istry, top_qn, "rust-top-return-refinement", expected, 3);
|
||||
failures += check_single_return(®istry, top_qn, "rust-top-return-refinement", "Result");
|
||||
failures += check_signature(®istry, method_qn, "rust-method-return-refinement", expected, 3);
|
||||
failures +=
|
||||
check_single_return(®istry, method_qn, "rust-method-return-refinement", "Result");
|
||||
|
||||
ts_tree_delete(tree);
|
||||
cbm_arena_destroy(&arena);
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_lsp_ordered_signatures_kotlin_local_registry) {
|
||||
const char *source =
|
||||
"class Thing {}\n"
|
||||
"class Result {}\n"
|
||||
"class Holder {\n"
|
||||
" fun method(first: Int, middle: Thing?, last: Int): Result { return Result() }\n"
|
||||
"}\n"
|
||||
"fun top(first: Int, middle: Thing?, last: Int): Result { return Result() }\n";
|
||||
TSTree *tree = parse_lsp_fixture(CBM_LANG_KOTLIN, source);
|
||||
ASSERT_NOT_NULL(tree);
|
||||
TSNode root = ts_tree_root_node(tree);
|
||||
ASSERT_FALSE(ts_node_has_error(root));
|
||||
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
CBMTypeRegistry registry;
|
||||
cbm_registry_init(®istry, &arena);
|
||||
CBMResolvedCallArray out = {0};
|
||||
KotlinLSPContext ctx;
|
||||
kotlin_lsp_init(&ctx, &arena, source, (int)strlen(source), ®istry,
|
||||
"cbm.ordered.kotlin.local", "cbm.ordered.kotlin.local", "cbm", "Local.kt",
|
||||
&out);
|
||||
kotlin_lsp_process_file(&ctx, root);
|
||||
|
||||
/* Kotlin intentionally erases nullability in the registry model, but
|
||||
* Thing? must still become the real named Thing rather than UNKNOWN. */
|
||||
const char *expected[] = {"Int", "Thing", "Int"};
|
||||
int failures = check_signature(®istry, "cbm.ordered.kotlin.local.Holder.method",
|
||||
"kotlin-local-method", expected, 3);
|
||||
failures += check_single_return(®istry, "cbm.ordered.kotlin.local.Holder.method",
|
||||
"kotlin-local-method", "Result");
|
||||
failures +=
|
||||
check_signature(®istry, "cbm.ordered.kotlin.local.top", "kotlin-local-top", expected, 3);
|
||||
failures += check_single_return(®istry, "cbm.ordered.kotlin.local.top", "kotlin-local-top",
|
||||
"Result");
|
||||
|
||||
ts_tree_delete(tree);
|
||||
cbm_arena_destroy(&arena);
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_lsp_ordered_signatures_php_return_refinement) {
|
||||
const char *module_qn = "cbm.ordered.php.local";
|
||||
const char *subject_qn = "cbm.ordered.php.local.Subject";
|
||||
const char *thing_qn = "cbm.ordered.php.local.Thing";
|
||||
const char *result_qn = "cbm.ordered.php.local.Result";
|
||||
const char *source =
|
||||
"<?php\n"
|
||||
"class Thing {}\n"
|
||||
"class Result {}\n"
|
||||
"class Subject {\n"
|
||||
" public function declared(int $first, ?Thing $middle, int $last): Result {\n"
|
||||
" return new Result();\n"
|
||||
" }\n"
|
||||
" /** @return Result */\n"
|
||||
" public function documented(int $first, ?Thing $middle, int $last) {\n"
|
||||
" return new Result();\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
TSTree *tree = parse_lsp_fixture(CBM_LANG_PHP, source);
|
||||
ASSERT_NOT_NULL(tree);
|
||||
TSNode root = ts_tree_root_node(tree);
|
||||
ASSERT_FALSE(ts_node_has_error(root));
|
||||
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
CBMTypeRegistry registry;
|
||||
cbm_registry_init(®istry, &arena);
|
||||
add_fixture_type(®istry, thing_qn, "Thing");
|
||||
add_fixture_type(®istry, result_qn, "Result");
|
||||
add_fixture_type(®istry, subject_qn, "Subject");
|
||||
/* Seed the state expected immediately before AST return refinement. This
|
||||
* isolates whether a declared/PHPDoc return rewrite preserves parameters;
|
||||
* it does not depend on the still-RED PHP registration materializer. */
|
||||
add_php_seeded_method(&arena, ®istry, "cbm.ordered.php.local.Subject.declared", "declared",
|
||||
subject_qn, thing_qn, cbm_type_unknown());
|
||||
add_php_seeded_method(&arena, ®istry, "cbm.ordered.php.local.Subject.documented",
|
||||
"documented", subject_qn, thing_qn, cbm_type_unknown());
|
||||
|
||||
CBMResolvedCallArray out = {0};
|
||||
PHPLSPContext ctx;
|
||||
php_lsp_init(&ctx, &arena, source, (int)strlen(source), ®istry, module_qn, &out);
|
||||
cbm_php_refine_lsp_registry(&ctx, ®istry, root);
|
||||
|
||||
const char *expected[] = {"int", "Thing", "int"};
|
||||
int failures = check_signature(®istry, "cbm.ordered.php.local.Subject.declared",
|
||||
"php-declared-return-refinement", expected, 3);
|
||||
failures += check_single_return(®istry, "cbm.ordered.php.local.Subject.declared",
|
||||
"php-declared-return-refinement", "Result");
|
||||
failures += check_signature(®istry, "cbm.ordered.php.local.Subject.documented",
|
||||
"phpdoc-return-refinement", expected, 3);
|
||||
failures += check_single_return(®istry, "cbm.ordered.php.local.Subject.documented",
|
||||
"phpdoc-return-refinement", "Result");
|
||||
|
||||
ts_tree_delete(tree);
|
||||
cbm_arena_destroy(&arena);
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_lsp_ordered_signatures_php_trait_self_refinement) {
|
||||
const char *module_qn = "cbm.ordered.php.trait";
|
||||
const char *thing_qn = "cbm.ordered.php.trait.Thing";
|
||||
const char *trait_qn = "cbm.ordered.php.trait.Fluent";
|
||||
const char *consumer_qn = "cbm.ordered.php.trait.Consumer";
|
||||
const char *source =
|
||||
"<?php\n"
|
||||
"class Thing {}\n"
|
||||
"trait Fluent {\n"
|
||||
" public function tap(int $first, ?Thing $middle, int $last) { return $this; }\n"
|
||||
"}\n"
|
||||
"class Consumer { use Fluent; }\n";
|
||||
TSTree *tree = parse_lsp_fixture(CBM_LANG_PHP, source);
|
||||
ASSERT_NOT_NULL(tree);
|
||||
TSNode root = ts_tree_root_node(tree);
|
||||
ASSERT_FALSE(ts_node_has_error(root));
|
||||
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
CBMTypeRegistry registry;
|
||||
cbm_registry_init(®istry, &arena);
|
||||
add_fixture_type(®istry, thing_qn, "Thing");
|
||||
add_fixture_type(®istry, trait_qn, "Fluent");
|
||||
add_fixture_type(®istry, consumer_qn, "Consumer");
|
||||
/* No AST return annotation is deliberate: only trait self-substitution
|
||||
* may rebuild this seeded signature, so parameter loss has one cause. */
|
||||
add_php_seeded_method(&arena, ®istry, "cbm.ordered.php.trait.Fluent.tap", "tap", trait_qn,
|
||||
thing_qn, cbm_type_named(&arena, trait_qn));
|
||||
|
||||
CBMResolvedCallArray out = {0};
|
||||
PHPLSPContext ctx;
|
||||
php_lsp_init(&ctx, &arena, source, (int)strlen(source), ®istry, module_qn, &out);
|
||||
cbm_php_refine_lsp_registry(&ctx, ®istry, root);
|
||||
|
||||
const char *expected[] = {"int", "Thing", "int"};
|
||||
int failures = check_signature(®istry, "cbm.ordered.php.trait.Consumer.tap",
|
||||
"php-trait-self-refinement", expected, 3);
|
||||
failures += check_single_return(®istry, "cbm.ordered.php.trait.Consumer.tap",
|
||||
"php-trait-self-refinement", "Consumer");
|
||||
|
||||
ts_tree_delete(tree);
|
||||
cbm_arena_destroy(&arena);
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(repro_lsp_ordered_signatures) {
|
||||
RUN_TEST(repro_lsp_signature_param_materializer_contract);
|
||||
RUN_TEST(repro_lsp_func_replace_returns_contract);
|
||||
RUN_TEST(repro_lsp_c_unqualified_explicit_template_value_preserves_signature);
|
||||
RUN_TEST(repro_lsp_c_qualified_explicit_template_value_preserves_signature);
|
||||
RUN_TEST(repro_lsp_ordered_signatures_c_control);
|
||||
RUN_TEST(repro_lsp_ordered_signatures_cpp_control);
|
||||
RUN_TEST(repro_lsp_ordered_signatures_cuda_control);
|
||||
RUN_TEST(repro_lsp_ordered_signatures_go);
|
||||
RUN_TEST(repro_lsp_ordered_signatures_javascript);
|
||||
RUN_TEST(repro_lsp_ordered_signatures_typescript);
|
||||
RUN_TEST(repro_lsp_ordered_signatures_tsx);
|
||||
RUN_TEST(repro_lsp_ordered_signatures_python);
|
||||
RUN_TEST(repro_lsp_ordered_signatures_csharp);
|
||||
RUN_TEST(repro_lsp_ordered_signatures_csharp_cross_return);
|
||||
RUN_TEST(repro_lsp_ordered_signatures_rust);
|
||||
RUN_TEST(repro_lsp_ordered_signatures_java);
|
||||
RUN_TEST(repro_lsp_ordered_signatures_kotlin);
|
||||
RUN_TEST(repro_lsp_ordered_signatures_php);
|
||||
RUN_TEST(repro_lsp_ordered_signatures_csharp_ast_return_refinement);
|
||||
RUN_TEST(repro_lsp_ordered_signatures_rust_ast_return_refinement);
|
||||
RUN_TEST(repro_lsp_ordered_signatures_kotlin_local_registry);
|
||||
RUN_TEST(repro_lsp_ordered_signatures_php_return_refinement);
|
||||
RUN_TEST(repro_lsp_ordered_signatures_php_trait_self_refinement);
|
||||
}
|
||||
@@ -64,11 +64,8 @@
|
||||
* registry).
|
||||
* lsp_ts_jsx_import (ts_lsp.c:2657) <Comp/> JSX element whose tag is an
|
||||
* imported component (TSX only; tag matches
|
||||
* an import local name → synthetic
|
||||
* "<module>.<Comp>" QN). NOTE: this site
|
||||
* builds the callee QN WITHOUT verifying the
|
||||
* symbol exists in the registry, so it can
|
||||
* emit even when the import target is absent.
|
||||
* an import local name and the exact
|
||||
* component exists in the registry.
|
||||
* lsp_ts (ts_lsp.c:116) DEFAULT fallback inside ts_emit_resolved_call
|
||||
* used only when a caller passes a NULL
|
||||
* strategy. Every concrete emit site passes a
|
||||
@@ -311,10 +308,9 @@ static const char kTsxJsx[] =
|
||||
" return <Widget />;\n"
|
||||
"}\n";
|
||||
|
||||
/* lsp_ts_jsx_import — <Comp/> JSX element whose tag is an imported component
|
||||
* (ts_lsp.c:2652-2657). TSX only; the tag matches an import local name → a
|
||||
* synthetic "<module>.<Comp>" callee QN is emitted (this site does NOT verify
|
||||
* the symbol is in the registry). Cross-file: widget.tsx exports Widget,
|
||||
/* lsp_ts_jsx_import — <Comp/> JSX element whose tag is an imported component.
|
||||
* TSX only; the tag matches an import local name and the cross-file registry
|
||||
* proves the exact component symbol. Cross-file: widget.tsx exports Widget,
|
||||
* app.tsx imports it and renders <Widget/>. */
|
||||
static const RFile kTsxJsxImport[] = {
|
||||
{"widget.tsx",
|
||||
|
||||
+51
-24
@@ -2,11 +2,11 @@
|
||||
* repro_main.c — Entry point for the cumulative BUG-REPRODUCTION suite.
|
||||
*
|
||||
* This runner is SEPARATE from the gating `make test` (test-runner). It exists
|
||||
* to hold reproduce-first cases for every OPEN bug issue. Each case asserts the
|
||||
* CORRECT behaviour, so it is **RED until the bug is fixed** — the redness is the
|
||||
* deliverable (proof the bug is real + the permanent regression guard).
|
||||
* to hold reproduce-first cases for OPEN bugs plus the controls needed to prove
|
||||
* their fixtures and oracles are meaningful. Reproduction cases assert the
|
||||
* correct behaviour and stay RED until fixed; controls remain GREEN.
|
||||
*
|
||||
* Because these cases are red by design, they MUST NOT live in `ALL_TEST_SRCS`
|
||||
* Because open reproductions are red by design, these suites MUST NOT live in `ALL_TEST_SRCS`
|
||||
* (that would turn the PR gate `ci-ok` red and wedge every merge). They are built
|
||||
* + run only via `make test-repro` and the `bug-repro.yml` workflow, neither of
|
||||
* which gates branch protection.
|
||||
@@ -26,33 +26,41 @@ int tf_fail_count = 0;
|
||||
int tf_skip_count = 0;
|
||||
|
||||
#include "test_framework.h"
|
||||
#include "repro_runner.h"
|
||||
#include "foundation/compat.h" /* cbm_setenv — #845 supervisor kill switch */
|
||||
|
||||
/* Per-suite summary + filter. RUN_SUITE prints a one-line
|
||||
* "[SUITE] <name> P passed, F failed" report (greppable for which suites still
|
||||
* have reds). When CBM_REPRO_ONLY is set (comma/space list of suite-name
|
||||
* substrings), only matching suites run — for fast targeted validation of a
|
||||
* single fix without rebuilding intent. */
|
||||
static int cbm_suite_enabled(const char *name) {
|
||||
const char *only = getenv("CBM_REPRO_ONLY");
|
||||
if (!only || !*only)
|
||||
return 1;
|
||||
return strstr(only, name) != NULL;
|
||||
}
|
||||
* have reds). The shared selector implementation lives with its gating tests in
|
||||
* repro_runner_filter.c. */
|
||||
#undef RUN_SUITE
|
||||
#define RUN_SUITE(name) \
|
||||
do { \
|
||||
if (!cbm_suite_enabled(#name)) \
|
||||
break; \
|
||||
int _p0 = tf_pass_count, _f0 = tf_fail_count; \
|
||||
printf("\n%s=== %s ===%s\n", tf_dim(), #name, tf_reset()); \
|
||||
suite_##name(); \
|
||||
printf("[SUITE] %-38s %d passed, %d failed\n", #name, tf_pass_count - _p0, \
|
||||
tf_fail_count - _f0); \
|
||||
#define RUN_SUITE(name) \
|
||||
do { \
|
||||
if (!cbm_suite_enabled(#name)) \
|
||||
break; \
|
||||
int _p0 = tf_pass_count, _f0 = tf_fail_count; \
|
||||
printf("\n%s=== %s ===%s\n", tf_dim(), #name, tf_reset()); \
|
||||
suite_##name(); \
|
||||
printf("[SUITE] %-38s %d passed, %d failed\n", #name, tf_pass_count - _p0, \
|
||||
tf_fail_count - _f0); \
|
||||
} while (0)
|
||||
|
||||
/* ── Repro suites (one per bug cluster / issue) ─────────────────── */
|
||||
extern void suite_repro_extraction(void);
|
||||
extern void suite_repro_runner_filter(void);
|
||||
extern void suite_repro_harness_cleanup(void);
|
||||
extern void suite_repro_language_registry(void);
|
||||
extern void suite_repro_call_node_manifest(void);
|
||||
extern void suite_repro_call_scope_usages(void);
|
||||
extern void suite_repro_call_argument_usages(void);
|
||||
extern void suite_repro_lsp_ordered_signatures(void);
|
||||
extern void suite_repro_lsp_ordered_local(void);
|
||||
extern void suite_repro_ts_overload_return_chains(void);
|
||||
extern void suite_repro_reference_precision(void);
|
||||
extern void suite_repro_lexical_binding_precision(void);
|
||||
extern void suite_repro_call_argument_matrix_a(void);
|
||||
extern void suite_repro_call_argument_matrix_b(void);
|
||||
extern void suite_repro_call_node_behaviors(void);
|
||||
extern void suite_repro_parallel_determinism(void);
|
||||
extern void suite_repro_issue495(void);
|
||||
extern void suite_repro_issue521(void);
|
||||
@@ -132,11 +140,25 @@ int main(void) {
|
||||
printf("════════════════════════════════════════════════════════════\n");
|
||||
printf(" CUMULATIVE BUG-REPRODUCTION SUITE\n");
|
||||
printf(" RED rows are EXPECTED — each is an open bug reproduced.\n");
|
||||
printf(" A row that PASSES means that bug appears FIXED → flip it\n");
|
||||
printf(" into the gating suite and close the issue with the guard.\n");
|
||||
printf(" PASS rows may be controls or candidate fixes. Verify each\n");
|
||||
printf(" RED→GREEN transition before promotion or issue closure.\n");
|
||||
printf("════════════════════════════════════════════════════════════\n");
|
||||
|
||||
RUN_SUITE(repro_extraction);
|
||||
RUN_SUITE(repro_runner_filter);
|
||||
RUN_SUITE(repro_harness_cleanup);
|
||||
RUN_SUITE(repro_language_registry);
|
||||
RUN_SUITE(repro_call_node_manifest);
|
||||
RUN_SUITE(repro_call_scope_usages);
|
||||
RUN_SUITE(repro_call_argument_usages);
|
||||
RUN_SUITE(repro_lsp_ordered_signatures);
|
||||
RUN_SUITE(repro_lsp_ordered_local);
|
||||
RUN_SUITE(repro_ts_overload_return_chains);
|
||||
RUN_SUITE(repro_reference_precision);
|
||||
RUN_SUITE(repro_lexical_binding_precision);
|
||||
RUN_SUITE(repro_call_argument_matrix_a);
|
||||
RUN_SUITE(repro_call_argument_matrix_b);
|
||||
RUN_SUITE(repro_call_node_behaviors);
|
||||
RUN_SUITE(repro_parallel_determinism);
|
||||
RUN_SUITE(repro_issue495);
|
||||
RUN_SUITE(repro_issue521);
|
||||
@@ -194,5 +216,10 @@ int main(void) {
|
||||
RUN_SUITE(repro_lsp_java_cs);
|
||||
RUN_SUITE(repro_lsp_kt_php_rust);
|
||||
|
||||
if (tf_pass_count + tf_fail_count + tf_skip_count == 0) {
|
||||
fprintf(stderr, "::error::bug-repro runner executed zero tests\n");
|
||||
return 2;
|
||||
}
|
||||
|
||||
TEST_SUMMARY();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
# Reproduce linked targets ignoring shared test or production-header changes,
|
||||
# which can execute a stale binary after a header-only semantic fix.
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
MAKE_DB="$(mktemp "${TMPDIR:-/tmp}/cbm-repro-make-db.XXXXXX")"
|
||||
trap 'rm -f "$MAKE_DB"' EXIT
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
make -f Makefile.cbm -pn build/c/test-repro-runner >"$MAKE_DB"
|
||||
repro_target_line="$(grep '^build/c/test-repro-runner:' "$MAKE_DB" | head -1 || true)"
|
||||
test_target_line="$(grep '^build/c/test-runner:' "$MAKE_DB" | head -1 || true)"
|
||||
tsan_target_line="$(grep '^build/c/test-runner-tsan:' "$MAKE_DB" | head -1 || true)"
|
||||
production_target_line="$(grep '^build/c/codebase-memory-mcp:' "$MAKE_DB" | head -1 || true)"
|
||||
|
||||
if [[ "$repro_target_line" != *'tests/repro/repro_harness.h'* ||
|
||||
"$repro_target_line" != *'tests/test_framework.h'* ||
|
||||
"$repro_target_line" != *'tests/test_helpers.h'* ]]; then
|
||||
printf '[repro-make-headers] invariant=shared_headers_missing_from_target\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
missing_test_header=0
|
||||
for target_name in build/c/test-runner build/c/test-runner-tsan; do
|
||||
target_line="$test_target_line"
|
||||
if [[ "$target_name" == 'build/c/test-runner-tsan' ]]; then
|
||||
target_line="$tsan_target_line"
|
||||
fi
|
||||
for header in tests/test_framework.h tests/test_helpers.h tests/grammar_cases.h; do
|
||||
if [[ "$target_line" != *"$header"* ]]; then
|
||||
printf '[repro-make-headers] invariant=test_header_missing_from_target target=%s header=%s\n' \
|
||||
"$target_name" "$header" >&2
|
||||
missing_test_header=1
|
||||
fi
|
||||
done
|
||||
done
|
||||
missing_production_header=0
|
||||
for target_name in build/c/test-runner build/c/test-repro-runner build/c/test-runner-tsan \
|
||||
build/c/codebase-memory-mcp; do
|
||||
case "$target_name" in
|
||||
build/c/test-runner) target_line="$test_target_line" ;;
|
||||
build/c/test-repro-runner) target_line="$repro_target_line" ;;
|
||||
build/c/test-runner-tsan) target_line="$tsan_target_line" ;;
|
||||
*) target_line="$production_target_line" ;;
|
||||
esac
|
||||
for header in src/pipeline/lsp_resolve.h internal/cbm/lsp/type_registry.h; do
|
||||
if [[ "$target_line" != *"$header"* ]]; then
|
||||
printf '[repro-make-headers] invariant=production_header_missing_from_target target=%s header=%s\n' \
|
||||
"$target_name" "$header" >&2
|
||||
missing_production_header=1
|
||||
fi
|
||||
done
|
||||
done
|
||||
if [[ "$missing_test_header" -ne 0 || "$missing_production_header" -ne 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '[repro-make-headers] shared headers tracked by runner and production targets\n'
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
#ifndef CBM_TESTS_REPRO_RUNNER_H
|
||||
#define CBM_TESTS_REPRO_RUNNER_H
|
||||
|
||||
int cbm_suite_enabled(const char *name);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,130 @@
|
||||
/* Reproductions for the cumulative repro runner itself. */
|
||||
#include "test_framework.h"
|
||||
#include "repro_runner.h"
|
||||
#include "foundation/compat.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static int selector_delimiter(char value) {
|
||||
return value == ',' || value == ' ' || value == '\t' || value == '\r' || value == '\n';
|
||||
}
|
||||
|
||||
static int suite_name_contains_token(const char *name, const char *token, size_t token_len) {
|
||||
size_t name_len = strlen(name);
|
||||
if (token_len > name_len) {
|
||||
return 0;
|
||||
}
|
||||
for (size_t offset = 0; offset + token_len <= name_len; offset++) {
|
||||
if (strncmp(name + offset, token, token_len) == 0) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cbm_suite_enabled(const char *name) {
|
||||
const char *only = getenv("CBM_REPRO_ONLY");
|
||||
if (!only || !*only) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char *cursor = only;
|
||||
while (*cursor) {
|
||||
while (*cursor && selector_delimiter(*cursor)) {
|
||||
cursor++;
|
||||
}
|
||||
const char *token = cursor;
|
||||
while (*cursor && !selector_delimiter(*cursor)) {
|
||||
cursor++;
|
||||
}
|
||||
size_t token_len = (size_t)(cursor - token);
|
||||
if (token_len > 0 && suite_name_contains_token(name, token, token_len)) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char *save_selector(void) {
|
||||
const char *prior = getenv("CBM_REPRO_ONLY");
|
||||
return prior ? cbm_strdup(prior) : NULL;
|
||||
}
|
||||
|
||||
static void restore_selector(char *saved) {
|
||||
if (saved) {
|
||||
cbm_setenv("CBM_REPRO_ONLY", saved, 1);
|
||||
free(saved);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_REPRO_ONLY");
|
||||
}
|
||||
}
|
||||
|
||||
/* The documented selector accepts suite-name substrings. */
|
||||
TEST(repro_runner_filter_accepts_suite_substring) {
|
||||
char *saved = save_selector();
|
||||
cbm_setenv("CBM_REPRO_ONLY", "call_argument", 1);
|
||||
int enabled = cbm_suite_enabled("repro_call_argument_usages");
|
||||
restore_selector(saved);
|
||||
if (!enabled)
|
||||
fprintf(stderr, " [repro-filter] invariant=suite_substring_not_matched\n");
|
||||
ASSERT_TRUE(enabled);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_runner_filter_accepts_comma_list) {
|
||||
char *saved = save_selector();
|
||||
cbm_setenv("CBM_REPRO_ONLY", "language_registry,call_argument", 1);
|
||||
int first = cbm_suite_enabled("repro_language_registry");
|
||||
int second = cbm_suite_enabled("repro_call_argument_usages");
|
||||
restore_selector(saved);
|
||||
if (!first || !second)
|
||||
fprintf(stderr, " [repro-filter] invariant=comma_list_not_matched\n");
|
||||
ASSERT_TRUE(first && second);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_runner_filter_accepts_space_list) {
|
||||
char *saved = save_selector();
|
||||
cbm_setenv("CBM_REPRO_ONLY", "language_registry call_argument", 1);
|
||||
int first = cbm_suite_enabled("repro_language_registry");
|
||||
int second = cbm_suite_enabled("repro_call_argument_usages");
|
||||
restore_selector(saved);
|
||||
if (!first || !second)
|
||||
fprintf(stderr, " [repro-filter] invariant=space_list_not_matched\n");
|
||||
ASSERT_TRUE(first && second);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_runner_filter_rejects_nonmatch) {
|
||||
char *saved = save_selector();
|
||||
cbm_setenv("CBM_REPRO_ONLY", "language_registry", 1);
|
||||
int enabled = cbm_suite_enabled("repro_call_argument_usages");
|
||||
restore_selector(saved);
|
||||
if (enabled)
|
||||
fprintf(stderr, " [repro-filter] invariant=nonmatch_enabled\n");
|
||||
ASSERT_FALSE(enabled);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(repro_runner_filter_unset_or_empty_enables_all) {
|
||||
char *saved = save_selector();
|
||||
cbm_unsetenv("CBM_REPRO_ONLY");
|
||||
int unset_enabled = cbm_suite_enabled("repro_call_argument_usages");
|
||||
cbm_setenv("CBM_REPRO_ONLY", "", 1);
|
||||
int empty_enabled = cbm_suite_enabled("repro_call_argument_usages");
|
||||
restore_selector(saved);
|
||||
if (!unset_enabled || !empty_enabled)
|
||||
fprintf(stderr, " [repro-filter] invariant=unset_or_empty_disabled\n");
|
||||
ASSERT_TRUE(unset_enabled && empty_enabled);
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(repro_runner_filter) {
|
||||
RUN_TEST(repro_runner_filter_accepts_suite_substring);
|
||||
RUN_TEST(repro_runner_filter_accepts_comma_list);
|
||||
RUN_TEST(repro_runner_filter_accepts_space_list);
|
||||
RUN_TEST(repro_runner_filter_rejects_nonmatch);
|
||||
RUN_TEST(repro_runner_filter_unset_or_empty_enables_all);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# The compiled repro runner must not claim every passing control is a fixed bug.
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
RUNNER="$PROJECT_ROOT/build/c/test-repro-runner"
|
||||
|
||||
if [[ ! -x "$RUNNER" ]]; then
|
||||
printf '[repro-runner-semantics] invariant=runner_missing path=%s\n' "$RUNNER" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set +e
|
||||
output="$(CBM_REPRO_ONLY=repro_runner_filter "$RUNNER" 2>&1)"
|
||||
status=$?
|
||||
set -e
|
||||
|
||||
if [[ $status -ne 0 ]]; then
|
||||
printf '[repro-runner-semantics] invariant=control_suite_failed status=%d\n' "$status" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
old_claim='A row that PASSES means that bug appears FIXED'
|
||||
if [[ "$output" == *"$old_claim"* ]]; then
|
||||
printf '[repro-runner-semantics] invariant=passing_controls_blanket_fixed old_claim=true\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
neutral_claim='PASS rows may be controls or candidate fixes'
|
||||
if [[ "$output" != *"$neutral_claim"* ]]; then
|
||||
printf '[repro-runner-semantics] invariant=neutral_passing_semantics_missing\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '[repro-runner-semantics] compiled runner reports neutral passing semantics\n'
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# Reproduce the compiled repro runner returning success when a selector matches
|
||||
# no suite. This protects direct `make test-repro`, not only scripts/repro.sh.
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
RUNNER="$PROJECT_ROOT/build/c/test-repro-runner"
|
||||
|
||||
if [[ ! -x "$RUNNER" ]]; then
|
||||
printf '[repro-runner-zero] invariant=runner_missing path=%s\n' "$RUNNER" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set +e
|
||||
output="$(CBM_REPRO_ONLY='__cbm_no_such_repro_suite__' "$RUNNER" 2>&1)"
|
||||
status=$?
|
||||
set -e
|
||||
|
||||
if [[ $status -eq 0 ]]; then
|
||||
printf '[repro-runner-zero] invariant=compiled_runner_accepted_zero_tests status=%d\n' \
|
||||
"$status" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
expected='bug-repro runner executed zero tests'
|
||||
if [[ "$output" != *"$expected"* ]]; then
|
||||
printf '[repro-runner-zero] invariant=zero_test_diagnostic_missing status=%d\n' \
|
||||
"$status" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '[repro-runner-zero] compiled runner rejected zero tests correctly\n'
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
# Reproduce scripts/repro.sh exiting through `set -e -o pipefail` before its
|
||||
# explicit build/runner diagnostic when the transcript has no aggregate line.
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/cbm-repro-missing-summary.XXXXXX")"
|
||||
|
||||
cleanup() {
|
||||
rm -f "$TMP_ROOT/scripts/repro.sh" "$TMP_ROOT/scripts/env.sh"
|
||||
rm -f "$TMP_ROOT/repro-out.txt" "$TMP_ROOT/summary.txt"
|
||||
rmdir "$TMP_ROOT/scripts" "$TMP_ROOT" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$TMP_ROOT/scripts"
|
||||
cp "$PROJECT_ROOT/scripts/repro.sh" "$TMP_ROOT/scripts/repro.sh"
|
||||
cp "$PROJECT_ROOT/scripts/env.sh" "$TMP_ROOT/scripts/env.sh"
|
||||
|
||||
# Model a compile/link failure: make emits a useful error but the test runner
|
||||
# never reaches its final aggregate summary.
|
||||
make() {
|
||||
printf '%s\n' 'clang: error: linker command failed with exit code 1'
|
||||
return 2
|
||||
}
|
||||
export -f make
|
||||
|
||||
set +e
|
||||
output="$(TMPDIR="$TMP_ROOT" CC=true CXX=true GITHUB_STEP_SUMMARY="$TMP_ROOT/summary.txt" \
|
||||
bash "$TMP_ROOT/scripts/repro.sh" 2>&1)"
|
||||
status=$?
|
||||
set -e
|
||||
|
||||
if [[ $status -eq 0 ]]; then
|
||||
printf '[repro-missing-summary] invariant=build_failure_accepted status=%d\n' "$status" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
linker_marker='clang: error: linker command failed with exit code 1'
|
||||
if [[ "$output" != *"$linker_marker"* ]]; then
|
||||
printf '[repro-missing-summary] invariant=mocked_linker_failure_not_observed status=%d\n' \
|
||||
"$status" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
expected='::error::bug-repro runner did not execute — build or link failure'
|
||||
if [[ "$output" != *"$expected"* ]]; then
|
||||
printf '[repro-missing-summary] invariant=explicit_diagnostic_missing status=%d\n' \
|
||||
"$status" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '[repro-missing-summary] missing aggregate rejected with explicit diagnostic\n'
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# Reproduce scripts/repro.sh leaving its generated runner transcript in the
|
||||
# repository root after a completed board run.
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/cbm-repro-artifact.XXXXXX")"
|
||||
|
||||
cleanup() {
|
||||
rm -f "$TMP_ROOT/scripts/repro.sh" "$TMP_ROOT/scripts/env.sh"
|
||||
rm -f "$TMP_ROOT/repro-out.txt" "$TMP_ROOT/summary.txt"
|
||||
rmdir "$TMP_ROOT/scripts" "$TMP_ROOT" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$TMP_ROOT/scripts"
|
||||
cp "$PROJECT_ROOT/scripts/repro.sh" "$TMP_ROOT/scripts/repro.sh"
|
||||
cp "$PROJECT_ROOT/scripts/env.sh" "$TMP_ROOT/scripts/env.sh"
|
||||
|
||||
make() {
|
||||
printf '%s\n' \
|
||||
'────────────────────────────────────────────' \
|
||||
' 1 passed'
|
||||
return 0
|
||||
}
|
||||
export -f make
|
||||
|
||||
TMPDIR="$TMP_ROOT" CC=true CXX=true GITHUB_STEP_SUMMARY="$TMP_ROOT/summary.txt" \
|
||||
bash "$TMP_ROOT/scripts/repro.sh" >/dev/null 2>&1
|
||||
|
||||
if [[ -e "$TMP_ROOT/repro-out.txt" ]]; then
|
||||
printf '[repro-artifact] invariant=workspace_transcript_left_behind path=repro-out.txt\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '[repro-artifact] workspace transcript cleaned correctly\n'
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
# Reproduce scripts/repro.sh treating the board as successful even though the
|
||||
# test framework's valid final aggregate reports one or more skipped tests.
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/cbm-repro-skipped.XXXXXX")"
|
||||
|
||||
cleanup() {
|
||||
rm -f "$TMP_ROOT/scripts/repro.sh" "$TMP_ROOT/scripts/env.sh"
|
||||
rm -f "$TMP_ROOT/repro-out.txt" "$TMP_ROOT/summary.txt"
|
||||
rmdir "$TMP_ROOT/scripts" "$TMP_ROOT" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$TMP_ROOT/scripts"
|
||||
cp "$PROJECT_ROOT/scripts/repro.sh" "$TMP_ROOT/scripts/repro.sh"
|
||||
cp "$PROJECT_ROOT/scripts/env.sh" "$TMP_ROOT/scripts/env.sh"
|
||||
|
||||
# TEST_SUMMARY appends the skipped count after pass/fail counts. The board must
|
||||
# parse and report the aggregate faithfully, but it must exit nonzero because a
|
||||
# board with skipped cases is incomplete.
|
||||
make() {
|
||||
printf '%s\n' \
|
||||
'[SUITE] repro_parallel_determinism 0 passed, 2 skipped' \
|
||||
'[SUITE] repro_call_argument_usages 1 passed, 17 failed' \
|
||||
'────────────────────────────────────────────' \
|
||||
' 1 passed, 17 failed, 2 skipped'
|
||||
return 1
|
||||
}
|
||||
export -f make
|
||||
|
||||
set +e
|
||||
output="$(TMPDIR="$TMP_ROOT" CC=true CXX=true GITHUB_STEP_SUMMARY="$TMP_ROOT/summary.txt" \
|
||||
bash "$TMP_ROOT/scripts/repro.sh" 2>&1)"
|
||||
status=$?
|
||||
set -e
|
||||
|
||||
expected_prefix='=== bug-repro board: 17 reproduced (RED), 1 '
|
||||
if [[ "$output" != *"$expected_prefix"* ]]; then
|
||||
actual="$(printf '%s\n' "$output" | grep '^=== bug-repro board:' || true)"
|
||||
printf '[repro-skipped] invariant=skipped_aggregate_misreported expected=17/1 actual=%s\n' \
|
||||
"${actual:-missing}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
skip_summary='- **2** case(s) SKIPPED — board is incomplete'
|
||||
if ! grep -Fq -- "$skip_summary" "$TMP_ROOT/summary.txt"; then
|
||||
printf '[repro-skipped] invariant=skipped_count_hidden expected=2\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
skip_warning='::warning::bug-repro board incomplete — 2 case(s) skipped'
|
||||
if [[ "$output" != *"$skip_warning"* ]]; then
|
||||
printf '[repro-skipped] invariant=skipped_warning_hidden expected=2\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $status -eq 0 ]]; then
|
||||
printf '[repro-skipped] invariant=incomplete_board_exited_zero expected=nonzero actual=0\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '[repro-skipped] skipped aggregate reported and rejected correctly\n'
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
# Reproduce the no-GITHUB_STEP_SUMMARY fallback writing through /dev/stderr,
|
||||
# which can be denied even though the process already has a valid stderr FD.
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/cbm-repro-stderr.XXXXXX")"
|
||||
|
||||
cleanup() {
|
||||
rm -f "$TMP_ROOT/scripts/repro.sh" "$TMP_ROOT/scripts/env.sh"
|
||||
rm -f "$TMP_ROOT/repro-out.txt"
|
||||
rmdir "$TMP_ROOT/scripts" "$TMP_ROOT" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$TMP_ROOT/scripts"
|
||||
cp "$PROJECT_ROOT/scripts/repro.sh" "$TMP_ROOT/scripts/repro.sh"
|
||||
cp "$PROJECT_ROOT/scripts/env.sh" "$TMP_ROOT/scripts/env.sh"
|
||||
|
||||
make() {
|
||||
printf '%s\n' \
|
||||
'────────────────────────────────────────────' \
|
||||
' 1 passed, 1 failed'
|
||||
return 1
|
||||
}
|
||||
export -f make
|
||||
|
||||
set +e
|
||||
output="$(
|
||||
unset GITHUB_STEP_SUMMARY
|
||||
TMPDIR="$TMP_ROOT" CC=true CXX=true bash "$TMP_ROOT/scripts/repro.sh" 2>&1
|
||||
)"
|
||||
status=$?
|
||||
set -e
|
||||
|
||||
if [[ $status -ne 0 ]]; then
|
||||
printf '[repro-stderr] invariant=fallback_run_failed status=%d\n' "$status" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$output" == *'Operation not permitted'* ]]; then
|
||||
printf '[repro-stderr] invariant=stderr_fallback_permission_denied\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$output" != *'## Bug-reproduction board'* ||
|
||||
"$output" != *'**1** open bug(s) still reproduced'* ]]; then
|
||||
printf '[repro-stderr] invariant=stderr_summary_missing\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '[repro-stderr] summary written through stderr correctly\n'
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
# Reproduce scripts/repro.sh selecting the first per-suite count instead of the
|
||||
# final cumulative total when more than one repro suite runs, while ensuring
|
||||
# known-green controls are not blanket-labeled as candidate fixes.
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/cbm-repro-board.XXXXXX")"
|
||||
|
||||
cleanup() {
|
||||
rm -f "$TMP_ROOT/scripts/repro.sh" "$TMP_ROOT/scripts/env.sh"
|
||||
rm -f "$TMP_ROOT/repro-out.txt" "$TMP_ROOT/summary.txt"
|
||||
rmdir "$TMP_ROOT/scripts" "$TMP_ROOT" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$TMP_ROOT/scripts"
|
||||
cp "$PROJECT_ROOT/scripts/repro.sh" "$TMP_ROOT/scripts/repro.sh"
|
||||
cp "$PROJECT_ROOT/scripts/env.sh" "$TMP_ROOT/scripts/env.sh"
|
||||
|
||||
# The copied production script calls make. This test-local function supplies a
|
||||
# deterministic runner transcript while exercising the production board parser.
|
||||
make() {
|
||||
printf '%s\n' \
|
||||
'[SUITE] repro_known_green_controls 2 passed' \
|
||||
'[SUITE] repro_runner_filter 0 passed, 1 failed' \
|
||||
'[SUITE] repro_call_argument_usages 0 passed, 17 failed' \
|
||||
'────────────────────────────────────────────' \
|
||||
' 2 passed, 18 failed'
|
||||
return 1
|
||||
}
|
||||
export -f make
|
||||
|
||||
set +e
|
||||
output="$(TMPDIR="$TMP_ROOT" CC=true CXX=true GITHUB_STEP_SUMMARY="$TMP_ROOT/summary.txt" \
|
||||
bash "$TMP_ROOT/scripts/repro.sh" 2>&1)"
|
||||
status=$?
|
||||
set -e
|
||||
|
||||
if [[ $status -ne 0 ]]; then
|
||||
printf '[repro-board] invariant=production_script_did_not_complete status=%d\n' "$status" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
expected_prefix='=== bug-repro board: 18 reproduced (RED), 2 '
|
||||
if [[ "$output" != *"$expected_prefix"* ]]; then
|
||||
actual="$(printf '%s\n' "$output" | grep '^=== bug-repro board:' || true)"
|
||||
printf '[repro-board] invariant=cumulative_total_not_reported expected=18/2 actual=%s\n' \
|
||||
"${actual:-missing}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
neutral_label='passing control(s) or candidate-fix candidate(s)'
|
||||
if [[ "$output" != *"$neutral_label"* ]] ||
|
||||
! grep -Fq -- "$neutral_label" "$TMP_ROOT/summary.txt"; then
|
||||
actual="$(printf '%s\n' "$output" | grep '^=== bug-repro board:' || true)"
|
||||
printf '[repro-board] invariant=passing_controls_blanket_candidate_fixed expected=%q actual=%s\n' \
|
||||
"$neutral_label" "${actual:-missing}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$output" == *'passing (candidate-fixed)'* ]] ||
|
||||
grep -Fq -- 'PASSING — candidate-fixed' "$TMP_ROOT/summary.txt"; then
|
||||
printf '[repro-board] invariant=passing_controls_blanket_candidate_fixed old_label_present=true\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '[repro-board] cumulative total and neutral passing semantics reported correctly\n'
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
# Reproduce scripts/repro.sh accepting a selector/build transcript in which the
|
||||
# runner executes zero tests. A zero-test board proves neither red nor green and
|
||||
# must be treated as a runner failure.
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/cbm-repro-zero.XXXXXX")"
|
||||
|
||||
cleanup() {
|
||||
rm -f "$TMP_ROOT/scripts/repro.sh" "$TMP_ROOT/scripts/env.sh"
|
||||
rm -f "$TMP_ROOT/repro-out.txt" "$TMP_ROOT/summary.txt"
|
||||
rmdir "$TMP_ROOT/scripts" "$TMP_ROOT" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$TMP_ROOT/scripts"
|
||||
cp "$PROJECT_ROOT/scripts/repro.sh" "$TMP_ROOT/scripts/repro.sh"
|
||||
cp "$PROJECT_ROOT/scripts/env.sh" "$TMP_ROOT/scripts/env.sh"
|
||||
|
||||
# Supply the exact transcript produced when CBM_REPRO_ONLY matches no suite.
|
||||
make() {
|
||||
printf '%s\n' \
|
||||
'────────────────────────────────────────────' \
|
||||
' 0 passed'
|
||||
return 0
|
||||
}
|
||||
export -f make
|
||||
|
||||
set +e
|
||||
output="$(TMPDIR="$TMP_ROOT" CC=true CXX=true GITHUB_STEP_SUMMARY="$TMP_ROOT/summary.txt" \
|
||||
bash "$TMP_ROOT/scripts/repro.sh" 2>&1)"
|
||||
status=$?
|
||||
set -e
|
||||
|
||||
if [[ $status -eq 0 ]]; then
|
||||
board="$(printf '%s\n' "$output" | grep '^=== bug-repro board:' || true)"
|
||||
printf '[repro-zero] invariant=zero_tests_accepted status=%d board=%s\n' \
|
||||
"$status" "${board:-missing}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$output" != *'bug-repro runner executed zero tests'* ]]; then
|
||||
printf '[repro-zero] invariant=zero_test_error_missing status=%d\n' "$status" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '[repro-zero] zero-test transcript rejected correctly\n'
|
||||
@@ -0,0 +1,514 @@
|
||||
/*
|
||||
* repro_ts_overload_return_chains.c — REDs for argument-aware return typing
|
||||
* of TypeScript/TSX overloads outside method dispatch.
|
||||
*
|
||||
* Both fixtures register two overloads with the same QN and arity but reversed
|
||||
* positional types. They invoke both orders and assert distinct downstream
|
||||
* method targets. A declaration-order, last-definition, or arity-only fallback
|
||||
* can satisfy at most one chain; both become GREEN only when the call arguments
|
||||
* select the return signature used by expression and contextual callback typing.
|
||||
* Shadow and exact-import cases guard the selector's fail-closed boundaries.
|
||||
*
|
||||
* The shadowing fixtures use untyped parameters on purpose. An UNKNOWN local
|
||||
* binding still owns the lexical name and must block both call-edge promotion
|
||||
* and return-type inheritance from a same-named registry symbol.
|
||||
*/
|
||||
|
||||
#include "test_framework.h"
|
||||
|
||||
#include "arena.h"
|
||||
#include "cbm.h"
|
||||
#include "lsp/ts_lsp.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
enum { OVERLOAD_DEF_COUNT = 7 };
|
||||
|
||||
typedef struct {
|
||||
const char *symbol_module_qn;
|
||||
const char *caller_module_qn;
|
||||
const char *reversed_type_qn;
|
||||
const char *pair_type_qn;
|
||||
const char *reversed_method_qn;
|
||||
const char *pair_method_qn;
|
||||
const char *choose_qn;
|
||||
const char *run_qn;
|
||||
} TSOverloadFixtureNames;
|
||||
|
||||
static const char *kNumberStringParams[] = {"number", "string"};
|
||||
static const char *kStringNumberParams[] = {"string", "number"};
|
||||
static const char *kNumberStringPairCallbackParams[] = {"number", "string",
|
||||
"(value: Pair) => void"};
|
||||
static const char *kStringNumberReversedCallbackParams[] = {"string", "number",
|
||||
"(value: Reversed) => void"};
|
||||
|
||||
static bool ends_with(const char *text, const char *suffix) {
|
||||
if (!text || !suffix)
|
||||
return false;
|
||||
size_t text_len = strlen(text);
|
||||
size_t suffix_len = strlen(suffix);
|
||||
return suffix_len <= text_len && strcmp(text + text_len - suffix_len, suffix) == 0;
|
||||
}
|
||||
|
||||
static int count_resolved_suffix(const CBMResolvedCallArray *calls, const char *callee_suffix,
|
||||
const char *strategy) {
|
||||
if (!calls)
|
||||
return 0;
|
||||
int count = 0;
|
||||
for (int i = 0; i < calls->count; i++) {
|
||||
const CBMResolvedCall *call = &calls->items[i];
|
||||
if (call->kind == CBM_RESOLVED_INVOCATION && call->confidence >= 0.5f && call->caller_qn &&
|
||||
strstr(call->caller_qn, "run") && ends_with(call->callee_qn, callee_suffix) &&
|
||||
call->strategy && strcmp(call->strategy, strategy) == 0) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static int count_any_callee_suffix(const CBMResolvedCallArray *calls, const char *callee_suffix) {
|
||||
if (!calls)
|
||||
return 0;
|
||||
int count = 0;
|
||||
for (int i = 0; i < calls->count; i++) {
|
||||
const CBMResolvedCall *call = &calls->items[i];
|
||||
if (call->caller_qn && strstr(call->caller_qn, "run") &&
|
||||
ends_with(call->callee_qn, callee_suffix)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static void print_resolved_calls(const CBMResolvedCallArray *calls) {
|
||||
if (!calls)
|
||||
return;
|
||||
for (int i = 0; i < calls->count; i++) {
|
||||
const CBMResolvedCall *call = &calls->items[i];
|
||||
printf(" %s -> %s [%s %.2f kind=%d]\n", call->caller_qn ? call->caller_qn : "(null)",
|
||||
call->callee_qn ? call->callee_qn : "(null)",
|
||||
call->strategy ? call->strategy : "(null)", call->confidence, (int)call->kind);
|
||||
}
|
||||
}
|
||||
|
||||
static void init_overload_defs(CBMLSPDef defs[OVERLOAD_DEF_COUNT], CBMLanguage language,
|
||||
const TSOverloadFixtureNames *names) {
|
||||
memset(defs, 0, (size_t)OVERLOAD_DEF_COUNT * sizeof(*defs));
|
||||
|
||||
defs[0].qualified_name = names->reversed_type_qn;
|
||||
defs[0].short_name = "Reversed";
|
||||
defs[0].label = "Interface";
|
||||
defs[0].def_module_qn = names->symbol_module_qn;
|
||||
defs[0].is_interface = true;
|
||||
defs[0].lang = language;
|
||||
|
||||
defs[1].qualified_name = names->pair_type_qn;
|
||||
defs[1].short_name = "Pair";
|
||||
defs[1].label = "Interface";
|
||||
defs[1].def_module_qn = names->symbol_module_qn;
|
||||
defs[1].is_interface = true;
|
||||
defs[1].lang = language;
|
||||
|
||||
defs[2].qualified_name = names->reversed_method_qn;
|
||||
defs[2].short_name = "reversedOnly";
|
||||
defs[2].label = "Method";
|
||||
defs[2].receiver_type = names->reversed_type_qn;
|
||||
defs[2].def_module_qn = names->symbol_module_qn;
|
||||
defs[2].return_types = "void";
|
||||
defs[2].lang = language;
|
||||
|
||||
defs[3].qualified_name = names->pair_method_qn;
|
||||
defs[3].short_name = "pairOnly";
|
||||
defs[3].label = "Method";
|
||||
defs[3].receiver_type = names->pair_type_qn;
|
||||
defs[3].def_module_qn = names->symbol_module_qn;
|
||||
defs[3].return_types = "void";
|
||||
defs[3].lang = language;
|
||||
|
||||
/* Same QN and arity; only ordered argument types distinguish these. */
|
||||
defs[4].qualified_name = names->choose_qn;
|
||||
defs[4].short_name = "choose";
|
||||
defs[4].label = "Function";
|
||||
defs[4].def_module_qn = names->symbol_module_qn;
|
||||
defs[4].return_types = "Pair";
|
||||
defs[4].signature_param_types = kNumberStringParams;
|
||||
defs[4].signature_param_count = 2;
|
||||
defs[4].lang = language;
|
||||
|
||||
defs[5].qualified_name = names->choose_qn;
|
||||
defs[5].short_name = "choose";
|
||||
defs[5].label = "Function";
|
||||
defs[5].def_module_qn = names->symbol_module_qn;
|
||||
defs[5].return_types = "Reversed";
|
||||
defs[5].signature_param_types = kStringNumberParams;
|
||||
defs[5].signature_param_count = 2;
|
||||
defs[5].lang = language;
|
||||
|
||||
defs[6].qualified_name = names->run_qn;
|
||||
defs[6].short_name = "run";
|
||||
defs[6].label = "Function";
|
||||
defs[6].def_module_qn = names->caller_module_qn;
|
||||
defs[6].return_types = "void";
|
||||
defs[6].lang = language;
|
||||
}
|
||||
|
||||
static int assert_both_return_chains(const CBMResolvedCallArray *calls, const char *language_name,
|
||||
const char *fixture_name, const char *inner_strategy) {
|
||||
int inner_calls = count_resolved_suffix(calls, "choose", inner_strategy);
|
||||
bool pair = count_resolved_suffix(calls, "Pair.pairOnly", "lsp_ts_method") > 0;
|
||||
bool reversed = count_resolved_suffix(calls, "Reversed.reversedOnly", "lsp_ts_method") > 0;
|
||||
if (inner_calls < 2 || !pair || !reversed) {
|
||||
printf(" ts-overload-return: %s %s inner=%d pair=%d reversed=%d\n", language_name,
|
||||
fixture_name, inner_calls, pair, reversed);
|
||||
print_resolved_calls(calls);
|
||||
}
|
||||
return inner_calls >= 2 && pair && reversed ? 0 : 1;
|
||||
}
|
||||
|
||||
static const char kFreeFunctionSource[] = "function run(): void {\n"
|
||||
" choose(1, 'x').pairOnly();\n"
|
||||
" choose('x', 1).reversedOnly();\n"
|
||||
"}\n";
|
||||
|
||||
static int assert_free_function_overload_returns(CBMLanguage language, bool jsx_mode,
|
||||
const char *language_name) {
|
||||
static const TSOverloadFixtureNames names = {
|
||||
"repro.free",
|
||||
"repro.free",
|
||||
"repro.free.Reversed",
|
||||
"repro.free.Pair",
|
||||
"repro.free.Reversed.reversedOnly",
|
||||
"repro.free.Pair.pairOnly",
|
||||
"repro.free.choose",
|
||||
"repro.free.run",
|
||||
};
|
||||
CBMLSPDef defs[OVERLOAD_DEF_COUNT];
|
||||
init_overload_defs(defs, language, &names);
|
||||
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
CBMTypeRegistry *registry = cbm_ts_build_cross_registry(&arena, defs, OVERLOAD_DEF_COUNT);
|
||||
CBMResolvedCallArray calls = {0};
|
||||
if (registry) {
|
||||
cbm_run_ts_lsp_cross_with_registry(&arena, kFreeFunctionSource,
|
||||
(int)strlen(kFreeFunctionSource), names.caller_module_qn,
|
||||
false, jsx_mode, false, registry, defs,
|
||||
OVERLOAD_DEF_COUNT, NULL, NULL, 0, NULL, &calls);
|
||||
}
|
||||
|
||||
int rc = registry
|
||||
? assert_both_return_chains(&calls, language_name, "free-function", "lsp_ts_local")
|
||||
: 1;
|
||||
if (!registry)
|
||||
printf(" ts-overload-return: %s free-function registry build failed\n", language_name);
|
||||
cbm_arena_destroy(&arena);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static const char kNamespaceSource[] = "import * as Lib from './provider';\n"
|
||||
"function run(): void {\n"
|
||||
" Lib.choose(1, 'x').pairOnly();\n"
|
||||
" Lib.choose('x', 1).reversedOnly();\n"
|
||||
"}\n";
|
||||
|
||||
static int assert_namespace_overload_returns(CBMLanguage language, bool jsx_mode,
|
||||
const char *language_name) {
|
||||
static const TSOverloadFixtureNames names = {
|
||||
"repro.provider",
|
||||
"repro.consumer",
|
||||
"repro.provider.Reversed",
|
||||
"repro.provider.Pair",
|
||||
"repro.provider.Reversed.reversedOnly",
|
||||
"repro.provider.Pair.pairOnly",
|
||||
"repro.provider.choose",
|
||||
"repro.consumer.run",
|
||||
};
|
||||
CBMLSPDef defs[OVERLOAD_DEF_COUNT];
|
||||
init_overload_defs(defs, language, &names);
|
||||
const char *import_names[] = {"Lib"};
|
||||
const char *import_qns[] = {names.symbol_module_qn};
|
||||
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
CBMTypeRegistry *registry = cbm_ts_build_cross_registry(&arena, defs, OVERLOAD_DEF_COUNT);
|
||||
CBMResolvedCallArray calls = {0};
|
||||
if (registry) {
|
||||
cbm_run_ts_lsp_cross_with_registry(&arena, kNamespaceSource, (int)strlen(kNamespaceSource),
|
||||
names.caller_module_qn, false, jsx_mode, false, registry,
|
||||
defs, OVERLOAD_DEF_COUNT, import_names, import_qns, 1,
|
||||
NULL, &calls);
|
||||
}
|
||||
|
||||
int rc = registry ? assert_both_return_chains(&calls, language_name, "namespace-import",
|
||||
"lsp_ts_namespace")
|
||||
: 1;
|
||||
if (!registry)
|
||||
printf(" ts-overload-return: %s namespace registry build failed\n", language_name);
|
||||
cbm_arena_destroy(&arena);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static const char kExactSymbolImportSource[] = "import { choose } from './provider';\n"
|
||||
"function run(): void {\n"
|
||||
" choose(1, 'x').pairOnly();\n"
|
||||
" choose('x', 1).reversedOnly();\n"
|
||||
"}\n";
|
||||
|
||||
static int assert_exact_symbol_import_overload_returns(CBMLanguage language, bool jsx_mode,
|
||||
const char *language_name) {
|
||||
static const TSOverloadFixtureNames names = {
|
||||
"repro.exact.provider",
|
||||
"repro.exact.consumer",
|
||||
"repro.exact.provider.Reversed",
|
||||
"repro.exact.provider.Pair",
|
||||
"repro.exact.provider.Reversed.reversedOnly",
|
||||
"repro.exact.provider.Pair.pairOnly",
|
||||
"repro.exact.provider.choose",
|
||||
"repro.exact.consumer.run",
|
||||
};
|
||||
CBMLSPDef defs[OVERLOAD_DEF_COUNT];
|
||||
init_overload_defs(defs, language, &names);
|
||||
const char *import_names[] = {"choose"};
|
||||
/* Some production import maps already carry the exact exported-symbol QN.
|
||||
* The call lookup must not append the local name a second time. */
|
||||
const char *import_qns[] = {names.choose_qn};
|
||||
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
CBMTypeRegistry *registry = cbm_ts_build_cross_registry(&arena, defs, OVERLOAD_DEF_COUNT);
|
||||
CBMResolvedCallArray calls = {0};
|
||||
if (registry) {
|
||||
cbm_run_ts_lsp_cross_with_registry(
|
||||
&arena, kExactSymbolImportSource, (int)strlen(kExactSymbolImportSource),
|
||||
names.caller_module_qn, false, jsx_mode, false, registry, defs, OVERLOAD_DEF_COUNT,
|
||||
import_names, import_qns, 1, NULL, &calls);
|
||||
}
|
||||
|
||||
int chain_rc = registry ? assert_both_return_chains(&calls, language_name,
|
||||
"exact-symbol-import", "lsp_ts_import")
|
||||
: 1;
|
||||
int doubled = count_any_callee_suffix(&calls, "choose.choose");
|
||||
if (!registry || doubled != 0) {
|
||||
printf(" ts-overload-return: %s exact-symbol-import registry=%d doubled=%d\n",
|
||||
language_name, registry != NULL, doubled);
|
||||
print_resolved_calls(&calls);
|
||||
}
|
||||
cbm_arena_destroy(&arena);
|
||||
return registry && chain_rc == 0 && doubled == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
static const char kContextualCallbackOverloadSource[] =
|
||||
"function run(): void {\n"
|
||||
" choose(1, 'x', value => value.pairOnly());\n"
|
||||
" choose('x', 1, value => value.reversedOnly());\n"
|
||||
"}\n";
|
||||
|
||||
static int assert_contextual_callback_uses_selected_overload(CBMLanguage language, bool jsx_mode,
|
||||
const char *language_name) {
|
||||
static const TSOverloadFixtureNames names = {
|
||||
"repro.callback",
|
||||
"repro.callback",
|
||||
"repro.callback.Reversed",
|
||||
"repro.callback.Pair",
|
||||
"repro.callback.Reversed.reversedOnly",
|
||||
"repro.callback.Pair.pairOnly",
|
||||
"repro.callback.choose",
|
||||
"repro.callback.run",
|
||||
};
|
||||
CBMLSPDef defs[OVERLOAD_DEF_COUNT];
|
||||
init_overload_defs(defs, language, &names);
|
||||
defs[4].return_types = "void";
|
||||
defs[4].signature_param_types = kNumberStringPairCallbackParams;
|
||||
defs[4].signature_param_count = 3;
|
||||
defs[5].return_types = "void";
|
||||
defs[5].signature_param_types = kStringNumberReversedCallbackParams;
|
||||
defs[5].signature_param_count = 3;
|
||||
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
CBMTypeRegistry *registry = cbm_ts_build_cross_registry(&arena, defs, OVERLOAD_DEF_COUNT);
|
||||
CBMResolvedCallArray calls = {0};
|
||||
if (registry) {
|
||||
cbm_run_ts_lsp_cross_with_registry(&arena, kContextualCallbackOverloadSource,
|
||||
(int)strlen(kContextualCallbackOverloadSource),
|
||||
names.caller_module_qn, false, jsx_mode, false, registry,
|
||||
defs, OVERLOAD_DEF_COUNT, NULL, NULL, 0, NULL, &calls);
|
||||
}
|
||||
|
||||
int outer = count_resolved_suffix(&calls, "choose", "lsp_ts_local");
|
||||
int pair = count_resolved_suffix(&calls, "Pair.pairOnly", "lsp_ts_method");
|
||||
int reversed = count_resolved_suffix(&calls, "Reversed.reversedOnly", "lsp_ts_method");
|
||||
if (!registry || outer < 2 || pair < 1 || reversed < 1) {
|
||||
printf(" ts-overload-return: %s contextual-callback registry=%d outer=%d pair=%d "
|
||||
"reversed=%d\n",
|
||||
language_name, registry != NULL, outer, pair, reversed);
|
||||
print_resolved_calls(&calls);
|
||||
}
|
||||
cbm_arena_destroy(&arena);
|
||||
return registry && outer >= 2 && pair >= 1 && reversed >= 1 ? 0 : 1;
|
||||
}
|
||||
|
||||
static int assert_no_shadowed_return_chains(const CBMResolvedCallArray *calls,
|
||||
const char *language_name, const char *fixture_name,
|
||||
const char *forbidden_strategy) {
|
||||
int promoted = count_resolved_suffix(calls, "choose", forbidden_strategy);
|
||||
int pair = count_resolved_suffix(calls, "Pair.pairOnly", "lsp_ts_method");
|
||||
int reversed = count_resolved_suffix(calls, "Reversed.reversedOnly", "lsp_ts_method");
|
||||
if (promoted != 0 || pair != 0 || reversed != 0) {
|
||||
printf(" ts-overload-return: %s %s promoted=%d pair=%d reversed=%d\n", language_name,
|
||||
fixture_name, promoted, pair, reversed);
|
||||
print_resolved_calls(calls);
|
||||
}
|
||||
return promoted == 0 && pair == 0 && reversed == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
static const char kShadowedFreeFunctionSource[] = "function run(choose): void {\n"
|
||||
" choose(1, 'x').pairOnly();\n"
|
||||
" choose('x', 1).reversedOnly();\n"
|
||||
"}\n";
|
||||
|
||||
static int assert_parameter_shadows_free_function(CBMLanguage language, bool jsx_mode,
|
||||
const char *language_name) {
|
||||
static const TSOverloadFixtureNames names = {
|
||||
"repro.shadow.free",
|
||||
"repro.shadow.free",
|
||||
"repro.shadow.free.Reversed",
|
||||
"repro.shadow.free.Pair",
|
||||
"repro.shadow.free.Reversed.reversedOnly",
|
||||
"repro.shadow.free.Pair.pairOnly",
|
||||
"repro.shadow.free.choose",
|
||||
"repro.shadow.free.run",
|
||||
};
|
||||
CBMLSPDef defs[OVERLOAD_DEF_COUNT];
|
||||
init_overload_defs(defs, language, &names);
|
||||
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
CBMTypeRegistry *registry = cbm_ts_build_cross_registry(&arena, defs, OVERLOAD_DEF_COUNT);
|
||||
CBMResolvedCallArray calls = {0};
|
||||
if (registry) {
|
||||
cbm_run_ts_lsp_cross_with_registry(&arena, kShadowedFreeFunctionSource,
|
||||
(int)strlen(kShadowedFreeFunctionSource),
|
||||
names.caller_module_qn, false, jsx_mode, false, registry,
|
||||
defs, OVERLOAD_DEF_COUNT, NULL, NULL, 0, NULL, &calls);
|
||||
}
|
||||
|
||||
int rc = registry ? assert_no_shadowed_return_chains(&calls, language_name,
|
||||
"parameter-shadows-free-function",
|
||||
"lsp_ts_local")
|
||||
: 1;
|
||||
if (!registry)
|
||||
printf(" ts-overload-return: %s free shadow registry build failed\n", language_name);
|
||||
cbm_arena_destroy(&arena);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static const char kShadowedNamespaceSource[] = "import * as Lib from './provider';\n"
|
||||
"function run(Lib): void {\n"
|
||||
" Lib.choose(1, 'x').pairOnly();\n"
|
||||
" Lib.choose('x', 1).reversedOnly();\n"
|
||||
"}\n";
|
||||
|
||||
static int assert_parameter_shadows_namespace(CBMLanguage language, bool jsx_mode,
|
||||
const char *language_name) {
|
||||
static const TSOverloadFixtureNames names = {
|
||||
"repro.shadow.provider",
|
||||
"repro.shadow.consumer",
|
||||
"repro.shadow.provider.Reversed",
|
||||
"repro.shadow.provider.Pair",
|
||||
"repro.shadow.provider.Reversed.reversedOnly",
|
||||
"repro.shadow.provider.Pair.pairOnly",
|
||||
"repro.shadow.provider.choose",
|
||||
"repro.shadow.consumer.run",
|
||||
};
|
||||
CBMLSPDef defs[OVERLOAD_DEF_COUNT];
|
||||
init_overload_defs(defs, language, &names);
|
||||
const char *import_names[] = {"Lib"};
|
||||
const char *import_qns[] = {names.symbol_module_qn};
|
||||
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
CBMTypeRegistry *registry = cbm_ts_build_cross_registry(&arena, defs, OVERLOAD_DEF_COUNT);
|
||||
CBMResolvedCallArray calls = {0};
|
||||
if (registry) {
|
||||
cbm_run_ts_lsp_cross_with_registry(
|
||||
&arena, kShadowedNamespaceSource, (int)strlen(kShadowedNamespaceSource),
|
||||
names.caller_module_qn, false, jsx_mode, false, registry, defs, OVERLOAD_DEF_COUNT,
|
||||
import_names, import_qns, 1, NULL, &calls);
|
||||
}
|
||||
|
||||
int rc = registry ? assert_no_shadowed_return_chains(&calls, language_name,
|
||||
"parameter-shadows-namespace",
|
||||
"lsp_ts_namespace")
|
||||
: 1;
|
||||
if (!registry)
|
||||
printf(" ts-overload-return: %s namespace shadow registry build failed\n", language_name);
|
||||
cbm_arena_destroy(&arena);
|
||||
return rc;
|
||||
}
|
||||
|
||||
TEST(repro_ts_free_function_overload_returns_typescript) {
|
||||
return assert_free_function_overload_returns(CBM_LANG_TYPESCRIPT, false, "TypeScript");
|
||||
}
|
||||
|
||||
TEST(repro_ts_free_function_overload_returns_tsx) {
|
||||
return assert_free_function_overload_returns(CBM_LANG_TSX, true, "TSX");
|
||||
}
|
||||
|
||||
TEST(repro_ts_namespace_overload_returns_typescript) {
|
||||
return assert_namespace_overload_returns(CBM_LANG_TYPESCRIPT, false, "TypeScript");
|
||||
}
|
||||
|
||||
TEST(repro_ts_namespace_overload_returns_tsx) {
|
||||
return assert_namespace_overload_returns(CBM_LANG_TSX, true, "TSX");
|
||||
}
|
||||
|
||||
TEST(repro_ts_exact_symbol_import_overload_returns_typescript) {
|
||||
return assert_exact_symbol_import_overload_returns(CBM_LANG_TYPESCRIPT, false, "TypeScript");
|
||||
}
|
||||
|
||||
TEST(repro_ts_exact_symbol_import_overload_returns_tsx) {
|
||||
return assert_exact_symbol_import_overload_returns(CBM_LANG_TSX, true, "TSX");
|
||||
}
|
||||
|
||||
TEST(repro_ts_contextual_callback_uses_selected_overload_typescript) {
|
||||
return assert_contextual_callback_uses_selected_overload(CBM_LANG_TYPESCRIPT, false,
|
||||
"TypeScript");
|
||||
}
|
||||
|
||||
TEST(repro_ts_contextual_callback_uses_selected_overload_tsx) {
|
||||
return assert_contextual_callback_uses_selected_overload(CBM_LANG_TSX, true, "TSX");
|
||||
}
|
||||
|
||||
TEST(repro_ts_parameter_shadows_free_function_typescript) {
|
||||
return assert_parameter_shadows_free_function(CBM_LANG_TYPESCRIPT, false, "TypeScript");
|
||||
}
|
||||
|
||||
TEST(repro_ts_parameter_shadows_free_function_tsx) {
|
||||
return assert_parameter_shadows_free_function(CBM_LANG_TSX, true, "TSX");
|
||||
}
|
||||
|
||||
TEST(repro_ts_parameter_shadows_namespace_typescript) {
|
||||
return assert_parameter_shadows_namespace(CBM_LANG_TYPESCRIPT, false, "TypeScript");
|
||||
}
|
||||
|
||||
TEST(repro_ts_parameter_shadows_namespace_tsx) {
|
||||
return assert_parameter_shadows_namespace(CBM_LANG_TSX, true, "TSX");
|
||||
}
|
||||
|
||||
SUITE(repro_ts_overload_return_chains) {
|
||||
RUN_TEST(repro_ts_free_function_overload_returns_typescript);
|
||||
RUN_TEST(repro_ts_free_function_overload_returns_tsx);
|
||||
RUN_TEST(repro_ts_namespace_overload_returns_typescript);
|
||||
RUN_TEST(repro_ts_namespace_overload_returns_tsx);
|
||||
RUN_TEST(repro_ts_exact_symbol_import_overload_returns_typescript);
|
||||
RUN_TEST(repro_ts_exact_symbol_import_overload_returns_tsx);
|
||||
RUN_TEST(repro_ts_contextual_callback_uses_selected_overload_typescript);
|
||||
RUN_TEST(repro_ts_contextual_callback_uses_selected_overload_tsx);
|
||||
RUN_TEST(repro_ts_parameter_shadows_free_function_typescript);
|
||||
RUN_TEST(repro_ts_parameter_shadows_free_function_tsx);
|
||||
RUN_TEST(repro_ts_parameter_shadows_namespace_typescript);
|
||||
RUN_TEST(repro_ts_parameter_shadows_namespace_tsx);
|
||||
}
|
||||
+572
-16
@@ -26,7 +26,9 @@
|
||||
#include "lsp/ts_lsp.h"
|
||||
#include "lsp/go_lsp.h"
|
||||
#include "lsp/type_registry.h"
|
||||
#include "../src/pipeline/lsp_resolve.h"
|
||||
#include "arena.h"
|
||||
#include "preprocessor.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
@@ -62,6 +64,13 @@ static CBMFileResult *extract_cpp(const char *src) {
|
||||
return cbm_extract_file(src, (int)strlen(src), CBM_LANG_CPP, "test", "main.cpp", 0, NULL, NULL);
|
||||
}
|
||||
|
||||
static CBMFileResult *extract_c_family(const char *src, CBMLanguage language) {
|
||||
const char *path = language == CBM_LANG_C ? "main.c"
|
||||
: language == CBM_LANG_CUDA ? "main.cu"
|
||||
: "main.cpp";
|
||||
return cbm_extract_file(src, (int)strlen(src), language, "test", path, 0, NULL, NULL);
|
||||
}
|
||||
|
||||
TEST(clsp_simple_var_decl) {
|
||||
CBMFileResult *r = extract_c("\n"
|
||||
"struct Foo {\n"
|
||||
@@ -572,17 +581,18 @@ TEST(clsp_nocrash_template_extra_call_args) {
|
||||
}
|
||||
|
||||
TEST(clsp_nocrash_template_function_multi_param_nested_call) {
|
||||
CBMFileResult *r = extract_cpp("\n"
|
||||
"void right_aligned_text(int color, int width, const char* fmt, float value) {}\n"
|
||||
"\n"
|
||||
"template<typename T, typename R = float>\n"
|
||||
"R format_units(T value, const char*& unit) { return value; }\n"
|
||||
"\n"
|
||||
"void f() {\n"
|
||||
" const char* unit = nullptr;\n"
|
||||
" right_aligned_text(0, 0, \"%.1f\", format_units(1, unit));\n"
|
||||
"}\n"
|
||||
"");
|
||||
CBMFileResult *r = extract_cpp(
|
||||
"\n"
|
||||
"void right_aligned_text(int color, int width, const char* fmt, float value) {}\n"
|
||||
"\n"
|
||||
"template<typename T, typename R = float>\n"
|
||||
"R format_units(T value, const char*& unit) { return value; }\n"
|
||||
"\n"
|
||||
"void f() {\n"
|
||||
" const char* unit = nullptr;\n"
|
||||
" right_aligned_text(0, 0, \"%.1f\", format_units(1, unit));\n"
|
||||
"}\n"
|
||||
"");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_GTE(find_resolved(r, "f", "right_aligned_text"), 0);
|
||||
ASSERT_GTE(find_resolved(r, "f", "format_units"), 0);
|
||||
@@ -6367,7 +6377,10 @@ TEST(clsp_cross_gap_new_then_method_chain) {
|
||||
ASSERT_GTE(find_resolved(r, "test", "Service.Service"), 0);
|
||||
ASSERT_GTE(find_resolved(r, "test", "start"), 0);
|
||||
ASSERT_GTE(find_resolved(r, "test", "stop"), 0);
|
||||
ASSERT_GTE(find_resolved(r, "test", "~Service"), 0);
|
||||
/* Service declares no destructor node. Explicit delete retains its
|
||||
* semantic-only carrier, but the resolver must not fabricate an implicit
|
||||
* ~Service target that the graph cannot materialize. */
|
||||
ASSERT_EQ(find_resolved(r, "test", "~Service"), -1);
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
@@ -7292,7 +7305,10 @@ TEST(clsp_pattern_auto_ptr_from_new) {
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_GTE(find_resolved(r, "test", "draw"), 0);
|
||||
ASSERT_GTE(find_resolved(r, "test", "Widget.Widget"), 0);
|
||||
ASSERT_GTE(find_resolved(r, "test", "~Widget"), 0);
|
||||
/* Widget has only an implicit destructor, so delete cannot resolve to a
|
||||
* concrete graph target. A declared ~Widget is covered by the dedicated
|
||||
* explicit-destructor test. */
|
||||
ASSERT_EQ(find_resolved(r, "test", "~Widget"), -1);
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
@@ -15370,7 +15386,7 @@ TEST(seal_py_shared_registry_readonly) {
|
||||
const char *src = "def helper(x):\n return x + 1\n\ndef caller():\n return helper(1)\n";
|
||||
CBMResolvedCallArray out = {0};
|
||||
cbm_run_py_lsp_cross_with_registry(&arena, src, (int)strlen(src), "test.mod", reg, NULL, NULL,
|
||||
0, NULL, &out);
|
||||
0, NULL, &out, NULL);
|
||||
ASSERT_EQ(reg->func_count, fb);
|
||||
ASSERT_EQ(reg->type_count, tb);
|
||||
cbm_arena_destroy(&arena);
|
||||
@@ -15436,8 +15452,8 @@ TEST(seal_py_shared_registry_readonly_fields) {
|
||||
" def m(self):\n"
|
||||
" self.x = 1\n";
|
||||
CBMResolvedCallArray out = {0};
|
||||
cbm_run_py_lsp_cross_with_registry(&arena, src, (int)strlen(src), "test.mod", reg, NULL, NULL, 0,
|
||||
NULL, &out);
|
||||
cbm_run_py_lsp_cross_with_registry(&arena, src, (int)strlen(src), "test.mod", reg, NULL, NULL,
|
||||
0, NULL, &out, NULL);
|
||||
|
||||
/* The sealed registry entry must be untouched by resolve. */
|
||||
int count_after = 0;
|
||||
@@ -15502,7 +15518,547 @@ TEST(seal_go_shared_registry_readonly) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
static int assert_cpp_family_repeated_method_occurrences(CBMLanguage language) {
|
||||
static const char source[] = "struct Alpha { int render() { return 1; } };\n"
|
||||
"struct Bravo { int render() { return 2; } };\n"
|
||||
"int occurrence_probe() {\n"
|
||||
" Alpha alpha;\n"
|
||||
" Bravo bravo;\n"
|
||||
" return alpha.render() + bravo.render();\n"
|
||||
"}\n";
|
||||
static const char *call_texts[2] = {"alpha.render()", "bravo.render()"};
|
||||
const char *sites[2] = {strstr(source, call_texts[0]), strstr(source, call_texts[1])};
|
||||
ASSERT_NOT_NULL(sites[0]);
|
||||
ASSERT_NOT_NULL(sites[1]);
|
||||
uint32_t starts[2] = {(uint32_t)(sites[0] - source), (uint32_t)(sites[1] - source)};
|
||||
uint32_t ends[2] = {starts[0] + (uint32_t)strlen(call_texts[0]),
|
||||
starts[1] + (uint32_t)strlen(call_texts[1])};
|
||||
|
||||
CBMFileResult *result = extract_c_family(source, language);
|
||||
ASSERT_NOT_NULL(result);
|
||||
const CBMCall *calls[2] = {NULL, NULL};
|
||||
const CBMResolvedCall *semantics[2] = {NULL, NULL};
|
||||
int carrier_count = 0;
|
||||
int semantic_count = 0;
|
||||
for (int i = 0; i < result->calls.count; i++) {
|
||||
const CBMCall *call = &result->calls.items[i];
|
||||
if (!call->enclosing_func_qn || !strstr(call->enclosing_func_qn, "occurrence_probe") ||
|
||||
!call->callee_name || strcmp(cbm_lsp_bare_segment(call->callee_name), "render") != 0) {
|
||||
continue;
|
||||
}
|
||||
carrier_count++;
|
||||
for (int occurrence = 0; occurrence < 2; occurrence++) {
|
||||
if (call->site_start_byte == starts[occurrence] &&
|
||||
call->site_end_byte == ends[occurrence]) {
|
||||
calls[occurrence] = call;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < result->resolved_calls.count; i++) {
|
||||
const CBMResolvedCall *resolved = &result->resolved_calls.items[i];
|
||||
if (resolved->kind != CBM_RESOLVED_INVOCATION || resolved->confidence <= 0.0f ||
|
||||
!resolved->caller_qn || !strstr(resolved->caller_qn, "occurrence_probe") ||
|
||||
!resolved->callee_qn ||
|
||||
strcmp(cbm_lsp_bare_segment(resolved->callee_qn), "render") != 0) {
|
||||
continue;
|
||||
}
|
||||
semantic_count++;
|
||||
for (int occurrence = 0; occurrence < 2; occurrence++) {
|
||||
if (resolved->site_start_byte == starts[occurrence] &&
|
||||
resolved->site_end_byte == ends[occurrence]) {
|
||||
semantics[occurrence] = resolved;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT_EQ(carrier_count, 2);
|
||||
ASSERT_EQ(semantic_count, 2);
|
||||
ASSERT_NOT_NULL(calls[0]);
|
||||
ASSERT_NOT_NULL(calls[1]);
|
||||
ASSERT_NOT_NULL(semantics[0]);
|
||||
ASSERT_NOT_NULL(semantics[1]);
|
||||
ASSERT_NOT_NULL(strstr(semantics[0]->callee_qn, "Alpha.render"));
|
||||
ASSERT_NOT_NULL(strstr(semantics[1]->callee_qn, "Bravo.render"));
|
||||
ASSERT_TRUE(cbm_pipeline_find_lsp_resolution(&result->resolved_calls, calls[0], false) ==
|
||||
semantics[0]);
|
||||
ASSERT_TRUE(cbm_pipeline_find_lsp_resolution(&result->resolved_calls, calls[1], false) ==
|
||||
semantics[1]);
|
||||
cbm_free_result(result);
|
||||
return 0;
|
||||
}
|
||||
|
||||
TEST(clsp_cpp_repeated_same_leaf_calls_join_exact_occurrences) {
|
||||
return assert_cpp_family_repeated_method_occurrences(CBM_LANG_CPP);
|
||||
}
|
||||
|
||||
TEST(clsp_cuda_repeated_same_leaf_calls_join_exact_occurrences) {
|
||||
return assert_cpp_family_repeated_method_occurrences(CBM_LANG_CUDA);
|
||||
}
|
||||
|
||||
TEST(clsp_c_reassigned_function_pointer_calls_join_exact_occurrences) {
|
||||
static const char source[] = "static int alpha_target(void) { return 1; }\n"
|
||||
"static int bravo_target(void) { return 2; }\n"
|
||||
"int occurrence_probe(void) {\n"
|
||||
" int (*fp)(void) = alpha_target;\n"
|
||||
" int first = fp();\n"
|
||||
" fp = bravo_target;\n"
|
||||
" return first + fp();\n"
|
||||
"}\n";
|
||||
static const char call_text[] = "fp()";
|
||||
const char *first_site = strstr(source, call_text);
|
||||
const char *second_site = first_site ? strstr(first_site + 1, call_text) : NULL;
|
||||
ASSERT_NOT_NULL(first_site);
|
||||
ASSERT_NOT_NULL(second_site);
|
||||
uint32_t starts[2] = {(uint32_t)(first_site - source), (uint32_t)(second_site - source)};
|
||||
uint32_t ends[2] = {starts[0] + (uint32_t)strlen(call_text),
|
||||
starts[1] + (uint32_t)strlen(call_text)};
|
||||
|
||||
CBMFileResult *result = extract_c_family(source, CBM_LANG_C);
|
||||
ASSERT_NOT_NULL(result);
|
||||
const CBMCall *calls[2] = {NULL, NULL};
|
||||
const CBMResolvedCall *semantics[2] = {NULL, NULL};
|
||||
int carrier_count = 0;
|
||||
int semantic_count = 0;
|
||||
for (int i = 0; i < result->calls.count; i++) {
|
||||
const CBMCall *call = &result->calls.items[i];
|
||||
if (!call->enclosing_func_qn || !strstr(call->enclosing_func_qn, "occurrence_probe") ||
|
||||
!call->callee_name || strcmp(call->callee_name, "fp") != 0) {
|
||||
continue;
|
||||
}
|
||||
carrier_count++;
|
||||
for (int occurrence = 0; occurrence < 2; occurrence++) {
|
||||
if (call->site_start_byte == starts[occurrence] &&
|
||||
call->site_end_byte == ends[occurrence]) {
|
||||
calls[occurrence] = call;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < result->resolved_calls.count; i++) {
|
||||
const CBMResolvedCall *resolved = &result->resolved_calls.items[i];
|
||||
if (resolved->kind != CBM_RESOLVED_INVOCATION || resolved->confidence <= 0.0f ||
|
||||
!resolved->caller_qn || !strstr(resolved->caller_qn, "occurrence_probe") ||
|
||||
!resolved->reason || strcmp(resolved->reason, "fp") != 0) {
|
||||
continue;
|
||||
}
|
||||
semantic_count++;
|
||||
for (int occurrence = 0; occurrence < 2; occurrence++) {
|
||||
if (resolved->site_start_byte == starts[occurrence] &&
|
||||
resolved->site_end_byte == ends[occurrence]) {
|
||||
semantics[occurrence] = resolved;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT_EQ(carrier_count, 2);
|
||||
ASSERT_EQ(semantic_count, 2);
|
||||
ASSERT_NOT_NULL(calls[0]);
|
||||
ASSERT_NOT_NULL(calls[1]);
|
||||
ASSERT_NOT_NULL(semantics[0]);
|
||||
ASSERT_NOT_NULL(semantics[1]);
|
||||
ASSERT_NOT_NULL(strstr(semantics[0]->callee_qn, "alpha_target"));
|
||||
ASSERT_NOT_NULL(strstr(semantics[1]->callee_qn, "bravo_target"));
|
||||
ASSERT_TRUE(cbm_pipeline_find_lsp_resolution(&result->resolved_calls, calls[0], false) ==
|
||||
semantics[0]);
|
||||
ASSERT_TRUE(cbm_pipeline_find_lsp_resolution(&result->resolved_calls, calls[1], false) ==
|
||||
semantics[1]);
|
||||
cbm_free_result(result);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(clsp_c_nested_function_pointer_shadow_restores_outer_target) {
|
||||
static const char source[] = "static int alpha_target(void) { return 1; }\n"
|
||||
"static int bravo_target(void) { return 2; }\n"
|
||||
"int occurrence_probe(void) {\n"
|
||||
" int (*fp)(void) = alpha_target;\n"
|
||||
" int inner = 0;\n"
|
||||
" {\n"
|
||||
" int (*fp)(void) = bravo_target;\n"
|
||||
" inner = fp();\n"
|
||||
" }\n"
|
||||
" return inner + fp();\n"
|
||||
"}\n";
|
||||
const char *inner_site = strstr(source, "fp();");
|
||||
const char *outer_site = inner_site ? strstr(inner_site + 1, "fp();") : NULL;
|
||||
ASSERT_NOT_NULL(inner_site);
|
||||
ASSERT_NOT_NULL(outer_site);
|
||||
uint32_t inner_start = (uint32_t)(inner_site - source);
|
||||
uint32_t outer_start = (uint32_t)(outer_site - source);
|
||||
uint32_t call_len = (uint32_t)strlen("fp()");
|
||||
|
||||
CBMFileResult *result = extract_c_family(source, CBM_LANG_C);
|
||||
ASSERT_NOT_NULL(result);
|
||||
const CBMResolvedCall *inner_semantic = NULL;
|
||||
const CBMResolvedCall *outer_semantic = NULL;
|
||||
for (int i = 0; i < result->resolved_calls.count; i++) {
|
||||
const CBMResolvedCall *resolved = &result->resolved_calls.items[i];
|
||||
if (resolved->kind != CBM_RESOLVED_INVOCATION || !resolved->reason ||
|
||||
strcmp(resolved->reason, "fp") != 0 || !resolved->callee_qn) {
|
||||
continue;
|
||||
}
|
||||
if (resolved->site_start_byte == inner_start &&
|
||||
resolved->site_end_byte == inner_start + call_len) {
|
||||
inner_semantic = resolved;
|
||||
}
|
||||
if (resolved->site_start_byte == outer_start &&
|
||||
resolved->site_end_byte == outer_start + call_len) {
|
||||
outer_semantic = resolved;
|
||||
}
|
||||
}
|
||||
ASSERT_NOT_NULL(inner_semantic);
|
||||
ASSERT_NOT_NULL(outer_semantic);
|
||||
ASSERT_NOT_NULL(strstr(inner_semantic->callee_qn, "bravo_target"));
|
||||
ASSERT_NOT_NULL(strstr(outer_semantic->callee_qn, "alpha_target"));
|
||||
cbm_free_result(result);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(clsp_cpp_new_expression_joins_exact_occurrence) {
|
||||
static const char source[] = "struct Widget { Widget() {} };\n"
|
||||
"Widget* occurrence_probe() { return new Widget(); }\n";
|
||||
const char *site = strstr(source, "new Widget()");
|
||||
ASSERT_NOT_NULL(site);
|
||||
uint32_t start = (uint32_t)(site - source);
|
||||
uint32_t end = start + (uint32_t)strlen("new Widget()");
|
||||
|
||||
CBMFileResult *result = extract_c_family(source, CBM_LANG_CPP);
|
||||
ASSERT_NOT_NULL(result);
|
||||
const CBMCall *carrier = NULL;
|
||||
const CBMResolvedCall *semantic = NULL;
|
||||
for (int i = 0; i < result->calls.count; i++) {
|
||||
const CBMCall *call = &result->calls.items[i];
|
||||
if (call->site_start_byte == start && call->site_end_byte == end) {
|
||||
carrier = call;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < result->resolved_calls.count; i++) {
|
||||
const CBMResolvedCall *resolved = &result->resolved_calls.items[i];
|
||||
if (resolved->kind == CBM_RESOLVED_INVOCATION && resolved->callee_qn &&
|
||||
strstr(resolved->callee_qn, "Widget.Widget") && resolved->site_start_byte == start &&
|
||||
resolved->site_end_byte == end) {
|
||||
semantic = resolved;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ASSERT_NOT_NULL(carrier);
|
||||
ASSERT_NOT_NULL(semantic);
|
||||
ASSERT_TRUE(cbm_pipeline_find_lsp_resolution(&result->resolved_calls, carrier, false) ==
|
||||
semantic);
|
||||
cbm_free_result(result);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(clsp_c_global_initializer_does_not_inherit_previous_function_caller) {
|
||||
static const char source[] = "int helper(void) { return 1; }\n"
|
||||
"void previous(void) {}\n"
|
||||
"int global_value = helper();\n";
|
||||
const char *site = strrchr(source, 'h');
|
||||
ASSERT_NOT_NULL(site);
|
||||
ASSERT_STR_EQ(site, "helper();\n");
|
||||
uint32_t start = (uint32_t)(site - source);
|
||||
uint32_t end = start + (uint32_t)strlen("helper()");
|
||||
|
||||
CBMFileResult *result = extract_c_family(source, CBM_LANG_C);
|
||||
ASSERT_NOT_NULL(result);
|
||||
const CBMCall *carrier = NULL;
|
||||
const CBMResolvedCall *semantic = NULL;
|
||||
for (int i = 0; i < result->calls.count; i++) {
|
||||
const CBMCall *call = &result->calls.items[i];
|
||||
if (call->site_start_byte == start && call->site_end_byte == end) {
|
||||
carrier = call;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < result->resolved_calls.count; i++) {
|
||||
const CBMResolvedCall *resolved = &result->resolved_calls.items[i];
|
||||
if (resolved->kind == CBM_RESOLVED_INVOCATION && resolved->callee_qn &&
|
||||
strstr(resolved->callee_qn, "helper") && resolved->site_start_byte == start &&
|
||||
resolved->site_end_byte == end) {
|
||||
semantic = resolved;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ASSERT_NOT_NULL(carrier);
|
||||
ASSERT_NOT_NULL(semantic);
|
||||
ASSERT_STR_EQ(semantic->caller_qn, carrier->enclosing_func_qn);
|
||||
ASSERT_TRUE(cbm_pipeline_find_lsp_resolution(&result->resolved_calls, carrier, false) ==
|
||||
semantic);
|
||||
cbm_free_result(result);
|
||||
PASS();
|
||||
}
|
||||
|
||||
static int assert_cpp_family_preprocessed_collision_isolated(CBMLanguage language) {
|
||||
/* simplecpp removes the 41-byte comment payload and normalizes tokens. As
|
||||
* verified by the non-vacuity assertions below, raw alpha.render and the
|
||||
* macro-hidden expanded bravo.render both land at numeric span 182:200,
|
||||
* even though those offsets refer to different source texts. */
|
||||
static const char source[] =
|
||||
"struct Alpha { int render(){return 1;} };\n"
|
||||
"struct Bravo { int render(){return 2;} };\n"
|
||||
"int occurrence_probe(){\n"
|
||||
" Alpha alpha; Bravo bravo;\n"
|
||||
" /*xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx*/ alpha . render ( );\n"
|
||||
"#define HIDDEN_RENDER(x) x.render()\n"
|
||||
" return HIDDEN_RENDER(bravo);\n"
|
||||
"}\n";
|
||||
const uint32_t collision_start = 182;
|
||||
const uint32_t collision_end = 200;
|
||||
ASSERT_EQ((uint32_t)(strstr(source, "alpha . render ( )") - source), collision_start);
|
||||
|
||||
CBMFileResult *result = extract_c_family(source, language);
|
||||
ASSERT_NOT_NULL(result);
|
||||
const CBMCall *raw_alpha = NULL;
|
||||
const CBMCall *expanded_bravo = NULL;
|
||||
int colliding_carriers = 0;
|
||||
for (int i = 0; i < result->calls.count; i++) {
|
||||
const CBMCall *call = &result->calls.items[i];
|
||||
if (!call->enclosing_func_qn || !strstr(call->enclosing_func_qn, "occurrence_probe") ||
|
||||
call->site_start_byte != collision_start || call->site_end_byte != collision_end ||
|
||||
!call->callee_name || !strstr(call->callee_name, "render")) {
|
||||
continue;
|
||||
}
|
||||
colliding_carriers++;
|
||||
if (strstr(call->callee_name, "alpha")) {
|
||||
raw_alpha = call;
|
||||
}
|
||||
if (strstr(call->callee_name, "bravo")) {
|
||||
expanded_bravo = call;
|
||||
}
|
||||
}
|
||||
ASSERT_EQ(colliding_carriers, 2);
|
||||
ASSERT_NOT_NULL(raw_alpha);
|
||||
ASSERT_NOT_NULL(expanded_bravo);
|
||||
|
||||
const CBMResolvedCall *raw_alpha_semantic = NULL;
|
||||
const CBMResolvedCall *expanded_bravo_semantic = NULL;
|
||||
for (int i = 0; i < result->resolved_calls.count; i++) {
|
||||
const CBMResolvedCall *resolved = &result->resolved_calls.items[i];
|
||||
if (resolved->kind != CBM_RESOLVED_INVOCATION || resolved->confidence <= 0.0f ||
|
||||
resolved->site_start_byte != collision_start ||
|
||||
resolved->site_end_byte != collision_end || !resolved->callee_qn) {
|
||||
continue;
|
||||
}
|
||||
if (strstr(resolved->callee_qn, "Alpha.render")) {
|
||||
raw_alpha_semantic = resolved;
|
||||
}
|
||||
if (strstr(resolved->callee_qn, "Bravo.render")) {
|
||||
expanded_bravo_semantic = resolved;
|
||||
}
|
||||
}
|
||||
ASSERT_NOT_NULL(raw_alpha_semantic);
|
||||
ASSERT_NOT_NULL(expanded_bravo_semantic);
|
||||
ASSERT_TRUE(cbm_pipeline_find_lsp_resolution(&result->resolved_calls, raw_alpha, false) ==
|
||||
raw_alpha_semantic);
|
||||
ASSERT_TRUE(cbm_pipeline_find_lsp_resolution(&result->resolved_calls, expanded_bravo, false) ==
|
||||
expanded_bravo_semantic);
|
||||
cbm_free_result(result);
|
||||
return 0;
|
||||
}
|
||||
|
||||
TEST(clsp_cpp_preprocessed_coordinate_collision_isolated) {
|
||||
return assert_cpp_family_preprocessed_collision_isolated(CBM_LANG_CPP);
|
||||
}
|
||||
|
||||
TEST(clsp_cuda_preprocessed_coordinate_collision_isolated) {
|
||||
return assert_cpp_family_preprocessed_collision_isolated(CBM_LANG_CUDA);
|
||||
}
|
||||
|
||||
TEST(clsp_c_preprocessed_coordinate_collision_isolated) {
|
||||
/* The raw fp call and macro-hidden expanded fp call both occupy 206:212,
|
||||
* but data flow resolves them to different targets. */
|
||||
static const char source[] =
|
||||
"static int alpha_target(void){return 1;}\n"
|
||||
"static int bravo_target(void){return 2;}\n"
|
||||
"int occurrence_probe(void){\n"
|
||||
" int (*fp)(void) = alpha_target;\n"
|
||||
" /*xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx*/ fp ( );\n"
|
||||
"#define HIDDEN_FP_CALL() (fp = bravo_target, fp())\n"
|
||||
" return HIDDEN_FP_CALL();\n"
|
||||
"}\n";
|
||||
const uint32_t collision_start = 206;
|
||||
const uint32_t collision_end = 212;
|
||||
const char *raw_probe = strstr(source, "occurrence_probe");
|
||||
ASSERT_NOT_NULL(raw_probe);
|
||||
ASSERT_EQ((uint32_t)(strstr(raw_probe, "fp ( )") - source), collision_start);
|
||||
|
||||
CBMFileResult *result = extract_c_family(source, CBM_LANG_C);
|
||||
ASSERT_NOT_NULL(result);
|
||||
const CBMCall *colliding_calls[2] = {NULL, NULL};
|
||||
int colliding_carriers = 0;
|
||||
for (int i = 0; i < result->calls.count; i++) {
|
||||
const CBMCall *call = &result->calls.items[i];
|
||||
if (call->enclosing_func_qn && strstr(call->enclosing_func_qn, "occurrence_probe") &&
|
||||
call->callee_name && strcmp(call->callee_name, "fp") == 0 &&
|
||||
call->site_start_byte == collision_start && call->site_end_byte == collision_end) {
|
||||
if (colliding_carriers < 2) {
|
||||
colliding_calls[colliding_carriers] = call;
|
||||
}
|
||||
colliding_carriers++;
|
||||
}
|
||||
}
|
||||
ASSERT_EQ(colliding_carriers, 2);
|
||||
ASSERT_NOT_NULL(colliding_calls[0]);
|
||||
ASSERT_NOT_NULL(colliding_calls[1]);
|
||||
|
||||
const CBMResolvedCall *alpha_semantic = NULL;
|
||||
const CBMResolvedCall *bravo_semantic = NULL;
|
||||
for (int i = 0; i < result->resolved_calls.count; i++) {
|
||||
const CBMResolvedCall *resolved = &result->resolved_calls.items[i];
|
||||
if (resolved->kind != CBM_RESOLVED_INVOCATION || resolved->confidence <= 0.0f ||
|
||||
resolved->site_start_byte != collision_start ||
|
||||
resolved->site_end_byte != collision_end || !resolved->callee_qn || !resolved->reason ||
|
||||
strcmp(resolved->reason, "fp") != 0) {
|
||||
continue;
|
||||
}
|
||||
if (strstr(resolved->callee_qn, "alpha_target")) {
|
||||
alpha_semantic = resolved;
|
||||
}
|
||||
if (strstr(resolved->callee_qn, "bravo_target")) {
|
||||
bravo_semantic = resolved;
|
||||
}
|
||||
}
|
||||
ASSERT_NOT_NULL(alpha_semantic);
|
||||
ASSERT_NOT_NULL(bravo_semantic);
|
||||
const CBMResolvedCall *first_join =
|
||||
cbm_pipeline_find_lsp_resolution(&result->resolved_calls, colliding_calls[0], false);
|
||||
const CBMResolvedCall *second_join =
|
||||
cbm_pipeline_find_lsp_resolution(&result->resolved_calls, colliding_calls[1], false);
|
||||
ASSERT_TRUE((first_join == alpha_semantic && second_join == bravo_semantic) ||
|
||||
(first_join == bravo_semantic && second_join == alpha_semantic));
|
||||
cbm_free_result(result);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(clsp_preprocessed_destructor_rewrite_respects_origin_during_rewrite) {
|
||||
/* Calibrate the comment padding against simplecpp itself: comments vanish
|
||||
* from the expanded buffer, so one padding length makes the raw
|
||||
* `delete alpha` and macro-expanded `delete bravo` occupy the same numeric
|
||||
* byte span. The names intentionally have equal length.
|
||||
*
|
||||
* The raw destructor carrier is rewritten during the first C-LSP walk. On
|
||||
* the second walk, a preprocessed carrier must be stamped PREPROCESSED
|
||||
* before c_rewrite_proved_destructor_candidates compares it with the
|
||||
* already-present raw semantic rows. Otherwise the earlier raw ~Alpha row
|
||||
* wins this colliding occurrence and rewrites the expanded carrier to the
|
||||
* wrong destructor. */
|
||||
static const char format[] = "struct Alpha { ~Alpha(){} };\n"
|
||||
"struct Bravo { ~Bravo(){} };\n"
|
||||
"void occurrence_probe(){\n"
|
||||
" Alpha* alpha = new Alpha(); Bravo* bravo = new Bravo();\n"
|
||||
" /*%s*/ delete alpha;\n"
|
||||
"#define HIDDEN_DELETE(p) delete p\n"
|
||||
" HIDDEN_DELETE(bravo);\n"
|
||||
"}\n";
|
||||
char padding[1024] = {0};
|
||||
char source[4096];
|
||||
int written = snprintf(source, sizeof(source), format, padding);
|
||||
ASSERT_GT(written, 0);
|
||||
ASSERT_LT((size_t)written, sizeof(source));
|
||||
|
||||
char *expanded = cbm_preprocess(source, written, "main.cpp", NULL, NULL, true);
|
||||
ASSERT_NOT_NULL(expanded);
|
||||
const char *raw_alpha = strstr(source, "delete alpha");
|
||||
const char *expanded_bravo = strstr(expanded, "delete bravo");
|
||||
ASSERT_NOT_NULL(raw_alpha);
|
||||
ASSERT_NOT_NULL(expanded_bravo);
|
||||
ptrdiff_t raw_offset = raw_alpha - source;
|
||||
ptrdiff_t expanded_offset = expanded_bravo - expanded;
|
||||
cbm_preprocess_free(expanded);
|
||||
ASSERT_GTE(expanded_offset, raw_offset);
|
||||
size_t padding_length = (size_t)(expanded_offset - raw_offset);
|
||||
ASSERT_LT(padding_length, sizeof(padding));
|
||||
|
||||
memset(padding, 'x', padding_length);
|
||||
padding[padding_length] = '\0';
|
||||
written = snprintf(source, sizeof(source), format, padding);
|
||||
ASSERT_GT(written, 0);
|
||||
ASSERT_LT((size_t)written, sizeof(source));
|
||||
|
||||
expanded = cbm_preprocess(source, written, "main.cpp", NULL, NULL, true);
|
||||
ASSERT_NOT_NULL(expanded);
|
||||
raw_alpha = strstr(source, "delete alpha");
|
||||
expanded_bravo = strstr(expanded, "delete bravo");
|
||||
ASSERT_NOT_NULL(raw_alpha);
|
||||
ASSERT_NOT_NULL(expanded_bravo);
|
||||
const uint32_t collision_start = (uint32_t)(raw_alpha - source);
|
||||
ASSERT_EQ((uint32_t)(expanded_bravo - expanded), collision_start);
|
||||
const uint32_t collision_end = collision_start + (uint32_t)strlen("delete alpha");
|
||||
cbm_preprocess_free(expanded);
|
||||
|
||||
CBMFileResult *result = extract_cpp(source);
|
||||
ASSERT_NOT_NULL(result);
|
||||
const CBMCall *raw_carrier = NULL;
|
||||
const CBMCall *preprocessed_carrier = NULL;
|
||||
const CBMResolvedCall *raw_semantic = NULL;
|
||||
const CBMResolvedCall *preprocessed_semantic = NULL;
|
||||
for (int i = 0; i < result->calls.count; i++) {
|
||||
const CBMCall *call = &result->calls.items[i];
|
||||
if (!call->requires_lsp_resolution || call->site_start_byte != collision_start ||
|
||||
call->site_end_byte != collision_end) {
|
||||
continue;
|
||||
}
|
||||
if (call->source_origin == CBM_SOURCE_ORIGIN_RAW) {
|
||||
raw_carrier = call;
|
||||
} else if (call->source_origin == CBM_SOURCE_ORIGIN_PREPROCESSED) {
|
||||
preprocessed_carrier = call;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < result->resolved_calls.count; i++) {
|
||||
const CBMResolvedCall *resolved = &result->resolved_calls.items[i];
|
||||
if (resolved->kind != CBM_RESOLVED_INVOCATION || !resolved->strategy ||
|
||||
strcmp(resolved->strategy, "lsp_destructor") != 0 || !resolved->callee_qn ||
|
||||
resolved->site_start_byte != collision_start ||
|
||||
resolved->site_end_byte != collision_end) {
|
||||
continue;
|
||||
}
|
||||
if (resolved->source_origin == CBM_SOURCE_ORIGIN_RAW &&
|
||||
strstr(resolved->callee_qn, "~Alpha")) {
|
||||
raw_semantic = resolved;
|
||||
} else if (resolved->source_origin == CBM_SOURCE_ORIGIN_PREPROCESSED &&
|
||||
strstr(resolved->callee_qn, "~Bravo")) {
|
||||
preprocessed_semantic = resolved;
|
||||
}
|
||||
}
|
||||
ASSERT_NOT_NULL(raw_carrier);
|
||||
ASSERT_NOT_NULL(preprocessed_carrier);
|
||||
ASSERT_NOT_NULL(raw_semantic);
|
||||
ASSERT_NOT_NULL(preprocessed_semantic);
|
||||
|
||||
bool raw_name_ok = raw_carrier->callee_name &&
|
||||
strcmp(cbm_lsp_bare_segment(raw_carrier->callee_name), "~Alpha") == 0;
|
||||
bool preprocessed_name_ok =
|
||||
preprocessed_carrier->callee_name &&
|
||||
strcmp(cbm_lsp_bare_segment(preprocessed_carrier->callee_name), "~Bravo") == 0;
|
||||
bool raw_join_ok = cbm_pipeline_find_lsp_resolution(&result->resolved_calls, raw_carrier,
|
||||
false) == raw_semantic;
|
||||
bool preprocessed_join_ok =
|
||||
cbm_pipeline_find_lsp_resolution(&result->resolved_calls, preprocessed_carrier, false) ==
|
||||
preprocessed_semantic;
|
||||
if (!raw_name_ok || !preprocessed_name_ok || !raw_join_ok || !preprocessed_join_ok) {
|
||||
printf(" destructor origin rewrite diagnostic: raw=%s/%d preprocessed=%s/%d\n",
|
||||
raw_carrier->callee_name ? raw_carrier->callee_name : "<null>", raw_join_ok,
|
||||
preprocessed_carrier->callee_name ? preprocessed_carrier->callee_name : "<null>",
|
||||
preprocessed_join_ok);
|
||||
}
|
||||
cbm_free_result(result);
|
||||
ASSERT_TRUE(raw_name_ok);
|
||||
ASSERT_TRUE(preprocessed_name_ok);
|
||||
ASSERT_TRUE(raw_join_ok);
|
||||
ASSERT_TRUE(preprocessed_join_ok);
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(c_lsp) {
|
||||
RUN_TEST(clsp_c_reassigned_function_pointer_calls_join_exact_occurrences);
|
||||
RUN_TEST(clsp_c_nested_function_pointer_shadow_restores_outer_target);
|
||||
RUN_TEST(clsp_cpp_repeated_same_leaf_calls_join_exact_occurrences);
|
||||
RUN_TEST(clsp_cuda_repeated_same_leaf_calls_join_exact_occurrences);
|
||||
RUN_TEST(clsp_cpp_new_expression_joins_exact_occurrence);
|
||||
RUN_TEST(clsp_c_global_initializer_does_not_inherit_previous_function_caller);
|
||||
RUN_TEST(clsp_c_preprocessed_coordinate_collision_isolated);
|
||||
RUN_TEST(clsp_cpp_preprocessed_coordinate_collision_isolated);
|
||||
RUN_TEST(clsp_cuda_preprocessed_coordinate_collision_isolated);
|
||||
RUN_TEST(clsp_preprocessed_destructor_rewrite_respects_origin_during_rewrite);
|
||||
RUN_TEST(clsp_tier2_shared_registry_readonly_c);
|
||||
RUN_TEST(clsp_tier2_shared_registry_readonly_cpp);
|
||||
RUN_TEST(seal_py_shared_registry_readonly);
|
||||
|
||||
@@ -0,0 +1,442 @@
|
||||
/* Production-graph contracts for callable values. These tests intentionally
|
||||
* exercise the full MCP indexing path rather than only raw extraction/LSP rows. */
|
||||
#include "test_framework.h"
|
||||
#include "repro_harness.h"
|
||||
|
||||
#include <store/store.h>
|
||||
|
||||
#include <string.h>
|
||||
|
||||
static int crc_edge_count(cbm_store_t *store, const char *project, const char *type,
|
||||
const char *source_name, const char *target_label,
|
||||
const char *target_name) {
|
||||
cbm_edge_t *edges = NULL;
|
||||
int edge_count = 0;
|
||||
if (cbm_store_find_edges_by_type(store, project, type, &edges, &edge_count) != CBM_STORE_OK) {
|
||||
return -1;
|
||||
}
|
||||
int matches = 0;
|
||||
for (int i = 0; i < edge_count; i++) {
|
||||
cbm_node_t source = {0};
|
||||
cbm_node_t target = {0};
|
||||
bool source_ok =
|
||||
cbm_store_find_node_by_id(store, edges[i].source_id, &source) == CBM_STORE_OK;
|
||||
bool target_ok =
|
||||
cbm_store_find_node_by_id(store, edges[i].target_id, &target) == CBM_STORE_OK;
|
||||
if (source_ok && target_ok && source.name && target.name &&
|
||||
strcmp(source.name, source_name) == 0 && strcmp(target.name, target_name) == 0 &&
|
||||
(!target_label || (target.label && strcmp(target.label, target_label) == 0))) {
|
||||
matches++;
|
||||
}
|
||||
cbm_node_free_fields(&source);
|
||||
cbm_node_free_fields(&target);
|
||||
}
|
||||
cbm_store_free_edges(edges, edge_count);
|
||||
return matches;
|
||||
}
|
||||
|
||||
static int crc_global_handler_edge_count(cbm_store_t *store, const char *project,
|
||||
const char *caller) {
|
||||
int references =
|
||||
crc_edge_count(store, project, "CALL_REFERENCE", caller, "Function", "handler");
|
||||
int calls = crc_edge_count(store, project, "CALLS", caller, "Function", "handler");
|
||||
int usages = crc_edge_count(store, project, "USAGE", caller, "Function", "handler");
|
||||
return references < 0 || calls < 0 || usages < 0 ? -1 : references + calls + usages;
|
||||
}
|
||||
|
||||
static int crc_function_count(cbm_store_t *store, const char *project, const char *name) {
|
||||
cbm_node_t *nodes = NULL;
|
||||
int node_count = 0;
|
||||
if (cbm_store_find_nodes_by_name(store, project, name, &nodes, &node_count) != CBM_STORE_OK) {
|
||||
return -1;
|
||||
}
|
||||
int functions = 0;
|
||||
for (int i = 0; i < node_count; i++) {
|
||||
if (nodes[i].label && strcmp(nodes[i].label, "Function") == 0) {
|
||||
functions++;
|
||||
}
|
||||
}
|
||||
cbm_store_free_nodes(nodes, node_count);
|
||||
return functions;
|
||||
}
|
||||
|
||||
TEST(call_reference_typescript_direct_argument_is_exact) {
|
||||
static const char source[] = "function handler(): void {}\n"
|
||||
"function register(callback: () => void): void {}\n"
|
||||
"function bootstrap(): void { register(handler); }\n";
|
||||
RProj project;
|
||||
cbm_store_t *store = rh_index(&project, "main.ts", source);
|
||||
ASSERT_NOT_NULL(store);
|
||||
int registrar =
|
||||
crc_edge_count(store, project.project, "CALLS", "bootstrap", "Function", "register");
|
||||
int reference = crc_edge_count(store, project.project, "CALL_REFERENCE", "bootstrap",
|
||||
"Function", "handler");
|
||||
int usage = crc_edge_count(store, project.project, "USAGE", "bootstrap", "Function", "handler");
|
||||
int fabricated_call =
|
||||
crc_edge_count(store, project.project, "CALLS", "bootstrap", "Function", "handler");
|
||||
rh_cleanup(&project, store);
|
||||
ASSERT_EQ(registrar, 1);
|
||||
ASSERT_EQ(reference, 1);
|
||||
ASSERT_EQ(usage, 0);
|
||||
ASSERT_EQ(fabricated_call, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(call_reference_kotlin_alias_argument_is_exact) {
|
||||
static const char source[] = "fun handler(): Unit {}\n"
|
||||
"fun accept(callback: () -> Unit): Unit {}\n"
|
||||
"val callback: () -> Unit = ::handler\n"
|
||||
"fun argument(): Unit {\n"
|
||||
" accept(callback)\n"
|
||||
"}\n";
|
||||
RProj project;
|
||||
cbm_store_t *store = rh_index(&project, "AliasArgument.kt", source);
|
||||
ASSERT_NOT_NULL(store);
|
||||
int reference =
|
||||
crc_edge_count(store, project.project, "CALL_REFERENCE", "argument", "Function", "handler");
|
||||
int usage = crc_edge_count(store, project.project, "USAGE", "argument", "Function", "handler");
|
||||
int fabricated_call =
|
||||
crc_edge_count(store, project.project, "CALLS", "argument", "Function", "handler");
|
||||
rh_cleanup(&project, store);
|
||||
ASSERT_EQ(reference, 1);
|
||||
ASSERT_EQ(usage, 0);
|
||||
ASSERT_EQ(fabricated_call, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(call_reference_python_use_before_local_shadow_is_not_global) {
|
||||
static const char source[] = "def handler():\n"
|
||||
" pass\n"
|
||||
"def accept(callback):\n"
|
||||
" pass\n"
|
||||
"def caller():\n"
|
||||
" accept(handler)\n"
|
||||
" handler = lambda: None\n";
|
||||
RProj project;
|
||||
cbm_store_t *store = rh_index(&project, "shadow.py", source);
|
||||
ASSERT_NOT_NULL(store);
|
||||
int registrar = crc_edge_count(store, project.project, "CALLS", "caller", "Function", "accept");
|
||||
int handler_nodes = crc_function_count(store, project.project, "handler");
|
||||
int global_edges = crc_global_handler_edge_count(store, project.project, "caller");
|
||||
rh_cleanup(&project, store);
|
||||
ASSERT_EQ(registrar, 1);
|
||||
ASSERT_GTE(handler_nodes, 1);
|
||||
ASSERT_EQ(global_edges, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(call_reference_javascript_use_before_local_shadow_is_not_global) {
|
||||
static const char source[] = "function handler() {}\n"
|
||||
"function accept(callback) {}\n"
|
||||
"function caller() {\n"
|
||||
" accept(handler);\n"
|
||||
" const handler = () => {};\n"
|
||||
"}\n";
|
||||
RProj project;
|
||||
cbm_store_t *store = rh_index(&project, "shadow.js", source);
|
||||
ASSERT_NOT_NULL(store);
|
||||
int registrar = crc_edge_count(store, project.project, "CALLS", "caller", "Function", "accept");
|
||||
int handler_nodes = crc_function_count(store, project.project, "handler");
|
||||
int global_edges = crc_global_handler_edge_count(store, project.project, "caller");
|
||||
rh_cleanup(&project, store);
|
||||
ASSERT_EQ(registrar, 1);
|
||||
ASSERT_GTE(handler_nodes, 1);
|
||||
ASSERT_EQ(global_edges, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(call_reference_typescript_use_before_local_shadow_is_not_global) {
|
||||
static const char source[] = "function handler(): void {}\n"
|
||||
"function accept(callback: () => void): void {}\n"
|
||||
"function caller(): void {\n"
|
||||
" accept(handler);\n"
|
||||
" const handler = (): void => {};\n"
|
||||
"}\n";
|
||||
RProj project;
|
||||
cbm_store_t *store = rh_index(&project, "shadow.ts", source);
|
||||
ASSERT_NOT_NULL(store);
|
||||
int registrar = crc_edge_count(store, project.project, "CALLS", "caller", "Function", "accept");
|
||||
int handler_nodes = crc_function_count(store, project.project, "handler");
|
||||
int global_edges = crc_global_handler_edge_count(store, project.project, "caller");
|
||||
rh_cleanup(&project, store);
|
||||
ASSERT_EQ(registrar, 1);
|
||||
ASSERT_GTE(handler_nodes, 1);
|
||||
ASSERT_EQ(global_edges, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(call_reference_python_keyword_argument_is_exact) {
|
||||
static const char source[] = "def handler():\n"
|
||||
" pass\n"
|
||||
"def accept(callback=None):\n"
|
||||
" pass\n"
|
||||
"def argument():\n"
|
||||
" accept(callback=handler)\n";
|
||||
RProj project;
|
||||
cbm_store_t *store = rh_index(&project, "keyword.py", source);
|
||||
ASSERT_NOT_NULL(store);
|
||||
int reference =
|
||||
crc_edge_count(store, project.project, "CALL_REFERENCE", "argument", "Function", "handler");
|
||||
int usage = crc_edge_count(store, project.project, "USAGE", "argument", "Function", "handler");
|
||||
int fabricated_call =
|
||||
crc_edge_count(store, project.project, "CALLS", "argument", "Function", "handler");
|
||||
rh_cleanup(&project, store);
|
||||
ASSERT_EQ(reference, 1);
|
||||
ASSERT_EQ(usage, 0);
|
||||
ASSERT_EQ(fabricated_call, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(call_reference_python_bound_method_argument_is_exact) {
|
||||
static const char source[] = "class Service:\n"
|
||||
" def handler(self):\n"
|
||||
" pass\n"
|
||||
"def accept(callback):\n"
|
||||
" pass\n"
|
||||
"def argument(service: Service):\n"
|
||||
" accept(service.handler)\n";
|
||||
RProj project;
|
||||
cbm_store_t *store = rh_index(&project, "bound.py", source);
|
||||
ASSERT_NOT_NULL(store);
|
||||
int reference =
|
||||
crc_edge_count(store, project.project, "CALL_REFERENCE", "argument", "Method", "handler");
|
||||
int usage = crc_edge_count(store, project.project, "USAGE", "argument", "Method", "handler");
|
||||
int fabricated_call =
|
||||
crc_edge_count(store, project.project, "CALLS", "argument", "Method", "handler");
|
||||
rh_cleanup(&project, store);
|
||||
ASSERT_EQ(reference, 1);
|
||||
ASSERT_EQ(usage, 0);
|
||||
ASSERT_EQ(fabricated_call, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(call_reference_python_parenthesized_bound_method_is_exact) {
|
||||
static const char source[] = "class Service:\n"
|
||||
" def handler(self):\n"
|
||||
" pass\n"
|
||||
"def accept(callback):\n"
|
||||
" pass\n"
|
||||
"def argument(service: Service):\n"
|
||||
" accept((service.handler))\n";
|
||||
RProj project;
|
||||
cbm_store_t *store = rh_index(&project, "parenthesized.py", source);
|
||||
ASSERT_NOT_NULL(store);
|
||||
int reference =
|
||||
crc_edge_count(store, project.project, "CALL_REFERENCE", "argument", "Method", "handler");
|
||||
int usage = crc_edge_count(store, project.project, "USAGE", "argument", "Method", "handler");
|
||||
int fabricated_call =
|
||||
crc_edge_count(store, project.project, "CALLS", "argument", "Method", "handler");
|
||||
rh_cleanup(&project, store);
|
||||
ASSERT_EQ(reference, 1);
|
||||
ASSERT_EQ(usage, 0);
|
||||
ASSERT_EQ(fabricated_call, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(call_reference_python_instance_field_shadow_is_not_method) {
|
||||
static const char source[] = "class Service:\n"
|
||||
" def __init__(self):\n"
|
||||
" self.handler = 42\n"
|
||||
" def handler(self):\n"
|
||||
" pass\n"
|
||||
"def accept(callback):\n"
|
||||
" pass\n"
|
||||
"def argument(service: Service):\n"
|
||||
" accept(service.handler)\n";
|
||||
RProj project;
|
||||
cbm_store_t *store = rh_index(&project, "field_shadow.py", source);
|
||||
ASSERT_NOT_NULL(store);
|
||||
int method_reference =
|
||||
crc_edge_count(store, project.project, "CALL_REFERENCE", "argument", "Method", "handler");
|
||||
int any_reference =
|
||||
crc_edge_count(store, project.project, "CALL_REFERENCE", "argument", NULL, "handler");
|
||||
int usage = crc_edge_count(store, project.project, "USAGE", "argument", NULL, "handler");
|
||||
int fabricated_call =
|
||||
crc_edge_count(store, project.project, "CALLS", "argument", NULL, "handler");
|
||||
rh_cleanup(&project, store);
|
||||
ASSERT_EQ(method_reference, 0);
|
||||
ASSERT_EQ(any_reference, 0);
|
||||
ASSERT_EQ(usage, 1);
|
||||
ASSERT_EQ(fabricated_call, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(call_reference_python_property_value_stays_usage) {
|
||||
static const char source[] = "class Service:\n"
|
||||
" @property\n"
|
||||
" def handler(self) -> int:\n"
|
||||
" return 42\n"
|
||||
"def accept(callback):\n"
|
||||
" pass\n"
|
||||
"def argument(service: Service):\n"
|
||||
" accept(service.handler)\n";
|
||||
RProj project;
|
||||
cbm_store_t *store = rh_index(&project, "property.py", source);
|
||||
ASSERT_NOT_NULL(store);
|
||||
int accept_call =
|
||||
crc_edge_count(store, project.project, "CALLS", "argument", "Function", "accept");
|
||||
int reference =
|
||||
crc_edge_count(store, project.project, "CALL_REFERENCE", "argument", NULL, "handler");
|
||||
int usage = crc_edge_count(store, project.project, "USAGE", "argument", "Method", "handler");
|
||||
int fabricated_call =
|
||||
crc_edge_count(store, project.project, "CALLS", "argument", NULL, "handler");
|
||||
rh_cleanup(&project, store);
|
||||
ASSERT_EQ(accept_call, 1);
|
||||
ASSERT_EQ(reference, 0);
|
||||
ASSERT_EQ(usage, 1);
|
||||
ASSERT_EQ(fabricated_call, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(call_reference_python_unknown_decorated_value_stays_usage) {
|
||||
static const char source[] = "def replace_with_value(function):\n"
|
||||
" return 42\n"
|
||||
"class Service:\n"
|
||||
" @replace_with_value\n"
|
||||
" def handler(self):\n"
|
||||
" pass\n"
|
||||
"def accept(callback):\n"
|
||||
" pass\n"
|
||||
"def argument(service: Service):\n"
|
||||
" accept(service.handler)\n";
|
||||
RProj project;
|
||||
cbm_store_t *store = rh_index(&project, "unknown_decorator.py", source);
|
||||
ASSERT_NOT_NULL(store);
|
||||
int accept_call =
|
||||
crc_edge_count(store, project.project, "CALLS", "argument", "Function", "accept");
|
||||
int reference =
|
||||
crc_edge_count(store, project.project, "CALL_REFERENCE", "argument", NULL, "handler");
|
||||
int usage = crc_edge_count(store, project.project, "USAGE", "argument", "Method", "handler");
|
||||
int fabricated_call =
|
||||
crc_edge_count(store, project.project, "CALLS", "argument", NULL, "handler");
|
||||
rh_cleanup(&project, store);
|
||||
ASSERT_EQ(accept_call, 1);
|
||||
ASSERT_EQ(reference, 0);
|
||||
ASSERT_EQ(usage, 1);
|
||||
ASSERT_EQ(fabricated_call, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(call_reference_python_later_decorated_function_rebinding_stays_usage) {
|
||||
static const char source[] = "def handler():\n"
|
||||
" pass\n"
|
||||
"def replace_with_value(function):\n"
|
||||
" return 42\n"
|
||||
"@replace_with_value\n"
|
||||
"def handler():\n"
|
||||
" pass\n"
|
||||
"def accept(callback):\n"
|
||||
" pass\n"
|
||||
"def argument():\n"
|
||||
" accept(handler)\n";
|
||||
RProj project;
|
||||
cbm_store_t *store = rh_index(&project, "function_rebinding.py", source);
|
||||
ASSERT_NOT_NULL(store);
|
||||
int accept_call =
|
||||
crc_edge_count(store, project.project, "CALLS", "argument", "Function", "accept");
|
||||
int reference =
|
||||
crc_edge_count(store, project.project, "CALL_REFERENCE", "argument", NULL, "handler");
|
||||
int usage = crc_edge_count(store, project.project, "USAGE", "argument", "Function", "handler");
|
||||
int fabricated_call =
|
||||
crc_edge_count(store, project.project, "CALLS", "argument", NULL, "handler");
|
||||
rh_cleanup(&project, store);
|
||||
ASSERT_EQ(accept_call, 1);
|
||||
ASSERT_EQ(reference, 0);
|
||||
ASSERT_EQ(usage, 1);
|
||||
ASSERT_EQ(fabricated_call, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(call_reference_python_later_decorated_method_rebinding_stays_usage) {
|
||||
static const char source[] = "def replace_with_value(function):\n"
|
||||
" return 42\n"
|
||||
"class Service:\n"
|
||||
" def handler(self):\n"
|
||||
" pass\n"
|
||||
" @replace_with_value\n"
|
||||
" def handler(self):\n"
|
||||
" pass\n"
|
||||
"def accept(callback):\n"
|
||||
" pass\n"
|
||||
"def argument(service: Service):\n"
|
||||
" accept(service.handler)\n";
|
||||
RProj project;
|
||||
cbm_store_t *store = rh_index(&project, "method_rebinding.py", source);
|
||||
ASSERT_NOT_NULL(store);
|
||||
int accept_call =
|
||||
crc_edge_count(store, project.project, "CALLS", "argument", "Function", "accept");
|
||||
int reference =
|
||||
crc_edge_count(store, project.project, "CALL_REFERENCE", "argument", NULL, "handler");
|
||||
int usage = crc_edge_count(store, project.project, "USAGE", "argument", "Method", "handler");
|
||||
int fabricated_call =
|
||||
crc_edge_count(store, project.project, "CALLS", "argument", NULL, "handler");
|
||||
rh_cleanup(&project, store);
|
||||
ASSERT_EQ(accept_call, 1);
|
||||
ASSERT_EQ(reference, 0);
|
||||
ASSERT_EQ(usage, 1);
|
||||
ASSERT_EQ(fabricated_call, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(call_reference_go_bound_method_argument_is_exact) {
|
||||
static const char source[] = "package refs\n"
|
||||
"type Service struct{}\n"
|
||||
"func (Service) handler() {}\n"
|
||||
"func accept(callback func()) {}\n"
|
||||
"func argument(service Service) { accept(service.handler) }\n";
|
||||
RProj project;
|
||||
cbm_store_t *store = rh_index(&project, "bound.go", source);
|
||||
ASSERT_NOT_NULL(store);
|
||||
int reference =
|
||||
crc_edge_count(store, project.project, "CALL_REFERENCE", "argument", "Method", "handler");
|
||||
int usage = crc_edge_count(store, project.project, "USAGE", "argument", "Method", "handler");
|
||||
int fabricated_call =
|
||||
crc_edge_count(store, project.project, "CALLS", "argument", "Method", "handler");
|
||||
rh_cleanup(&project, store);
|
||||
ASSERT_EQ(reference, 1);
|
||||
ASSERT_EQ(usage, 0);
|
||||
ASSERT_EQ(fabricated_call, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(call_reference_go_ambiguous_promoted_method_stays_usage) {
|
||||
static const char source[] = "package refs\n"
|
||||
"type Alpha struct{}\n"
|
||||
"func (Alpha) handler() {}\n"
|
||||
"type Beta struct{}\n"
|
||||
"func (Beta) handler() {}\n"
|
||||
"type Service struct { Alpha; Beta }\n"
|
||||
"func accept(callback func()) {}\n"
|
||||
"func argument(service Service) { accept(service.handler) }\n";
|
||||
RProj project;
|
||||
cbm_store_t *store = rh_index(&project, "ambiguous.go", source);
|
||||
ASSERT_NOT_NULL(store);
|
||||
int reference =
|
||||
crc_edge_count(store, project.project, "CALL_REFERENCE", "argument", "Method", "handler");
|
||||
int usage = crc_edge_count(store, project.project, "USAGE", "argument", "Method", "handler");
|
||||
int fabricated_call =
|
||||
crc_edge_count(store, project.project, "CALLS", "argument", "Method", "handler");
|
||||
rh_cleanup(&project, store);
|
||||
ASSERT_EQ(reference, 0);
|
||||
ASSERT_EQ(usage, 1);
|
||||
ASSERT_EQ(fabricated_call, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(call_reference_contract) {
|
||||
RUN_TEST(call_reference_typescript_direct_argument_is_exact);
|
||||
RUN_TEST(call_reference_kotlin_alias_argument_is_exact);
|
||||
RUN_TEST(call_reference_python_use_before_local_shadow_is_not_global);
|
||||
RUN_TEST(call_reference_javascript_use_before_local_shadow_is_not_global);
|
||||
RUN_TEST(call_reference_typescript_use_before_local_shadow_is_not_global);
|
||||
RUN_TEST(call_reference_python_keyword_argument_is_exact);
|
||||
RUN_TEST(call_reference_python_bound_method_argument_is_exact);
|
||||
RUN_TEST(call_reference_python_parenthesized_bound_method_is_exact);
|
||||
RUN_TEST(call_reference_python_instance_field_shadow_is_not_method);
|
||||
RUN_TEST(call_reference_python_property_value_stays_usage);
|
||||
RUN_TEST(call_reference_python_unknown_decorated_value_stays_usage);
|
||||
RUN_TEST(call_reference_python_later_decorated_function_rebinding_stays_usage);
|
||||
RUN_TEST(call_reference_python_later_decorated_method_rebinding_stays_usage);
|
||||
RUN_TEST(call_reference_go_bound_method_argument_is_exact);
|
||||
RUN_TEST(call_reference_go_ambiguous_promoted_method_stays_usage);
|
||||
}
|
||||
@@ -275,14 +275,36 @@ static int cp_callables(cbm_store_t *store, const char *project) {
|
||||
}
|
||||
|
||||
/* Diagnostic edge histogram (stderr only, so test output stays clean). */
|
||||
static const char *CP_ALL_EDGE_TYPES[] = {
|
||||
"CALLS", "CONFIGURES", "CONTAINS_FILE", "CONTAINS_FOLDER",
|
||||
"DATA_FLOWS", "DECORATES", "DEFINES", "DEFINES_METHOD",
|
||||
"DEPENDS_ON", "FILE_CHANGES_WITH", "GRAPHQL_CALLS", "GRPC_CALLS",
|
||||
"HANDLES", "HTTP_CALLS", "IMPLEMENTS", "IMPORTS",
|
||||
"INHERITS", "INFRA_MAPS", "OVERRIDE", "READS",
|
||||
"SEMANTICALLY_RELATED", "SIMILAR_TO", "TESTS_FILE", "TESTS",
|
||||
"TRPC_CALLS", "USAGE", "ASYNC_CALLS", "WRITES", NULL};
|
||||
static const char *CP_ALL_EDGE_TYPES[] = {"CALLS",
|
||||
"CALL_REFERENCE",
|
||||
"CONFIGURES",
|
||||
"CONTAINS_FILE",
|
||||
"CONTAINS_FOLDER",
|
||||
"DATA_FLOWS",
|
||||
"DECORATES",
|
||||
"DEFINES",
|
||||
"DEFINES_METHOD",
|
||||
"DEPENDS_ON",
|
||||
"FILE_CHANGES_WITH",
|
||||
"GRAPHQL_CALLS",
|
||||
"GRPC_CALLS",
|
||||
"HANDLES",
|
||||
"HTTP_CALLS",
|
||||
"IMPLEMENTS",
|
||||
"IMPORTS",
|
||||
"INHERITS",
|
||||
"INFRA_MAPS",
|
||||
"OVERRIDE",
|
||||
"READS",
|
||||
"SEMANTICALLY_RELATED",
|
||||
"SIMILAR_TO",
|
||||
"TESTS_FILE",
|
||||
"TESTS",
|
||||
"TRPC_CALLS",
|
||||
"USAGE",
|
||||
"ASYNC_CALLS",
|
||||
"WRITES",
|
||||
NULL};
|
||||
|
||||
static void cp_diag(cbm_store_t *store, const char *project, const char *label) {
|
||||
if (!store) { fprintf(stderr, " [CP] %s: no graph DB\n", label); return; }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user