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:
Martin Vogel committed 2026-11-02 23:09:26 +01:00
1 parent 4ee39037f7
commit 4cf24b147c
29 files changed
+5395 -453

No files matched your search

+5
View File
@@ -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 \
+12
View File
@@ -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);
}
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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
+91
View File
@@ -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};
+412
View File
@@ -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};
+308
View File
@@ -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};
+250
View File
@@ -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
View File
@@ -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;
+19
View File
@@ -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);
+51 -18
View File
@@ -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";
+46 -3
View File
@@ -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 = {
+3
View File
@@ -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,
+56 -2
View File
@@ -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
+93
View File
@@ -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;
}
+96 -1
View File
@@ -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))
+10 -19
View File
@@ -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();
}
+8 -19
View File
@@ -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
View File
@@ -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();
+18 -38
View File
@@ -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();
}
-27
View File
@@ -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);
}
+48 -29
View File
@@ -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 "
+3 -2
View File
@@ -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]),
+6 -4
View File
@@ -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"));
+1 -6
View File
@@ -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