mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
feat(resolver): Lua, Vimscript, Shell, Tcl, Make and Vue edges from real rules
Lua CALLS on neovim go from 4 to 19,685 against 20,307 at baseline, whose count holds wrong targets: lexical scope and value flow through aliases, function M.f, return M, require via package.path forms or a unique lua, src or lib root, and __index with @class parents. Whole-file gates on load, _G, goto and ... are gone, because none can rebind a local; _ENV, setfenv and debug.setlocal still abstain. New adapters give Vimscript, Shell, Tcl and Make proven edges: neovim vim CALLS 0 to 3,365, redis Tcl CALLS 0 to 2,815, with a function table per process following source.
A Vue or Svelte script is a module whatever its markup says: koel Vue CALLS 2 to 1,484, IMPORTS 0 to 1,650. Members of an imported object literal, and const { open } = useThing() where every return names it, resolve. An adapter may now declare parse errors local: 74% of Tcl files have one. About 490 edges read from source, one wrong and fixed.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
4ee39037f7
commit
4cf24b147c
29 files changed
+5395
-453
No files matched your search
@@ -544,6 +544,11 @@ PIPELINE_SRCS = \
|
||||
internal/cbm/resolve/adapter_perl.c \
|
||||
internal/cbm/resolve/adapter_ruby.c \
|
||||
internal/cbm/resolve/adapter_zig.c \
|
||||
internal/cbm/resolve/adapter_lua.c \
|
||||
internal/cbm/resolve/adapter_vim.c \
|
||||
internal/cbm/resolve/adapter_shell.c \
|
||||
internal/cbm/resolve/adapter_tcl.c \
|
||||
internal/cbm/resolve/adapter_make.c \
|
||||
internal/cbm/resolve/adapter_haskell.c \
|
||||
internal/cbm/resolve/adapter_ocaml.c \
|
||||
internal/cbm/resolve/adapter_dart.c \
|
||||
|
||||
@@ -6059,6 +6059,18 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C
|
||||
}
|
||||
cbm_run_ts_lsp(a, result, source, source_len, root, js_mode, jsx_mode, dts_mode);
|
||||
}
|
||||
/* A component's scripts are a module of their own language. */
|
||||
if (language == CBM_LANG_VUE || language == CBM_LANG_SVELTE) {
|
||||
CBMLanguage script_language = CBM_LANG_JAVASCRIPT;
|
||||
TSRange range;
|
||||
memset(&range, 0, sizeof(range));
|
||||
TSTree *script = cbm_component_binding_tree(&ctx, &script_language, &range);
|
||||
if (script) {
|
||||
cbm_run_ts_lsp(a, result, source, source_len, ts_tree_root_node(script),
|
||||
script_language == CBM_LANG_JAVASCRIPT, false, false);
|
||||
ts_tree_delete(script);
|
||||
}
|
||||
}
|
||||
if (language == CBM_LANG_CSHARP) {
|
||||
cbm_run_cs_lsp(a, result, source, source_len, root);
|
||||
}
|
||||
|
||||
@@ -1531,6 +1531,8 @@ void cbm_extract_imports(CBMExtractCtx *ctx);
|
||||
/* Owned inner tree for one fully audited Vue/Svelte inline script. Absolute
|
||||
* source coordinates are preserved; NULL means the host domain is unproved. */
|
||||
TSTree *cbm_component_binding_tree(CBMExtractCtx *ctx, CBMLanguage *language, TSRange *range);
|
||||
/* The script names a component's markup assigns. The names live in ctx->arena. */
|
||||
size_t cbm_component_template_writes(CBMExtractCtx *ctx, const char ***names);
|
||||
void cbm_extract_usages(CBMExtractCtx *ctx);
|
||||
void cbm_extract_semantic(CBMExtractCtx *ctx);
|
||||
void cbm_extract_type_refs(CBMExtractCtx *ctx);
|
||||
|
||||
+22
-11
@@ -11788,21 +11788,30 @@ static void extract_lua_vars(CBMExtractCtx *ctx, TSNode node, CBMArena *a) {
|
||||
continue;
|
||||
}
|
||||
TSNode expr_list = cbm_find_child_by_kind(child, "expression_list");
|
||||
if (!ts_node_is_null(expr_list) && ts_node_named_child_count(expr_list) > 0) {
|
||||
TSNode val = ts_node_named_child(expr_list, 0);
|
||||
if (!ts_node_is_null(val) && strcmp(ts_node_type(val), "function_definition") == 0) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
TSNode vars = ts_node_child_by_field_name(child, TS_FIELD("variables"));
|
||||
if (ts_node_is_null(vars)) {
|
||||
vars = cbm_find_child_by_kind(child, "variable_list");
|
||||
}
|
||||
if (!ts_node_is_null(vars) && ts_node_named_child_count(vars) > 0) {
|
||||
TSNode first = ts_node_named_child(vars, 0);
|
||||
if (!ts_node_is_null(first)) {
|
||||
push_var_def(ctx, cbm_node_text(a, first, ctx->source), node);
|
||||
if (ts_node_is_null(vars)) {
|
||||
continue;
|
||||
}
|
||||
uint32_t names = ts_node_named_child_count(vars);
|
||||
uint32_t values = ts_node_is_null(expr_list) ? 0 : ts_node_named_child_count(expr_list);
|
||||
/* `local describe, it = t.describe, t.it` declares both. A function value is a
|
||||
* Function node of that name, and an attribute is not a name. */
|
||||
for (uint32_t k = 0, position = 0; k < names; k++) {
|
||||
TSNode name = ts_node_named_child(vars, k);
|
||||
if (strcmp(ts_node_type(name), "identifier") != 0) {
|
||||
continue;
|
||||
}
|
||||
TSNode value =
|
||||
position < values ? ts_node_named_child(expr_list, position) : (TSNode){0};
|
||||
position++;
|
||||
if (!ts_node_is_null(value) &&
|
||||
strcmp(ts_node_type(value), "function_definition") == 0) {
|
||||
continue;
|
||||
}
|
||||
push_var_def(ctx, cbm_node_text(a, name, ctx->source), node);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14771,12 +14780,14 @@ static void walk_defs(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec,
|
||||
// the body's `name = args: ...` bindings are reached. Inner curried
|
||||
// lambdas (`f = a: b: ...`) resolve no name and mint nothing, so the
|
||||
// extra descent adds defs without adding noise.
|
||||
// Lua: a test file is one `describe(name, function() ... end)`, so nearly
|
||||
// all of its `local function`s live inside an anonymous function.
|
||||
bool descend_into_func =
|
||||
(ctx->language == CBM_LANG_C && cbm_test_definition_qn(ctx, node) != NULL) ||
|
||||
(ctx->language == CBM_LANG_WOLFRAM || ctx->language == CBM_LANG_TYPESCRIPT ||
|
||||
ctx->language == CBM_LANG_JAVASCRIPT || ctx->language == CBM_LANG_TSX ||
|
||||
ctx->language == CBM_LANG_ARKTS || ctx->language == CBM_LANG_ADA ||
|
||||
ctx->language == CBM_LANG_NIX);
|
||||
ctx->language == CBM_LANG_NIX || ctx->language == CBM_LANG_LUA);
|
||||
if (!descend_into_func) {
|
||||
continue;
|
||||
}
|
||||
|
||||
+253
-171
@@ -2312,62 +2312,92 @@ static void parse_elixir_imports(CBMExtractCtx *ctx) {
|
||||
// --- Lua imports ---
|
||||
// function_call: require("X")
|
||||
|
||||
/* A name in a binding position that is `require`: this file's require is not the loader. */
|
||||
static bool lua_binds_require(const CBMExtractCtx *ctx, TSNode node) {
|
||||
const char *kind = ts_node_type(node);
|
||||
TSNode bound = (TSNode){0};
|
||||
if (strcmp(kind, "function_declaration") == 0) {
|
||||
bound = ts_node_child_by_field_name(node, TS_FIELD("name"));
|
||||
} else if (strcmp(kind, "parameters") == 0 || strcmp(kind, "variable_list") == 0) {
|
||||
uint32_t count = ts_node_named_child_count(node);
|
||||
for (uint32_t i = 0; i < count && ts_node_is_null(bound); i++) {
|
||||
TSNode name = ts_node_named_child(node, i);
|
||||
uint32_t start = ts_node_start_byte(name), end = ts_node_end_byte(name);
|
||||
if (strcmp(ts_node_type(name), "identifier") == 0 && end - start == strlen("require") &&
|
||||
memcmp(ctx->source + start, "require", strlen("require")) == 0)
|
||||
bound = name;
|
||||
}
|
||||
}
|
||||
if (ts_node_is_null(bound) || strcmp(ts_node_type(bound), "identifier") != 0)
|
||||
return false;
|
||||
uint32_t start = ts_node_start_byte(bound), end = ts_node_end_byte(bound);
|
||||
return end - start == strlen("require") &&
|
||||
memcmp(ctx->source + start, "require", strlen("require")) == 0;
|
||||
}
|
||||
|
||||
/* The module of `require('a.b')` or `pcall(require, 'a.b')`, when spelled as one literal. */
|
||||
static const char *lua_required_module(CBMExtractCtx *ctx, TSNode call) {
|
||||
TSNode callee = ts_node_child_by_field_name(call, TS_FIELD("name"));
|
||||
TSNode arguments = ts_node_child_by_field_name(call, TS_FIELD("arguments"));
|
||||
if (ts_node_is_null(callee) || ts_node_is_null(arguments) ||
|
||||
strcmp(ts_node_type(callee), "identifier") != 0)
|
||||
return NULL;
|
||||
char *name = cbm_node_text(ctx->arena, callee, ctx->source);
|
||||
uint32_t count = ts_node_named_child_count(arguments);
|
||||
TSNode literal = (TSNode){0};
|
||||
if (name && strcmp(name, "require") == 0 && count == 1) {
|
||||
literal = ts_node_named_child(arguments, 0);
|
||||
} else if (name && strcmp(name, "pcall") == 0 && count == 2) {
|
||||
TSNode loader = ts_node_named_child(arguments, 0);
|
||||
char *text = strcmp(ts_node_type(loader), "identifier") == 0
|
||||
? cbm_node_text(ctx->arena, loader, ctx->source)
|
||||
: NULL;
|
||||
if (text && strcmp(text, "require") == 0)
|
||||
literal = ts_node_named_child(arguments, 1);
|
||||
}
|
||||
if (ts_node_is_null(literal) || strcmp(ts_node_type(literal), "string") != 0 ||
|
||||
ts_node_named_child_count(literal) != 1)
|
||||
return NULL;
|
||||
TSNode content = ts_node_named_child(literal, 0);
|
||||
return strcmp(ts_node_type(content), "string_content") == 0
|
||||
? cbm_node_text(ctx->arena, content, ctx->source)
|
||||
: NULL;
|
||||
}
|
||||
|
||||
/* A require loads a module wherever it runs, so every depth counts, and a comment that spells
|
||||
* one loads nothing. */
|
||||
static void parse_lua_imports(CBMExtractCtx *ctx) {
|
||||
CBMArena *a = ctx->arena;
|
||||
|
||||
int first = ctx->result->imports.count;
|
||||
bool rebound = false;
|
||||
bool entering = true;
|
||||
TSTreeCursor cursor = ts_tree_cursor_new(ctx->root);
|
||||
if (!ts_tree_cursor_goto_first_child(&cursor)) {
|
||||
ts_tree_cursor_delete(&cursor);
|
||||
return;
|
||||
while (!rebound) {
|
||||
if (entering) {
|
||||
TSNode node = ts_tree_cursor_current_node(&cursor);
|
||||
rebound = lua_binds_require(ctx, node);
|
||||
if (strcmp(ts_node_type(node), "function_call") == 0) {
|
||||
const char *module = lua_required_module(ctx, node);
|
||||
if (module && module[0]) {
|
||||
char *mod = cbm_arena_strdup(a, module);
|
||||
CBMImport imp = {.local_name = path_last(a, mod), .module_path = mod};
|
||||
push_import_at(ctx, node, imp);
|
||||
}
|
||||
}
|
||||
if (ts_tree_cursor_goto_first_child(&cursor))
|
||||
continue;
|
||||
}
|
||||
if (ts_tree_cursor_goto_next_sibling(&cursor)) {
|
||||
entering = true;
|
||||
continue;
|
||||
}
|
||||
if (!ts_tree_cursor_goto_parent(&cursor))
|
||||
break;
|
||||
entering = false;
|
||||
}
|
||||
do {
|
||||
TSNode node = ts_tree_cursor_current_node(&cursor);
|
||||
// Lua: local X = require("Y") → assignment_statement or variable_declaration
|
||||
// containing function_call(require, "Y")
|
||||
char *text = cbm_node_text(a, node, ctx->source);
|
||||
if (!text) {
|
||||
continue;
|
||||
}
|
||||
if (strstr(text, "require") == NULL) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Simple extraction: find require("...") pattern in node text
|
||||
const char *req = strstr(text, "require");
|
||||
if (!req) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the string argument
|
||||
const char *open = strchr(req, '(');
|
||||
if (!open) {
|
||||
open = strchr(req, '"');
|
||||
}
|
||||
if (!open) {
|
||||
open = strchr(req, '\'');
|
||||
}
|
||||
if (!open) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char *q1 = strchr(open, '"');
|
||||
const char *q2 = strchr(open, '\'');
|
||||
if (!q1 && !q2) {
|
||||
continue;
|
||||
}
|
||||
const char *start = q1 && (!q2 || q1 < q2) ? q1 : q2;
|
||||
char delim = *start;
|
||||
start++;
|
||||
const char *end = strchr(start, delim);
|
||||
if (!end) {
|
||||
continue;
|
||||
}
|
||||
|
||||
char *mod = cbm_arena_strndup(a, start, (size_t)(end - start));
|
||||
CBMImport imp = {.local_name = path_last(a, mod), .module_path = mod};
|
||||
push_import_at(ctx, node, imp);
|
||||
} while (ts_tree_cursor_goto_next_sibling(&cursor));
|
||||
ts_tree_cursor_delete(&cursor);
|
||||
if (rebound)
|
||||
ctx->result->imports.count = first;
|
||||
}
|
||||
|
||||
// --- R imports: library()/require()/source() + box::use() (#218) ---
|
||||
@@ -3297,81 +3327,14 @@ static bool markup_script_host(CBMLanguage language) {
|
||||
language == CBM_LANG_ASTRO;
|
||||
}
|
||||
|
||||
/* Component scripts have module-local bindings even without an explicit ESM
|
||||
* statement. This proof subset admits one inline script and an inert host.
|
||||
* Multiple script contexts, template expressions/directives, external scripts
|
||||
* and unknown markup remain unproved; extraction is deliberately broader. */
|
||||
static bool embedded_binding_identifier(const char *source, TSNode value) {
|
||||
uint32_t start = ts_node_start_byte(value), end = ts_node_end_byte(value);
|
||||
if (end <= start + 2 || source[start++] != '{' || source[--end] != '}')
|
||||
return false;
|
||||
while (start < end && isspace((unsigned char)source[start]))
|
||||
start++;
|
||||
while (end > start && isspace((unsigned char)source[end - 1]))
|
||||
end--;
|
||||
if (start == end || !((source[start] >= 'a' && source[start] <= 'z') ||
|
||||
(source[start] >= 'A' && source[start] <= 'Z') || source[start] == '_' ||
|
||||
source[start] == '$'))
|
||||
return false;
|
||||
for (uint32_t i = start + 1; i < end; i++) {
|
||||
unsigned char c = (unsigned char)source[i];
|
||||
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') ||
|
||||
c == '_' || c == '$'))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool embedded_binding_attribute(CBMExtractCtx *ctx, TSNode attribute, bool script,
|
||||
CBMLanguage *language, unsigned *seen) {
|
||||
uint32_t count = ts_node_named_child_count(attribute);
|
||||
if (!count || count > 2)
|
||||
return false;
|
||||
TSNode name_node = ts_node_named_child(attribute, 0);
|
||||
if (strcmp(ts_node_type(name_node), "attribute_name"))
|
||||
return false;
|
||||
const char *name = cbm_node_text(ctx->arena, name_node, ctx->source);
|
||||
if (!name || !name[0])
|
||||
return false;
|
||||
if (script) {
|
||||
unsigned flag = ascii_trimmed_equals(name, "lang") ? 1u
|
||||
: ctx->language == CBM_LANG_VUE && !strcmp(name, "setup") ? 2u
|
||||
: 0u;
|
||||
if (!flag || (*seen & flag) || (flag == 2u && count != 1))
|
||||
return false;
|
||||
*seen |= flag;
|
||||
return apply_script_attribute(ctx, attribute, language);
|
||||
}
|
||||
/* A Svelte event handler may capture a bare identifier, but may not run
|
||||
* an expression while creating the handler or write a script binding. */
|
||||
if (ctx->language == CBM_LANG_SVELTE && !strncmp(name, "on:", 3)) {
|
||||
if (!name[3] || count != 2)
|
||||
return false;
|
||||
for (const char *p = name + 3; *p; p++)
|
||||
if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z')))
|
||||
return false;
|
||||
TSNode value = ts_node_named_child(attribute, 1);
|
||||
return !strcmp(ts_node_type(value), "expression") &&
|
||||
embedded_binding_identifier(ctx->source, value);
|
||||
}
|
||||
if (!strncmp(name, "v-", 2) || !strncmp(name, "on", 2))
|
||||
return false;
|
||||
for (const char *p = name; *p; p++)
|
||||
if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') ||
|
||||
*p == '_' || *p == '-'))
|
||||
return false;
|
||||
/* Static attribute text cannot conceal a component expression. This is
|
||||
* intentionally narrower than either host grammar's value syntax. */
|
||||
uint32_t begin = ts_node_end_byte(name_node), end = ts_node_end_byte(attribute);
|
||||
for (uint32_t i = begin; i < end; i++)
|
||||
if (ctx->source[i] == '{' || ctx->source[i] == '}' || ctx->source[i] == '&')
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* A component's scripts have module-local bindings even without an explicit ESM statement. A
|
||||
* Vue SFC's `<script>` and `<script setup>` compile to one module, as do Svelte's
|
||||
* `context="module"` script and its instance script. Markup cannot declare a script binding:
|
||||
* its expressions read script names and call them, and the only way they touch a binding is
|
||||
* assigning it, which cbm_component_template_writes reports. A script loaded from `src`, or in
|
||||
* a language this does not parse, stays unproved. */
|
||||
static bool embedded_binding_script(CBMExtractCtx *ctx, TSNode script, CBMEmbeddedBlock *block,
|
||||
CBMLanguage *language) {
|
||||
unsigned attributes = 0;
|
||||
bool content = false, start_tag = false, end_tag = false;
|
||||
uint32_t count = ts_node_named_child_count(script);
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
@@ -3383,10 +3346,8 @@ static bool embedded_binding_script(CBMExtractCtx *ctx, TSNode script, CBMEmbedd
|
||||
for (uint32_t j = 0; j < children; j++) {
|
||||
TSNode item = ts_node_named_child(child, j);
|
||||
const char *item_kind = ts_node_type(item);
|
||||
if (!strcmp(item_kind, "tag_name"))
|
||||
continue;
|
||||
if (strcmp(item_kind, "attribute") ||
|
||||
!embedded_binding_attribute(ctx, item, true, language, &attributes))
|
||||
if (!strcmp(item_kind, "attribute") ? !apply_script_attribute(ctx, item, language)
|
||||
: strcmp(item_kind, "tag_name") != 0)
|
||||
return false;
|
||||
}
|
||||
} else if (!strcmp(kind, "end_tag") && !end_tag) {
|
||||
@@ -3406,57 +3367,43 @@ TSTree *cbm_component_binding_tree(CBMExtractCtx *ctx, CBMLanguage *language, TS
|
||||
ctx->source_len <= 0 || ts_node_is_null(ctx->root) || ts_node_has_error(ctx->root) ||
|
||||
ts_node_end_byte(ctx->root) != (uint32_t)ctx->source_len)
|
||||
return NULL;
|
||||
enum { BINDING_HOST_STACK = 512, BINDING_HOST_NODES = 1000000 };
|
||||
TSNode stack[BINDING_HOST_STACK];
|
||||
size_t top = 0, visited = 0;
|
||||
stack[top++] = ctx->root;
|
||||
CBMEmbeddedBlock block = {0};
|
||||
bool found = false;
|
||||
enum { COMPONENT_SCRIPTS = 2 };
|
||||
TSRange ranges[COMPONENT_SCRIPTS];
|
||||
uint32_t count = 0;
|
||||
bool usable = true;
|
||||
*language = CBM_LANG_JAVASCRIPT;
|
||||
while (top) {
|
||||
TSNode node = stack[--top];
|
||||
if (++visited > BINDING_HOST_NODES)
|
||||
return NULL;
|
||||
const char *kind = ts_node_type(node);
|
||||
if (!strcmp(kind, "script_element")) {
|
||||
if (!ts_node_eq(ts_node_parent(node), ctx->root) || found ||
|
||||
!embedded_binding_script(ctx, node, &block, language))
|
||||
return NULL;
|
||||
found = true;
|
||||
continue;
|
||||
}
|
||||
if (!strcmp(kind, "attribute")) {
|
||||
unsigned unused = 0;
|
||||
if (!embedded_binding_attribute(ctx, node, false, language, &unused))
|
||||
return NULL;
|
||||
continue;
|
||||
}
|
||||
bool container = !strcmp(kind, "document") || !strcmp(kind, "element") ||
|
||||
!strcmp(kind, "template_element") || !strcmp(kind, "start_tag") ||
|
||||
!strcmp(kind, "end_tag") || !strcmp(kind, "self_closing_tag");
|
||||
bool leaf = !strcmp(kind, "text") || !strcmp(kind, "comment") || !strcmp(kind, "tag_name");
|
||||
if (!container && !leaf)
|
||||
return NULL;
|
||||
uint32_t children = ts_node_named_child_count(node);
|
||||
if (leaf && children)
|
||||
return NULL;
|
||||
if (children > BINDING_HOST_STACK - top)
|
||||
return NULL;
|
||||
for (uint32_t i = children; i > 0; i--)
|
||||
stack[top++] = ts_node_named_child(node, i - 1);
|
||||
TSTreeCursor cursor = ts_tree_cursor_new(ctx->root);
|
||||
if (ts_tree_cursor_goto_first_child(&cursor)) {
|
||||
do {
|
||||
TSNode node = ts_tree_cursor_current_node(&cursor);
|
||||
if (strcmp(ts_node_type(node), "script_element"))
|
||||
continue;
|
||||
CBMEmbeddedBlock block = {0};
|
||||
CBMLanguage own = CBM_LANG_JAVASCRIPT;
|
||||
if (count == COMPONENT_SCRIPTS || !embedded_binding_script(ctx, node, &block, &own) ||
|
||||
(count && own != *language)) {
|
||||
usable = false;
|
||||
break;
|
||||
}
|
||||
*language = own;
|
||||
ranges[count++] = (TSRange){.start_point = ts_node_start_point(block.content),
|
||||
.end_point = ts_node_end_point(block.content),
|
||||
.start_byte = ts_node_start_byte(block.content),
|
||||
.end_byte = ts_node_end_byte(block.content)};
|
||||
} while (ts_tree_cursor_goto_next_sibling(&cursor));
|
||||
}
|
||||
if (!found)
|
||||
ts_tree_cursor_delete(&cursor);
|
||||
if (!usable || !count)
|
||||
return NULL;
|
||||
*range = (TSRange){.start_point = ts_node_start_point(block.content),
|
||||
.end_point = ts_node_end_point(block.content),
|
||||
.start_byte = ts_node_start_byte(block.content),
|
||||
.end_byte = ts_node_end_byte(block.content)};
|
||||
*range = ranges[0];
|
||||
range->end_point = ranges[count - 1].end_point;
|
||||
range->end_byte = ranges[count - 1].end_byte;
|
||||
if (range->end_byte <= range->start_byte || range->end_byte > (uint32_t)ctx->source_len)
|
||||
return NULL;
|
||||
TSParser *parser = ts_parser_new();
|
||||
TSTree *tree = NULL;
|
||||
if (parser && ts_parser_set_language(parser, cbm_ts_language(*language)) &&
|
||||
ts_parser_set_included_ranges(parser, range, 1))
|
||||
ts_parser_set_included_ranges(parser, ranges, count))
|
||||
tree = ts_parser_parse_string(parser, NULL, ctx->source, (uint32_t)ctx->source_len);
|
||||
if (parser)
|
||||
ts_parser_delete(parser);
|
||||
@@ -3467,6 +3414,141 @@ TSTree *cbm_component_binding_tree(CBMExtractCtx *ctx, CBMLanguage *language, TS
|
||||
return tree;
|
||||
}
|
||||
|
||||
static bool component_name_byte(unsigned char c) {
|
||||
return isalnum(c) || c == '_' || c == '$';
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const char **items;
|
||||
size_t count, capacity;
|
||||
} component_writes_t;
|
||||
|
||||
static void component_note_write(CBMExtractCtx *ctx, component_writes_t *writes, const char *name,
|
||||
size_t length) {
|
||||
if (writes->count == writes->capacity) {
|
||||
size_t capacity = writes->capacity ? writes->capacity * 2 : 8;
|
||||
const char **items = cbm_arena_alloc(ctx->arena, capacity * sizeof(*items));
|
||||
if (!items)
|
||||
return;
|
||||
if (writes->count)
|
||||
memcpy(items, writes->items, writes->count * sizeof(*items));
|
||||
writes->items = items;
|
||||
writes->capacity = capacity;
|
||||
}
|
||||
const char *copy = cbm_arena_strndup(ctx->arena, name, length);
|
||||
if (copy)
|
||||
writes->items[writes->count++] = copy;
|
||||
}
|
||||
|
||||
/* The names an expression assigns: `x = y`, `x += y`, `x++`. `x.y = z` writes a member and
|
||||
* leaves the binding alone; `===`, `=>` and string contents are not assignments. */
|
||||
static void component_scan_assignments(CBMExtractCtx *ctx, component_writes_t *writes,
|
||||
const char *text, size_t length) {
|
||||
for (size_t at = 0; at < length;) {
|
||||
unsigned char c = (unsigned char)text[at];
|
||||
if (c == '\'' || c == '"' || c == '`') {
|
||||
size_t close = at + 1;
|
||||
while (close < length && text[close] != (char)c)
|
||||
close += text[close] == '\\' ? 2 : 1;
|
||||
at = close + 1;
|
||||
continue;
|
||||
}
|
||||
if (!component_name_byte(c) || isdigit(c)) {
|
||||
at++;
|
||||
continue;
|
||||
}
|
||||
size_t begin = at;
|
||||
while (at < length && component_name_byte((unsigned char)text[at]))
|
||||
at++;
|
||||
bool member = begin && text[begin - 1] == '.';
|
||||
size_t next = at;
|
||||
while (next < length && isspace((unsigned char)text[next]))
|
||||
next++;
|
||||
if (member || next >= length)
|
||||
continue;
|
||||
const char *rest = text + next;
|
||||
size_t left = length - next;
|
||||
bool plain = rest[0] == '=' && (left < 2 || (rest[1] != '=' && rest[1] != '>'));
|
||||
bool compound = left >= 2 && rest[1] == '=' && strchr("+-*/%|&^", rest[0]);
|
||||
bool step =
|
||||
left >= 2 && ((rest[0] == '+' && rest[1] == '+') || (rest[0] == '-' && rest[1] == '-'));
|
||||
if (plain || compound || step)
|
||||
component_note_write(ctx, writes, text + begin, at - begin);
|
||||
}
|
||||
}
|
||||
|
||||
/* `v-model="x"` and `bind:value={x}` assign x. */
|
||||
static void component_scan_bound_name(CBMExtractCtx *ctx, component_writes_t *writes,
|
||||
const char *text, size_t length) {
|
||||
size_t at = 0;
|
||||
while (at < length && (isspace((unsigned char)text[at]) || text[at] == '{'))
|
||||
at++;
|
||||
size_t begin = at;
|
||||
while (at < length && component_name_byte((unsigned char)text[at]))
|
||||
at++;
|
||||
size_t end = at;
|
||||
while (at < length && (isspace((unsigned char)text[at]) || text[at] == '}'))
|
||||
at++;
|
||||
if (end > begin && at == length)
|
||||
component_note_write(ctx, writes, text + begin, end - begin);
|
||||
}
|
||||
|
||||
static void component_scan_attribute(CBMExtractCtx *ctx, component_writes_t *writes, TSNode node) {
|
||||
uint32_t count = ts_node_named_child_count(node);
|
||||
if (count < 2)
|
||||
return;
|
||||
TSNode name = ts_node_named_child(node, 0);
|
||||
TSNode value = ts_node_named_child(node, count - 1);
|
||||
uint32_t begin = ts_node_start_byte(name), end = ts_node_end_byte(name);
|
||||
const char *text = ctx->source + begin;
|
||||
size_t size = end - begin;
|
||||
bool bound = (size >= strlen("v-model") && !strncmp(text, "v-model", strlen("v-model"))) ||
|
||||
(size >= strlen("bind:") && !strncmp(text, "bind:", strlen("bind:")));
|
||||
if (!bound && strcmp(ts_node_type(node), "directive_attribute"))
|
||||
return;
|
||||
uint32_t from = ts_node_start_byte(value), to = ts_node_end_byte(value);
|
||||
bool quoted = to - from >= 2 && (ctx->source[from] == '"' || ctx->source[from] == '\'');
|
||||
const char *body = ctx->source + from + (quoted ? 1 : 0);
|
||||
size_t length = to - from - (quoted ? 2 : 0);
|
||||
if (bound)
|
||||
component_scan_bound_name(ctx, writes, body, length);
|
||||
else
|
||||
component_scan_assignments(ctx, writes, body, length);
|
||||
}
|
||||
|
||||
size_t cbm_component_template_writes(CBMExtractCtx *ctx, const char ***names) {
|
||||
component_writes_t writes = {0};
|
||||
*names = NULL;
|
||||
if (!ctx || !ctx->source || ts_node_is_null(ctx->root))
|
||||
return 0;
|
||||
TSTreeCursor cursor = ts_tree_cursor_new(ctx->root);
|
||||
bool entering = true;
|
||||
for (;;) {
|
||||
TSNode node = ts_tree_cursor_current_node(&cursor);
|
||||
const char *kind = ts_node_type(node);
|
||||
bool skip = !strcmp(kind, "script_element") || !strcmp(kind, "style_element");
|
||||
if (entering && !skip) {
|
||||
if (!strcmp(kind, "directive_attribute") || !strcmp(kind, "attribute"))
|
||||
component_scan_attribute(ctx, &writes, node);
|
||||
else if (!strcmp(kind, "expression"))
|
||||
component_scan_assignments(ctx, &writes, ctx->source + ts_node_start_byte(node),
|
||||
ts_node_end_byte(node) - ts_node_start_byte(node));
|
||||
if (ts_tree_cursor_goto_first_child(&cursor))
|
||||
continue;
|
||||
}
|
||||
if (ts_tree_cursor_goto_next_sibling(&cursor)) {
|
||||
entering = true;
|
||||
continue;
|
||||
}
|
||||
if (!ts_tree_cursor_goto_parent(&cursor))
|
||||
break;
|
||||
entering = false;
|
||||
}
|
||||
ts_tree_cursor_delete(&cursor);
|
||||
*names = writes.items;
|
||||
return writes.count;
|
||||
}
|
||||
|
||||
static void parse_one_embedded_block(CBMExtractCtx *ctx, const CBMEmbeddedBlock *block,
|
||||
CBMLanguage embedded_language) {
|
||||
const TSLanguage *language = cbm_ts_language(embedded_language);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,91 @@
|
||||
/* Make variable rules, from the GNU make manual's "How to Use Variables".
|
||||
*
|
||||
* A make process has one variable namespace. `X = v`, `X := v`, `X ?= v` and `X += v` all write
|
||||
* the same variable, which is why the graph has one node for them, and `$(X)` or `${X}` reads it
|
||||
* wherever the assignment sits, since a recursive variable expands when it is used. A name the
|
||||
* file does not assign comes from an included file, the environment or the command line, which
|
||||
* this does not decide. A computed name (`$($(X)_FLAGS)`) is unknown. */
|
||||
#include "source_adapter_internal.h"
|
||||
|
||||
#define MAKE_VARIABLES "V:"
|
||||
|
||||
typedef struct {
|
||||
cbm_source_walk_t *walk;
|
||||
const char *owner;
|
||||
} make_ctx_t;
|
||||
|
||||
static void make_collect_walk(make_ctx_t *ctx, TSNode node, unsigned depth) {
|
||||
if (!cbm_source_step(ctx->walk, depth))
|
||||
return;
|
||||
if (cbm_source_is(node, "variable_assignment")) {
|
||||
TSNode name = cbm_source_field(node, "name");
|
||||
const char *text = cbm_source_is(name, "word") ? cbm_source_text(ctx->walk, name) : NULL;
|
||||
if (text && !cbm_source_lookup(ctx->walk, ctx->owner, text))
|
||||
(void)cbm_source_declare(
|
||||
ctx->walk, ctx->owner, text,
|
||||
cbm_source_def_qn(ctx->walk, text, ts_node_start_point(node).row + 1), 0, 0);
|
||||
}
|
||||
TSTreeCursor cursor = ts_tree_cursor_new(node);
|
||||
if (ts_tree_cursor_goto_first_child(&cursor)) {
|
||||
do {
|
||||
TSNode child = ts_tree_cursor_current_node(&cursor);
|
||||
if (ts_node_is_named(child))
|
||||
make_collect_walk(ctx, child, depth + 1);
|
||||
} while (ts_tree_cursor_goto_next_sibling(&cursor));
|
||||
}
|
||||
ts_tree_cursor_delete(&cursor);
|
||||
}
|
||||
|
||||
static void make_visit_reference(make_ctx_t *ctx, TSNode node) {
|
||||
cbm_source_walk_t *walk = ctx->walk;
|
||||
const char *name = NULL;
|
||||
if (ts_node_named_child_count(node) == 1 && cbm_source_is(ts_node_named_child(node, 0), "word"))
|
||||
name = cbm_source_text(walk, ts_node_named_child(node, 0));
|
||||
const cbm_source_decl_t *decl = name ? cbm_source_lookup(walk, ctx->owner, name) : NULL;
|
||||
cbm_source_emit_node(walk, node, CBM_SOURCE_SITE_REFERENCE, decl ? decl->qn : NULL,
|
||||
CBM_RESOLVE_RULE_SCOPE_BINDING, ctx->owner,
|
||||
!name ? "make_variable_name_computed"
|
||||
: !decl ? "make_variable_not_assigned_in_this_file"
|
||||
: decl->qn ? "make_variable_of_this_file"
|
||||
: "make_variable_has_no_graph_node");
|
||||
}
|
||||
|
||||
static void make_resolve_walk(make_ctx_t *ctx, TSNode node, unsigned depth) {
|
||||
if (!cbm_source_step(ctx->walk, depth))
|
||||
return;
|
||||
if (cbm_source_is(node, "variable_reference"))
|
||||
make_visit_reference(ctx, node);
|
||||
TSTreeCursor cursor = ts_tree_cursor_new(node);
|
||||
if (ts_tree_cursor_goto_first_child(&cursor)) {
|
||||
do {
|
||||
TSNode child = ts_tree_cursor_current_node(&cursor);
|
||||
if (ts_node_is_named(child))
|
||||
make_resolve_walk(ctx, child, depth + 1);
|
||||
} while (ts_tree_cursor_goto_next_sibling(&cursor));
|
||||
}
|
||||
ts_tree_cursor_delete(&cursor);
|
||||
}
|
||||
|
||||
static const char *make_owner(cbm_source_walk_t *walk) {
|
||||
return cbm_arena_sprintf(walk->arena, MAKE_VARIABLES "%s", walk->file);
|
||||
}
|
||||
|
||||
static void make_collect(cbm_source_walk_t *walk) {
|
||||
walk->table = walk->writable;
|
||||
make_ctx_t ctx = {.walk = walk, .owner = make_owner(walk)};
|
||||
if (ctx.owner)
|
||||
make_collect_walk(&ctx, walk->root, 0);
|
||||
}
|
||||
|
||||
static void make_resolve(cbm_source_walk_t *walk) {
|
||||
make_ctx_t ctx = {.walk = walk, .owner = make_owner(walk)};
|
||||
if (ctx.owner)
|
||||
make_resolve_walk(&ctx, walk->root, 0);
|
||||
}
|
||||
|
||||
const cbm_source_adapter_t cbm_source_adapter_make = {.language = CBM_LANG_MAKEFILE,
|
||||
.collect = make_collect,
|
||||
.resolve = make_resolve,
|
||||
.decides_references = true,
|
||||
.parse_errors_are_local = true,
|
||||
.one_name_one_node = true};
|
||||
@@ -0,0 +1,412 @@
|
||||
/* Shell binding rules, from the Bash manual's "Command Search and Execution".
|
||||
*
|
||||
* A command name without a slash is looked up as a function first, then as a builtin, then on
|
||||
* PATH, so a function a script defines wins over everything but an alias. Functions live in one
|
||||
* table per shell process, and a process consists of the script and what it `source`s: another
|
||||
* script in the project is another process, however uniquely it names a function.
|
||||
*
|
||||
* A definition takes effect when it runs. A function body runs later, so it can call what is
|
||||
* defined further down; a command at the top level runs now, and only sees definitions above it.
|
||||
* `command foo` and `builtin foo` skip the table by design, and need nothing here: the command
|
||||
* there is `command`. `unset -f foo` makes the name unknown again, and a command computed from a
|
||||
* variable is unknown.
|
||||
*
|
||||
* `source` reaches another file when its path is spelled literally, or relative to the script's
|
||||
* own directory through `$(dirname "$0")`, its BASH_SOURCE spellings, or a variable assigned one
|
||||
* of those once (`ROOT=$(dirname "$0")/..`, `source "$ROOT/lib.sh"`). A literal path is relative
|
||||
* to the working directory: the script's own directory or the repository root. */
|
||||
#include "source_adapter_internal.h"
|
||||
#include <stdio.h>
|
||||
|
||||
#define SHELL_FUNCTIONS "F:"
|
||||
#define SHELL_SOURCES "S:"
|
||||
#define SHELL_UNSET "U:"
|
||||
#define SHELL_VARIABLES "V:"
|
||||
#define SHELL_LOCALS "W:"
|
||||
#define SHELL_FILES "P:"
|
||||
|
||||
enum { SHELL_PATH_BUF = 1024, SHELL_CLOSURE_MAX = 256 };
|
||||
|
||||
typedef struct {
|
||||
cbm_source_walk_t *walk;
|
||||
const char *functions, *sources, *unset, *variables, *locals;
|
||||
const char **closure; /* files this script's process consists of, itself first */
|
||||
size_t closure_count;
|
||||
unsigned function_depth;
|
||||
} shell_ctx_t;
|
||||
|
||||
static const char *shell_directory(cbm_source_walk_t *walk) {
|
||||
const char *slash = strrchr(walk->file, '/');
|
||||
return slash ? cbm_arena_strndup(walk->arena, walk->file, (size_t)(slash - walk->file)) : "";
|
||||
}
|
||||
|
||||
/* Past `$(dirname "$0")` or its BASH_SOURCE spellings: the script's own directory. */
|
||||
static const char *shell_after_dirname(const char *text) {
|
||||
static const char *const forms[] = {"\"$0\"",
|
||||
"$0",
|
||||
"\"${0}\"",
|
||||
"${0}",
|
||||
"\"${BASH_SOURCE[0]}\"",
|
||||
"${BASH_SOURCE[0]}",
|
||||
"\"${BASH_SOURCE}\"",
|
||||
"${BASH_SOURCE}",
|
||||
"\"$BASH_SOURCE\"",
|
||||
"$BASH_SOURCE"};
|
||||
const char *at = strstr(text, "$(dirname ");
|
||||
if (!at)
|
||||
return NULL;
|
||||
at += strlen("$(dirname ");
|
||||
for (size_t i = 0; i < sizeof(forms) / sizeof(forms[0]); i++) {
|
||||
size_t length = strlen(forms[i]);
|
||||
if (!strncmp(at, forms[i], length) && at[length] == ')')
|
||||
return at + length + 1;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static size_t shell_path_span(const char *text) {
|
||||
return strspn(text, "/._-+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
|
||||
}
|
||||
|
||||
/* What a path expression names, relative to the script's directory (*own) or to the working
|
||||
* directory. It reads the forms scripts use to find themselves: a literal, `$(dirname "$0")/x`,
|
||||
* `"$(cd "$(dirname "$0")/.." && pwd)"/x`, and a variable assigned one of those. */
|
||||
static const char *shell_path_expression(shell_ctx_t *ctx, const char *text, bool *own) {
|
||||
cbm_source_walk_t *walk = ctx->walk;
|
||||
*own = false;
|
||||
const char *after = shell_after_dirname(text);
|
||||
if (after) {
|
||||
size_t first = shell_path_span(after);
|
||||
const char *rest = after + first;
|
||||
const char *close = strrchr(rest, ')');
|
||||
rest = close ? close + 1 : rest;
|
||||
while (*rest == '"')
|
||||
rest++;
|
||||
size_t second = shell_path_span(rest);
|
||||
const char *tail = rest + second;
|
||||
while (*tail == '"')
|
||||
tail++;
|
||||
if (*tail)
|
||||
return NULL;
|
||||
*own = true;
|
||||
const char *joined =
|
||||
cbm_arena_sprintf(walk->arena, "%.*s%.*s", (int)first, after, (int)second, rest);
|
||||
while (joined && *joined == '/')
|
||||
joined++;
|
||||
return joined;
|
||||
}
|
||||
const char *at = text;
|
||||
while (*at == '"')
|
||||
at++;
|
||||
if (*at != '$') {
|
||||
if (strpbrk(at, "$`*?"))
|
||||
return NULL;
|
||||
size_t length = strlen(at);
|
||||
while (length && (at[length - 1] == '"' || at[length - 1] == '\''))
|
||||
length--;
|
||||
if (*at == '\'') {
|
||||
at++;
|
||||
length--;
|
||||
}
|
||||
return cbm_arena_strndup(walk->arena, at, length);
|
||||
}
|
||||
at++;
|
||||
bool braced = *at == '{';
|
||||
if (braced)
|
||||
at++;
|
||||
size_t name_size =
|
||||
strspn(at, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_");
|
||||
if (!name_size || (braced && at[name_size] != '}'))
|
||||
return NULL;
|
||||
const char *name = cbm_arena_strndup(walk->arena, at, name_size);
|
||||
const cbm_source_decl_t *variable = name ? cbm_source_lookup(walk, ctx->variables, name) : NULL;
|
||||
if (!variable || (variable->flags & CBM_SOURCE_DECL_F_OPEN) || !variable->type)
|
||||
return NULL;
|
||||
at += name_size + (braced ? 1 : 0);
|
||||
while (*at == '"')
|
||||
at++;
|
||||
size_t tail_size = shell_path_span(at);
|
||||
const char *tail = at + tail_size;
|
||||
while (*tail == '"')
|
||||
tail++;
|
||||
if (*tail)
|
||||
return NULL;
|
||||
*own = true;
|
||||
const char *joined =
|
||||
cbm_arena_sprintf(walk->arena, "%s%.*s", variable->type, (int)tail_size, at);
|
||||
while (joined && *joined == '/')
|
||||
joined++;
|
||||
return joined;
|
||||
}
|
||||
|
||||
static const char *shell_command_word(cbm_source_walk_t *walk, TSNode command) {
|
||||
TSNode name = cbm_source_field(command, "name");
|
||||
if (!cbm_source_is(name, "command_name") || ts_node_named_child_count(name) != 1 ||
|
||||
!cbm_source_is(ts_node_named_child(name, 0), "word"))
|
||||
return NULL;
|
||||
return cbm_source_text(walk, name);
|
||||
}
|
||||
|
||||
/* One chained list per file, under the empty name: a closure walks a file's sources. */
|
||||
static void shell_note_source(shell_ctx_t *ctx, const char *path) {
|
||||
cbm_source_decl_t *decl = cbm_source_declare(ctx->walk, ctx->sources, "", NULL, 0, 0);
|
||||
if (decl)
|
||||
decl->type = cbm_source_table_intern(ctx->walk, path);
|
||||
}
|
||||
|
||||
static void shell_collect_source(shell_ctx_t *ctx, TSNode command) {
|
||||
cbm_source_walk_t *walk = ctx->walk;
|
||||
TSNode argument = cbm_source_field(command, "argument");
|
||||
const char *text = cbm_source_text(walk, argument);
|
||||
bool own = false;
|
||||
const char *path = text ? shell_path_expression(ctx, text, &own) : NULL;
|
||||
if (!path || !*path || *path == '/')
|
||||
return;
|
||||
char joined[SHELL_PATH_BUF];
|
||||
const char *directory = shell_directory(walk);
|
||||
if (directory && cbm_source_join_path(directory, path, joined, sizeof(joined)))
|
||||
shell_note_source(ctx, joined);
|
||||
if (!own && cbm_source_join_path("", path, joined, sizeof(joined)))
|
||||
shell_note_source(ctx, joined);
|
||||
}
|
||||
|
||||
/* `ROOT=$(dirname "$0")/..`: a name for a directory relative to the script. */
|
||||
static void shell_collect_variable(shell_ctx_t *ctx, TSNode assignment) {
|
||||
cbm_source_walk_t *walk = ctx->walk;
|
||||
const char *name = cbm_source_text(walk, cbm_source_field(assignment, "name"));
|
||||
const char *text = cbm_source_text(walk, cbm_source_field(assignment, "value"));
|
||||
if (!name)
|
||||
return;
|
||||
cbm_source_decl_t *known = (cbm_source_decl_t *)cbm_source_lookup(walk, ctx->variables, name);
|
||||
const char *qn = cbm_source_def_qn(walk, name, ts_node_start_point(assignment).row + 1);
|
||||
if (known) {
|
||||
known->flags |= CBM_SOURCE_DECL_F_OPEN;
|
||||
if (!known->qn && qn)
|
||||
known->qn = cbm_source_table_intern(walk, qn);
|
||||
return;
|
||||
}
|
||||
bool own = false;
|
||||
const char *path = text ? shell_path_expression(ctx, text, &own) : NULL;
|
||||
cbm_source_decl_t *decl = cbm_source_declare(walk, ctx->variables, name, qn, 0, 0);
|
||||
if (decl && path && own)
|
||||
decl->type = cbm_source_table_intern(walk, path);
|
||||
}
|
||||
|
||||
/* `local x`, `declare x`: this name is a function's own somewhere in the file, and a read of it
|
||||
* is not decided here. */
|
||||
static void shell_collect_local(shell_ctx_t *ctx, TSNode node) {
|
||||
cbm_source_walk_t *walk = ctx->walk;
|
||||
uint32_t count = ts_node_named_child_count(node);
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
TSNode child = ts_node_named_child(node, i);
|
||||
TSNode bound =
|
||||
cbm_source_is(child, "variable_assignment") ? cbm_source_field(child, "name") : child;
|
||||
if (!cbm_source_is(bound, "variable_name"))
|
||||
continue;
|
||||
const char *name = cbm_source_text(walk, bound);
|
||||
if (name && !cbm_source_lookup(walk, ctx->locals, name))
|
||||
(void)cbm_source_declare(walk, ctx->locals, name, NULL, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void shell_collect_walk(shell_ctx_t *ctx, TSNode node, unsigned depth) {
|
||||
cbm_source_walk_t *walk = ctx->walk;
|
||||
if (!cbm_source_step(walk, depth))
|
||||
return;
|
||||
if (cbm_source_is(node, "function_definition")) {
|
||||
const char *name = cbm_source_text(walk, cbm_source_field(node, "name"));
|
||||
uint32_t line = ts_node_start_point(node).row + 1;
|
||||
const cbm_source_decl_t *first =
|
||||
name ? cbm_source_lookup(walk, ctx->functions, name) : NULL;
|
||||
if (name && !first)
|
||||
(void)cbm_source_declare(walk, ctx->functions, name,
|
||||
cbm_source_def_qn_at(walk, name, line), 0, line);
|
||||
} else if (cbm_source_is(node, "command")) {
|
||||
const char *word = shell_command_word(walk, node);
|
||||
if (word && (!strcmp(word, "source") || !strcmp(word, ".")))
|
||||
shell_collect_source(ctx, node);
|
||||
} else if (cbm_source_is(node, "declaration_command")) {
|
||||
shell_collect_local(ctx, node);
|
||||
} else if (cbm_source_is(node, "variable_assignment")) {
|
||||
shell_collect_variable(ctx, node);
|
||||
} else if (cbm_source_is(node, "unset_command")) {
|
||||
uint32_t count = ts_node_named_child_count(node);
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
TSNode child = ts_node_named_child(node, i);
|
||||
const char *name = cbm_source_text(walk, child);
|
||||
if (name && name[0] != '-')
|
||||
(void)cbm_source_declare(walk, ctx->unset, name, NULL, 0, 0);
|
||||
}
|
||||
}
|
||||
TSTreeCursor cursor = ts_tree_cursor_new(node);
|
||||
if (ts_tree_cursor_goto_first_child(&cursor)) {
|
||||
do {
|
||||
TSNode child = ts_tree_cursor_current_node(&cursor);
|
||||
if (ts_node_is_named(child))
|
||||
shell_collect_walk(ctx, child, depth + 1);
|
||||
} while (ts_tree_cursor_goto_next_sibling(&cursor));
|
||||
}
|
||||
ts_tree_cursor_delete(&cursor);
|
||||
}
|
||||
|
||||
static bool shell_ctx_init(shell_ctx_t *ctx, cbm_source_walk_t *walk) {
|
||||
*ctx = (shell_ctx_t){.walk = walk};
|
||||
ctx->functions = cbm_arena_sprintf(walk->arena, SHELL_FUNCTIONS "%s", walk->file);
|
||||
ctx->sources = cbm_arena_sprintf(walk->arena, SHELL_SOURCES "%s", walk->file);
|
||||
ctx->unset = cbm_arena_sprintf(walk->arena, SHELL_UNSET "%s", walk->file);
|
||||
ctx->variables = cbm_arena_sprintf(walk->arena, SHELL_VARIABLES "%s", walk->file);
|
||||
ctx->locals = cbm_arena_sprintf(walk->arena, SHELL_LOCALS "%s", walk->file);
|
||||
if (!ctx->functions || !ctx->sources || !ctx->unset || !ctx->variables || !ctx->locals) {
|
||||
cbm_source_fail(walk, "shell_scope_allocation");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void shell_collect(cbm_source_walk_t *walk) {
|
||||
walk->table = walk->writable;
|
||||
shell_ctx_t ctx;
|
||||
if (!shell_ctx_init(&ctx, walk))
|
||||
return;
|
||||
(void)cbm_source_declare(walk, SHELL_FILES, walk->file, NULL, 0, 0);
|
||||
shell_collect_walk(&ctx, walk->root, 0);
|
||||
}
|
||||
|
||||
static bool shell_in_closure(const shell_ctx_t *ctx, const char *file) {
|
||||
for (size_t i = 0; i < ctx->closure_count; i++)
|
||||
if (!strcmp(ctx->closure[i], file))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Breadth first through literal `source` statements; each file enters once. Past the bound the
|
||||
* process is not fully known, and a name no file so far declares stays unknown. */
|
||||
static bool shell_build_closure(shell_ctx_t *ctx) {
|
||||
cbm_source_walk_t *walk = ctx->walk;
|
||||
ctx->closure = cbm_arena_calloc(walk->arena, SHELL_CLOSURE_MAX * sizeof(*ctx->closure));
|
||||
if (!ctx->closure)
|
||||
return false;
|
||||
ctx->closure[ctx->closure_count++] = walk->file;
|
||||
for (size_t at = 0; at < ctx->closure_count; at++) {
|
||||
const char *owner = cbm_arena_sprintf(walk->arena, SHELL_SOURCES "%s", ctx->closure[at]);
|
||||
if (!owner)
|
||||
return false;
|
||||
for (const cbm_source_decl_t *entry = cbm_source_lookup(walk, owner, ""); entry;
|
||||
entry = entry->next) {
|
||||
if (!entry->type || !cbm_source_lookup(walk, SHELL_FILES, entry->type) ||
|
||||
shell_in_closure(ctx, entry->type))
|
||||
continue;
|
||||
if (ctx->closure_count == SHELL_CLOSURE_MAX)
|
||||
return true;
|
||||
ctx->closure[ctx->closure_count++] = entry->type;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void shell_visit_command(shell_ctx_t *ctx, TSNode command) {
|
||||
cbm_source_walk_t *walk = ctx->walk;
|
||||
const char *word = shell_command_word(walk, command);
|
||||
if (!word) {
|
||||
cbm_source_emit_node(walk, command, CBM_SOURCE_SITE_CALL, NULL, CBM_RESOLVE_RULE_NONE, NULL,
|
||||
"shell_command_computed");
|
||||
return;
|
||||
}
|
||||
if (cbm_source_lookup(walk, ctx->unset, word)) {
|
||||
cbm_source_emit_node(walk, command, CBM_SOURCE_SITE_CALL, NULL, CBM_RESOLVE_RULE_NONE, NULL,
|
||||
"shell_function_unset_in_this_file");
|
||||
return;
|
||||
}
|
||||
const cbm_source_decl_t *own = cbm_source_lookup(walk, ctx->functions, word);
|
||||
if (own) {
|
||||
uint32_t line = ts_node_start_point(command).row + 1;
|
||||
if (!ctx->function_depth && own->line > line) {
|
||||
cbm_source_emit_node(walk, command, CBM_SOURCE_SITE_CALL, NULL, CBM_RESOLVE_RULE_NONE,
|
||||
NULL, "shell_function_not_yet_defined");
|
||||
return;
|
||||
}
|
||||
cbm_source_emit_node(walk, command, CBM_SOURCE_SITE_CALL, own->qn,
|
||||
CBM_RESOLVE_RULE_SCOPE_BINDING, ctx->functions,
|
||||
own->qn ? "shell_function_table" : "shell_function_has_no_graph_node");
|
||||
return;
|
||||
}
|
||||
const cbm_source_decl_t *found = NULL;
|
||||
for (size_t i = 1; i < ctx->closure_count; i++) {
|
||||
const char *owner = cbm_arena_sprintf(walk->arena, SHELL_FUNCTIONS "%s", ctx->closure[i]);
|
||||
const cbm_source_decl_t *decl = owner ? cbm_source_lookup(walk, owner, word) : NULL;
|
||||
if (!decl)
|
||||
continue;
|
||||
/* The later `source` wins at runtime, and order across files is not modelled. */
|
||||
if (found && (!decl->qn || !found->qn || strcmp(decl->qn, found->qn))) {
|
||||
cbm_source_emit_node(walk, command, CBM_SOURCE_SITE_CALL, NULL, CBM_RESOLVE_RULE_NONE,
|
||||
NULL, "shell_function_in_two_sourced_files");
|
||||
return;
|
||||
}
|
||||
found = decl;
|
||||
}
|
||||
if (found) {
|
||||
cbm_source_emit_node(walk, command, CBM_SOURCE_SITE_CALL, found->qn,
|
||||
CBM_RESOLVE_RULE_IMPORT_BINDING, found->file,
|
||||
found->qn ? "shell_function_from_sourced_file"
|
||||
: "shell_function_has_no_graph_node");
|
||||
return;
|
||||
}
|
||||
cbm_source_emit_node(walk, command, CBM_SOURCE_SITE_CALL, NULL, CBM_RESOLVE_RULE_NONE, NULL,
|
||||
"shell_command_not_a_function_of_this_process");
|
||||
}
|
||||
|
||||
/* `$x` and `${x}`: the variable this file assigns, unless a function here makes the name local. */
|
||||
static void shell_visit_expansion(shell_ctx_t *ctx, TSNode node) {
|
||||
cbm_source_walk_t *walk = ctx->walk;
|
||||
TSNode name = ts_node_named_child_count(node) ? ts_node_named_child(node, 0) : (TSNode){0};
|
||||
if (!cbm_source_is(name, "variable_name"))
|
||||
return;
|
||||
const char *text = cbm_source_text(walk, name);
|
||||
if (!text)
|
||||
return;
|
||||
const cbm_source_decl_t *variable = cbm_source_lookup(walk, ctx->variables, text);
|
||||
bool local = cbm_source_lookup(walk, ctx->locals, text) != NULL;
|
||||
const char *target = variable && !local ? variable->qn : NULL;
|
||||
cbm_source_emit_node(walk, name, CBM_SOURCE_SITE_REFERENCE, target,
|
||||
CBM_RESOLVE_RULE_SCOPE_BINDING, ctx->variables,
|
||||
target ? "shell_variable_of_this_file"
|
||||
: local ? "shell_name_is_local_in_this_file"
|
||||
: "shell_variable_not_assigned_at_the_top_of_this_file");
|
||||
}
|
||||
|
||||
static void shell_resolve_walk(shell_ctx_t *ctx, TSNode node, unsigned depth) {
|
||||
if (!cbm_source_step(ctx->walk, depth))
|
||||
return;
|
||||
bool function = cbm_source_is(node, "function_definition");
|
||||
if (cbm_source_is(node, "command"))
|
||||
shell_visit_command(ctx, node);
|
||||
else if (cbm_source_is(node, "simple_expansion") || cbm_source_is(node, "expansion"))
|
||||
shell_visit_expansion(ctx, node);
|
||||
if (function)
|
||||
ctx->function_depth++;
|
||||
TSTreeCursor cursor = ts_tree_cursor_new(node);
|
||||
if (ts_tree_cursor_goto_first_child(&cursor)) {
|
||||
do {
|
||||
TSNode child = ts_tree_cursor_current_node(&cursor);
|
||||
if (ts_node_is_named(child))
|
||||
shell_resolve_walk(ctx, child, depth + 1);
|
||||
} while (ts_tree_cursor_goto_next_sibling(&cursor));
|
||||
}
|
||||
ts_tree_cursor_delete(&cursor);
|
||||
if (function)
|
||||
ctx->function_depth--;
|
||||
}
|
||||
|
||||
static void shell_resolve(cbm_source_walk_t *walk) {
|
||||
shell_ctx_t ctx;
|
||||
if (!shell_ctx_init(&ctx, walk) || !shell_build_closure(&ctx))
|
||||
return;
|
||||
shell_resolve_walk(&ctx, walk->root, 0);
|
||||
}
|
||||
|
||||
const cbm_source_adapter_t cbm_source_adapter_shell = {.language = CBM_LANG_BASH,
|
||||
.collect = shell_collect,
|
||||
.resolve = shell_resolve,
|
||||
.decides_references = true,
|
||||
.parse_errors_are_local = true,
|
||||
.one_name_one_node = true};
|
||||
@@ -0,0 +1,308 @@
|
||||
/* Tcl binding rules, from the Tcl(n) dodecalogue and the `namespace` manual page.
|
||||
*
|
||||
* Every word in command position is looked up in the command table of the current namespace,
|
||||
* then of the global one, when the command runs; nothing lexical can shadow it, and a variable
|
||||
* lives in another table. `proc` writes that table, so which procs are visible is a matter of
|
||||
* which files ran in the interpreter. `a::b` and `::a::b` name their namespace directly.
|
||||
*
|
||||
* A script's own procs are always visible to it. Another file's proc reaches it only through the
|
||||
* global table of a shared interpreter, and the evidence for one is `source` in the project: a
|
||||
* file sourced by a literal path is library code, and a file that sources by a computed path is
|
||||
* a harness whose procs the scripts it loads can call. A proc declared by two such files leaves
|
||||
* the name open, because the last `source` decides, and `rename` of a name makes it unknown
|
||||
* everywhere. A standalone script's procs are visible to nobody else.
|
||||
*
|
||||
* The Tcl grammar leaves expression syntax as parse errors in most real files. No binding here
|
||||
* depends on the structure around such an error, so the rest of the file still decides. */
|
||||
#include "source_adapter_internal.h"
|
||||
#include <stdio.h>
|
||||
|
||||
#define TCL_PROCS "P:"
|
||||
#define TCL_OWN "H:"
|
||||
#define TCL_SOURCED "L:"
|
||||
|
||||
enum { TCL_PATH_BUF = 1024 };
|
||||
|
||||
typedef struct {
|
||||
cbm_source_walk_t *walk;
|
||||
const char *directory;
|
||||
const char *namespace_name; /* current namespace, "" for the global one */
|
||||
} tcl_ctx_t;
|
||||
|
||||
static const char *tcl_directory(cbm_source_walk_t *walk) {
|
||||
const char *slash = strrchr(walk->file, '/');
|
||||
return slash ? cbm_arena_strndup(walk->arena, walk->file, (size_t)(slash - walk->file)) : "";
|
||||
}
|
||||
|
||||
static const char *tcl_owner(tcl_ctx_t *ctx, const char *table, const char *space) {
|
||||
return cbm_arena_sprintf(ctx->walk->arena, "%s%s", table, space);
|
||||
}
|
||||
|
||||
static const char *tcl_own_owner(tcl_ctx_t *ctx, const char *space) {
|
||||
return cbm_arena_sprintf(ctx->walk->arena, TCL_OWN "%s\x1d%s", ctx->walk->file, space);
|
||||
}
|
||||
|
||||
/* A name's namespace and leaf, from where it is written. `::a::b` is absolute, and so is a name
|
||||
* inside a namespace once it has been joined with that namespace. */
|
||||
static void tcl_split(tcl_ctx_t *ctx, const char *name, const char *space, const char **out_space,
|
||||
const char **out_leaf) {
|
||||
bool absolute = !strncmp(name, "::", 2);
|
||||
const char *body = absolute ? name + 2 : name;
|
||||
const char *last = NULL;
|
||||
for (const char *p = body; (p = strstr(p, "::")); p += 2)
|
||||
last = p;
|
||||
*out_leaf = last ? last + 2 : body;
|
||||
if (!last) {
|
||||
*out_space = absolute ? "" : space;
|
||||
return;
|
||||
}
|
||||
const char *prefix = cbm_arena_strndup(ctx->walk->arena, body, (size_t)(last - body));
|
||||
*out_space = absolute || !*space
|
||||
? prefix
|
||||
: cbm_arena_sprintf(ctx->walk->arena, "%s::%s", space, prefix ? prefix : "");
|
||||
}
|
||||
|
||||
static void tcl_declare_once(tcl_ctx_t *ctx, const char *owner, const char *name, const char *qn) {
|
||||
cbm_source_decl_t *head = (cbm_source_decl_t *)cbm_source_lookup(ctx->walk, owner, name);
|
||||
if (!head) {
|
||||
(void)cbm_source_declare(ctx->walk, owner, name, qn, 0, 0);
|
||||
return;
|
||||
}
|
||||
if (!qn || !head->qn || strcmp(qn, head->qn))
|
||||
head->flags |= CBM_SOURCE_DECL_F_OPEN;
|
||||
}
|
||||
|
||||
static const char *tcl_word(tcl_ctx_t *ctx, TSNode word) {
|
||||
if (cbm_source_is(word, "simple_word"))
|
||||
return cbm_source_text(ctx->walk, word);
|
||||
if (cbm_source_is(word, "quoted_word") && ts_node_named_child_count(word) == 0) {
|
||||
const char *text = cbm_source_text(ctx->walk, word);
|
||||
size_t length = text ? strlen(text) : 0;
|
||||
return length > 2 ? cbm_arena_strndup(ctx->walk->arena, text + 1, length - 2) : NULL;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void tcl_collect_source(tcl_ctx_t *ctx, TSNode arguments) {
|
||||
/* The grammar splits `tests/$name.tcl` into words: the whole list says if it is computed. */
|
||||
const char *text = cbm_source_text(ctx->walk, arguments);
|
||||
size_t length = text ? strlen(text) : 0;
|
||||
const char *path = text && length >= 2 && text[0] == '"' && text[length - 1] == '"'
|
||||
? cbm_arena_strndup(ctx->walk->arena, text + 1, length - 2)
|
||||
: text;
|
||||
if (!path || strpbrk(path, "$[")) {
|
||||
(void)cbm_source_declare(ctx->walk, TCL_SOURCED, ctx->walk->file, NULL, 0, 0);
|
||||
return;
|
||||
}
|
||||
if (!*path || *path == '/')
|
||||
return;
|
||||
char joined[TCL_PATH_BUF];
|
||||
/* Relative to the working directory: the script's own, or the repository's. */
|
||||
if (ctx->directory && cbm_source_join_path(ctx->directory, path, joined, sizeof(joined)))
|
||||
(void)cbm_source_declare(ctx->walk, TCL_SOURCED, joined, NULL, 0, 0);
|
||||
if (cbm_source_join_path("", path, joined, sizeof(joined)))
|
||||
(void)cbm_source_declare(ctx->walk, TCL_SOURCED, joined, NULL, 0, 0);
|
||||
}
|
||||
|
||||
static void tcl_collect_command(tcl_ctx_t *ctx, TSNode command) {
|
||||
const char *name = cbm_source_text(ctx->walk, cbm_source_field(command, "name"));
|
||||
TSNode arguments = cbm_source_field(command, "arguments");
|
||||
if (!name || ts_node_is_null(arguments) || !ts_node_named_child_count(arguments))
|
||||
return;
|
||||
TSNode first = ts_node_named_child(arguments, 0);
|
||||
if (!strcmp(name, "source")) {
|
||||
tcl_collect_source(ctx, arguments);
|
||||
} else if (!strcmp(name, "rename")) {
|
||||
const char *renamed = tcl_word(ctx, first);
|
||||
if (renamed) {
|
||||
const char *space = "", *leaf = renamed;
|
||||
tcl_split(ctx, renamed, ctx->namespace_name, &space, &leaf);
|
||||
const char *owner = tcl_owner(ctx, TCL_PROCS, space ? space : "");
|
||||
if (owner) {
|
||||
tcl_declare_once(ctx, owner, leaf, NULL);
|
||||
tcl_declare_once(ctx, owner, leaf, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void tcl_collect_walk(tcl_ctx_t *ctx, TSNode node, unsigned depth);
|
||||
|
||||
static void tcl_collect_children(tcl_ctx_t *ctx, TSNode node, unsigned depth) {
|
||||
TSTreeCursor cursor = ts_tree_cursor_new(node);
|
||||
if (ts_tree_cursor_goto_first_child(&cursor)) {
|
||||
do {
|
||||
TSNode child = ts_tree_cursor_current_node(&cursor);
|
||||
if (ts_node_is_named(child))
|
||||
tcl_collect_walk(ctx, child, depth + 1);
|
||||
} while (ts_tree_cursor_goto_next_sibling(&cursor));
|
||||
}
|
||||
ts_tree_cursor_delete(&cursor);
|
||||
}
|
||||
|
||||
/* `namespace eval a { ... }`: the namespace a word list names, NULL for any other subcommand. */
|
||||
static const char *tcl_namespace_of(tcl_ctx_t *ctx, TSNode node, TSNode *body) {
|
||||
TSNode words = (TSNode){0};
|
||||
uint32_t count = ts_node_named_child_count(node);
|
||||
for (uint32_t i = 0; i < count && ts_node_is_null(words); i++)
|
||||
if (cbm_source_is(ts_node_named_child(node, i), "word_list"))
|
||||
words = ts_node_named_child(node, i);
|
||||
if (ts_node_is_null(words) || ts_node_named_child_count(words) < 3)
|
||||
return NULL;
|
||||
const char *verb = cbm_source_text(ctx->walk, ts_node_named_child(words, 0));
|
||||
const char *name = tcl_word(ctx, ts_node_named_child(words, 1));
|
||||
if (!verb || strcmp(verb, "eval") || !name)
|
||||
return NULL;
|
||||
*body = ts_node_named_child(words, 2);
|
||||
const char *leaf = NULL, *space = NULL;
|
||||
const char *full = cbm_arena_sprintf(ctx->walk->arena, "%s::x", name);
|
||||
if (!full)
|
||||
return NULL;
|
||||
tcl_split(ctx, full, ctx->namespace_name, &space, &leaf);
|
||||
return space;
|
||||
}
|
||||
|
||||
static void tcl_collect_walk(tcl_ctx_t *ctx, TSNode node, unsigned depth) {
|
||||
cbm_source_walk_t *walk = ctx->walk;
|
||||
if (!cbm_source_step(walk, depth))
|
||||
return;
|
||||
if (cbm_source_is(node, "procedure")) {
|
||||
const char *name = cbm_source_text(walk, cbm_source_field(node, "name"));
|
||||
if (name) {
|
||||
const char *space = "", *leaf = name;
|
||||
tcl_split(ctx, name, ctx->namespace_name, &space, &leaf);
|
||||
const char *qn = cbm_source_def_qn_at(walk, name, ts_node_start_point(node).row + 1);
|
||||
const char *project = tcl_owner(ctx, TCL_PROCS, space ? space : "");
|
||||
const char *own = tcl_own_owner(ctx, space ? space : "");
|
||||
if (project && own) {
|
||||
tcl_declare_once(ctx, project, leaf, qn);
|
||||
tcl_declare_once(ctx, own, leaf, qn);
|
||||
}
|
||||
}
|
||||
} else if (cbm_source_is(node, "command")) {
|
||||
tcl_collect_command(ctx, node);
|
||||
} else if (cbm_source_is(node, "namespace")) {
|
||||
TSNode body = (TSNode){0};
|
||||
const char *inner = tcl_namespace_of(ctx, node, &body);
|
||||
if (inner) {
|
||||
const char *outer = ctx->namespace_name;
|
||||
ctx->namespace_name = inner;
|
||||
tcl_collect_children(ctx, body, depth);
|
||||
ctx->namespace_name = outer;
|
||||
return;
|
||||
}
|
||||
}
|
||||
tcl_collect_children(ctx, node, depth);
|
||||
}
|
||||
|
||||
static void tcl_collect(cbm_source_walk_t *walk) {
|
||||
walk->table = walk->writable;
|
||||
tcl_ctx_t ctx = {.walk = walk, .namespace_name = ""};
|
||||
ctx.directory = tcl_directory(walk);
|
||||
if (!ctx.directory) {
|
||||
cbm_source_fail(walk, "tcl_scope_allocation");
|
||||
return;
|
||||
}
|
||||
tcl_collect_walk(&ctx, walk->root, 0);
|
||||
}
|
||||
|
||||
static void tcl_emit_abstain(tcl_ctx_t *ctx, TSNode command, const char *reason) {
|
||||
cbm_source_emit_node(ctx->walk, command, CBM_SOURCE_SITE_CALL, NULL, CBM_RESOLVE_RULE_NONE,
|
||||
NULL, reason);
|
||||
}
|
||||
|
||||
/* A command's own namespace first, then the global one, for a name with no `::` in it. */
|
||||
static const cbm_source_decl_t *tcl_lookup_table(tcl_ctx_t *ctx, const char *space,
|
||||
const char *leaf, bool own) {
|
||||
const char *owner = own ? tcl_own_owner(ctx, space) : tcl_owner(ctx, TCL_PROCS, space);
|
||||
return owner ? cbm_source_lookup(ctx->walk, owner, leaf) : NULL;
|
||||
}
|
||||
|
||||
static void tcl_visit_command(tcl_ctx_t *ctx, TSNode command) {
|
||||
cbm_source_walk_t *walk = ctx->walk;
|
||||
TSNode name_node = cbm_source_field(command, "name");
|
||||
const char *name =
|
||||
cbm_source_is(name_node, "simple_word") ? cbm_source_text(walk, name_node) : NULL;
|
||||
if (!name || strpbrk(name, "$[")) {
|
||||
tcl_emit_abstain(ctx, command, "tcl_command_computed");
|
||||
return;
|
||||
}
|
||||
const char *space = "", *leaf = name;
|
||||
bool qualified = strstr(name, "::") != NULL;
|
||||
tcl_split(ctx, name, ctx->namespace_name, &space, &leaf);
|
||||
if (!space)
|
||||
space = "";
|
||||
/* Inside a namespace an unqualified name tries it, then the global one. */
|
||||
const char *tries[2] = {space, qualified || !*space ? NULL : ""};
|
||||
for (size_t i = 0; i < 2 && tries[i]; i++) {
|
||||
const cbm_source_decl_t *own = tcl_lookup_table(ctx, tries[i], leaf, true);
|
||||
const cbm_source_decl_t *project = tcl_lookup_table(ctx, tries[i], leaf, false);
|
||||
if (!project)
|
||||
continue;
|
||||
/* `rename` marked the shared entry: the name is not what its proc declared. */
|
||||
if (own && !(own->flags & CBM_SOURCE_DECL_F_OPEN)) {
|
||||
cbm_source_emit_node(
|
||||
walk, command, CBM_SOURCE_SITE_CALL, own->qn, CBM_RESOLVE_RULE_SCOPE_BINDING,
|
||||
walk->file, own->qn ? "tcl_proc_of_this_script" : "tcl_proc_has_no_graph_node");
|
||||
return;
|
||||
}
|
||||
if (project->flags & CBM_SOURCE_DECL_F_OPEN) {
|
||||
tcl_emit_abstain(ctx, command, "tcl_proc_declared_by_more_than_one_file");
|
||||
return;
|
||||
}
|
||||
if (!cbm_source_lookup(walk, TCL_SOURCED, project->file)) {
|
||||
tcl_emit_abstain(ctx, command, "tcl_proc_of_a_file_nothing_sources");
|
||||
return;
|
||||
}
|
||||
cbm_source_emit_node(walk, command, CBM_SOURCE_SITE_CALL, project->qn,
|
||||
CBM_RESOLVE_RULE_IMPORT_BINDING, project->file,
|
||||
project->qn ? "tcl_proc_of_a_sourced_file"
|
||||
: "tcl_proc_has_no_graph_node");
|
||||
return;
|
||||
}
|
||||
tcl_emit_abstain(ctx, command, "tcl_command_not_a_project_proc");
|
||||
}
|
||||
|
||||
static void tcl_resolve_walk(tcl_ctx_t *ctx, TSNode node, unsigned depth);
|
||||
|
||||
static void tcl_resolve_children(tcl_ctx_t *ctx, TSNode node, unsigned depth) {
|
||||
TSTreeCursor cursor = ts_tree_cursor_new(node);
|
||||
if (ts_tree_cursor_goto_first_child(&cursor)) {
|
||||
do {
|
||||
TSNode child = ts_tree_cursor_current_node(&cursor);
|
||||
if (ts_node_is_named(child))
|
||||
tcl_resolve_walk(ctx, child, depth + 1);
|
||||
} while (ts_tree_cursor_goto_next_sibling(&cursor));
|
||||
}
|
||||
ts_tree_cursor_delete(&cursor);
|
||||
}
|
||||
|
||||
static void tcl_resolve_walk(tcl_ctx_t *ctx, TSNode node, unsigned depth) {
|
||||
if (!cbm_source_step(ctx->walk, depth))
|
||||
return;
|
||||
if (cbm_source_is(node, "command")) {
|
||||
tcl_visit_command(ctx, node);
|
||||
} else if (cbm_source_is(node, "namespace")) {
|
||||
TSNode body = (TSNode){0};
|
||||
const char *inner = tcl_namespace_of(ctx, node, &body);
|
||||
if (inner) {
|
||||
const char *outer = ctx->namespace_name;
|
||||
ctx->namespace_name = inner;
|
||||
tcl_resolve_children(ctx, body, depth);
|
||||
ctx->namespace_name = outer;
|
||||
return;
|
||||
}
|
||||
}
|
||||
tcl_resolve_children(ctx, node, depth);
|
||||
}
|
||||
|
||||
static void tcl_resolve(cbm_source_walk_t *walk) {
|
||||
tcl_ctx_t ctx = {.walk = walk, .namespace_name = ""};
|
||||
tcl_resolve_walk(&ctx, walk->root, 0);
|
||||
}
|
||||
|
||||
const cbm_source_adapter_t cbm_source_adapter_tcl = {.language = CBM_LANG_TCL,
|
||||
.collect = tcl_collect,
|
||||
.resolve = tcl_resolve,
|
||||
.parse_errors_are_local = true,
|
||||
.one_name_one_node = true};
|
||||
@@ -0,0 +1,250 @@
|
||||
/* Vim script binding rules, from `:help user-function`, `:help script-local` and
|
||||
* `:help autoload`.
|
||||
*
|
||||
* The prefix of a function name says where it lives, and nothing can move it:
|
||||
* s:Name, <SID>Name the script's own functions, one namespace per file;
|
||||
* a#b#c an autoload function, which only runtime file autoload/a/b.vim defines;
|
||||
* Name, g:Name one global namespace for the whole editor session.
|
||||
* A bare name that starts in lower case is a builtin: `:function` refuses to declare one, so a
|
||||
* project declaration can never be what `get(...)` means.
|
||||
*
|
||||
* A global is read from the file's own declaration when it has one, then from the one project
|
||||
* file that declares it. Two files declaring a global leave it open: which one is in effect
|
||||
* depends on what was sourced last. A variable holding a Funcref is looked up before a function
|
||||
* of that name (`let F = function('X')`, `call F()`), so a capitalized name a file also assigns
|
||||
* abstains there. A dict function is selected by the dict's runtime value and stays unknown. */
|
||||
#include "source_adapter_internal.h"
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#define VIM_GLOBAL "G"
|
||||
#define VIM_AUTOLOAD "A"
|
||||
#define VIM_SCRIPT "S:"
|
||||
#define VIM_OWN "H:"
|
||||
#define VIM_ASSIGNED "V:"
|
||||
|
||||
typedef struct {
|
||||
cbm_source_walk_t *walk;
|
||||
const char *script_owner;
|
||||
const char *own_owner;
|
||||
const char *assigned_owner;
|
||||
} vim_ctx_t;
|
||||
|
||||
static bool vim_script_scope(const char *text) {
|
||||
return !strncmp(text, "s:", 2) || !strncmp(text, "<SID>", 5) || !strncmp(text, "<sid>", 5);
|
||||
}
|
||||
|
||||
static void vim_declare_once(vim_ctx_t *ctx, const char *owner, const char *name, const char *qn) {
|
||||
cbm_source_decl_t *head = (cbm_source_decl_t *)cbm_source_lookup(ctx->walk, owner, name);
|
||||
if (!head) {
|
||||
(void)cbm_source_declare(ctx->walk, owner, name, qn, 0, 0);
|
||||
return;
|
||||
}
|
||||
if (!qn || !head->qn || strcmp(qn, head->qn))
|
||||
head->flags |= CBM_SOURCE_DECL_F_OPEN;
|
||||
}
|
||||
|
||||
/* a#b#c belongs in autoload/a/b.vim, the only file `:help autoload` reads for it. */
|
||||
static bool vim_autoload_file(const vim_ctx_t *ctx, const char *name) {
|
||||
const char *last = strrchr(name, '#');
|
||||
const char *file = ctx->walk->file;
|
||||
if (!last || !file)
|
||||
return false;
|
||||
char path[512];
|
||||
int length = snprintf(path, sizeof(path), "autoload/%.*s.vim", (int)(last - name), name);
|
||||
if (length <= 0 || (size_t)length >= sizeof(path))
|
||||
return false;
|
||||
for (char *p = path; *p; p++)
|
||||
if (*p == '#')
|
||||
*p = '/';
|
||||
size_t size = strlen(file);
|
||||
if (size < (size_t)length || strcmp(file + size - (size_t)length, path))
|
||||
return false;
|
||||
return size == (size_t)length || file[size - (size_t)length - 1] == '/';
|
||||
}
|
||||
|
||||
static void vim_collect_function(vim_ctx_t *ctx, TSNode node) {
|
||||
cbm_source_walk_t *walk = ctx->walk;
|
||||
TSNode declaration = (TSNode){0};
|
||||
uint32_t count = ts_node_named_child_count(node);
|
||||
for (uint32_t i = 0; i < count && ts_node_is_null(declaration); i++)
|
||||
if (cbm_source_is(ts_node_named_child(node, i), "function_declaration"))
|
||||
declaration = ts_node_named_child(node, i);
|
||||
TSNode name = cbm_source_field(declaration, "name");
|
||||
const char *text = cbm_source_text(walk, name);
|
||||
if (!text)
|
||||
return;
|
||||
const char *qn = cbm_source_def_qn_at(walk, text, ts_node_start_point(node).row + 1);
|
||||
if (cbm_source_is(name, "scoped_identifier")) {
|
||||
const char *bare = cbm_source_text(walk, ts_node_named_child(name, 1));
|
||||
if (!bare)
|
||||
return;
|
||||
if (vim_script_scope(text)) {
|
||||
vim_declare_once(ctx, ctx->script_owner, bare, qn);
|
||||
} else if (!strncmp(text, "g:", 2)) {
|
||||
vim_declare_once(ctx, VIM_GLOBAL, bare, qn);
|
||||
vim_declare_once(ctx, ctx->own_owner, bare, qn);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!cbm_source_is(name, "identifier"))
|
||||
return;
|
||||
if (strchr(text, '#')) {
|
||||
/* The file's own definition is what it reads, wherever the file sits. */
|
||||
vim_declare_once(ctx, ctx->own_owner, text, qn);
|
||||
if (vim_autoload_file(ctx, text))
|
||||
vim_declare_once(ctx, VIM_AUTOLOAD, text, qn);
|
||||
return;
|
||||
}
|
||||
vim_declare_once(ctx, VIM_GLOBAL, text, qn);
|
||||
vim_declare_once(ctx, ctx->own_owner, text, qn);
|
||||
}
|
||||
|
||||
/* A capitalized name assigned anywhere in the file can hold a Funcref. */
|
||||
static void vim_collect_assigned(vim_ctx_t *ctx, TSNode node) {
|
||||
if (!ts_node_named_child_count(node))
|
||||
return;
|
||||
TSNode target = ts_node_named_child(node, 0);
|
||||
if (cbm_source_is(target, "scoped_identifier"))
|
||||
target = ts_node_named_child(target, 1);
|
||||
if (!cbm_source_is(target, "identifier"))
|
||||
return;
|
||||
const char *text = cbm_source_text(ctx->walk, target);
|
||||
if (text && isupper((unsigned char)text[0]))
|
||||
vim_declare_once(ctx, ctx->assigned_owner, text, NULL);
|
||||
}
|
||||
|
||||
static void vim_collect_walk(vim_ctx_t *ctx, TSNode node, unsigned depth) {
|
||||
if (!cbm_source_step(ctx->walk, depth))
|
||||
return;
|
||||
if (cbm_source_is(node, "function_definition"))
|
||||
vim_collect_function(ctx, node);
|
||||
else if (cbm_source_is(node, "let_statement") || cbm_source_is(node, "const_statement") ||
|
||||
cbm_source_is(node, "for_loop"))
|
||||
vim_collect_assigned(ctx, node);
|
||||
TSTreeCursor cursor = ts_tree_cursor_new(node);
|
||||
if (ts_tree_cursor_goto_first_child(&cursor)) {
|
||||
do {
|
||||
TSNode child = ts_tree_cursor_current_node(&cursor);
|
||||
if (ts_node_is_named(child))
|
||||
vim_collect_walk(ctx, child, depth + 1);
|
||||
} while (ts_tree_cursor_goto_next_sibling(&cursor));
|
||||
}
|
||||
ts_tree_cursor_delete(&cursor);
|
||||
}
|
||||
|
||||
static bool vim_ctx_init(vim_ctx_t *ctx, cbm_source_walk_t *walk) {
|
||||
*ctx = (vim_ctx_t){.walk = walk};
|
||||
ctx->script_owner = cbm_arena_sprintf(walk->arena, VIM_SCRIPT "%s", walk->file);
|
||||
ctx->own_owner = cbm_arena_sprintf(walk->arena, VIM_OWN "%s", walk->file);
|
||||
ctx->assigned_owner = cbm_arena_sprintf(walk->arena, VIM_ASSIGNED "%s", walk->file);
|
||||
if (!ctx->script_owner || !ctx->own_owner || !ctx->assigned_owner) {
|
||||
cbm_source_fail(walk, "vim_scope_allocation");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void vim_collect(cbm_source_walk_t *walk) {
|
||||
walk->table = walk->writable;
|
||||
vim_ctx_t ctx;
|
||||
if (vim_ctx_init(&ctx, walk))
|
||||
vim_collect_walk(&ctx, walk->root, 0);
|
||||
}
|
||||
|
||||
static void vim_emit(vim_ctx_t *ctx, TSNode call, const cbm_source_decl_t *decl, const char *domain,
|
||||
const char *absent) {
|
||||
if (!decl) {
|
||||
cbm_source_emit_node(ctx->walk, call, CBM_SOURCE_SITE_CALL, NULL, CBM_RESOLVE_RULE_NONE,
|
||||
NULL, absent);
|
||||
return;
|
||||
}
|
||||
if (decl->flags & CBM_SOURCE_DECL_F_OPEN) {
|
||||
cbm_source_emit_node(ctx->walk, call, CBM_SOURCE_SITE_CALL, NULL, CBM_RESOLVE_RULE_NONE,
|
||||
NULL, "vim_declared_by_more_than_one_file");
|
||||
return;
|
||||
}
|
||||
cbm_resolve_rule_t rule = strcmp(decl->file, ctx->walk->file) ? CBM_RESOLVE_RULE_IMPORT_BINDING
|
||||
: CBM_RESOLVE_RULE_SCOPE_BINDING;
|
||||
cbm_source_emit_node(ctx->walk, call, CBM_SOURCE_SITE_CALL, decl->qn, rule, domain,
|
||||
decl->qn ? "vim_namespace_declaration" : "vim_function_has_no_graph_node");
|
||||
}
|
||||
|
||||
static void vim_emit_global(vim_ctx_t *ctx, TSNode call, const char *name) {
|
||||
const cbm_source_decl_t *own = cbm_source_lookup(ctx->walk, ctx->own_owner, name);
|
||||
vim_emit(ctx, call, own ? own : cbm_source_lookup(ctx->walk, VIM_GLOBAL, name), VIM_GLOBAL,
|
||||
"vim_global_not_declared_in_project");
|
||||
}
|
||||
|
||||
static void vim_visit_call(vim_ctx_t *ctx, TSNode call) {
|
||||
cbm_source_walk_t *walk = ctx->walk;
|
||||
TSNode function = cbm_source_field(call, "function");
|
||||
const char *text = cbm_source_text(walk, function);
|
||||
if (!text)
|
||||
return;
|
||||
if (cbm_source_is(function, "scoped_identifier")) {
|
||||
const char *bare = cbm_source_text(walk, ts_node_named_child(function, 1));
|
||||
if (!bare)
|
||||
return;
|
||||
if (vim_script_scope(text)) {
|
||||
vim_emit(ctx, call, cbm_source_lookup(walk, ctx->script_owner, bare), ctx->script_owner,
|
||||
"vim_script_function_not_in_this_file");
|
||||
} else if (!strncmp(text, "g:", 2)) {
|
||||
vim_emit_global(ctx, call, bare);
|
||||
} else {
|
||||
cbm_source_emit_node(walk, call, CBM_SOURCE_SITE_CALL, NULL, CBM_RESOLVE_RULE_NONE,
|
||||
NULL, "vim_funcref_in_a_variable");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!cbm_source_is(function, "identifier")) {
|
||||
cbm_source_emit_node(walk, call, CBM_SOURCE_SITE_CALL, NULL, CBM_RESOLVE_RULE_NONE, NULL,
|
||||
"vim_callee_is_a_runtime_value");
|
||||
return;
|
||||
}
|
||||
if (strchr(text, '#')) {
|
||||
const cbm_source_decl_t *own = cbm_source_lookup(walk, ctx->own_owner, text);
|
||||
vim_emit(ctx, call, own ? own : cbm_source_lookup(walk, VIM_AUTOLOAD, text), VIM_AUTOLOAD,
|
||||
"vim_autoload_file_not_in_project");
|
||||
return;
|
||||
}
|
||||
if (!isupper((unsigned char)text[0])) {
|
||||
cbm_source_emit_node(walk, call, CBM_SOURCE_SITE_CALL, NULL, CBM_RESOLVE_RULE_NONE, NULL,
|
||||
"vim_builtin_function");
|
||||
return;
|
||||
}
|
||||
if (cbm_source_lookup(walk, ctx->assigned_owner, text)) {
|
||||
cbm_source_emit_node(walk, call, CBM_SOURCE_SITE_CALL, NULL, CBM_RESOLVE_RULE_NONE, NULL,
|
||||
"vim_name_also_assigned_a_funcref");
|
||||
return;
|
||||
}
|
||||
vim_emit_global(ctx, call, text);
|
||||
}
|
||||
|
||||
static void vim_resolve_walk(vim_ctx_t *ctx, TSNode node, unsigned depth) {
|
||||
if (!cbm_source_step(ctx->walk, depth))
|
||||
return;
|
||||
if (cbm_source_is(node, "call_expression"))
|
||||
vim_visit_call(ctx, node);
|
||||
TSTreeCursor cursor = ts_tree_cursor_new(node);
|
||||
if (ts_tree_cursor_goto_first_child(&cursor)) {
|
||||
do {
|
||||
TSNode child = ts_tree_cursor_current_node(&cursor);
|
||||
if (ts_node_is_named(child))
|
||||
vim_resolve_walk(ctx, child, depth + 1);
|
||||
} while (ts_tree_cursor_goto_next_sibling(&cursor));
|
||||
}
|
||||
ts_tree_cursor_delete(&cursor);
|
||||
}
|
||||
|
||||
static void vim_resolve(cbm_source_walk_t *walk) {
|
||||
vim_ctx_t ctx;
|
||||
if (vim_ctx_init(&ctx, walk))
|
||||
vim_resolve_walk(&ctx, walk->root, 0);
|
||||
}
|
||||
|
||||
const cbm_source_adapter_t cbm_source_adapter_vim = {.language = CBM_LANG_VIMSCRIPT,
|
||||
.collect = vim_collect,
|
||||
.resolve = vim_resolve,
|
||||
.parse_errors_are_local = true,
|
||||
.one_name_one_node = true};
|
||||
+369
-82
@@ -62,6 +62,8 @@ typedef struct {
|
||||
const cbm_resolve_js_import_t *js_import;
|
||||
/* A module-level class's own `constructor`, which `new` runs. */
|
||||
const char *js_constructor;
|
||||
/* `const { m: x } = f()`: what calling x runs is f's returned m. */
|
||||
const char *js_call_callee, *js_call_member;
|
||||
/* One-based index of a Python import below module level or under a
|
||||
* statement: the extractor records only top-level imports. */
|
||||
uint32_t nested_import;
|
||||
@@ -125,6 +127,20 @@ typedef struct {
|
||||
bool self_is_class;
|
||||
} binding_scope_t;
|
||||
|
||||
/* One member of a `return { ... }` in a module-level function. A NULL member is a return of
|
||||
* anything else, which leaves the function's value open. */
|
||||
typedef struct {
|
||||
const char *function_name, *function_target;
|
||||
uint32_t function_start, function_end;
|
||||
const char *member;
|
||||
const char *local;
|
||||
uint32_t scope;
|
||||
const char *value_name;
|
||||
uint32_t value_start, value_end;
|
||||
const char *target;
|
||||
bool conflict;
|
||||
} js_returned_t;
|
||||
|
||||
/* A module-scope write or an `exports.x = v` value, captured during the walk
|
||||
* and settled once every declaration in the file is known. */
|
||||
typedef struct {
|
||||
@@ -264,6 +280,8 @@ struct cbm_resolve_binding_index {
|
||||
size_t import_count, import_capacity;
|
||||
js_write_t *js_writes;
|
||||
size_t js_write_count, js_write_capacity;
|
||||
const char **markup_writes;
|
||||
size_t markup_write_count;
|
||||
js_raw_export_t *js_raw_exports;
|
||||
size_t js_raw_export_count, js_raw_export_capacity;
|
||||
cbm_resolve_js_export_t *js_exports;
|
||||
@@ -273,6 +291,8 @@ struct cbm_resolve_binding_index {
|
||||
size_t js_export_alias_count, js_export_alias_capacity;
|
||||
js_member_t *js_members;
|
||||
size_t js_member_count, js_member_capacity;
|
||||
js_returned_t *js_returned;
|
||||
size_t js_returned_count, js_returned_capacity;
|
||||
const char *current_this_owner;
|
||||
bool current_this_prototype;
|
||||
};
|
||||
@@ -893,52 +913,8 @@ static bool supported_node(CBMLanguage language, const char *kind) {
|
||||
"keyword_pattern",
|
||||
"type_alias_statement",
|
||||
NULL};
|
||||
static const char *const lua[] = {"chunk",
|
||||
"block",
|
||||
"function_declaration",
|
||||
"function_definition",
|
||||
"parameters",
|
||||
"identifier",
|
||||
"variable",
|
||||
"variable_list",
|
||||
"expression_list",
|
||||
"function_call",
|
||||
"arguments",
|
||||
"return_statement",
|
||||
"assignment_statement",
|
||||
"variable_declaration",
|
||||
"number",
|
||||
"string",
|
||||
"string_content",
|
||||
"escape_sequence",
|
||||
"true",
|
||||
"false",
|
||||
"nil",
|
||||
"binary_expression",
|
||||
"unary_expression",
|
||||
"parenthesized_expression",
|
||||
"if_statement",
|
||||
"elseif_statement",
|
||||
"else_statement",
|
||||
"while_statement",
|
||||
"repeat_statement",
|
||||
"for_statement",
|
||||
"for_numeric_clause",
|
||||
"for_generic_clause",
|
||||
"do_statement",
|
||||
"break_statement",
|
||||
"comment",
|
||||
"empty_statement",
|
||||
"table_constructor",
|
||||
"field",
|
||||
"dot_index_expression",
|
||||
"method_index_expression",
|
||||
"bracket_index_expression",
|
||||
NULL};
|
||||
if (language == CBM_LANG_PYTHON)
|
||||
return listed(kind, python) || listed(kind, python_expressions) ||
|
||||
listed(kind, python_binders);
|
||||
return listed(kind, lua);
|
||||
(void)language;
|
||||
return listed(kind, python) || listed(kind, python_expressions) || listed(kind, python_binders);
|
||||
}
|
||||
|
||||
/* These grammar containers add no binders. The ordinary walk still audits
|
||||
@@ -1019,12 +995,8 @@ static bool dynamic_name(CBMLanguage language, const char *name) {
|
||||
* here. __import__ loads a module and returns it. exec, eval, globals and
|
||||
* locals/vars are classified at the occurrence below. */
|
||||
static const char *const python[] = {"exec", "eval", "globals", NULL};
|
||||
static const char *const lua[] = {"exec", "eval", "globals", "locals", "vars",
|
||||
"setattr", "delattr", "__import__", "__dict__", "load",
|
||||
"loadfile", "dofile", "loadstring", "setfenv", "getfenv",
|
||||
"debug", "_ENV", "_G", "rawset", NULL};
|
||||
/* Lua environment operations have no special meaning in Python. */
|
||||
return listed(name, language == CBM_LANG_LUA ? lua : python);
|
||||
(void)language;
|
||||
return listed(name, python);
|
||||
}
|
||||
|
||||
/* An identifier is a lexical read only outside the member and keyword roles:
|
||||
@@ -1129,19 +1101,6 @@ static bool python_namespace_view(cbm_resolve_binding_index_t *index, TSNode nod
|
||||
return !in_function && (ts_node_is_null(args) || ts_node_named_child_count(args) == 0);
|
||||
}
|
||||
|
||||
static bool lua_local_function(TSNode node) {
|
||||
if (!ts_node_is_null(field(node, "local_declaration"))) {
|
||||
return true;
|
||||
}
|
||||
uint32_t children = ts_node_child_count(node);
|
||||
for (uint32_t i = 0; i < children; i++) {
|
||||
if (kind_is(ts_node_child(node, i), "local")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* A name bound by `if TYPE_CHECKING:` exists only for annotations; the
|
||||
* extractor keeps that distinction and this audit leaves such names blocked. */
|
||||
static bool python_type_checking_import(cbm_resolve_binding_index_t *index, TSNode node) {
|
||||
@@ -1276,7 +1235,7 @@ static void note_import(cbm_resolve_binding_index_t *index, TSNode node, bool ne
|
||||
}
|
||||
|
||||
static void note_call(cbm_resolve_binding_index_t *index, TSNode node) {
|
||||
TSNode callee = field(node, index->language == CBM_LANG_LUA ? "name" : "function");
|
||||
TSNode callee = field(node, "function");
|
||||
if (kind_is(node, "new_expression")) {
|
||||
callee = field(node, "constructor");
|
||||
}
|
||||
@@ -1306,6 +1265,9 @@ typedef struct {
|
||||
unsigned char js_binder, js_declaration, js_for_binder;
|
||||
bool js_type_level, js_ambient, js_body, js_no_site, js_this_prototype;
|
||||
const char *js_this_owner;
|
||||
/* The module-level function a `return` here belongs to; NULL in anything nested. */
|
||||
const char *js_function_name;
|
||||
uint32_t js_function_start, js_function_end;
|
||||
} binding_walk_t;
|
||||
|
||||
/* Function defaults/annotations and class bases execute in the enclosing
|
||||
@@ -2926,27 +2888,37 @@ static void js_member_value(cbm_resolve_binding_index_t *index, js_member_t *row
|
||||
static void note_javascript_object_members(cbm_resolve_binding_index_t *index, const char *owner,
|
||||
TSNode object, bool prototype) {
|
||||
uint32_t count = ts_node_named_child_count(object);
|
||||
/* A later key wins, so `{ ...base, m() {} }` has this m whatever base holds. A spread or a
|
||||
* computed key can only replace what comes before it. */
|
||||
uint32_t settled_from = 0;
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
TSNode item = ts_node_named_child(object, i);
|
||||
bool computed = kind_is(item, "spread_element") ||
|
||||
((kind_is(item, "pair") || kind_is(item, "method_definition")) &&
|
||||
!kind_is(kind_is(item, "pair") ? field(item, "key") : field(item, "name"),
|
||||
"property_identifier"));
|
||||
if (computed)
|
||||
settled_from = i + 1;
|
||||
}
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
TSNode item = ts_node_named_child(object, i);
|
||||
const char *member = NULL;
|
||||
js_member_t *row = NULL;
|
||||
if (kind_is(item, "spread_element")) {
|
||||
(void)js_member_row(index, owner, JS_MEMBER_COMPUTED, prototype);
|
||||
} else if (kind_is(item, "shorthand_property_identifier")) {
|
||||
if (kind_is(item, "shorthand_property_identifier")) {
|
||||
member = node_text(index, item);
|
||||
row = js_member_row(index, owner, member, prototype);
|
||||
if (row)
|
||||
if (row) {
|
||||
row->local = member;
|
||||
row->conflict = i < settled_from;
|
||||
}
|
||||
} else if (kind_is(item, "pair") || kind_is(item, "method_definition")) {
|
||||
TSNode key = kind_is(item, "pair") ? field(item, "key") : field(item, "name");
|
||||
if (!kind_is(key, "property_identifier")) {
|
||||
(void)js_member_row(index, owner, JS_MEMBER_COMPUTED, prototype);
|
||||
if (!kind_is(key, "property_identifier"))
|
||||
continue;
|
||||
}
|
||||
member = node_text(index, key);
|
||||
row = js_member_row(index, owner, member, prototype);
|
||||
/* `get m()` runs on the read; what a call then invokes is its return value. */
|
||||
if (row && (child_token(item, "get") || child_token(item, "set")))
|
||||
if (row && (i < settled_from || child_token(item, "get") || child_token(item, "set")))
|
||||
row->conflict = true;
|
||||
else if (row)
|
||||
js_member_value(index, row, kind_is(item, "pair") ? field(item, "value") : item,
|
||||
@@ -3546,6 +3518,43 @@ static const char *js_require_pattern_member(cbm_resolve_binding_index_t *index,
|
||||
return member;
|
||||
}
|
||||
|
||||
/* The `variable_declarator` whose pattern this name sits in, however deep. */
|
||||
static TSNode js_pattern_declarator(TSNode node) {
|
||||
for (TSNode at = ts_node_parent(node); !ts_node_is_null(at); at = ts_node_parent(at)) {
|
||||
if (kind_is(at, "variable_declarator"))
|
||||
return at;
|
||||
if (!kind_is(at, "object_pattern") && !kind_is(at, "array_pattern") &&
|
||||
!kind_is(at, "pair_pattern") && !kind_is(at, "object_assignment_pattern") &&
|
||||
!kind_is(at, "assignment_pattern") && !kind_is(at, "rest_pattern"))
|
||||
break;
|
||||
}
|
||||
return (TSNode){0};
|
||||
}
|
||||
|
||||
/* `const { m, n: x } = f()`: the pattern's own member of one plain call. */
|
||||
static void js_note_destructured_call(cbm_resolve_binding_index_t *index, binding_name_t *row,
|
||||
TSNode node, TSNode declarator) {
|
||||
TSNode value = js_unwrap_value(field(declarator, "value"));
|
||||
TSNode callee = kind_is(value, "call_expression") ? field(value, "function") : (TSNode){0};
|
||||
if (!kind_is(callee, "identifier") || child_token(value, "optional_chain"))
|
||||
return;
|
||||
TSNode pattern = ts_node_parent(node);
|
||||
const char *member = NULL;
|
||||
if (kind_is(node, "shorthand_property_identifier_pattern") &&
|
||||
kind_is(pattern, "object_pattern")) {
|
||||
member = row->name;
|
||||
} else if (kind_is(pattern, "pair_pattern") && ts_node_eq(node, field(pattern, "value")) &&
|
||||
kind_is(field(pattern, "key"), "property_identifier")) {
|
||||
member = node_text(index, field(pattern, "key"));
|
||||
pattern = ts_node_parent(pattern);
|
||||
}
|
||||
if (!member || !kind_is(pattern, "object_pattern") ||
|
||||
!ts_node_eq(pattern, field(declarator, "name")))
|
||||
return;
|
||||
row->js_call_callee = node_text(index, callee);
|
||||
row->js_call_member = member;
|
||||
}
|
||||
|
||||
static void note_javascript_binder(cbm_resolve_binding_index_t *index, TSNode node,
|
||||
const char *name, const binding_walk_t *state) {
|
||||
if (state->js_binder == JS_BINDER_WRITE) {
|
||||
@@ -3602,6 +3611,23 @@ static void note_javascript_binder(cbm_resolve_binding_index_t *index, TSNode no
|
||||
js_set_lines(index, row, declarator);
|
||||
if (index->name_count > row)
|
||||
index->names[row].at_start = ts_node_start_byte(declarator);
|
||||
} else if (!direct && !ts_node_is_null(js_pattern_declarator(node))) {
|
||||
/* `const { a, b: c } = f()`: the name is declared here, whatever the value turns out
|
||||
* to be, so a read of it names this variable. Calling it stays with what it holds.
|
||||
* Only module level has a Variable node to name. */
|
||||
TSNode whole = js_pattern_declarator(node);
|
||||
bool is_const = kind_is(ts_node_parent(whole), "lexical_declaration") &&
|
||||
child_token(ts_node_parent(whole), "const");
|
||||
add_name(index, name, ts_node_end_byte(whole),
|
||||
!module_level ? BINDING_BLOCKED
|
||||
: BINDING_VALUE_DECLARATION | (is_const ? BINDING_CONST_VALUE : 0));
|
||||
if (module_level)
|
||||
js_set_lines(index, row, whole);
|
||||
if (index->name_count > row) {
|
||||
index->names[row].at_start = ts_node_start_byte(whole);
|
||||
if (is_const)
|
||||
js_note_destructured_call(index, &index->names[row], node, whole);
|
||||
}
|
||||
} else {
|
||||
add_name(index, name, 0, BINDING_BLOCKED);
|
||||
if (direct && kind_is(value, "this") && state->js_this_owner && index->name_count > row) {
|
||||
@@ -3635,6 +3661,92 @@ static bool javascript_named_function_scope_kind(const char *kind) {
|
||||
return !strcmp(kind, "function_expression") || !strcmp(kind, "generator_function");
|
||||
}
|
||||
|
||||
static js_returned_t *js_returned_row(cbm_resolve_binding_index_t *index,
|
||||
const binding_walk_t *state, const char *member) {
|
||||
js_returned_t *items = grow_array(index, index->js_returned, index->js_returned_count,
|
||||
&index->js_returned_capacity, sizeof(*items));
|
||||
if (!items)
|
||||
return NULL;
|
||||
index->js_returned = items;
|
||||
js_returned_t *row = &items[index->js_returned_count++];
|
||||
*row = (js_returned_t){.function_name = state->js_function_name,
|
||||
.function_start = state->js_function_start,
|
||||
.function_end = state->js_function_end,
|
||||
.member = member,
|
||||
.scope = state->scope};
|
||||
return row;
|
||||
}
|
||||
|
||||
static void js_returned_function(js_returned_t *row, TSNode value, const char *name) {
|
||||
row->value_name = name;
|
||||
row->value_start = ts_node_start_point(value).row + 1;
|
||||
row->value_end = ts_node_end_point(value).row + 1;
|
||||
}
|
||||
|
||||
/* What a module-level function returns. A later key wins, so only a spread or a computed key can
|
||||
* take a member from what is spelled before it. */
|
||||
static void js_note_returned(cbm_resolve_binding_index_t *index, TSNode value,
|
||||
const binding_walk_t *state) {
|
||||
if (!kind_is(value, "object")) {
|
||||
(void)js_returned_row(index, state, NULL);
|
||||
return;
|
||||
}
|
||||
size_t first = index->js_returned_count;
|
||||
TSTreeCursor cursor = ts_tree_cursor_new(value);
|
||||
if (ts_tree_cursor_goto_first_child(&cursor)) {
|
||||
do {
|
||||
TSNode item = ts_tree_cursor_current_node(&cursor);
|
||||
if (!ts_node_is_named(item) || kind_is(item, "comment"))
|
||||
continue;
|
||||
TSNode key = kind_is(item, "pair") ? field(item, "key")
|
||||
: kind_is(item, "method_definition") ? field(item, "name")
|
||||
: (TSNode){0};
|
||||
if (kind_is(item, "shorthand_property_identifier")) {
|
||||
const char *member = node_text(index, item);
|
||||
js_returned_t *row = js_returned_row(index, state, member);
|
||||
if (row)
|
||||
row->local = member;
|
||||
} else if (kind_is(key, "property_identifier")) {
|
||||
const char *member = node_text(index, key);
|
||||
js_returned_t *row = js_returned_row(index, state, member);
|
||||
TSNode held = kind_is(item, "pair") ? js_unwrap_value(field(item, "value")) : item;
|
||||
if (!row)
|
||||
continue;
|
||||
if (kind_is(held, "identifier"))
|
||||
row->local = node_text(index, held);
|
||||
else if (kind_is(item, "pair") && js_function_value(held))
|
||||
js_returned_function(row, held, member);
|
||||
else if (kind_is(item, "method_definition") && !child_token(item, "get") &&
|
||||
!child_token(item, "set"))
|
||||
js_returned_function(row, held, member);
|
||||
else
|
||||
row->conflict = true;
|
||||
} else {
|
||||
for (size_t r = first; r < index->js_returned_count; r++)
|
||||
index->js_returned[r].conflict = true;
|
||||
}
|
||||
} while (ts_tree_cursor_goto_next_sibling(&cursor));
|
||||
}
|
||||
ts_tree_cursor_delete(&cursor);
|
||||
}
|
||||
|
||||
/* A module-level function, by the name and lines its extracted definition has. */
|
||||
static void js_enter_function(cbm_resolve_binding_index_t *index, TSNode node,
|
||||
binding_walk_t *state) {
|
||||
state->js_function_name = NULL;
|
||||
if (state->scope != 0)
|
||||
return;
|
||||
TSNode name = field(node, "name");
|
||||
TSNode parent = ts_node_is_null(name) ? ts_node_parent(node) : (TSNode){0};
|
||||
if (kind_is(parent, "variable_declarator") && ts_node_eq(node, field(parent, "value")))
|
||||
name = field(parent, "name");
|
||||
if (!kind_is(name, "identifier"))
|
||||
return;
|
||||
state->js_function_name = node_text(index, name);
|
||||
state->js_function_start = ts_node_start_point(node).row + 1;
|
||||
state->js_function_end = ts_node_end_point(node).row + 1;
|
||||
}
|
||||
|
||||
static binding_walk_t note_javascript_node(cbm_resolve_binding_index_t *index, TSNode node,
|
||||
const char *parent_kind, const char *field_name,
|
||||
unsigned depth, binding_walk_t state) {
|
||||
@@ -3742,7 +3854,12 @@ static binding_walk_t note_javascript_node(cbm_resolve_binding_index_t *index, T
|
||||
state.js_no_site = true;
|
||||
}
|
||||
|
||||
if (kind_is(node, "return_statement") && state.js_function_name &&
|
||||
ts_node_named_child_count(node))
|
||||
js_note_returned(index, js_unwrap_value(ts_node_named_child(node, 0)), &state);
|
||||
|
||||
if (javascript_runtime_function(node)) {
|
||||
js_enter_function(index, node, &state);
|
||||
javascript_this_owner(index, node, &state);
|
||||
index->current_this_owner = state.js_this_owner;
|
||||
index->current_this_prototype = state.js_this_prototype;
|
||||
@@ -3756,6 +3873,11 @@ static binding_walk_t note_javascript_node(cbm_resolve_binding_index_t *index, T
|
||||
index->current_scope = state.scope;
|
||||
state.js_body = true;
|
||||
state.named_export = false;
|
||||
if (state.js_function_name && kind_is(node, "arrow_function")) {
|
||||
TSNode body = js_unwrap_value(field(node, "body"));
|
||||
if (!kind_is(body, "statement_block"))
|
||||
js_note_returned(index, body, &state);
|
||||
}
|
||||
/* A function expression's own name is visible only inside it. */
|
||||
if (javascript_named_function_scope_kind(kind)) {
|
||||
const char *text = node_text(index, field(node, "name"));
|
||||
@@ -4564,8 +4686,7 @@ static binding_walk_t note_node(cbm_resolve_binding_index_t *index, TSNode node,
|
||||
bool decorated = index->language == CBM_LANG_PYTHON && parent_kind &&
|
||||
!strcmp(parent_kind, "decorated_definition");
|
||||
bool supported =
|
||||
kind_is(name, "identifier") && !decorated &&
|
||||
(index->language == CBM_LANG_PYTHON || (depth == 1 && lua_local_function(node)));
|
||||
kind_is(name, "identifier") && !decorated && index->language == CBM_LANG_PYTHON;
|
||||
if (!ts_node_is_null(name)) {
|
||||
const char *text = node_text(index, name);
|
||||
add_name(index, text, ts_node_start_byte(node),
|
||||
@@ -4793,6 +4914,23 @@ static bool js_row_in_space(const binding_name_t *row, js_space_t space) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Markup assigning a script's `let` or function rebinds it like a script write would. Assigning
|
||||
* a const is the compiler's unref, `x.value = v`, which leaves the const's value in place. */
|
||||
static void js_note_markup_writes(cbm_resolve_binding_index_t *index) {
|
||||
for (size_t w = 0; w < index->markup_write_count; w++) {
|
||||
const char *name = index->markup_writes[w];
|
||||
size_t high = 0;
|
||||
size_t low = js_name_range(index, name, strlen(name), 0, &high);
|
||||
for (size_t r = low; r < high; r++) {
|
||||
const binding_name_t *row = &index->names[r];
|
||||
if (js_row_in_space(row, JS_SPACE_VALUE) && !(row->flags & BINDING_CONST_VALUE)) {
|
||||
js_write(index, name, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void js_apply_writes(cbm_resolve_binding_index_t *index) {
|
||||
size_t missing = 0;
|
||||
for (size_t w = 0; w < index->js_write_count; w++) {
|
||||
@@ -5214,6 +5352,112 @@ static void js_settle_members(cbm_resolve_binding_index_t *index) {
|
||||
}
|
||||
}
|
||||
|
||||
static const binding_name_t *find_lexical_name(const cbm_resolve_binding_index_t *index,
|
||||
const char *name, size_t length, uint32_t scope,
|
||||
cbm_resolve_lookup_t *query, js_space_t space);
|
||||
|
||||
static int js_compare_returned(const void *left, const void *right) {
|
||||
const js_returned_t *a = left, *b = right;
|
||||
int order = strcmp(a->function_target ? a->function_target : "",
|
||||
b->function_target ? b->function_target : "");
|
||||
if (order)
|
||||
return order;
|
||||
/* The rows that leave the whole function open come first. */
|
||||
return strcmp(a->member ? a->member : "", b->member ? b->member : "");
|
||||
}
|
||||
|
||||
/* What each function's `return` statements agree a member holds. */
|
||||
static void js_settle_returned(cbm_resolve_binding_index_t *index) {
|
||||
int definitions = index->result->defs.count;
|
||||
if (!index->js_returned_count || !definitions ||
|
||||
index->analysis != CBM_RESOLVE_ANALYSIS_COMPLETE)
|
||||
return;
|
||||
const CBMDefinition **sorted =
|
||||
cbm_arena_alloc(index->arena, (size_t)definitions * sizeof(*sorted));
|
||||
if (!sorted) {
|
||||
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "binding_member_allocation");
|
||||
return;
|
||||
}
|
||||
for (int d = 0; d < definitions; d++)
|
||||
sorted[d] = &index->result->defs.items[d];
|
||||
qsort(sorted, (size_t)definitions, sizeof(*sorted), js_compare_definition);
|
||||
for (size_t r = 0; r < index->js_returned_count; r++) {
|
||||
js_returned_t *row = &index->js_returned[r];
|
||||
js_raw_export_t function = {.function_name = row->function_name,
|
||||
.start_line = row->function_start,
|
||||
.end_line = row->function_end};
|
||||
row->function_target = js_function_target(index, sorted, &function);
|
||||
if (!row->member || row->conflict)
|
||||
continue;
|
||||
if (row->value_name) {
|
||||
js_raw_export_t value = {.function_name = row->value_name,
|
||||
.start_line = row->value_start,
|
||||
.end_line = row->value_end};
|
||||
row->target = js_function_target(index, sorted, &value);
|
||||
} else if (row->local) {
|
||||
const binding_name_t *binding = find_lexical_name(index, row->local, strlen(row->local),
|
||||
row->scope, NULL, JS_SPACE_VALUE);
|
||||
if (binding && !(binding->flags & BINDING_BLOCKED) &&
|
||||
(binding->flags & BINDING_DECLARATION))
|
||||
row->target = binding->target;
|
||||
}
|
||||
}
|
||||
qsort(index->js_returned, index->js_returned_count, sizeof(*index->js_returned),
|
||||
js_compare_returned);
|
||||
const char *open_function = NULL;
|
||||
for (size_t r = 0; r < index->js_returned_count;) {
|
||||
js_returned_t *row = &index->js_returned[r];
|
||||
size_t next = r + 1;
|
||||
bool same = row->target != NULL;
|
||||
while (next < index->js_returned_count &&
|
||||
!js_compare_returned(row, &index->js_returned[next])) {
|
||||
const char *other = index->js_returned[next].target;
|
||||
same = same && other && !strcmp(row->target, other);
|
||||
next++;
|
||||
}
|
||||
if (!row->member)
|
||||
open_function = row->function_target;
|
||||
bool open = !same || !row->function_target ||
|
||||
(open_function && !strcmp(open_function, row->function_target));
|
||||
for (size_t at = r; at < next; at++)
|
||||
index->js_returned[at].target = open ? NULL : row->target;
|
||||
r = next;
|
||||
}
|
||||
}
|
||||
|
||||
bool cbm_resolve_binding_js_returned_member_at(const cbm_resolve_binding_index_t *index,
|
||||
size_t offset, const char **function,
|
||||
const char **member, const char **target) {
|
||||
if (!index || !javascript_language(index->language) ||
|
||||
index->analysis != CBM_RESOLVE_ANALYSIS_COMPLETE || offset >= index->js_returned_count)
|
||||
return false;
|
||||
const js_returned_t *row = &index->js_returned[offset];
|
||||
*function = row->function_target;
|
||||
*member = row->member;
|
||||
*target = row->target;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cbm_resolve_binding_js_literal_member_at(const cbm_resolve_binding_index_t *index,
|
||||
size_t offset, const char **owner,
|
||||
const char **member, const char **target) {
|
||||
if (!index || !javascript_language(index->language) ||
|
||||
index->analysis != CBM_RESOLVE_ANALYSIS_COMPLETE || offset >= index->js_member_count)
|
||||
return false;
|
||||
const js_member_t *row = &index->js_members[offset];
|
||||
bool conflict = false;
|
||||
const binding_name_t *holder = js_module_row(index, row->owner, JS_SPACE_VALUE, &conflict);
|
||||
js_member_t computed = {.owner = row->owner, .member = JS_MEMBER_COMPUTED};
|
||||
bool open = !holder || conflict || (holder->flags & (BINDING_BLOCKED | BINDING_WRITTEN)) ||
|
||||
holder->js_import || row->conflict || row->prototype ||
|
||||
bsearch(&computed, index->js_members, index->js_member_count,
|
||||
sizeof(*index->js_members), js_compare_member);
|
||||
*owner = holder ? holder->target : NULL;
|
||||
*member = row->member;
|
||||
*target = open ? NULL : row->target;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cbm_resolve_binding_js_exports_complete(const cbm_resolve_binding_index_t *index) {
|
||||
return index && javascript_language(index->language) &&
|
||||
index->analysis == CBM_RESOLVE_ANALYSIS_COMPLETE && !index->js_exports_open;
|
||||
@@ -5652,8 +5896,8 @@ static cbm_resolve_binding_index_t *binding_build(CBMArena *arena, const CBMFile
|
||||
return index;
|
||||
}
|
||||
bool component = language == CBM_LANG_VUE || language == CBM_LANG_SVELTE;
|
||||
if (language != CBM_LANG_PYTHON && language != CBM_LANG_LUA && language != CBM_LANG_DART &&
|
||||
language != CBM_LANG_RUST && !javascript_language(language) && !component) {
|
||||
if (language != CBM_LANG_PYTHON && language != CBM_LANG_DART && language != CBM_LANG_RUST &&
|
||||
!javascript_language(language) && !component) {
|
||||
return index;
|
||||
}
|
||||
if (!result || !source || !result->module_qn || source_len > UINT32_MAX || result->has_error ||
|
||||
@@ -5695,7 +5939,11 @@ static cbm_resolve_binding_index_t *binding_build(CBMArena *arena, const CBMFile
|
||||
.language = language,
|
||||
.module_qn = result->module_qn,
|
||||
.root = ts_tree_root_node(tree)};
|
||||
const char **markup_writes = NULL;
|
||||
size_t markup_write_count = cbm_component_template_writes(&host, &markup_writes);
|
||||
TSTree *script = cbm_component_binding_tree(&host, &language, &range);
|
||||
index->markup_writes = markup_writes;
|
||||
index->markup_write_count = markup_write_count;
|
||||
if (owned_tree)
|
||||
ts_tree_delete(owned_tree);
|
||||
tree = owned_tree = script;
|
||||
@@ -5762,10 +6010,12 @@ static cbm_resolve_binding_index_t *binding_build(CBMArena *arena, const CBMFile
|
||||
python_refuse_class_comprehension_reads(index);
|
||||
}
|
||||
if (javascript_language(language)) {
|
||||
js_note_markup_writes(index);
|
||||
js_apply_writes(index);
|
||||
audit_javascript_values(index);
|
||||
js_settle_exports(index);
|
||||
js_settle_members(index);
|
||||
js_settle_returned(index);
|
||||
js_settle_constructors(index);
|
||||
}
|
||||
/* An explicit replacement of annotation storage can invoke user code
|
||||
@@ -6158,6 +6408,37 @@ const cbm_resolve_js_import_t *cbm_resolve_binding_js_import(
|
||||
static const js_member_t *js_site_member(const cbm_resolve_binding_index_t *index, const char *name,
|
||||
const binding_site_t *site, cbm_resolve_lookup_t *query);
|
||||
|
||||
bool cbm_resolve_binding_js_destructured_call(const cbm_resolve_binding_index_t *index,
|
||||
const char *name, uint32_t start, uint32_t end,
|
||||
CBMSourceOrigin origin,
|
||||
const cbm_resolve_js_import_t **import,
|
||||
const char **callee, const char **function,
|
||||
const char **member) {
|
||||
if (!index || !javascript_language(index->language) ||
|
||||
index->analysis != CBM_RESOLVE_ANALYSIS_COMPLETE || !name ||
|
||||
origin != CBM_SOURCE_ORIGIN_RAW || end <= start || !index->sites || !index->site_count)
|
||||
return false;
|
||||
binding_site_t key = {.name = name, .start = start, .end = end};
|
||||
const binding_site_t *site =
|
||||
bsearch(&key, index->sites, index->site_count, sizeof(*index->sites), compare_site);
|
||||
if (!site || site->member_path || !site->invocation)
|
||||
return false;
|
||||
const binding_name_t *binding =
|
||||
find_lexical_name(index, name, strlen(name), site->scope, NULL, JS_SPACE_VALUE);
|
||||
if (!binding || !binding->js_call_member)
|
||||
return false;
|
||||
const binding_name_t *called =
|
||||
find_lexical_name(index, binding->js_call_callee, strlen(binding->js_call_callee),
|
||||
binding->scope, NULL, JS_SPACE_VALUE);
|
||||
if (!called || (called->flags & BINDING_BLOCKED))
|
||||
return false;
|
||||
*import = called->js_import;
|
||||
*callee = binding->js_call_callee;
|
||||
*function = (called->flags & BINDING_DECLARATION) ? called->target : NULL;
|
||||
*member = binding->js_call_member;
|
||||
return *import || *function;
|
||||
}
|
||||
|
||||
bool cbm_resolve_binding_js_declaration_lines(const cbm_resolve_binding_index_t *index,
|
||||
const char *name, uint32_t start, uint32_t end,
|
||||
CBMSourceOrigin origin, uint32_t *start_line,
|
||||
@@ -6263,6 +6544,11 @@ cbm_resolve_result_t cbm_resolve_binding_validate(const cbm_resolve_binding_inde
|
||||
find_lexical_name(index, spelled, root_length, located->scope, &query, space);
|
||||
/* `import type` names a type, so a type position may use it. */
|
||||
bool type_import = binding && located->type_position && (binding->flags & BINDING_TYPE_IMPORT);
|
||||
/* `const { m } = f(); m()`: f's returned m, which the caller established. */
|
||||
bool returned_member = ecma && !dot && binding && binding->js_call_member &&
|
||||
candidate.rule == CBM_RESOLVE_RULE_IMPORT_BINDING &&
|
||||
(prior_facts & CBM_RESOLVE_FACT_IMPORT_BOUND) &&
|
||||
(prior_facts & CBM_RESOLVE_FACT_LOOKUP_COMPLETE);
|
||||
/* A later declaration binds a closure that runs after it, in any scope but
|
||||
* its own and never inside its own initializer. */
|
||||
bool too_early = binding && start < binding->at &&
|
||||
@@ -6276,8 +6562,9 @@ cbm_resolve_result_t cbm_resolve_binding_validate(const cbm_resolve_binding_inde
|
||||
candidate.rule == CBM_RESOLVE_RULE_IMPORT_BINDING &&
|
||||
(prior_facts & CBM_RESOLVE_FACT_IMPORT_BOUND) &&
|
||||
(prior_facts & CBM_RESOLVE_FACT_LOOKUP_COMPLETE);
|
||||
if (!dart_import && (!binding || ((binding->flags & BINDING_BLOCKED) && !type_import) ||
|
||||
(too_early && !python_deferred_site(index, located, binding)))) {
|
||||
if (!dart_import &&
|
||||
(!binding || ((binding->flags & BINDING_BLOCKED) && !type_import && !returned_member) ||
|
||||
(too_early && !python_deferred_site(index, located, binding)))) {
|
||||
candidate.reason = "binding_shadowed_or_unavailable";
|
||||
return binding_query_finish(index, &query, candidate);
|
||||
}
|
||||
@@ -6288,7 +6575,7 @@ cbm_resolve_result_t cbm_resolve_binding_validate(const cbm_resolve_binding_inde
|
||||
return binding_query_finish(index, &query, candidate);
|
||||
}
|
||||
}
|
||||
if (binding && (binding->flags & BINDING_VALUE_DECLARATION) &&
|
||||
if (binding && (binding->flags & BINDING_VALUE_DECLARATION) && !returned_member &&
|
||||
(located->invocation ||
|
||||
(prior_facts & (CBM_RESOLVE_FACT_CALLABLE_TYPED | CBM_RESOLVE_FACT_RECEIVER_TYPED |
|
||||
CBM_RESOLVE_FACT_TYPE_BOUND | CBM_RESOLVE_FACT_CLASS_CALLABLE)))) {
|
||||
@@ -6303,7 +6590,7 @@ cbm_resolve_result_t cbm_resolve_binding_validate(const cbm_resolve_binding_inde
|
||||
binding->target && strcmp(binding->target, candidate.target) == 0) {
|
||||
candidate.facts |= CBM_RESOLVE_FACT_SCOPE_BOUND;
|
||||
} else if (candidate.rule == CBM_RESOLVE_RULE_IMPORT_BINDING &&
|
||||
((binding->flags & BINDING_IMPORT) || type_import) &&
|
||||
((binding->flags & BINDING_IMPORT) || type_import || returned_member) &&
|
||||
(prior_facts & CBM_RESOLVE_FACT_IMPORT_BOUND) &&
|
||||
(prior_facts & CBM_RESOLVE_FACT_LOOKUP_COMPLETE)) {
|
||||
candidate.facts |= CBM_RESOLVE_FACT_SCOPE_BOUND;
|
||||
|
||||
@@ -158,6 +158,25 @@ const cbm_resolve_js_import_t *cbm_resolve_binding_js_import(const cbm_resolve_b
|
||||
cbm_resolve_result_t cbm_resolve_binding_js_member(const cbm_resolve_binding_index_t *,
|
||||
const char *name, uint32_t start, uint32_t end,
|
||||
CBMSourceOrigin origin);
|
||||
/* The module-level object literals of this file, member by member, for another file's `x.m()`
|
||||
* through an import of `x`: QN of x, the member's name, and the declaration it holds. False past
|
||||
* the end; a member with no one declaration, or whose owner is written, reports a NULL target. */
|
||||
bool cbm_resolve_binding_js_literal_member_at(const cbm_resolve_binding_index_t *, size_t offset,
|
||||
const char **owner, const char **member,
|
||||
const char **target);
|
||||
/* The members of `return { ... }` in this file's module-level functions, for another file's
|
||||
* `const { m } = f(); m()`: QN of f, the member's name, and the declaration every return gives it.
|
||||
* False past the end; a member with no one declaration reports a NULL target. */
|
||||
bool cbm_resolve_binding_js_returned_member_at(const cbm_resolve_binding_index_t *, size_t offset,
|
||||
const char **function, const char **member,
|
||||
const char **target);
|
||||
/* This invocation calls a const destructured from one plain call: `const { m: x } = f(); x()`.
|
||||
* f is an import or a declaration of this file; member is m. */
|
||||
bool cbm_resolve_binding_js_destructured_call(const cbm_resolve_binding_index_t *, const char *name,
|
||||
uint32_t start, uint32_t end, CBMSourceOrigin origin,
|
||||
const cbm_resolve_js_import_t **import,
|
||||
const char **callee, const char **function,
|
||||
const char **member);
|
||||
/* This occurrence names a type, in a type annotation or as its `Ns` path root. */
|
||||
bool cbm_resolve_binding_js_type_occurrence(const cbm_resolve_binding_index_t *, const char *name,
|
||||
uint32_t start, uint32_t end, CBMSourceOrigin origin);
|
||||
|
||||
@@ -53,12 +53,12 @@ static const edge_row_t edge_rows[] = {
|
||||
"entries abstain."},
|
||||
{CBM_LANG_ARKTS, SOURCE_EDGES,
|
||||
"Source JavaScript-style imports only; containing-language typed dispatch is unsupported."},
|
||||
{CBM_LANG_VUE, CALLABLE_EDGES,
|
||||
"One inline script with audited lexical bindings and an inert template; multiple script "
|
||||
"contexts, template effects and external script sources abstain."},
|
||||
{CBM_LANG_SVELTE, CALLABLE_EDGES,
|
||||
"One inline script with audited lexical bindings; only static markup and bare-identifier "
|
||||
"event references are supported. Multiple contexts and template effects abstain."},
|
||||
{CBM_LANG_VUE, SOURCE_EDGES | EDGE(WRITES) | EDGE(USES_TYPE),
|
||||
"Inline scripts as one module under the ECMAScript resolver; an external script source and "
|
||||
"an unparsed script language abstain, and expressions in the template are not occurrences."},
|
||||
{CBM_LANG_SVELTE, SOURCE_EDGES | EDGE(WRITES) | EDGE(USES_TYPE),
|
||||
"Inline scripts as one module under the ECMAScript resolver; an external script source and "
|
||||
"an unparsed script language abstain, and expressions in markup are not occurrences."},
|
||||
{CBM_LANG_PYTHON, SOURCE_EDGES | EDGE(WRITES) | EDGE(USES_TYPE),
|
||||
"Static project imports from the file's own search path, reexports and closed star imports; "
|
||||
"dynamic imports, unknown star sources and competing search-path candidates abstain."},
|
||||
@@ -107,7 +107,17 @@ static const edge_row_t edge_rows[] = {
|
||||
"pubspec's own name and path dependencies; declared and constructed receiver types, and "
|
||||
"class type arguments fixed in extends clauses, along class, mixin and superclass chains. "
|
||||
"Collection element types, conditional imports and part-of by library name abstain."},
|
||||
{CBM_LANG_LUA, EDGE(CALLS), NULL},
|
||||
{CBM_LANG_VIMSCRIPT, EDGE(CALLS), NULL},
|
||||
{CBM_LANG_MAKEFILE, EDGE(USAGE), NULL},
|
||||
{CBM_LANG_BASH, EDGE(IMPORTS) | EDGE(CALLS) | EDGE(USAGE),
|
||||
"source of a literal path relative to the script's directory or the repository root; a path "
|
||||
"computed at runtime abstains."},
|
||||
{CBM_LANG_TCL, EDGE(IMPORTS) | EDGE(CALLS),
|
||||
"source of a literal path relative to the script's directory or the repository root; a path "
|
||||
"computed at runtime abstains."},
|
||||
{CBM_LANG_LUA, EDGE(IMPORTS) | CALLABLE_EDGES | EDGE(USAGE),
|
||||
"require of a literal found through package.path's default forms under the requiring file's "
|
||||
"ancestors, or under one project lua/ root; other package.path entries abstain."},
|
||||
{CBM_LANG_CSS, EDGE(IMPORTS),
|
||||
"Top-level quoted literal imports before style rules, relative to the source stylesheet; "
|
||||
"URL functions, escaping, layer ordering and remote paths abstain."},
|
||||
@@ -269,14 +279,11 @@ bool cbm_resolve_capabilities(CBMLanguage language, cbm_resolve_capabilities_t *
|
||||
case CBM_LANG_VUE:
|
||||
case CBM_LANG_SVELTE:
|
||||
out->adapter = "embedded_javascript_lexical";
|
||||
out->proof_subsets = CBM_RESOLVE_CAP_SCOPE | CBM_RESOLVE_CAP_CALLABLE;
|
||||
out->limitation = language == CBM_LANG_VUE
|
||||
? "One inline script with audited lexical bindings and an inert "
|
||||
"template; multiple scripts, template effects and external script "
|
||||
"sources abstain."
|
||||
: "One inline script with audited lexical bindings; static markup "
|
||||
"and bare-identifier event references only. Multiple script "
|
||||
"contexts and template effects abstain.";
|
||||
out->proof_subsets =
|
||||
CBM_RESOLVE_CAP_SCOPE | CBM_RESOLVE_CAP_IMPORT_EXPORT | CBM_RESOLVE_CAP_CALLABLE;
|
||||
out->limitation = "Inline scripts as one module under the ECMAScript resolver. Markup "
|
||||
"assignments rebind what they spell; expressions in markup are not "
|
||||
"occurrences, and an external script source abstains.";
|
||||
break;
|
||||
case CBM_LANG_GO:
|
||||
out->adapter = "go";
|
||||
@@ -473,11 +480,37 @@ bool cbm_resolve_capabilities(CBMLanguage language, cbm_resolve_capabilities_t *
|
||||
"receiver types with class type arguments fixed by subclasses; collection "
|
||||
"element types and conditional imports abstain.";
|
||||
break;
|
||||
case CBM_LANG_MAKEFILE:
|
||||
out->adapter = "make_source";
|
||||
out->proof_subsets = CBM_RESOLVE_CAP_SCOPE;
|
||||
out->limitation = "Variables assigned in the file that reads them; included files, the "
|
||||
"environment and computed names abstain.";
|
||||
break;
|
||||
case CBM_LANG_VIMSCRIPT:
|
||||
out->adapter = "vim_source";
|
||||
out->proof_subsets = CBM_RESOLVE_CAP_SCOPE | CBM_RESOLVE_CAP_IMPORT_EXPORT;
|
||||
out->limitation = "Script-local, autoload and global function namespaces by name prefix; "
|
||||
"dict functions and Funcrefs in variables abstain.";
|
||||
break;
|
||||
case CBM_LANG_BASH:
|
||||
out->adapter = "shell_source";
|
||||
out->proof_subsets = CBM_RESOLVE_CAP_SCOPE | CBM_RESOLVE_CAP_IMPORT_EXPORT;
|
||||
out->limitation = "A script's functions and those of files it sources by a literal path; "
|
||||
"another script is another process, and computed commands abstain.";
|
||||
break;
|
||||
case CBM_LANG_TCL:
|
||||
out->adapter = "tcl_source";
|
||||
out->proof_subsets = CBM_RESOLVE_CAP_SCOPE | CBM_RESOLVE_CAP_IMPORT_EXPORT;
|
||||
out->limitation = "A script's own procs, and procs of a file the project sources by a "
|
||||
"literal path; computed commands and renamed procs abstain.";
|
||||
break;
|
||||
case CBM_LANG_LUA:
|
||||
out->adapter = "lua_source";
|
||||
out->proof_subsets = CBM_RESOLVE_CAP_SCOPE;
|
||||
out->limitation = "Direct local function bindings; runtime require/export and receiver "
|
||||
"type proofs unimplemented.";
|
||||
out->proof_subsets = CBM_RESOLVE_CAP_SCOPE | CBM_RESOLVE_CAP_IMPORT_EXPORT |
|
||||
CBM_RESOLVE_CAP_MEMBER | CBM_RESOLVE_CAP_TYPE_FLOW;
|
||||
out->limitation = "Lexical scope, the one global namespace, table members, require through "
|
||||
"package.path and metatable __index. A value with more than one "
|
||||
"assignment, computed keys and untyped parameters abstain.";
|
||||
break;
|
||||
case CBM_LANG_ARKTS:
|
||||
out->adapter = "javascript_source";
|
||||
|
||||
@@ -37,7 +37,9 @@ static const cbm_source_adapter_t *adapter_for(CBMLanguage language) {
|
||||
&cbm_source_adapter_php, &cbm_source_adapter_elixir, &cbm_source_adapter_erlang,
|
||||
&cbm_source_adapter_perl, &cbm_source_adapter_ruby, &cbm_source_adapter_zig,
|
||||
&cbm_source_adapter_haskell, &cbm_source_adapter_ocaml, &cbm_source_adapter_dart,
|
||||
&cbm_source_adapter_swift, &cbm_source_adapter_scala,
|
||||
&cbm_source_adapter_swift, &cbm_source_adapter_scala, &cbm_source_adapter_lua,
|
||||
&cbm_source_adapter_vim, &cbm_source_adapter_shell, &cbm_source_adapter_tcl,
|
||||
&cbm_source_adapter_make,
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(adapters) / sizeof(adapters[0]); i++)
|
||||
if (adapters[i]->language == language)
|
||||
@@ -276,6 +278,44 @@ const char *cbm_source_def_qn(cbm_source_walk_t *walk, const char *name, uint32_
|
||||
return cbm_source_def_qn_label(walk, name, line, NULL);
|
||||
}
|
||||
|
||||
const char *cbm_source_def_qn_at(cbm_source_walk_t *walk, const char *name, uint32_t line) {
|
||||
if (!name || !source_def_index(walk))
|
||||
return NULL;
|
||||
for (const source_def_link_t *link = cbm_ht_get(walk->def_index, name); link; link = link->next)
|
||||
if (link->def->start_line == line)
|
||||
return link->def->qualified_name;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool cbm_source_join_path(const char *directory, const char *relative, char *out, size_t capacity) {
|
||||
char joined[1024];
|
||||
int length =
|
||||
snprintf(joined, sizeof(joined), "%s%s%s", directory, *directory ? "/" : "", relative);
|
||||
if (length <= 0 || (size_t)length >= sizeof(joined) || (size_t)length >= capacity)
|
||||
return false;
|
||||
size_t used = 0;
|
||||
for (char *part = joined; *part;) {
|
||||
char *end = strchr(part, '/');
|
||||
size_t size = end ? (size_t)(end - part) : strlen(part);
|
||||
if (size == 2 && part[0] == '.' && part[1] == '.') {
|
||||
if (!used)
|
||||
return false;
|
||||
while (used && out[used - 1] != '/')
|
||||
used--;
|
||||
if (used)
|
||||
used--;
|
||||
} else if (size && !(size == 1 && part[0] == '.')) {
|
||||
if (used)
|
||||
out[used++] = '/';
|
||||
memcpy(out + used, part, size);
|
||||
used += size;
|
||||
}
|
||||
part += size + (end ? 1 : 0);
|
||||
}
|
||||
out[used] = '\0';
|
||||
return used > 0;
|
||||
}
|
||||
|
||||
const char *cbm_source_table_intern(cbm_source_walk_t *walk, const char *text) {
|
||||
cbm_source_table_t *table = walk->writable;
|
||||
return text && table ? cbm_arena_strdup(&table->arena, text) : NULL;
|
||||
@@ -473,8 +513,9 @@ static const char *source_gate(const cbm_source_adapter_t *adapter, const CBMFil
|
||||
const char *source, size_t source_len) {
|
||||
if (!result || !source || !result->module_qn || source_len > UINT32_MAX)
|
||||
return "source_adapter_input_unavailable";
|
||||
if (result->has_error || result->parse_unusable ||
|
||||
(result->parse_incomplete && !adapter->contains_parse_damage))
|
||||
if (!adapter->parse_errors_are_local &&
|
||||
(result->has_error || result->parse_unusable ||
|
||||
(result->parse_incomplete && !adapter->contains_parse_damage)))
|
||||
return "source_adapter_parse_incomplete";
|
||||
return NULL;
|
||||
}
|
||||
@@ -586,6 +627,8 @@ static bool source_tree_usable(const cbm_source_adapter_t *adapter, TSTree *tree
|
||||
TSNode root = ts_tree_root_node(tree);
|
||||
if (!ts_node_has_error(root))
|
||||
return true;
|
||||
if (adapter->parse_errors_are_local)
|
||||
return true;
|
||||
if (!adapter->contains_parse_damage)
|
||||
return false;
|
||||
source_damage_scan_t scan = {
|
||||
|
||||
@@ -38,6 +38,9 @@ bool cbm_source_adapter_supported(CBMLanguage language);
|
||||
/* All clauses and arities of one name share one graph node in this language, so
|
||||
* several same-QN definitions in one file are that one name, not a conflict. */
|
||||
bool cbm_source_adapter_one_name_one_node(CBMLanguage language);
|
||||
/* The next module name a project's `lua/` root gives this file, as a span of rel_path after
|
||||
* from (NULL for the first): runtime/lua/vim/lsp.lua is `vim/lsp`. NULL when there is no other. */
|
||||
const char *cbm_lua_root_module(const char *rel_path, const char *from, size_t *length);
|
||||
/* Relations this language's adapter actually decides. Others stay unsupported. */
|
||||
bool cbm_source_adapter_decides_references(CBMLanguage language);
|
||||
/* This language keeps older binding rules, which decide what its adapter leaves. */
|
||||
|
||||
@@ -96,6 +96,9 @@ typedef struct {
|
||||
/* optional: collects a file whose tree is unusable, when the adapter has another way to
|
||||
* read it. It sets walk->failure when it has none. */
|
||||
cbm_source_pass_fn collect_unusable;
|
||||
/* A parse error costs this language only the commands inside it: no binding depends on
|
||||
* the structure around an error, so the rest of the file still decides. */
|
||||
bool parse_errors_are_local;
|
||||
} cbm_source_adapter_t;
|
||||
|
||||
/* Per-language rule tables. A row here is what makes a language supported. */
|
||||
@@ -110,6 +113,11 @@ extern const cbm_source_adapter_t cbm_source_adapter_ocaml;
|
||||
extern const cbm_source_adapter_t cbm_source_adapter_dart;
|
||||
extern const cbm_source_adapter_t cbm_source_adapter_swift;
|
||||
extern const cbm_source_adapter_t cbm_source_adapter_scala;
|
||||
extern const cbm_source_adapter_t cbm_source_adapter_lua;
|
||||
extern const cbm_source_adapter_t cbm_source_adapter_vim;
|
||||
extern const cbm_source_adapter_t cbm_source_adapter_shell;
|
||||
extern const cbm_source_adapter_t cbm_source_adapter_tcl;
|
||||
extern const cbm_source_adapter_t cbm_source_adapter_make;
|
||||
|
||||
/* One walk of a class chain. A class appears once on a linearized chain, so a
|
||||
* revisit adds nothing: a cycle or a diamond cannot make the walk exponential. */
|
||||
@@ -157,6 +165,12 @@ const char *cbm_source_def_qn(cbm_source_walk_t *walk, const char *name, uint32_
|
||||
const char *cbm_source_def_qn_of_name(cbm_source_walk_t *walk, const char *name);
|
||||
const char *cbm_source_def_qn_label(cbm_source_walk_t *walk, const char *name, uint32_t line,
|
||||
const char *label);
|
||||
/* The definition of this name that starts on exactly this line. For a language whose inner
|
||||
* declaration may have no node of its own, where the enclosing one's span would also match. */
|
||||
const char *cbm_source_def_qn_at(cbm_source_walk_t *walk, const char *name, uint32_t line);
|
||||
|
||||
/* directory/relative with `.` and `..` collapsed. False when it climbs above the repository. */
|
||||
bool cbm_source_join_path(const char *directory, const char *relative, char *out, size_t capacity);
|
||||
|
||||
/* Phase one. Copies everything into the table; returns NULL on allocation failure. */
|
||||
cbm_source_decl_t *cbm_source_declare(cbm_source_walk_t *walk, const char *owner, const char *name,
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "resolve/snapshot.h"
|
||||
|
||||
#include "callable_sig.h"
|
||||
#include "lang_specs.h"
|
||||
#include "foundation/compat.h" /* CBM_TLS */
|
||||
#include "foundation/compat_fs.h"
|
||||
#include "foundation/constants.h"
|
||||
@@ -1756,8 +1757,9 @@ int cbm_pxc_build_import_map_ex(const cbm_pipeline_ctx_t *ctx, const char *rel_p
|
||||
const char ***out_vals, int *out_count) {
|
||||
const cbm_gbuf_t *gbuf = ctx->gbuf;
|
||||
const char *project_name = ctx->project_name;
|
||||
if (result && (lang == CBM_LANG_JAVASCRIPT || lang == CBM_LANG_TYPESCRIPT ||
|
||||
lang == CBM_LANG_TSX || lang == CBM_LANG_ARKTS))
|
||||
if (result &&
|
||||
(lang == CBM_LANG_JAVASCRIPT || lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX ||
|
||||
lang == CBM_LANG_ARKTS || lang == CBM_LANG_VUE || lang == CBM_LANG_SVELTE))
|
||||
return cbm_pipeline_ecma_import_map(ctx, rel_path, result, include_type_only, out_keys,
|
||||
out_vals, out_count);
|
||||
if (result && (lang == CBM_LANG_C || lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA))
|
||||
@@ -1956,6 +1958,8 @@ bool cbm_pxc_has_cross_lsp(CBMLanguage lang) {
|
||||
case CBM_LANG_JAVASCRIPT:
|
||||
case CBM_LANG_TYPESCRIPT:
|
||||
case CBM_LANG_TSX:
|
||||
case CBM_LANG_VUE: /* its script: see cbm_pxc_dispatch_file_with_imports */
|
||||
case CBM_LANG_SVELTE: /* its script: see cbm_pxc_dispatch_file_with_imports */
|
||||
case CBM_LANG_PHP:
|
||||
case CBM_LANG_CSHARP: /* tier-2 prebuilt registry path (pass_parallel.c) */
|
||||
case CBM_LANG_JAVA: /* fallback cbm_pxc_run_one path */
|
||||
@@ -2828,6 +2832,39 @@ static void pxc_run_oo(CBMLanguage lang, CBMFileResult *result, const char *sour
|
||||
pxc_scratch_give(PXC_SCRATCH_DISPATCH, &scratch);
|
||||
}
|
||||
|
||||
/* The parse of a component's scripts. It needs the host tree, which a retained result keeps. */
|
||||
static TSTree *pxc_component_script(CBMFileResult *result, CBMLanguage lang, const char *source,
|
||||
int source_len, CBMLanguage *script_language) {
|
||||
TSTree *host = result->cached_tree;
|
||||
TSTree *owned = NULL;
|
||||
if (!host) {
|
||||
TSParser *parser = ts_parser_new();
|
||||
if (parser && ts_parser_set_language(parser, cbm_ts_language(lang)))
|
||||
owned = ts_parser_parse_string(parser, NULL, source, (uint32_t)source_len);
|
||||
if (parser)
|
||||
ts_parser_delete(parser);
|
||||
host = owned;
|
||||
}
|
||||
if (!host)
|
||||
return NULL;
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
CBMExtractCtx context = {.arena = &arena,
|
||||
.result = result,
|
||||
.source = source,
|
||||
.source_len = source_len,
|
||||
.language = lang,
|
||||
.module_qn = result->module_qn,
|
||||
.root = ts_tree_root_node(host)};
|
||||
TSRange range;
|
||||
memset(&range, 0, sizeof(range));
|
||||
TSTree *script = cbm_component_binding_tree(&context, script_language, &range);
|
||||
cbm_arena_destroy(&arena);
|
||||
if (owned)
|
||||
ts_tree_delete(owned);
|
||||
return script;
|
||||
}
|
||||
|
||||
void cbm_pxc_dispatch_file_with_imports(CBMLanguage lang, CBMFileResult *result, const char *source,
|
||||
int source_len, const char *rel, const char *def_module,
|
||||
const CBMCrossLspRegistries *cross_registries,
|
||||
@@ -2846,6 +2883,23 @@ void cbm_pxc_dispatch_file_with_imports(CBMLanguage lang, CBMFileResult *result,
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
/* A component's script is a TypeScript or JavaScript module, in the original bytes: its
|
||||
* walk is that language's, over the tree of the script alone. */
|
||||
if (lang == CBM_LANG_VUE || lang == CBM_LANG_SVELTE) {
|
||||
CBMLanguage script_language = CBM_LANG_JAVASCRIPT;
|
||||
TSTree *script = pxc_component_script(result, lang, source, source_len, &script_language);
|
||||
if (script) {
|
||||
TSTree *host = result->cached_tree;
|
||||
result->cached_tree = script;
|
||||
cbm_pxc_dispatch_file_with_imports(
|
||||
script_language, result, source, source_len, rel, def_module, cross_registries,
|
||||
module_def_index, all_defs, all_def_count, imp_keys, imp_vals, imp_count,
|
||||
rust_shared_get, rust_shared_ctx, bindings, imports);
|
||||
result->cached_tree = host;
|
||||
ts_tree_delete(script);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (cross_registries && cross_registries->oo && cbm_oo_language(lang))
|
||||
pxc_run_oo(lang, result, source, source_len, rel, cross_registries->oo);
|
||||
/* Source occurrence identities use the extractor's file module, while
|
||||
|
||||
@@ -3863,6 +3863,16 @@ static bool source_module_facts(const cbm_file_info_t *files, int count, CBMFile
|
||||
if (length <= 0 || (size_t)length >= sizeof(module) ||
|
||||
!source_module_put(map, "erlang_module", module, files[i].rel_path))
|
||||
return false;
|
||||
} else if (files[i].language == CBM_LANG_LUA) {
|
||||
size_t size = 0;
|
||||
for (const char *name = cbm_lua_root_module(files[i].rel_path, NULL, &size); name;
|
||||
name = cbm_lua_root_module(files[i].rel_path, name, &size)) {
|
||||
char module[PKGMAP_PATH_BUF];
|
||||
int length = snprintf(module, sizeof(module), "%.*s", (int)size, name);
|
||||
if (length <= 0 || (size_t)length >= sizeof(module) ||
|
||||
!source_module_put(map, "lua_module", module, files[i].rel_path))
|
||||
return false;
|
||||
}
|
||||
} else if (files[i].language == CBM_LANG_ELIXIR && cache && cache[i]) {
|
||||
const CBMFileResult *result = cache[i];
|
||||
for (int d = 0; d < result->defs.count; d++) {
|
||||
@@ -5612,6 +5622,47 @@ static const cbm_gbuf_node_t *resolve_ancestor_lib_file(const cbm_pipeline_ctx_t
|
||||
}
|
||||
}
|
||||
|
||||
/* `require('a.b')` searches package.path, whose default is `./?.lua;./?/init.lua`. The working
|
||||
* directory is the requiring file's or an ancestor of it; the nearest one holding the file is
|
||||
* the program it belongs to. */
|
||||
static const cbm_gbuf_node_t *resolve_lua_ancestor_module(const cbm_pipeline_ctx_t *ctx,
|
||||
const char *source_rel,
|
||||
const char *module) {
|
||||
char *directory = path_dirname(source_rel ? source_rel : "");
|
||||
char current[PKGMAP_PATH_BUF];
|
||||
int length = directory ? snprintf(current, sizeof(current), "%s", directory) : -1;
|
||||
cbm_free(CBM_MEM_CLASS_SEMANTIC, directory);
|
||||
if (length < 0 || (size_t)length >= sizeof(current))
|
||||
return NULL;
|
||||
if (!strcmp(current, "."))
|
||||
current[0] = '\0';
|
||||
for (;;) {
|
||||
char path[PKGMAP_PATH_BUF];
|
||||
static const char *const forms[] = {"%s.lua", "%s/init.lua"};
|
||||
for (size_t f = 0; f < sizeof(forms) / sizeof(forms[0]); f++) {
|
||||
char leaf[PKGMAP_PATH_BUF];
|
||||
int size = snprintf(leaf, sizeof(leaf), forms[f], module);
|
||||
int written =
|
||||
size > 0 && (size_t)size < sizeof(leaf)
|
||||
? snprintf(path, sizeof(path), "%s%s%s", current, current[0] ? "/" : "", leaf)
|
||||
: -1;
|
||||
const cbm_gbuf_node_t *found =
|
||||
written > 0 && (size_t)written < sizeof(path)
|
||||
? cbm_pipeline_file_node(ctx->gbuf, ctx->project_name, path)
|
||||
: NULL;
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
if (!current[0])
|
||||
return NULL;
|
||||
char *slash = strrchr(current, '/');
|
||||
if (slash)
|
||||
*slash = '\0';
|
||||
else
|
||||
current[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
static const cbm_gbuf_node_t *resolve_source_module(const cbm_pipeline_ctx_t *ctx,
|
||||
CBMHashTable *namespace_map, const char *kind,
|
||||
const char *module, bool *ambiguous) {
|
||||
@@ -5710,6 +5761,48 @@ static bool resolve_source_language_import(const cbm_pipeline_ctx_t *ctx, const
|
||||
*reason = "haskell_declared_module";
|
||||
*out = resolve_source_module(ctx, namespace_map, "haskell_module", path, ambiguous);
|
||||
return true;
|
||||
case CBM_LANG_BASH:
|
||||
case CBM_LANG_TCL:
|
||||
/* A literal path is relative to the working directory: the script's or the repository's. */
|
||||
*reason = "script_source_path_computed";
|
||||
if (path[0] == '/' || strpbrk(path, "$[`"))
|
||||
return true;
|
||||
*reason = "script_source_literal_path";
|
||||
directory = path_dirname(source_rel);
|
||||
if (directory && cbm_repo_rel_join(directory, path, joined, sizeof(joined)))
|
||||
*out = cbm_pipeline_file_node(ctx->gbuf, ctx->project_name, joined);
|
||||
cbm_free(CBM_MEM_CLASS_SEMANTIC, directory);
|
||||
if (!*out && cbm_repo_rel_join("", path, joined, sizeof(joined)))
|
||||
*out = cbm_pipeline_file_node(ctx->gbuf, ctx->project_name, joined);
|
||||
return true;
|
||||
case CBM_LANG_LUA: {
|
||||
char module[PKGMAP_PATH_BUF];
|
||||
int length = snprintf(module, sizeof(module), "%s", path);
|
||||
if (length <= 0 || (size_t)length >= sizeof(module)) {
|
||||
*reason = "lua_module_name_too_long";
|
||||
return true;
|
||||
}
|
||||
for (char *p = module; *p; p++)
|
||||
if (*p == '.')
|
||||
*p = '/';
|
||||
/* The host preloads these in package.loaded: `require('os')` never reads an os.lua. */
|
||||
static const char *const preloaded[] = {"string", "table", "math", "io", "os",
|
||||
"debug", "utf8", "package", "bit", "bit32",
|
||||
"ffi", "jit", "coroutine"};
|
||||
for (size_t p = 0; p < sizeof(preloaded) / sizeof(preloaded[0]); p++) {
|
||||
if (!strcmp(module, preloaded[p])) {
|
||||
*reason = "lua_require_of_a_preloaded_library";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
*reason = "lua_require_package_path";
|
||||
*out = resolve_lua_ancestor_module(ctx, source_rel, module);
|
||||
if (!*out) {
|
||||
*reason = "lua_require_lua_root";
|
||||
*out = resolve_source_module(ctx, namespace_map, "lua_module", module, ambiguous);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -205,6 +205,11 @@ struct cbm_shared_resolver {
|
||||
CBMHashTable *python_namespaces;
|
||||
bool python_unknown_namespace_escape;
|
||||
cbm_ecma_table_t *ecma;
|
||||
/* QN of a module-level object literal + U+001F + member: the declaration it holds. */
|
||||
CBMHashTable *ecma_literal_members;
|
||||
/* QN of a module-level function + U+001F + member: what its `return { ... }` statements give.
|
||||
*/
|
||||
CBMHashTable *ecma_returned_members;
|
||||
CBMHashTable *python_star_provided; /* "<module>.<name>" a closed star import binds */
|
||||
CBMHashTable *python_written; /* "<module>.<path>" a statement stores or deletes */
|
||||
CBMHashTable *python_parent_loaded; /* a submodule its package's own __init__ loads */
|
||||
@@ -2029,6 +2034,40 @@ static bool python_owner_pair_present(const cbm_shared_resolver_t *resolver,
|
||||
return false;
|
||||
}
|
||||
|
||||
/* `export const store = { fetch() {} }`: what another file's `store.fetch()` selects. The table
|
||||
* holds what this file's own `store.fetch()` would select, by the same member rule. */
|
||||
static bool ecma_note_literal_members(cbm_shared_resolver_t *resolver,
|
||||
const cbm_resolve_binding_index_t *index) {
|
||||
const char *owner = NULL, *member = NULL, *target = NULL;
|
||||
for (size_t m = 0; cbm_resolve_binding_js_literal_member_at(index, m, &owner, &member, &target);
|
||||
m++) {
|
||||
if (!owner || !target)
|
||||
continue;
|
||||
const char *key = cbm_arena_sprintf(&resolver->arena, "%s\x1f%s", owner, member);
|
||||
const char *value = cbm_arena_strdup(&resolver->arena, target);
|
||||
if (!key || !value)
|
||||
return false;
|
||||
cbm_ht_set(resolver->ecma_literal_members, key, (void *)value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ecma_note_returned_members(cbm_shared_resolver_t *resolver,
|
||||
const cbm_resolve_binding_index_t *index) {
|
||||
const char *function = NULL, *member = NULL, *target = NULL;
|
||||
for (size_t m = 0;
|
||||
cbm_resolve_binding_js_returned_member_at(index, m, &function, &member, &target); m++) {
|
||||
if (!function || !member || !target)
|
||||
continue;
|
||||
const char *key = cbm_arena_sprintf(&resolver->arena, "%s\x1f%s", function, member);
|
||||
const char *value = cbm_arena_strdup(&resolver->arena, target);
|
||||
if (!key || !value)
|
||||
return false;
|
||||
cbm_ht_set(resolver->ecma_returned_members, key, (void *)value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool export_file(cbm_shared_resolver_t *resolver, const cbm_pipeline_ctx_t *ctx,
|
||||
const CBMFileResult *result, const cbm_file_info_t *file) {
|
||||
if (!export_language(file->language))
|
||||
@@ -2083,6 +2122,9 @@ static bool export_file(cbm_shared_resolver_t *resolver, const cbm_pipeline_ctx_
|
||||
ok = audit && cbm_ecma_table_add(resolver->ecma, ctx, file->rel_path, result->module_qn,
|
||||
index, audit);
|
||||
}
|
||||
if (ok && ecma_language(file->language))
|
||||
ok = ecma_note_literal_members(resolver, index) &&
|
||||
ecma_note_returned_members(resolver, index);
|
||||
if (ok && (file->language == CBM_LANG_JAVASCRIPT || file->language == CBM_LANG_TYPESCRIPT ||
|
||||
file->language == CBM_LANG_TSX || file->language == CBM_LANG_ARKTS))
|
||||
ok = http_file_capture(resolver, ctx, &arena, index, result, file->rel_path);
|
||||
@@ -3245,6 +3287,8 @@ cbm_shared_resolver_t *cbm_shared_resolver_build(const cbm_pipeline_ctx_t *ctx,
|
||||
resolver->go_types =
|
||||
cbm_go_stabilization_new(&resolver->arena, &resolver->proofs, resolver->solver);
|
||||
resolver->ecma = cbm_ecma_table_new(&resolver->arena);
|
||||
resolver->ecma_literal_members = cbm_ht_create(64);
|
||||
resolver->ecma_returned_members = cbm_ht_create(64);
|
||||
resolver->source_table = cbm_source_table_new();
|
||||
cbm_source_table_set_packages(resolver->source_table, cbm_pipeline_get_pkgmap());
|
||||
resolver->complete = true;
|
||||
@@ -3254,7 +3298,9 @@ cbm_shared_resolver_t *cbm_shared_resolver_build(const cbm_pipeline_ctx_t *ctx,
|
||||
!resolver->python_parent_loaded || !resolver->python_nested || !resolver->python_members ||
|
||||
!resolver->http_defaults || !resolver->http_sites || !resolver->http_unsafe ||
|
||||
!resolver->http_candidates || !resolver->http_veto || !resolver->project ||
|
||||
!resolver->solver || !resolver->go_types || !resolver->ecma || !resolver->source_table) {
|
||||
!resolver->solver || !resolver->go_types || !resolver->ecma ||
|
||||
!resolver->ecma_literal_members || !resolver->ecma_returned_members ||
|
||||
!resolver->source_table) {
|
||||
cbm_shared_resolver_free(resolver);
|
||||
return NULL;
|
||||
}
|
||||
@@ -3489,6 +3535,8 @@ void cbm_shared_resolver_free(cbm_shared_resolver_t *resolver) {
|
||||
cbm_ht_free(resolver->http_veto);
|
||||
cbm_go_stabilization_dispose(resolver->go_types);
|
||||
cbm_ecma_table_free(resolver->ecma);
|
||||
cbm_ht_free(resolver->ecma_literal_members);
|
||||
cbm_ht_free(resolver->ecma_returned_members);
|
||||
cbm_source_table_free(resolver->source_table);
|
||||
cbm_arena_destroy(&resolver->arena);
|
||||
cbm_free(CBM_MEM_CLASS_SEMANTIC, resolver);
|
||||
@@ -4128,6 +4176,11 @@ static cbm_ecma_value_t ecma_import_value(const file_resolver_t *resolver,
|
||||
* declaration only by an exact member declared in the same file. */
|
||||
static const char *ecma_member_target(file_resolver_t *resolver, const char *owner,
|
||||
const char *member) {
|
||||
const char *key = cbm_arena_sprintf(resolver->arena, "%s\x1f%s", owner, member);
|
||||
const char *literal =
|
||||
key && resolver->shared ? cbm_ht_get(resolver->shared->ecma_literal_members, key) : NULL;
|
||||
if (literal)
|
||||
return literal;
|
||||
const char *qn = cbm_arena_sprintf(resolver->arena, "%s.%s", owner, member);
|
||||
const cbm_gbuf_node_t *node = qn ? cbm_gbuf_find_by_qn(resolver->file->graph, qn) : NULL;
|
||||
const cbm_gbuf_node_t *base = cbm_gbuf_find_by_qn(resolver->file->graph, owner);
|
||||
@@ -4251,6 +4304,44 @@ static cbm_resolve_result_t ecma_import_result(file_resolver_t *resolver,
|
||||
return ecma_finish(&query, result);
|
||||
}
|
||||
|
||||
/* `const { open } = useThing(); open()`: what every `return { open }` of useThing names. */
|
||||
static bool ecma_returned_member_result(file_resolver_t *resolver, const char *name, uint32_t start,
|
||||
uint32_t end, CBMSourceOrigin origin,
|
||||
cbm_resolve_result_t *out) {
|
||||
const cbm_resolve_js_import_t *import = NULL;
|
||||
const char *callee = NULL, *function = NULL, *member = NULL;
|
||||
if (!resolver->shared ||
|
||||
!cbm_resolve_binding_js_destructured_call(resolver->bindings, name, start, end, origin,
|
||||
&import, &callee, &function, &member))
|
||||
return false;
|
||||
if (import)
|
||||
function = ecma_import_result(resolver, import, callee, false, false).target;
|
||||
const char *key =
|
||||
function ? cbm_arena_sprintf(resolver->arena, "%s\x1f%s", function, member) : NULL;
|
||||
const char *target = key ? cbm_ht_get(resolver->shared->ecma_returned_members, key) : NULL;
|
||||
if (!target)
|
||||
return false;
|
||||
cbm_resolve_lookup_t query;
|
||||
cbm_resolve_lookup_begin(&query, cbm_resolve_binding_lookup_context(resolver->bindings),
|
||||
"ecma.returned_member", name, 0, 0, CBM_SOURCE_ORIGIN_RAW);
|
||||
(void)cbm_resolve_lookup_observe(&query, "ecma.returned_members", key, target,
|
||||
CBM_RESOLVE_BINDING_UNIQUE, CBM_RESOLVE_ANALYSIS_COMPLETE, 0,
|
||||
true, "ecma_returned_member_observed");
|
||||
cbm_resolve_result_t found = {.binding = CBM_RESOLVE_BINDING_UNIQUE,
|
||||
.domain = CBM_RESOLVE_DOMAIN_PROJECT,
|
||||
.analysis = CBM_RESOLVE_ANALYSIS_COMPLETE,
|
||||
.rule = CBM_RESOLVE_RULE_IMPORT_BINDING,
|
||||
.facts = CBM_RESOLVE_FACT_EXACT_TARGET |
|
||||
CBM_RESOLVE_FACT_LOOKUP_COMPLETE |
|
||||
CBM_RESOLVE_FACT_IMPORT_BOUND,
|
||||
.target = target,
|
||||
.candidate_count = 1,
|
||||
.reason = "ecma_returned_member"};
|
||||
*out = cbm_resolve_binding_validate(resolver->bindings, name, start, end, origin,
|
||||
ecma_finish(&query, found));
|
||||
return cbm_resolve_admit(out);
|
||||
}
|
||||
|
||||
static bool python_member_result(file_resolver_t *resolver, const char *name, uint32_t start,
|
||||
uint32_t end, CBMSourceOrigin origin, bool invocation,
|
||||
cbm_resolve_result_t *out) {
|
||||
@@ -4300,6 +4391,10 @@ static cbm_resolve_result_t generic_result(file_resolver_t *resolver, const char
|
||||
return unresolved("reference_spelling_missing");
|
||||
if (ecma_language(file->language) || file->language == CBM_LANG_VUE ||
|
||||
file->language == CBM_LANG_SVELTE) {
|
||||
cbm_resolve_result_t returned;
|
||||
if (invocation &&
|
||||
ecma_returned_member_result(resolver, name, start, end, origin, &returned))
|
||||
return returned;
|
||||
cbm_resolve_result_t local =
|
||||
cbm_resolve_binding_local(resolver->bindings, name, start, end, origin);
|
||||
if (cbm_resolve_admit(&local))
|
||||
|
||||
@@ -1133,23 +1133,19 @@ TEST(probe_vue_cross_component_import) {
|
||||
.case_id = "probe_vue_cross_component_import-exact-dependency",
|
||||
.site = {.file = "App.vue",
|
||||
.caller = "App.vue.__file__",
|
||||
.needle = "import BaseButton from './BaseButton.vue'"},
|
||||
.needle = "BaseButton"},
|
||||
.relation = "IMPORTS",
|
||||
.target = {.file = "BaseButton.vue",
|
||||
.suffix = "BaseButton.vue.__file__",
|
||||
.label = "File",
|
||||
.declaration_line = 1,
|
||||
.declaration_text = "<template><button><slot /></button></template>"},
|
||||
.mode = RQ_KNOWN_GAP,
|
||||
.gap_id = "vue-embedded-component-import-proof",
|
||||
.why = "Vue script imports need an audited embedded-source origin and raw literal proof.",
|
||||
.tried = "Preserved original sources and indexing checks; exact target, wrong-target "
|
||||
"control, and required ESM call use full production indexing."};
|
||||
.mode = RQ_REQUIRED};
|
||||
const RQ_Forbidden forbidden = {
|
||||
.case_id = "probe_vue_cross_component_import-invalid-target",
|
||||
.site = {.file = "App.vue",
|
||||
.caller = "App.vue.__file__",
|
||||
.needle = "import BaseButton from './BaseButton.vue'"},
|
||||
.needle = "BaseButton"},
|
||||
.relation = "IMPORTS",
|
||||
.target = {.file = "decoy/BaseButton.vue",
|
||||
.suffix = "decoy.BaseButton.vue.__file__",
|
||||
@@ -1162,8 +1158,8 @@ TEST(probe_vue_cross_component_import) {
|
||||
"probe_vue_cross_component_import", &expected, &forbidden,
|
||||
&report));
|
||||
ASSERT_EQ(report.genuine, 2);
|
||||
ASSERT_EQ(report.found, 1);
|
||||
ASSERT_EQ(report.missing, 1);
|
||||
ASSERT_EQ(report.found, 2);
|
||||
ASSERT_EQ(report.missing, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
@@ -1226,23 +1222,18 @@ TEST(probe_svelte_cross_component_import) {
|
||||
.case_id = "probe_svelte_cross_component_import-exact-dependency",
|
||||
.site = {.file = "App.svelte",
|
||||
.caller = "App.svelte.__file__",
|
||||
.needle = "import Button from './Button.svelte';"},
|
||||
.needle = "Button"},
|
||||
.relation = "IMPORTS",
|
||||
.target = {.file = "Button.svelte",
|
||||
.suffix = "Button.svelte.__file__",
|
||||
.label = "File",
|
||||
.declaration_line = 1,
|
||||
.declaration_text = "<script>"},
|
||||
.mode = RQ_KNOWN_GAP,
|
||||
.gap_id = "svelte-embedded-component-import-proof",
|
||||
.why =
|
||||
"Svelte script imports need an audited embedded-source origin and raw literal proof.",
|
||||
.tried = "Preserved original sources and indexing checks; exact target, wrong-target "
|
||||
"control, and required ESM call use full production indexing."};
|
||||
.mode = RQ_REQUIRED};
|
||||
const RQ_Forbidden forbidden = {.case_id = "probe_svelte_cross_component_import-invalid-target",
|
||||
.site = {.file = "App.svelte",
|
||||
.caller = "App.svelte.__file__",
|
||||
.needle = "import Button from './Button.svelte';"},
|
||||
.needle = "Button"},
|
||||
.relation = "IMPORTS",
|
||||
.target = {.file = "decoy/Button.svelte",
|
||||
.suffix = "decoy.Button.svelte.__file__",
|
||||
@@ -1255,8 +1246,8 @@ TEST(probe_svelte_cross_component_import) {
|
||||
"probe_svelte_cross_component_import", &expected, &forbidden,
|
||||
&report));
|
||||
ASSERT_EQ(report.genuine, 2);
|
||||
ASSERT_EQ(report.found, 1);
|
||||
ASSERT_EQ(report.missing, 1);
|
||||
ASSERT_EQ(report.found, 2);
|
||||
ASSERT_EQ(report.missing, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
|
||||
@@ -3629,9 +3629,8 @@ TEST(probe_mcp_trace_run) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Deploy traversal follows retained DEPLOYS/SPAWNS edges and exact CALLS.
|
||||
* Its source-authored missing shell link is measured below; call-graph mode
|
||||
* still stops at the chart. */
|
||||
/* Deploy traversal follows retained DEPLOYS/SPAWNS edges and exact CALLS;
|
||||
* call-graph mode still stops at the chart. */
|
||||
static int gpf_query_int(const GpfFile *files, int nfiles, const char *sql);
|
||||
|
||||
TEST(probe_mcp_trace_deploy) {
|
||||
@@ -3655,13 +3654,10 @@ TEST(probe_mcp_trace_deploy) {
|
||||
"\"format\":\"json\"}");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_NOT_NULL(strstr(r, "install_web"));
|
||||
/* WHY: deploy mode reaches the installing function through DEPLOYS. The
|
||||
* exact script-to-function CALLS gap below prevents reaching the script
|
||||
* and its CI caller; it must not be bridged by a guessed graph edge. Tried
|
||||
* the unchanged source plus direct deployment/run edge checks. */
|
||||
ASSERT_NULL(strstr(r, ".hack\""));
|
||||
ASSERT_NULL(strstr(r, ".github.workflows\""));
|
||||
ASSERT_NULL(strstr(r, "\"release\""));
|
||||
/* Deploy mode reaches the installing function through DEPLOYS, the script through its own
|
||||
* proven call of that function, and the CI job through SPAWNS. */
|
||||
ASSERT_NOT_NULL(strstr(r, ".hack\""));
|
||||
ASSERT_NOT_NULL(strstr(r, "\"release\""));
|
||||
free(r);
|
||||
r = gpf_tool_on(files, 3, "trace_path",
|
||||
"{\"function_name\":\"web-app\",\"project\":\"%s\","
|
||||
@@ -3670,7 +3666,7 @@ TEST(probe_mcp_trace_deploy) {
|
||||
ASSERT_NULL(strstr(r, "install_web"));
|
||||
free(r);
|
||||
/* Preserve the two direct non-controlled relations independently of the
|
||||
* missing shell CALLS edge and the consumer's bounded traversal. */
|
||||
* shell CALLS edge and the consumer's bounded traversal. */
|
||||
ASSERT_EQ(
|
||||
gpf_query_int(
|
||||
files, 3,
|
||||
@@ -3696,14 +3692,7 @@ TEST(probe_mcp_trace_deploy) {
|
||||
.label = "Function",
|
||||
.declaration_line = 3,
|
||||
.declaration_text = "install_web() {"},
|
||||
.mode = RQ_KNOWN_GAP,
|
||||
.gap_id = "bash_local_function_binding_proof_missing",
|
||||
.why = "The script defines install_web once and invokes it directly at module scope "
|
||||
"without rebinding; Bash lacks an audited immutable lexical binding proof.",
|
||||
.tried =
|
||||
"Preserved the original script/workflow/chart and exact direct DEPLOYS/SPAWNS "
|
||||
"contracts. The missing source-authored CALLS obligation is measured with a required "
|
||||
"ESM control in both drivers; deploy traversal must stop at that missing link."}};
|
||||
.mode = RQ_REQUIRED}};
|
||||
ASSERT_TRUE(gpf_infra_quality(files, 3, "probe_mcp_trace_deploy", expected, 1, NULL, 0));
|
||||
PASS();
|
||||
}
|
||||
|
||||
+17
-21
@@ -983,7 +983,7 @@ typedef struct {
|
||||
/* Source-authored invocation sites and full declaration-line anchors. Binding
|
||||
* classifications identify an audited missing proof stage, not a claim that
|
||||
* extraction has no additional gap. Dart, CUDA and the languages with a source
|
||||
* adapter (Ruby, Swift, Scala, Perl, Zig, Elixir, OCaml, Haskell, Erlang) are
|
||||
* adapter (Ruby, Swift, Scala, Perl, Zig, Elixir, OCaml, Haskell, Erlang, Lua, Bash, Tcl, Vim) are
|
||||
* required capabilities. */
|
||||
static const CallOracle CALL_ORACLES[] = {
|
||||
{"ruby",
|
||||
@@ -1006,12 +1006,10 @@ static const CallOracle CALL_ORACLES[] = {
|
||||
1,
|
||||
1,
|
||||
"function helper(x)",
|
||||
RQ_KNOWN_GAP,
|
||||
RQ_REQUIRED,
|
||||
"binding",
|
||||
"Global function declarations write _ENV; the Lua subset proves only direct local function "
|
||||
"bindings.",
|
||||
"The original global declarations remain unchanged; lua_local_function rejects both and "
|
||||
"records BINDING_BLOCKED."},
|
||||
"",
|
||||
""},
|
||||
{"swift",
|
||||
"run",
|
||||
"helper",
|
||||
@@ -1056,11 +1054,10 @@ static const CallOracle CALL_ORACLES[] = {
|
||||
1,
|
||||
1,
|
||||
"helper() {",
|
||||
RQ_KNOWN_GAP,
|
||||
RQ_REQUIRED,
|
||||
"binding",
|
||||
"Shell function lookup versus command execution has no audited Bash binding environment.",
|
||||
"Kept the bare helper command and preceding function declaration; the generic language gate "
|
||||
"excludes Bash."},
|
||||
"",
|
||||
""},
|
||||
{"perl",
|
||||
"run",
|
||||
"helper",
|
||||
@@ -1236,10 +1233,10 @@ static const CallOracle CALL_ORACLES[] = {
|
||||
1,
|
||||
1,
|
||||
"proc helper {x} {",
|
||||
RQ_KNOWN_GAP,
|
||||
RQ_REQUIRED,
|
||||
"binding",
|
||||
"Tcl proc registration and command-substitution lookup have no audited interpreter binding.",
|
||||
"Kept the nested [helper 21] command inside run; no Tcl semantic proof adapter is selected."},
|
||||
"",
|
||||
""},
|
||||
{"solidity",
|
||||
"run",
|
||||
"helper",
|
||||
@@ -1638,16 +1635,15 @@ static const CallOracle CALL_ORACLES[] = {
|
||||
{"vimscript",
|
||||
"Run",
|
||||
"Helper",
|
||||
{"call Helper()"},
|
||||
{0},
|
||||
{"Helper()"},
|
||||
{1},
|
||||
1,
|
||||
1,
|
||||
"function! Helper() abort",
|
||||
RQ_KNOWN_GAP,
|
||||
RQ_REQUIRED,
|
||||
"binding",
|
||||
"Vimscript named-function lookup and call-statement binding have no audited adapter.",
|
||||
"Kept function! Helper and call Helper() with case preserved; the generic language gate "
|
||||
"excludes Vimscript."},
|
||||
"",
|
||||
""},
|
||||
{"wgsl",
|
||||
"run",
|
||||
"helper",
|
||||
@@ -1839,8 +1835,8 @@ TEST(contract_calls_breadth) {
|
||||
for (int run = 0; run < RUNS; run++) {
|
||||
ASSERT(accepted[run]);
|
||||
ASSERT_EQ(reports[run].genuine, SITES);
|
||||
ASSERT_EQ(reports[run].found, 11); /* Dart, CUDA and the nine adapter languages */
|
||||
ASSERT_EQ(reports[run].missing, 44); /* includes both original Fortran references */
|
||||
ASSERT_EQ(reports[run].found, 15); /* Dart, CUDA and the thirteen adapter languages */
|
||||
ASSERT_EQ(reports[run].missing, 40); /* includes both original Fortran references */
|
||||
}
|
||||
ASSERT(equivalent);
|
||||
PASS();
|
||||
|
||||
@@ -1563,12 +1563,7 @@ TEST(probe_bash_functions_calls) {
|
||||
.suffix = "log",
|
||||
.declaration_line = 3,
|
||||
.declaration_text = "log() {"},
|
||||
.mode = RQ_KNOWN_GAP,
|
||||
.gap_id = "no-bash-call-proof",
|
||||
.why = "This language has no admitted semantic CALLS binding bridge; each authored local "
|
||||
"declaration remains a genuine missing target.",
|
||||
.tried = "The original canonical v69 probe failed its CALLS assertion; reviewed the "
|
||||
"unchanged fixture and current proof boundary."},
|
||||
.mode = RQ_REQUIRED},
|
||||
{.case_id = "probe_bash_functions_calls-2",
|
||||
.site = {.file = "deploy.sh",
|
||||
.caller = "deploy",
|
||||
@@ -1579,12 +1574,7 @@ TEST(probe_bash_functions_calls) {
|
||||
.suffix = "log",
|
||||
.declaration_line = 3,
|
||||
.declaration_text = "log() {"},
|
||||
.mode = RQ_KNOWN_GAP,
|
||||
.gap_id = "no-bash-call-proof",
|
||||
.why = "This language has no admitted semantic CALLS binding bridge; each authored local "
|
||||
"declaration remains a genuine missing target.",
|
||||
.tried = "The original canonical v69 probe failed its CALLS assertion; reviewed the "
|
||||
"unchanged fixture and current proof boundary."},
|
||||
.mode = RQ_REQUIRED},
|
||||
{.case_id = "probe_bash_functions_calls-3",
|
||||
.site =
|
||||
{.file = "deploy.sh", .caller = "deploy", .needle = "build", .occurrence_index = 2},
|
||||
@@ -1593,12 +1583,7 @@ TEST(probe_bash_functions_calls) {
|
||||
.suffix = "build",
|
||||
.declaration_line = 7,
|
||||
.declaration_text = "build() {"},
|
||||
.mode = RQ_KNOWN_GAP,
|
||||
.gap_id = "no-bash-call-proof",
|
||||
.why = "This language has no admitted semantic CALLS binding bridge; each authored local "
|
||||
"declaration remains a genuine missing target.",
|
||||
.tried = "The original canonical v69 probe failed its CALLS assertion; reviewed the "
|
||||
"unchanged fixture and current proof boundary."},
|
||||
.mode = RQ_REQUIRED},
|
||||
{.case_id = "probe_bash_functions_calls-4",
|
||||
.site = {.file = "deploy.sh",
|
||||
.caller = "main",
|
||||
@@ -1609,12 +1594,15 @@ TEST(probe_bash_functions_calls) {
|
||||
.suffix = "deploy",
|
||||
.declaration_line = 12,
|
||||
.declaration_text = "deploy() {"},
|
||||
.mode = RQ_KNOWN_GAP,
|
||||
.gap_id = "no-bash-call-proof",
|
||||
.why = "This language has no admitted semantic CALLS binding bridge; each authored local "
|
||||
"declaration remains a genuine missing target.",
|
||||
.tried = "The original canonical v69 probe failed its CALLS assertion; reviewed the "
|
||||
"unchanged fixture and current proof boundary."}};
|
||||
.mode = RQ_REQUIRED},
|
||||
{.case_id = "probe_bash_functions_calls-5",
|
||||
.site = {.file = "deploy.sh", .caller = "deploy", .needle = "main \"$@\""},
|
||||
.relation = "CALLS",
|
||||
.target = {.file = "deploy.sh",
|
||||
.suffix = "main",
|
||||
.declaration_line = 17,
|
||||
.declaration_text = "main() {"},
|
||||
.mode = RQ_REQUIRED}};
|
||||
NcpMetrics m = ncp_metrics_quality("deploy.sh",
|
||||
"#!/usr/bin/env bash\n\n"
|
||||
"log() {\n"
|
||||
@@ -1637,7 +1625,8 @@ TEST(probe_bash_functions_calls) {
|
||||
ASSERT_TRUE(m.functions >= 3); /* log, build, deploy, main */
|
||||
ASSERT_TRUE(m.quality_ok);
|
||||
ASSERT_EQ(m.quality.genuine, sizeof(expected) / sizeof(expected[0]));
|
||||
ASSERT_EQ(m.quality.missing, sizeof(expected) / sizeof(expected[0]));
|
||||
ASSERT_EQ(m.quality.found, sizeof(expected) / sizeof(expected[0]));
|
||||
ASSERT_EQ(m.quality.missing, 0);
|
||||
ASSERT_EQ(m.quality.wrong, 0);
|
||||
PASS();
|
||||
}
|
||||
@@ -1655,12 +1644,7 @@ TEST(probe_bash_function_keyword) {
|
||||
.suffix = "helper",
|
||||
.declaration_line = 1,
|
||||
.declaration_text = "function helper {"},
|
||||
.mode = RQ_KNOWN_GAP,
|
||||
.gap_id = "no-bash-call-proof",
|
||||
.why = "This language has no admitted semantic CALLS binding bridge; each authored local "
|
||||
"declaration remains a genuine missing target.",
|
||||
.tried = "The original canonical v69 probe failed its CALLS assertion; reviewed the "
|
||||
"unchanged fixture and current proof boundary."},
|
||||
.mode = RQ_REQUIRED},
|
||||
{.case_id = "probe_bash_function_keyword-2",
|
||||
.site = {.file = "lib.sh",
|
||||
.caller = "runner",
|
||||
@@ -1671,12 +1655,7 @@ TEST(probe_bash_function_keyword) {
|
||||
.suffix = "helper",
|
||||
.declaration_line = 1,
|
||||
.declaration_text = "function helper {"},
|
||||
.mode = RQ_KNOWN_GAP,
|
||||
.gap_id = "no-bash-call-proof",
|
||||
.why = "This language has no admitted semantic CALLS binding bridge; each authored local "
|
||||
"declaration remains a genuine missing target.",
|
||||
.tried = "The original canonical v69 probe failed its CALLS assertion; reviewed the "
|
||||
"unchanged fixture and current proof boundary."}};
|
||||
.mode = RQ_REQUIRED}};
|
||||
NcpMetrics m = ncp_metrics_quality("lib.sh",
|
||||
"function helper {\n"
|
||||
" echo \"help: $1\"\n"
|
||||
@@ -1690,7 +1669,8 @@ TEST(probe_bash_function_keyword) {
|
||||
ASSERT_TRUE(m.functions >= 2);
|
||||
ASSERT_TRUE(m.quality_ok);
|
||||
ASSERT_EQ(m.quality.genuine, sizeof(expected) / sizeof(expected[0]));
|
||||
ASSERT_EQ(m.quality.missing, sizeof(expected) / sizeof(expected[0]));
|
||||
ASSERT_EQ(m.quality.found, sizeof(expected) / sizeof(expected[0]));
|
||||
ASSERT_EQ(m.quality.missing, 0);
|
||||
ASSERT_EQ(m.quality.wrong, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
@@ -221,32 +221,6 @@ TEST(resolver_binding_exact_occurrence_and_origin_required) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(resolver_binding_lua_local_function_and_shadow) {
|
||||
const char *sources[] = {
|
||||
"local function helper() return 1 end\nlocal function caller() return helper() end\n",
|
||||
"local function helper() return 1 end\nlocal function caller(helper) return helper() end\n",
|
||||
"function helper() return 1 end\nfunction caller() return helper() end\n",
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(sources) / sizeof(sources[0]); i++) {
|
||||
const char *source = sources[i];
|
||||
CBMFileResult *result = cbm_extract_file(source, (int)strlen(source), CBM_LANG_LUA, "test",
|
||||
"binding.lua", 0, NULL, NULL);
|
||||
ASSERT_NOT_NULL(result);
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
cbm_resolve_binding_index_t *index =
|
||||
cbm_resolve_binding_build(&arena, result, CBM_LANG_LUA, source, strlen(source));
|
||||
const CBMCall *call = find_call(result, "helper");
|
||||
ASSERT_NOT_NULL(call);
|
||||
cbm_resolve_result_t resolved =
|
||||
validate_call(index, call, local_candidate(&arena, result, "helper"));
|
||||
ASSERT_EQ(cbm_resolve_admit(&resolved), i == 0);
|
||||
cbm_arena_destroy(&arena);
|
||||
cbm_free_result(result);
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(resolver_binding_unsupported_language_is_explicit) {
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
@@ -670,6 +644,5 @@ SUITE(resolver_binding) {
|
||||
RUN_TEST(resolver_binding_python_import_needs_independent_export_proof);
|
||||
RUN_TEST(resolver_binding_python_relative_import_preserves_local_authority);
|
||||
RUN_TEST(resolver_binding_exact_occurrence_and_origin_required);
|
||||
RUN_TEST(resolver_binding_lua_local_function_and_shadow);
|
||||
RUN_TEST(resolver_binding_unsupported_language_is_explicit);
|
||||
}
|
||||
@@ -842,34 +842,17 @@ TEST(resolver_binding_language_dynamic_controls_remain_blocked) {
|
||||
"globals()['target'] = None", "locals()['target'] = None",
|
||||
"vars()['target'] = None",
|
||||
"setattr(sys.modules[__name__], 'target', None)"};
|
||||
const char *lua[] = {"debug.getregistry()",
|
||||
"load('return nil')",
|
||||
"rawset(_G, 'target', nil)",
|
||||
"loadfile('unknown.lua')",
|
||||
"dofile('unknown.lua')",
|
||||
"loadstring('return nil')",
|
||||
"setfenv(1, {})",
|
||||
"getfenv(1)",
|
||||
"_ENV.target = nil",
|
||||
"_G.target = nil"};
|
||||
for (int language = 0; language < 2; language++) {
|
||||
const char *const *effects = language ? lua : python;
|
||||
size_t count = language ? sizeof(lua) / sizeof(lua[0]) : sizeof(python) / sizeof(python[0]);
|
||||
{
|
||||
size_t count = sizeof(python) / sizeof(python[0]);
|
||||
const char *const *effects = python;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
char source[512];
|
||||
if (language)
|
||||
snprintf(source, sizeof(source),
|
||||
"local function target() return 1 end\n%s\n"
|
||||
"local function caller() return target() end\n",
|
||||
effects[i]);
|
||||
else
|
||||
snprintf(source, sizeof(source),
|
||||
"def target():\n return 1\n%s\ndef caller():\n return target()\n",
|
||||
effects[i]);
|
||||
CBMLanguage kind = language ? CBM_LANG_LUA : CBM_LANG_PYTHON;
|
||||
CBMFileResult *file =
|
||||
cbm_extract_file(source, (int)strlen(source), kind, "isolation",
|
||||
language ? "main.lua" : "main.py", 0, NULL, NULL);
|
||||
snprintf(source, sizeof(source),
|
||||
"def target():\n return 1\n%s\ndef caller():\n return target()\n",
|
||||
effects[i]);
|
||||
CBMLanguage kind = CBM_LANG_PYTHON;
|
||||
CBMFileResult *file = cbm_extract_file(source, (int)strlen(source), kind, "isolation",
|
||||
"main.py", 0, NULL, NULL);
|
||||
ASSERT_NOT_NULL(file);
|
||||
ASSERT(!file->has_error && !file->parse_incomplete && !file->parse_unusable);
|
||||
CBMArena arena;
|
||||
@@ -891,8 +874,8 @@ TEST(resolver_binding_language_dynamic_controls_remain_blocked) {
|
||||
CBM_SOURCE_ORIGIN_RAW, candidate);
|
||||
bool admitted = cbm_resolve_admit(&proof);
|
||||
if (!blocked || admitted)
|
||||
fprintf(stderr, "Dynamic control language=%d case=%zu status=%s admitted=%d\n",
|
||||
language, i, cbm_resolve_binding_status(index), admitted);
|
||||
fprintf(stderr, "Dynamic control case=%zu status=%s admitted=%d\n", i,
|
||||
cbm_resolve_binding_status(index), admitted);
|
||||
cbm_arena_destroy(&arena);
|
||||
cbm_free_result(file);
|
||||
ASSERT(blocked);
|
||||
@@ -1885,6 +1868,27 @@ TEST(resolver_binding_component_scripts_exact_local_scope) {
|
||||
} fixtures[] = {{"App.vue", "<template><p>Vue</p></template>\n<script setup lang=\"ts\">\n"
|
||||
"function callee(): number { return 42; }\n"
|
||||
"function caller(): number { return callee(); }\n</script>\n"},
|
||||
/* koel's SFCs: directives, bound attributes, handlers, a style block and
|
||||
* interpolation neither declare nor rebind a script name. */
|
||||
{"App.vue",
|
||||
"<template><Card v-if=\"caller\" :title=\"`${caller()}`\" @click.prevent=\"caller\">"
|
||||
"{{ caller() }}</Card></template>\n<script setup lang=\"ts\">\n"
|
||||
"function callee(): number { return 42; }\n"
|
||||
"function caller(): number { return callee(); }\n</script>\n"
|
||||
"<style scoped>\n.a { color: red; }\n</style>\n"},
|
||||
/* A normal script and `<script setup>` are one module. */
|
||||
{"App.vue",
|
||||
"<script lang=\"ts\">\nfunction callee(): number { return 42; }\n</script>\n"
|
||||
"<script setup lang=\"ts\" generic=\"T\">\n"
|
||||
"function caller(): number { return callee(); }\n</script>\n"},
|
||||
/* Assigning a const in markup writes its .value: the const stays. */
|
||||
{"App.vue",
|
||||
"<template><input v-model=\"count\" @click=\"count = 1\"/></template>\n"
|
||||
"<script setup>\nconst count = 1;\nfunction callee() { return count; }\n"
|
||||
"function caller() { return callee(); }\n</script>\n"},
|
||||
{"Toggle.svelte", "<script context=\"module\">\nfunction callee() { return 42; }\n"
|
||||
"</script>\n<script>\nfunction caller() { return callee(); }\n"
|
||||
"</script>\n<p>{caller()}</p>\n"},
|
||||
{"App.vue", "<template><p>Vue</p></template>\n<script setup>\n"
|
||||
"function callee() { return 42; }\n"
|
||||
"function caller() { return callee(); }\n</script>\n"},
|
||||
@@ -1908,6 +1912,14 @@ TEST(resolver_binding_component_scripts_scope_and_write_barriers) {
|
||||
{"App.vue", "<template><button @click=\"callee = caller\">Change</button></template>\n"
|
||||
"<script setup>\nfunction callee() { return 42; }\n"
|
||||
"function caller() { return callee(); }\n</script>\n"},
|
||||
{"App.vue", "<template><input v-model=\"callee\"/></template>\n"
|
||||
"<script setup>\nfunction callee() { return 42; }\n"
|
||||
"function caller() { return callee(); }\n</script>\n"},
|
||||
{"App.vue", "<template><p>x</p></template>\n<script src=\"./x.js\"></script>\n"
|
||||
"<script setup>\nfunction callee() { return 42; }\n"
|
||||
"function caller() { return callee(); }\n</script>\n"},
|
||||
{"App.vue", "<script lang=\"coffee\">\nfunction callee() { return 42; }\n"
|
||||
"function caller() { return callee(); }\n</script>\n"},
|
||||
{"Toggle.svelte", "<script>\nfunction callee() { return 42; }\n"
|
||||
"function caller() { return callee(); }\n</script>\n"
|
||||
"<button on:click={() => callee = caller}>Change</button>\n"},
|
||||
@@ -2250,7 +2262,14 @@ TEST(resolver_binding_unsupported_import_carriers_do_not_authorize_paths) {
|
||||
"project='unsupported-imports' "
|
||||
"AND relation='IMPORTS' AND emitted=0 AND target='' "
|
||||
"AND reason='unsupported_import_language' AND file_path IN "
|
||||
"('main.jsonnet','main.ncl','main.sh')") == 3 &&
|
||||
"('main.jsonnet','main.ncl')") == 2 &&
|
||||
ok;
|
||||
/* Shell has an import rule now: a path built at runtime is its own abstention. */
|
||||
ok = generic_count(store, "SELECT count(*) FROM resolver_occurrences WHERE "
|
||||
"project='unsupported-imports' "
|
||||
"AND relation='IMPORTS' AND emitted=0 AND target='' "
|
||||
"AND reason='script_source_path_computed' AND "
|
||||
"file_path='main.sh'") == 1 &&
|
||||
ok;
|
||||
ok = generic_count(store,
|
||||
"SELECT count(*) FROM edges e JOIN nodes s ON s.id=e.source_id "
|
||||
|
||||
@@ -627,7 +627,6 @@ TEST(resolver_binding_js_const_initialization_shadow_and_writes_abstain) {
|
||||
"}",
|
||||
"return target"},
|
||||
{"export {}; function caller() { const target = 42; return target; }", "return target"},
|
||||
{"export const {target} = object; function caller() { return target; }", "return target"},
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(fixtures) / sizeof(fixtures[0]); i++) {
|
||||
const char *source = fixtures[i].source;
|
||||
@@ -650,12 +649,14 @@ TEST(resolver_binding_js_const_initialization_shadow_and_writes_abstain) {
|
||||
|
||||
/* These used to be pinned as abstentions. A `let` or `var` with a later write still
|
||||
* names its one declaration, and a USAGE edge is about which declaration a name
|
||||
* selects, not which value it holds. A function const is a declared function. */
|
||||
* selects, not which value it holds. A function const is a declared function, and a
|
||||
* name a module-level pattern declares is that variable, whatever `object` holds. */
|
||||
TEST(resolver_binding_js_variable_and_function_const_references_bind) {
|
||||
static const char *const sources[] = {
|
||||
"export let target = 42; function caller() { return target; }",
|
||||
"export var target = 42; function caller() { return target; }",
|
||||
"export const target = () => 42; function caller() { return target; }",
|
||||
"export const {target} = object; function caller() { return target; }",
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(sources) / sizeof(sources[0]); i++) {
|
||||
CBMFileResult *result = cbm_extract_file(sources[i], (int)strlen(sources[i]),
|
||||
|
||||
@@ -25,7 +25,8 @@ TEST(resolver_capability_is_never_an_occurrence_proof) {
|
||||
ASSERT(cbm_resolve_capabilities(CBM_LANG_JULIA, &capability));
|
||||
ASSERT_EQ(capability.proof_subsets, 0);
|
||||
ASSERT(cbm_resolve_capabilities(CBM_LANG_LUA, &capability));
|
||||
ASSERT_EQ(capability.proof_subsets, CBM_RESOLVE_CAP_SCOPE);
|
||||
ASSERT(capability.proof_subsets & CBM_RESOLVE_CAP_MEMBER);
|
||||
ASSERT_STR_EQ(capability.adapter, "lua_source");
|
||||
ASSERT(cbm_resolve_capabilities(CBM_LANG_DART, &capability));
|
||||
ASSERT(!(capability.proof_subsets & CBM_RESOLVE_CAP_TYPE_FLOW));
|
||||
PASS();
|
||||
@@ -138,14 +139,15 @@ TEST(resolver_capabilities_match_existing_bounded_producers) {
|
||||
for (size_t i = 0; i < sizeof(embedded) / sizeof(embedded[0]); i++) {
|
||||
ASSERT(cbm_resolve_capabilities(embedded[i], &capability));
|
||||
ASSERT_STR_EQ(capability.adapter, "embedded_javascript_lexical");
|
||||
ASSERT_EQ(capability.proof_subsets, CBM_RESOLVE_CAP_SCOPE | CBM_RESOLVE_CAP_CALLABLE);
|
||||
ASSERT_EQ(capability.proof_subsets, CBM_RESOLVE_CAP_SCOPE | CBM_RESOLVE_CAP_IMPORT_EXPORT |
|
||||
CBM_RESOLVE_CAP_CALLABLE);
|
||||
ASSERT_EQ(capability.edges[CBM_RESOLVE_EDGE_CALLS].availability,
|
||||
CBM_RESOLVE_EDGE_SUPPORTED_SUBSET);
|
||||
ASSERT_EQ(capability.edges[CBM_RESOLVE_EDGE_CALL_REFERENCE].availability,
|
||||
CBM_RESOLVE_EDGE_SUPPORTED_SUBSET);
|
||||
ASSERT_EQ(capability.edges[CBM_RESOLVE_EDGE_IMPORTS].availability,
|
||||
CBM_RESOLVE_EDGE_UNSUPPORTED);
|
||||
ASSERT_NOT_NULL(strstr(capability.limitation, "inline script"));
|
||||
CBM_RESOLVE_EDGE_SUPPORTED_SUBSET);
|
||||
ASSERT_NOT_NULL(strstr(capability.limitation, "Inline scripts"));
|
||||
}
|
||||
ASSERT(cbm_resolve_capabilities(CBM_LANG_YAML, &capability));
|
||||
ASSERT_NOT_NULL(strstr(capability.edges[CBM_RESOLVE_EDGE_USAGE].limitation, "Compose"));
|
||||
|
||||
@@ -35,15 +35,10 @@ static const quality_case_t QUALITY_CASES[] = {
|
||||
{"Rust", "main.rs", "actual.rs", "decoy.rs",
|
||||
"mod actual;\nuse crate::actual::target;\nfn caller() { target(); }\nfn main() {}\n",
|
||||
"pub fn target() {}\n", "pub fn target() {}\n", true, NULL, NULL},
|
||||
/* WHY: Lua require result/export-table binding has no proof adapter.
|
||||
* TRIED: explicit local require plus a named function in a literal table;
|
||||
* scope-only binding cannot prove the cross-file returned member. */
|
||||
{"Lua", "main.lua", "actual.lua", "decoy.lua",
|
||||
"local actual = require('actual')\nlocal function caller() actual.target() end\n",
|
||||
"local function target() end\nreturn {target=target}\n",
|
||||
"local function target() end\nreturn {target=target}\n", false,
|
||||
"require return-table member binding unimplemented",
|
||||
"explicit local require and literal returned table"},
|
||||
"local function target() end\nreturn {target=target}\n", true, NULL, NULL},
|
||||
/* WHY: Perl's legacy engine has no cross-file proof bridge.
|
||||
* TRIED: a literal require of a file declaring a main-package sub, with
|
||||
* repository-root cwd as the fixture's execution context. */
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user