Files
codebase-memory-mcp/internal/cbm/resolve/binding.c
T
Martin Vogel 7c8b1360ac fix(python): a namespace handed on or aliased only to be read writes nothing
sqlalchemy's package inits (sqlalchemy, sqlalchemy.orm, sqlalchemy.sql) and
orm/collections.py end with `__go(locals())`. Any module-level locals() handed
to a call failed the whole file as a dynamic namespace effect, so every import
there was unaudited and every export UNSUPPORTED: 14,464 real CALLS edges of
sqlalchemy abstained as python_stabilized_export_binding, and their children
(`from sqlalchemy import event`) as python_export_member_dispatch_unproven.

Three rules, each true of Python:

- `f(locals())` / `f(vars())` at module level writes nothing when f is the
  module's one undecorated def, bound before the call, whose body never spells
  the parameter receiving the namespace, nor locals, vars, eval, exec or a
  frame's locals. The body's own effects are walked as anywhere else.
- eval/exec of one plain string literal that is a single identifier only reads
  it (pydantic/v1/utils.py `eval('__IPYTHON__')`).
- `alias = globals()` in a function writes nothing when every other spelling
  of the alias there is a membership test, an item read or a read-only method,
  and the function has no other way to reach it (sqlalchemy exc.py and
  ext/automap.py). A module-level alias stays an effect: it is a module
  attribute another file can write through.

Referee against SCIP, python, 10 repos: recall 70.5% -> 82.0%, precision
99.3% -> 99.2%. sqlalchemy 42.9% -> 67.6%, pydantic 85.4% -> 90.6%; click,
flask, poetry, django, celery, httpx, scrapy, starlette unchanged. Every new
sqlalchemy SCIP disagreement was listed and read: builtin/typing name
collisions, same-line chain mappings, and narrowed-type selections.

The pipeline gap python-reexport.go_reexport is now required: __go never
spells lcls. Its parameter-write decoy stays forbidden.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-11-03 06:21:26 +01:00

7727 lines
373 KiB
C

#include "binding.h"
#include "source_adapter.h"
#include "stats.h"
#include "../helpers.h"
#include "../lang_specs.h"
#include <stdlib.h>
#include <limits.h>
#include <string.h>
#include "syntax_index.h"
enum { BINDING_MAX_DEPTH = 512, BINDING_INITIAL_CAP = 32 };
enum {
BINDING_DECLARATION = 1,
BINDING_IMPORT = 2,
BINDING_BLOCKED = 4,
BINDING_EXPORTED = 8,
BINDING_EXPORT_MARKER = 16,
BINDING_TYPE_DECLARATION = 32,
BINDING_VALUE_TYPE = 64,
BINDING_EXPORT_CONFLICT = 128,
BINDING_TYPE_EXPORT_MARKER = 256,
BINDING_TYPE_EXPORTED = 512,
BINDING_NAMESPACE_MEMBER = 1024,
BINDING_VALUE_DECLARATION = 2048,
/* ECMAScript only. A type-only import names nothing at runtime. */
BINDING_TYPE_IMPORT = 4096,
/* A declarator initialized by a function: callable once joined to its
* extracted Function definition by exact source lines. */
BINDING_FUNCTION_VALUE = 8192,
/* Assigning a const is a runtime error, so a write cannot be a value flow. */
BINDING_CONST_VALUE = 16384,
/* Some write rebinds this variable: its members are no longer knowable. */
BINDING_WRITTEN = 32768,
/* Rebound by a decorator: blocked until the decorators are proven to keep
* the declaration's identity. */
BINDING_DECORATED = 65536
};
typedef enum {
JS_BINDER_NONE = 0,
JS_BINDER_VAR,
JS_BINDER_LEXICAL,
JS_BINDER_PARAMETER,
JS_BINDER_WRITE
} js_binder_t;
typedef enum { JS_SPACE_ANY = 0, JS_SPACE_VALUE, JS_SPACE_TYPE } js_space_t;
typedef struct {
const char *name;
const char *target;
uint32_t at;
unsigned flags;
uint32_t scope;
uint32_t value_start_line, value_end_line;
unsigned value_definitions;
bool http_escaped, http_factory_escaped;
const char *http_import_path;
/* ECMAScript: start of the initializer that binds at `at`, and the module
* binding this row introduces. */
uint32_t at_start;
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;
/* What one Python binder received; see cbm_resolve_python_root_t. */
cbm_resolve_python_root_kind_t value_kind;
const char *value_chain;
uint32_t value_scope;
bool value_none;
/* Python: from value_from to value_until every path reaches this binder. Zero until:
* no such stretch. */
uint32_t value_from, value_until;
bool import_root;
/* JavaScript `var self = this`: the object that `this` is there. */
const char *js_this_owner;
bool js_this_prototype;
} binding_name_t;
typedef struct binding_pending_declaration {
const char *name;
uint32_t scope;
const struct binding_pending_declaration *parent;
} binding_pending_declaration_t;
typedef struct {
const char *name;
uint32_t start;
uint32_t end;
uint32_t scope;
const binding_pending_declaration_t *pending;
bool invocation;
bool jsx_component;
bool type_position;
/* The `Ns` of a type annotation's `Ns.Type`: spelled as a value, used as a type. */
bool type_path_root;
/* A plain `a.b.c` member read. The occurrence is `c`; `a` is what binds. */
const char *member_path;
/* What `this` means here, when a member assignment or literal decides it. */
const char *this_owner;
bool this_prototype;
bool http_escape, http_factory_escape;
const char *http_scope, *http_url;
/* Python receiver expression with call arguments dropped: `a.b().c`. */
const char *chain;
/* The direct-argument carrier spells `a.b` as b over the attribute's span.
* The spelling is a member name here, never a lexical read of b. */
bool attribute_terminal;
/* Inside a comprehension written directly in a class body. */
bool class_comprehension;
} binding_site_t;
typedef struct {
const char *qualified_name;
uint32_t parent;
bool class_scope;
bool function_scope; /* ECMAScript: the boundary a `var` hoists to. */
bool namespace_scope;
/* The class whose body directly declares this function, as scope + 1, and
* the first parameter that carries its instance or class. */
uint32_t method_of;
const char *self_name;
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 {
const char *name;
uint32_t scope;
} js_write_t;
typedef enum {
JS_VALUE_UNKNOWN = 0,
JS_VALUE_IDENTIFIER,
JS_VALUE_FUNCTION,
JS_VALUE_REQUIRE,
JS_VALUE_OBJECT
} js_value_kind_t;
/* `Owner.member = function ...`, `Owner.prototype.member = ...` or a member of the
* object literal `const Owner = {...}`, at module level. */
#define JS_MEMBER_COMPUTED "[]"
typedef struct {
const char *owner, *member;
const char *function_name, *local;
const char *target;
uint32_t start_line, end_line;
bool prototype, conflict;
} js_member_t;
typedef struct {
const char *export_name;
const char *owner; /* `owner.name = v`, kept only if owner proves to be module.exports. */
js_value_kind_t value;
const char *local;
const char *source, *member;
const char *function_name;
uint32_t start_line, end_line;
unsigned kind;
bool whole, bare_exports, default_import;
} js_raw_export_t;
typedef struct {
cbm_resolve_proof_pool_t pool;
cbm_resolve_lookup_context_t context;
const char *name;
uint32_t start, end;
CBMSourceOrigin origin;
const cbm_resolve_proof_t *proof;
} binding_observation_t;
typedef struct {
uint32_t start, end;
uint32_t parent; /* one-based enclosing loop */
} python_loop_t;
/* `if isinstance(E, C):` and its kin: in [from, until] E holds a C. */
typedef struct {
const char *chain, *type;
uint32_t from, until, scope;
uint32_t parent; /* one-based enclosing narrowing */
} python_narrow_t;
typedef struct {
const char *chain;
uint32_t at; /* where the statement ends: its right side still reads what was there */
uint32_t scope;
const char *value; /* what a plain assignment stores, when followable */
uint32_t value_at;
} python_store_t;
typedef struct {
const char *name; /* the local name the statement went through */
const char *member; /* the dotted path it writes, NULL when any name can change */
} python_write_t;
typedef struct {
const char *alias, *of;
} python_alias_t;
/* `__go(locals())` at module level: the callee, and where the call starts. */
typedef struct {
const char *callee;
uint32_t at;
} python_handoff_t;
struct cbm_resolve_binding_index {
CBMArena *arena;
binding_observation_t *observation;
const CBMFileResult *result;
CBMLanguage language;
const char *source;
size_t source_len;
binding_name_t *names;
size_t name_count;
size_t name_capacity;
struct binding_seen *seen;
size_t seen_capacity, seen_count, seen_rows;
const CBMImport **python_import_order;
size_t python_import_order_count;
bool python_import_order_ready;
binding_site_t *sites;
size_t site_count;
size_t site_capacity;
const char *reason;
cbm_resolve_analysis_t analysis;
bool javascript_module;
/* Only the manifest's declaration made this file a module. */
bool javascript_module_declared_only;
bool python_annotation_binding_preserved;
bool python_annotation_storage_bound;
python_write_t *python_writes; /* sorted by name */
python_alias_t *python_aliases; /* `alias = name` statements */
size_t python_alias_count, python_alias_capacity;
size_t python_write_count, python_write_capacity;
bool python_effects_complete;
bool python_writes_unseen; /* the audit stopped before the end of the file */
bool python_name_handed; /* __name__ goes to a call: that may build the module */
bool python_namespace_open; /* a computed globals() store can add any name */
python_handoff_t *python_handoffs; /* checked once every binder is known */
size_t python_handoff_count, python_handoff_capacity;
python_narrow_t *python_narrows; /* sorted by from, outermost first, once the walk ends */
size_t python_narrow_count, python_narrow_capacity;
python_store_t *python_stores; /* attribute stores, sorted by chain then position */
size_t python_store_count, python_store_capacity;
python_loop_t *python_loops; /* in start order: the walk's */
size_t python_loop_count, python_loop_capacity;
uint32_t python_open_loop; /* one-based innermost loop being walked */
cbm_resolve_python_nested_import_t *python_nested;
size_t python_nested_count, python_nested_capacity;
cbm_resolve_python_star_t *python_stars;
size_t python_star_count, python_star_capacity;
cbm_resolve_python_class_t *python_classes;
size_t python_class_count, python_class_capacity;
cbm_resolve_python_member_t *python_members;
size_t python_member_count, python_member_capacity;
cbm_resolve_python_decorated_t *python_decorated;
size_t python_decorated_count, python_decorated_capacity;
const char *http_default_target, *http_default_base;
cbm_resolve_http_dependency_t *http_dependencies;
size_t http_dependency_count, http_dependency_capacity;
bool http_domain_unknown;
binding_scope_t *scopes;
size_t scope_count, scope_capacity;
uint32_t current_scope;
const binding_pending_declaration_t *current_pending;
uint32_t python_wildcard_barrier;
cbm_resolve_source_import_t *imports;
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;
size_t js_export_count;
bool js_commonjs, js_whole_export, js_exports_rebound, js_exports_detached, js_exports_open;
const char **js_export_aliases;
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;
};
static bool kind_is(TSNode node, const char *kind) {
return !ts_node_is_null(node) && strcmp(ts_node_type(node), kind) == 0;
}
static TSNode field(TSNode node, const char *name) {
if (ts_node_is_null(node))
return (TSNode){0};
return ts_node_child_by_field_name(node, name, (uint32_t)strlen(name));
}
static const char *node_text(cbm_resolve_binding_index_t *index, TSNode node) {
if (ts_node_is_null(node))
return NULL;
uint32_t start = ts_node_start_byte(node);
uint32_t end = ts_node_end_byte(node);
if (end <= start || end > index->source_len) {
return NULL;
}
return cbm_arena_strndup(index->arena, index->source + start, end - start);
}
static void fail_index(cbm_resolve_binding_index_t *index, cbm_resolve_analysis_t analysis,
const char *reason) {
index->analysis = analysis;
index->reason = reason;
}
static void *grow_array(cbm_resolve_binding_index_t *index, void *old, size_t count,
size_t *capacity, size_t width) {
if (count < *capacity) {
return old;
}
size_t next = *capacity ? *capacity * 2 : BINDING_INITIAL_CAP;
if (next < *capacity || next > SIZE_MAX / width) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "binding_capacity");
return NULL;
}
void *result = cbm_arena_alloc(index->arena, next * width);
if (!result) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "binding_allocation");
return NULL;
}
if (count) {
memcpy(result, old, count * width);
}
*capacity = next;
return result;
}
static void add_name(cbm_resolve_binding_index_t *index, const char *name, uint32_t at,
unsigned flags) {
if (!name || !*name) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "binding_name_missing");
return;
}
if (index->language == CBM_LANG_PYTHON && index->current_scope == 0 &&
!strcmp(name, "__annotations__"))
index->python_annotation_storage_bound = true;
binding_name_t *names =
grow_array(index, index->names, index->name_count, &index->name_capacity, sizeof(*names));
if (!names) {
return;
}
index->names = names;
const char *target = NULL;
if (flags & (BINDING_DECLARATION | BINDING_TYPE_DECLARATION | BINDING_NAMESPACE_MEMBER |
BINDING_VALUE_DECLARATION | BINDING_DECORATED)) {
const char *owner = index->current_scope < index->scope_count
? index->scopes[index->current_scope].qualified_name
: index->result->module_qn;
target = cbm_arena_sprintf(index->arena, "%s.%s", owner, name);
if (!target) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "binding_target_allocation");
return;
}
}
names[index->name_count++] = (binding_name_t){
.name = name, .target = target, .at = at, .flags = flags, .scope = index->current_scope};
}
/* Whether a (scope, name) is bound yet, asked while names still arrive in source order.
* Rows enter on the first question after they were added, so a file that never asks
* pays nothing and each row is hashed once however often the question is asked. */
typedef struct binding_seen {
const char *name;
uint32_t scope;
bool import_root;
} binding_seen_t;
static size_t binding_seen_slot(const binding_seen_t *slots, size_t capacity, const char *name,
uint32_t scope) {
uint64_t hash = UINT64_C(14695981039346656037) ^ scope;
for (const unsigned char *at = (const unsigned char *)name; *at; at++)
hash = (hash ^ *at) * UINT64_C(1099511628211);
size_t slot = (size_t)(hash ^ (hash >> 29)) & (capacity - 1);
while (slots[slot].name && (slots[slot].scope != scope || strcmp(slots[slot].name, name)))
slot = (slot + 1) & (capacity - 1);
return slot;
}
static const binding_seen_t *binding_seen_find(cbm_resolve_binding_index_t *index, const char *name,
uint32_t scope) {
size_t needed = (index->seen_count + index->name_count - index->seen_rows) * 2 + 1;
if (needed > index->seen_capacity) {
size_t capacity = index->seen_capacity ? index->seen_capacity : 32;
while (capacity < needed * 2)
capacity *= 2;
binding_seen_t *slots = cbm_arena_calloc(index->arena, capacity * sizeof(*slots));
if (!slots) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "binding_allocation");
return NULL;
}
for (size_t i = 0; i < index->seen_capacity; i++)
if (index->seen[i].name)
slots[binding_seen_slot(slots, capacity, index->seen[i].name,
index->seen[i].scope)] = index->seen[i];
index->seen = slots;
index->seen_capacity = capacity;
}
cbm_resolve_stats_add(CBM_RESOLVE_STAT_LOOKUP_PROBES, index->name_count - index->seen_rows + 1);
for (; index->seen_rows < index->name_count; index->seen_rows++) {
const binding_name_t *row = &index->names[index->seen_rows];
binding_seen_t *slot = &index->seen[binding_seen_slot(index->seen, index->seen_capacity,
row->name, row->scope)];
if (!slot->name) {
*slot = (binding_seen_t){.name = row->name, .scope = row->scope};
index->seen_count++;
}
slot->import_root |= row->import_root;
}
if (!index->seen_capacity)
return NULL;
const binding_seen_t *found =
&index->seen[binding_seen_slot(index->seen, index->seen_capacity, name, scope)];
return found->name ? found : NULL;
}
/* A value declaration never establishes callable identity. Mark both the
* complete invocation and its identifier, including transparent wrappers;
* duplicate site rows therefore retain the same invocation classification. */
static bool invocation_site(TSNode node) {
if (kind_is(node, "call_expression") || kind_is(node, "new_expression"))
return true;
for (unsigned depth = 0; depth < BINDING_MAX_DEPTH; depth++) {
TSNode parent = ts_node_parent(node);
if (ts_node_is_null(parent))
return false;
if (kind_is(parent, "call_expression"))
return ts_node_eq(node, field(parent, "function"));
if (kind_is(parent, "new_expression"))
return ts_node_eq(node, field(parent, "constructor"));
if (!kind_is(parent, "parenthesized_expression") && !kind_is(parent, "as_expression") &&
!kind_is(parent, "satisfies_expression") && !kind_is(parent, "non_null_expression") &&
!kind_is(parent, "type_assertion"))
return false;
node = parent;
}
return true;
}
static bool javascript_language(CBMLanguage language);
static bool http_method(TSNode member, cbm_resolve_binding_index_t *index) {
const char *method = node_text(index, field(member, "property"));
return method && (!strcmp(method, "get") || !strcmp(method, "post") || !strcmp(method, "put") ||
!strcmp(method, "delete") || !strcmp(method, "patch") ||
!strcmp(method, "head") || !strcmp(method, "options"));
}
/* A request is the sole non-escaping use admitted here. Even reading a
* mutable subobject, passing the client as an argument, or calling an unknown
* method invalidates the HTTP-only claim. Generic binding facts are unchanged. */
static bool http_escape(TSNode node, cbm_resolve_binding_index_t *index, bool factory) {
if (!kind_is(node, "identifier") && !kind_is(node, "shorthand_property_identifier"))
return false;
TSNode member = ts_node_parent(node), call = ts_node_parent(member);
const char *method = factory ? node_text(index, field(member, "property")) : NULL;
if (method && !strcmp(method, "create") && kind_is(member, "member_expression") &&
ts_node_eq(node, field(member, "object")) && kind_is(call, "call_expression") &&
ts_node_eq(member, field(call, "function")))
return false;
return !kind_is(member, "member_expression") || !ts_node_eq(node, field(member, "object")) ||
!http_method(member, index) || !kind_is(call, "call_expression") ||
!ts_node_eq(member, field(call, "function")) ||
ts_node_is_null(field(call, "arguments")) ||
ts_node_named_child_count(field(call, "arguments")) != 1 ||
!kind_is(ts_node_named_child(field(call, "arguments"), 0), "string");
}
static const char *http_call_scope(cbm_resolve_binding_index_t *index, TSNode node) {
for (unsigned depth = 0; depth < BINDING_MAX_DEPTH; depth++) {
node = ts_node_parent(node);
if (ts_node_is_null(node))
return NULL;
if (kind_is(node, "program"))
return index->result->module_qn;
if (kind_is(node, "function_declaration")) {
TSNode parent = ts_node_parent(node);
if (kind_is(parent, "export_statement"))
parent = ts_node_parent(parent);
TSNode name = field(node, "name");
if (!kind_is(parent, "program") || !kind_is(name, "identifier"))
return NULL;
const char *text = node_text(index, name);
return text ? cbm_arena_sprintf(index->arena, "%s.%s", index->result->module_qn, text)
: NULL;
}
if (kind_is(node, "arrow_function") || kind_is(node, "function_expression") ||
kind_is(node, "generator_function") ||
kind_is(node, "generator_function_declaration") || kind_is(node, "method_definition") ||
kind_is(node, "class_body"))
return NULL;
}
return NULL;
}
static const char *http_call_url(cbm_resolve_binding_index_t *index, TSNode node) {
if (!kind_is(node, "call_expression"))
return NULL;
TSNode args = field(node, "arguments");
/* Additional request options may override baseURL or the HTTP method. */
if (ts_node_is_null(args) || ts_node_named_child_count(args) != 1)
return NULL;
TSNode value = ts_node_named_child(args, 0);
if (!kind_is(value, "string"))
return NULL;
const char *text = node_text(index, value);
size_t length = text ? strlen(text) : 0;
if (length < 2 || strchr(text, '\\'))
return NULL;
return cbm_arena_strndup(index->arena, text + 1, length - 2);
}
/* The extractor's invocation span is the whole opening tag. Only an exact
* simple component identifier may use ordinary lexical binding proof;
* member, namespace and intrinsic tag syntax needs a separate contract. */
static bool jsx_component_site(const cbm_resolve_binding_index_t *index, TSNode node) {
if (!kind_is(node, "jsx_opening_element") && !kind_is(node, "jsx_self_closing_element"))
return false;
TSNode name = field(node, "name");
if (!kind_is(name, "identifier"))
return false;
uint32_t start = ts_node_start_byte(name), end = ts_node_end_byte(name);
return end > start && end <= index->source_len && index->source[start] >= 'A' &&
index->source[start] <= 'Z';
}
static const char *python_import_canonical(const cbm_resolve_binding_index_t *index,
const char *chain);
static const char *python_annotation_chain(cbm_resolve_binding_index_t *index, TSNode node,
unsigned depth);
const char *cbm_resolve_python_typed_text(CBMArena *arena, const char *type, const char *rest) {
const char *marker = strchr(type, '@');
size_t head = marker ? (size_t)(marker - type) : strlen(type);
if (marker && !strcmp(marker, "@type"))
return cbm_arena_sprintf(arena, "%.*s%s", (int)head, type, rest);
return cbm_arena_sprintf(arena, "%.*s()%s%s", (int)head, type, marker ? marker : "", rest);
}
static bool listed(const char *kind, const char *const *list);
/* The enclosing def declares this name as a parameter whose default is None. */
static bool python_parameter_defaults_none(cbm_resolve_binding_index_t *index, TSNode node,
const char *name) {
TSNode function = node;
for (unsigned depth = 0; depth < BINDING_MAX_DEPTH && !ts_node_is_null(function); depth++) {
if (kind_is(function, "lambda") || kind_is(function, "class_definition"))
return false;
if (kind_is(function, "function_definition"))
break;
function = ts_node_parent(function);
}
TSNode parameters =
kind_is(function, "function_definition") ? field(function, "parameters") : (TSNode){0};
if (ts_node_is_null(parameters) || !name)
return false;
bool found = false;
TSTreeCursor cursor = ts_tree_cursor_new(parameters);
for (bool more = ts_tree_cursor_goto_first_child(&cursor); more && !found;
more = ts_tree_cursor_goto_next_sibling(&cursor)) {
TSNode parameter = ts_tree_cursor_current_node(&cursor);
if (!kind_is(parameter, "default_parameter") &&
!kind_is(parameter, "typed_default_parameter"))
continue;
const char *spelled = node_text(index, field(parameter, "name"));
found = spelled && !strcmp(spelled, name) && kind_is(field(parameter, "value"), "none");
}
ts_tree_cursor_delete(&cursor);
return found;
}
/* The access path of a Python expression, as text: names, member steps and
* calls with their arguments dropped. Only forms whose value can be followed
* statically have one. Two-argument super and one-argument type keep their
* identifiers, because those name the owner. */
static const char *python_chain(cbm_resolve_binding_index_t *index, TSNode node, unsigned depth) {
if (ts_node_is_null(node) || depth > 32)
return NULL;
if (kind_is(node, "identifier"))
return node_text(index, node);
/* Awaiting a coroutine yields what its function returns; calling one without
* await and then reading a member is not code that runs. */
if (kind_is(node, "parenthesized_expression") || kind_is(node, "await"))
return ts_node_named_child_count(node) == 1
? python_chain(index, ts_node_named_child(node, 0), depth + 1)
: NULL;
if (kind_is(node, "attribute")) {
const char *object = python_chain(index, field(node, "object"), depth + 1);
const char *member = node_text(index, field(node, "attribute"));
return object && member ? cbm_arena_sprintf(index->arena, "%s.%s", object, member) : NULL;
}
/* x[k] and x[a:b]: what the container holds, read at evaluation. */
if (kind_is(node, "subscript") && ts_node_named_child_count(node) == 2) {
const char *object = python_chain(index, field(node, "value"), depth + 1);
return object ? cbm_arena_sprintf(index->arena, "%s%s", object,
kind_is(field(node, "subscript"), "slice") ? "[:]" : "[]")
: NULL;
}
/* `x or Y()` for a parameter defaulting to None: Y() is what the signature provides. */
if (kind_is(node, "boolean_operator")) {
const char *operator= node_text(index, field(node, "operator"));
TSNode left = field(node, "left");
if (operator&& !strcmp(operator, "or") && kind_is(left, "identifier") &&
python_parameter_defaults_none(index, node, node_text(index, left)))
return python_chain(index, field(node, "right"), depth + 1);
return NULL;
}
/* `A() if c else None`: a None says nothing about the class a member read finds. */
if (kind_is(node, "conditional_expression") && ts_node_named_child_count(node) == 3) {
TSNode first = ts_node_named_child(node, 0), second = ts_node_named_child(node, 2);
if (kind_is(first, "none") != kind_is(second, "none"))
return python_chain(index, kind_is(first, "none") ? second : first, depth + 1);
return NULL;
}
if (!kind_is(node, "call"))
return NULL;
TSNode function = field(node, "function"), args = field(node, "arguments");
const char *callee = python_chain(index, function, depth + 1);
if (!callee)
return NULL;
uint32_t count = ts_node_is_null(args) ? 0 : ts_node_named_child_count(args);
/* partial(f, ...) keeps what it wraps: py_members checks the import. */
if (count && (!strcmp(callee, "partial") || !strcmp(callee, "functools.partial"))) {
const char *wrapped = python_chain(index, ts_node_named_child(args, 0), depth + 1);
if (wrapped)
return cbm_arena_sprintf(index->arena, "%s(%s)", callee, wrapped);
}
/* copy.copy(x) is an x, by the standard library's own signature. */
size_t spelled = strlen(callee);
if (count == 1 && ((spelled >= 4 && !strcmp(callee + spelled - 4, "copy")))) {
const char *canonical = python_import_canonical(index, callee);
if (canonical && (!strcmp(canonical, "copy.copy") || !strcmp(canonical, "copy.deepcopy")))
return python_chain(index, ts_node_named_child(args, 0), depth + 1);
}
/* typing.cast(T, x) is a T: that is all it does. */
if (count == 2 && spelled >= 4 && !strcmp(callee + spelled - 4, "cast")) {
const char *canonical = python_import_canonical(index, callee);
if (canonical &&
(!strcmp(canonical, "typing.cast") || !strcmp(canonical, "typing_extensions.cast"))) {
const char *type = python_annotation_chain(index, ts_node_named_child(args, 0), 0);
return type ? cbm_resolve_python_typed_text(index->arena, type, "") : NULL;
}
}
/* sorted(x) and friends hold what x holds; py_members checks the name is the builtin. */
static const char *const passing[] = {"list", "tuple", "set", "frozenset",
"sorted", "reversed", "iter", NULL};
if (count >= 1 && listed(callee, passing)) {
const char *wrapped = python_chain(index, ts_node_named_child(args, 0), depth + 1);
if (wrapped)
return cbm_arena_sprintf(index->arena, "%s(%s)", callee, wrapped);
}
bool owner_form =
(!strcmp(callee, "super") && count == 2) || (!strcmp(callee, "type") && count == 1);
if (!owner_form)
return cbm_arena_sprintf(index->arena, "%s()", callee);
const char *first = node_text(index, ts_node_named_child(args, 0));
const char *second = count == 2 ? node_text(index, ts_node_named_child(args, 1)) : NULL;
if (!first || !kind_is(ts_node_named_child(args, 0), "identifier") ||
(count == 2 && (!second || !kind_is(ts_node_named_child(args, 1), "identifier"))))
return NULL;
return count == 2 ? cbm_arena_sprintf(index->arena, "%s(%s,%s)", callee, first, second)
: cbm_arena_sprintf(index->arena, "%s(%s)", callee, first);
}
static bool python_in_class_comprehension(const cbm_resolve_binding_index_t *index, TSNode node) {
if (!index->current_scope || !index->scopes[index->current_scope].class_scope)
return false;
for (unsigned depth = 0; !ts_node_is_null(node) && depth < 64; depth++) {
if (kind_is(node, "list_comprehension") || kind_is(node, "set_comprehension") ||
kind_is(node, "dictionary_comprehension") || kind_is(node, "generator_expression"))
return true;
if (kind_is(node, "class_definition") || kind_is(node, "function_definition") ||
kind_is(node, "lambda"))
return false;
node = ts_node_parent(node);
}
return false;
}
static void add_site(cbm_resolve_binding_index_t *index, const char *name, TSNode node) {
if (!name || !*name) {
return;
}
binding_site_t *sites =
grow_array(index, index->sites, index->site_count, &index->site_capacity, sizeof(*sites));
if (!sites) {
return;
}
index->sites = sites;
bool ecma = javascript_language(index->language);
bool jsx = ecma && jsx_component_site(index, node);
sites[index->site_count++] = (binding_site_t){
.name = name,
.start = ts_node_start_byte(node),
.end = ts_node_end_byte(node),
.scope = index->current_scope,
.pending = index->current_pending,
.invocation = jsx || invocation_site(node),
.jsx_component = jsx,
.this_owner = index->current_this_owner,
.this_prototype = index->current_this_prototype,
.http_escape = ecma && http_escape(node, index, false),
.http_factory_escape = ecma && http_escape(node, index, true),
.http_scope =
ecma && kind_is(node, "call_expression") ? http_call_scope(index, node) : NULL,
.http_url = ecma ? http_call_url(index, node) : NULL};
if (index->language == CBM_LANG_PYTHON && (kind_is(node, "call") || kind_is(node, "attribute")))
index->sites[index->site_count - 1].chain =
python_chain(index, kind_is(node, "call") ? field(node, "function") : node, 0);
if (index->language == CBM_LANG_PYTHON)
index->sites[index->site_count - 1].class_comprehension =
python_in_class_comprehension(index, node);
}
/* The same carrier over a parenthesized direct argument, `f((a.b))`. */
static void python_note_terminal_wrappers(cbm_resolve_binding_index_t *index, const char *terminal,
TSNode attribute) {
const char *chain = python_chain(index, attribute, 0);
TSNode value = attribute, parent = ts_node_parent(value);
for (unsigned depth = 0; depth < 32 && kind_is(parent, "parenthesized_expression"); depth++) {
size_t sites = index->site_count;
add_site(index, terminal, parent);
if (index->site_count > sites) {
index->sites[index->site_count - 1].attribute_terminal = true;
index->sites[index->site_count - 1].chain = chain;
}
value = parent;
parent = ts_node_parent(value);
}
}
/* Python's direct-argument carrier retains transparent parentheses. Record
* only those exact AST sites, with the identifier's lexical context. A tuple,
* invocation result, attribute or other expression cannot borrow its name. */
static void note_python_argument_wrappers(cbm_resolve_binding_index_t *index, const char *name,
TSNode identifier) {
TSNode value = identifier;
TSNode parent = ts_node_parent(value);
unsigned wrappers = 0;
while (kind_is(parent, "parenthesized_expression") && wrappers < BINDING_MAX_DEPTH) {
if (ts_node_named_child_count(parent) != 1 ||
!ts_node_eq(ts_node_named_child(parent, 0), value))
return;
wrappers++;
value = parent;
parent = ts_node_parent(value);
}
if (!wrappers || wrappers == BINDING_MAX_DEPTH || !kind_is(parent, "argument_list"))
return;
TSNode call = ts_node_parent(parent);
if (!kind_is(call, "call") || !ts_node_eq(parent, field(call, "arguments")))
return;
value = identifier;
for (unsigned i = 0; i < wrappers; i++) {
value = ts_node_parent(value);
add_site(index, name, value);
}
}
static bool listed(const char *kind, const char *const *list) {
for (; *list; list++) {
if (strcmp(kind, *list) == 0) {
return true;
}
}
return false;
}
/* Explicit grammar capability lists: a new binding form cannot silently
* become a complete lookup merely because generic extraction supports it. */
static bool supported_node(CBMLanguage language, const char *kind) {
static const char *const python[] = {"module",
"lambda",
"lambda_parameters",
"decorated_definition",
"decorator",
"class_definition",
"with_statement",
"with_clause",
"with_item",
"as_pattern",
"as_pattern_target",
"function_definition",
"parameters",
"identifier",
"block",
"expression_statement",
"return_statement",
"call",
"argument_list",
"integer",
"float",
"string",
"string_start",
"string_content",
"string_end",
"escape_sequence",
"escape_interpolation",
"interpolation",
"type_conversion",
"format_specifier",
"format_expression",
"comment",
"attribute",
"comparison_operator",
"binary_operator",
"unary_operator",
"boolean_operator",
"not_operator",
"conditional_expression",
"true",
"false",
"none",
"parenthesized_expression",
"list",
"dictionary",
"pair",
"set",
"tuple",
"subscript",
"slice",
"if_statement",
"elif_clause",
"else_clause",
"pass_statement",
"break_statement",
"continue_statement",
"while_statement",
"try_statement",
"except_clause",
"finally_clause",
"assert_statement",
"raise_statement",
"yield",
"await",
"concatenated_string",
"assignment",
"augmented_assignment",
"named_expression",
"for_statement",
"for_in_clause",
"list_comprehension",
"set_comprehension",
"dictionary_comprehension",
"generator_expression",
"if_clause",
"default_parameter",
"typed_parameter",
"typed_default_parameter",
"list_splat_pattern",
"dictionary_splat_pattern",
"list_pattern",
"tuple_pattern",
"pattern_list",
"keyword_argument",
"type",
"import_statement",
"import_from_statement",
"relative_import",
"import_prefix",
"wildcard_import",
"dotted_name",
"aliased_import",
"future_import_statement",
"delete_statement",
"global_statement",
"nonlocal_statement",
NULL};
/* Expressions the ordinary walk audits through their children. */
static const char *const python_expressions[] = {"list_splat",
"dictionary_splat",
"parenthesized_list_splat",
"ellipsis",
"except_group_clause",
"print_statement",
"chevron",
"union_type",
"member_type",
"constrained_type",
"splat_type",
"line_continuation",
NULL};
/* Pattern identifiers bind, see python_target_role, as a type alias does. */
static const char *const python_binders[] = {"match_statement",
"case_clause",
"case_pattern",
"dict_pattern",
"class_pattern",
"complex_pattern",
"splat_pattern",
"union_pattern",
"keyword_pattern",
"type_alias_statement",
NULL};
(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
* every child in the inherited evaluation/target context. In particular,
* type_parameter is also used by PEP 695 declarations: only a generic_type
* argument list is transparent, never a declaration's type_parameters field. */
static bool python_transparent_container(TSNode node, const char *parent) {
const char *kind = ts_node_type(node);
if (!strcmp(kind, "expression_list"))
return true;
if (!strcmp(kind, "generic_type"))
return parent && !strcmp(parent, "type");
if (!strcmp(kind, "type_parameter"))
return parent &&
(!strcmp(parent, "generic_type") || !strcmp(parent, "function_definition") ||
!strcmp(parent, "class_definition"));
if (!strcmp(kind, "keyword_separator") || !strcmp(kind, "positional_separator"))
return parent && (!strcmp(parent, "parameters") || !strcmp(parent, "lambda_parameters")) &&
ts_node_named_child_count(node) == 0;
return false;
}
static bool blocks_identifiers(const char *kind) {
return strstr(kind, "parameter") || strstr(kind, "assignment") || strstr(kind, "pattern") ||
strcmp(kind, "named_expression") == 0 || strcmp(kind, "for_statement") == 0 ||
strcmp(kind, "for_in_clause") == 0 || strcmp(kind, "for_generic_clause") == 0 ||
strcmp(kind, "for_numeric_clause") == 0 || strcmp(kind, "variable_declaration") == 0 ||
strcmp(kind, "delete_statement") == 0 || strcmp(kind, "global_statement") == 0 ||
strcmp(kind, "nonlocal_statement") == 0;
}
/* A Python binder is the target of a declaration/write, never every name
* read underneath the enclosing statement. Preserve the role through tuple
* and splat targets, and stop it at attribute/subscript expressions. */
static bool python_target_role(const char *kind, const char *parent, const char *field_name,
bool inherited) {
bool target = inherited;
if (parent) {
if (!strcmp(parent, "assignment") || !strcmp(parent, "augmented_assignment") ||
!strcmp(parent, "for_statement") || !strcmp(parent, "for_in_clause"))
target = field_name && !strcmp(field_name, "left");
else if (!strcmp(parent, "named_expression"))
target = field_name && !strcmp(field_name, "name");
else if (!strcmp(parent, "parameters") || !strcmp(parent, "lambda_parameters"))
target = true;
else if ((!strcmp(parent, "default_parameter") ||
!strcmp(parent, "typed_default_parameter")) &&
field_name)
target = !strcmp(field_name, "name");
else if (!strcmp(parent, "typed_parameter") && field_name && !strcmp(field_name, "type"))
target = false;
}
if (!strcmp(kind, "as_pattern_target") || !strcmp(kind, "delete_statement") ||
!strcmp(kind, "case_pattern"))
target = true;
/* def f[T](x: T) binds T; the alias name in `type X = int` is a binder too. */
if (parent && field_name && !strcmp(field_name, "type_parameters"))
target = true;
if (parent && !strcmp(parent, "type_alias_statement") && field_name)
target = !strcmp(field_name, "left");
if (!strcmp(kind, "attribute") || !strcmp(kind, "subscript"))
target = false;
return target;
}
/* A no-value annotation has a lexical target but no runtime target read or
* write. Only module simple names preserve a preceding runtime binding here;
* function locals remain shadowing declarations and class scope stays closed. */
static bool python_annotation_only(TSNode node) {
return kind_is(node, "assignment") && !ts_node_is_null(field(node, "type")) &&
ts_node_is_null(field(node, "right"));
}
static bool dynamic_name(CBMLanguage language, const char *name) {
/* setattr/delattr/__dict__ change the object they are given. When that is
* a module binding, python_namespace_effects records the escape and that
* namespace is invalidated; otherwise they cannot rebind a name audited
* 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};
(void)language;
return listed(name, python);
}
/* An identifier is a lexical read only outside the member and keyword roles:
* `node.eval`, `f(eval=1)` and `def eval(self)` never touch the builtin, and
* `value.helper` never refers to a module-level `helper`. */
static bool python_lexical_identifier(const char *parent, const char *field_name) {
if (!parent || !field_name)
return true;
if (!strcmp(parent, "attribute") && !strcmp(field_name, "attribute"))
return false;
return strcmp(parent, "keyword_argument") || strcmp(field_name, "name");
}
/* A parameter or local spelled `vars` or `eval` is not the builtin. Source
* order suffices: a parameter or earlier binder has already been recorded, and
* this runs only for the few identifiers spelled like a dynamic builtin. */
static bool python_name_shadowed(cbm_resolve_binding_index_t *index, const char *name) {
uint32_t scope = index->current_scope;
for (unsigned depth = 0; depth < BINDING_MAX_DEPTH; depth++) {
if (binding_seen_find(index, name, scope))
return true;
if (!scope || scope >= index->scope_count)
return false;
scope = index->scopes[scope].parent;
}
return true;
}
/* `eval('__IPYTHON__')`, pydantic's in_ipython: code that is one literal name only reads it.
* The argument list holds that string alone, with no escapes, interpolation or prefix. */
static bool python_code_reads_one_name(cbm_resolve_binding_index_t *index, TSNode args) {
if (ts_node_is_null(args) || ts_node_named_child_count(args) != 1)
return false;
TSNode code = ts_node_named_child(args, 0);
if (!kind_is(code, "string") || ts_node_named_child_count(code) != 3)
return false;
TSNode start = ts_node_named_child(code, 0), content = ts_node_named_child(code, 1);
if (!kind_is(start, "string_start") || !kind_is(content, "string_content") ||
ts_node_named_child_count(content) != 0 ||
ts_node_end_byte(start) - ts_node_start_byte(start) != 1)
return false;
uint32_t from = ts_node_start_byte(content), to = ts_node_end_byte(content);
if (to <= from || to > index->source_len ||
(index->source[from] >= '0' && index->source[from] <= '9'))
return false;
for (uint32_t at = from; at < to; at++) {
char c = index->source[at];
if (!(c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9')))
return false;
}
return true;
}
static bool python_spells(const cbm_resolve_binding_index_t *index, TSNode node, const char *word);
/* Names through which code can reach a namespace it was handed by another name. */
static const char *const python_namespace_reach[] = {"locals", "vars", "eval",
"exec", "f_locals", "_getframe",
"currentframe", "getargvalues", NULL};
/* One use of a namespace alias only reads: a membership test, an item read, or a read-only
* method. An item that is an assignment, del, for or with target writes. */
static bool python_alias_use_reads(cbm_resolve_binding_index_t *index, TSNode use) {
TSNode parent = ts_node_parent(use);
if (kind_is(parent, "comparison_operator"))
return true;
if (kind_is(parent, "attribute") && ts_node_eq(field(parent, "object"), use)) {
const char *member = node_text(index, field(parent, "attribute"));
TSNode call = ts_node_parent(parent);
return member && kind_is(call, "call") && ts_node_eq(field(call, "function"), parent) &&
(!strcmp(member, "get") || !strcmp(member, "keys") || !strcmp(member, "items") ||
!strcmp(member, "values") || !strcmp(member, "copy"));
}
if (!kind_is(parent, "subscript") || !ts_node_eq(field(parent, "value"), use))
return false;
TSNode target = parent, user = ts_node_parent(parent);
static const char *const wrappers[] = {"pattern_list",
"tuple_pattern",
"list_pattern",
"expression_list",
"parenthesized_expression",
"tuple",
"list",
"list_splat_pattern",
NULL};
for (unsigned depth = 0; depth < BINDING_MAX_DEPTH && listed(ts_node_type(user), wrappers);
depth++) {
target = user;
user = ts_node_parent(user);
}
if (kind_is(user, "delete_statement") || kind_is(user, "as_pattern_target"))
return false;
return !((kind_is(user, "assignment") || kind_is(user, "augmented_assignment") ||
kind_is(user, "for_statement") || kind_is(user, "for_in_clause")) &&
ts_node_eq(field(user, "left"), target));
}
/* `glob = globals()` in a function, as sqlalchemy's exc.py and automap.py do. The alias writes
* nothing if every other use of it in the function only reads and nothing there can reach it
* by another name. A module-level alias is a module attribute another file can write through,
* so it stays an effect. One cursor pass over the function per alias. */
static bool python_view_alias_reads(cbm_resolve_binding_index_t *index, TSNode call) {
TSNode assignment = ts_node_parent(call), left = field(assignment, "left");
if (!kind_is(assignment, "assignment") || !ts_node_eq(field(assignment, "right"), call) ||
!kind_is(left, "identifier") ||
!kind_is(ts_node_parent(assignment), "expression_statement"))
return false;
const char *alias = node_text(index, left);
TSNode scope = ts_node_parent(assignment);
for (unsigned depth = 0; depth < BINDING_MAX_DEPTH && !ts_node_is_null(scope) &&
!kind_is(scope, "function_definition") && !kind_is(scope, "module");
depth++) {
if (kind_is(scope, "class_definition") || kind_is(scope, "lambda"))
return false;
scope = ts_node_parent(scope);
}
if (!alias || !kind_is(scope, "function_definition"))
return false;
TSNode own = field(call, "function");
bool reads = true;
TSTreeCursor cursor = ts_tree_cursor_new(scope);
bool more = ts_tree_cursor_goto_first_child(&cursor);
unsigned depth = 1;
while (more && reads) {
TSNode node = ts_tree_cursor_current_node(&cursor);
if (kind_is(node, "identifier") && !ts_node_eq(node, own)) {
for (size_t i = 0; reads && python_namespace_reach[i]; i++)
reads = !python_spells(index, node, python_namespace_reach[i]);
if (reads && !ts_node_eq(node, left) && python_spells(index, node, alias))
reads = python_alias_use_reads(index, node);
}
if (ts_tree_cursor_goto_first_child(&cursor)) {
depth++;
continue;
}
while (!ts_tree_cursor_goto_next_sibling(&cursor)) {
if (--depth == 0 || !ts_tree_cursor_goto_parent(&cursor)) {
more = false;
break;
}
}
}
ts_tree_cursor_delete(&cursor);
return reads;
}
/* exec and eval write the namespace they run in, which an explicit globals
* argument replaces. globals() is a write only if it can be written through: a
* membership test, an item read, a read-only method or a splat into a new dict
* leaves the module as it was. */
static bool python_namespace_call_writes(cbm_resolve_binding_index_t *index, TSNode node,
const char *name) {
TSNode call = ts_node_parent(node);
if (!kind_is(call, "call") || !ts_node_eq(node, field(call, "function")))
return true;
if (strcmp(name, "globals")) {
TSNode args = field(call, "arguments");
uint32_t positional = 0;
for (uint32_t i = 0; !ts_node_is_null(args) && i < ts_node_named_child_count(args); i++)
positional += !kind_is(ts_node_named_child(args, i), "keyword_argument") &&
!kind_is(ts_node_named_child(args, i), "comment");
return positional < 2 && !python_code_reads_one_name(index, args);
}
TSNode parent = ts_node_parent(call);
if (kind_is(parent, "comparison_operator") || kind_is(parent, "dictionary_splat"))
return false;
/* exec(code, globals(), d) stores in d. */
if (kind_is(parent, "argument_list") && ts_node_named_child_count(parent) == 3 &&
ts_node_eq(call, ts_node_named_child(parent, 1))) {
const char *runner = node_text(index, field(ts_node_parent(parent), "function"));
if (runner && (!strcmp(runner, "exec") || !strcmp(runner, "eval")))
return false;
}
if (kind_is(parent, "subscript")) {
TSNode user = ts_node_parent(parent), key = field(parent, "subscript");
bool stored = (kind_is(user, "assignment") || kind_is(user, "augmented_assignment")) &&
ts_node_eq(parent, field(user, "left"));
bool literal = kind_is(key, "string") && ts_node_named_child_count(key) == 3 &&
kind_is(ts_node_named_child(key, 1), "string_content");
/* A computed key names no binding this audit saw. Names bound by statements stay what
* those say, as celery's STATICA_HACK relies on; what the module lacks is unknowable. */
if (stored && !literal) {
index->python_namespace_open = true;
return false;
}
return stored;
}
if (kind_is(parent, "attribute")) {
const char *member = node_text(index, field(parent, "attribute"));
return !member ||
(strcmp(member, "get") && strcmp(member, "keys") && strcmp(member, "items") &&
strcmp(member, "values") && strcmp(member, "copy"));
}
return !python_view_alias_reads(index, call);
}
/* `__go(locals())` at module level hands the namespace to the callee's first parameter, as
* sqlalchemy's package inits do. The callee is known only once every binder is seen, so the
* call is noted here and python_settle_handoffs decides it. */
static bool python_note_handoff(cbm_resolve_binding_index_t *index, TSNode view) {
TSNode arguments = ts_node_parent(view);
TSNode call = ts_node_parent(arguments);
if (index->current_scope != 0 || !kind_is(arguments, "argument_list") ||
!kind_is(call, "call") || !ts_node_eq(ts_node_named_child(arguments, 0), view))
return false;
TSNode callee = field(call, "function");
const char *name = kind_is(callee, "identifier") ? node_text(index, callee) : NULL;
python_handoff_t *rows =
name ? grow_array(index, index->python_handoffs, index->python_handoff_count,
&index->python_handoff_capacity, sizeof(*rows))
: NULL;
if (!rows)
return false;
index->python_handoffs = rows;
rows[index->python_handoff_count++] =
(python_handoff_t){.callee = name, .at = ts_node_start_byte(call)};
return true;
}
/* Module and class bodies expose their real namespace through locals()/vars().
* A function's locals() is a snapshot: writing to it rebinds nothing, and vars(x)
* reads x. Only the zero-argument form outside a function can write a binding;
* any other read of those names passes the view on and stays unknown. */
static bool python_namespace_view(cbm_resolve_binding_index_t *index, TSNode node,
const char *name) {
if (strcmp(name, "locals") && strcmp(name, "vars"))
return false;
TSNode call = ts_node_parent(node);
if (!kind_is(call, "call") || !ts_node_eq(node, field(call, "function")))
return true;
/* A read-only method of the view leaves the namespace as it was. */
TSNode user = ts_node_parent(call);
if (kind_is(user, "attribute") && ts_node_eq(call, field(user, "object"))) {
const char *member = node_text(index, field(user, "attribute"));
if (member &&
(!strcmp(member, "get") || !strcmp(member, "keys") || !strcmp(member, "items") ||
!strcmp(member, "values") || !strcmp(member, "copy")))
return false;
}
bool in_function = index->current_scope && index->current_scope < index->scope_count &&
!index->scopes[index->current_scope].class_scope;
TSNode args = field(call, "arguments");
if (in_function || (!ts_node_is_null(args) && ts_node_named_child_count(args) != 0) ||
python_name_shadowed(index, name))
return false;
return !python_note_handoff(index, call);
}
/* 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) {
for (unsigned depth = 0; depth < BINDING_MAX_DEPTH; depth++) {
TSNode parent = ts_node_parent(node);
if (ts_node_is_null(parent))
return false;
if (kind_is(parent, "if_statement") && ts_node_eq(node, field(parent, "consequence"))) {
TSNode condition = field(parent, "condition");
if (kind_is(condition, "attribute"))
condition = field(condition, "attribute");
const char *text =
kind_is(condition, "identifier") ? node_text(index, condition) : NULL;
if (text && !strcmp(text, "TYPE_CHECKING"))
return true;
}
node = parent;
}
return false;
}
static const char *python_from_module(cbm_resolve_binding_index_t *index, TSNode node) {
return node_text(index, field(node, "module_name"));
}
/* The same module-path spelling the extractor records for a top-level import. */
static void python_note_nested_import(cbm_resolve_binding_index_t *index, TSNode node, TSNode child,
const char *local) {
bool from = kind_is(node, "import_from_statement");
TSNode alias = field(child, "alias");
TSNode named = kind_is(child, "aliased_import") ? field(child, "name") : child;
const char *spelled = node_text(index, named);
const char *module = from ? python_from_module(index, node) : NULL;
cbm_resolve_python_nested_import_t *items =
grow_array(index, index->python_nested, index->python_nested_count,
&index->python_nested_capacity, sizeof(*items));
if (!items || !spelled || (from && !module)) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "python_nested_import_allocation");
return;
}
index->python_nested = items;
size_t length = module ? strlen(module) : 0;
const char *path = !from ? spelled
: length && module[length - 1] == '.'
? cbm_arena_sprintf(index->arena, "%s%s", module, spelled)
: cbm_arena_sprintf(index->arena, "%s.%s", module, spelled);
if (!path) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "python_nested_import_allocation");
return;
}
cbm_resolve_python_nested_import_t *entry = &items[index->python_nested_count++];
*entry = (cbm_resolve_python_nested_import_t){
.import = {.local_name = local,
.module_path = path,
.kind = from ? CBM_IMPORT_KIND_PYTHON_FROM
: ts_node_is_null(alias) ? CBM_IMPORT_KIND_PYTHON_MODULE_ROOT
: CBM_IMPORT_KIND_PYTHON_MODULE_ALIAS,
.site_start_byte = ts_node_start_byte(child),
.site_end_byte = ts_node_end_byte(child),
.source_origin = CBM_SOURCE_ORIGIN_RAW},
.at = ts_node_start_byte(child),
.scope = index->current_scope};
index->names[index->name_count - 1].nested_import = (uint32_t)index->python_nested_count;
}
static bool python_root_import_bound(cbm_resolve_binding_index_t *index, const char *name) {
const binding_seen_t *seen = name ? binding_seen_find(index, name, index->current_scope) : NULL;
return seen && seen->import_root;
}
static void note_import(cbm_resolve_binding_index_t *index, TSNode node, bool nested);
/* Only annotation lookups see these names: runtime reads find a blocked binder. */
static void python_note_annotation_imports(cbm_resolve_binding_index_t *index, TSNode node) {
size_t names = index->name_count, nested = index->python_nested_count;
size_t stars = index->python_star_count;
note_import(index, node, true);
for (size_t i = names; i < index->name_count; i++)
index->names[i].flags = BINDING_BLOCKED;
for (size_t i = nested; i < index->python_nested_count; i++)
index->python_nested[i].annotation_only = true;
for (size_t i = stars; i < index->python_star_count; i++)
index->python_stars[i].annotation_only = true;
}
static void note_import(cbm_resolve_binding_index_t *index, TSNode node, bool nested) {
bool from = kind_is(node, "import_from_statement");
uint32_t count = ts_node_child_count(node);
for (uint32_t i = 0; i < count; i++) {
TSNode child = ts_node_child(node, i);
if (from && kind_is(child, "wildcard_import")) {
index->python_wildcard_barrier = ts_node_end_byte(node);
cbm_resolve_python_star_t *stars =
grow_array(index, index->python_stars, index->python_star_count,
&index->python_star_capacity, sizeof(*stars));
const char *module = python_from_module(index, node);
if (!stars || !module) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "python_star_allocation");
return;
}
index->python_stars = stars;
stars[index->python_star_count++] =
(cbm_resolve_python_star_t){.module_path = module, .at = ts_node_end_byte(node)};
continue;
}
const char *field_name = ts_node_field_name_for_child(node, i);
if (!field_name || strcmp(field_name, "name") != 0) {
continue;
}
TSNode alias = field(child, "alias");
const char *name = node_text(index, ts_node_is_null(alias) ? child : alias);
if (ts_node_is_null(alias) && kind_is(child, "aliased_import")) {
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED, "import_alias_unavailable");
return;
}
if (name && !from && ts_node_is_null(alias)) {
const char *dot = strchr(name, '.');
if (dot) {
name = cbm_arena_strndup(index->arena, name, (size_t)(dot - name));
}
}
/* `import a.b` and `import a.c` bind the one package a, not two values. */
if (!from && ts_node_is_null(alias) && python_root_import_bound(index, name))
continue;
size_t before = index->name_count;
add_name(index, name, ts_node_start_byte(node), BINDING_IMPORT);
if (!from && ts_node_is_null(alias) && index->name_count > before)
index->names[index->name_count - 1].import_root = true;
if (nested && index->name_count > before)
python_note_nested_import(index, node, child, name);
}
}
static void note_call(cbm_resolve_binding_index_t *index, TSNode node) {
TSNode callee = field(node, "function");
if (kind_is(node, "new_expression")) {
callee = field(node, "constructor");
}
if (ts_node_is_null(callee) && ts_node_named_child_count(node)) {
callee = ts_node_named_child(node, 0);
}
if (!kind_is(callee, "identifier") && !kind_is(callee, "attribute") &&
!kind_is(callee, "dot_index_expression") && !kind_is(callee, "member_expression")) {
return;
}
const char *name = node_text(index, callee);
add_site(index, name, node);
add_site(index, name, callee);
}
typedef struct {
bool blocked;
bool import;
bool named_export;
bool type_only;
bool conditional;
uint32_t scope;
uint32_t header_scope;
const binding_pending_declaration_t *pending;
/* ECMAScript: how an identifier here binds, what a declarator under this
* statement declares, and whether the next block is a function body. */
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
* namespace. Parameter names still bind in the body scope. Keep declarations
* under construction unavailable to these eager reads, including a class
* name while its methods' defaults are being evaluated. */
static binding_walk_t python_child_context(const char *parent, const char *field_name,
binding_walk_t state) {
if (!parent || !field_name)
return state;
bool function = !strcmp(parent, "function_definition") || !strcmp(parent, "lambda");
if (function && !strcmp(field_name, "body")) {
state.pending = NULL;
} else if ((function && !strcmp(field_name, "return_type")) ||
(!strcmp(parent, "class_definition") && !strcmp(field_name, "superclasses")) ||
((!strcmp(parent, "default_parameter") ||
!strcmp(parent, "typed_default_parameter")) &&
!strcmp(field_name, "value")) ||
((!strcmp(parent, "typed_parameter") ||
!strcmp(parent, "typed_default_parameter")) &&
!strcmp(field_name, "type"))) {
state.scope = state.header_scope;
}
return state;
}
/* `global x` and `nonlocal x` make x's enclosing binding writable from here.
* That invalidates x, not the file: block exactly the listed names in the
* scopes they can rebind, including this one. Lookup through any of them then
* abstains, while every unrelated binding keeps its proof. */
static void python_note_rebinding_scope(cbm_resolve_binding_index_t *index, TSNode node,
bool global) {
uint32_t count = ts_node_named_child_count(node), declaring = index->current_scope;
for (uint32_t i = 0; i < count; i++) {
TSNode child = ts_node_named_child(node, i);
if (!kind_is(child, "identifier"))
continue;
const char *name = node_text(index, child);
add_name(index, name, ts_node_start_byte(node), BINDING_BLOCKED);
uint32_t scope = declaring;
for (unsigned depth = 0; scope && depth < BINDING_MAX_DEPTH; depth++) {
scope = index->scopes[scope].parent;
if (!global || !scope) {
index->current_scope = scope;
add_name(index, name, ts_node_start_byte(node), BINDING_BLOCKED);
}
}
index->current_scope = declaring;
}
}
/* `sys.modules[__name__]` hands out this module's own namespace object. That
* is the one way the dynamic-attribute builtins reach a binding audited here
* without a module import escaping. hasattr/getattr and a plain attribute read
* only look at it. */
static bool python_own_module_entry(cbm_resolve_binding_index_t *index, TSNode subscript) {
TSNode value = field(subscript, "value");
if (!kind_is(value, "attribute"))
return false;
const char *member = node_text(index, field(value, "attribute"));
if (!member || strcmp(member, "modules"))
return false;
TSNode key = field(subscript, "subscript");
const char *text = kind_is(key, "identifier") ? node_text(index, key) : NULL;
if (!text || strcmp(text, "__name__"))
return false;
TSNode parent = ts_node_parent(subscript);
if (kind_is(parent, "attribute")) {
TSNode user = ts_node_parent(parent);
return kind_is(user, "assignment") && ts_node_eq(parent, field(user, "left"));
}
if (kind_is(parent, "argument_list")) {
TSNode call = ts_node_parent(parent);
const char *callee = node_text(index, field(call, "function"));
return !callee || (strcmp(callee, "hasattr") && strcmp(callee, "getattr"));
}
return true;
}
/* The class annotation names, for the supported annotation forms: a dotted
* name, its string spelling, and Optional[...] around one of those. */
static const char *python_import_canonical(const cbm_resolve_binding_index_t *index,
const char *chain);
static const char *python_annotation_chain(cbm_resolve_binding_index_t *index, TSNode node,
unsigned depth);
static bool python_annotation_is_none(TSNode node) {
while (kind_is(node, "type") && ts_node_named_child_count(node) == 1)
node = ts_node_named_child(node, 0);
return kind_is(node, "none");
}
/* `X | None` says X. Two classes say neither. */
/* What a generic annotation says a value is. Its marker: @seq holds and yields X, @map holds
* X by key, @type is the class X itself. A generic of the project's own is an instance of it. */
static const char *python_generic_chain(cbm_resolve_binding_index_t *index, TSNode head,
const TSNode *args, uint32_t count, unsigned depth) {
static const char *const seq[] = {"list",
"set",
"frozenset",
"typing.List",
"typing.Set",
"typing.FrozenSet",
"typing.Sequence",
"typing.MutableSequence",
"typing.Iterable",
"typing.Iterator",
"typing.Collection",
"typing.Generator",
"typing.AsyncIterator",
"typing.AsyncIterable",
"typing.AsyncGenerator",
"typing.Deque",
"typing.Reversible",
"typing.AbstractSet",
"typing.MutableSet",
"collections.abc.Sequence",
"collections.abc.MutableSequence",
"collections.abc.Iterable",
"collections.abc.Iterator",
"collections.abc.Collection",
"collections.abc.Generator",
"collections.abc.AsyncIterator",
"collections.abc.AsyncIterable",
"collections.abc.AsyncGenerator",
"collections.abc.Set",
"collections.abc.MutableSet",
"collections.deque",
NULL};
static const char *const map[] = {"dict",
"typing.Dict",
"typing.Mapping",
"typing.MutableMapping",
"typing.DefaultDict",
"typing.OrderedDict",
"collections.abc.Mapping",
"collections.abc.MutableMapping",
"collections.OrderedDict",
"collections.defaultdict",
NULL};
static const char *const wrapper[] = {
"typing.Optional", "typing.Annotated",
"typing.ClassVar", "typing.Final",
"typing.Awaitable", "typing_extensions.Annotated",
"collections.abc.Awaitable", NULL};
const char *spelled = python_chain(index, head, 0);
if (!spelled || !count)
return NULL;
const char *canonical = python_import_canonical(index, spelled);
if (!canonical)
canonical = spelled;
const char *marker = NULL;
uint32_t arg = 0;
if (listed(canonical, wrapper)) {
return python_annotation_chain(index, args[0], depth + 1);
} else if (!strcmp(canonical, "typing.Coroutine") ||
!strcmp(canonical, "collections.abc.Coroutine")) {
return count == 3 ? python_annotation_chain(index, args[2], depth + 1) : NULL;
} else if (listed(canonical, seq)) {
marker = "@seq";
} else if (!strcmp(canonical, "tuple") || !strcmp(canonical, "typing.Tuple")) {
if (count != 2 || !kind_is(args[1], "type") || !ts_node_named_child_count(args[1]) ||
!kind_is(ts_node_named_child(args[1], 0), "ellipsis"))
return NULL;
marker = "@seq";
} else if (listed(canonical, map)) {
if (count != 2)
return NULL;
marker = "@map";
arg = 1;
} else if (!strcmp(canonical, "type") || !strcmp(canonical, "typing.Type")) {
marker = "@type";
} else if (!strncmp(canonical, "typing.", 7) || !strncmp(canonical, "typing_extensions.", 18) ||
!strncmp(canonical, "collections.", 12)) {
return NULL;
} else {
return spelled; /* Repo[str] is a Repo. */
}
const char *element = python_annotation_chain(index, args[arg], depth + 1);
/* One layer: list[list[X]] says nothing this model can carry. */
return element && !strchr(element, '@')
? cbm_arena_sprintf(index->arena, "%s%s", element, marker)
: NULL;
}
static const char *python_union_chain(cbm_resolve_binding_index_t *index, TSNode left, TSNode right,
unsigned depth) {
bool left_none = python_annotation_is_none(left), right_none = python_annotation_is_none(right);
if (left_none == right_none)
return NULL;
return python_annotation_chain(index, left_none ? right : left, depth + 1);
}
static const char *python_annotation_chain(cbm_resolve_binding_index_t *index, TSNode node,
unsigned depth) {
if (ts_node_is_null(node) || depth > 8)
return NULL;
if (kind_is(node, "type") && ts_node_named_child_count(node) == 1)
return python_annotation_chain(index, ts_node_named_child(node, 0), depth + 1);
if (kind_is(node, "identifier") || kind_is(node, "attribute"))
return python_chain(index, node, 0);
if (kind_is(node, "string")) {
const char *text = node_text(index, node);
size_t length = text ? strlen(text) : 0;
if (length < 3 || (text[0] != '"' && text[0] != '\'') || text[length - 1] != text[0])
return NULL;
for (size_t i = 1; i + 1 < length; i++)
if (!(text[i] == '.' || text[i] == '_' || (text[i] >= '0' && text[i] <= '9') ||
(text[i] >= 'a' && text[i] <= 'z') || (text[i] >= 'A' && text[i] <= 'Z')))
return NULL;
return cbm_arena_strndup(index->arena, text + 1, length - 2);
}
if (kind_is(node, "generic_type") && ts_node_named_child_count(node) == 2) {
TSNode arguments = ts_node_named_child(node, 1), args[3];
memset(args, 0, sizeof(args));
uint32_t count = ts_node_named_child_count(arguments);
for (uint32_t i = 0; i < count && i < 3; i++)
args[i] = ts_node_named_child(arguments, i);
return count <= 3
? python_generic_chain(index, ts_node_named_child(node, 0), args, count, depth)
: NULL;
}
/* The grammar reads a dotted generic, typing.Optional[X], as a subscript. */
if (kind_is(node, "subscript")) {
TSNode args[3];
memset(args, 0, sizeof(args));
uint32_t count = ts_node_named_child_count(node), used = 0;
for (uint32_t i = 1; i < count && used < 3; i++)
args[used++] = ts_node_named_child(node, i);
return count >= 2 && count <= 4
? python_generic_chain(index, field(node, "value"), args, used, depth)
: NULL;
}
if (kind_is(node, "union_type") && ts_node_named_child_count(node) == 2)
return python_union_chain(index, ts_node_named_child(node, 0), ts_node_named_child(node, 1),
depth);
if (kind_is(node, "binary_operator")) {
const char *operator= node_text(index, field(node, "operator"));
return operator&& !strcmp(operator, "|")
? python_union_chain(index, field(node, "left"), field(node, "right"), depth)
: NULL;
}
return NULL;
}
/* The top-level imports the extractor recorded, joined with the rest of a chain:
* `functools.wraps` for both `import functools; functools.wraps` and
* `from functools import wraps`. NULL when the root is not such an import. */
static int python_import_order_compare(const void *left, const void *right) {
const CBMImport *a = *(const CBMImport *const *)left, *b = *(const CBMImport *const *)right;
int order = strcmp(a->local_name, b->local_name);
return order ? order : (a > b) - (a < b);
}
/* A file's imports by local name and, within a name, in source order. */
static void python_import_order(cbm_resolve_binding_index_t *index) {
index->python_import_order_ready = true;
int count = index->result->imports.count;
const CBMImport **order =
count > 0 ? cbm_arena_alloc(index->arena, (size_t)count * sizeof(*order)) : NULL;
if (!order)
return;
size_t used = 0;
for (int i = 0; i < count; i++) {
const CBMImport *import = &index->result->imports.items[i];
if (import->local_name && import->module_path)
order[used++] = import;
}
qsort(order, used, sizeof(*order), python_import_order_compare);
index->python_import_order = order;
index->python_import_order_count = used;
}
static const char *python_import_canonical(const cbm_resolve_binding_index_t *index,
const char *chain) {
size_t root = strcspn(chain, ".(");
/* The order is a private cache of this one thread's index, even behind a const reader. */
if (!index->python_import_order_ready)
python_import_order((cbm_resolve_binding_index_t *)index);
cbm_resolve_stats_add(CBM_RESOLVE_STAT_LOOKUP_PROBES, 1);
size_t low = 0, high = index->python_import_order_count;
while (low < high) {
size_t mid = low + (high - low) / 2;
const char *name = index->python_import_order[mid]->local_name;
int compare = strncmp(name, chain, root);
if (!compare && name[root])
compare = 1;
if (compare < 0)
low = mid + 1;
else
high = mid;
}
if (low == index->python_import_order_count)
return NULL;
const CBMImport *import = index->python_import_order[low];
if (strlen(import->local_name) != root || strncmp(import->local_name, chain, root))
return NULL;
if (import->kind == CBM_IMPORT_KIND_PYTHON_MODULE_ROOT &&
strcmp(import->module_path, import->local_name))
return cbm_arena_strdup(index->arena, chain);
return cbm_arena_sprintf(index->arena, "%s%s", import->module_path, chain + root);
}
/* Every path through the block ends in return or raise: it cannot fall off the
* end, which would rebind the decorated name to None. */
static bool python_block_always_exits(TSNode block, unsigned depth) {
if (ts_node_is_null(block) || depth > 16)
return false;
TSNode last = {0};
for (uint32_t i = ts_node_named_child_count(block); i-- > 0;) {
TSNode child = ts_node_named_child(block, i);
if (!kind_is(child, "comment")) {
last = child;
break;
}
}
if (kind_is(last, "return_statement") || kind_is(last, "raise_statement"))
return true;
if (!kind_is(last, "if_statement"))
return false;
if (!python_block_always_exits(field(last, "consequence"), depth + 1))
return false;
bool has_else = false;
for (uint32_t i = 0; i < ts_node_named_child_count(last); i++) {
TSNode branch = ts_node_named_child(last, i);
if (kind_is(branch, "elif_clause") &&
!python_block_always_exits(field(branch, "consequence"), depth + 1))
return false;
if (kind_is(branch, "else_clause")) {
has_else = true;
if (!python_block_always_exits(field(branch, "body"), depth + 1))
return false;
}
}
return has_else;
}
/* Every value this function returns, outside nested scopes. A bare return and
* None say nothing about the class of the others. Returns false when one is not
* an access path, or the function yields. */
static bool python_gather_returns(cbm_resolve_binding_index_t *index, TSNode node,
const char ***out, uint32_t *count, uint32_t *room,
unsigned depth) {
if (depth > 48)
return false;
for (uint32_t i = 0; i < ts_node_named_child_count(node); i++) {
TSNode child = ts_node_named_child(node, i);
if (kind_is(child, "function_definition") || kind_is(child, "class_definition") ||
kind_is(child, "decorated_definition") || kind_is(child, "lambda"))
continue;
if (kind_is(child, "yield"))
return false;
if (kind_is(child, "return_statement")) {
if (!ts_node_named_child_count(child))
continue;
TSNode value = ts_node_named_child(child, 0);
if (kind_is(value, "none"))
continue;
const char *chain = python_chain(index, value, 0);
if (!chain)
return false;
if (*count == *room) {
uint32_t capacity = *room ? *room * 2 : 4;
const char **items = cbm_arena_alloc(index->arena, capacity * sizeof(*items));
if (!items)
return false;
if (*count)
memcpy(items, *out, *count * sizeof(*items));
*out = items;
*room = capacity;
}
(*out)[(*count)++] = chain;
continue;
}
if (!python_gather_returns(index, child, out, count, room, depth + 1))
return false;
}
return true;
}
/* Called once the function's own scope exists: its locals resolve the returns. */
static void python_note_returns(cbm_resolve_binding_index_t *index, TSNode function, size_t member,
uint32_t scope) {
const char **chains = NULL;
uint32_t count = 0, room = 0;
/* `-> Self` and `def f(self: T) -> T` return the receiver, whichever class that is. */
TSNode declared_return = field(function, "return_type");
binding_scope_t *body_scope = scope < index->scope_count ? &index->scopes[scope] : NULL;
if (member < index->python_member_count && !ts_node_is_null(declared_return) && body_scope &&
body_scope->self_name) {
const char *declared = python_annotation_chain(index, declared_return, 0);
const char *canonical = declared ? python_import_canonical(index, declared) : NULL;
TSNode parameters = field(function, "parameters");
TSNode first = ts_node_named_child_count(parameters) ? ts_node_named_child(parameters, 0)
: (TSNode){0};
const char *first_type = kind_is(first, "typed_parameter")
? python_annotation_chain(index, field(first, "type"), 0)
: NULL;
if (declared && ((canonical && (!strcmp(canonical, "typing.Self") ||
!strcmp(canonical, "typing_extensions.Self"))) ||
(first_type && !strcmp(first_type, declared)))) {
const char **kept = cbm_arena_alloc(index->arena, 2 * sizeof(*kept));
if (!kept) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "python_member_allocation");
return;
}
kept[0] = body_scope->self_is_class
? cbm_arena_sprintf(index->arena, "%s()", body_scope->self_name)
: body_scope->self_name;
kept[1] = NULL;
index->python_members[member].return_chain = NULL;
index->python_members[member].returned = kept;
index->python_members[member].returned_scope = scope;
return;
}
}
/* What __new__ and friends answer with decides whether they defer to the base, whatever
* they are annotated to return. */
const char *name = node_text(index, field(function, "name"));
bool special = name && (!strcmp(name, "__new__") || !strcmp(name, "__getattribute__") ||
!strcmp(name, "__class_getitem__"));
if (member >= index->python_member_count ||
(!special && !ts_node_is_null(field(function, "return_type"))) ||
!python_gather_returns(index, field(function, "body"), &chains, &count, &room, 0) || !count)
return;
const char **kept = cbm_arena_alloc(index->arena, (count + 1) * sizeof(*kept));
if (!kept) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "python_member_allocation");
return;
}
memcpy(kept, chains, count * sizeof(*kept));
kept[count] = NULL;
index->python_members[member].returned = kept;
index->python_members[member].returned_scope = scope;
}
/* A definition under decorators: its name, until they are proven, is blocked. */
static void python_note_decorated(cbm_resolve_binding_index_t *index, TSNode definition,
const char *target, const char *name, bool is_class) {
TSNode decorated = ts_node_parent(definition);
uint32_t count = ts_node_named_child_count(decorated);
const char **chains = cbm_arena_alloc(index->arena, (count + 1) * sizeof(*chains));
const char **canonical = cbm_arena_alloc(index->arena, (count + 1) * sizeof(*canonical));
cbm_resolve_python_decorated_t *items =
grow_array(index, index->python_decorated, index->python_decorated_count,
&index->python_decorated_capacity, sizeof(*items));
if (!chains || !canonical || !items) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "python_decorator_allocation");
return;
}
index->python_decorated = items;
uint32_t used = 0;
for (uint32_t i = 0; i < count; i++) {
TSNode child = ts_node_named_child(decorated, i);
if (!kind_is(child, "decorator"))
continue;
const char *chain = ts_node_named_child_count(child)
? python_chain(index, ts_node_named_child(child, 0), 0)
: NULL;
chains[used] = chain ? chain : "?";
canonical[used] = chain ? python_import_canonical(index, chain) : NULL;
used++;
}
chains[used] = NULL;
canonical[used] = NULL;
items[index->python_decorated_count++] =
(cbm_resolve_python_decorated_t){.target = target,
.name = name,
.scope = index->current_scope,
.is_class = is_class,
.decorators = chains,
.canonical = canonical};
}
static const char *python_decorator_chain(cbm_resolve_binding_index_t *index, TSNode decorator) {
TSNode value =
ts_node_named_child_count(decorator) ? ts_node_named_child(decorator, 0) : (TSNode){0};
return python_chain(index, value, 0);
}
static bool python_overload_stub(cbm_resolve_binding_index_t *index, TSNode decorated) {
uint32_t count = ts_node_named_child_count(decorated);
for (uint32_t i = 0; i < count; i++) {
TSNode child = ts_node_named_child(decorated, i);
if (!kind_is(child, "decorator") || !ts_node_named_child_count(child))
continue;
const char *chain = python_chain(index, ts_node_named_child(child, 0), 0);
size_t length = chain ? strlen(chain) : 0;
if (length < 8 || strcmp(chain + length - 8, "overload"))
continue;
const char *canonical = python_import_canonical(index, chain);
if (canonical && (!strcmp(canonical, "typing.overload") ||
!strcmp(canonical, "typing_extensions.overload")))
return true;
}
return false;
}
static void python_finish_narrows(cbm_resolve_binding_index_t *index);
static void python_add_narrow(cbm_resolve_binding_index_t *index, TSNode test, uint32_t from,
uint32_t until) {
TSNode function = field(test, "function"), args = field(test, "arguments");
const char *callee = kind_is(test, "call") && kind_is(function, "identifier")
? node_text(index, function)
: NULL;
if (!callee || strcmp(callee, "isinstance") || ts_node_is_null(args) ||
ts_node_named_child_count(args) != 2 || until <= from)
return;
TSNode kind = ts_node_named_child(args, 1);
const char *chain = python_chain(index, ts_node_named_child(args, 0), 0);
/* A tuple of classes says only that it is one of them. */
const char *type = kind_is(kind, "identifier") || kind_is(kind, "attribute")
? python_chain(index, kind, 0)
: NULL;
/* A call or subscript computes a new value each time. */
if (!chain || !type || strpbrk(chain, "([@"))
return;
python_narrow_t *items = grow_array(index, index->python_narrows, index->python_narrow_count,
&index->python_narrow_capacity, sizeof(*items));
if (!items) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "python_narrow_allocation");
return;
}
index->python_narrows = items;
items[index->python_narrow_count++] = (python_narrow_t){
.chain = chain, .type = type, .from = from, .until = until, .scope = index->current_scope};
}
/* Each conjunct of `a and b` holds in what follows it, up to the end of the block it guards. */
static void python_note_condition(cbm_resolve_binding_index_t *index, TSNode condition,
uint32_t until, unsigned depth) {
if (kind_is(condition, "parenthesized_expression") && ts_node_named_child_count(condition) == 1)
condition = ts_node_named_child(condition, 0);
const char *operator= kind_is(condition, "boolean_operator")
? node_text(index, field(condition, "operator"))
: NULL;
if (operator&& !strcmp(operator, "and") && depth < 8) {
python_note_condition(index, field(condition, "left"), until, depth + 1);
python_note_condition(index, field(condition, "right"), until, depth + 1);
return;
}
python_add_narrow(index, condition, ts_node_end_byte(condition), until);
}
static uint32_t python_block_end(TSNode statement);
/* isinstance as if and elif conditions, as `if not isinstance: leave` and as an assert. */
static void python_note_narrowing(cbm_resolve_binding_index_t *index, TSNode node,
const char *kind) {
if (!strcmp(kind, "assert_statement")) {
TSNode test = ts_node_named_child_count(node) ? ts_node_named_child(node, 0) : (TSNode){0};
python_add_narrow(index, test, ts_node_end_byte(node), python_block_end(node));
return;
}
TSNode condition = field(node, "condition"), body = field(node, "consequence");
if (ts_node_is_null(condition) || ts_node_is_null(body))
return;
if (kind_is(condition, "not_operator")) {
/* The other way to reach what follows left the block. */
if (!strcmp(kind, "if_statement") && python_block_always_exits(body, 0) &&
ts_node_is_null(field(node, "alternative")))
python_add_narrow(index, field(condition, "argument"), ts_node_end_byte(node),
python_block_end(node));
return;
}
python_note_condition(index, condition, ts_node_end_byte(body), 0);
}
/* Where the block holding this statement ends. Later statements of a block run only after
* this one. A def or lambda in between means the statement is not in such a block. */
static uint32_t python_block_end(TSNode statement) {
TSNode node = statement;
for (unsigned depth = 0; depth < BINDING_MAX_DEPTH; depth++) {
node = ts_node_parent(node);
if (ts_node_is_null(node) || kind_is(node, "lambda"))
return 0;
if (kind_is(node, "block") || kind_is(node, "module"))
return ts_node_end_byte(node);
}
return 0;
}
/* `with X() as y`: y holds what __enter__ returns, from there to the end of the with
* statement's block. `except E as e`: e holds an E, for that clause only. */
static void python_note_as_target(cbm_resolve_binding_index_t *index, binding_name_t *name,
TSNode identifier) {
TSNode pattern = ts_node_parent(ts_node_parent(identifier));
TSNode holder = ts_node_parent(pattern);
if (!kind_is(pattern, "as_pattern") || !ts_node_named_child_count(pattern))
return;
const char *chain = python_chain(index, ts_node_named_child(pattern, 0), 0);
name->value_from = ts_node_end_byte(pattern);
if (kind_is(holder, "except_clause") || kind_is(holder, "except_group_clause")) {
name->value_chain = chain;
name->value_kind = CBM_PY_ROOT_TYPED;
name->value_until = ts_node_end_byte(holder);
} else if (kind_is(holder, "with_item")) {
TSNode statement = ts_node_parent(ts_node_parent(holder));
bool awaited = kind_is(statement, "with_statement") && ts_node_child_count(statement) &&
kind_is(ts_node_child(statement, 0), "async");
name->value_chain = chain ? cbm_arena_sprintf(index->arena, "%s.%s()", chain,
awaited ? "__aenter__" : "__enter__")
: NULL;
name->value_kind = CBM_PY_ROOT_ASSIGNED;
name->value_until = python_block_end(statement);
}
}
/* One name of a tuple a loop unpacks, when the iterable says what that position holds. */
static void python_note_unpacked_loop_target(cbm_resolve_binding_index_t *index,
binding_name_t *name, TSNode identifier) {
TSNode pattern = ts_node_parent(identifier), loop = ts_node_parent(pattern);
if ((!kind_is(loop, "for_statement") && !kind_is(loop, "for_in_clause")) ||
!ts_node_eq(pattern, field(loop, "left")))
return;
uint32_t position = 0, count = ts_node_named_child_count(pattern);
bool located = false;
TSTreeCursor cursor = ts_tree_cursor_new(pattern);
for (bool more = ts_tree_cursor_goto_first_child(&cursor); more && !located;
more = ts_tree_cursor_goto_next_sibling(&cursor)) {
TSNode child = ts_tree_cursor_current_node(&cursor);
if (ts_node_eq(child, identifier))
located = true;
else if (ts_node_is_named(child))
position++;
}
ts_tree_cursor_delete(&cursor);
TSNode right = field(loop, "right");
TSNode function = field(right, "function"), args = field(right, "arguments");
if (!located || !kind_is(right, "call") || ts_node_is_null(args))
return;
uint32_t supplied = ts_node_named_child_count(args);
const char *value = NULL;
if (kind_is(function, "attribute")) {
/* d.items() against an annotated map: the second is the value. */
const char *member = node_text(index, field(function, "attribute"));
const char *object = python_chain(index, field(function, "object"), 0);
if (member && !strcmp(member, "items") && object && count == 2 && position == 1)
value = cbm_arena_sprintf(index->arena, "%s@values@iter", object);
} else if (kind_is(function, "identifier")) {
const char *callee = node_text(index, function);
const char *source = NULL;
if (callee && !strcmp(callee, "enumerate") && count == 2 && position == 1 && supplied >= 1)
source = python_chain(index, ts_node_named_child(args, 0), 0);
else if (callee && !strcmp(callee, "zip") && supplied == count)
source = python_chain(index, ts_node_named_child(args, position), 0);
/* The name is checked to be the builtin when this is read. */
value = source ? cbm_arena_sprintf(index->arena, "%s(%s)@iter", callee, source) : NULL;
}
if (!value)
return;
name->value_chain = value;
name->value_kind = CBM_PY_ROOT_ASSIGNED;
name->value_from = ts_node_end_byte(right);
TSNode body = kind_is(loop, "for_statement") ? field(loop, "body") : ts_node_parent(loop);
name->value_until = ts_node_is_null(body) ? 0 : ts_node_end_byte(body);
}
/* What the name just recorded was bound to, when that is statically followable.
* Lookup uses it only while this stays the scope's one binder of the name. */
static void python_note_binder_value(cbm_resolve_binding_index_t *index, TSNode identifier,
const char *parent, const char *field_name) {
binding_name_t *name = &index->names[index->name_count - 1];
uint32_t scope = index->current_scope;
name->value_scope = scope;
if (parent && !strcmp(parent, "as_pattern_target")) {
python_note_as_target(index, name, identifier);
return;
}
if (parent &&
(!strcmp(parent, "parameters") || !strcmp(parent, "typed_parameter") ||
!strcmp(parent, "typed_default_parameter") || !strcmp(parent, "default_parameter")))
name->value_until = UINT32_MAX;
/* `def make(self, Request=Request)`: what the signature says it holds unless told
* otherwise, read where the def stands. */
if (parent && !strcmp(parent, "default_parameter") && field_name &&
!strcmp(field_name, "name") && scope) {
name->value_chain = python_chain(index, field(ts_node_parent(identifier), "value"), 0);
name->value_kind = name->value_chain ? CBM_PY_ROOT_ASSIGNED : name->value_kind;
name->value_scope = index->scopes[scope].method_of ? index->scopes[scope].method_of - 1
: index->scopes[scope].parent;
return;
}
/* `for k, v in d.items()`, `for i, x in enumerate(E)`, `for a, b in zip(A, B)`. */
if (parent && !strcmp(parent, "pattern_list")) {
python_note_unpacked_loop_target(index, name, identifier);
return;
}
/* `if (m := Foo()):` */
if (parent && !strcmp(parent, "named_expression")) {
name->value_chain = python_chain(index, field(ts_node_parent(identifier), "value"), 0);
name->value_kind = name->value_chain ? CBM_PY_ROOT_ASSIGNED : name->value_kind;
return;
}
/* `for x in E`: x holds what E yields, for the loop's body. */
if (parent && (!strcmp(parent, "for_statement") || !strcmp(parent, "for_in_clause")) &&
field_name && !strcmp(field_name, "left")) {
TSNode loop = ts_node_parent(identifier), right = field(loop, "right");
const char *chain = python_chain(index, right, 0);
TSNode body = !strcmp(parent, "for_statement") ? field(loop, "body") : ts_node_parent(loop);
name->value_chain = chain ? cbm_arena_sprintf(index->arena, "%s@iter", chain) : NULL;
name->value_kind = name->value_chain ? CBM_PY_ROOT_ASSIGNED : name->value_kind;
name->value_from = ts_node_end_byte(right);
name->value_until = ts_node_is_null(body) ? 0 : ts_node_end_byte(body);
return;
}
if (parent && !strcmp(parent, "assignment") && field_name && !strcmp(field_name, "left")) {
TSNode assignment = ts_node_parent(identifier);
TSNode type = field(assignment, "type"), right = field(assignment, "right");
name->value_from = ts_node_end_byte(assignment);
name->value_until = python_block_end(assignment);
if (!ts_node_is_null(type)) {
name->value_chain = python_annotation_chain(index, type, 0);
name->value_kind = CBM_PY_ROOT_TYPED;
} else if (!ts_node_is_null(right)) {
name->value_chain = python_chain(index, right, 0);
name->value_kind = CBM_PY_ROOT_ASSIGNED;
name->value_none = kind_is(right, "none");
}
if (name->value_chain && scope && index->scopes[scope].class_scope)
name->value_scope = scope;
return;
}
if (scope && parent && !strcmp(parent, "parameters") && index->scopes[scope].self_name &&
!strcmp(index->scopes[scope].self_name, name->name) && index->scopes[scope].method_of) {
name->value_kind = index->scopes[scope].self_is_class ? CBM_PY_ROOT_CLASS_PARAMETER
: CBM_PY_ROOT_SELF_PARAMETER;
return;
}
if (parent &&
(!strcmp(parent, "typed_parameter") || !strcmp(parent, "typed_default_parameter"))) {
TSNode parameter = ts_node_parent(identifier);
if (scope && index->scopes[scope].self_name && index->scopes[scope].method_of &&
!strcmp(index->scopes[scope].self_name, name->name)) {
name->value_kind = index->scopes[scope].self_is_class ? CBM_PY_ROOT_CLASS_PARAMETER
: CBM_PY_ROOT_SELF_PARAMETER;
return;
}
name->value_chain = python_annotation_chain(index, field(parameter, "type"), 0);
name->value_kind = CBM_PY_ROOT_TYPED;
/* Annotations evaluate in the namespace that declares the function. */
name->value_scope = index->scopes[scope].method_of ? index->scopes[scope].method_of - 1
: index->scopes[scope].parent;
}
}
static cbm_resolve_python_member_t *python_add_member(cbm_resolve_binding_index_t *index,
uint32_t owner, const char *name,
cbm_resolve_python_member_kind_t kind,
uint32_t at) {
cbm_resolve_python_member_t *members =
grow_array(index, index->python_members, index->python_member_count,
&index->python_member_capacity, sizeof(*members));
if (!members || !name)
return NULL;
index->python_members = members;
const char *qualified = index->scopes[owner].qualified_name;
cbm_resolve_python_member_t *entry = &members[index->python_member_count++];
*entry = (cbm_resolve_python_member_t){
.owner_scope = owner,
.owner = qualified,
.name = name,
.target = cbm_arena_sprintf(index->arena, "%s.%s", qualified, name),
.kind = kind,
.at = at};
return entry->target ? entry : NULL;
}
static bool python_constant_value(TSNode value) {
return kind_is(value, "integer") || kind_is(value, "float") || kind_is(value, "string") ||
kind_is(value, "concatenated_string") || kind_is(value, "true") ||
kind_is(value, "false") || kind_is(value, "none") || kind_is(value, "list") ||
kind_is(value, "dictionary") || kind_is(value, "tuple") || kind_is(value, "set");
}
/* This body can only hand back a constant or nothing: a decorator like that discards the
* function. A stub says nothing, and neither does a body that only raises. The walk stays
* out of nested scopes, so across a file each node is visited once per enclosing def. */
static bool python_body_discards(TSNode body) {
bool returned = false;
TSTreeCursor cursor = ts_tree_cursor_new(body);
bool discards = true, more = ts_tree_cursor_goto_first_child(&cursor);
unsigned depth = 1;
while (more) {
TSNode node = ts_tree_cursor_current_node(&cursor);
bool nested = kind_is(node, "function_definition") || kind_is(node, "class_definition") ||
kind_is(node, "decorated_definition") || kind_is(node, "lambda");
if (kind_is(node, "yield")) {
/* A generator returns a generator, whatever its return says. */
ts_tree_cursor_delete(&cursor);
return true;
}
if (kind_is(node, "return_statement")) {
returned = true;
if (ts_node_named_child_count(node) &&
!python_constant_value(ts_node_named_child(node, 0))) {
discards = false;
break;
}
}
if (!nested && !kind_is(node, "return_statement") &&
ts_tree_cursor_goto_first_child(&cursor)) {
depth++;
continue;
}
while (!ts_tree_cursor_goto_next_sibling(&cursor)) {
if (--depth == 0 || !ts_tree_cursor_goto_parent(&cursor)) {
more = false;
break;
}
}
}
ts_tree_cursor_delete(&cursor);
if (!discards || returned)
return discards;
if (python_block_always_exits(body, 0))
return false;
uint32_t count = ts_node_named_child_count(body);
TSNode last = count ? ts_node_named_child(body, count - 1) : (TSNode){0};
return !(kind_is(last, "expression_statement") && ts_node_named_child_count(last) &&
kind_is(ts_node_named_child(last, 0), "ellipsis"));
}
/* The name of a def's first parameter, when it has one a body could spell. */
static const char *python_first_parameter(cbm_resolve_binding_index_t *index, TSNode function) {
TSNode parameters = field(function, "parameters");
if (ts_node_is_null(parameters) || !ts_node_named_child_count(parameters))
return NULL;
TSNode first = ts_node_named_child(parameters, 0);
if (kind_is(first, "typed_parameter") && ts_node_named_child_count(first))
first = ts_node_named_child(first, 0);
else if (kind_is(first, "default_parameter") || kind_is(first, "typed_default_parameter"))
first = field(first, "name");
return kind_is(first, "identifier") ? node_text(index, first) : NULL;
}
static bool python_spells(const cbm_resolve_binding_index_t *index, TSNode node, const char *word) {
uint32_t start = ts_node_start_byte(node), end = ts_node_end_byte(node);
size_t length = strlen(word);
return end - start == length && end <= index->source_len &&
!memcmp(index->source + start, word, length);
}
/* `def replace(function): return other`. A body that returns a value and never spells its
* parameter cannot have built that value from the function. locals() and the like could reach
* it by another name. Stops at the first mention, and only top-level and nested defs get here,
* so across a file each node is visited once per enclosing def. */
static bool python_body_replaces(cbm_resolve_binding_index_t *index, TSNode function) {
const char *parameter = python_first_parameter(index, function);
TSNode body = field(function, "body");
if (!parameter || ts_node_is_null(body))
return false;
bool returns = false, mentioned = false;
TSTreeCursor cursor = ts_tree_cursor_new(body);
bool more = ts_tree_cursor_goto_first_child(&cursor);
unsigned depth = 1, nested = 0;
while (more && !mentioned) {
TSNode node = ts_tree_cursor_current_node(&cursor);
if (kind_is(node, "identifier"))
mentioned = python_spells(index, node, parameter) ||
python_spells(index, node, "locals") ||
python_spells(index, node, "vars") || python_spells(index, node, "eval") ||
python_spells(index, node, "exec");
returns = returns ||
(!nested && kind_is(node, "return_statement") && ts_node_named_child_count(node));
if (ts_tree_cursor_goto_first_child(&cursor)) {
depth++;
nested += kind_is(node, "function_definition") || kind_is(node, "lambda");
continue;
}
while (!ts_tree_cursor_goto_next_sibling(&cursor)) {
if (--depth == 0 || !ts_tree_cursor_goto_parent(&cursor)) {
more = false;
break;
}
TSNode up = ts_tree_cursor_current_node(&cursor);
nested -= nested && (kind_is(up, "function_definition") || kind_is(up, "lambda"));
}
}
ts_tree_cursor_delete(&cursor);
return returns && !mentioned;
}
static void python_note_member_shape(cbm_resolve_binding_index_t *index,
cbm_resolve_python_member_t *member, TSNode function) {
member->discards = python_body_discards(field(function, "body"));
member->replaces = !member->discards && python_body_replaces(index, function);
}
/* A def directly in a class body. Its decorators are classified with the file's
* imports once they are all known. */
static void python_note_method(cbm_resolve_binding_index_t *index, TSNode node, const char *parent,
uint32_t owner, bool conditional) {
cbm_resolve_python_member_t *member = python_add_member(
index, owner, node_text(index, field(node, "name")),
conditional ? CBM_PY_MEMBER_OPAQUE : CBM_PY_MEMBER_FUNCTION, ts_node_start_byte(node));
if (!member) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "python_member_allocation");
return;
}
member->return_chain = python_annotation_chain(index, field(node, "return_type"), 0);
member->chain_scope = owner;
if (!index->scopes[owner].class_scope)
python_note_member_shape(index, member, node);
if (!parent || strcmp(parent, "decorated_definition"))
return;
TSNode decorated = ts_node_parent(node);
uint32_t count = ts_node_named_child_count(decorated);
const char **chains = cbm_arena_alloc(index->arena, (count + 1) * sizeof(*chains));
if (!chains) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "python_member_allocation");
return;
}
uint32_t used = 0;
for (uint32_t i = 0; i < count; i++) {
TSNode child = ts_node_named_child(decorated, i);
if (!kind_is(child, "decorator"))
continue;
const char *chain = python_decorator_chain(index, child);
chains[used++] = chain ? chain : "?";
}
chains[used] = NULL;
member->decorators = chains;
}
/* A function outside a class matters to callers only through its annotated
* return, or as a decorator its source proves. */
static void python_note_function(cbm_resolve_binding_index_t *index, TSNode node, uint32_t owner) {
const char *return_chain = python_annotation_chain(index, field(node, "return_type"), 0);
cbm_resolve_python_member_t *member =
python_add_member(index, owner, node_text(index, field(node, "name")),
CBM_PY_MEMBER_FUNCTION, ts_node_start_byte(node));
if (!member) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "python_member_allocation");
return;
}
member->return_chain = return_chain;
member->chain_scope = owner;
python_note_member_shape(index, member, node);
}
/* `go = classmethod(go)` is the decorator spelled out: the callee, when this is that shape. */
static const char *python_spelled_decorator(cbm_resolve_binding_index_t *index, TSNode target,
const char *name) {
TSNode assignment = ts_node_parent(target);
if (!kind_is(assignment, "assignment"))
return NULL;
TSNode right = field(assignment, "right");
TSNode function = field(right, "function"), args = field(right, "arguments");
if (!kind_is(right, "call") || !kind_is(function, "identifier") || ts_node_is_null(args) ||
ts_node_named_child_count(args) != 1)
return NULL;
TSNode only = ts_node_named_child(args, 0);
const char *callee = node_text(index, function);
return kind_is(only, "identifier") && python_spells(index, only, name) && callee &&
(!strcmp(callee, "classmethod") || !strcmp(callee, "staticmethod"))
? callee
: NULL;
}
/* Class-level assignment or declaration. No graph node carries it, but it stops
* lookup: a base's method of the same name is not what this class exposes. */
static void python_note_class_value(cbm_resolve_binding_index_t *index, const char *name, TSNode at,
const char *chain, bool typed, bool none, uint32_t owner) {
cbm_resolve_python_member_t *member =
python_add_member(index, owner, name, CBM_PY_MEMBER_VALUE, ts_node_start_byte(at));
if (!member) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "python_member_allocation");
return;
}
if (typed)
member->type_chain = chain;
else
member->value_chain = chain;
member->none_value = none;
member->spelled_decorator = python_spelled_decorator(index, at, name);
/* `ResultConsumer = ResultConsumer` in a class body reads the name from outside it: the
* binding does not exist until the statement has run. */
size_t root = chain ? strcspn(chain, ".(") : 0;
bool self_named = root && strlen(name) == root && !strncmp(chain, name, root);
member->chain_scope = self_named ? index->scopes[owner].parent : owner;
}
/* The method whose first parameter a name in this scope chain denotes. */
static uint32_t python_enclosing_method(const cbm_resolve_binding_index_t *index, uint32_t scope) {
for (unsigned depth = 0; scope && scope < index->scope_count && depth < BINDING_MAX_DEPTH;
depth++) {
if (index->scopes[scope].method_of)
return scope;
scope = index->scopes[scope].parent;
}
return 0;
}
/* `self.x = value`, `self.x: T = value`: an instance attribute of the class the
* method belongs to. Whatever else stores into it leaves its type unknown. */
static void python_note_instance_write(cbm_resolve_binding_index_t *index, TSNode attribute,
const char *parent, const char *field_name) {
TSNode object = field(attribute, "object");
if (!kind_is(object, "identifier"))
return;
uint32_t method = python_enclosing_method(index, index->current_scope);
if (!method)
return;
const char *receiver = node_text(index, object);
const char *self = index->scopes[method].self_name;
if (!receiver || !self || strcmp(receiver, self))
return;
uint32_t owner = index->scopes[method].method_of - 1;
cbm_resolve_python_member_t *member =
python_add_member(index, owner, node_text(index, field(attribute, "attribute")),
CBM_PY_MEMBER_INSTANCE, ts_node_start_byte(attribute));
if (!member) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "python_member_allocation");
return;
}
member->chain_scope = index->current_scope;
bool direct =
parent && !strcmp(parent, "assignment") && field_name && !strcmp(field_name, "left");
if (!direct)
return;
TSNode assignment = ts_node_parent(attribute), type = field(assignment, "type"),
right = field(assignment, "right");
if (!ts_node_is_null(type)) {
member->type_chain = python_annotation_chain(index, type, 0);
return;
}
if (kind_is(right, "none"))
member->none_value = true;
else
member->value_chain = python_chain(index, right, 0);
}
static void python_note_class(cbm_resolve_binding_index_t *index, TSNode node, uint32_t scope,
uint32_t header_scope) {
cbm_resolve_python_class_t *classes =
grow_array(index, index->python_classes, index->python_class_count,
&index->python_class_capacity, sizeof(*classes));
if (!classes) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "python_class_allocation");
return;
}
index->python_classes = classes;
cbm_resolve_python_class_t *entry = &classes[index->python_class_count++];
*entry = (cbm_resolve_python_class_t){.qualified_name = index->scopes[scope].qualified_name,
.scope = scope,
.header_scope = header_scope,
.at = ts_node_start_byte(node)};
TSNode bases = field(node, "superclasses");
uint32_t count = ts_node_is_null(bases) ? 0 : ts_node_named_child_count(bases);
entry->bases = count ? cbm_arena_alloc(index->arena, count * sizeof(*entry->bases)) : NULL;
if (count && !entry->bases) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "python_class_allocation");
return;
}
for (uint32_t i = 0; i < count; i++) {
TSNode base = ts_node_named_child(bases, i);
if (kind_is(base, "keyword_argument")) {
continue;
}
if (kind_is(base, "comment"))
continue;
/* Generic[T] and Base[int] inherit the subscripted class. */
bool subscripted = kind_is(base, "subscript");
if (subscripted)
base = field(base, "value");
const char *chain = python_chain(index, base, 0);
if (!chain || (subscripted && entry->base_count >= 64)) {
entry->unknown_base = true;
continue;
}
if (subscripted)
entry->subscripted |= UINT64_C(1) << entry->base_count;
/* These replace what the class body assigned: an Enum member reads as the enum. */
const char *canonical = python_import_canonical(index, chain);
entry->metaclass |= canonical && (!strncmp(canonical, "enum.", 5) ||
!strcmp(canonical, "typing.NamedTuple"));
entry->bases[entry->base_count++] = chain;
}
}
static const binding_pending_declaration_t *python_pending_declaration(
cbm_resolve_binding_index_t *index, TSNode node, uint32_t scope,
const binding_pending_declaration_t *parent) {
if (kind_is(node, "lambda"))
return parent;
const char *name = node_text(index, field(node, "name"));
binding_pending_declaration_t *pending = cbm_arena_alloc(index->arena, sizeof(*pending));
if (!name || !pending) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "python_declaration_allocation");
return parent;
}
*pending = (binding_pending_declaration_t){name, scope, parent};
return pending;
}
/* A def in a class body receives its instance or class first. staticmethod
* receives neither; __new__ and friends are implicitly class-level. */
static void python_note_method_scope(cbm_resolve_binding_index_t *index, TSNode node, uint32_t id,
uint32_t owner) {
binding_scope_t *scope = &index->scopes[id];
TSNode decorated = ts_node_parent(node);
bool is_class = false;
if (kind_is(decorated, "decorated_definition")) {
for (uint32_t i = 0; i < ts_node_named_child_count(decorated); i++) {
TSNode child = ts_node_named_child(decorated, i);
const char *chain =
kind_is(child, "decorator") ? python_decorator_chain(index, child) : NULL;
if (chain && !strcmp(chain, "staticmethod"))
return;
if (chain && !strcmp(chain, "classmethod"))
is_class = true;
}
}
const char *name = node_text(index, field(node, "name"));
if (name && (!strcmp(name, "__new__") || !strcmp(name, "__init_subclass__") ||
!strcmp(name, "__class_getitem__")))
is_class = true;
TSNode parameters = field(node, "parameters");
TSNode first = ts_node_is_null(parameters) || !ts_node_named_child_count(parameters)
? (TSNode){0}
: ts_node_named_child(parameters, 0);
if (kind_is(first, "typed_parameter") && ts_node_named_child_count(first))
first = ts_node_named_child(first, 0);
if (!kind_is(first, "identifier"))
return;
scope->method_of = owner + 1;
scope->self_name = node_text(index, first);
scope->self_is_class = is_class;
}
static uint32_t python_child_scope(cbm_resolve_binding_index_t *index, TSNode node, uint32_t parent,
bool class_scope) {
const char *name = kind_is(node, "lambda") ? cbm_arena_sprintf(index->arena, "<lambda@%u>",
ts_node_start_byte(node))
: node_text(index, field(node, "name"));
const char *owner = parent < index->scope_count ? index->scopes[parent].qualified_name
: index->result->module_qn;
const char *qn = name ? cbm_arena_sprintf(index->arena, "%s.%s", owner, name) : NULL;
binding_scope_t *scopes = grow_array(index, index->scopes, index->scope_count,
&index->scope_capacity, sizeof(*scopes));
if (!scopes || !qn) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "python_scope_allocation");
return parent;
}
index->scopes = scopes;
uint32_t declaring = parent;
/* A method closes over enclosing functions, never its class dictionary.
* The qualified owner above still retains that class in graph identity. */
if (!class_scope)
while (parent && scopes[parent].class_scope)
parent = scopes[parent].parent;
uint32_t id = (uint32_t)index->scope_count++;
scopes[id] =
(binding_scope_t){.qualified_name = qn, .parent = parent, .class_scope = class_scope};
if (kind_is(node, "function_definition") && scopes[declaring].class_scope && declaring)
python_note_method_scope(index, node, id, declaring);
return id;
}
static bool javascript_language(CBMLanguage language) {
return language == CBM_LANG_JAVASCRIPT || language == CBM_LANG_TYPESCRIPT ||
language == CBM_LANG_TSX || language == CBM_LANG_ARKTS;
}
static bool supported_javascript_node(const char *kind) {
static const char *const syntax[] = {
"program", "function_declaration", "generator_function_declaration", "function_expression",
"generator_function", "arrow_function", "formal_parameters", "identifier",
"statement_block", "expression_statement", "return_statement", "call_expression",
"arguments", "number", "string", "string_fragment", "escape_sequence", "comment",
"member_expression", "property_identifier", "binary_expression", "unary_expression",
"ternary_expression", "true", "false", "null", "undefined", "parenthesized_expression",
"array", "object", "pair", "shorthand_property_identifier", "subscript_expression",
"if_statement", "else_clause", "empty_statement", "break_statement", "continue_statement",
"while_statement", "do_statement", "for_statement", "for_in_statement", "throw_statement",
"try_statement", "catch_clause", "finally_clause", "switch_statement", "switch_body",
"switch_case", "switch_default", "await_expression", "yield_expression",
"sequence_expression", "lexical_declaration", "variable_declaration", "variable_declarator",
"assignment_expression", "augmented_assignment_expression", "update_expression",
"assignment_pattern", "array_pattern", "object_pattern", "pair_pattern", "rest_pattern",
"shorthand_property_identifier_pattern", "object_assignment_pattern", "spread_element",
"required_parameter", "optional_parameter", "type_annotation", "predefined_type",
"type_identifier", "generic_type", "type_arguments", "nested_type_identifier", "array_type",
"union_type", "intersection_type", "parenthesized_type", "literal_type", "tuple_type",
"optional_type", "rest_type", "function_type", "type_query", "keyof_type", "lookup_type",
"conditional_type", "infer_type", "type_predicate", "type_predicate_annotation",
"asserts_annotation", "asserts", "type_parameters", "type_parameter", "constraint",
"default_type", "object_type", "property_signature", "method_signature", "call_signature",
"construct_signature", "index_signature", "mapped_type_clause", "index_type_query",
"readonly_type", "template_literal_type", "template_string", "template_substitution",
"regex", "regex_pattern", "regex_flags", "non_null_expression", "as_expression",
"satisfies_expression", "type_assertion", "new_expression", "this", "super",
"class_declaration", "abstract_class_declaration", "class", "class_body", "class_heritage",
"extends_clause", "implements_clause", "method_definition", "public_field_definition",
"field_definition", "private_property_identifier", "accessibility_modifier", "static_block",
"interface_declaration", "extends_type_clause", "interface_body", "type_alias_declaration",
"enum_declaration", "enum_body", "enum_assignment", "computed_property_name",
"import_statement", "import_clause", "named_imports", "import_specifier",
"namespace_import", "export_statement", "export_clause", "export_specifier",
"namespace_export", "meta_property", "jsx_element", "jsx_opening_element",
"jsx_closing_element", "jsx_self_closing_element", "jsx_attribute", "jsx_expression",
"jsx_text", "jsx_namespace_name", "jsx_fragment",
/* Tokens and statements that bind nothing. */
"optional_chain", "hash_bang_line", "html_comment", "html_character_reference",
"debugger_statement", "labeled_statement", "statement_identifier", "import",
"import_attribute", "instantiation_expression", "decorator", "class_static_block",
"using_declaration",
/* Type-level forms have no runtime binder. */
"function_signature", "abstract_method_signature", "adding_type_annotation",
"opting_type_annotation", "omitting_type_annotation", "constructor_type",
"existential_type", "flow_maybe_type", "template_type", "this_type", "override_modifier",
/* Scopes and module forms with an explicit rule below. */
"ambient_declaration", "internal_module", "module", "nested_identifier", "import_alias",
"import_require_clause", NULL};
return listed(kind, syntax);
}
static bool child_token(TSNode node, const char *token) {
uint32_t count = ts_node_child_count(node);
for (uint32_t i = 0; i < count; i++) {
if (kind_is(ts_node_child(node, i), token)) {
return true;
}
}
return false;
}
static bool javascript_function_kind(const char *kind) {
return strcmp(kind, "function_declaration") == 0 ||
strcmp(kind, "generator_function_declaration") == 0;
}
static unsigned javascript_declaration_kind(const char *kind) {
if (javascript_function_kind(kind)) {
return BINDING_DECLARATION;
}
if (strcmp(kind, "class_declaration") == 0 || strcmp(kind, "abstract_class_declaration") == 0 ||
strcmp(kind, "enum_declaration") == 0) {
return BINDING_TYPE_DECLARATION | BINDING_VALUE_TYPE;
}
if (strcmp(kind, "interface_declaration") == 0 || strcmp(kind, "type_alias_declaration") == 0) {
return BINDING_TYPE_DECLARATION;
}
return 0;
}
static const char *http_module_literal(cbm_resolve_binding_index_t *index, TSNode node) {
const char *text = kind_is(node, "string") ? node_text(index, node) : NULL;
size_t length = text ? strlen(text) : 0;
return length >= 2 && !strchr(text, '\\')
? cbm_arena_strndup(index->arena, text + 1, length - 2)
: NULL;
}
static void http_dependency(cbm_resolve_binding_index_t *index, const char *path, const char *local,
bool side_effect) {
if (!path || !*path) {
index->http_domain_unknown = true;
return;
}
/* These packages expose loaders/evaluators whose aliases need a separate
* capability analysis. They can reach any client in this repository. */
if (!strcmp(path, "module") || !strcmp(path, "node:module") || !strcmp(path, "vm") ||
!strcmp(path, "node:vm"))
index->http_domain_unknown = true;
cbm_resolve_http_dependency_t *items =
grow_array(index, index->http_dependencies, index->http_dependency_count,
&index->http_dependency_capacity, sizeof(*items));
if (!items)
return;
index->http_dependencies = items;
items[index->http_dependency_count++] =
(cbm_resolve_http_dependency_t){path, local, side_effect};
}
static void note_http_dependency(cbm_resolve_binding_index_t *index, TSNode node) {
/* Computed/reflection access can hide loader and evaluator aliases.
* Until that capability flow is audited, the repository domain is open. */
if (kind_is(node, "subscript_expression"))
index->http_domain_unknown = true;
if (kind_is(node, "identifier") || kind_is(node, "property_identifier")) {
const char *capability = node_text(index, node);
if (capability &&
(!strcmp(capability, "globalThis") || !strcmp(capability, "global") ||
!strcmp(capability, "window") || !strcmp(capability, "self") ||
!strcmp(capability, "Reflect") || !strcmp(capability, "eval") ||
!strcmp(capability, "Function") || !strcmp(capability, "mainModule") ||
!strcmp(capability, "setTimeout") || !strcmp(capability, "setInterval") ||
(!strcmp(capability, "constructor") &&
kind_is(ts_node_parent(node), "member_expression"))))
index->http_domain_unknown = true;
}
if (kind_is(node, "import_statement") || kind_is(node, "export_statement")) {
TSNode source = field(node, "source");
if (ts_node_is_null(source) || child_token(node, "type"))
return;
const char *local = NULL;
bool side_effect = kind_is(node, "import_statement");
uint32_t count = ts_node_named_child_count(node);
for (uint32_t i = 0; i < count; i++) {
TSNode clause = ts_node_named_child(node, i);
if (!kind_is(clause, "import_clause"))
continue;
side_effect = false;
if (ts_node_named_child_count(clause) == 1 &&
kind_is(ts_node_named_child(clause, 0), "identifier"))
local = node_text(index, ts_node_named_child(clause, 0));
}
http_dependency(index, http_module_literal(index, source), local, side_effect);
}
if (kind_is(node, "call_expression")) {
TSNode function = field(node, "function");
const char *text = node_text(index, function);
bool loader = kind_is(function, "import") ||
(kind_is(function, "identifier") && text && !strcmp(text, "require"));
if (loader) {
TSNode args = field(node, "arguments");
const char *path = !ts_node_is_null(args) && ts_node_named_child_count(args) == 1
? http_module_literal(index, ts_node_named_child(args, 0))
: NULL;
http_dependency(index, path, NULL, false);
}
}
if (kind_is(node, "identifier") || kind_is(node, "property_identifier")) {
const char *text = node_text(index, node);
if (!text || strcmp(text, "require"))
return;
TSNode parent = ts_node_parent(node);
if (!kind_is(node, "identifier") || !kind_is(parent, "call_expression") ||
!ts_node_eq(node, field(parent, "function")))
index->http_domain_unknown = true;
}
}
/* The export name carrying a CommonJS whole-module value. It cannot collide
* with a source export name: those are identifiers or string literals that the
* lookup never spells with this exact dotted form. */
static const char JS_WHOLE_EXPORT_NAME[] = "module.exports";
static js_raw_export_t *js_raw_export(cbm_resolve_binding_index_t *index) {
js_raw_export_t *items = grow_array(index, index->js_raw_exports, index->js_raw_export_count,
&index->js_raw_export_capacity, sizeof(*items));
if (!items)
return NULL;
index->js_raw_exports = items;
js_raw_export_t *raw = &items[index->js_raw_export_count++];
*raw = (js_raw_export_t){.kind = CBM_RESOLVE_EXPORT_VALUE | CBM_RESOLVE_EXPORT_TYPE};
return raw;
}
static void js_write(cbm_resolve_binding_index_t *index, const char *name, uint32_t scope) {
js_write_t *items = grow_array(index, index->js_writes, index->js_write_count,
&index->js_write_capacity, sizeof(*items));
if (!items || !name)
return;
index->js_writes = items;
items[index->js_write_count++] = (js_write_t){name, scope};
}
static void js_export_alias(cbm_resolve_binding_index_t *index, const char *name) {
const char **items = grow_array(index, index->js_export_aliases, index->js_export_alias_count,
&index->js_export_alias_capacity, sizeof(*items));
if (!items || !name)
return;
index->js_export_aliases = items;
items[index->js_export_alias_count++] = name;
}
static const cbm_resolve_js_import_t *js_import_fact(cbm_resolve_binding_index_t *index,
cbm_resolve_js_import_t fact) {
cbm_resolve_js_import_t *copy = cbm_arena_alloc(index->arena, sizeof(*copy));
if (!copy) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "binding_import_allocation");
return NULL;
}
*copy = fact;
return copy;
}
static void note_javascript_import(cbm_resolve_binding_index_t *index, TSNode node,
const char *parent_kind, bool type_only) {
TSNode name = {0};
TSNode member = {0};
TSNode statement = ts_node_parent(node);
bool is_default = false;
bool is_require = false;
if (kind_is(node, "import_specifier")) {
name = field(node, "alias");
member = field(node, "name");
if (ts_node_is_null(name)) {
name = member;
}
type_only |= child_token(node, "type");
} else if (kind_is(node, "namespace_import")) {
name = ts_node_named_child(node, 0);
} else if (kind_is(node, "identifier") && parent_kind &&
strcmp(parent_kind, "import_clause") == 0) {
name = node;
is_default = true;
} else if (kind_is(node, "import_require_clause")) {
name = ts_node_named_child(node, 0);
is_require = true;
statement = node;
}
if (ts_node_is_null(name))
return;
if (!kind_is(name, "identifier")) {
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED, "unsupported_import_binding");
return;
}
if (!is_require)
while (!ts_node_is_null(statement) && !kind_is(statement, "import_statement"))
statement = ts_node_parent(statement);
const char *local = node_text(index, name);
size_t before = index->name_count;
add_name(index, local, 0, type_only ? BINDING_BLOCKED | BINDING_TYPE_IMPORT : BINDING_IMPORT);
if (index->name_count == before)
return;
const char *member_name = ts_node_is_null(member) ? NULL : node_text(index, member);
if (member_name && !strcmp(member_name, "default")) {
member_name = NULL;
is_default = true;
}
index->names[index->name_count - 1].js_import = js_import_fact(
index,
(cbm_resolve_js_import_t){.local = local,
.source = http_module_literal(index, field(statement, "source")),
.member = member_name,
.is_default = is_default,
.is_require = is_require,
.type_only = type_only});
/* Retain the actual default import's module literal for the HTTP
* domain. A mutable extraction carrier cannot replace that spelling. */
if (!type_only && kind_is(node, "identifier") && parent_kind &&
!strcmp(parent_kind, "import_clause") && index->analysis == CBM_RESOLVE_ANALYSIS_COMPLETE) {
TSNode source = field(statement, "source");
const char *text = kind_is(statement, "import_statement") && kind_is(source, "string")
? node_text(index, source)
: NULL;
size_t length = text ? strlen(text) : 0;
if (length >= 2 && !strchr(text, '\\'))
index->names[index->name_count - 1].http_import_path =
cbm_arena_strndup(index->arena, text + 1, length - 2);
}
}
static TSNode js_unwrap_value(TSNode value) {
for (unsigned depth = 0; depth < BINDING_MAX_DEPTH && !ts_node_is_null(value); depth++) {
if (kind_is(value, "assignment_expression"))
value = field(value, "right");
else if (kind_is(value, "parenthesized_expression") &&
ts_node_named_child_count(value) == 1)
value = ts_node_named_child(value, 0);
else
break;
}
return value;
}
static bool js_identifier_is(cbm_resolve_binding_index_t *index, TSNode node, const char *name) {
if (!kind_is(node, "identifier"))
return false;
size_t length = strlen(name);
uint32_t start = ts_node_start_byte(node), end = ts_node_end_byte(node);
return end - start == length && end <= index->source_len &&
!memcmp(index->source + start, name, length);
}
static bool js_is_module_exports(cbm_resolve_binding_index_t *index, TSNode node) {
if (!kind_is(node, "member_expression") ||
!js_identifier_is(index, field(node, "object"), "module"))
return false;
TSNode property = field(node, "property");
uint32_t start = ts_node_start_byte(property), end = ts_node_end_byte(property);
return kind_is(property, "property_identifier") && end - start == 7 &&
!memcmp(index->source + start, "exports", 7);
}
static bool js_require_call(cbm_resolve_binding_index_t *index, TSNode call, const char **source) {
if (!kind_is(call, "call_expression") ||
!js_identifier_is(index, field(call, "function"), "require"))
return false;
TSNode args = field(call, "arguments");
if (ts_node_is_null(args) || ts_node_named_child_count(args) != 1)
return false;
TSNode literal = ts_node_named_child(args, 0);
if (!kind_is(literal, "string"))
return false;
*source = http_module_literal(index, literal);
return *source != NULL;
}
/* `require("m")` or `require("m").member`. */
static bool js_require_value(cbm_resolve_binding_index_t *index, TSNode value, const char **source,
const char **member) {
value = js_unwrap_value(value);
*member = NULL;
if (js_require_call(index, value, source))
return true;
if (!kind_is(value, "member_expression"))
return false;
TSNode property = field(value, "property");
if (!kind_is(property, "property_identifier") ||
!js_require_call(index, field(value, "object"), source))
return false;
*member = node_text(index, property);
return *member != NULL;
}
static bool js_chain_writes_module_exports(cbm_resolve_binding_index_t *index, TSNode value) {
for (unsigned depth = 0; depth < BINDING_MAX_DEPTH && !ts_node_is_null(value); depth++) {
if (kind_is(value, "parenthesized_expression") && ts_node_named_child_count(value) == 1) {
value = ts_node_named_child(value, 0);
continue;
}
if (!kind_is(value, "assignment_expression"))
return false;
if (js_is_module_exports(index, field(value, "left")))
return true;
value = field(value, "right");
}
return false;
}
static bool js_function_value(TSNode value) {
return kind_is(value, "arrow_function") || kind_is(value, "function_expression") ||
kind_is(value, "generator_function");
}
/* Classify one exported value. A function needs an extracted definition at
* these exact lines; nothing else is taken on trust from its spelling. */
static void js_fill_value(cbm_resolve_binding_index_t *index, js_raw_export_t *raw, TSNode value,
const char *fallback_name) {
value = js_unwrap_value(value);
const char *source = NULL, *member = NULL;
if (kind_is(value, "identifier")) {
raw->value = JS_VALUE_IDENTIFIER;
raw->local = node_text(index, value);
} else if (kind_is(value, "class") && !ts_node_is_null(field(value, "name"))) {
raw->value = JS_VALUE_FUNCTION;
raw->function_name = node_text(index, field(value, "name"));
raw->start_line = ts_node_start_point(value).row + 1;
raw->end_line = ts_node_end_point(value).row + 1;
} else if (js_function_value(value) || kind_is(value, "method_definition")) {
TSNode name = field(value, "name");
raw->value = JS_VALUE_FUNCTION;
raw->function_name = kind_is(value, "method_definition") ? node_text(index, name)
: !ts_node_is_null(name) ? node_text(index, name)
: fallback_name;
raw->start_line = ts_node_start_point(value).row + 1;
raw->end_line = ts_node_end_point(value).row + 1;
} else if (js_require_value(index, value, &source, &member)) {
raw->value = JS_VALUE_REQUIRE;
raw->source = source;
raw->member = member;
} else if (kind_is(value, "object")) {
raw->value = JS_VALUE_OBJECT;
}
}
static void js_export_object_members(cbm_resolve_binding_index_t *index, TSNode object) {
uint32_t count = ts_node_named_child_count(object);
for (uint32_t i = 0; i < count; i++) {
TSNode item = ts_node_named_child(object, i);
if (kind_is(item, "comment"))
continue;
if (kind_is(item, "shorthand_property_identifier")) {
const char *name = node_text(index, item);
js_raw_export_t *raw = js_raw_export(index);
if (raw) {
raw->export_name = name;
raw->value = JS_VALUE_IDENTIFIER;
raw->local = name;
}
} 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")) {
index->js_exports_open = true;
continue;
}
const char *name = node_text(index, key);
js_raw_export_t *raw = js_raw_export(index);
if (raw) {
raw->export_name = name;
js_fill_value(index, raw, kind_is(item, "pair") ? field(item, "value") : item,
name);
/* A nested object has no declaration of its own. */
if (raw->value == JS_VALUE_OBJECT)
raw->value = JS_VALUE_UNKNOWN;
}
} else {
index->js_exports_open = true; /* spread or computed member */
}
}
}
static js_member_t *js_member_row(cbm_resolve_binding_index_t *index, const char *owner,
const char *member, bool prototype) {
js_member_t *items = grow_array(index, index->js_members, index->js_member_count,
&index->js_member_capacity, sizeof(*items));
if (!items || !owner || !member)
return NULL;
index->js_members = items;
js_member_t *row = &items[index->js_member_count++];
*row = (js_member_t){.owner = owner, .member = member, .prototype = prototype};
return row;
}
/* The function or identifier a member is given. Anything else is not a declaration
* the member could name, so the row stays a known conflict for that name. */
static void js_member_value(cbm_resolve_binding_index_t *index, js_member_t *row, TSNode value,
const char *fallback_name) {
value = js_unwrap_value(value);
if (kind_is(value, "identifier")) {
row->local = node_text(index, value);
} else if (js_function_value(value) || kind_is(value, "method_definition")) {
TSNode name = field(value, "name");
row->function_name = !ts_node_is_null(name) ? node_text(index, name) : fallback_name;
row->start_line = ts_node_start_point(value).row + 1;
row->end_line = ts_node_end_point(value).row + 1;
} else {
row->conflict = true;
}
}
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, "shorthand_property_identifier")) {
member = node_text(index, item);
row = js_member_row(index, owner, member, prototype);
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"))
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 && (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,
member);
}
}
}
/* `this` inside `Owner.member = function` and friends. An arrow keeps the `this`
* around it; any other function starts over unless a member assignment names it. */
static void javascript_this_owner(cbm_resolve_binding_index_t *index, TSNode node,
binding_walk_t *state) {
if (kind_is(node, "arrow_function"))
return;
state->js_this_owner = NULL;
state->js_this_prototype = false;
/* Owners are later matched by name at module level, so a nested or parameter
* `Owner` must not reach them. */
if (state->scope != 0)
return;
TSNode parent = ts_node_parent(node);
TSNode object = (TSNode){0};
if (kind_is(node, "function_declaration")) {
/* A constructor function's `this` is its instance. */
state->js_this_owner = node_text(index, field(node, "name"));
state->js_this_prototype = true;
return;
}
while (kind_is(parent, "assignment_expression")) {
object = field(parent, "left");
parent = ts_node_parent(parent);
}
if (kind_is(object, "member_expression")) {
TSNode owner = field(object, "object");
if (kind_is(owner, "identifier")) {
state->js_this_owner = node_text(index, owner);
return;
}
if (kind_is(owner, "member_expression") && kind_is(field(owner, "object"), "identifier") &&
kind_is(field(owner, "property"), "property_identifier")) {
const char *property = node_text(index, field(owner, "property"));
if (property && !strcmp(property, "prototype")) {
state->js_this_owner = node_text(index, field(owner, "object"));
state->js_this_prototype = true;
}
}
return;
}
/* A member of `const Owner = {...}` or `Owner.prototype = {...}`. */
TSNode literal =
kind_is(node, "method_definition") ? ts_node_parent(node) : ts_node_parent(parent);
if (!kind_is(literal, "object") ||
(!kind_is(node, "method_definition") && !kind_is(parent, "pair")))
return;
TSNode holder = ts_node_parent(literal);
if (kind_is(holder, "variable_declarator") && ts_node_eq(literal, field(holder, "value")) &&
kind_is(field(holder, "name"), "identifier")) {
state->js_this_owner = node_text(index, field(holder, "name"));
} else if (kind_is(holder, "assignment_expression") &&
ts_node_eq(literal, field(holder, "right"))) {
TSNode left = field(holder, "left");
if (kind_is(left, "member_expression") && kind_is(field(left, "object"), "identifier") &&
kind_is(field(left, "property"), "property_identifier")) {
const char *property = node_text(index, field(left, "property"));
if (property && !strcmp(property, "prototype")) {
state->js_this_owner = node_text(index, field(left, "object"));
state->js_this_prototype = true;
}
}
}
}
/* `owner`, `owner.prototype` or `this` as the object whose members are named. */
static const char *js_member_owner(cbm_resolve_binding_index_t *index, TSNode object,
const binding_walk_t *state, bool *prototype) {
*prototype = false;
if (kind_is(object, "this")) {
*prototype = state->js_this_prototype;
return state->js_this_owner;
}
if (kind_is(object, "identifier"))
return node_text(index, object);
if (kind_is(object, "member_expression") && kind_is(field(object, "object"), "identifier") &&
kind_is(field(object, "property"), "property_identifier")) {
const char *middle = node_text(index, field(object, "property"));
if (middle && !strcmp(middle, "prototype")) {
*prototype = true;
return node_text(index, field(object, "object"));
}
}
return NULL;
}
/* `owner['m'] = v` writes that member as plainly as `owner.m = v`, so it answers for nothing.
* Express's `app[method] = fn` declares no member at all: which key it writes is runtime
* dispatch, and the declaration `this.set()` selects by static rules is still `app.set`. */
static void note_javascript_computed_write(cbm_resolve_binding_index_t *index, TSNode left,
const binding_walk_t *state) {
TSNode key = field(left, "index");
if (!kind_is(key, "string"))
return;
bool prototype = false;
const char *owner = js_member_owner(index, field(left, "object"), state, &prototype);
const char *member = owner ? http_module_literal(index, key) : NULL;
js_member_t *row =
owner ? js_member_row(index, owner, member ? member : JS_MEMBER_COMPUTED, prototype) : NULL;
if (row)
row->conflict = true;
}
/* A call that names its first argument the object it copies members into. */
static bool js_mutator_name(const char *name) {
static const char *const names[] = {"assign",
"defineProperty",
"defineProperties",
"deleteProperty",
"extend",
"merge",
"mixin",
"set"};
for (size_t i = 0; name && i < sizeof(names) / sizeof(names[0]); i++) {
if (!strcmp(name, names[i]))
return true;
}
return false;
}
/* `Object.assign(owner, ...)` and `mixin(owner.prototype, ...)` can replace members no
* assignment names. `Reflect.set` is the one receiver-sensitive name: a plain
* `map.set(owner, v)` mutates `map`, so `set` needs that receiver. `delete owner.m` and
* `Object.defineProperty(owner, 'm', d)` spell the one member they touch, as Express's
* `Object.defineProperty(this, 'router', ...)` does beside every other member of `app`. */
static void note_javascript_member_mutation(cbm_resolve_binding_index_t *index, TSNode node,
const binding_walk_t *state) {
TSNode object = (TSNode){0};
const char *spelled = NULL;
if (kind_is(node, "unary_expression") && child_token(node, "delete")) {
TSNode target = field(node, "argument");
if (kind_is(target, "member_expression") || kind_is(target, "subscript_expression"))
object = field(target, "object");
if (kind_is(target, "member_expression") &&
kind_is(field(target, "property"), "property_identifier"))
spelled = node_text(index, field(target, "property"));
} else if (kind_is(node, "call_expression")) {
TSNode callee = field(node, "function");
bool member = kind_is(callee, "member_expression");
const char *name = member
? node_text(index, field(callee, "property"))
: (kind_is(callee, "identifier") ? node_text(index, callee) : NULL);
if (!js_mutator_name(name))
return;
const char *receiver = member ? node_text(index, field(callee, "object")) : NULL;
if (!strcmp(name, "set") && (!receiver || strcmp(receiver, "Reflect")))
return;
TSNode arguments = field(node, "arguments");
if (kind_is(arguments, "arguments") && ts_node_named_child_count(arguments) > 0)
object = ts_node_named_child(arguments, 0);
if (!strcmp(name, "defineProperty") && !ts_node_is_null(object) &&
ts_node_named_child_count(arguments) > 1)
spelled = http_module_literal(index, ts_node_named_child(arguments, 1));
}
if (ts_node_is_null(object))
return;
bool prototype = false;
const char *owner = js_member_owner(index, object, state, &prototype);
if (!owner)
return;
js_member_t *row =
js_member_row(index, owner, spelled ? spelled : JS_MEMBER_COMPUTED, prototype);
if (row && spelled)
row->conflict = true;
}
/* Writes that define or replace a member. Only a module-level assignment defines
* one; `this.x = v` or an assignment inside a function just makes x unknowable. */
static void note_javascript_member_write(cbm_resolve_binding_index_t *index, TSNode left,
TSNode right, const binding_walk_t *state) {
TSNode object = field(left, "object");
TSNode property = field(left, "property");
if (!kind_is(property, "property_identifier"))
return;
const char *member = node_text(index, property);
if (!member)
return;
if (kind_is(object, "this")) {
if (state->js_this_owner) {
js_member_t *row =
js_member_row(index, state->js_this_owner, member, state->js_this_prototype);
if (row)
row->conflict = true;
}
return;
}
bool prototype = false;
const char *owner = js_member_owner(index, object, state, &prototype);
/* `Owner.prototype = {...}` itself. Any other value replaces every member. */
if (owner && !prototype && !strcmp(member, "prototype")) {
TSNode value = js_unwrap_value(right);
if (kind_is(value, "object") && !state->scope)
note_javascript_object_members(index, owner, value, true);
else
(void)js_member_row(index, owner, JS_MEMBER_COMPUTED, true);
return;
}
js_member_t *row = owner ? js_member_row(index, owner, member, prototype) : NULL;
if (!row)
return;
if (state->scope)
row->conflict = true;
else
js_member_value(index, row, right, member);
}
static void note_javascript_commonjs(cbm_resolve_binding_index_t *index, TSNode node,
const binding_walk_t *state) {
const char *ignored = NULL;
if (js_is_module_exports(index, node)) {
index->javascript_module = true;
index->js_commonjs = true;
} else if (js_require_call(index, node, &ignored)) {
index->javascript_module = true;
}
if (!kind_is(node, "assignment_expression"))
return;
TSNode left = field(node, "left"), right = field(node, "right");
if (js_identifier_is(index, left, "exports")) {
/* `exports = module.exports = x` keeps them one object. `exports = x` does not. */
bool chained = js_chain_writes_module_exports(index, right);
index->js_exports_rebound |= chained;
index->js_exports_detached |= !chained;
return;
}
if (kind_is(left, "subscript_expression"))
note_javascript_computed_write(index, left, state);
if (kind_is(left, "subscript_expression") &&
(js_identifier_is(index, field(left, "object"), "exports") ||
js_is_module_exports(index, field(left, "object")))) {
index->js_exports_open = true;
return;
}
if (!kind_is(left, "member_expression"))
return;
TSNode object = field(left, "object"), property = field(left, "property");
bool whole = js_is_module_exports(index, left);
bool bare = js_identifier_is(index, object, "exports");
bool member = js_is_module_exports(index, object);
if (bare) {
index->javascript_module = true;
index->js_commonjs = true;
}
note_javascript_member_write(index, left, right, state);
/* A value assigned inside a function may or may not run. */
if (state->scope) {
index->js_exports_open |= whole || bare || member;
return;
}
if (whole) {
js_raw_export_t *raw = js_raw_export(index);
if (!raw)
return;
raw->export_name = JS_WHOLE_EXPORT_NAME;
raw->whole = true;
js_fill_value(index, raw, right, NULL);
index->js_whole_export = true;
if (raw->value == JS_VALUE_OBJECT)
js_export_object_members(index, js_unwrap_value(right));
else if (raw->value == JS_VALUE_IDENTIFIER)
js_export_alias(index, raw->local);
return;
}
if (!kind_is(property, "property_identifier"))
return;
const char *owner = bare || member ? NULL
: kind_is(object, "identifier") ? node_text(index, object)
: NULL;
if (!bare && !member && !owner)
return;
const char *name = node_text(index, property);
js_raw_export_t *raw = js_raw_export(index);
if (!raw)
return;
raw->export_name = name;
raw->owner = owner;
raw->bare_exports = bare;
js_fill_value(index, raw, right, name);
if (raw->value == JS_VALUE_OBJECT)
raw->value = JS_VALUE_UNKNOWN;
}
static void note_javascript_export(cbm_resolve_binding_index_t *index, TSNode node,
bool module_level) {
/* The exported-name -> target map is built from these raw facts once every
* declaration is known. The QN summary still certifies only same-named
* local exports: an alias or re-export has no declaration of that QN. */
if (!kind_is(node, "export_statement"))
return;
TSNode source_node = field(node, "source");
bool local_export = ts_node_is_null(source_node);
const char *source = local_export ? NULL : http_module_literal(index, source_node);
bool type_export = child_token(node, "type");
unsigned kind =
type_export ? CBM_RESOLVE_EXPORT_TYPE : CBM_RESOLVE_EXPORT_VALUE | CBM_RESOLVE_EXPORT_TYPE;
if (child_token(node, "default")) {
js_raw_export_t *raw = module_level ? js_raw_export(index) : NULL;
if (!raw)
return;
raw->export_name = "default";
raw->kind = kind;
TSNode declaration = field(node, "declaration");
TSNode name = field(declaration, "name");
if (!ts_node_is_null(declaration) && !ts_node_is_null(name)) {
raw->value = JS_VALUE_IDENTIFIER;
raw->local = node_text(index, name);
} else {
js_fill_value(index, raw, field(node, "value"), NULL);
if (raw->value == JS_VALUE_OBJECT)
raw->value = JS_VALUE_UNKNOWN;
}
return;
}
if (child_token(node, "=")) {
js_raw_export_t *raw = module_level ? js_raw_export(index) : NULL;
if (!raw)
return;
raw->export_name = JS_WHOLE_EXPORT_NAME;
raw->whole = true;
js_fill_value(index, raw, ts_node_named_child(node, 0), NULL);
index->js_whole_export = true;
if (raw->value == JS_VALUE_OBJECT)
js_export_object_members(index, js_unwrap_value(ts_node_named_child(node, 0)));
return;
}
if (!local_export && child_token(node, "*")) {
js_raw_export_t *raw = module_level ? js_raw_export(index) : NULL;
if (raw) {
raw->kind = kind;
raw->source = source;
raw->value = JS_VALUE_REQUIRE;
}
return;
}
uint32_t count = ts_node_named_child_count(node);
for (uint32_t i = 0; i < count; i++) {
TSNode clause = ts_node_named_child(node, i);
if (kind_is(clause, "namespace_export") && module_level) {
js_raw_export_t *raw = js_raw_export(index);
if (raw) {
raw->export_name = node_text(index, ts_node_named_child(clause, 0));
raw->kind = kind;
raw->source = source;
raw->value = JS_VALUE_REQUIRE;
}
continue;
}
if (!kind_is(clause, "export_clause")) {
continue;
}
uint32_t specifiers = ts_node_named_child_count(clause);
for (uint32_t s = 0; s < specifiers; s++) {
TSNode specifier = ts_node_named_child(clause, s);
if (!kind_is(specifier, "export_specifier"))
continue;
TSNode local = field(specifier, "name");
TSNode alias = field(specifier, "alias");
const char *local_name = node_text(index, local);
const char *alias_name = ts_node_is_null(alias) ? local_name : node_text(index, alias);
if (local_name && alias_name) {
bool type_specifier = type_export || child_token(specifier, "type");
if (local_export && kind_is(local, "identifier") &&
strcmp(local_name, alias_name) == 0) {
add_name(index, local_name, 0,
type_specifier ? BINDING_TYPE_EXPORT_MARKER : BINDING_EXPORT_MARKER);
} else {
add_name(index, alias_name, 0, BINDING_EXPORT_CONFLICT);
}
js_raw_export_t *raw = module_level ? js_raw_export(index) : NULL;
if (raw) {
raw->export_name = alias_name;
raw->kind = type_specifier ? CBM_RESOLVE_EXPORT_TYPE : kind;
if (local_export) {
raw->value = JS_VALUE_IDENTIFIER;
raw->local = local_name;
} else {
raw->value = JS_VALUE_REQUIRE;
raw->source = source;
raw->member = local_name;
}
}
}
}
}
}
static bool javascript_runtime_function(TSNode node) {
const char *kind = ts_node_type(node);
return javascript_function_kind(kind) || !strcmp(kind, "function_expression") ||
!strcmp(kind, "generator_function") || !strcmp(kind, "arrow_function") ||
!strcmp(kind, "method_definition");
}
static uint32_t javascript_new_scope(cbm_resolve_binding_index_t *index, const char *qualified_name,
uint32_t parent, bool function_scope, bool namespace_scope) {
binding_scope_t *scopes = grow_array(index, index->scopes, index->scope_count,
&index->scope_capacity, sizeof(*scopes));
if (!scopes) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "javascript_scope_allocation");
return parent;
}
index->scopes = scopes;
uint32_t scope = (uint32_t)index->scope_count++;
scopes[scope] = (binding_scope_t){.qualified_name = qualified_name,
.parent = parent,
.function_scope = function_scope,
.namespace_scope = namespace_scope};
return scope;
}
/* A var hoists to here. Scope 0 is the module's own function scope. */
static uint32_t javascript_function_scope(const cbm_resolve_binding_index_t *index,
uint32_t scope) {
while (scope && scope < index->scope_count && !index->scopes[scope].function_scope)
scope = index->scopes[scope].parent;
return scope;
}
/* How the child in this grammar position binds. Only a pattern position
* passes a binder down: a default value, a computed key or `a.b = 1` reads its
* identifiers and rebinds nothing. */
static binding_walk_t javascript_child_context(const char *parent, const char *field_name,
binding_walk_t state) {
if (!parent)
return state;
unsigned char inherited = state.js_binder;
unsigned char binder = JS_BINDER_NONE;
bool name = field_name && !strcmp(field_name, "name");
bool left = field_name && !strcmp(field_name, "left");
bool type = field_name && !strcmp(field_name, "type");
if (!strcmp(parent, "variable_declarator")) {
if (name)
binder = state.js_declaration;
else
state.named_export = false;
} else if (!strcmp(parent, "for_in_statement")) {
if (left)
binder = state.js_for_binder;
} else if (!strcmp(parent, "assignment_expression") ||
!strcmp(parent, "augmented_assignment_expression")) {
if (left)
binder = JS_BINDER_WRITE;
} else if (!strcmp(parent, "update_expression")) {
if (field_name && !strcmp(field_name, "argument"))
binder = JS_BINDER_WRITE;
} else if (!strcmp(parent, "catch_clause") || !strcmp(parent, "arrow_function")) {
if (field_name && !strcmp(field_name, "parameter"))
binder = JS_BINDER_PARAMETER;
} else if (!strcmp(parent, "formal_parameters")) {
binder = JS_BINDER_PARAMETER;
} else if (!strcmp(parent, "required_parameter") || !strcmp(parent, "optional_parameter")) {
if (name || (field_name && !strcmp(field_name, "pattern")))
binder = JS_BINDER_PARAMETER;
} else if (!strcmp(parent, "assignment_pattern") ||
!strcmp(parent, "object_assignment_pattern")) {
if (left)
binder = inherited;
} else if (!strcmp(parent, "pair_pattern")) {
if (field_name && !strcmp(field_name, "value"))
binder = inherited;
} else if (!strcmp(parent, "object_pattern") || !strcmp(parent, "array_pattern") ||
!strcmp(parent, "rest_pattern")) {
binder = inherited;
} else if (inherited == JS_BINDER_WRITE &&
(!strcmp(parent, "parenthesized_expression") ||
!strcmp(parent, "non_null_expression") || !strcmp(parent, "as_expression") ||
!strcmp(parent, "satisfies_expression"))) {
binder = inherited;
}
if (type)
state.js_type_level = true;
state.js_binder = state.js_type_level ? JS_BINDER_NONE : binder;
if (!strcmp(parent, "nested_identifier") ||
(name && (!strcmp(parent, "internal_module") || !strcmp(parent, "module"))))
state.js_no_site = true;
return state;
}
static bool javascript_type_level_kind(const char *kind) {
static const char *const kinds[] = {"type_annotation",
"type_arguments",
"type_parameters",
"function_type",
"constructor_type",
"object_type",
"interface_declaration",
"interface_body",
"type_alias_declaration",
"extends_type_clause",
"implements_clause",
"function_signature",
"abstract_method_signature",
"method_signature",
"call_signature",
"construct_signature",
"index_signature",
"adding_type_annotation",
"opting_type_annotation",
"omitting_type_annotation",
"asserts_annotation",
"type_predicate_annotation",
"template_type",
NULL};
return listed(kind, kinds);
}
/* One token the grammar cannot place, in a position that only names types. */
static bool js_parse_gap_inert(TSNode gap, const char *source, bool in_type) {
if (ts_node_is_missing(gap) || ts_node_start_point(gap).row != ts_node_end_point(gap).row)
return false;
uint32_t start = ts_node_start_byte(gap), end = ts_node_end_byte(gap);
if (memchr(source + start, '{', end - start) || memchr(source + start, '}', end - start))
return false;
/* `export type * as Name from 'x'` keeps its whole export but the word `type`. */
TSNode parent = ts_node_parent(gap);
if (kind_is(parent, "export_statement"))
return end - start == 4 && !memcmp(source + start, "type", 4);
return in_type;
}
/* `field?: import('x').Y`, `in2?: number` in an interface and `export type * as N from 'x'`
* are valid TypeScript this grammar cannot parse. A type has no runtime binding and these
* gaps open or close no block, so the rest of the tree still says what the file binds.
* Any other parse error can hide a declaration or move a scope, and voids the file. */
bool cbm_resolve_binding_js_parse_gaps_inert(TSNode root, const char *source) {
if (ts_node_is_null(root) || !source || !ts_node_has_error(root))
return false;
TSTreeCursor cursor = ts_tree_cursor_new(root);
bool inert = true;
bool walking = ts_tree_cursor_goto_first_child(&cursor);
unsigned depth = 1, type_depth = 0;
while (walking && inert) {
TSNode node = ts_tree_cursor_current_node(&cursor);
bool descend = false;
if (ts_node_is_missing(node) || kind_is(node, "ERROR")) {
inert = js_parse_gap_inert(node, source, type_depth != 0);
} else if (ts_node_has_error(node)) {
descend = true;
if (!type_depth && javascript_type_level_kind(ts_node_type(node)))
type_depth = depth;
}
if (descend && ts_tree_cursor_goto_first_child(&cursor)) {
depth++;
continue;
}
while (!ts_tree_cursor_goto_next_sibling(&cursor)) {
if (depth == 1 || !ts_tree_cursor_goto_parent(&cursor)) {
walking = false;
break;
}
depth--;
if (type_depth > depth)
type_depth = 0;
}
if (type_depth == depth)
type_depth = 0;
}
ts_tree_cursor_delete(&cursor);
return inert;
}
static void js_set_lines(cbm_resolve_binding_index_t *index, size_t row, TSNode node) {
if (row < index->name_count) {
index->names[row].value_start_line = ts_node_start_point(node).row + 1;
index->names[row].value_end_line = ts_node_end_point(node).row + 1;
}
}
/* `const { a, b: c } = require("m")`: the property name a local binds. */
static const char *js_require_pattern_member(cbm_resolve_binding_index_t *index, TSNode node,
const char **source) {
TSNode pattern = ts_node_parent(node);
const char *member = NULL;
if (kind_is(node, "shorthand_property_identifier_pattern") &&
kind_is(pattern, "object_pattern")) {
member = node_text(index, node);
} 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"))
return NULL;
TSNode declarator = ts_node_parent(pattern);
const char *property = NULL;
if (!kind_is(declarator, "variable_declarator") ||
!ts_node_eq(pattern, field(declarator, "name")) ||
!js_require_value(index, field(declarator, "value"), source, &property) || property)
return NULL;
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) {
js_write(index, name, state->scope);
add_site(index, name, node);
return;
}
if (state->js_binder == JS_BINDER_PARAMETER) {
add_name(index, name, 0, BINDING_BLOCKED);
return;
}
/* The occurrence reads in its own block; a var binds at its function. */
add_site(index, name, node);
uint32_t site_scope = index->current_scope;
if (state->js_binder == JS_BINDER_VAR)
index->current_scope = javascript_function_scope(index, site_scope);
const binding_scope_t *scope = &index->scopes[index->current_scope];
bool module_level = index->current_scope == 0 || scope->namespace_scope;
TSNode declarator = ts_node_parent(node);
bool direct =
kind_is(declarator, "variable_declarator") && ts_node_eq(node, field(declarator, "name"));
TSNode value = direct ? field(declarator, "value") : (TSNode){0};
const char *source = NULL, *member = NULL;
size_t row = index->name_count;
if (state->js_ambient) {
add_name(index, name, 0, BINDING_BLOCKED);
} else if (direct && js_require_value(index, value, &source, &member)) {
add_name(index, name, 0, BINDING_IMPORT);
if (index->name_count > row)
index->names[row].js_import = js_import_fact(
index, (cbm_resolve_js_import_t){
.local = name, .source = source, .member = member, .is_require = true});
} else if (!direct && (member = js_require_pattern_member(index, node, &source)) != NULL) {
add_name(index, name, 0, BINDING_IMPORT);
if (index->name_count > row)
index->names[row].js_import = js_import_fact(
index, (cbm_resolve_js_import_t){
.local = name, .source = source, .member = member, .is_require = true});
} else if (direct && js_function_value(value)) {
/* Its own scope reads nothing before this line ran (TDZ, or undefined for
* `var`). A closure, its own body included, runs later and may name it. */
add_name(index, name, ts_node_end_byte(declarator),
BINDING_VALUE_DECLARATION | BINDING_FUNCTION_VALUE |
(state->named_export && module_level ? BINDING_EXPORTED : 0));
js_set_lines(index, row, value);
if (index->name_count > row)
index->names[row].at_start = UINT32_MAX;
} else if (direct && module_level) {
bool is_const = kind_is(ts_node_parent(declarator), "lexical_declaration") &&
child_token(ts_node_parent(declarator), "const");
add_name(index, name, ts_node_end_byte(declarator),
BINDING_VALUE_DECLARATION | (is_const ? BINDING_CONST_VALUE : 0) |
(state->named_export ? BINDING_EXPORTED : 0));
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) {
index->names[row].js_this_owner = state->js_this_owner;
index->names[row].js_this_prototype = state->js_this_prototype;
}
}
if (direct && js_chain_writes_module_exports(index, value))
js_export_alias(index, name);
if (direct && module_level && !state->js_ambient && index->current_scope == 0 &&
kind_is(js_unwrap_value(value), "object"))
note_javascript_object_members(index, name, js_unwrap_value(value), false);
index->current_scope = site_scope;
}
/* `a.b.c`: identifiers and dots only, nothing evaluated along the way. */
static bool js_plain_member_path(const cbm_resolve_binding_index_t *index, TSNode member) {
(void)index;
for (unsigned depth = 0; depth < BINDING_MAX_DEPTH; depth++) {
TSNode object = field(member, "object");
if (kind_is(object, "identifier"))
return true;
if (!kind_is(object, "member_expression") || child_token(object, "optional_chain"))
return false;
member = object;
}
return false;
}
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) {
const char *kind = ts_node_type(node);
state = javascript_child_context(parent_kind, field_name, state);
index->current_scope = state.scope;
index->current_this_owner = state.js_this_owner;
index->current_this_prototype = state.js_this_prototype;
if (!ts_node_is_named(node)) {
return state;
}
/* Only an inert parse gap reaches this walk: see cbm_resolve_binding_js_parse_gaps_inert. */
if (kind_is(node, "ERROR")) {
state.js_no_site = true;
state.js_binder = JS_BINDER_NONE;
state.js_type_level = true;
return state;
}
if (!supported_javascript_node(kind)) {
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED, "unsupported_binding_syntax");
return state;
}
if (javascript_type_level_kind(kind)) {
state.js_type_level = true;
state.js_binder = JS_BINDER_NONE;
}
if (kind_is(node, "import_statement")) {
index->javascript_module |= depth == 1;
state.import = true;
state.type_only = child_token(node, "type");
if (depth != 1) {
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED, "non_module_import_binding");
}
}
note_http_dependency(index, node);
note_javascript_commonjs(index, node, &state);
note_javascript_member_mutation(index, node, &state);
if (kind_is(node, "export_statement")) {
index->javascript_module |= depth == 1;
bool namespace_level = index->scopes[state.scope].namespace_scope && parent_kind &&
!strcmp(parent_kind, "statement_block");
state.named_export = (depth == 1 || namespace_level) && !child_token(node, "default");
note_javascript_export(index, node, depth == 1);
}
if (state.import) {
note_javascript_import(index, node, parent_kind, state.type_only);
}
if (kind_is(node, "ambient_declaration"))
state.js_ambient = true;
if (kind_is(node, "variable_declaration"))
state.js_declaration = JS_BINDER_VAR;
else if (kind_is(node, "lexical_declaration") || kind_is(node, "using_declaration"))
state.js_declaration = JS_BINDER_LEXICAL;
if (kind_is(node, "for_in_statement"))
state.js_for_binder = child_token(node, "var") ? JS_BINDER_VAR
: child_token(node, "let") || child_token(node, "const")
? JS_BINDER_LEXICAL
: JS_BINDER_WRITE;
unsigned declaration = javascript_declaration_kind(kind);
bool namespace_kind = kind_is(node, "internal_module") || kind_is(node, "module");
if (declaration || namespace_kind) {
TSNode name = field(node, "name");
const binding_scope_t *scope = &index->scopes[state.scope];
bool module_level = state.scope == 0 || scope->namespace_scope;
unsigned flags =
namespace_kind ? BINDING_TYPE_DECLARATION | BINDING_VALUE_TYPE : declaration;
if (state.js_ambient)
flags = BINDING_BLOCKED;
else if (module_level && state.named_export)
flags |= BINDING_EXPORTED;
const char *text = kind_is(name, "identifier") || kind_is(name, "type_identifier")
? node_text(index, name)
: NULL;
if (text) {
size_t row = index->name_count;
add_name(index, text, 0, flags);
js_set_lines(index, row, node);
/* A sloppy-mode block function also assigns a function-scoped var. */
uint32_t boundary = javascript_function_scope(index, state.scope);
if (javascript_function_kind(kind) && boundary != state.scope) {
index->current_scope = boundary;
add_name(index, text, 0, BINDING_BLOCKED);
index->current_scope = state.scope;
}
}
if (namespace_kind) {
const char *owner = scope->qualified_name;
const char *qn =
text && owner ? cbm_arena_sprintf(index->arena, "%s.%s", owner, text) : owner;
state.scope = javascript_new_scope(index, qn, state.scope, true, true);
state.js_body = true;
state.named_export = false;
index->current_scope = state.scope;
}
} else if (kind_is(node, "function_signature")) {
const char *text = node_text(index, field(node, "name"));
if (state.js_ambient && text)
add_name(index, text, 0, BINDING_BLOCKED);
} else if (kind_is(node, "import_alias")) {
/* `import a = b.c` names a namespace path with no binding proof. */
const char *text = node_text(index, ts_node_named_child(node, 0));
if (text)
add_name(index, text, 0, BINDING_BLOCKED);
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;
const char *owner = index->scopes[state.scope].qualified_name;
const char *qn =
owner ? cbm_arena_sprintf(index->arena, "%s.<function@%u>", owner,
ts_node_start_byte(node))
: cbm_arena_sprintf(index->arena, "%s.<function@%u>", index->result->module_qn,
ts_node_start_byte(node));
state.scope = javascript_new_scope(index, qn, state.scope, true, false);
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"));
if (text) {
size_t row = index->name_count;
add_name(index, text, 0, BINDING_DECLARATION);
js_set_lines(index, row, node);
}
}
} else if (kind_is(node, "class")) {
const char *text = node_text(index, field(node, "name"));
if (text) {
state.scope = javascript_new_scope(index, NULL, state.scope, false, false);
index->current_scope = state.scope;
add_name(index, text, 0, BINDING_BLOCKED);
}
} else if (kind_is(node, "statement_block")) {
if (state.js_body)
state.js_body = false;
else
state.scope = javascript_new_scope(index, NULL, state.scope, false, false);
state.named_export = false;
index->current_scope = state.scope;
} else if (kind_is(node, "for_statement") || kind_is(node, "for_in_statement") ||
kind_is(node, "switch_body")) {
state.scope = javascript_new_scope(index, NULL, state.scope, false, false);
index->current_scope = state.scope;
} else if (kind_is(node, "catch_clause")) {
state.scope = javascript_new_scope(index, NULL, state.scope, false, false);
state.js_body = true;
index->current_scope = state.scope;
} else if (kind_is(node, "class_static_block")) {
state.scope = javascript_new_scope(index, NULL, state.scope, true, false);
state.js_body = true;
index->current_scope = state.scope;
} else if (kind_is(node, "class_body")) {
state.named_export = false;
/* Field initializers and static blocks have the class's own `this`. */
state.js_this_owner = NULL;
state.js_this_prototype = false;
index->current_this_owner = NULL;
index->current_this_prototype = false;
}
bool declaration_name =
parent_kind && field_name && strcmp(field_name, "name") == 0 &&
(javascript_declaration_kind(parent_kind) || !strcmp(parent_kind, "function_signature") ||
javascript_named_function_scope_kind(parent_kind) || !strcmp(parent_kind, "class"));
if (strcmp(kind, "identifier") == 0 || strcmp(kind, "shorthand_property_identifier") == 0 ||
strcmp(kind, "shorthand_property_identifier_pattern") == 0) {
const char *name = node_text(index, node);
/* Only a direct `eval(code)` runs code in this scope. `Function(code)` and an
* indirect eval compile in the global scope, which sees no module or function
* binding, and a bare mention such as `typeof Function` runs nothing. */
if (name && !strcmp(name, "eval") && parent_kind && field_name &&
!strcmp(parent_kind, "call_expression") && !strcmp(field_name, "function")) {
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED, "dynamic_namespace_effect");
}
if (!state.import && !declaration_name && !state.js_no_site) {
if (state.js_binder) {
note_javascript_binder(index, node, name, &state);
} else {
size_t before = index->site_count;
add_site(index, name, node);
if (index->site_count > before && parent_kind &&
!strcmp(parent_kind, "nested_type_identifier"))
index->sites[before].type_path_root = true;
}
}
} else if (kind_is(node, "nested_type_identifier") && !state.import) {
size_t before = index->site_count;
add_site(index, node_text(index, node), node);
if (index->site_count > before)
index->sites[before].type_position = true;
} else if (kind_is(node, "type_identifier") &&
(kind_is(ts_node_parent(node), "type_parameter") ||
kind_is(ts_node_parent(node), "mapped_type_clause") ||
kind_is(ts_node_parent(node), "infer_type"))) {
/* `<T>`, `[K in Keys]` and `infer U` shadow a same-named type wherever they are
* visible. The other names of a mapped clause are ordinary references. */
if (!kind_is(ts_node_parent(node), "mapped_type_clause") ||
ts_node_eq(node, field(ts_node_parent(node), "name"))) {
add_name(index, node_text(index, node), 0, BINDING_BLOCKED | BINDING_TYPE_DECLARATION);
} else if (!state.import) {
size_t before = index->site_count;
add_site(index, node_text(index, node), node);
if (index->site_count > before)
index->sites[before].type_position = true;
}
} else if (kind_is(node, "type_identifier") && !state.import && !declaration_name) {
size_t before = index->site_count;
add_site(index, node_text(index, node), node);
if (index->site_count > before)
index->sites[before].type_position = true;
}
if (kind_is(node, "property_identifier") &&
kind_is(ts_node_parent(node), "member_expression") &&
ts_node_eq(node, field(ts_node_parent(node), "property")) &&
js_plain_member_path(index, ts_node_parent(node))) {
size_t before = index->site_count;
add_site(index, node_text(index, node), node);
if (index->site_count > before)
index->sites[before].member_path = node_text(index, ts_node_parent(node));
}
if (strcmp(kind, "call_expression") == 0 || strcmp(kind, "new_expression") == 0) {
note_call(index, node);
} else if (jsx_component_site(index, node)) {
add_site(index, node_text(index, field(node, "name")), node);
}
return state;
}
/* Dart libraries bind their complete public namespace. Source prefixes and
* combinators are retained independently of the generic import extractor. */
static TSNode dart_identifier(TSNode node) {
TSNode name = field(node, "name");
if (kind_is(name, "identifier") || kind_is(name, "type_identifier"))
return name;
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);
if (kind_is(child, "identifier"))
return child;
}
for (uint32_t i = 0; i < count; i++) {
TSNode child = ts_node_named_child(node, i);
if (kind_is(child, "type_identifier"))
return child;
}
return (TSNode){0};
}
static const char *dart_uri(cbm_resolve_binding_index_t *index, TSNode node) {
const char *text = node_text(index, node);
size_t length = text ? strlen(text) : 0;
if (length < 3 || (text[0] != '\'' && text[0] != '"') || text[length - 1] != text[0])
return NULL;
for (size_t i = 1; i + 1 < length; i++) {
if (text[i] == '\\' || text[i] == '$' || text[i] == '\n' || text[i] == '\r' ||
text[i] == '%' || text[i] == '?' || text[i] == '#' || text[i] == text[0])
return NULL;
}
return cbm_arena_strndup(index->arena, text + 1, length - 2);
}
static void dart_combinator(cbm_resolve_binding_index_t *index, TSNode node,
cbm_resolve_source_import_t *import) {
bool show = child_token(node, "show"), hide = child_token(node, "hide");
if (show == hide || (show && import->has_show) || (hide && import->hide)) {
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED, "dart_combinator_composition_required");
return;
}
uint32_t count = ts_node_named_child_count(node);
const char **names = cbm_arena_calloc(index->arena, (size_t)count * sizeof(*names));
if (!names && count) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "dart_import_allocation");
return;
}
size_t size = 0;
for (uint32_t c = 0; c < count; c++) {
TSNode child = ts_node_named_child(node, c);
if (!kind_is(child, "identifier")) {
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED, "dart_combinator_syntax_required");
return;
}
names[size++] = node_text(index, child);
}
if (show) {
import->has_show = true;
import->show = names;
import->show_count = size;
} else {
import->hide = names;
import->hide_count = size;
}
}
static void note_dart_import(cbm_resolve_binding_index_t *index, TSNode node) {
cbm_resolve_source_import_t import = {0};
TSTreeCursor cursor = ts_tree_cursor_new(node);
unsigned depth = 0;
for (;;) {
TSNode child = ts_tree_cursor_current_node(&cursor);
if (depth >= BINDING_MAX_DEPTH) {
fail_index(index, CBM_RESOLVE_ANALYSIS_LIMITED, "dart_import_walk_limit");
break;
}
const char *kind = ts_node_type(child);
if (strcmp(kind, "configuration_uri") == 0 || strcmp(kind, "deferred") == 0) {
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED,
"dart_import_configuration_required");
break;
}
if (strcmp(kind, "string_literal") == 0) {
if (import.uri || !(import.uri = dart_uri(index, child))) {
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED, "dart_import_uri_required");
break;
}
import.uri_start_byte = ts_node_start_byte(child);
import.uri_end_byte = ts_node_end_byte(child);
}
if (strcmp(kind, "identifier") == 0 &&
kind_is(ts_node_parent(child), "import_specification")) {
if (import.prefix) {
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED, "dart_import_prefix_required");
break;
}
import.prefix = node_text(index, child);
}
if (strcmp(kind, "combinator") == 0)
dart_combinator(index, child, &import);
if (index->analysis != CBM_RESOLVE_ANALYSIS_COMPLETE)
break;
if (ts_tree_cursor_goto_first_child(&cursor)) {
depth++;
continue;
}
while (!ts_tree_cursor_goto_next_sibling(&cursor)) {
if (!ts_tree_cursor_goto_parent(&cursor))
goto finished;
depth--;
}
}
finished:
ts_tree_cursor_delete(&cursor);
if (index->analysis != CBM_RESOLVE_ANALYSIS_COMPLETE)
return;
if (!import.uri) {
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED, "dart_import_uri_required");
return;
}
cbm_resolve_source_import_t *imports = grow_array(index, index->imports, index->import_count,
&index->import_capacity, sizeof(*imports));
if (!imports)
return;
index->imports = imports;
imports[index->import_count++] = import;
if (import.prefix)
add_name(index, import.prefix, 0, BINDING_IMPORT);
}
static bool supported_dart_node(const char *kind) {
static const char *const syntax[] = {"program",
"void_type",
"identifier",
"type_identifier",
"decimal_integer_literal",
"hex_integer_literal",
"decimal_floating_point_literal",
"true",
"false",
"string_literal",
"template_substitution",
"escape_sequence",
"null_literal",
"list_literal",
"set_or_map_literal",
"pair",
"record_literal",
"record_field",
"throw_expression",
"throw_expression_without_cascade",
"rethrow_expression",
"assignment_expression",
"assignment_expression_without_cascade",
"assignable_expression",
"lambda_expression",
"function_expression",
"if_null_expression",
"conditional_expression",
"logical_or_expression",
"logical_and_expression",
"equality_expression",
"type_cast_expression",
"type_test_expression",
"relational_expression",
"relational_operator",
"bitwise_or_expression",
"bitwise_xor_expression",
"bitwise_and_expression",
"shift_expression",
"additive_expression",
"multiplicative_expression",
"bitwise_operator",
"shift_operator",
"additive_operator",
"multiplicative_operator",
"unary_expression",
"postfix_expression",
"postfix_operator",
"spread_element",
"selector",
"prefix_operator",
"negation_operator",
"tilde_operator",
"await_expression",
"type_test",
"is_operator",
"type_cast",
"as_operator",
"new_expression",
"const_object_expression",
"parenthesized_expression",
"constructor_invocation",
"arguments",
"argument",
"named_argument",
"index_selector",
"argument_part",
"unconditional_assignable_selector",
"conditional_assignable_selector",
"type_arguments",
"local_function_declaration",
"block",
"expression_statement",
"assert_statement",
"assertion",
"assertion_arguments",
"do_statement",
"break_statement",
"continue_statement",
"yield_statement",
"yield_each_statement",
"return_statement",
"try_statement",
"catch_clause",
"catch_parameters",
"finally_clause",
"if_element",
"if_statement",
"while_statement",
"for_statement",
"for_loop_parts",
"for_element",
"import_or_export",
"library_import",
"import_specification",
"uri",
"configurable_uri",
"combinator",
"class_definition",
"superclass",
"interfaces",
"class_body",
"getter_signature",
"setter_signature",
"method_signature",
"declaration",
"identifier_list",
"initialized_identifier_list",
"initialized_identifier",
"static_final_declaration_list",
"static_final_declaration",
"initialized_variable_definition",
"function_type",
"parameter_type_list",
"normal_parameter_type",
"optional_parameter_types",
"optional_positional_parameter_types",
"named_parameter_types",
"typed_identifier",
"nullable_type",
"nullable_selector",
"inferred_type",
"function_body",
"function_expression_body",
"function_signature",
"formal_parameter_list",
"optional_formal_parameters",
"formal_parameter",
"local_variable_declaration",
"type_parameters",
"type_parameter",
"type_bound",
"type_alias",
"this",
"super",
"label",
"comment",
"documentation_comment",
"script_tag",
"library_name",
"dotted_identifier_list",
"abstract",
"interface",
"base",
"sealed",
NULL};
return listed(kind, syntax);
}
static void note_dart_call(cbm_resolve_binding_index_t *index, TSNode node) {
TSNode callee = ts_node_prev_named_sibling(node);
if (!kind_is(callee, "identifier"))
return;
/* A selector for a member is not an invocation of the preceding receiver. */
TSNode first = ts_node_named_child(node, 0);
if (!kind_is(first, "argument_part") && !kind_is(first, "arguments"))
return;
const char *name = node_text(index, callee);
add_site(index, name, node);
add_site(index, name, callee);
binding_site_t *sites =
grow_array(index, index->sites, index->site_count, &index->site_capacity, sizeof(*sites));
if (!sites)
return;
index->sites = sites;
sites[index->site_count++] = (binding_site_t){.name = name,
.start = ts_node_start_byte(callee),
.end = ts_node_end_byte(node),
.scope = 0};
}
static binding_walk_t note_dart_node(cbm_resolve_binding_index_t *index, TSNode node,
const char *parent_kind, const char *field_name,
unsigned depth, binding_walk_t state) {
(void)field_name;
const char *kind = ts_node_type(node);
if (!ts_node_is_named(node))
return state;
if (strcmp(kind, "part_directive") == 0 || strcmp(kind, "part_of_directive") == 0 ||
strcmp(kind, "library_export") == 0) {
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED,
"dart_library_namespace_expansion_required");
return state;
}
if (!supported_dart_node(kind)) {
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED, "unsupported_dart_binding_syntax");
return state;
}
if (strcmp(kind, "library_import") == 0)
note_dart_import(index, node);
if (strcmp(kind, "import_or_export") == 0)
state.import = true;
if (strcmp(kind, "function_body") == 0 || strcmp(kind, "class_body") == 0)
state.named_export = false;
bool function = strcmp(kind, "function_signature") == 0;
bool type = strcmp(kind, "class_definition") == 0 || strcmp(kind, "type_alias") == 0;
bool accessor = strcmp(kind, "getter_signature") == 0 || strcmp(kind, "setter_signature") == 0;
if (function || type || accessor) {
TSNode name = dart_identifier(node);
unsigned flags = BINDING_BLOCKED;
if (depth == 1) {
flags = BINDING_NAMESPACE_MEMBER;
flags |= function ? BINDING_DECLARATION
: type ? BINDING_TYPE_DECLARATION
: BINDING_BLOCKED;
}
if (!ts_node_is_null(name))
add_name(index, node_text(index, name), 0, flags);
else
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED, "dart_declaration_name_required");
}
bool variable = strcmp(kind, "initialized_identifier_list") == 0 ||
strcmp(kind, "initialized_variable_definition") == 0 ||
strcmp(kind, "static_final_declaration_list") == 0 ||
strcmp(kind, "local_variable_declaration") == 0;
if (variable || strcmp(kind, "formal_parameter_list") == 0 ||
strcmp(kind, "catch_parameters") == 0 || strcmp(kind, "for_loop_parts") == 0 ||
strstr(kind, "assignment_expression") ||
((strcmp(kind, "postfix_expression") == 0 || strcmp(kind, "unary_expression") == 0) &&
(child_token(node, "++") || child_token(node, "--"))))
state.blocked = true;
if (variable && depth == 1)
state.named_export = true;
/* Selector/type names are not free lexical occurrences. */
if (strcmp(kind, "selector") == 0 || strcmp(kind, "type_arguments") == 0 ||
strcmp(kind, "type_parameters") == 0 || strcmp(kind, "library_name") == 0)
state.type_only = true;
if (strcmp(kind, "identifier") == 0) {
const char *name = node_text(index, node);
bool declaration_name = parent_kind && (strcmp(parent_kind, "function_signature") == 0 ||
strcmp(parent_kind, "class_definition") == 0 ||
strcmp(parent_kind, "type_alias") == 0 ||
strcmp(parent_kind, "getter_signature") == 0 ||
strcmp(parent_kind, "setter_signature") == 0);
if (state.blocked && !state.import && !declaration_name)
add_name(index, name, 0,
BINDING_BLOCKED | (state.named_export ? BINDING_NAMESPACE_MEMBER : 0));
TSNode parent = ts_node_parent(node);
bool label_name =
kind_is(parent, "label") ||
(kind_is(parent, "named_argument") && ts_node_eq(node, ts_node_named_child(parent, 0)));
if (!state.blocked && !state.import && !state.type_only && !declaration_name && !label_name)
add_site(index, name, node);
}
if (strcmp(kind, "selector") == 0 && !state.import)
note_dart_call(index, node);
return state;
}
static bool supported_rust_node(const char *kind) {
static const char *const syntax[] = {"source_file",
"function_item",
"function_modifiers",
"visibility_modifier",
"identifier",
"type_identifier",
"primitive_type",
"parameters",
"parameter",
"self_parameter",
"mutable_specifier",
"reference_type",
"pointer_type",
"function_type",
"unit_type",
"tuple_type",
"block",
"expression_statement",
"empty_statement",
"call_expression",
"arguments",
"integer_literal",
"float_literal",
"boolean_literal",
"char_literal",
"string_literal",
"string_content",
"escape_sequence",
"raw_string_literal",
"line_comment",
"block_comment",
"doc_comment",
"inner_doc_comment_marker",
"outer_doc_comment_marker",
"return_expression",
"binary_expression",
"unary_expression",
"parenthesized_expression",
"reference_expression",
"assignment_expression",
"compound_assignment_expr",
"let_declaration",
"tuple_pattern",
"tuple_struct_pattern",
"struct_pattern",
"field_pattern",
"ref_pattern",
"mut_pattern",
"remaining_field_pattern",
"captured_pattern",
"slice_pattern",
"use_declaration",
"scoped_identifier",
"use_as_clause",
"crate",
"self",
"super",
"mod_item",
"const_item",
"static_item",
"struct_item",
"impl_item",
"declaration_list",
"field_declaration_list",
"field_declaration",
"field_identifier",
"ordered_field_declaration_list",
"field_expression",
"struct_expression",
"field_initializer_list",
"field_initializer",
"shorthand_field_initializer",
"array_expression",
"tuple_expression",
"type_cast_expression",
"if_expression",
"else_clause",
"while_expression",
"loop_expression",
"for_expression",
"break_expression",
"continue_expression",
"range_expression",
"index_expression",
NULL};
return listed(kind, syntax);
}
static void note_rust_import(cbm_resolve_binding_index_t *index, TSNode node, unsigned depth) {
TSNode argument = field(node, "argument");
TSNode name = argument;
if (kind_is(argument, "use_as_clause")) {
name = field(argument, "alias");
argument = field(argument, "path");
} else if (kind_is(argument, "scoped_identifier")) {
name = field(argument, "name");
}
if (depth != 1 || !kind_is(name, "identifier") ||
(!kind_is(argument, "identifier") && !kind_is(argument, "scoped_identifier"))) {
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED, "rust_import_binding_unsupported");
return;
}
add_name(index, node_text(index, name), 0, BINDING_IMPORT);
}
static binding_walk_t note_rust_node(cbm_resolve_binding_index_t *index, TSNode node,
const char *parent_kind, const char *field_name,
unsigned depth, binding_walk_t inherited) {
binding_walk_t state = inherited;
const char *kind = ts_node_type(node);
if (!ts_node_is_named(node))
return state;
if (!supported_rust_node(kind)) {
/* Attributes can conditionally remove or generate declarations; a
* macro can introduce lexical bindings. Neither is a completed domain. */
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED, "unsupported_rust_binding_syntax");
return state;
}
if (strcmp(kind, "use_declaration") == 0) {
note_rust_import(index, node, depth);
state.import = true;
}
if (strcmp(kind, "mod_item") == 0 && !ts_node_is_null(field(node, "body"))) {
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED, "rust_inline_module_domain_required");
return state;
}
bool function = strcmp(kind, "function_item") == 0;
bool declaration = function || strcmp(kind, "struct_item") == 0 ||
strcmp(kind, "const_item") == 0 || strcmp(kind, "static_item") == 0 ||
strcmp(kind, "mod_item") == 0;
if (declaration) {
unsigned flags = function && depth == 1 ? BINDING_DECLARATION : BINDING_BLOCKED;
for (uint32_t i = 0; function && depth == 1 && i < ts_node_named_child_count(node); i++) {
TSNode child = ts_node_named_child(node, i);
if (kind_is(child, "visibility_modifier")) {
const char *visibility = node_text(index, child);
if (visibility && strcmp(visibility, "pub") == 0)
flags |= BINDING_EXPORTED;
}
}
add_name(index, node_text(index, field(node, "name")), 0, flags);
}
if (strcmp(kind, "parameter") == 0 || strcmp(kind, "self_parameter") == 0 ||
strcmp(kind, "let_declaration") == 0 || strcmp(kind, "for_expression") == 0 ||
strcmp(kind, "assignment_expression") == 0 || strcmp(kind, "compound_assignment_expr") == 0)
state.blocked = true;
if (field_name && strcmp(field_name, "type") == 0)
state.type_only = true;
if (strcmp(kind, "identifier") == 0) {
bool declaration_name =
field_name && strcmp(field_name, "name") == 0 && parent_kind &&
(strcmp(parent_kind, "function_item") == 0 || strcmp(parent_kind, "const_item") == 0 ||
strcmp(parent_kind, "static_item") == 0 || strcmp(parent_kind, "mod_item") == 0);
const char *name = node_text(index, node);
if (state.blocked && !state.import && !state.type_only && !declaration_name)
add_name(index, name, 0, BINDING_BLOCKED);
if (!state.import && !state.type_only && !declaration_name)
add_site(index, name, node);
}
if (strcmp(kind, "call_expression") == 0)
note_call(index, node);
return state;
}
static binding_walk_t note_node(cbm_resolve_binding_index_t *index, TSNode node,
const char *parent_kind, const char *field_name, unsigned depth,
binding_walk_t inherited) {
if (index->language == CBM_LANG_RUST) {
return note_rust_node(index, node, parent_kind, field_name, depth, inherited);
}
if (index->language == CBM_LANG_DART) {
return note_dart_node(index, node, parent_kind, field_name, depth, inherited);
}
if (javascript_language(index->language)) {
return note_javascript_node(index, node, parent_kind, field_name, depth, inherited);
}
const char *kind = ts_node_type(node);
binding_walk_t state = inherited;
if (index->language == CBM_LANG_PYTHON) {
state = python_child_context(parent_kind, field_name, state);
index->current_scope = state.scope;
index->current_pending = state.pending;
if (!strcmp(kind, "global_statement") || !strcmp(kind, "nonlocal_statement")) {
python_note_rebinding_scope(index, node, !strcmp(kind, "global_statement"));
return state;
}
if (!strcmp(kind, "subscript") && python_own_module_entry(index, node)) {
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED, "dynamic_namespace_effect");
return state;
}
if (!strcmp(kind, "if_statement") || !strcmp(kind, "for_statement") ||
!strcmp(kind, "while_statement") || !strcmp(kind, "try_statement") ||
!strcmp(kind, "with_statement") || !strcmp(kind, "match_statement"))
state.conditional = true;
}
if (!ts_node_is_named(node)) {
return state;
}
if (!supported_node(index->language, kind) &&
!(index->language == CBM_LANG_PYTHON && python_transparent_container(node, parent_kind))) {
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED, "unsupported_binding_syntax");
return state;
}
if (index->language == CBM_LANG_PYTHON && state.scope == 0 && python_annotation_only(node) &&
kind_is(field(node, "left"), "identifier")) {
index->python_annotation_binding_preserved = true;
/* The annotation expression is an ordinary read, as it is on an
* assignment. Continue the walk: it cannot hide dynamic names, nested
* writes or unsupported syntax from the source audit. */
}
/* An interpolation is an ordinary read. Its __format__ can run user code,
* exactly as any call or operator in this file can; neither rebinds a name
* this audit sees. The walk still visits every nested expression, so a
* walrus target or a dynamic name inside the braces keeps its own effect. */
if (index->language == CBM_LANG_PYTHON && !strcmp(kind, "type_conversion")) {
const char *conversion = node_text(index, node);
if (!conversion ||
(strcmp(conversion, "!s") && strcmp(conversion, "!r") && strcmp(conversion, "!a"))) {
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED,
"python_format_conversion_unproven");
return state;
}
}
/* A star under TYPE_CHECKING states the module's names as its plain imports there do. */
if (index->language == CBM_LANG_PYTHON && !strcmp(kind, "wildcard_import") &&
(inherited.scope || (inherited.conditional && !python_type_checking_import(index, node)))) {
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED, "conditional_wildcard_import");
return state;
}
if (index->language == CBM_LANG_PYTHON &&
(!strcmp(kind, "if_statement") || !strcmp(kind, "elif_clause") ||
!strcmp(kind, "assert_statement")))
python_note_narrowing(index, node, kind);
/* A store through an attribute ends what a narrowing said about that attribute. */
if (index->language == CBM_LANG_PYTHON && kind_is(node, "attribute") && field_name &&
!strcmp(field_name, "left") && parent_kind &&
(!strcmp(parent_kind, "assignment") || !strcmp(parent_kind, "augmented_assignment"))) {
const char *chain = python_chain(index, node, 0);
python_store_t *stores =
chain ? grow_array(index, index->python_stores, index->python_store_count,
&index->python_store_capacity, sizeof(*stores))
: NULL;
if (stores) {
index->python_stores = stores;
stores[index->python_store_count++] = (python_store_t){
.chain = chain,
.at = ts_node_end_byte(ts_node_parent(node)),
.scope = state.scope,
.value = strcmp(parent_kind, "assignment")
? NULL
: python_chain(index, field(ts_node_parent(node), "right"), 0),
.value_at = ts_node_start_byte(node)};
}
}
if (index->language == CBM_LANG_PYTHON &&
(!strcmp(kind, "for_statement") || !strcmp(kind, "while_statement"))) {
python_loop_t *loops = grow_array(index, index->python_loops, index->python_loop_count,
&index->python_loop_capacity, sizeof(*loops));
if (!loops) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "python_loop_allocation");
return state;
}
index->python_loops = loops;
uint32_t start = ts_node_start_byte(node);
/* Loops nest or are disjoint, so the walk's order alone says which are still open. */
while (index->python_open_loop && loops[index->python_open_loop - 1].end <= start)
index->python_open_loop = loops[index->python_open_loop - 1].parent;
loops[index->python_loop_count++] = (python_loop_t){
.start = start, .end = ts_node_end_byte(node), .parent = index->python_open_loop};
index->python_open_loop = (uint32_t)index->python_loop_count;
}
bool import = kind_is(node, "import_statement") || kind_is(node, "import_from_statement");
if (import) {
state.import = true;
if (depth == 1) {
note_import(index, node, false);
} else if (index->language != CBM_LANG_PYTHON) {
state.blocked = true;
} else if (python_type_checking_import(index, node)) {
/* Nothing binds at runtime, but an annotation still names the class. */
python_note_annotation_imports(index, node);
} else {
/* `from a import b` binds b in the scope that runs it, and only b.
* One binder is proof of identity wherever it is bound at all;
* alternatives such as try/except ImportError stay ambiguous. */
note_import(index, node, true);
}
}
/* typing.overload stubs announce the def that follows: with it they are one declaration,
* and they bind nothing of their own. */
bool overload_stub = index->language == CBM_LANG_PYTHON &&
!strcmp(kind, "function_definition") && parent_kind &&
!strcmp(parent_kind, "decorated_definition") &&
python_overload_stub(index, ts_node_parent(node));
if (index->language == CBM_LANG_PYTHON && strcmp(kind, "class_definition") == 0) {
/* A class creates a binding in its enclosing namespace. Its members
* do not authorize callable targets, but must not erase independent
* module function/import proofs elsewhere in the file. */
bool decorated = parent_kind && !strcmp(parent_kind, "decorated_definition");
const char *class_name = node_text(index, field(node, "name"));
add_name(index, class_name, ts_node_start_byte(node),
decorated ? BINDING_BLOCKED | BINDING_DECORATED
: BINDING_TYPE_DECLARATION | BINDING_VALUE_TYPE);
if (decorated && index->name_count)
python_note_decorated(index, node, index->names[index->name_count - 1].target,
class_name, true);
}
if ((strcmp(kind, "function_definition") == 0 || strcmp(kind, "function_declaration") == 0) &&
!overload_stub) {
TSNode name = field(node, "name");
/* One binder proves identity wherever it is bound at all, so a def under
* a condition still counts; an alternative def makes two binders. */
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;
if (!ts_node_is_null(name)) {
const char *text = node_text(index, name);
add_name(index, text, ts_node_start_byte(node),
supported ? BINDING_DECLARATION
: decorated ? BINDING_BLOCKED | BINDING_DECORATED
: BINDING_BLOCKED);
if (decorated && index->name_count)
python_note_decorated(index, node, index->names[index->name_count - 1].target, text,
false);
}
}
if (index->language == CBM_LANG_PYTHON &&
(!strcmp(kind, "function_definition") || !strcmp(kind, "class_definition") ||
!strcmp(kind, "lambda"))) {
uint32_t enclosing_scope = state.scope;
bool in_class = enclosing_scope && index->scopes[enclosing_scope].class_scope;
size_t function_member = index->python_member_count;
if (in_class && !strcmp(kind, "function_definition") && !overload_stub)
python_note_method(index, node, parent_kind, enclosing_scope, state.conditional);
else if (!strcmp(kind, "function_definition") && !overload_stub)
python_note_function(index, node, enclosing_scope);
if (in_class && !strcmp(kind, "class_definition"))
python_note_class_value(index, node_text(index, field(node, "name")), node, NULL, false,
false, enclosing_scope);
state.pending = python_pending_declaration(index, node, enclosing_scope, state.pending);
state.scope =
python_child_scope(index, node, enclosing_scope, !strcmp(kind, "class_definition"));
if (!strcmp(kind, "class_definition"))
python_note_class(index, node, state.scope, enclosing_scope);
if (!strcmp(kind, "function_definition") && !overload_stub)
python_note_returns(index, node, function_member, state.scope);
state.header_scope = enclosing_scope;
state.conditional = false;
index->current_scope = state.scope;
index->current_pending = state.pending;
}
/* Annotation expressions read names; they do not bind the names inside
* the type. The parameter name itself remains a shadowing declaration. */
if (index->language == CBM_LANG_PYTHON && strcmp(kind, "type") == 0 && field_name &&
strcmp(field_name, "type") == 0 && parent_kind &&
(strcmp(parent_kind, "typed_parameter") == 0 ||
strcmp(parent_kind, "typed_default_parameter") == 0))
state.blocked = false;
bool annotation_target = index->language == CBM_LANG_PYTHON && !strcmp(kind, "identifier") &&
field_name && !strcmp(field_name, "left") &&
python_annotation_only(ts_node_parent(node));
if (index->language == CBM_LANG_PYTHON) {
/* An attribute under a target role stores into its object's attribute. */
bool attribute_target = !strcmp(kind, "attribute") &&
((parent_kind && field_name && !strcmp(field_name, "left") &&
(!strcmp(parent_kind, "assignment") ||
!strcmp(parent_kind, "augmented_assignment"))) ||
inherited.blocked);
if (attribute_target)
python_note_instance_write(index, node, parent_kind, field_name);
if (!strcmp(kind, "attribute") && !attribute_target) {
add_site(index, node_text(index, node), node);
/* The site exists to reach the chain; lexical lookups refuse it. */
const char *terminal = node_text(index, field(node, "attribute"));
size_t sites = index->site_count;
add_site(index, terminal, node);
if (index->site_count > sites)
index->sites[index->site_count - 1].attribute_terminal = true;
python_note_terminal_wrappers(index, terminal, node);
}
state.blocked = annotation_target
? state.scope != 0
: python_target_role(kind, parent_kind, field_name, state.blocked);
} else if (blocks_identifiers(kind)) {
state.blocked = true;
}
if (strcmp(kind, "identifier") == 0) {
const char *name = node_text(index, node);
if (!name) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "identifier_unavailable");
return state;
}
bool python = index->language == CBM_LANG_PYTHON;
bool lexical = !python || python_lexical_identifier(parent_kind, field_name);
bool function_name = field_name && strcmp(field_name, "name") == 0 && parent_kind &&
(strcmp(parent_kind, "function_definition") == 0 ||
strcmp(parent_kind, "function_declaration") == 0);
bool dynamic = python ? (dynamic_name(index->language, name) &&
python_namespace_call_writes(index, node, name)) ||
python_namespace_view(index, node, name)
: dynamic_name(index->language, name);
if (dynamic && !python) {
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED, "dynamic_namespace_effect");
} else if (dynamic && lexical && !function_name && !state.blocked && !state.import &&
!python_name_shadowed(index, name)) {
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED, "dynamic_namespace_effect");
}
/* A simple module-level assignment declares that variable. One binder
* proves a value identity only: invocation and member lookup through it
* still abstain, see BINDING_VALUE_DECLARATION in validation. */
bool module_value = python && state.blocked && state.scope == 0 && parent_kind &&
!strcmp(parent_kind, "assignment") && field_name &&
!strcmp(field_name, "left") && !annotation_target &&
!ts_node_is_null(field(ts_node_parent(node), "right"));
if (state.blocked && !function_name && lexical) {
size_t recorded = index->name_count;
add_name(index, name, ts_node_start_byte(node),
module_value ? BINDING_VALUE_DECLARATION : BINDING_BLOCKED);
bool binder = python && index->name_count > recorded;
if (binder)
python_note_binder_value(index, node, parent_kind, field_name);
if (binder && state.scope && index->scopes[state.scope].class_scope &&
!annotation_target)
python_note_class_value(
index, name, node, index->names[index->name_count - 1].value_chain,
index->names[index->name_count - 1].value_kind == CBM_PY_ROOT_TYPED,
parent_kind && !strcmp(parent_kind, "assignment") &&
kind_is(field(ts_node_parent(node), "right"), "none"),
state.scope);
}
if (python && annotation_target && state.scope && index->scopes[state.scope].class_scope) {
/* `x: T` in a class body types its instances' x without binding it. */
cbm_resolve_python_member_t *member = python_add_member(
index, state.scope, name, CBM_PY_MEMBER_INSTANCE, ts_node_start_byte(node));
if (!member) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "python_member_allocation");
return state;
}
member->type_chain =
python_annotation_chain(index, field(ts_node_parent(node), "type"), 0);
member->chain_scope = state.scope;
}
if (!state.import && !function_name && !annotation_target && lexical) {
add_site(index, name, node);
if (index->language == CBM_LANG_PYTHON)
note_python_argument_wrappers(index, name, node);
}
}
if (strcmp(kind, "call") == 0 || strcmp(kind, "function_call") == 0) {
note_call(index, node);
}
return state;
}
static int compare_name(const void *left, const void *right) {
const binding_name_t *a = left;
const binding_name_t *b = right;
int order = strcmp(a->name, b->name);
return order ? order : (a->scope > b->scope) - (a->scope < b->scope);
}
static void apply_export_markers(cbm_resolve_binding_index_t *index) {
size_t destination = 0;
for (size_t start = 0; start < index->name_count;) {
size_t end = start;
bool exported = false;
bool conflict = false;
bool type_exported = false;
while (end < index->name_count &&
strcmp(index->names[start].name, index->names[end].name) == 0 &&
index->names[start].scope == index->names[end].scope) {
exported |= (index->names[end].flags & BINDING_EXPORT_MARKER) != 0;
conflict |= (index->names[end].flags & BINDING_EXPORT_CONFLICT) != 0;
type_exported |= (index->names[end].flags & BINDING_TYPE_EXPORT_MARKER) != 0;
end++;
}
for (size_t i = start; i < end; i++) {
binding_name_t name = index->names[i];
if (name.flags &
(BINDING_EXPORT_MARKER | BINDING_EXPORT_CONFLICT | BINDING_TYPE_EXPORT_MARKER)) {
continue;
}
if (exported && (name.flags & (BINDING_DECLARATION | BINDING_TYPE_DECLARATION |
BINDING_VALUE_DECLARATION))) {
name.flags |= BINDING_EXPORTED;
}
if (type_exported && (name.flags & BINDING_TYPE_DECLARATION)) {
name.flags |= BINDING_TYPE_EXPORTED;
}
if (conflict) {
name.flags &= ~(BINDING_EXPORTED | BINDING_TYPE_EXPORTED);
}
index->names[destination++] = name;
}
start = end;
}
index->name_count = destination;
}
/* Rows of one (name, scope), [low, *high). */
static size_t js_name_range(const cbm_resolve_binding_index_t *index, const char *name,
size_t length, uint32_t scope, size_t *high) {
size_t low = 0, top = index->name_count;
while (low < top) {
size_t mid = low + (top - low) / 2;
int compare = strncmp(index->names[mid].name, name, length);
if (!compare && index->names[mid].name[length])
compare = 1;
if (!compare)
compare = (index->names[mid].scope > scope) - (index->names[mid].scope < scope);
if (compare < 0)
low = mid + 1;
else
top = mid;
}
size_t end = low;
while (end < index->name_count && index->names[end].scope == scope &&
strlen(index->names[end].name) == length &&
!strncmp(index->names[end].name, name, length))
end++;
*high = end;
return low;
}
/* TypeScript keeps value and type names apart, so `type Error` does not shadow
* the value Error and `export const Info` + `export type Info` do not collide. */
static bool js_row_in_space(const binding_name_t *row, js_space_t space) {
bool type_only =
(row->flags & BINDING_TYPE_IMPORT) ||
((row->flags & BINDING_TYPE_DECLARATION) && !(row->flags & BINDING_VALUE_TYPE));
/* A parameter or block-local name lives in the value space alone. */
bool value_only =
(row->flags & (BINDING_DECLARATION | BINDING_VALUE_DECLARATION)) != 0 ||
((row->flags & BINDING_BLOCKED) &&
!(row->flags & (BINDING_TYPE_IMPORT | BINDING_TYPE_DECLARATION | BINDING_VALUE_TYPE)));
if (space == JS_SPACE_VALUE)
return !type_only;
if (space == JS_SPACE_TYPE)
return !value_only;
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++) {
const js_write_t *write = &index->js_writes[w];
size_t length = strlen(write->name);
uint32_t scope = write->scope;
bool found = false;
for (unsigned depth = 0; depth < BINDING_MAX_DEPTH && !found; depth++) {
size_t high = 0;
size_t low = js_name_range(index, write->name, length, scope, &high);
for (size_t r = low; r < high; r++) {
binding_name_t *row = &index->names[r];
if (!js_row_in_space(row, JS_SPACE_VALUE))
continue;
found = true;
row->flags |= BINDING_WRITTEN;
/* A write never changes which declaration a variable names. It
* replaces a callable, an import or a const's only value. */
if (!(row->flags & BINDING_VALUE_DECLARATION) ||
(row->flags & (BINDING_CONST_VALUE | BINDING_FUNCTION_VALUE)))
row->flags |= BINDING_BLOCKED;
}
if (found || scope == 0 || scope >= index->scope_count)
break;
scope = index->scopes[scope].parent;
}
if (!found)
missing++;
}
/* A sloppy write to an undeclared name creates a global of that name. */
if (missing) {
uint32_t scope = index->current_scope;
index->current_scope = 0;
size_t count = index->js_write_count;
for (size_t w = 0; w < count; w++) {
const js_write_t *write = &index->js_writes[w];
bool declared = false;
uint32_t at = write->scope;
for (unsigned depth = 0; depth < BINDING_MAX_DEPTH; depth++) {
size_t high = 0;
size_t low = js_name_range(index, write->name, strlen(write->name), at, &high);
declared |= low < high;
if (declared || at == 0 || at >= index->scope_count)
break;
at = index->scopes[at].parent;
}
if (!declared)
add_name(index, write->name, 0, BINDING_BLOCKED);
}
index->current_scope = scope;
qsort(index->names, index->name_count, sizeof(*index->names), compare_name);
}
}
/* Join each source declaration to its extracted definition by name and exact
* lines. A function const gains callable identity from that definition; a
* nested declaration has no target except through this join. */
static int js_value_compare(const void *left, const void *right) {
const binding_name_t *a = *(const binding_name_t *const *)left;
const binding_name_t *b = *(const binding_name_t *const *)right;
int order = strcmp(a->name, b->name);
if (!order && (a->flags & BINDING_VALUE_DECLARATION) && !(a->flags & BINDING_FUNCTION_VALUE))
return strcmp(a->target, b->target);
if (!order)
order = (a->value_start_line > b->value_start_line) -
(a->value_start_line < b->value_start_line);
return order
? order
: (a->value_end_line > b->value_end_line) - (a->value_end_line < b->value_end_line);
}
/* The rows one definition can touch: same name, and either the same value lines or, for a
* value declaration, the same target. A join on the name alone is quadratic in a name that
* repeats, as `data` or `render` does across a generated file. Which of the two a row is
* depends only on its flags, which this audit does not change until every join is done. */
static const binding_name_t **js_value_rows(cbm_resolve_binding_index_t *index, bool declaration,
size_t *count) {
*count = 0;
const binding_name_t **rows =
index->name_count ? cbm_arena_alloc(index->arena, index->name_count * sizeof(*rows)) : NULL;
for (size_t n = 0; rows && n < index->name_count; n++) {
const binding_name_t *row = &index->names[n];
bool value =
(row->flags & BINDING_VALUE_DECLARATION) && !(row->flags & BINDING_FUNCTION_VALUE);
if (declaration ? value && row->target
: !value && ((row->flags & BINDING_FUNCTION_VALUE) ||
(row->scope && (row->flags & (BINDING_DECLARATION |
BINDING_TYPE_DECLARATION)))))
rows[(*count)++] = row;
}
if (*count > 1)
qsort(rows, *count, sizeof(*rows), js_value_compare);
return rows;
}
static size_t js_value_first(const binding_name_t **rows, size_t count, const binding_name_t *key) {
size_t low = 0, high = count;
while (low < high) {
size_t mid = low + (high - low) / 2;
if (js_value_compare(&rows[mid], &key) < 0)
low = mid + 1;
else
high = mid;
}
return low;
}
static void audit_javascript_values(cbm_resolve_binding_index_t *index) {
size_t line_count = 0, target_count = 0;
const binding_name_t **by_lines = js_value_rows(index, false, &line_count);
const binding_name_t **by_target = js_value_rows(index, true, &target_count);
if (index->name_count && (!by_lines || !by_target)) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "binding_allocation");
return;
}
for (int d = 0; d < index->result->defs.count; d++) {
const CBMDefinition *definition = &index->result->defs.items[d];
if (!definition->name || !definition->qualified_name)
continue;
binding_name_t line_key = {.name = definition->name,
.value_start_line = (uint32_t)definition->start_line,
.value_end_line = (uint32_t)definition->end_line};
binding_name_t target_key = {.name = definition->name,
.target = definition->qualified_name,
.flags = BINDING_VALUE_DECLARATION};
const binding_name_t *line_probe = &line_key, *target_probe = &target_key;
size_t at_line = js_value_first(by_lines, line_count, line_probe);
size_t at_target = js_value_first(by_target, target_count, target_probe);
for (;;) {
binding_name_t *binding = NULL;
if (at_line < line_count && !js_value_compare(&by_lines[at_line], &line_probe))
binding = (binding_name_t *)by_lines[at_line++];
else if (at_target < target_count &&
!js_value_compare(&by_target[at_target], &target_probe))
binding = (binding_name_t *)by_target[at_target++];
if (!binding)
break;
cbm_resolve_stats_add(CBM_RESOLVE_STAT_CANDIDATE_VISITS, 1);
bool lines = definition->start_line == binding->value_start_line &&
definition->end_line == binding->value_end_line;
if (binding->flags & BINDING_FUNCTION_VALUE) {
if (lines && definition->label && !strcmp(definition->label, "Function")) {
binding->value_definitions++;
binding->target = definition->qualified_name;
}
} else if (binding->flags & BINDING_VALUE_DECLARATION) {
if (!binding->target || strcmp(binding->target, definition->qualified_name))
continue;
/* `export const Info` + `export type Info`: the type's definition
* shares this QN and backs the other row, not this one. */
if (definition->label &&
(!strcmp(definition->label, "Type") || !strcmp(definition->label, "Interface")))
continue;
binding->value_definitions++;
if (!definition->label || strcmp(definition->label, "Variable") ||
definition->parent_class || !lines)
binding->flags |= BINDING_BLOCKED;
} else if (binding->scope &&
(binding->flags & (BINDING_DECLARATION | BINDING_TYPE_DECLARATION)) &&
lines) {
binding->value_definitions++;
binding->target = definition->qualified_name;
}
}
}
for (size_t n = 0; n < index->name_count; n++) {
binding_name_t *binding = &index->names[n];
if (binding->flags & BINDING_VALUE_DECLARATION) {
if (binding->value_definitions != 1)
binding->flags |= BINDING_BLOCKED;
else if (binding->flags & BINDING_FUNCTION_VALUE)
binding->flags =
(binding->flags & ~(unsigned)BINDING_VALUE_DECLARATION) | BINDING_DECLARATION;
} else if (binding->scope &&
(binding->flags & (BINDING_DECLARATION | BINDING_TYPE_DECLARATION)) &&
binding->value_definitions != 1) {
binding->flags |= BINDING_BLOCKED;
}
}
}
static const binding_name_t *js_module_row(const cbm_resolve_binding_index_t *index,
const char *name, js_space_t space, bool *conflict) {
size_t high = 0;
size_t low = js_name_range(index, name, strlen(name), 0, &high);
const binding_name_t *found = NULL;
for (size_t r = low; r < high; r++) {
if (!js_row_in_space(&index->names[r], space))
continue;
if (found)
*conflict = true;
found = &index->names[r];
}
return found;
}
static int js_compare_definition(const void *left, const void *right) {
const CBMDefinition *a = *(const CBMDefinition *const *)left;
const CBMDefinition *b = *(const CBMDefinition *const *)right;
int order = strcmp(a->name ? a->name : "", b->name ? b->name : "");
if (order)
return order;
return (a->start_line > b->start_line) - (a->start_line < b->start_line);
}
/* An exported function value: its definition at exactly these lines, or none. */
static const char *js_function_target(cbm_resolve_binding_index_t *index,
const CBMDefinition **sorted, const js_raw_export_t *raw) {
if (!raw->function_name)
return NULL;
CBMDefinition probe = {.name = raw->function_name, .start_line = raw->start_line};
const CBMDefinition *key = &probe;
const CBMDefinition **found = bsearch(&key, sorted, (size_t)index->result->defs.count,
sizeof(*sorted), js_compare_definition);
if (!found || !(*found)->label ||
(strcmp((*found)->label, "Function") && strcmp((*found)->label, "Class")) ||
(*found)->end_line != raw->end_line)
return NULL;
return (*found)->qualified_name;
}
static bool js_owner_is_alias(const cbm_resolve_binding_index_t *index, const char *owner) {
for (size_t a = 0; a < index->js_export_alias_count; a++)
if (!strcmp(index->js_export_aliases[a], owner))
return true;
return false;
}
static void js_settle_identifier(cbm_resolve_binding_index_t *index, const js_raw_export_t *raw,
cbm_resolve_js_export_t *out) {
bool conflict = false;
const binding_name_t *row = js_module_row(
index, raw->local, raw->kind == CBM_RESOLVE_EXPORT_TYPE ? JS_SPACE_TYPE : JS_SPACE_VALUE,
&conflict);
/* A same-named value and type can leave together in one specifier. */
if (!row && raw->kind != CBM_RESOLVE_EXPORT_TYPE)
row = js_module_row(index, raw->local, JS_SPACE_TYPE, &conflict);
if (!row || conflict || ((row->flags & BINDING_BLOCKED) && !(row->flags & BINDING_TYPE_IMPORT)))
return;
if (row->js_import) {
out->known = row->js_import->source != NULL;
out->source = row->js_import->source;
out->member = row->js_import->member;
out->default_import = row->js_import->is_default;
} else if (row->target) {
out->known = true;
out->target = row->target;
out->kind =
((row->flags & (BINDING_DECLARATION | BINDING_VALUE_TYPE | BINDING_VALUE_DECLARATION))
? CBM_RESOLVE_EXPORT_VALUE
: 0) |
((row->flags & BINDING_TYPE_DECLARATION) ? CBM_RESOLVE_EXPORT_TYPE : 0);
}
}
/* Settle the raw facts into exported-name -> proved value. A declaration that
* also leaves under its own name via `export` is one more raw fact. */
static void js_settle_exports(cbm_resolve_binding_index_t *index) {
size_t exported = 0;
for (size_t n = 0; n < index->name_count; n++)
if (index->names[n].scope == 0 &&
(index->names[n].flags & (BINDING_EXPORTED | BINDING_TYPE_EXPORTED)))
exported++;
size_t capacity = index->js_raw_export_count + exported;
if (!capacity)
return;
index->js_exports = cbm_arena_alloc(index->arena, capacity * sizeof(*index->js_exports));
if (!index->js_exports) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "binding_export_allocation");
return;
}
const CBMDefinition **sorted = NULL;
int definitions = index->result->defs.count;
for (size_t r = 0; r < index->js_raw_export_count && !sorted && definitions; r++) {
if (index->js_raw_exports[r].value == JS_VALUE_FUNCTION) {
sorted = cbm_arena_alloc(index->arena, (size_t)definitions * sizeof(*sorted));
if (!sorted) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "binding_export_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 n = 0; n < index->name_count; n++) {
const binding_name_t *row = &index->names[n];
if (row->scope || !(row->flags & (BINDING_EXPORTED | BINDING_TYPE_EXPORTED)))
continue;
cbm_resolve_js_export_t *out = &index->js_exports[index->js_export_count++];
bool blocked = (row->flags & BINDING_BLOCKED) != 0;
*out = (cbm_resolve_js_export_t){
.name = row->name,
.target = blocked ? NULL : row->target,
.known = !blocked && row->target,
.kind = ((row->flags &
(BINDING_DECLARATION | BINDING_VALUE_TYPE | BINDING_VALUE_DECLARATION)) &&
(row->flags & BINDING_EXPORTED)
? CBM_RESOLVE_EXPORT_VALUE
: 0) |
((row->flags & BINDING_TYPE_DECLARATION) ? CBM_RESOLVE_EXPORT_TYPE : 0)};
}
size_t last_whole = 0;
for (size_t r = 0; r < index->js_raw_export_count; r++) {
if (index->js_raw_exports[r].whole)
last_whole = r;
}
for (size_t r = 0; r < index->js_raw_export_count; r++) {
const js_raw_export_t *raw = &index->js_raw_exports[r];
if (raw->owner && !js_owner_is_alias(index, raw->owner))
continue;
cbm_resolve_js_export_t *out = &index->js_exports[index->js_export_count++];
*out = (cbm_resolve_js_export_t){
.name = raw->export_name, .kind = raw->kind, .whole = raw->whole};
/* `module.exports = x` replaces the object a bare `exports.y` wrote to, and any
* `exports.y` or `module.exports.y` written before it. A plain `exports = x`
* detaches the name, so the bare writes never reach module.exports at all. */
if (raw->bare_exports &&
(index->js_exports_detached || (index->js_whole_export && !index->js_exports_rebound)))
continue;
if (!raw->whole && !raw->owner && r < last_whole)
continue;
switch (raw->value) {
case JS_VALUE_IDENTIFIER:
js_settle_identifier(index, raw, out);
break;
case JS_VALUE_FUNCTION:
out->target = sorted ? js_function_target(index, sorted, raw) : NULL;
out->known = out->target != NULL;
out->kind = CBM_RESOLVE_EXPORT_VALUE;
break;
case JS_VALUE_REQUIRE:
out->source = raw->source;
out->member = raw->member;
out->known = raw->source != NULL;
break;
case JS_VALUE_OBJECT:
out->is_object = true;
out->known = true;
break;
case JS_VALUE_UNKNOWN:
/* `var app = module.exports = express()`: one value, two names. */
if (raw->whole && index->js_export_alias_count == 1) {
js_raw_export_t alias = *raw;
alias.local = index->js_export_aliases[0];
js_settle_identifier(index, &alias, out);
}
break;
}
}
}
static int js_compare_member(const void *left, const void *right) {
const js_member_t *a = left, *b = right;
int order = strcmp(a->owner, b->owner);
if (!order)
order = strcmp(a->member, b->member);
return order ? order : (a->prototype > b->prototype) - (a->prototype < b->prototype);
}
/* `class X { constructor() {} }` at module level: what `new X()` runs. */
static void js_settle_constructors(cbm_resolve_binding_index_t *index) {
if (index->analysis != CBM_RESOLVE_ANALYSIS_COMPLETE)
return;
for (int d = 0; d < index->result->defs.count; d++) {
const CBMDefinition *definition = &index->result->defs.items[d];
const char *qn = definition->qualified_name;
if (!definition->name || strcmp(definition->name, "constructor") || !qn)
continue;
const char *dot = strrchr(qn, '.');
const char *owner = dot ? dot : qn;
while (owner > qn && owner[-1] != '.')
owner--;
if (!dot || owner == dot)
continue;
const char *name = cbm_arena_strndup(index->arena, owner, (size_t)(dot - owner));
bool conflict = false;
binding_name_t *row =
name ? (binding_name_t *)js_module_row(index, name, JS_SPACE_VALUE, &conflict) : NULL;
if (row && !conflict && row->target && strlen(row->target) == (size_t)(dot - qn) &&
!strncmp(row->target, qn, (size_t)(dot - qn)))
row->js_constructor = qn;
}
}
/* Each member's declaration, by its extracted definition at exact lines or the
* declaration an identifier names. Two rows for one member answer nothing. */
static void js_settle_members(cbm_resolve_binding_index_t *index) {
if (!index->js_member_count || index->analysis != CBM_RESOLVE_ANALYSIS_COMPLETE)
return;
qsort(index->js_members, index->js_member_count, sizeof(*index->js_members), js_compare_member);
int definitions = index->result->defs.count;
const CBMDefinition **sorted =
definitions ? cbm_arena_alloc(index->arena, (size_t)definitions * sizeof(*sorted)) : NULL;
if (definitions && !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];
if (definitions)
qsort(sorted, (size_t)definitions, sizeof(*sorted), js_compare_definition);
for (size_t m = 0; m < index->js_member_count; m++) {
js_member_t *row = &index->js_members[m];
if (m + 1 < index->js_member_count && !js_compare_member(row, row + 1))
row->conflict = row[1].conflict = true;
if (row->conflict)
continue;
if (row->function_name && definitions) {
js_raw_export_t raw = {.function_name = row->function_name,
.start_line = row->start_line,
.end_line = row->end_line};
row->target = js_function_target(index, sorted, &raw);
} else if (row->local) {
bool conflict = false;
const binding_name_t *binding =
js_module_row(index, row->local, JS_SPACE_VALUE, &conflict);
if (binding && !conflict && !(binding->flags & BINDING_BLOCKED) &&
(binding->flags & BINDING_DECLARATION))
row->target = binding->target;
}
}
}
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;
}
const char *cbm_resolve_binding_js_default_target(const cbm_resolve_binding_index_t *index) {
size_t count = cbm_resolve_binding_js_export_count(index);
const char *found = NULL;
for (size_t i = 0; i < count; i++) {
const cbm_resolve_js_export_t *item = &index->js_exports[i];
if (!item->name || strcmp(item->name, "default"))
continue;
if (found || !item->known || item->source || !item->target)
return NULL;
found = item->target;
}
return found;
}
bool cbm_resolve_binding_js_commonjs(const cbm_resolve_binding_index_t *index) {
return index && index->js_commonjs;
}
size_t cbm_resolve_binding_js_export_count(const cbm_resolve_binding_index_t *index) {
return index && index->analysis == CBM_RESOLVE_ANALYSIS_COMPLETE ? index->js_export_count : 0;
}
const cbm_resolve_js_export_t *cbm_resolve_binding_js_export_at(
const cbm_resolve_binding_index_t *index, size_t offset) {
return offset < cbm_resolve_binding_js_export_count(index) ? &index->js_exports[offset] : NULL;
}
static int compare_site(const void *left, const void *right) {
const binding_site_t *a = left;
const binding_site_t *b = right;
if (a->start != b->start) {
return a->start < b->start ? -1 : 1;
}
if (a->end != b->end) {
return a->end < b->end ? -1 : 1;
}
return strcmp(a->name, b->name);
}
static void walk_tree(cbm_resolve_binding_index_t *index, TSNode root) {
binding_walk_t state[BINDING_MAX_DEPTH] = {{0}};
const char *kinds[BINDING_MAX_DEPTH] = {0};
TSTreeCursor cursor = ts_tree_cursor_new(root);
unsigned depth = 0;
for (;;) {
TSNode node = ts_tree_cursor_current_node(&cursor);
/* This walk is linear in the file. A node count would void the largest
* sources for their size alone; only nesting has a real bound. */
if (depth >= BINDING_MAX_DEPTH) {
fail_index(index, CBM_RESOLVE_ANALYSIS_LIMITED, "binding_walk_limit");
break;
}
kinds[depth] = ts_node_type(node);
state[depth] = note_node(index, node, depth ? kinds[depth - 1] : NULL,
ts_tree_cursor_current_field_name(&cursor), depth,
depth ? state[depth - 1] : (binding_walk_t){0});
if (index->analysis != CBM_RESOLVE_ANALYSIS_COMPLETE) {
break;
}
if (ts_tree_cursor_goto_first_child(&cursor)) {
depth++;
continue;
}
while (!ts_tree_cursor_goto_next_sibling(&cursor)) {
if (!ts_tree_cursor_goto_parent(&cursor)) {
ts_tree_cursor_delete(&cursor);
return;
}
depth--;
}
}
ts_tree_cursor_delete(&cursor);
}
bool cbm_resolve_binding_set_identity(cbm_resolve_binding_index_t *index, const char *project,
const char *file) {
if (!index || !index->observation)
return false;
cbm_resolve_proof_source_t source = index->observation->context.source;
source.project = project;
source.file = file;
bool ok = cbm_resolve_lookup_context_init(&index->observation->context,
&index->observation->pool, source);
if (ok)
index->observation->context.source =
cbm_resolve_proof_view(index->observation->context.source_fact)->source;
return ok;
}
cbm_resolve_lookup_context_t *cbm_resolve_binding_lookup_context(
const cbm_resolve_binding_index_t *index) {
return index && index->observation ? &index->observation->context : NULL;
}
const cbm_resolve_proof_t *cbm_resolve_binding_site_proof(const cbm_resolve_binding_index_t *index,
const char *name, uint32_t start,
uint32_t end, CBMSourceOrigin origin) {
const binding_observation_t *value = index ? index->observation : NULL;
return value && name && value->name && !strcmp(value->name, name) && value->start == start &&
value->end == end && value->origin == origin
? value->proof
: NULL;
}
static void binding_query_begin(const cbm_resolve_binding_index_t *index,
cbm_resolve_lookup_t *query, const char *name, uint32_t start,
uint32_t end, CBMSourceOrigin origin) {
cbm_resolve_lookup_context_t *context = cbm_resolve_binding_lookup_context(index);
cbm_resolve_lookup_begin(query, context, "source_lexical_binding", name, start, end, origin);
if (context)
(void)cbm_resolve_lookup_observe(
query, "source_binding_audit", name,
index->analysis == CBM_RESOLVE_ANALYSIS_COMPLETE ? index->reason : NULL,
index->analysis == CBM_RESOLVE_ANALYSIS_COMPLETE ? CBM_RESOLVE_BINDING_UNIQUE
: CBM_RESOLVE_BINDING_UNRESOLVED,
index->analysis, 0, index->analysis == CBM_RESOLVE_ANALYSIS_COMPLETE, index->reason);
}
static cbm_resolve_result_t binding_query_finish(const cbm_resolve_binding_index_t *index,
cbm_resolve_lookup_t *query,
cbm_resolve_result_t result) {
if (!index || !index->observation)
return result;
binding_observation_t *site = index->observation;
site->name =
query->key.lookup_key ? cbm_arena_strdup(index->arena, query->key.lookup_key) : NULL;
site->start = query->key.start_byte;
site->end = query->key.end_byte;
site->origin = (CBMSourceOrigin)query->key.origin;
cbm_resolve_result_t local = result;
/* This is the target checked against the exact lexical binding above,
* separate from a reusable exported semantic root retained in result. */
query->key.binding_identity = local.target;
site->proof = cbm_resolve_lookup_finish(query, CBM_PROOF_REGISTRY_LOOKUP, local);
if (!site->proof) {
result.analysis = site->pool.failure == CBM_RESOLVE_ANALYSIS_LIMITED
? CBM_RESOLVE_ANALYSIS_LIMITED
: CBM_RESOLVE_ANALYSIS_FAILED;
result.facts = 0;
result.reason = "source_binding_proof_unavailable";
} else if (cbm_resolve_proof_view(site->proof)->outcome.analysis !=
CBM_RESOLVE_ANALYSIS_COMPLETE) {
result.analysis = cbm_resolve_proof_view(site->proof)->outcome.analysis;
result.facts &= ~CBM_RESOLVE_FACT_LOOKUP_COMPLETE;
}
/* Shared import roots stay in their owner pool. The coordinator consumes
* this independent local site root alongside that original root. */
if (!result.proof)
result.proof = site->proof;
return result;
}
typedef enum { PY_NS_READ, PY_NS_WRITE, PY_NS_ANY } python_namespace_use_t;
/* A plain string's text, NULL for anything computed. */
static const char *python_plain_string(cbm_resolve_binding_index_t *index, TSNode node) {
if (!kind_is(node, "string") || ts_node_named_child_count(node) != 3)
return NULL;
TSNode content = ts_node_named_child(node, 1);
return kind_is(content, "string_content") ? node_text(index, content) : NULL;
}
/* What one use of a name does to the module it may hold. A namespace changes by a
* statement that stores or deletes through it, and that statement names what it changes.
* Reading a member, calling, or handing the module to a call selects no other
* declaration: what a callee does with it is not a static fact. __dict__, vars() and a
* computed setattr can reach any name. Queries are binary searches, never rescans. */
static python_namespace_use_t python_namespace_use(cbm_resolve_binding_index_t *index, TSNode node,
const char **written) {
TSNode parent = ts_node_parent(node);
if (kind_is(parent, "argument_list")) {
TSNode call = ts_node_parent(parent);
const char *callee = node_text(index, field(call, "function"));
if (callee && !strcmp(callee, "vars"))
return PY_NS_ANY;
if (!callee || (strcmp(callee, "setattr") && strcmp(callee, "delattr")) ||
!ts_node_eq(node, ts_node_named_child(parent, 0)))
return PY_NS_READ;
*written = python_plain_string(index, ts_node_named_child(parent, 1));
return *written ? PY_NS_WRITE : PY_NS_ANY;
}
if (!kind_is(parent, "attribute") || !ts_node_eq(node, field(parent, "object")))
return PY_NS_READ;
const char *path = NULL;
for (unsigned depth = 0; depth < BINDING_MAX_DEPTH; depth++) {
const char *member = node_text(index, field(parent, "attribute"));
if (!member || !strcmp(member, "__dict__"))
return PY_NS_ANY;
path = path ? cbm_arena_sprintf(index->arena, "%s.%s", path, member) : member;
node = parent;
parent = ts_node_parent(node);
if (kind_is(parent, "attribute") && ts_node_eq(node, field(parent, "object")))
continue;
bool stored = ((kind_is(parent, "assignment") || kind_is(parent, "augmented_assignment")) &&
ts_node_eq(node, field(parent, "left"))) ||
kind_is(parent, "delete_statement") ||
(kind_is(parent, "expression_list") &&
kind_is(ts_node_parent(parent), "delete_statement")) ||
kind_is(parent, "pattern_list") || kind_is(parent, "as_pattern_target");
if (!stored)
return PY_NS_READ;
*written = path;
return path ? PY_NS_WRITE : PY_NS_ANY;
}
return PY_NS_ANY;
}
static int python_write_compare(const void *left, const void *right) {
return strcmp(((const python_write_t *)left)->name, ((const python_write_t *)right)->name);
}
/* `alias = pkg; alias.child = None` writes through pkg. Two rounds follow an alias of an alias;
* each is one search per alias statement. */
static void python_follow_write_aliases(cbm_resolve_binding_index_t *index) {
for (unsigned round = 0; round < 2; round++) {
size_t sorted = index->python_write_count;
for (size_t a = 0; a < index->python_alias_count; a++) {
const python_alias_t alias = index->python_aliases[a];
size_t low = 0, high = sorted;
while (low < high) {
size_t middle = low + (high - low) / 2;
if (strcmp(index->python_writes[middle].name, alias.alias) < 0)
low = middle + 1;
else
high = middle;
}
for (; low < sorted && !strcmp(index->python_writes[low].name, alias.alias); low++) {
const char *member = index->python_writes[low].member;
python_write_t *writes =
grow_array(index, index->python_writes, index->python_write_count,
&index->python_write_capacity, sizeof(*writes));
if (!writes) {
index->python_writes_unseen = true;
return;
}
index->python_writes = writes;
writes[index->python_write_count++] =
(python_write_t){.name = alias.of, .member = member};
}
}
if (index->python_write_count == sorted)
return;
qsort(index->python_writes, index->python_write_count, sizeof(*index->python_writes),
python_write_compare);
}
}
static void python_namespace_effects(cbm_resolve_binding_index_t *index, TSNode root) {
TSTreeCursor cursor = ts_tree_cursor_new(root);
unsigned depth = 0;
index->python_effects_complete = true;
for (;;) {
TSNode node = ts_tree_cursor_current_node(&cursor);
if (depth > BINDING_MAX_DEPTH) {
index->python_effects_complete = false;
index->python_writes_unseen = true;
break;
}
bool skip = kind_is(node, "import_statement") || kind_is(node, "import_from_statement");
TSNode parent = ts_node_parent(node);
bool member =
kind_is(node, "identifier") &&
((kind_is(parent, "attribute") && ts_node_eq(node, field(parent, "attribute"))) ||
(kind_is(parent, "keyword_argument") && ts_node_eq(node, field(parent, "name"))));
if (kind_is(node, "identifier") && !member) {
const char *name = node_text(index, node);
if (name && dynamic_name(CBM_LANG_PYTHON, name) &&
python_namespace_call_writes(index, node, name))
index->python_effects_complete = false;
}
if (kind_is(node, "identifier") && !member && kind_is(parent, "argument_list") &&
python_spells(index, node, "__name__"))
index->python_name_handed = true;
if (kind_is(node, "identifier") && !member) {
const char *written = NULL;
python_namespace_use_t use = python_namespace_use(index, node, &written);
const char *name = use != PY_NS_READ ? node_text(index, node) : NULL;
python_write_t *writes =
use != PY_NS_READ
? grow_array(index, index->python_writes, index->python_write_count,
&index->python_write_capacity, sizeof(*writes))
: NULL;
if (use != PY_NS_READ && (!name || !writes)) {
index->python_effects_complete = false;
index->python_writes_unseen = true;
break;
}
if (use != PY_NS_READ) {
index->python_writes = writes;
writes[index->python_write_count++] =
(python_write_t){.name = name, .member = use == PY_NS_WRITE ? written : NULL};
} else if (kind_is(parent, "assignment") && ts_node_eq(node, field(parent, "right")) &&
kind_is(field(parent, "left"), "identifier")) {
python_alias_t *aliases =
grow_array(index, index->python_aliases, index->python_alias_count,
&index->python_alias_capacity, sizeof(*aliases));
const char *alias = node_text(index, field(parent, "left"));
const char *of = node_text(index, node);
if (!aliases || !alias || !of) {
index->python_effects_complete = false;
index->python_writes_unseen = true;
break;
}
index->python_aliases = aliases;
aliases[index->python_alias_count++] = (python_alias_t){alias, of};
}
}
if (!skip && ts_tree_cursor_goto_first_child(&cursor)) {
depth++;
continue;
}
while (!ts_tree_cursor_goto_next_sibling(&cursor)) {
if (!ts_tree_cursor_goto_parent(&cursor))
goto finished;
depth--;
}
}
finished:
ts_tree_cursor_delete(&cursor);
if (index->python_write_count > 1)
qsort(index->python_writes, index->python_write_count, sizeof(*index->python_writes),
python_write_compare);
python_follow_write_aliases(index);
}
/* Symbol QNs omit a terminal index filename, while module QNs retain it.
* Use the captured path and the same normalization as extraction: trimming
* ".index" from a module string would corrupt public.index.ts identities. */
static const char *javascript_symbol_scope(cbm_resolve_binding_index_t *index) {
const CBMFileResult *result = index->result;
if (!result->source_project || !result->source_project[0] || !result->source_file ||
!result->source_file[0] || !result->source_content_id[CBM_SOURCE_ORIGIN_RAW][0] ||
!result->source_configuration_id[0]) {
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED, "javascript_source_identity_missing");
return NULL;
}
const char *module = cbm_fqn_module(index->arena, result->source_project, result->source_file);
char *scope = cbm_fqn_compute(index->arena, result->source_project, result->source_file,
"__binding_scope__");
if (!module || !scope) {
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED, "binding_symbol_scope_allocation");
return NULL;
}
if (strcmp(module, result->module_qn)) {
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED, "javascript_source_identity_mismatch");
return NULL;
}
/* The fixed nonempty member above always adds this final separator. */
*strrchr(scope, '.') = '\0';
return scope;
}
static void audit_http_default(cbm_resolve_binding_index_t *index, TSNode root);
static void audit_http_escapes(cbm_resolve_binding_index_t *index);
static const binding_name_t *find_scoped_name(const cbm_resolve_binding_index_t *index,
const char *name, size_t length, uint32_t scope,
bool *present, js_space_t space);
/* A comprehension body does not see the class it is written in, and has no scope of its
* own here. So a name the class binds could be the comprehension's or the enclosing
* scope's, and it abstains. Any other name reads on through the class as usual. */
static void python_refuse_class_comprehension_reads(cbm_resolve_binding_index_t *index) {
for (size_t i = 0; i < index->site_count; i++) {
binding_site_t *site = &index->sites[i];
if (!site->class_comprehension)
continue;
bool present = false;
(void)find_scoped_name(index, site->name, strcspn(site->name, ".("), site->scope, &present,
JS_SPACE_ANY);
site->attribute_terminal |= present;
}
}
/* Language-level module signals, independent of file contents. A TypeScript
* script can only redeclare a global implementation as a compile error. */
static bool javascript_module_by_source(const CBMFileResult *result, CBMLanguage language) {
if (language == CBM_LANG_TYPESCRIPT || language == CBM_LANG_TSX || language == CBM_LANG_ARKTS)
return true;
const char *file = result ? result->source_file : NULL;
const char *extension = file ? strrchr(file, '.') : NULL;
return extension &&
(!strcmp(extension, ".mjs") || !strcmp(extension, ".cjs") || !strcmp(extension, ".jsx"));
}
/* The callee's body never spells the parameter that receives the namespace, nor a way to
* reach it by another name: locals, vars, eval, exec, or a frame's locals. One cursor pass. */
static bool python_handoff_body_inert(cbm_resolve_binding_index_t *index, TSNode function) {
const char *parameter = python_first_parameter(index, function);
TSNode body = field(function, "body");
if (!parameter || ts_node_is_null(body))
return false;
bool mentioned = false;
TSTreeCursor cursor = ts_tree_cursor_new(body);
bool more = ts_tree_cursor_goto_first_child(&cursor);
unsigned depth = 1;
while (more && !mentioned) {
TSNode node = ts_tree_cursor_current_node(&cursor);
if (kind_is(node, "identifier")) {
mentioned = python_spells(index, node, parameter);
for (size_t i = 0; !mentioned && python_namespace_reach[i]; i++)
mentioned = python_spells(index, node, python_namespace_reach[i]);
}
if (ts_tree_cursor_goto_first_child(&cursor)) {
depth++;
continue;
}
while (!ts_tree_cursor_goto_next_sibling(&cursor)) {
if (--depth == 0 || !ts_tree_cursor_goto_parent(&cursor)) {
more = false;
break;
}
}
}
ts_tree_cursor_delete(&cursor);
return !mentioned;
}
/* A hand-off writes nothing when the callee is this module's one undecorated def, bound before
* the call, whose body cannot reach the namespace it was given. What the body does by other
* means (global, imports, writes through a module) the walk has already seen. Anything else
* stays a whole-namespace effect. One lookup per hand-off; a body is scanned once per def. */
static void python_settle_handoffs(cbm_resolve_binding_index_t *index, TSNode root) {
uint32_t verified = UINT32_MAX;
for (size_t i = 0;
i < index->python_handoff_count && index->analysis == CBM_RESOLVE_ANALYSIS_COMPLETE; i++) {
const python_handoff_t *handoff = &index->python_handoffs[i];
bool present = false;
const binding_name_t *row = find_scoped_name(
index, handoff->callee, strlen(handoff->callee), 0, &present, JS_SPACE_ANY);
bool inert = row && row->at < handoff->at && (row->flags & BINDING_DECLARATION) &&
!(row->flags &
(BINDING_BLOCKED | BINDING_DECORATED | BINDING_WRITTEN | BINDING_IMPORT));
if (inert && row->at != verified) {
TSNode function = ts_node_descendant_for_byte_range(root, row->at, row->at);
for (unsigned depth = 0; depth < BINDING_MAX_DEPTH && !ts_node_is_null(function) &&
!(kind_is(function, "function_definition") &&
ts_node_start_byte(function) == row->at);
depth++)
function = ts_node_parent(function);
inert = kind_is(function, "function_definition") &&
ts_node_start_byte(function) == row->at &&
python_handoff_body_inert(index, function);
if (inert)
verified = row->at;
}
if (!inert)
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED, "dynamic_namespace_effect");
}
}
static cbm_resolve_binding_index_t *binding_build(CBMArena *arena, const CBMFileResult *result,
CBMLanguage language, const char *source,
size_t source_len, bool declared_module,
bool adapter_sites_only) {
cbm_resolve_binding_index_t *index = arena ? cbm_arena_calloc(arena, sizeof(*index)) : NULL;
if (!index) {
return NULL;
}
*index = (cbm_resolve_binding_index_t){.arena = arena,
.result = result,
.language = language,
.source = source,
.source_len = source_len,
.analysis = CBM_RESOLVE_ANALYSIS_UNSUPPORTED,
.reason = "unsupported_binding_language"};
index->observation = cbm_arena_calloc(arena, sizeof(*index->observation));
if (!index->observation)
return NULL;
/* One node per distinct fact of this file. A budget here would drop the later
* occurrences of a large file for being later, not for being unproved. */
cbm_resolve_proof_pool_init_project(&index->observation->pool, arena);
cbm_resolve_proof_source_t captured = {
.module = result ? result->module_qn : NULL,
.scope = result ? result->module_qn : NULL,
.content_identity = result ? result->source_content_id[CBM_SOURCE_ORIGIN_RAW] : NULL,
.configuration_identity = result ? result->source_configuration_id : NULL,
.source_origin = CBM_SOURCE_ORIGIN_RAW};
if (!cbm_resolve_lookup_context_init(&index->observation->context, &index->observation->pool,
captured))
return NULL;
if (result && result->source_content_id[CBM_SOURCE_ORIGIN_RAW][0] &&
!cbm_result_source_matches(result, source, source_len, CBM_SOURCE_ORIGIN_RAW)) {
index->reason = "binding_source_snapshot_mismatch";
return index;
}
/* These languages state their rules per occurrence in adapter_<lang>.c.
* This index only attests the exact site through cbm_resolve_binding_source_site. */
if (adapter_sites_only || (cbm_source_adapter_supported(language) &&
!cbm_source_adapter_has_binding_rules(language))) {
index->analysis = CBM_RESOLVE_ANALYSIS_COMPLETE;
index->reason = "source_adapter_binding";
return index;
}
bool component = language == CBM_LANG_VUE || language == CBM_LANG_SVELTE;
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 ||
(result->parse_incomplete && !javascript_language(language)) || result->parse_unusable ||
result->lsp_skipped) {
index->reason = "incomplete_binding_source";
return index;
}
TSTree *owned_tree = NULL;
const TSTree *tree = result->cached_tree;
if (!tree) {
TSParser *parser = ts_parser_new();
if (parser && ts_parser_set_language(parser, cbm_ts_language(language))) {
owned_tree = language == CBM_LANG_RUST
? cbm_parse_rust_file(parser, result->source_file, source,
(uint32_t)source_len, (TSParseOptions){0})
: cbm_parse_plain(parser, source, (uint32_t)source_len);
}
if (parser) {
ts_parser_delete(parser);
}
tree = owned_tree;
}
bool inert_gaps = tree && javascript_language(language) &&
cbm_resolve_binding_js_parse_gaps_inert(ts_tree_root_node(tree), source);
if (result->parse_incomplete && !component && !inert_gaps) {
index->reason = "incomplete_binding_source";
if (owned_tree)
ts_tree_delete(owned_tree);
return index;
}
uint32_t tree_end = (uint32_t)source_len;
if (component && tree) {
TSRange range = {0};
CBMExtractCtx host = {.arena = arena,
.result = (CBMFileResult *)result,
.source = source,
.source_len = source_len <= INT_MAX ? (int)source_len : -1,
.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;
tree_end = range.end_byte;
if (script) {
index->language = language;
index->javascript_module = true;
}
}
cbm_syntax_index_t syntax;
cbm_syntax_index_begin(&syntax, tree);
if (!tree || (ts_node_has_error(ts_tree_root_node(tree)) && !inert_gaps) ||
ts_node_end_byte(ts_tree_root_node(tree)) != tree_end) {
index->reason = "incomplete_binding_tree";
} else {
index->analysis = CBM_RESOLVE_ANALYSIS_COMPLETE;
index->reason = "binding_complete_supported_subset";
if (language == CBM_LANG_PYTHON || javascript_language(language)) {
index->scopes =
grow_array(index, NULL, 0, &index->scope_capacity, sizeof(*index->scopes));
if (index->scopes) {
const char *scope = result->module_qn;
if (javascript_language(language)) {
scope = javascript_symbol_scope(index);
} else {
size_t symbol_scope_len = cbm_fqn_symbol_scope_len(scope);
if (symbol_scope_len != strlen(scope))
scope = cbm_arena_strndup(arena, scope, symbol_scope_len);
}
if (!scope) {
if (index->analysis == CBM_RESOLVE_ANALYSIS_COMPLETE)
fail_index(index, CBM_RESOLVE_ANALYSIS_FAILED,
"python_symbol_scope_allocation");
} else {
index->scopes[0] =
(binding_scope_t){.qualified_name = scope, .function_scope = true};
index->scope_count = 1;
}
}
}
if (index->analysis == CBM_RESOLVE_ANALYSIS_COMPLETE) {
if (javascript_language(language))
index->javascript_module |= javascript_module_by_source(result, language);
walk_tree(index, ts_tree_root_node(tree));
if (javascript_language(language)) {
index->javascript_module_declared_only =
declared_module && !index->javascript_module;
index->javascript_module |= declared_module;
}
if (language == CBM_LANG_PYTHON) {
python_namespace_effects(index, ts_tree_root_node(tree));
python_finish_narrows(index);
}
}
if (index->analysis == CBM_RESOLVE_ANALYSIS_COMPLETE && javascript_language(language) &&
!index->javascript_module) {
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED, "javascript_script_scope_unproven");
}
}
if (index->name_count) {
qsort(index->names, index->name_count, sizeof(*index->names), compare_name);
apply_export_markers(index);
if (language == CBM_LANG_PYTHON)
python_refuse_class_comprehension_reads(index);
}
if (language == CBM_LANG_PYTHON && tree && index->python_handoff_count)
python_settle_handoffs(index, ts_tree_root_node(tree));
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
* even when the annotation itself is a bare name. Do not infer the
* builtin mapping contract for a source-defined __annotations__. */
if (index->python_annotation_binding_preserved && index->python_annotation_storage_bound)
fail_index(index, CBM_RESOLVE_ANALYSIS_UNSUPPORTED, "python_annotation_storage_unproven");
if (index->site_count) {
qsort(index->sites, index->site_count, sizeof(*index->sites), compare_site);
}
if (tree && javascript_language(language) && index->analysis == CBM_RESOLVE_ANALYSIS_COMPLETE) {
audit_http_escapes(index);
audit_http_default(index, ts_tree_root_node(tree));
}
cbm_syntax_index_end(&syntax);
if (owned_tree)
ts_tree_delete(owned_tree);
return index;
}
bool cbm_resolve_binding_needs_declared_module(const cbm_resolve_binding_index_t *index) {
return index && index->javascript_module_declared_only;
}
void cbm_resolve_binding_release_source(cbm_resolve_binding_index_t *index) {
if (index) {
index->source = NULL;
index->source_len = 0;
}
}
cbm_resolve_binding_index_t *cbm_resolve_binding_build_declared_module(
CBMArena *arena, const CBMFileResult *result, CBMLanguage language, const char *source,
size_t source_len, bool declared_module) {
return binding_build(arena, result, language, source, source_len, declared_module, false);
}
cbm_resolve_binding_index_t *cbm_resolve_binding_build(CBMArena *arena, const CBMFileResult *result,
CBMLanguage language, const char *source,
size_t source_len) {
return binding_build(arena, result, language, source, source_len, false, false);
}
cbm_resolve_binding_index_t *cbm_resolve_binding_source_index(CBMArena *arena,
const CBMFileResult *result,
CBMLanguage language,
const char *source,
size_t source_len) {
return binding_build(arena, result, language, source, source_len, false, true);
}
static const binding_name_t *find_scoped_name(const cbm_resolve_binding_index_t *index,
const char *name, size_t length, uint32_t scope,
bool *present, js_space_t space) {
if (space != JS_SPACE_ANY) {
size_t top = 0;
const binding_name_t *found = NULL;
bool duplicate = false;
for (size_t r = js_name_range(index, name, length, scope, &top); r < top; r++) {
if (!js_row_in_space(&index->names[r], space))
continue;
duplicate |= found != NULL;
found = &index->names[r];
}
*present = found != NULL;
return duplicate ? NULL : found;
}
size_t low = 0, high = index->name_count;
while (low < high) {
size_t mid = low + (high - low) / 2;
int compare = strncmp(index->names[mid].name, name, length);
if (!compare && index->names[mid].name[length])
compare = 1;
if (!compare)
compare = (index->names[mid].scope > scope) - (index->names[mid].scope < scope);
if (compare < 0)
low = mid + 1;
else
high = mid;
}
*present = low < index->name_count && index->names[low].scope == scope &&
strlen(index->names[low].name) == length &&
!strncmp(index->names[low].name, name, length);
if (!*present)
return NULL;
if (low + 1 < index->name_count && index->names[low + 1].scope == scope &&
!strcmp(index->names[low].name, index->names[low + 1].name))
return NULL;
if (index->language == CBM_LANG_PYTHON && scope == 0 &&
index->names[low].at < index->python_wildcard_barrier)
return NULL;
return &index->names[low];
}
static const binding_name_t *find_name(const cbm_resolve_binding_index_t *index, const char *name,
size_t length) {
bool present;
return find_scoped_name(index, name, length, 0, &present, JS_SPACE_ANY);
}
static void audit_http_escapes(cbm_resolve_binding_index_t *index) {
for (size_t i = 0; i < index->site_count; i++) {
binding_site_t *site = &index->sites[i];
if (!site->http_escape && !site->http_factory_escape)
continue;
binding_name_t *binding =
(binding_name_t *)find_name(index, site->name, strlen(site->name));
if (binding) {
binding->http_escaped |= site->http_escape;
binding->http_factory_escaped |= site->http_factory_escape;
}
}
}
/* HTTP default values are a separate domain from named exports. This
* deliberately small source form has no executable module statements other
* than const initializers, and cannot certify an arbitrary default object. */
static bool http_literal(TSNode node) {
return kind_is(node, "number") || kind_is(node, "string") || kind_is(node, "true") ||
kind_is(node, "false") || kind_is(node, "null");
}
/* ts_node_named_child(node, i) walks from the first child on every call, so an index loop
* is quadratic in the sibling count, and TypeScript's own baselines are files with that many
* top-level statements. One cursor pass snapshots the children. */
static const TSNode *top_level_children(cbm_resolve_binding_index_t *index, TSNode root,
uint32_t *count) {
uint32_t expected = ts_node_named_child_count(root);
TSNode *children =
expected ? cbm_arena_alloc(index->arena, expected * sizeof(*children)) : NULL;
uint32_t found = 0;
if (children) {
TSTreeCursor cursor = ts_tree_cursor_new(root);
if (ts_tree_cursor_goto_first_child(&cursor)) {
do {
TSNode child = ts_tree_cursor_current_node(&cursor);
if (ts_node_is_named(child) && found < expected)
children[found++] = child;
} while (ts_tree_cursor_goto_next_sibling(&cursor));
}
ts_tree_cursor_delete(&cursor);
}
cbm_resolve_stats_add(CBM_RESOLVE_STAT_SYNTAX_CHILD_STEPS, found);
*count = found;
return children;
}
static TSNode http_default_value(const TSNode *children, uint32_t count) {
TSNode value = {0};
for (uint32_t i = 0; i < count; i++) {
TSNode node = children[i];
if (kind_is(node, "comment") || kind_is(node, "import_statement") ||
kind_is(node, "lexical_declaration"))
continue;
if (!kind_is(node, "export_statement") || !child_token(node, "default") ||
!ts_node_is_null(value))
return (TSNode){0};
value = field(node, "value");
if (!kind_is(value, "identifier"))
return (TSNode){0};
}
return value;
}
static TSNode http_default_initializer(cbm_resolve_binding_index_t *index, const TSNode *children,
uint32_t count, const char *name) {
TSNode selected = {0};
for (uint32_t i = 0; i < count; i++) {
TSNode declaration = children[i];
if (!kind_is(declaration, "lexical_declaration"))
continue;
if (!child_token(declaration, "const"))
return (TSNode){0};
uint32_t variables = ts_node_named_child_count(declaration);
for (uint32_t v = 0; v < variables; v++) {
TSNode declarator = ts_node_named_child(declaration, v);
if (kind_is(declarator, "comment"))
continue;
TSNode identifier = field(declarator, "name"), value = field(declarator, "value");
if (!kind_is(declarator, "variable_declarator") || !kind_is(identifier, "identifier"))
return (TSNode){0};
const char *spelling = node_text(index, identifier);
if (spelling && !strcmp(spelling, name)) {
if (!ts_node_is_null(selected))
return (TSNode){0};
selected = value;
} else if (!http_literal(value)) {
return (TSNode){0};
}
}
}
return selected;
}
bool cbm_resolve_binding_http_domain_complete(const cbm_resolve_binding_index_t *index) {
return index && index->analysis == CBM_RESOLVE_ANALYSIS_COMPLETE && !index->http_domain_unknown;
}
size_t cbm_resolve_binding_http_dependency_count(const cbm_resolve_binding_index_t *index) {
return index ? index->http_dependency_count : 0;
}
const cbm_resolve_http_dependency_t *cbm_resolve_binding_http_dependency_at(
const cbm_resolve_binding_index_t *index, size_t offset) {
return index && offset < index->http_dependency_count ? &index->http_dependencies[offset]
: NULL;
}
bool cbm_resolve_binding_http_import_safe(const cbm_resolve_binding_index_t *index,
const char *name, bool axios_factory) {
if (!name || !cbm_resolve_binding_import(index, name))
return false;
const binding_name_t *binding = find_name(index, name, strlen(name));
return binding && binding->http_import_path &&
!(axios_factory ? binding->http_factory_escaped : binding->http_escaped);
}
const char *cbm_resolve_binding_http_import_path(const cbm_resolve_binding_index_t *index,
const char *name) {
if (!cbm_resolve_binding_import(index, name))
return NULL;
const binding_name_t *binding = find_name(index, name, strlen(name));
return binding ? binding->http_import_path : NULL;
}
static bool http_axios_origin(const cbm_resolve_binding_index_t *index, const char *name) {
const char *path = cbm_resolve_binding_http_import_path(index, name);
return path && !strcmp(path, "axios");
}
static bool http_client_base(cbm_resolve_binding_index_t *index, TSNode call, const char **base) {
TSNode args = field(call, "arguments");
if (ts_node_is_null(args) || ts_node_named_child_count(args) != 1)
return false;
TSNode object = ts_node_named_child(args, 0);
if (!kind_is(object, "object"))
return false;
bool seen = false;
uint32_t count = ts_node_named_child_count(object);
for (uint32_t i = 0; i < count; i++) {
TSNode pair = ts_node_named_child(object, i);
if (kind_is(pair, "comment"))
continue;
if (!kind_is(pair, "pair"))
return false;
const char *key = node_text(index, field(pair, "key"));
TSNode value = field(pair, "value");
if (key && !strcmp(key, "timeout") && kind_is(value, "number"))
continue;
if (!key || strcmp(key, "baseURL") || seen)
return false;
seen = true;
if (kind_is(value, "string")) {
const char *text = node_text(index, value);
size_t length = text ? strlen(text) : 0;
if (length >= 2 && !strchr(text, '\\'))
*base = cbm_arena_strndup(index->arena, text + 1, length - 2);
}
}
/* An absent/nonliteral base is unknown, not an empty literal base. */
return true;
}
static void audit_http_default(cbm_resolve_binding_index_t *index, TSNode root) {
uint32_t count = 0;
const TSNode *children = top_level_children(index, root, &count);
TSNode value = http_default_value(children, count);
const char *name = ts_node_is_null(value) ? NULL : node_text(index, value);
const binding_name_t *binding = name ? find_name(index, name, strlen(name)) : NULL;
if (!binding || (binding->flags & BINDING_BLOCKED) ||
!(binding->flags & BINDING_VALUE_DECLARATION) || !binding->target ||
ts_node_start_byte(value) < binding->at)
return;
TSNode call = http_default_initializer(index, children, count, name);
TSNode function = field(call, "function");
TSNode object = field(function, "object"), property = field(function, "property");
const char *receiver = kind_is(object, "identifier") ? node_text(index, object) : NULL;
const char *method = node_text(index, property);
const char *base = NULL;
if (!kind_is(call, "call_expression") || !kind_is(function, "member_expression") || !receiver ||
!method || strcmp(method, "create") || !http_axios_origin(index, receiver) ||
!http_client_base(index, call, &base))
return;
index->http_default_target = binding->target;
index->http_default_base = base;
}
bool cbm_resolve_binding_http_default(const cbm_resolve_binding_index_t *index, const char **target,
const char **base) {
*target = *base = NULL;
if (!index || index->analysis != CBM_RESOLVE_ANALYSIS_COMPLETE || !index->http_default_target)
return false;
*target = index->http_default_target;
*base = index->http_default_base;
return true;
}
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) {
for (unsigned depth = 0; depth < BINDING_MAX_DEPTH; depth++) {
bool present;
const binding_name_t *found = find_scoped_name(index, name, length, scope, &present, space);
/* An ECMAScript block that holds no such name adds nothing to the proof
* beyond this file's own source fact. Blocks nest deeply; the occupied
* scope and the module scope are what the lookup establishes. */
if (space != JS_SPACE_ANY && !present && scope) {
if (scope >= index->scope_count)
return NULL;
scope = index->scopes[scope].parent;
continue;
}
const char *domain = scope < index->scope_count && index->scopes[scope].qualified_name
? index->scopes[scope].qualified_name
: index->result->module_qn;
const char *key = cbm_arena_strndup(index->arena, name, length);
(void)cbm_resolve_lookup_observe(
query, domain, key, found ? (found->target ? found->target : key) : NULL,
found ? CBM_RESOLVE_BINDING_UNIQUE
: present ? CBM_RESOLVE_BINDING_AMBIGUOUS
: CBM_RESOLVE_BINDING_UNRESOLVED,
CBM_RESOLVE_ANALYSIS_COMPLETE, found ? found->flags : 0, true,
!present ? "lexical_scope_key_absent"
: !found ? "lexical_scope_key_ambiguous"
: "lexical_scope_key_observed");
if (present || !scope)
return found;
if (scope >= index->scope_count)
return NULL;
scope = index->scopes[scope].parent;
}
return NULL;
}
/* Query the exact AST occurrence and require its first visible binding to be
* the unique module import. Parameter shadows stop lexical lookup; other
* binders, writes and escapes retain the conservative module-wide veto. */
bool cbm_resolve_binding_import_occurrence(const cbm_resolve_binding_index_t *index,
const CBMCall *call, const char *local_name) {
if (!index || !call || !local_name || !call->callee_name || !call->enclosing_func_qn ||
!call->first_string_arg || index->analysis != CBM_RESOLVE_ANALYSIS_COMPLETE ||
call->source_origin != CBM_SOURCE_ORIGIN_RAW ||
call->site_end_byte <= call->site_start_byte ||
!cbm_resolve_binding_import(index, local_name))
return false;
binding_site_t key = {
.name = call->callee_name, .start = call->site_start_byte, .end = call->site_end_byte};
const binding_site_t *site =
index->sites && index->site_count
? bsearch(&key, index->sites, index->site_count, sizeof(*index->sites), compare_site)
: NULL;
const binding_name_t *binding = find_name(index, local_name, strlen(local_name));
const binding_name_t *visible = site ? find_lexical_name(index, local_name, strlen(local_name),
site->scope, NULL, JS_SPACE_VALUE)
: NULL;
return site && site->invocation && binding && visible == binding && !binding->http_escaped &&
site->http_scope && site->http_url &&
!strcmp(site->http_scope, call->enclosing_func_qn) &&
!strcmp(site->http_url, call->first_string_arg) && call->site_start_byte >= binding->at;
}
const cbm_resolve_js_import_t *cbm_resolve_binding_js_import(
const cbm_resolve_binding_index_t *index, const char *name, uint32_t start, uint32_t end,
CBMSourceOrigin origin, const char **path, bool *type_position) {
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 NULL;
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)
return NULL;
const char *spelled = site->member_path ? site->member_path : name;
if (path)
*path = spelled;
if (type_position)
*type_position = site->type_position;
const char *dot = strchr(spelled, '.');
size_t root = dot ? (size_t)(dot - spelled) : strlen(spelled);
const binding_name_t *binding =
find_lexical_name(index, spelled, root, site->scope, NULL,
site->type_position ? JS_SPACE_TYPE : JS_SPACE_VALUE);
if (!binding || !binding->js_import)
return NULL;
bool type_import = site->type_position && (binding->flags & BINDING_TYPE_IMPORT);
return (binding->flags & BINDING_BLOCKED) && !type_import ? NULL : 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,
uint32_t *end_line) {
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);
/* `this.resolve()`: the member's own function, beside a `var resolve` of the same QN. */
if (site && strchr(name, '.')) {
const js_member_t *row = js_site_member(index, name, site, NULL);
if (!row || !row->function_name || !row->start_line)
return false;
*start_line = row->start_line;
*end_line = row->end_line;
return true;
}
if (!site || site->member_path)
return false;
const binding_name_t *binding =
find_lexical_name(index, name, strlen(name), site->scope, NULL,
site->type_position ? JS_SPACE_TYPE : JS_SPACE_VALUE);
if (!binding || (binding->flags & BINDING_BLOCKED) || !binding->value_start_line)
return false;
*start_line = binding->value_start_line;
*end_line = binding->value_end_line;
return true;
}
bool cbm_resolve_binding_jsx_occurrence(const cbm_resolve_binding_index_t *index, const char *name,
uint32_t start, uint32_t end, CBMSourceOrigin origin) {
if (!index || !name || !*name || !javascript_language(index->language) ||
index->analysis != CBM_RESOLVE_ANALYSIS_COMPLETE || origin != CBM_SOURCE_ORIGIN_RAW ||
!index->sites || !index->site_count || end <= start)
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);
return site && site->jsx_component && site->invocation;
}
/* A function body runs when called, not where it is written, so a module-level
* binder below the def is still the one it sees. Module and class bodies run in
* order, so for them the source position decides. */
static bool python_deferred_site(const cbm_resolve_binding_index_t *index,
const binding_site_t *site, const binding_name_t *binding) {
if (index->language != CBM_LANG_PYTHON)
return false;
uint32_t scope = site->scope;
for (unsigned depth = 0; depth < BINDING_MAX_DEPTH && scope != binding->scope; depth++) {
if (scope >= index->scope_count || !scope)
return false;
if (!index->scopes[scope].class_scope)
return true;
scope = index->scopes[scope].parent;
}
return false;
}
cbm_resolve_result_t cbm_resolve_binding_validate(const cbm_resolve_binding_index_t *index,
const char *name, uint32_t start, uint32_t end,
CBMSourceOrigin origin,
cbm_resolve_result_t candidate) {
cbm_resolve_lookup_t query;
binding_query_begin(index, &query, name, start, end, origin);
uint32_t prior_facts = candidate.facts;
candidate.facts &= ~CBM_RESOLVE_FACT_LOOKUP_COMPLETE;
candidate.analysis = index && index->analysis != CBM_RESOLVE_ANALYSIS_COMPLETE
? index->analysis
: CBM_RESOLVE_ANALYSIS_UNSUPPORTED;
candidate.reason = cbm_resolve_binding_status(index);
if (!index || index->analysis != CBM_RESOLVE_ANALYSIS_COMPLETE || !name || !*name ||
origin != CBM_SOURCE_ORIGIN_RAW || end <= start || !candidate.target ||
candidate.binding != CBM_RESOLVE_BINDING_UNIQUE || candidate.candidate_count != 1 ||
!(prior_facts & CBM_RESOLVE_FACT_EXACT_TARGET)) {
return binding_query_finish(index, &query, candidate);
}
binding_site_t site = {.name = name, .start = start, .end = end};
const binding_site_t *located =
index->sites && index->site_count
? bsearch(&site, index->sites, index->site_count, sizeof(*index->sites), compare_site)
: NULL;
(void)cbm_resolve_lookup_observe(
&query, "source_occurrences", name, located ? name : NULL,
located ? CBM_RESOLVE_BINDING_UNIQUE : CBM_RESOLVE_BINDING_UNRESOLVED,
CBM_RESOLVE_ANALYSIS_COMPLETE, 0, true,
located ? "exact_occurrence_observed" : "exact_occurrence_absent");
if (!located || located->attribute_terminal) {
candidate.reason = "binding_occurrence_missing";
return binding_query_finish(index, &query, candidate);
}
const char *spelled = located->member_path ? located->member_path : name;
const char *dot = strchr(spelled, '.');
size_t root_length = dot ? (size_t)(dot - spelled) : strlen(spelled);
bool ecma = javascript_language(index->language);
js_space_t space = !ecma ? JS_SPACE_ANY
: located->type_position ? JS_SPACE_TYPE
: JS_SPACE_VALUE;
const binding_name_t *binding =
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 &&
(!ecma || located->scope == binding->scope || start >= binding->at_start);
/* Dart library members, and a Python name no scope binds that a closed star
* import of this module provides: the export solver established the target. */
bool implicit_language = index->language == CBM_LANG_DART ||
(index->language == CBM_LANG_PYTHON && index->python_star_count);
bool dart_import = implicit_language && !dot && !binding &&
!cbm_resolve_binding_name_present(index, name) &&
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 && !returned_member) ||
(too_early && !python_deferred_site(index, located, binding)))) {
candidate.reason = "binding_shadowed_or_unavailable";
return binding_query_finish(index, &query, candidate);
}
for (const binding_pending_declaration_t *pending = located->pending; binding && pending;
pending = pending->parent) {
if (binding->scope == pending->scope && !strcmp(binding->name, pending->name)) {
candidate.reason = "binding_declaration_not_yet_bound";
return binding_query_finish(index, &query, candidate);
}
}
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)))) {
candidate.reason = "value_binding_does_not_prove_callable_or_type";
return binding_query_finish(index, &query, candidate);
}
if (dart_import) {
candidate.facts |= CBM_RESOLVE_FACT_SCOPE_BOUND;
} else if (candidate.rule == CBM_RESOLVE_RULE_SCOPE_BINDING && !dot &&
(binding->flags &
(BINDING_DECLARATION | BINDING_VALUE_TYPE | BINDING_VALUE_DECLARATION)) &&
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 || returned_member) &&
(prior_facts & CBM_RESOLVE_FACT_IMPORT_BOUND) &&
(prior_facts & CBM_RESOLVE_FACT_LOOKUP_COMPLETE)) {
candidate.facts |= CBM_RESOLVE_FACT_SCOPE_BOUND;
} else if (ecma && candidate.rule == CBM_RESOLVE_RULE_SCOPE_BINDING && !dot &&
located->type_position && (binding->flags & BINDING_TYPE_DECLARATION) &&
binding->target && strcmp(binding->target, candidate.target) == 0) {
candidate.facts |= CBM_RESOLVE_FACT_SCOPE_BOUND;
} else if (ecma && candidate.rule == CBM_RESOLVE_RULE_SCOPE_BINDING && dot &&
(binding->flags & BINDING_VALUE_TYPE) && binding->target &&
!strncmp(binding->target, candidate.target, strlen(binding->target)) &&
!strcmp(candidate.target + strlen(binding->target), dot)) {
/* `Service.create()`: the exact member of the class or namespace this
* identifier lexically names. */
candidate.facts |= CBM_RESOLVE_FACT_SCOPE_BOUND;
} else {
candidate.reason = "binding_target_or_export_unproven";
return binding_query_finish(index, &query, candidate);
}
if (index->language == CBM_LANG_PYTHON && candidate.rule == CBM_RESOLVE_RULE_SCOPE_BINDING &&
(binding->flags & BINDING_TYPE_DECLARATION) && (binding->flags & BINDING_VALUE_TYPE))
candidate.facts |= CBM_RESOLVE_FACT_CLASS_CALLABLE;
if (ecma && located->invocation && !dot && candidate.rule == CBM_RESOLVE_RULE_SCOPE_BINDING &&
(binding->flags & BINDING_TYPE_DECLARATION) && (binding->flags & BINDING_VALUE_TYPE)) {
if (binding->js_constructor)
candidate.target = binding->js_constructor;
else
candidate.facts |= CBM_RESOLVE_FACT_CLASS_CALLABLE;
}
candidate.facts |= CBM_RESOLVE_FACT_LOOKUP_COMPLETE | CBM_RESOLVE_FACT_OCCURRENCE_EXACT;
candidate.analysis = CBM_RESOLVE_ANALYSIS_COMPLETE;
candidate.reason = "exact_lexical_binding";
return binding_query_finish(index, &query, candidate);
}
cbm_resolve_result_t cbm_resolve_binding_local(const cbm_resolve_binding_index_t *index,
const char *name, uint32_t start, uint32_t end,
CBMSourceOrigin origin) {
cbm_resolve_lookup_t query;
binding_query_begin(index, &query, name, start, end, origin);
cbm_resolve_result_t result = {.reason = "local_binding_unavailable"};
bool ecma = index && javascript_language(index->language);
if (!index || (index->language != CBM_LANG_PYTHON && !ecma) ||
index->analysis != CBM_RESOLVE_ANALYSIS_COMPLETE || !name || strchr(name, '.') ||
origin != CBM_SOURCE_ORIGIN_RAW || end <= start)
return binding_query_finish(index, &query, result);
binding_site_t site = {.name = name, .start = start, .end = end};
const binding_site_t *located =
index->sites && index->site_count
? bsearch(&site, index->sites, index->site_count, sizeof(*index->sites), compare_site)
: NULL;
(void)cbm_resolve_lookup_observe(
&query, "source_occurrences", name, located ? name : NULL,
located ? CBM_RESOLVE_BINDING_UNIQUE : CBM_RESOLVE_BINDING_UNRESOLVED,
CBM_RESOLVE_ANALYSIS_COMPLETE, 0, true,
located ? "exact_occurrence_observed" : "exact_occurrence_absent");
js_space_t space = !ecma || !located ? JS_SPACE_ANY
: located->type_position ? JS_SPACE_TYPE
: JS_SPACE_VALUE;
const binding_name_t *binding =
located && !located->member_path && !located->attribute_terminal
? find_lexical_name(index, name, strlen(name), located->scope, &query, space)
: NULL;
unsigned local_kinds = BINDING_DECLARATION | BINDING_VALUE_TYPE;
if (ecma)
local_kinds |= located && located->type_position ? BINDING_TYPE_DECLARATION
: BINDING_VALUE_DECLARATION;
else
local_kinds |= BINDING_VALUE_DECLARATION;
if (!binding || !binding->target || (binding->flags & BINDING_BLOCKED) ||
!(binding->flags & local_kinds))
return binding_query_finish(index, &query, result);
result.binding = CBM_RESOLVE_BINDING_UNIQUE;
result.domain = CBM_RESOLVE_DOMAIN_PROJECT;
result.rule = CBM_RESOLVE_RULE_SCOPE_BINDING;
result.facts = CBM_RESOLVE_FACT_EXACT_TARGET | CBM_RESOLVE_FACT_SCOPE_BOUND;
result.target = binding->target;
result.candidate_count = 1;
return cbm_resolve_binding_validate(index, name, start, end, origin, result);
}
bool cbm_resolve_binding_js_type_occurrence(const cbm_resolve_binding_index_t *index,
const char *name, uint32_t start, uint32_t end,
CBMSourceOrigin origin) {
if (!index || !name || origin != CBM_SOURCE_ORIGIN_RAW || !index->sites ||
!javascript_language(index->language))
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);
return site && (site->type_position || site->type_path_root);
}
/* The name is `this` at every declaration of it in its scope, and nothing writes it. */
static bool js_name_is_this(const cbm_resolve_binding_index_t *index, const char *name,
size_t length, const binding_name_t *binding) {
size_t high = 0;
size_t low = js_name_range(index, name, length, binding->scope, &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->js_this_owner || strcmp(row->js_this_owner, binding->js_this_owner) ||
(row->flags & BINDING_WRITTEN)))
return false;
}
return true;
}
/* The module-level object `this.m` or `owner.m` names at this site, or NULL. */
static const char *js_site_member_owner(const cbm_resolve_binding_index_t *index, const char *name,
const binding_site_t *site, cbm_resolve_lookup_t *query,
bool *prototype) {
const char *dot = strchr(name, '.');
size_t root = (size_t)(dot - name);
const char *owner = NULL;
if (prototype)
*prototype = false;
if (root == 4 && !strncmp(name, "this", 4)) {
owner = site->this_owner;
if (prototype)
*prototype = site->this_prototype;
} else {
const binding_name_t *binding =
find_lexical_name(index, name, root, site->scope, query, JS_SPACE_VALUE);
if (binding && binding->js_this_owner && js_name_is_this(index, name, root, binding)) {
owner = binding->js_this_owner;
if (prototype)
*prototype = binding->js_this_prototype;
} else if (binding && binding->scope == 0 && !binding->js_import) {
owner = binding->name;
}
}
if (!owner)
return NULL;
bool conflict = false;
const binding_name_t *holder = js_module_row(index, owner, JS_SPACE_VALUE, &conflict);
if (!holder || conflict || (holder->flags & (BINDING_BLOCKED | BINDING_WRITTEN)) ||
holder->js_import)
return NULL;
return owner;
}
/* That object's member row, when nothing could have replaced every member. */
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 prototype = false;
const char *owner = js_site_member_owner(index, name, site, query, &prototype);
const char *dot = strchr(name, '.');
if (!owner || !index->js_member_count || strchr(dot + 1, '.'))
return NULL;
js_member_t probe = {.owner = owner, .member = dot + 1, .prototype = prototype};
const js_member_t *row = bsearch(&probe, index->js_members, index->js_member_count,
sizeof(*index->js_members), js_compare_member);
js_member_t computed = {.owner = owner, .member = JS_MEMBER_COMPUTED, .prototype = prototype};
if (row && bsearch(&computed, index->js_members, index->js_member_count,
sizeof(*index->js_members), js_compare_member))
return NULL;
return row;
}
cbm_resolve_result_t cbm_resolve_binding_js_member(const cbm_resolve_binding_index_t *index,
const char *name, uint32_t start, uint32_t end,
CBMSourceOrigin origin) {
cbm_resolve_lookup_t query;
binding_query_begin(index, &query, name, start, end, origin);
cbm_resolve_result_t result = {.reason = "js_member_unavailable"};
const char *dot = name ? strchr(name, '.') : NULL;
if (!index || !javascript_language(index->language) ||
index->analysis != CBM_RESOLVE_ANALYSIS_COMPLETE || !dot || strchr(dot + 1, '.') ||
origin != CBM_SOURCE_ORIGIN_RAW || end <= start || !index->sites || !index->site_count)
return binding_query_finish(index, &query, result);
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->invocation)
return binding_query_finish(index, &query, result);
const char *owner = js_site_member_owner(index, name, site, &query, NULL);
if (!owner)
return binding_query_finish(index, &query, result);
const js_member_t *row = js_site_member(index, name, site, NULL);
(void)cbm_resolve_lookup_observe(
&query, owner, dot + 1, row ? row->target : NULL,
row && row->target ? CBM_RESOLVE_BINDING_UNIQUE : CBM_RESOLVE_BINDING_UNRESOLVED,
CBM_RESOLVE_ANALYSIS_COMPLETE, 0, true,
row && row->target ? "js_member_declaration_observed" : "js_member_declaration_absent");
if (!row || !row->target || row->conflict)
return binding_query_finish(index, &query, result);
result = (cbm_resolve_result_t){
.binding = CBM_RESOLVE_BINDING_UNIQUE,
.domain = CBM_RESOLVE_DOMAIN_PROJECT,
.analysis = CBM_RESOLVE_ANALYSIS_COMPLETE,
.rule = CBM_RESOLVE_RULE_SCOPE_BINDING,
.facts = CBM_RESOLVE_FACT_EXACT_TARGET | CBM_RESOLVE_FACT_SCOPE_BOUND |
CBM_RESOLVE_FACT_LOOKUP_COMPLETE | CBM_RESOLVE_FACT_OCCURRENCE_EXACT,
.target = row->target,
.candidate_count = 1,
.reason = "js_member_declaration"};
return binding_query_finish(index, &query, result);
}
static uint32_t source_rule_fact(cbm_resolve_rule_t rule) {
switch (rule) {
case CBM_RESOLVE_RULE_SCOPE_BINDING:
return CBM_RESOLVE_FACT_SCOPE_BOUND;
case CBM_RESOLVE_RULE_IMPORT_BINDING:
return CBM_RESOLVE_FACT_IMPORT_BOUND;
case CBM_RESOLVE_RULE_TYPE_MEMBER:
return CBM_RESOLVE_FACT_RECEIVER_TYPED;
case CBM_RESOLVE_RULE_LANGUAGE_BINDING:
return CBM_RESOLVE_FACT_LANGUAGE_BOUND;
default:
return 0;
}
}
cbm_resolve_result_t cbm_resolve_binding_source_site(const cbm_resolve_binding_index_t *index,
const char *name, uint32_t start, uint32_t end,
CBMSourceOrigin origin, const char *domain,
const char *target, cbm_resolve_rule_t rule,
uint32_t facts, const char *decision) {
cbm_resolve_lookup_t query;
binding_query_begin(index, &query, name, start, end, origin);
uint32_t fact = source_rule_fact(rule);
cbm_resolve_result_t result = {.reason = decision};
if (!index || index->analysis != CBM_RESOLVE_ANALYSIS_COMPLETE || !name || !*name ||
origin != CBM_SOURCE_ORIGIN_RAW || end <= start || !target || !*target || !fact)
return binding_query_finish(index, &query, result);
(void)cbm_resolve_lookup_observe(&query, "source_occurrences", name, name,
CBM_RESOLVE_BINDING_UNIQUE, CBM_RESOLVE_ANALYSIS_COMPLETE, 0,
true, "exact_occurrence_observed");
(void)cbm_resolve_lookup_observe(&query, domain ? domain : "source_adapter", name, target,
CBM_RESOLVE_BINDING_UNIQUE, CBM_RESOLVE_ANALYSIS_COMPLETE, 0,
true, decision);
result.binding = CBM_RESOLVE_BINDING_UNIQUE;
result.domain = CBM_RESOLVE_DOMAIN_PROJECT;
result.analysis = CBM_RESOLVE_ANALYSIS_COMPLETE;
result.rule = rule;
result.facts = CBM_RESOLVE_FACT_EXACT_TARGET | CBM_RESOLVE_FACT_LOOKUP_COMPLETE |
CBM_RESOLVE_FACT_OCCURRENCE_EXACT | fact | facts;
result.target = target;
result.candidate_count = 1;
return binding_query_finish(index, &query, result);
}
const char *cbm_resolve_binding_status(const cbm_resolve_binding_index_t *index) {
return index ? index->reason : "binding_allocation_failed";
}
unsigned cbm_resolve_binding_export_kind(const cbm_resolve_binding_index_t *index,
const char *qualified_name) {
if (!index || index->analysis != CBM_RESOLVE_ANALYSIS_COMPLETE || !qualified_name ||
(index->language != CBM_LANG_PYTHON && index->language != CBM_LANG_DART &&
index->language != CBM_LANG_RUST && !javascript_language(index->language))) {
return 0;
}
const char *name = strrchr(qualified_name, '.');
if (!name || !name[1]) {
return 0;
}
name++;
if (index->language == CBM_LANG_DART && name[0] == '_')
return 0;
if (javascript_language(index->language)) {
/* `export const Info` and `export type Info` share one qualified name. */
size_t top = 0;
unsigned merged = 0;
for (size_t r = js_name_range(index, name, strlen(name), 0, &top); r < top; r++) {
const binding_name_t *row = &index->names[r];
if (!row->target || strcmp(row->target, qualified_name) ||
(row->flags & BINDING_BLOCKED) ||
!(row->flags & (BINDING_EXPORTED | BINDING_TYPE_EXPORTED)))
return 0;
if ((row->flags &
(BINDING_DECLARATION | BINDING_VALUE_TYPE | BINDING_VALUE_DECLARATION)) &&
(row->flags & BINDING_EXPORTED))
merged |= CBM_RESOLVE_EXPORT_VALUE;
if (row->flags & BINDING_TYPE_DECLARATION)
merged |= CBM_RESOLVE_EXPORT_TYPE;
}
return merged;
}
const binding_name_t *binding = find_name(index, name, strlen(name));
if (!binding || !binding->target || (binding->flags & BINDING_BLOCKED) ||
strcmp(binding->target, qualified_name) != 0 ||
((javascript_language(index->language) || index->language == CBM_LANG_RUST) &&
!(binding->flags & (BINDING_EXPORTED | BINDING_TYPE_EXPORTED)))) {
return 0;
}
unsigned kind = 0;
if ((binding->flags & (BINDING_DECLARATION | BINDING_VALUE_TYPE | BINDING_VALUE_DECLARATION)) &&
(index->language == CBM_LANG_PYTHON || index->language == CBM_LANG_DART ||
(binding->flags & BINDING_EXPORTED))) {
kind |= CBM_RESOLVE_EXPORT_VALUE;
}
if (binding->flags & BINDING_TYPE_DECLARATION) {
kind |= CBM_RESOLVE_EXPORT_TYPE;
}
return kind;
}
bool cbm_resolve_binding_import(const cbm_resolve_binding_index_t *index, const char *local_name) {
if (!index || index->analysis != CBM_RESOLVE_ANALYSIS_COMPLETE || !local_name)
return false;
const binding_name_t *binding = find_name(index, local_name, strlen(local_name));
return binding && binding->flags == BINDING_IMPORT;
}
bool cbm_resolve_binding_export(const cbm_resolve_binding_index_t *index,
const char *qualified_name) {
return cbm_resolve_binding_export_kind(index, qualified_name) != 0;
}
bool cbm_resolve_binding_name_present(const cbm_resolve_binding_index_t *index, const char *name) {
if (!index || !name)
return false;
size_t low = 0, high = index->name_count;
while (low < high) {
size_t middle = low + (high - low) / 2;
if (strcmp(index->names[middle].name, name) < 0)
low = middle + 1;
else
high = middle;
}
return low < index->name_count && strcmp(index->names[low].name, name) == 0;
}
bool cbm_resolve_binding_python_effects_complete(const cbm_resolve_binding_index_t *index) {
return index && index->language == CBM_LANG_PYTHON && index->python_effects_complete;
}
bool cbm_resolve_binding_python_import_write(const cbm_resolve_binding_index_t *index,
const char *name, size_t offset, const char **member) {
*member = NULL;
if (!index || !name || index->python_writes_unseen)
return offset == 0;
size_t low = 0, high = index->python_write_count;
while (low < high) {
size_t middle = low + (high - low) / 2;
if (strcmp(index->python_writes[middle].name, name) < 0)
low = middle + 1;
else
high = middle;
}
if (offset >= index->python_write_count - low ||
strcmp(index->python_writes[low + offset].name, name))
return false;
*member = index->python_writes[low + offset].member;
return true;
}
/* A completed audit saw every statement that binds at module level, including
* imports and conditionals, so the name table is that namespace's static domain.
* Module __getattr__ answers any absent name; a star import is judged later by
* the shared resolver, which knows what its source module provides. */
bool cbm_resolve_binding_python_namespace_closed(const cbm_resolve_binding_index_t *index) {
bool present = false;
if (!index || index->language != CBM_LANG_PYTHON ||
index->analysis != CBM_RESOLVE_ANALYSIS_COMPLETE)
return false;
(void)find_scoped_name(index, "__getattr__", strlen("__getattr__"), 0, &present, JS_SPACE_ANY);
return !present && !index->python_namespace_open;
}
/* Names this module binds nothing for can still be there at run time: __getattr__, a computed
* store into globals(), or handing its own __name__ on (celery's recreate_module,
* lazy_loader.attach). */
bool cbm_resolve_binding_python_names_come_at_run_time(const cbm_resolve_binding_index_t *index) {
return index &&
(!cbm_resolve_binding_python_namespace_closed(index) || index->python_name_handed);
}
size_t cbm_resolve_binding_python_namespace_size(const cbm_resolve_binding_index_t *index) {
return index && index->language == CBM_LANG_PYTHON ? index->name_count : 0;
}
const char *cbm_resolve_binding_python_namespace_name(const cbm_resolve_binding_index_t *index,
size_t offset, uint32_t *at) {
if (!index || index->language != CBM_LANG_PYTHON || offset >= index->name_count ||
index->names[offset].scope != 0)
return NULL;
if (at)
*at = index->names[offset].at;
return index->names[offset].name;
}
size_t cbm_resolve_binding_python_class_count(const cbm_resolve_binding_index_t *index) {
return index && index->language == CBM_LANG_PYTHON ? index->python_class_count : 0;
}
const cbm_resolve_python_class_t *cbm_resolve_binding_python_class_at(
const cbm_resolve_binding_index_t *index, size_t offset) {
return offset < cbm_resolve_binding_python_class_count(index) ? &index->python_classes[offset]
: NULL;
}
size_t cbm_resolve_binding_python_member_count(const cbm_resolve_binding_index_t *index) {
return index && index->language == CBM_LANG_PYTHON ? index->python_member_count : 0;
}
const cbm_resolve_python_member_t *cbm_resolve_binding_python_member_at(
const cbm_resolve_binding_index_t *index, size_t offset) {
return offset < cbm_resolve_binding_python_member_count(index) ? &index->python_members[offset]
: NULL;
}
bool cbm_resolve_binding_python_site(const cbm_resolve_binding_index_t *index, const char *name,
uint32_t start, uint32_t end, CBMSourceOrigin origin,
const char **chain, uint32_t *scope) {
if (!index || index->language != CBM_LANG_PYTHON ||
index->analysis != CBM_RESOLVE_ANALYSIS_COMPLETE || !name || !index->site_count ||
origin != CBM_SOURCE_ORIGIN_RAW || end <= start)
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->chain)
return false;
*chain = site->chain;
*scope = site->scope;
return true;
}
const char *cbm_resolve_binding_python_method_owner(const cbm_resolve_binding_index_t *index,
uint32_t scope) {
if (!index || scope >= index->scope_count || !index->scopes[scope].method_of)
return NULL;
return index->scopes[index->scopes[scope].method_of - 1].qualified_name;
}
static int python_narrow_compare(const void *left, const void *right) {
const python_narrow_t *a = left, *b = right;
if (a->from != b->from)
return a->from < b->from ? -1 : 1;
return a->until == b->until ? 0 : a->until > b->until ? -1 : 1;
}
static int python_store_compare(const void *left, const void *right) {
const python_store_t *a = left, *b = right;
int order = strcmp(a->chain, b->chain);
if (order)
return order;
return a->at == b->at ? 0 : a->at < b->at ? -1 : 1;
}
/* Sorted once after the walk. Regions nest or are disjoint, so one pass with the chain of open
* regions finds each one's parent, and a site reaches its enclosing regions by those links. */
static void python_finish_narrows(cbm_resolve_binding_index_t *index) {
if (index->python_narrow_count > 1)
qsort(index->python_narrows, index->python_narrow_count, sizeof(*index->python_narrows),
python_narrow_compare);
if (index->python_store_count > 1)
qsort(index->python_stores, index->python_store_count, sizeof(*index->python_stores),
python_store_compare);
uint32_t open = 0;
for (size_t i = 0; i < index->python_narrow_count; i++) {
python_narrow_t *narrow = &index->python_narrows[i];
while (open && index->python_narrows[open - 1].until < narrow->from)
open = index->python_narrows[open - 1].parent;
narrow->parent = open;
open = (uint32_t)i + 1;
}
}
/* Any store of exactly this attribute chain between two positions. */
static bool python_attribute_stored(const cbm_resolve_binding_index_t *index, const char *chain,
uint32_t from, uint32_t to) {
size_t low = 0, high = index->python_store_count;
while (low < high) {
size_t mid = low + (high - low) / 2;
int order = strcmp(index->python_stores[mid].chain, chain);
if (order < 0 || (!order && index->python_stores[mid].at < from))
low = mid + 1;
else
high = mid;
}
return low < index->python_store_count && !strcmp(index->python_stores[low].chain, chain) &&
index->python_stores[low].at <= to;
}
/* Any binder of this local between two positions. */
static bool python_local_rebound(const cbm_resolve_binding_index_t *index, const char *name,
size_t length, uint32_t scope, uint32_t from, uint32_t to) {
size_t low = 0, high = index->name_count;
while (low < high) {
size_t mid = low + (high - low) / 2;
int compare = strncmp(index->names[mid].name, name, length);
if (!compare && index->names[mid].name[length])
compare = 1;
if (!compare)
compare = (index->names[mid].scope > scope) - (index->names[mid].scope < scope);
if (compare < 0)
low = mid + 1;
else
high = mid;
}
for (size_t i = low;
i < index->name_count && index->names[i].scope == scope &&
strlen(index->names[i].name) == length && !strncmp(index->names[i].name, name, length);
i++)
if (index->names[i].at >= from && index->names[i].at <= to)
return true;
return false;
}
/* A store of this exact chain finished before the site, in the scope holding the site or one
* around it. Scopes nest as text does, so when the site's own scope has such a store the nearest
* store before the site lies in it or inside it: one search, two walks up the nesting. */
static const python_store_t *python_stored_earlier(const cbm_resolve_binding_index_t *index,
const char *chain, size_t length, uint32_t scope,
uint32_t site) {
size_t low = 0, high = index->python_store_count;
while (low < high) {
size_t mid = low + (high - low) / 2;
const python_store_t *store = &index->python_stores[mid];
int order = strncmp(store->chain, chain, length);
if (!order && store->chain[length])
order = 1;
if (order < 0 || (!order && store->at <= site))
low = mid + 1;
else
high = mid;
}
if (!low)
return NULL;
const python_store_t *store = &index->python_stores[low - 1];
if (strncmp(store->chain, chain, length) || store->chain[length])
return NULL;
uint32_t inside = store->scope, around = scope;
for (unsigned depth = 0; depth < BINDING_MAX_DEPTH; depth++) {
if (inside == scope || around == store->scope)
return store;
if (!inside && !around)
return NULL;
inside = inside ? index->scopes[inside].parent : 0;
around = around ? index->scopes[around].parent : 0;
}
return NULL;
}
/* `t.x = make()` says what t.x holds from there on, whatever class t.x is declared in, for what
* is read through it: t.x itself stays the member it names. Stores through a method's own first
* parameter are read as the class's instance attributes, so only what lies past its first
* attribute counts for that root. The longest such prefix of the chain answers. */
bool cbm_resolve_binding_python_store_precedes(const cbm_resolve_binding_index_t *index,
const char *chain, uint32_t scope, uint32_t site,
size_t *prefix, const char **value,
uint32_t *value_at, uint32_t *value_scope) {
if (!index || !index->python_store_count)
return false;
size_t at = strcspn(chain, ".([@");
cbm_resolve_python_root_kind_t root =
cbm_resolve_binding_python_root(index, chain, at, scope, site).kind;
bool own = root == CBM_PY_ROOT_SELF_PARAMETER || root == CBM_PY_ROOT_CLASS_PARAMETER;
bool found = false;
for (bool first = true; chain[at] == '.'; first = false) {
at += 1 + strcspn(chain + at + 1, ".([@");
const python_store_t *store = chain[at] && !(first && own)
? python_stored_earlier(index, chain, at, scope, site)
: NULL;
if (store) {
found = true;
*prefix = at;
*value = store->value;
*value_at = store->value_at;
*value_scope = store->scope;
}
}
return found;
}
const char *cbm_resolve_binding_python_narrowed(const cbm_resolve_binding_index_t *index,
const char *chain, uint32_t scope, uint32_t site,
size_t *consumed) {
if (!index || !index->python_narrow_count)
return NULL;
size_t low = 0, high = index->python_narrow_count;
while (low < high) {
size_t mid = low + (high - low) / 2;
if (index->python_narrows[mid].from <= site)
low = mid + 1;
else
high = mid;
}
for (uint32_t at = (uint32_t)low; at; at = index->python_narrows[at - 1].parent) {
const python_narrow_t *narrow = &index->python_narrows[at - 1];
size_t length = strlen(narrow->chain);
if (narrow->until < site || narrow->scope != scope ||
strncmp(chain, narrow->chain, length) ||
(chain[length] && !strchr(".([@", chain[length])))
continue;
size_t root = strcspn(narrow->chain, ".([@");
if (python_local_rebound(index, narrow->chain, root, scope, narrow->from, site) ||
(narrow->chain[root] &&
python_attribute_stored(index, narrow->chain, narrow->from, site)))
return NULL;
*consumed = length;
return narrow->type;
}
return NULL;
}
/* A later binder inside a loop that holds the site but began after this binder comes back
* round to the site without passing it. Nesting is what bounds the hops. */
static bool python_loop_comes_back(const cbm_resolve_binding_index_t *index, uint32_t binder,
uint32_t site, uint32_t later) {
size_t low = 0, high = index->python_loop_count;
while (low < high) {
size_t mid = low + (high - low) / 2;
if (index->python_loops[mid].start <= site)
low = mid + 1;
else
high = mid;
}
for (uint32_t at = (uint32_t)low; at;) {
const python_loop_t *loop = &index->python_loops[at - 1];
if (loop->start <= binder)
return false;
if (loop->end >= site && loop->end >= later)
return true;
at = loop->parent;
}
return false;
}
cbm_resolve_python_flow_t cbm_resolve_binding_python_reaching(
const cbm_resolve_binding_index_t *index, const char *name, size_t length, uint32_t scope,
uint32_t site, const char **chain, uint32_t *at, uint32_t *chain_scope, bool *typed) {
if (!index || index->language != CBM_LANG_PYTHON ||
index->analysis != CBM_RESOLVE_ANALYSIS_COMPLETE || !scope || scope >= index->scope_count ||
index->scopes[scope].class_scope)
return CBM_PY_FLOW_NONE;
size_t low = 0, high = index->name_count;
while (low < high) {
size_t mid = low + (high - low) / 2;
int compare = strncmp(index->names[mid].name, name, length);
if (!compare && index->names[mid].name[length])
compare = 1;
if (!compare)
compare = (index->names[mid].scope > scope) - (index->names[mid].scope < scope);
if (compare < 0)
low = mid + 1;
else
high = mid;
}
const binding_name_t *best = NULL, *later = NULL;
size_t count = 0;
for (; low + count < index->name_count && index->names[low + count].scope == scope &&
strlen(index->names[low + count].name) == length &&
!strncmp(index->names[low + count].name, name, length);
count++) {
const binding_name_t *binder = &index->names[low + count];
uint32_t from = binder->value_from ? binder->value_from : binder->at;
if (from <= site) {
if (!best || binder->at > best->at)
best = binder;
} else if (!later || binder->at < later->at) {
later = binder;
}
if (count > 64)
return CBM_PY_FLOW_NONE;
}
if (count < 2 || !best || best->value_until < site ||
(later && python_loop_comes_back(index, best->at, site, later->at)))
return CBM_PY_FLOW_NONE;
if (best->flags != BINDING_BLOCKED || best->value_none || !best->value_chain ||
(best->value_kind != CBM_PY_ROOT_ASSIGNED && best->value_kind != CBM_PY_ROOT_TYPED))
return CBM_PY_FLOW_UNKNOWN;
*chain = best->value_chain;
*at = best->at;
*chain_scope = best->value_scope;
*typed = best->value_kind == CBM_PY_ROOT_TYPED;
return CBM_PY_FLOW_VALUE;
}
/* A local rebound only by plain assignments: x = None, then x = Store(). A None
* says nothing about the class a member read finds. Anything else that binds
* the name leaves it unknown. */
static cbm_resolve_python_root_t python_assigned_root(const cbm_resolve_binding_index_t *index,
const char *name, size_t length,
uint32_t scope) {
cbm_resolve_python_root_t root = {.kind = CBM_PY_ROOT_UNKNOWN};
size_t low = 0, high = index->name_count;
while (low < high) {
size_t mid = low + (high - low) / 2;
int compare = strncmp(index->names[mid].name, name, length);
if (!compare && index->names[mid].name[length])
compare = 1;
if (!compare)
compare = (index->names[mid].scope > scope) - (index->names[mid].scope < scope);
if (compare < 0)
low = mid + 1;
else
high = mid;
}
size_t count = 0;
while (low + count < index->name_count && index->names[low + count].scope == scope &&
strlen(index->names[low + count].name) == length &&
!strncmp(index->names[low + count].name, name, length))
count++;
if (count < 2 || count > 16 || !scope)
return root;
const char **chains = cbm_arena_alloc(index->arena, count * sizeof(*chains));
if (!chains)
return root;
uint32_t used = 0;
for (size_t i = 0; i < count; i++) {
const binding_name_t *binder = &index->names[low + i];
if (binder->flags != BINDING_BLOCKED || binder->value_kind != CBM_PY_ROOT_ASSIGNED)
return root;
if (binder->value_none)
continue;
if (!binder->value_chain)
return root;
chains[used++] = binder->value_chain;
}
root.kind = CBM_PY_ROOT_ASSIGNED_MANY;
root.chains = chains;
root.chain_count = used;
root.chain_scope = scope;
return root;
}
cbm_resolve_python_root_t cbm_resolve_binding_python_root(const cbm_resolve_binding_index_t *index,
const char *name, size_t length,
uint32_t scope, uint32_t at) {
cbm_resolve_python_root_t root = {.kind = CBM_PY_ROOT_UNKNOWN};
if (!index || index->language != CBM_LANG_PYTHON ||
index->analysis != CBM_RESOLVE_ANALYSIS_COMPLETE)
return root;
uint32_t current = scope;
for (unsigned depth = 0; depth < BINDING_MAX_DEPTH; depth++) {
bool present = false;
const binding_name_t *found =
find_scoped_name(index, name, length, current, &present, JS_SPACE_ANY);
if (present) {
if (!found)
return python_assigned_root(index, name, length, current);
if (at && found->at > at)
return root;
root.at = found->at;
if (found->flags & BINDING_IMPORT) {
root.kind = CBM_PY_ROOT_IMPORT;
root.nested =
found->nested_import ? &index->python_nested[found->nested_import - 1] : NULL;
} else if ((found->flags & BINDING_BLOCKED) && found->nested_import &&
index->python_nested[found->nested_import - 1].annotation_only) {
root.kind = CBM_PY_ROOT_ANNOTATION_IMPORT;
root.nested = &index->python_nested[found->nested_import - 1];
} else if (found->flags & BINDING_DECORATED) {
/* py_members decides whether the decorators keep this identity. */
root.kind = CBM_PY_ROOT_DECLARATION;
root.target = found->target;
} else if (found->flags & BINDING_BLOCKED) {
root.kind = found->value_kind ? found->value_kind : CBM_PY_ROOT_UNKNOWN;
root.chain = found->value_chain;
root.chain_scope = found->value_scope;
if (found->value_kind == CBM_PY_ROOT_SELF_PARAMETER ||
found->value_kind == CBM_PY_ROOT_CLASS_PARAMETER)
root.owner = cbm_resolve_binding_python_method_owner(index, current);
} else if (found->flags & BINDING_VALUE_DECLARATION) {
root.kind = CBM_PY_ROOT_MODULE_VALUE;
root.target = found->target;
root.chain = found->value_chain;
root.chain_scope = found->value_scope;
root.value_typed = found->value_kind == CBM_PY_ROOT_TYPED;
} else if (found->flags & (BINDING_DECLARATION | BINDING_VALUE_TYPE)) {
root.kind = CBM_PY_ROOT_DECLARATION;
root.target = found->target;
}
return root;
}
if (!current || current >= index->scope_count)
break;
current = index->scopes[current].parent;
}
root.kind = CBM_PY_ROOT_UNBOUND;
return root;
}
/* Attach the audited member selection to this exact occurrence. The caller
* established the class and MRO facts; this records them with the lexical root
* that the receiver came from. */
cbm_resolve_result_t cbm_resolve_binding_python_member(const cbm_resolve_binding_index_t *index,
const char *name, uint32_t start,
uint32_t end, CBMSourceOrigin origin,
const char *owner, const char *target,
bool class_target, bool property_read) {
cbm_resolve_lookup_t query;
binding_query_begin(index, &query, name, start, end, origin);
(void)cbm_resolve_lookup_observe(&query, "source_occurrences", name, name,
CBM_RESOLVE_BINDING_UNIQUE, CBM_RESOLVE_ANALYSIS_COMPLETE, 0,
true, "exact_occurrence_observed");
(void)cbm_resolve_lookup_observe(&query, owner, name, target, CBM_RESOLVE_BINDING_UNIQUE,
CBM_RESOLVE_ANALYSIS_COMPLETE, 0, true,
"python_receiver_member_selected");
cbm_resolve_result_t result = {
.binding = CBM_RESOLVE_BINDING_UNIQUE,
.domain = CBM_RESOLVE_DOMAIN_PROJECT,
.analysis = CBM_RESOLVE_ANALYSIS_COMPLETE,
.rule = CBM_RESOLVE_RULE_TYPE_MEMBER,
.facts = CBM_RESOLVE_FACT_EXACT_TARGET | CBM_RESOLVE_FACT_LOOKUP_COMPLETE |
CBM_RESOLVE_FACT_RECEIVER_TYPED | CBM_RESOLVE_FACT_OCCURRENCE_EXACT |
(class_target ? CBM_RESOLVE_FACT_CLASS_CALLABLE : 0),
.target = target,
.candidate_count = 1,
.reason = property_read ? CBM_RESOLVE_PYTHON_PROPERTY_READ : "python_receiver_member"};
return binding_query_finish(index, &query, result);
}
size_t cbm_resolve_binding_python_decorated_count(const cbm_resolve_binding_index_t *index) {
return index && index->language == CBM_LANG_PYTHON ? index->python_decorated_count : 0;
}
const cbm_resolve_python_decorated_t *cbm_resolve_binding_python_decorated_at(
const cbm_resolve_binding_index_t *index, size_t offset) {
return offset < cbm_resolve_binding_python_decorated_count(index)
? &index->python_decorated[offset]
: NULL;
}
void cbm_resolve_binding_python_keep_decorated(cbm_resolve_binding_index_t *index,
const cbm_resolve_python_decorated_t *item) {
bool present = false;
if (!index || !item)
return;
binding_name_t *binding = (binding_name_t *)find_scoped_name(
index, item->name, strlen(item->name), item->scope, &present, JS_SPACE_ANY);
if (binding && (binding->flags & BINDING_DECORATED))
binding->flags =
item->is_class ? BINDING_TYPE_DECLARATION | BINDING_VALUE_TYPE : BINDING_DECLARATION;
}
bool cbm_resolve_binding_python_import_is(const cbm_resolve_binding_index_t *index,
const char *chain, const char *canonical) {
const char *spelled = index && chain ? python_import_canonical(index, chain) : NULL;
return spelled && !strcmp(spelled, canonical);
}
size_t cbm_resolve_binding_python_star_count(const cbm_resolve_binding_index_t *index) {
return index && index->language == CBM_LANG_PYTHON ? index->python_star_count : 0;
}
const cbm_resolve_python_star_t *cbm_resolve_binding_python_star_at(
const cbm_resolve_binding_index_t *index, size_t offset) {
return offset < cbm_resolve_binding_python_star_count(index) ? &index->python_stars[offset]
: NULL;
}
void cbm_resolve_binding_python_set_star_barrier(cbm_resolve_binding_index_t *index,
uint32_t barrier) {
if (index && index->language == CBM_LANG_PYTHON)
index->python_wildcard_barrier = barrier;
}
size_t cbm_resolve_binding_python_nested_count(const cbm_resolve_binding_index_t *index) {
return index && index->language == CBM_LANG_PYTHON ? index->python_nested_count : 0;
}
const cbm_resolve_python_nested_import_t *cbm_resolve_binding_python_nested_at(
const cbm_resolve_binding_index_t *index, size_t offset) {
return offset < cbm_resolve_binding_python_nested_count(index) ? &index->python_nested[offset]
: NULL;
}
bool cbm_resolve_binding_python_nested_audited(const cbm_resolve_binding_index_t *index,
const cbm_resolve_python_nested_import_t *import) {
bool present = false;
if (!index || !import || index->analysis != CBM_RESOLVE_ANALYSIS_COMPLETE)
return false;
const char *name = import->import.local_name;
const binding_name_t *binding =
find_scoped_name(index, name, strlen(name), import->scope, &present, JS_SPACE_ANY);
return binding && binding->nested_import &&
&index->python_nested[binding->nested_import - 1] == import;
}
const cbm_resolve_python_nested_import_t *cbm_resolve_binding_python_nested_site(
const cbm_resolve_binding_index_t *index, const char *name, uint32_t start, uint32_t end,
CBMSourceOrigin origin) {
if (!index || index->language != CBM_LANG_PYTHON || !index->python_nested_count ||
index->analysis != CBM_RESOLVE_ANALYSIS_COMPLETE || !name || !index->site_count ||
origin != CBM_SOURCE_ORIGIN_RAW)
return NULL;
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);
const char *dot = strchr(name, '.');
size_t root = dot ? (size_t)(dot - name) : strlen(name);
const binding_name_t *binding =
site && !site->attribute_terminal
? find_lexical_name(index, name, root, site->scope, NULL, JS_SPACE_ANY)
: NULL;
return binding && binding->flags == BINDING_IMPORT && binding->nested_import
? &index->python_nested[binding->nested_import - 1]
: NULL;
}
bool cbm_resolve_binding_namespace_complete(const cbm_resolve_binding_index_t *index) {
return index && index->language == CBM_LANG_DART &&
index->analysis == CBM_RESOLVE_ANALYSIS_COMPLETE;
}
size_t cbm_resolve_binding_namespace_size(const cbm_resolve_binding_index_t *index) {
return cbm_resolve_binding_namespace_complete(index) ? index->name_count : 0;
}
const char *cbm_resolve_binding_namespace_member(const cbm_resolve_binding_index_t *index,
size_t offset, unsigned *kind) {
if (kind)
*kind = 0;
if (!cbm_resolve_binding_namespace_complete(index) || offset >= index->name_count)
return NULL;
const binding_name_t *name = &index->names[offset];
if (!(name->flags & BINDING_NAMESPACE_MEMBER) || name->name[0] == '_')
return NULL;
if (kind)
*kind = cbm_resolve_binding_export_kind(index, name->target);
return name->target;
}
size_t cbm_resolve_binding_import_count(const cbm_resolve_binding_index_t *index) {
return cbm_resolve_binding_namespace_complete(index) ? index->import_count : 0;
}
const cbm_resolve_source_import_t *cbm_resolve_binding_import_at(
const cbm_resolve_binding_index_t *index, size_t offset) {
return cbm_resolve_binding_namespace_complete(index) && offset < index->import_count
? &index->imports[offset]
: NULL;
}