feat(resolver): model references in Ruby, Elixir, OCaml, Scala, Zig, Perl and Haskell

USAGE goes from none to thousands of SCIP-confirmed pairs on Ruby (rubocop 3,322, vagrant 4,488) and to 890 sites on Elixir's plug. dune CALLS sites rise from 6,891 to 43,659: .mli files are read inside sig, includes reach paths below them, and functor parameters are bindings. Elixir's use reads __using__, Haskell's T(..) matches, Zig reads a field's type with its own file's visibility, Scala class-body vals carry their class in the QN.

A Ruby block keeps self only under a core iterator or a project method whose declaration does not rebind it, followed through a forwarded &block, so blocks under unknown gem methods abstain. Several OCaml and Scala false edges from the earlier tree are gone. CBM_INDEX_FORMAT_VERSION moves to 5 because OCaml, Zig and Scala QNs changed.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-11-02 18:09:12 +01:00
1 parent c3dbdf21f8
commit c1eb542277
25 files changed
+6920 -592

No files matched your search

+11
View File
@@ -1602,6 +1602,17 @@ static char *extract_callee_lang_specific(CBMArena *a, TSNode node, const char *
}
}
/* Scala `f[T](x)` and `a.f[T](x)`: the callee is what the type arguments follow. */
if (lang == CBM_LANG_SCALA && strcmp(nk, "call_expression") == 0) {
TSNode fnf = ts_node_child_by_field_name(node, TS_FIELD("function"));
if (!ts_node_is_null(fnf) && strcmp(ts_node_type(fnf), "generic_function") == 0) {
TSNode inner = ts_node_child_by_field_name(fnf, TS_FIELD("function"));
if (!ts_node_is_null(inner)) {
return cbm_node_text(a, inner, source);
}
}
}
if (lang == CBM_LANG_JSONNET) {
char *c = extract_jsonnet_callee(a, node, source, nk);
return c ? c : extract_scripting_callee(a, node, source, lang, nk);
+38 -4
View File
@@ -7107,7 +7107,8 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
(ctx->language == CBM_LANG_CPP || ctx->language == CBM_LANG_CUDA ||
ctx->language == CBM_LANG_TYPESCRIPT || ctx->language == CBM_LANG_TSX ||
ctx->language == CBM_LANG_ARKTS || ctx->language == CBM_LANG_NIX ||
ctx->language == CBM_LANG_PYTHON)) {
ctx->language == CBM_LANG_PYTHON || ctx->language == CBM_LANG_OCAML ||
ctx->language == CBM_LANG_ZIG)) {
def.qualified_name = cbm_arena_sprintf(a, "%s.%s", ctx->enclosing_class_qn, qn_name);
}
def.label = "Function";
@@ -10175,6 +10176,22 @@ static void extract_class_methods(CBMExtractCtx *ctx, TSNode class_node, const c
// Ruby `class << self` holds the class's singleton methods. The walk
// otherwise stops at the class body, leaving each of them without a node
// even though the calls inside it are attributed to that name.
/* `private def x`: the def is an argument of the visibility call, and evaluates in
* the class body like any other. */
if (ctx->language == CBM_LANG_RUBY && strcmp(ts_node_type(child), "call") == 0) {
TSNode arguments = ts_node_child_by_field_name(child, TS_FIELD("arguments"));
uint32_t members =
ts_node_is_null(arguments) ? 0 : ts_node_named_child_count(arguments);
for (uint32_t m = 0; m < members; m++) {
TSNode member = ts_node_named_child(arguments, m);
if (!cbm_kind_in_set(member, spec->function_node_types))
continue;
TSNode member_name = resolve_method_name(member, ctx->language);
if (!ts_node_is_null(member_name))
push_method_def(ctx, member, class_node, class_qn, spec, member_name);
}
continue;
}
if (ctx->language == CBM_LANG_RUBY && strcmp(ts_node_type(child), "singleton_class") == 0) {
TSNode singleton_body = ts_node_child_by_field_name(child, TS_FIELD("body"));
uint32_t members =
@@ -10835,8 +10852,13 @@ static void extract_elixir_call(CBMExtractCtx *ctx, TSNode node, const CBMLangSp
}
branch = cbm_find_child_by_kind(branch, "else_block");
}
} else if (strcmp(macro, "defmodule") == 0) {
TSNode do_block = emit_elixir_module_class(ctx, cur);
} else if (strcmp(macro, "defmodule") == 0 || strcmp(macro, "defprotocol") == 0 ||
strcmp(macro, "defimpl") == 0) {
/* A protocol is a module of its own. An implementation holds functions, under
* the protocol's name rather than one of its own. */
TSNode do_block = strcmp(macro, "defimpl") == 0
? cbm_find_child_by_kind(cur, "do_block")
: emit_elixir_module_class(ctx, cur);
if (!ts_node_is_null(do_block)) {
uint32_t dbc = ts_node_child_count(do_block);
for (int di = (int)dbc - SKIP_CHAR; di >= 0; di--) {
@@ -10869,7 +10891,8 @@ static void push_var_def_qn_doc(CBMExtractCtx *ctx, const char *name, const char
/* Java/Go: directory-based module (package), so a Go package-level var in
* myapp/db/conn.go is proj.myapp.db.Var, matching its siblings. */
def.qualified_name =
ctx->language == CBM_LANG_KOTLIN && ctx->var_parent_class
(ctx->language == CBM_LANG_KOTLIN || ctx->language == CBM_LANG_SCALA) &&
ctx->var_parent_class
? cbm_arena_sprintf(a, "%s.%s", ctx->var_parent_class, qn_name ? qn_name : name)
: cbm_fqn_compute_source_lang(a, ctx->project, ctx->rel_path, qn_name ? qn_name : name,
ctx->language);
@@ -13703,6 +13726,17 @@ static const char *compute_class_qn(CBMExtractCtx *ctx, TSNode node, const char
return cbm_nix_binding_scope_qn(ctx, node, saved_enclosing);
}
TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name"));
if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_OCAML) {
name_node = cbm_find_child_by_kind(node, "module_name");
}
/* `const Foo = struct {...}`: the container's name is its declaration's. */
if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_ZIG) {
TSNode declaration = ts_node_parent(node);
if (!ts_node_is_null(declaration) &&
strcmp(ts_node_type(declaration), "variable_declaration") == 0) {
name_node = cbm_find_child_by_kind(declaration, "identifier");
}
}
if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_OBJC) {
name_node = cbm_find_child_by_kind(node, "identifier");
}
+11
View File
@@ -1251,6 +1251,17 @@ static const char *compute_class_qn(CBMExtractCtx *ctx, TSNode node, const WalkS
return cbm_arena_sprintf(ctx->arena, "%s.Companion", state->enclosing_class_qn);
}
}
if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_OCAML) {
name_node = cbm_find_child_by_kind(node, "module_name");
}
/* `const Foo = struct {...}`, as the def side names it. */
if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_ZIG) {
TSNode declaration = ts_node_parent(node);
if (!ts_node_is_null(declaration) &&
strcmp(ts_node_type(declaration), "variable_declaration") == 0) {
name_node = cbm_find_child_by_kind(declaration, "identifier");
}
}
/* Objective-C: class_interface / class_implementation have no `name` field;
* the class name is a plain `identifier` child. Without this the walk pushes
* no class scope, so a method body's calls source to the Module and the
+5
View File
@@ -715,6 +715,11 @@ bool cbm_is_namespace_scope_kind(CBMLanguage lang, const char *kind) {
if (lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX || lang == CBM_LANG_ARKTS) {
return strcmp(kind, "internal_module") == 0;
}
/* `module Name = struct let f x = x end` is `Name.f`. Without its segment every f of
* a file is one node, and neither a call to it nor a call from it can be stated. */
if (lang == CBM_LANG_OCAML) {
return strcmp(kind, "module_binding") == 0;
}
return false;
}
+2 -3
View File
@@ -538,9 +538,8 @@ static const char *haskell_var_types[] = {"function", NULL};
// ==================== OCAML ====================
static const char *ocaml_func_types[] = {"value_definition", "constructor_declaration",
"method_definition", NULL};
static const char *ocaml_class_types[] = {"type_definition", "class_definition",
"module_definition", "exception_definition",
"record_declaration", NULL};
static const char *ocaml_class_types[] = {"type_definition", "class_definition",
"exception_definition", "record_declaration", NULL};
static const char *ocaml_module_types[] = {"compilation_unit", NULL};
static const char *ocaml_call_types[] = {"application_expression", "infix_expression",
"method_invocation", "module_application",
+428 -24
View File
@@ -13,6 +13,21 @@
* Inside `quote` a name belongs to the eventual expansion site, so nothing there
* is decided. A remote call reaches only `def`/`defmacro`, never `defp`.
*
* `use M` expands M's `__using__`. What that macro returns is known when its last
* statement is a `quote`: an `import X` or `use Y` standing directly in that quote holds
* at the use site, and a `def` standing there is a function of the using module, which
* has no node of its own, so a call of that name abstains.
*
* `:name.f(x)` is a call of Erlang module name, so it selects that module's exported
* f/1 when the project has one. `Proto.f(x)` selects the function a `defprotocol`
* declares; which `defimpl` runs is dispatch. A `defimpl P, for: T` is module P.T, so
* its own functions call each other like any module's.
*
* `&f/1` and `&Mod.f/1` capture by the same selection, without calling. A module's
* name wherever it is read is a mention of that module, if the project declares it
* once. A bare identifier is a variable, and the graph has no node for one; only `x
* |> f` makes one a call, of f/1.
*
* The graph's Elixir QN holds neither module nor arity, so all clauses and
* arities of one name in a module are one node. Two modules of a file sharing a
* name share a node too, and that identity cannot name either: such a target
@@ -28,11 +43,21 @@ typedef struct {
typedef struct {
const char *module;
TSNode only, except;
/* The same filters when the import stands in another file's `__using__`. */
const char *only_text, *except_text;
/* Not an import: a function `use` put into this module. */
const char *injected;
int injected_min, injected_max;
} elixir_import_t;
/* A statement in a module's `__using__`, kept on that module under this name. */
#define ELIXIR_USING "&using"
enum { ELIXIR_USING_KIND, ELIXIR_USING_ONLY, ELIXIR_USING_EXCEPT, ELIXIR_USING_PARTS };
typedef struct {
cbm_source_walk_t *walk;
const char *module; /* NULL at file level */
const char *scope; /* the enclosing definition's QN, or NULL */
elixir_alias_t *aliases;
size_t alias_count, alias_capacity;
elixir_import_t *imports;
@@ -102,7 +127,34 @@ static const char *elixir_module_of_declaration(elixir_ctx_t *ctx, TSNode call)
return ctx->module ? cbm_arena_sprintf(ctx->walk->arena, "%s.%s", ctx->module, text) : text;
}
static void elixir_declare_function(elixir_ctx_t *ctx, TSNode call, const char *definer) {
/* `defimpl P, for: T` is module P.T; with no `for:` T is the enclosing module. A list of
* types is several modules, which one body cannot be. */
static const char *elixir_module_of_impl(elixir_ctx_t *ctx, TSNode call) {
TSNode arguments = elixir_arguments(call);
uint32_t count = ts_node_is_null(arguments) ? 0 : ts_node_named_child_count(arguments);
TSNode protocol = count ? ts_node_named_child(arguments, 0) : (TSNode){0};
if (!cbm_source_is(protocol, "alias"))
return NULL;
const char *name = cbm_source_text(ctx->walk, protocol);
const char *type = ctx->module;
for (uint32_t i = 1; i < count; i++) {
TSNode keywords = ts_node_named_child(arguments, i);
uint32_t pairs =
cbm_source_is(keywords, "keywords") ? ts_node_named_child_count(keywords) : 0;
for (uint32_t p = 0; p < pairs; p++) {
TSNode pair = ts_node_named_child(keywords, p);
const char *key = cbm_source_text(ctx->walk, cbm_source_field(pair, "key"));
if (!key || strncmp(key, "for:", 4))
continue;
TSNode value = cbm_source_field(pair, "value");
type = cbm_source_is(value, "alias") ? cbm_source_text(ctx->walk, value) : NULL;
}
}
return name && type ? cbm_arena_sprintf(ctx->walk->arena, "%s.%s", name, type) : NULL;
}
/* A definition's name and the arities its head accepts. */
static const char *elixir_signature(elixir_ctx_t *ctx, TSNode call, int *fewest, int *most) {
TSNode head = elixir_head(call);
const char *name = NULL;
int arity_min = 0, arity_max = 0;
@@ -123,6 +175,14 @@ static void elixir_declare_function(elixir_ctx_t *ctx, TSNode call, const char *
arity_min++;
}
}
*fewest = arity_min;
*most = arity_max;
return name;
}
static void elixir_declare_function(elixir_ctx_t *ctx, TSNode call, const char *definer) {
int arity_min = 0, arity_max = 0;
const char *name = elixir_signature(ctx, call, &arity_min, &arity_max);
if (!name || !ctx->module)
return;
uint32_t line = ts_node_start_point(call).row + 1;
@@ -136,6 +196,137 @@ static void elixir_declare_function(elixir_ctx_t *ctx, TSNode call, const char *
}
}
/* `only: [a: 1, b: 2]` as ",a/1,b/2,"; "?" when the list is not written out. */
static const char *elixir_filter_text(elixir_ctx_t *ctx, TSNode filter) {
CBMArena *arena = ctx->walk->arena;
if (!cbm_source_is(filter, "list"))
return "?";
const char *text = ",";
uint32_t count = ts_node_named_child_count(filter);
for (uint32_t i = 0; text && i < count; i++) {
TSNode keywords = ts_node_named_child(filter, i);
if (!cbm_source_is(keywords, "keywords"))
return "?";
uint32_t pairs = ts_node_named_child_count(keywords);
for (uint32_t p = 0; text && p < pairs; p++) {
TSNode pair = ts_node_named_child(keywords, p);
const char *key = cbm_source_text(ctx->walk, cbm_source_field(pair, "key"));
const char *value = cbm_source_text(ctx->walk, cbm_source_field(pair, "value"));
const char *colon = key ? strchr(key, ':') : NULL;
if (!colon || !value)
return "?";
text =
cbm_arena_sprintf(arena, "%s%.*s/%d,", text, (int)(colon - key), key, atoi(value));
}
}
return text ? text : "?";
}
static cbm_source_decl_t *elixir_declare_using(elixir_ctx_t *ctx, TSNode statement,
const char *value, const char *kind) {
cbm_source_decl_t *decl =
cbm_source_declare(ctx->walk, ctx->module, ELIXIR_USING, NULL, CBM_SOURCE_DECL_F_TYPE,
ts_node_start_point(statement).row + 1);
const char **parts =
decl ? cbm_source_table_alloc(ctx->walk, ELIXIR_USING_PARTS * sizeof(*parts)) : NULL;
if (!parts)
return NULL;
decl->type = cbm_source_table_intern(ctx->walk, value);
parts[ELIXIR_USING_KIND] = kind;
decl->bases = parts;
return decl;
}
static void elixir_collect_using_statement(elixir_ctx_t *ctx, TSNode statement) {
const char *name =
cbm_source_is(statement, "call") ? elixir_target_name(ctx->walk, statement) : NULL;
if (!name)
return;
if (elixir_definer(name)) {
int arity_min = 0, arity_max = 0;
const char *defined = elixir_signature(ctx, statement, &arity_min, &arity_max);
cbm_source_decl_t *decl =
defined ? elixir_declare_using(ctx, statement, defined, "def") : NULL;
if (decl) {
decl->arity_min = arity_min;
decl->arity_max = arity_max;
}
return;
}
bool import = !strcmp(name, "import");
if (!import && strcmp(name, "use"))
return;
TSNode arguments = elixir_arguments(statement);
uint32_t count = ts_node_is_null(arguments) ? 0 : ts_node_named_child_count(arguments);
TSNode first = count ? ts_node_named_child(arguments, 0) : (TSNode){0};
const char *written = cbm_source_text(ctx->walk, first);
/* Inside the quote `unquote(__MODULE__)` is the module this macro is in. */
const char *module = !written ? NULL
: cbm_source_is(first, "alias") ? written
: !strcmp(written, "unquote(__MODULE__)") ? ctx->module
: NULL;
cbm_source_decl_t *decl =
module ? elixir_declare_using(ctx, statement, module, import ? "import" : "use") : NULL;
if (!decl || !import)
return;
const char **parts = (const char **)decl->bases;
for (uint32_t i = 1; i < count; i++) {
TSNode keywords = ts_node_named_child(arguments, i);
uint32_t pairs =
cbm_source_is(keywords, "keywords") ? ts_node_named_child_count(keywords) : 0;
for (uint32_t p = 0; p < pairs; p++) {
TSNode pair = ts_node_named_child(keywords, p);
const char *key = cbm_source_text(ctx->walk, cbm_source_field(pair, "key"));
TSNode value = cbm_source_field(pair, "value");
if (key && !strncmp(key, "only:", 5))
parts[ELIXIR_USING_ONLY] =
cbm_source_table_intern(ctx->walk, elixir_filter_text(ctx, value));
else if (key && !strncmp(key, "except:", 7))
parts[ELIXIR_USING_EXCEPT] =
cbm_source_table_intern(ctx->walk, elixir_filter_text(ctx, value));
}
}
}
/* What `defmacro __using__` returns, when its last statement says so. */
static void elixir_collect_using(elixir_ctx_t *ctx, TSNode definition) {
int arity_min = 0, arity_max = 0;
const char *name = elixir_signature(ctx, definition, &arity_min, &arity_max);
TSNode body = elixir_do_block(definition);
if (!name || strcmp(name, "__using__") || !ctx->module || ts_node_is_null(body))
return;
uint32_t statements = ts_node_named_child_count(body);
TSNode last = statements ? ts_node_named_child(body, statements - 1) : (TSNode){0};
const char *returned = cbm_source_is(last, "call") ? elixir_target_name(ctx->walk, last) : NULL;
if (!returned || strcmp(returned, "quote"))
return;
TSNode quoted = elixir_do_block(last);
if (!ts_node_is_null(quoted)) {
TSTreeCursor cursor = ts_tree_cursor_new(quoted);
if (ts_tree_cursor_goto_first_child(&cursor)) {
do {
elixir_collect_using_statement(ctx, ts_tree_cursor_current_node(&cursor));
} while (ts_tree_cursor_goto_next_sibling(&cursor));
}
ts_tree_cursor_delete(&cursor);
return;
}
/* `quote do: import(X)` */
TSNode arguments = elixir_arguments(last);
uint32_t count = ts_node_is_null(arguments) ? 0 : ts_node_named_child_count(arguments);
for (uint32_t i = 0; i < count; i++) {
TSNode keywords = ts_node_named_child(arguments, i);
uint32_t pairs =
cbm_source_is(keywords, "keywords") ? ts_node_named_child_count(keywords) : 0;
for (uint32_t p = 0; p < pairs; p++) {
TSNode pair = ts_node_named_child(keywords, p);
const char *key = cbm_source_text(ctx->walk, cbm_source_field(pair, "key"));
if (key && !strncmp(key, "do:", 3))
elixir_collect_using_statement(ctx, cbm_source_field(pair, "value"));
}
}
}
static void elixir_collect_node(elixir_ctx_t *ctx, TSNode node, unsigned depth) {
if (!cbm_source_step(ctx->walk, depth))
return;
@@ -146,12 +337,17 @@ static void elixir_collect_node(elixir_ctx_t *ctx, TSNode node, unsigned depth)
const char *name = elixir_target_name(ctx->walk, child);
if (name && !strcmp(name, "quote"))
continue;
if (name && !strcmp(name, "defmodule")) {
if (name && (!strcmp(name, "defmodule") || !strcmp(name, "defprotocol"))) {
const char *module = elixir_module_of_declaration(ctx, child);
if (!module)
continue;
uint32_t line = ts_node_start_point(child).row + 1;
(void)cbm_source_declare(ctx->walk, module, "", NULL, CBM_SOURCE_DECL_F_TYPE, line);
TSNode spelled = ts_node_named_child(elixir_arguments(child), 0);
(void)cbm_source_declare(
ctx->walk, module, "",
cbm_source_def_qn_label(ctx->walk, cbm_source_text(ctx->walk, spelled), line,
"Class"),
CBM_SOURCE_DECL_F_TYPE, line);
const char *saved = ctx->module;
ctx->module = module;
TSNode body = elixir_do_block(child);
@@ -162,12 +358,21 @@ static void elixir_collect_node(elixir_ctx_t *ctx, TSNode node, unsigned depth)
}
if (name && elixir_definer(name)) {
elixir_declare_function(ctx, child, name);
if (!strcmp(name, "defmacro"))
elixir_collect_using(ctx, child);
continue;
}
/* Protocol and implementation bodies have their own dispatch, which
* these rules do not state; their functions are not modelled. */
if (name && (!strcmp(name, "defprotocol") || !strcmp(name, "defimpl")))
if (name && !strcmp(name, "defimpl")) {
const char *module = elixir_module_of_impl(ctx, child);
TSNode body = elixir_do_block(child);
if (!module || ts_node_is_null(body))
continue;
const char *saved = ctx->module;
ctx->module = module;
elixir_collect_node(ctx, body, depth + 1);
ctx->module = saved;
continue;
}
}
elixir_collect_node(ctx, child, depth + 1);
}
@@ -272,6 +477,22 @@ static void elixir_note_alias(elixir_ctx_t *ctx, TSNode call) {
elixir_add_alias(ctx, local, module);
}
static void elixir_push_import(elixir_ctx_t *ctx, elixir_import_t import) {
if (ctx->import_count == ctx->import_capacity) {
size_t capacity = ctx->import_capacity ? ctx->import_capacity * 2 : 8;
elixir_import_t *items = cbm_arena_alloc(ctx->walk->arena, capacity * sizeof(*items));
if (!items) {
cbm_source_fail(ctx->walk, "elixir_import_allocation");
return;
}
if (ctx->import_count)
memcpy(items, ctx->imports, ctx->import_count * sizeof(*items));
ctx->imports = items;
ctx->import_capacity = capacity;
}
ctx->imports[ctx->import_count++] = import;
}
static void elixir_note_import(elixir_ctx_t *ctx, TSNode call) {
TSNode arguments = elixir_arguments(call);
uint32_t count = ts_node_is_null(arguments) ? 0 : ts_node_named_child_count(arguments);
@@ -294,19 +515,30 @@ static void elixir_note_import(elixir_ctx_t *ctx, TSNode call) {
import.except = cbm_source_field(pair, "value");
}
}
if (ctx->import_count == ctx->import_capacity) {
size_t capacity = ctx->import_capacity ? ctx->import_capacity * 2 : 8;
elixir_import_t *items = cbm_arena_alloc(ctx->walk->arena, capacity * sizeof(*items));
if (!items) {
cbm_source_fail(ctx->walk, "elixir_import_allocation");
return;
elixir_push_import(ctx, import);
}
/* `use M`: what M's `__using__` is known to return. A `use` in there follows on, and each
* module is read once, so this costs the modules reachable from M. */
static void elixir_apply_using(elixir_ctx_t *ctx, const char *module, cbm_source_chain_t *chain) {
if (!cbm_source_chain_first_visit(chain, module))
return;
for (const cbm_source_decl_t *decl = cbm_source_lookup(ctx->walk, module, ELIXIR_USING); decl;
decl = decl->next) {
const char *kind = decl->bases[ELIXIR_USING_KIND];
if (!strcmp(kind, "use")) {
elixir_apply_using(ctx, decl->type, chain);
} else if (!strcmp(kind, "def")) {
elixir_push_import(ctx, (elixir_import_t){.injected = decl->type,
.injected_min = decl->arity_min,
.injected_max = decl->arity_max});
} else {
elixir_push_import(ctx,
(elixir_import_t){.module = decl->type,
.only_text = decl->bases[ELIXIR_USING_ONLY],
.except_text = decl->bases[ELIXIR_USING_EXCEPT]});
}
if (ctx->import_count)
memcpy(items, ctx->imports, ctx->import_count * sizeof(*items));
ctx->imports = items;
ctx->import_capacity = capacity;
}
ctx->imports[ctx->import_count++] = import;
}
/* `only: [a: 1]` or `except: [a: 1]`. `only: :functions` and any computed list
@@ -337,6 +569,14 @@ static elixir_list_t elixir_filter_has(elixir_ctx_t *ctx, TSNode filter, const c
return ELIXIR_LIST_NO;
}
static elixir_list_t elixir_text_filter_has(elixir_ctx_t *ctx, const char *filter, const char *name,
int arity) {
if (!strcmp(filter, "?"))
return ELIXIR_LIST_UNKNOWN;
const char *entry = cbm_arena_sprintf(ctx->walk->arena, ",%s/%d,", name, arity);
return entry && strstr(filter, entry) ? ELIXIR_LIST_YES : ELIXIR_LIST_NO;
}
/* Every declaration of (module, name) accepting this arity; several QNs mean
* the name is not one declaration. */
static const char *elixir_select(elixir_ctx_t *ctx, const char *module, const char *name, int arity,
@@ -405,6 +645,30 @@ static void elixir_visit_bare_call(elixir_ctx_t *ctx, TSNode call, const char *n
const char *selected = NULL, *from = NULL;
for (size_t i = 0; i < ctx->import_count; i++) {
const elixir_import_t *import = &ctx->imports[i];
if (import->injected) {
if (!strcmp(import->injected, name) && arity >= import->injected_min &&
arity <= import->injected_max) {
elixir_emit(ctx, call, NULL, CBM_RESOLVE_RULE_NONE, NULL,
"elixir_function_put_here_by_use");
return;
}
continue;
}
if (import->only_text || import->except_text) {
elixir_list_t only = import->only_text
? elixir_text_filter_has(ctx, import->only_text, name, arity)
: ELIXIR_LIST_YES;
elixir_list_t except =
import->except_text ? elixir_text_filter_has(ctx, import->except_text, name, arity)
: ELIXIR_LIST_NO;
if (only == ELIXIR_LIST_UNKNOWN || except == ELIXIR_LIST_UNKNOWN) {
elixir_emit(ctx, call, NULL, CBM_RESOLVE_RULE_NONE, NULL,
"elixir_import_filter_not_literal");
return;
}
if (only == ELIXIR_LIST_NO || except == ELIXIR_LIST_YES)
continue;
}
if (!ts_node_is_null(import->only)) {
elixir_list_t member = elixir_filter_has(ctx, import->only, name, arity);
if (member == ELIXIR_LIST_UNKNOWN) {
@@ -446,6 +710,30 @@ static void elixir_visit_bare_call(elixir_ctx_t *ctx, TSNode call, const char *n
"elixir_name_not_in_module_or_project_imports");
}
/* `:name` read as a module: an Erlang module of that name. */
static const char *elixir_erlang_module(elixir_ctx_t *ctx, TSNode node) {
if (!cbm_source_is(node, "atom"))
return NULL;
const char *text = cbm_source_text(ctx->walk, node);
return text && text[0] == ':' && text[1] != '"' ? text + 1 : NULL;
}
/* An Erlang module's exported function of this name and arity. */
static const char *elixir_erlang_function(elixir_ctx_t *ctx, const char *module, const char *name,
int arity) {
const char *selected = NULL;
for (const cbm_source_decl_t *decl =
cbm_source_lookup_in(ctx->walk, CBM_LANG_ERLANG, module, name);
decl; decl = decl->next) {
if (decl->arity_min != arity || !(decl->flags & CBM_SOURCE_DECL_F_PUBLIC))
continue;
if (!decl->qn || (selected && strcmp(selected, decl->qn)))
return NULL;
selected = decl->qn;
}
return selected;
}
static void elixir_visit_remote_call(elixir_ctx_t *ctx, TSNode call, TSNode dot, int arity) {
TSNode left = cbm_source_field(dot, "left");
TSNode right = cbm_source_field(dot, "right");
@@ -453,6 +741,14 @@ static void elixir_visit_remote_call(elixir_ctx_t *ctx, TSNode call, TSNode dot,
elixir_emit(ctx, call, NULL, CBM_RESOLVE_RULE_NONE, NULL, "elixir_anonymous_function_call");
return;
}
const char *erlang = elixir_erlang_module(ctx, left);
if (erlang) {
const char *target =
elixir_erlang_function(ctx, erlang, cbm_source_text(ctx->walk, right), arity);
elixir_emit(ctx, call, target, CBM_RESOLVE_RULE_LANGUAGE_BINDING, erlang,
target ? "elixir_erlang_module_function" : "elixir_erlang_function_absent");
return;
}
const char *module = elixir_module_operand(ctx, left);
if (!module) {
elixir_emit(ctx, call, NULL, CBM_RESOLVE_RULE_NONE, NULL, "elixir_receiver_not_a_module");
@@ -467,6 +763,62 @@ static void elixir_visit_remote_call(elixir_ctx_t *ctx, TSNode call, TSNode dot,
module, target ? "elixir_remote_module_function" : reason);
}
static void elixir_extra_begin(elixir_ctx_t *ctx, TSNode node, const char *name) {
cbm_source_walk_t *walk = ctx->walk;
walk->extra_scope = ctx->scope ? ctx->scope : walk->result->module_qn;
walk->extra_name = name;
walk->extra_line = ts_node_start_point(node).row + 1;
}
static void elixir_extra_end(elixir_ctx_t *ctx) {
ctx->walk->extra_scope = NULL;
ctx->walk->extra_name = NULL;
ctx->walk->extra_line = 0;
}
/* A module's name, read anywhere other than its own declaration. */
static void elixir_visit_module_mention(elixir_ctx_t *ctx, TSNode node) {
const char *text = cbm_source_text(ctx->walk, node);
const char *module = text ? elixir_expand_alias(ctx, text) : NULL;
const char *reason = NULL;
const char *qn =
module ? cbm_source_unique_qn(cbm_source_lookup(ctx->walk, module, ""), &reason) : NULL;
if (!qn)
return;
elixir_extra_begin(ctx, node, text);
cbm_source_emit_node(ctx->walk, node, CBM_SOURCE_SITE_REFERENCE, qn,
CBM_RESOLVE_RULE_LANGUAGE_BINDING, module, "elixir_module_name");
elixir_extra_end(ctx);
}
/* `&f/1` or `&Mod.f/1`. The grammar spells f as an identifier or a call without arguments. */
static void elixir_visit_capture(elixir_ctx_t *ctx, TSNode site, TSNode left, int arity) {
const char *target = NULL;
const char *domain = NULL;
const char *reason = NULL;
const char *name = NULL;
TSNode callee = cbm_source_is(left, "call") ? cbm_source_field(left, "target") : left;
if (cbm_source_is(callee, "identifier") && ctx->module) {
name = cbm_source_text(ctx->walk, callee);
domain = ctx->module;
target = name ? elixir_select(ctx, ctx->module, name, arity, false, &reason) : NULL;
} else if (cbm_source_is(callee, "dot") &&
cbm_source_is(cbm_source_field(callee, "right"), "identifier")) {
domain = elixir_module_operand(ctx, cbm_source_field(callee, "left"));
name = cbm_source_text(ctx->walk, cbm_source_field(callee, "right"));
target = domain && name
? elixir_select(ctx, domain, name, arity,
!ctx->module || strcmp(ctx->module, domain), &reason)
: NULL;
}
if (!target)
return;
elixir_extra_begin(ctx, site, name);
cbm_source_emit_node(ctx->walk, site, CBM_SOURCE_SITE_REFERENCE, target,
CBM_RESOLVE_RULE_LANGUAGE_BINDING, domain, "elixir_function_capture");
elixir_extra_end(ctx);
}
static void elixir_visit(elixir_ctx_t *ctx, TSNode node, bool piped, unsigned depth);
static void elixir_visit_children(elixir_ctx_t *ctx, TSNode node, unsigned depth) {
@@ -499,20 +851,24 @@ static void elixir_visit_call(elixir_ctx_t *ctx, TSNode node, bool piped, unsign
if (!ts_node_is_null(body))
arity++; /* A do block is the trailing keyword argument. */
if (name) {
if (!strcmp(name, "quote") || !strcmp(name, "defprotocol") || !strcmp(name, "defimpl"))
if (!strcmp(name, "quote"))
return;
if (!strcmp(name, "defmodule")) {
const char *module = elixir_module_of_declaration(ctx, node);
bool protocol = !strcmp(name, "defprotocol");
bool implementation = !strcmp(name, "defimpl");
if (!strcmp(name, "defmodule") || protocol || implementation) {
const char *module = implementation ? elixir_module_of_impl(ctx, node)
: elixir_module_of_declaration(ctx, node);
if (!module)
return;
/* A nested defmodule leaves an alias of its last segment in the parent. */
if (ctx->module)
if (ctx->module && !implementation)
elixir_add_alias(ctx, cbm_source_text(ctx->walk, ts_node_named_child(arguments, 0)),
module);
/* Imports are lexical, so a nested module sees those around it, and what it adds
* ends with it. */
const char *saved = ctx->module;
size_t imports = ctx->import_count;
ctx->module = module;
ctx->import_count = 0;
if (!ts_node_is_null(body))
elixir_visit_scoped(ctx, body, depth + 1);
ctx->module = saved;
@@ -524,10 +880,20 @@ static void elixir_visit_call(elixir_ctx_t *ctx, TSNode node, bool piped, unsign
TSNode head_arguments = elixir_arguments(node);
uint32_t count =
ts_node_is_null(head_arguments) ? 0 : ts_node_named_child_count(head_arguments);
const char *saved_scope = ctx->scope;
TSNode head = elixir_head(node);
TSNode head_name =
cbm_source_is(head, "call") ? cbm_source_field(head, "target") : head;
ctx->scope =
cbm_source_is(head_name, "identifier")
? cbm_source_def_qn_label(ctx->walk, cbm_source_text(ctx->walk, head_name),
ts_node_start_point(node).row + 1, "Function")
: NULL;
for (uint32_t i = 1; i < count; i++)
elixir_visit_scoped(ctx, ts_node_named_child(head_arguments, i), depth + 1);
if (!ts_node_is_null(body))
elixir_visit_scoped(ctx, body, depth + 1);
ctx->scope = saved_scope;
return;
}
if (!strcmp(name, "alias")) {
@@ -545,11 +911,22 @@ static void elixir_visit_call(elixir_ctx_t *ctx, TSNode node, bool piped, unsign
elixir_note_import(ctx, node);
return;
}
if (!strcmp(name, "use"))
if (!strcmp(name, "use")) {
const char *used = !ts_node_is_null(arguments) && ts_node_named_child_count(arguments)
? elixir_module_operand(ctx, ts_node_named_child(arguments, 0))
: NULL;
cbm_source_chain_t chain = {0};
if (used)
elixir_apply_using(ctx, used, &chain);
return;
}
elixir_visit_bare_call(ctx, node, name, arity);
} else if (cbm_source_is(cbm_source_field(node, "target"), "dot")) {
elixir_visit_remote_call(ctx, node, cbm_source_field(node, "target"), arity);
/* The receiver is read first: `f(x).y` calls f, and `Mod.g(x)` mentions Mod. */
TSNode receiver = cbm_source_field(cbm_source_field(node, "target"), "left");
if (!ts_node_is_null(receiver))
elixir_visit(ctx, receiver, false, depth + 1);
}
if (!ts_node_is_null(arguments))
elixir_visit_children(ctx, arguments, depth);
@@ -564,6 +941,26 @@ static void elixir_visit(elixir_ctx_t *ctx, TSNode node, bool piped, unsigned de
elixir_visit_call(ctx, node, piped, depth);
return;
}
if (cbm_source_is(node, "alias")) {
elixir_visit_module_mention(ctx, node);
return;
}
if (cbm_source_is(node, "identifier")) {
const char *name = cbm_source_text(ctx->walk, node);
if (piped && name) {
elixir_extra_begin(ctx, node, name);
elixir_visit_bare_call(ctx, node, name, 1);
elixir_extra_end(ctx);
cbm_source_emit_node(ctx->walk, node, CBM_SOURCE_SITE_REFERENCE, NULL,
CBM_RESOLVE_RULE_NONE, NULL, "elixir_piped_name_is_a_call");
} else {
elixir_emit(ctx, node, NULL, CBM_RESOLVE_RULE_NONE, NULL,
"elixir_bare_name_is_a_variable");
cbm_source_emit_node(ctx->walk, node, CBM_SOURCE_SITE_REFERENCE, NULL,
CBM_RESOLVE_RULE_NONE, NULL, "elixir_variable_has_no_node");
}
return;
}
/* A module attribute evaluates at compile time and typespecs are not code. */
if (cbm_source_is(node, "unary_operator") && cbm_source_token(node, "@"))
return;
@@ -575,6 +972,11 @@ static void elixir_visit(elixir_ctx_t *ctx, TSNode node, bool piped, unsigned de
if (cbm_source_is(left, "call"))
elixir_emit(ctx, left, NULL, CBM_RESOLVE_RULE_NONE, NULL,
"elixir_capture_is_not_an_invocation");
TSNode arity = cbm_source_field(operand, "right");
const char *digits =
cbm_source_is(arity, "integer") ? cbm_source_text(ctx->walk, arity) : NULL;
if (digits)
elixir_visit_capture(ctx, node, left, atoi(digits));
return;
}
}
@@ -599,4 +1001,6 @@ static void elixir_resolve(cbm_source_walk_t *walk) {
const cbm_source_adapter_t cbm_source_adapter_elixir = {.language = CBM_LANG_ELIXIR,
.collect = elixir_collect,
.resolve = elixir_resolve,
.one_name_one_node = true};
.one_name_one_node = true,
.decides_references = true,
.contains_parse_damage = true};
+57 -1
View File
@@ -9,6 +9,8 @@
* to be exported, by -export or export_all. `?MODULE:f(X)` is the same call on
* the current module. A variable module or function is runtime dispatch.
*
* `fun f/1` and `fun m:f/1` name a function by the same rules without calling it.
*
* The graph's QN holds no arity, so f/1 and f/2 are one node: the edge names that
* function name in that module, which is what the graph can represent. */
#include "source_adapter_internal.h"
@@ -30,6 +32,7 @@ typedef struct {
size_t export_count;
erlang_import_t *imports;
size_t import_count;
const char *scope; /* the enclosing function's QN, or NULL */
} erlang_ctx_t;
/* The attribute kinds a module header can use. */
@@ -232,11 +235,61 @@ static void erlang_visit_call(erlang_ctx_t *ctx, TSNode call) {
"erlang_fun_value_call");
}
/* `fun f/1`: the module's own f/1, else an imported one. `fun m:f/1`: m's exported f/1. */
static void erlang_visit_fun_reference(erlang_ctx_t *ctx, TSNode node) {
cbm_source_walk_t *walk = ctx->walk;
const char *name = cbm_source_text(walk, cbm_source_field(node, "fun"));
const char *digits =
cbm_source_text(walk, cbm_source_field(cbm_source_field(node, "arity"), "value"));
if (!name || !digits)
return;
int arity = atoi(digits);
const char *reason = NULL;
const char *target = NULL;
const char *domain = NULL;
if (cbm_source_is(node, "external_fun")) {
TSNode wrapper = cbm_source_field(node, "module");
TSNode module_node =
ts_node_named_child_count(wrapper) ? ts_node_named_child(wrapper, 0) : (TSNode){0};
domain = cbm_source_is(module_node, "atom") ? cbm_source_text(walk, module_node) : NULL;
target = domain ? erlang_select(ctx, domain, name, arity, true, &reason) : NULL;
} else if (ctx->module) {
domain = ctx->module;
target = erlang_select(ctx, ctx->module, name, arity, false, &reason);
for (size_t i = 0; !target && i < ctx->import_count; i++) {
const erlang_import_t *import = &ctx->imports[i];
if (import->arity != arity || strcmp(import->name, name) || !import->module)
continue;
domain = import->module;
target = erlang_select(ctx, import->module, name, arity, true, &reason);
}
}
if (!target)
return;
walk->extra_scope = ctx->scope ? ctx->scope : walk->result->module_qn;
walk->extra_name = name;
walk->extra_line = ts_node_start_point(node).row + 1;
cbm_source_emit_node(walk, node, CBM_SOURCE_SITE_REFERENCE, target,
CBM_RESOLVE_RULE_LANGUAGE_BINDING, domain, "erlang_function_reference");
walk->extra_scope = NULL;
walk->extra_name = NULL;
walk->extra_line = 0;
}
static void erlang_visit(erlang_ctx_t *ctx, TSNode node, unsigned depth) {
if (!cbm_source_step(ctx->walk, depth))
return;
if (cbm_source_is(node, "call"))
erlang_visit_call(ctx, node);
else if (cbm_source_is(node, "internal_fun") || cbm_source_is(node, "external_fun"))
erlang_visit_fun_reference(ctx, node);
const char *saved_scope = ctx->scope;
if (cbm_source_is(node, "function_clause")) {
const char *name = cbm_source_text(ctx->walk, cbm_source_field(node, "name"));
ctx->scope = name ? cbm_source_def_qn_label(ctx->walk, name,
ts_node_start_point(node).row + 1, "Function")
: NULL;
}
TSTreeCursor cursor = ts_tree_cursor_new(node);
if (ts_tree_cursor_goto_first_child(&cursor)) {
do {
@@ -246,6 +299,7 @@ static void erlang_visit(erlang_ctx_t *ctx, TSNode node, unsigned depth) {
} while (ts_tree_cursor_goto_next_sibling(&cursor));
}
ts_tree_cursor_delete(&cursor);
ctx->scope = saved_scope;
}
static void erlang_resolve(cbm_source_walk_t *walk) {
@@ -257,4 +311,6 @@ static void erlang_resolve(cbm_source_walk_t *walk) {
const cbm_source_adapter_t cbm_source_adapter_erlang = {.language = CBM_LANG_ERLANG,
.collect = erlang_collect,
.resolve = erlang_resolve,
.one_name_one_node = true};
.one_name_one_node = true,
.decides_references = true,
.contains_parse_damage = true};
+179 -21
View File
@@ -13,13 +13,26 @@
* followed, so a name reached only through a re-export abstains.
*
* All equations of a function and its signature are one name, and the graph has
* one node per name in a file. */
* one node per name in a file.
*
* Constructors have a namespace of their own, looked up the same way. The graph has a
* node for a type, none for a constructor, so `data T = T a` selects T and a
* constructor named otherwise is known but selects nothing. An export or import entry
* reaches a type's constructors only when it lists children, as in `T(..)`; a
* pattern matches a constructor without calling it.
*
* A class's methods are top-level names of its module. `status x` selects the method's
* declaration in the class; which instance runs is dispatch. A signature alone has no node;
* the file's default or instance bodies of that name are its node, and with none the method
* is known but selects nothing. `C(..)` and `C(m)` reach methods the same way an entry
* reaches constructors. */
#include "source_adapter_internal.h"
#include <stdio.h>
#include <stdlib.h>
#define HS_EXPORT_KEY_PREFIX "&export:"
#define HS_EXPORTS_ALL "&exports_all"
#define HS_EXPORT_CHILDREN_PREFIX "&export_children:"
typedef struct {
const char *module;
@@ -36,7 +49,8 @@ typedef struct hs_local {
typedef struct {
cbm_source_walk_t *walk;
bool reference; /* the name being read is a value, not a call head */
bool reference; /* the name being read is a value, not a call head */
bool constructor; /* the name being read is a constructor */
const char *module;
hs_import_t *imports;
size_t import_count;
@@ -69,13 +83,97 @@ static void hs_collect_exports(cbm_source_walk_t *walk, const char *module) {
for (uint32_t i = 0; i < count; i++) {
TSNode entry = ts_node_named_child(exports, i);
const char *name = cbm_source_text(walk, cbm_source_field(entry, "variable"));
/* `T(..)` exports T's constructors; a bare `T` keeps them private. */
TSNode children = cbm_source_field(entry, "children");
if (!name && !ts_node_is_null(children))
name = cbm_source_text(walk, hs_child(entry, "name"));
const char *key =
name ? cbm_arena_sprintf(walk->arena, "%s%s", HS_EXPORT_KEY_PREFIX, name) : NULL;
if (key)
(void)cbm_source_declare(walk, module, key, NULL, CBM_SOURCE_DECL_F_VALUE, 1);
/* `C(..)` exports every method of C, and `C(m)` exports m. */
uint32_t listed = ts_node_is_null(children) ? 0 : ts_node_named_child_count(children);
for (uint32_t c = 0; name && c < listed; c++) {
TSNode child = ts_node_named_child(children, c);
const char *child_key =
cbm_source_is(child, "all_names")
? cbm_arena_sprintf(walk->arena, "%s%s", HS_EXPORT_CHILDREN_PREFIX, name)
: cbm_arena_sprintf(walk->arena, "%s%s", HS_EXPORT_KEY_PREFIX,
cbm_source_text(walk, child));
if (child_key)
(void)cbm_source_declare(walk, module, child_key, NULL, CBM_SOURCE_DECL_F_VALUE, 1);
}
}
}
static void hs_collect_method(cbm_source_walk_t *walk, const char *module, const char *class_name,
TSNode name) {
const char *text = cbm_source_text(walk, name);
uint32_t line = ts_node_start_point(name).row + 1;
cbm_source_decl_t *decl =
text ? cbm_source_declare(walk, module, text, cbm_source_def_qn_of_name(walk, text),
CBM_SOURCE_DECL_F_PUBLIC, line)
: NULL;
if (decl)
decl->type = cbm_source_table_intern(walk, class_name);
}
/* What a class declares a signature for; a default body repeats a name, adding nothing. */
static void hs_collect_class(cbm_source_walk_t *walk, const char *module, TSNode declaration) {
const char *class_name = cbm_source_text(walk, hs_child(declaration, "name"));
TSNode body = hs_child(declaration, "class_declarations");
if (!class_name || ts_node_is_null(body))
return;
TSTreeCursor cursor = ts_tree_cursor_new(body);
if (ts_tree_cursor_goto_first_child(&cursor)) {
do {
TSNode signature = ts_tree_cursor_current_node(&cursor);
if (!cbm_source_is(signature, "signature"))
continue;
TSNode head = ts_node_named_child(signature, 0);
if (cbm_source_is(head, "variable"))
hs_collect_method(walk, module, class_name, head);
uint32_t names =
cbm_source_is(head, "binding_list") ? ts_node_named_child_count(head) : 0;
for (uint32_t n = 0; n < names; n++)
hs_collect_method(walk, module, class_name, ts_node_named_child(head, n));
} while (ts_tree_cursor_goto_next_sibling(&cursor));
}
ts_tree_cursor_delete(&cursor);
}
/* Every constructor is declared, so that one with no node still hides an imported
* name. Only the constructor sharing its type's name has a node to select. */
static void hs_collect_constructor(cbm_source_walk_t *walk, const char *module, TSNode constructor,
const char *type_name, const char *type_qn, uint32_t line) {
const char *text = cbm_source_text(walk, constructor);
if (text)
(void)cbm_source_declare(walk, module, text,
type_name && !strcmp(text, type_name) ? type_qn : NULL,
CBM_SOURCE_DECL_F_TYPE | CBM_SOURCE_DECL_F_PUBLIC, line);
}
static void hs_collect_constructors(cbm_source_walk_t *walk, const char *module, TSNode node,
const char *type_name, const char *type_qn, uint32_t line,
unsigned depth) {
if (!cbm_source_step(walk, depth))
return;
TSTreeCursor cursor = ts_tree_cursor_new(node);
if (ts_tree_cursor_goto_first_child(&cursor)) {
do {
TSNode child = ts_tree_cursor_current_node(&cursor);
if (cbm_source_is(child, "constructor"))
hs_collect_constructor(walk, module, child, type_name, type_qn, line);
else if (cbm_source_is(child, "data_constructors") ||
cbm_source_is(child, "data_constructor") ||
cbm_source_is(child, "newtype_constructor") ||
cbm_source_is(child, "prefix") || cbm_source_is(child, "record"))
hs_collect_constructors(walk, module, child, type_name, type_qn, line, depth + 1);
} while (ts_tree_cursor_goto_next_sibling(&cursor));
}
ts_tree_cursor_delete(&cursor);
}
static void hs_collect(cbm_source_walk_t *walk) {
const char *module = hs_header_module(walk);
(void)cbm_source_declare(walk, module, "", NULL, CBM_SOURCE_DECL_F_TYPE, 1);
@@ -84,6 +182,18 @@ static void hs_collect(cbm_source_walk_t *walk) {
uint32_t count = ts_node_is_null(declarations) ? 0 : ts_node_named_child_count(declarations);
for (uint32_t i = 0; i < count; i++) {
TSNode declaration = ts_node_named_child(declarations, i);
if (cbm_source_is(declaration, "data_type") || cbm_source_is(declaration, "newtype")) {
const char *type_name = cbm_source_text(walk, cbm_source_field(declaration, "name"));
uint32_t type_line = ts_node_start_point(declaration).row + 1;
hs_collect_constructors(walk, module, declaration, type_name,
cbm_source_def_qn_label(walk, type_name, type_line, "Class"),
type_line, 1);
continue;
}
if (cbm_source_is(declaration, "class")) {
hs_collect_class(walk, module, declaration);
continue;
}
if (!cbm_source_is(declaration, "function") && !cbm_source_is(declaration, "bind") &&
!cbm_source_is(declaration, "signature"))
continue;
@@ -128,12 +238,33 @@ static void hs_gather_imports(hs_ctx_t *ctx) {
}
}
static bool hs_list_names(hs_ctx_t *ctx, TSNode names, const char *name) {
/* An import entry `C(..)` or `C(m)` naming method m of class C. */
static bool hs_entry_lists_method(hs_ctx_t *ctx, TSNode entry, const char *class_name,
const char *name) {
TSNode children = cbm_source_field(entry, "children");
const char *listed = cbm_source_text(ctx->walk, hs_child(entry, "name"));
if (ts_node_is_null(children) || !class_name || !listed || strcmp(listed, class_name))
return false;
uint32_t count = ts_node_named_child_count(children);
for (uint32_t c = 0; c < count; c++) {
TSNode child = ts_node_named_child(children, c);
const char *text = cbm_source_text(ctx->walk, child);
if (cbm_source_is(child, "all_names") || (text && !strcmp(text, name)))
return true;
}
return false;
}
static bool hs_list_names(hs_ctx_t *ctx, TSNode names, const char *module, const char *name) {
uint32_t count = ts_node_is_null(names) ? 0 : ts_node_named_child_count(names);
const cbm_source_decl_t *decl = cbm_source_lookup(ctx->walk, module, name);
for (uint32_t i = 0; i < count; i++) {
TSNode entry = ts_node_named_child(names, i);
const char *text = cbm_source_text(ctx->walk, cbm_source_field(entry, "variable"));
if (text && !strcmp(text, name))
if (!text && !ts_node_is_null(cbm_source_field(entry, "children")))
text = cbm_source_text(ctx->walk, hs_child(entry, "name"));
if ((text && !strcmp(text, name)) ||
(decl && hs_entry_lists_method(ctx, entry, decl->type, name)))
return true;
}
return false;
@@ -150,12 +281,16 @@ static const char *hs_visible_member(hs_ctx_t *ctx, const char *module, const ch
int length = snprintf(key, sizeof(key), "%s%s", HS_EXPORT_KEY_PREFIX, name);
if (length < 0 || (size_t)length >= sizeof(key))
return NULL;
if (!cbm_source_lookup(ctx->walk, module, HS_EXPORTS_ALL) &&
!cbm_source_lookup(ctx->walk, module, key)) {
*reason = "haskell_name_not_in_export_list";
return NULL;
if (cbm_source_lookup(ctx->walk, module, HS_EXPORTS_ALL) ||
cbm_source_lookup(ctx->walk, module, key))
return qn;
if (decl->type) {
length = snprintf(key, sizeof(key), "%s%s", HS_EXPORT_CHILDREN_PREFIX, decl->type);
if (length > 0 && (size_t)length < sizeof(key) && cbm_source_lookup(ctx->walk, module, key))
return qn;
}
return qn;
*reason = "haskell_name_not_in_export_list";
return NULL;
}
static bool hs_is_local(const hs_ctx_t *ctx, const char *name) {
@@ -199,9 +334,10 @@ static void hs_bind_decls(hs_ctx_t *ctx, TSNode binds) {
static void hs_emit(hs_ctx_t *ctx, TSNode site, const char *target, cbm_resolve_rule_t rule,
const char *domain, const char *reason) {
cbm_source_emit_node(ctx->walk, site,
ctx->reference ? CBM_SOURCE_SITE_REFERENCE : CBM_SOURCE_SITE_CALL, target,
rule, domain, reason);
cbm_source_emit_facts(ctx->walk, ts_node_start_byte(site), ts_node_end_byte(site),
ctx->reference ? CBM_SOURCE_SITE_REFERENCE : CBM_SOURCE_SITE_CALL, target,
rule, domain, reason,
ctx->constructor ? CBM_RESOLVE_FACT_CLASS_CALLABLE : 0);
}
static void hs_resolve_bare(hs_ctx_t *ctx, TSNode site, const char *name) {
@@ -222,9 +358,11 @@ static void hs_resolve_bare(hs_ctx_t *ctx, TSNode site, const char *name) {
const hs_import_t *import = &ctx->imports[i];
if (import->qualified)
continue;
if (import->has_list && !import->hiding && !hs_list_names(ctx, import->names, name))
if (import->has_list && !import->hiding &&
!hs_list_names(ctx, import->names, import->module, name))
continue;
if (import->has_list && import->hiding && hs_list_names(ctx, import->names, name))
if (import->has_list && import->hiding &&
hs_list_names(ctx, import->names, import->module, name))
continue;
const char *why = NULL;
const char *qn = hs_visible_member(ctx, import->module, name, &why);
@@ -261,9 +399,11 @@ static void hs_resolve_qualified(hs_ctx_t *ctx, TSNode site, TSNode qualified) {
const char *visible_as = import->alias ? import->alias : import->module;
if (strcmp(visible_as, qualifier))
continue;
if (import->has_list && !import->hiding && !hs_list_names(ctx, import->names, name))
if (import->has_list && !import->hiding &&
!hs_list_names(ctx, import->names, import->module, name))
continue;
if (import->has_list && import->hiding && hs_list_names(ctx, import->names, name))
if (import->has_list && import->hiding &&
hs_list_names(ctx, import->names, import->module, name))
continue;
const char *why = NULL;
const char *qn = hs_visible_member(ctx, import->module, name, &why);
@@ -300,8 +440,9 @@ static void hs_visit_children(hs_ctx_t *ctx, TSNode node, unsigned depth) {
continue;
/* These fields hold names being bound or spelled, not read. */
const char *field = ts_tree_cursor_current_field_name(&cursor);
if (cbm_source_is(child, "variable") && field &&
(!strcmp(field, "name") || !strcmp(field, "pattern") || !strcmp(field, "patterns")))
if (field && (!strcmp(field, "pattern") || !strcmp(field, "patterns")))
continue;
if (cbm_source_is(child, "variable") && field && !strcmp(field, "name"))
continue;
hs_visit(ctx, child, depth + 1);
} while (ts_tree_cursor_goto_next_sibling(&cursor));
@@ -322,9 +463,19 @@ static void hs_visit(hs_ctx_t *ctx, TSNode node, unsigned depth) {
ctx->reference = false;
return;
}
/* A qualified name's parts and a signature's names are not reads. */
if (!strcmp(kind, "constructor")) {
const char *name = cbm_source_text(ctx->walk, node);
ctx->reference = true;
if (name)
hs_resolve_bare(ctx, node, name);
ctx->reference = false;
return;
}
/* A qualified name's parts and a signature's names are not reads, and a declaration
* of a type spells constructors rather than reading them. */
if (!strcmp(kind, "qualified") || !strcmp(kind, "signature") || !strcmp(kind, "patterns") ||
!strcmp(kind, "imports") || !strcmp(kind, "exports") || !strcmp(kind, "header"))
!strcmp(kind, "imports") || !strcmp(kind, "exports") || !strcmp(kind, "header") ||
!strcmp(kind, "data_type") || !strcmp(kind, "newtype"))
return;
/* A do statement's or generator's pattern holds for the statements after it.
* Leaving it bound until the enclosing `do` ends can only make more names
@@ -374,8 +525,14 @@ static void hs_visit(hs_ctx_t *ctx, TSNode node, unsigned depth) {
TSNode head = hs_apply_head(node);
if (cbm_source_is(head, "variable")) {
hs_resolve_bare(ctx, node, cbm_source_text(ctx->walk, head));
} else if (cbm_source_is(head, "constructor")) {
ctx->constructor = true;
hs_resolve_bare(ctx, node, cbm_source_text(ctx->walk, head));
ctx->constructor = false;
} else if (cbm_source_is(head, "qualified")) {
ctx->constructor = cbm_source_is(cbm_source_field(head, "id"), "constructor");
hs_resolve_qualified(ctx, node, head);
ctx->constructor = false;
} else {
hs_emit(ctx, node, NULL, CBM_RESOLVE_RULE_NONE, NULL, "haskell_computed_function");
}
@@ -395,4 +552,5 @@ const cbm_source_adapter_t cbm_source_adapter_haskell = {.language = CBM_LANG_HA
.collect = hs_collect,
.resolve = hs_resolve,
.one_name_one_node = true,
.decides_references = true};
.decides_references = true,
.contains_parse_damage = true};
File diff suppressed because it is too large. Load diff
+250 -45
View File
@@ -18,8 +18,18 @@
* that isa P, so P's chain is what static rules can select from. Override in a
* subclass is runtime dispatch.
*
* `shift->m` and `$_[0]->m` read that same first argument directly. They count when
* nothing has shifted it away first, in a named sub of a package other than main that
* the project never calls as a plain function or exports. Only such a sub is known to
* be reached as a method; `_select($list)` hands over whatever it was given. The sub
* given for an operator in `use overload` gets the object first, by that pragma's own
* contract, and `my ($self, $x) = (shift, shift)` takes the first argument too.
*
* An import binds only through Exporter: the exporting package uses Exporter,
* has no sub import of its own, and lists the name in @EXPORT or @EXPORT_OK.
* has no sub import of its own, and lists the name in @EXPORT or @EXPORT_OK. What it
* lists can be a name it imported itself, which is then that module's export.
*
* `\&name` takes a reference to the sub the same lookup selects, without calling it.
*
* Literal glob assignment and lexical `my sub` replace a name, so that name
* abstains. A glob assigned from a computed name can touch any package anywhere
@@ -33,6 +43,8 @@ enum { PERL_MAX_BASES = 32 };
#define PERL_EXPORTER_KEY "&exporter"
#define PERL_EXPORT_PREFIX "&export:"
#define PERL_IMPORTED_PREFIX "&imported:"
#define PERL_FUNCTION_CALL_PREFIX "&function:"
typedef struct perl_import {
const char *package; /* importing package */
@@ -45,6 +57,10 @@ typedef struct {
cbm_source_walk_t *walk;
const char *package;
perl_import_t *imports;
CBMHashTable *function_calls; /* phase one: "package\x1fname" already noted in this file */
uint32_t first_shift; /* byte of the sub's first shift of its arguments */
bool reads_invocant; /* shift-> and $_[0]-> name the invocant here */
bool overload_list; /* directly in the list of a `use overload` */
} perl_ctx_t;
static const char *perl_field_text(perl_ctx_t *ctx, TSNode node, const char *name) {
@@ -193,6 +209,18 @@ static void perl_note_exporter(perl_ctx_t *ctx, const char *package, uint32_t li
line);
}
/* A name this package imported is in its symbol table, so it can export it onward. */
// cppcheck-suppress constParameterCallback
static void perl_declare_imported(perl_ctx_t *ctx, const char *word, void *user) {
const char *name = word[0] == '&' ? word + 1 : word;
const char *key = cbm_arena_sprintf(ctx->walk->arena, "%s%s", PERL_IMPORTED_PREFIX, name);
cbm_source_decl_t *decl =
key ? cbm_source_declare(ctx->walk, ctx->package, key, NULL, CBM_SOURCE_DECL_F_VALUE, 0)
: NULL;
if (decl)
decl->type = cbm_source_table_intern(ctx->walk, (const char *)user);
}
/* `use parent`, `use base` and Mojo::Base set @ISA at compile time. */
static void perl_collect_use(perl_ctx_t *ctx, TSNode use) {
const char *module = perl_field_text(ctx, use, "module");
@@ -205,8 +233,11 @@ static void perl_collect_use(perl_ctx_t *ctx, TSNode use) {
return;
}
bool mojo = !strcmp(module, "Mojo::Base");
if (strcmp(module, "parent") && strcmp(module, "base") && !mojo)
if (strcmp(module, "parent") && strcmp(module, "base") && !mojo) {
if (!ts_node_is_null(arguments))
(void)perl_literal_words(ctx, arguments, perl_declare_imported, (void *)module);
return;
}
perl_words_t words = {0};
bool literal =
ts_node_is_null(arguments) || perl_literal_words(ctx, arguments, perl_gather_word, &words);
@@ -300,6 +331,23 @@ static void perl_collect_sub(perl_ctx_t *ctx, TSNode node) {
CBM_SOURCE_DECL_F_PUBLIC, line);
}
/* Which subs the project calls as plain functions, once per file and name. */
static void perl_note_function_call(perl_ctx_t *ctx, TSNode call) {
const char *raw = cbm_source_text(ctx->walk, cbm_source_field(call, "function"));
if (!raw)
return;
const char *owner = NULL, *leaf = NULL;
perl_split(ctx, raw[0] == '&' ? raw + 1 : raw, ctx->package, &owner, &leaf);
const char *seen = cbm_arena_sprintf(ctx->walk->arena, "%s\x1f%s", owner, leaf);
if (!seen || !ctx->function_calls || cbm_ht_get(ctx->function_calls, seen))
return;
cbm_ht_set(ctx->function_calls, seen, (void *)seen);
const char *key = cbm_arena_sprintf(ctx->walk->arena, "%s%s", PERL_FUNCTION_CALL_PREFIX, leaf);
if (key)
(void)cbm_source_declare(ctx->walk, owner, key, NULL, CBM_SOURCE_DECL_F_VALUE,
ts_node_start_point(call).row + 1);
}
static void perl_collect_node(perl_ctx_t *ctx, TSNode node, unsigned depth) {
if (!cbm_source_step(ctx->walk, depth))
return;
@@ -352,7 +400,10 @@ static void perl_collect_node(perl_ctx_t *ctx, TSNode node, unsigned depth) {
cbm_source_named_field(child, "right"),
ts_node_start_point(child).row + 1);
}
} else if (cbm_source_is(child, "function_call_expression")) {
perl_note_function_call(ctx, child);
} else if (cbm_source_is(child, "ambiguous_function_call_expression")) {
perl_note_function_call(ctx, child);
/* `push @ISA, 'X'` */
const char *function = perl_field_text(ctx, child, "function");
TSNode arguments = cbm_source_field(child, "arguments");
@@ -380,8 +431,9 @@ static void perl_collect_node(perl_ctx_t *ctx, TSNode node, unsigned depth) {
}
static void perl_collect(cbm_source_walk_t *walk) {
perl_ctx_t ctx = {.walk = walk, .package = "main"};
perl_ctx_t ctx = {.walk = walk, .package = "main", .function_calls = cbm_ht_create(64)};
perl_collect_node(&ctx, walk->root, 0);
cbm_ht_free(ctx.function_calls);
}
/* ── Phase two ─────────────────────────────────────────────────────── */
@@ -483,24 +535,40 @@ static void perl_gather_imports(perl_ctx_t *ctx, TSNode node, unsigned depth) {
ts_tree_cursor_delete(&cursor);
}
/* The named export of an Exporter-based module, or nothing. */
static const char *perl_imported_target(perl_ctx_t *ctx, const char *name, const char **module) {
/* What a module exports under a name: its own sub, or one it imported in turn. A chain
* of modules visits each once, so a cycle ends and the cost is the chain's length. */
static bool perl_exported_sub(const perl_ctx_t *ctx, const char *module, const char *name,
cbm_source_chain_t *chain, const cbm_source_decl_t **out) {
char key[256];
int length = snprintf(key, sizeof(key), "%s%s", PERL_EXPORT_PREFIX, name);
if (length < 0 || (size_t)length >= sizeof(key) ||
!cbm_source_chain_first_visit(chain, module) ||
!cbm_source_lookup(ctx->walk, module, PERL_EXPORTER_KEY) ||
cbm_source_lookup(ctx->walk, module, "import") ||
!cbm_source_lookup(ctx->walk, module, key))
return false;
perl_lookup_t own = perl_own_sub(ctx, module, name, out);
if (own != PERL_ABSENT)
return own == PERL_FOUND;
length = snprintf(key, sizeof(key), "%s%s", PERL_IMPORTED_PREFIX, name);
if (length < 0 || (size_t)length >= sizeof(key))
return NULL;
return false;
const cbm_source_decl_t *imported = cbm_source_lookup(ctx->walk, module, key);
return imported && !imported->next && imported->type &&
perl_exported_sub(ctx, imported->type, name, chain, out);
}
/* The named export of an Exporter-based module, or nothing. */
static const char *perl_imported_target(perl_ctx_t *ctx, const char *name, const char **module) {
const char *selected = NULL;
for (const perl_import_t *import = ctx->imports; import; import = import->next) {
if (strcmp(import->package, ctx->package))
continue;
if (import->name && strcmp(import->name, name))
continue;
if (!cbm_source_lookup(ctx->walk, import->module, PERL_EXPORTER_KEY) ||
cbm_source_lookup(ctx->walk, import->module, "import") ||
!cbm_source_lookup(ctx->walk, import->module, key))
continue;
const cbm_source_decl_t *decl = NULL;
if (perl_own_sub(ctx, import->module, name, &decl) != PERL_FOUND)
cbm_source_chain_t chain = {0};
if (!perl_exported_sub(ctx, import->module, name, &chain, &decl))
continue;
if (selected && strcmp(selected, decl->qn))
return NULL;
@@ -515,6 +583,39 @@ static void perl_emit(perl_ctx_t *ctx, TSNode node, const char *target, cbm_reso
cbm_source_emit_node(ctx->walk, node, CBM_SOURCE_SITE_CALL, target, rule, domain, reason);
}
/* A sub named in the current package or spelled with its own. */
static void perl_emit_sub(perl_ctx_t *ctx, TSNode node, cbm_source_site_kind_t kind,
const char *text) {
const char *owner = NULL, *leaf = NULL;
perl_split(ctx, text, ctx->package, &owner, &leaf);
bool qualified = strstr(text, "::") != NULL;
const cbm_source_decl_t *decl = NULL;
perl_lookup_t own = perl_own_sub(ctx, owner, leaf, &decl);
if (own == PERL_FOUND) {
cbm_source_emit_node(ctx->walk, node, kind, decl->qn,
qualified ? CBM_RESOLVE_RULE_LANGUAGE_BINDING
: CBM_RESOLVE_RULE_SCOPE_BINDING,
owner, qualified ? "perl_qualified_sub" : "perl_current_package_sub");
return;
}
if (own == PERL_AMBIGUOUS) {
cbm_source_emit_node(ctx->walk, node, kind, NULL, CBM_RESOLVE_RULE_NONE, NULL,
"perl_sub_name_not_one_declaration");
return;
}
if (!qualified) {
const char *module = NULL;
const char *target = perl_imported_target(ctx, leaf, &module);
if (target) {
cbm_source_emit_node(ctx->walk, node, kind, target, CBM_RESOLVE_RULE_IMPORT_BINDING,
module, "perl_exporter_import");
return;
}
}
cbm_source_emit_node(ctx->walk, node, kind, NULL, CBM_RESOLVE_RULE_NONE, NULL,
"perl_sub_not_in_package");
}
static void perl_visit_function_call(perl_ctx_t *ctx, TSNode node) {
TSNode function = cbm_source_field(node, "function");
const char *raw = cbm_source_text(ctx->walk, function);
@@ -540,31 +641,26 @@ static void perl_visit_function_call(perl_ctx_t *ctx, TSNode node) {
perl_emit(ctx, node, NULL, CBM_RESOLVE_RULE_NONE, NULL, "perl_core_namespace");
return;
}
const cbm_source_decl_t *decl = NULL;
perl_lookup_t own = perl_own_sub(ctx, owner, leaf, &decl);
if (own == PERL_FOUND) {
perl_emit(ctx, node, decl->qn,
qualified ? CBM_RESOLVE_RULE_LANGUAGE_BINDING : CBM_RESOLVE_RULE_SCOPE_BINDING,
owner, qualified ? "perl_qualified_sub" : "perl_current_package_sub");
return;
}
if (own == PERL_AMBIGUOUS) {
perl_emit(ctx, node, NULL, CBM_RESOLVE_RULE_NONE, NULL,
"perl_sub_name_not_one_declaration");
return;
}
if (!qualified) {
const char *module = NULL;
const char *target = perl_imported_target(ctx, leaf, &module);
if (target) {
perl_emit(ctx, node, target, CBM_RESOLVE_RULE_IMPORT_BINDING, module,
"perl_exporter_import");
return;
}
}
perl_emit(ctx, node, NULL, CBM_RESOLVE_RULE_NONE, NULL, "perl_sub_not_in_package");
perl_emit_sub(ctx, node, CBM_SOURCE_SITE_CALL, text);
}
/* `\&name`: the sub the lookup selects, referred to rather than called. */
static void perl_visit_code_reference(perl_ctx_t *ctx, TSNode node) {
const char *text = cbm_source_text(ctx->walk, node);
const char *amp = text ? strchr(text, '&') : NULL;
if (!amp || amp == text || amp[-1] != '\\')
return;
const char *name = amp + 1;
for (const char *p = name; *p; p++)
if (!(*p == ':' || *p == '_' || (*p >= '0' && *p <= '9') || (*p >= 'a' && *p <= 'z') ||
(*p >= 'A' && *p <= 'Z')))
return; /* `\&$name` and `\&{...}` compute it. */
if (name[0])
perl_emit_sub(ctx, node, CBM_SOURCE_SITE_REFERENCE, name);
}
static bool perl_shifts_arguments(const perl_ctx_t *ctx, TSNode node);
/* The invocant variable a sub declares in its first statement or signature. */
static const char *perl_invocant(perl_ctx_t *ctx, TSNode sub) {
uint32_t parts = ts_node_named_child_count(sub);
@@ -584,14 +680,19 @@ static const char *perl_invocant(perl_ctx_t *ctx, TSNode sub) {
TSNode left = cbm_source_named_field(assign, "left");
if (!cbm_source_is(left, "variable_declaration"))
return NULL;
const char *right = cbm_source_text(ctx->walk, cbm_source_named_field(assign, "right"));
TSNode value = cbm_source_named_field(assign, "right");
const char *right = cbm_source_text(ctx->walk, value);
TSNode single = cbm_source_field(left, "variable");
if (cbm_source_is(single, "scalar"))
return right && !strcmp(right, "shift") ? cbm_source_text(ctx->walk, single) : NULL;
TSNode first = cbm_source_named_field(left, "variables");
return right && !strcmp(right, "@_") && cbm_source_is(first, "scalar")
? cbm_source_text(ctx->walk, first)
: NULL;
if (!cbm_source_is(first, "scalar"))
return NULL;
bool takes_first = right && !strcmp(right, "@_");
/* `my ($self, $name) = (shift, shift);` */
if (cbm_source_is(value, "list_expression") && ts_node_named_child_count(value))
takes_first = perl_shifts_arguments(ctx, ts_node_named_child(value, 0));
return takes_first ? cbm_source_text(ctx->walk, first) : NULL;
}
static bool perl_left_binds(perl_ctx_t *ctx, TSNode left, const char *variable, unsigned depth) {
@@ -625,12 +726,21 @@ static bool perl_rebinds(perl_ctx_t *ctx, TSNode node, const char *variable, uns
return false;
}
static bool perl_has_signature(TSNode sub) {
bool signature = false;
TSTreeCursor cursor = ts_tree_cursor_new(sub);
if (ts_tree_cursor_goto_first_child(&cursor)) {
do {
signature |= cbm_source_is(ts_tree_cursor_current_node(&cursor), "signature");
} while (ts_tree_cursor_goto_next_sibling(&cursor));
}
ts_tree_cursor_delete(&cursor);
return signature;
}
/* The statement that took the invocant is the one write that is allowed. */
static bool perl_body_rebinds(perl_ctx_t *ctx, TSNode sub, TSNode body, const char *variable) {
bool signature = false;
uint32_t parts = ts_node_named_child_count(sub);
for (uint32_t i = 0; i < parts; i++)
signature |= cbm_source_is(ts_node_named_child(sub, i), "signature");
bool signature = perl_has_signature(sub);
uint32_t count = ts_node_named_child_count(body);
for (uint32_t i = signature ? 0 : 1; i < count; i++)
if (perl_rebinds(ctx, ts_node_named_child(body, i), variable, 0))
@@ -638,6 +748,60 @@ static bool perl_body_rebinds(perl_ctx_t *ctx, TSNode sub, TSNode body, const ch
return false;
}
/* `shift`, `shift()`, `shift @_` or `shift(@_)`: the sub's arguments, not another array. */
static bool perl_shifts_arguments(const perl_ctx_t *ctx, TSNode node) {
uint32_t start = ts_node_start_byte(node), end = ts_node_end_byte(node);
size_t length = end - start;
const char *text = ctx->walk->source + start;
if (length < 5 || length > 9 || strncmp(text, "shift", 5) ||
strstr(ts_node_type(node), "bareword"))
return false;
return length == 5 || (length == 7 && !strncmp(text + 5, "()", 2)) ||
(length == 8 && !strncmp(text + 5, " @_", 3)) ||
(length == 9 && !strncmp(text + 5, "(@_)", 4));
}
/* The first shift of a sub's own arguments; a nested sub has an @_ of its own. */
static uint32_t perl_first_shift(perl_ctx_t *ctx, TSNode node, unsigned depth) {
if (!cbm_source_step(ctx->walk, depth) ||
cbm_source_is(node, "anonymous_subroutine_expression") ||
cbm_source_is(node, "subroutine_declaration_statement"))
return UINT32_MAX;
if (ts_node_is_named(node) && perl_shifts_arguments(ctx, node))
return ts_node_start_byte(node);
uint32_t first = UINT32_MAX;
TSTreeCursor cursor = ts_tree_cursor_new(node);
if (ts_tree_cursor_goto_first_child(&cursor)) {
do {
first = perl_first_shift(ctx, ts_tree_cursor_current_node(&cursor), depth + 1);
} while (first == UINT32_MAX && ts_tree_cursor_goto_next_sibling(&cursor));
}
ts_tree_cursor_delete(&cursor);
return first;
}
/* Reached only as a method, by what the project's own calls and exports show. */
static bool perl_only_a_method(const perl_ctx_t *ctx, const char *name) {
char key[256];
int length = snprintf(key, sizeof(key), "%s%s", PERL_FUNCTION_CALL_PREFIX, name);
if (length < 0 || (size_t)length >= sizeof(key) ||
cbm_source_lookup(ctx->walk, ctx->package, key))
return false;
length = snprintf(key, sizeof(key), "%s%s", PERL_EXPORT_PREFIX, name);
return length > 0 && (size_t)length < sizeof(key) &&
!cbm_source_lookup(ctx->walk, ctx->package, key);
}
static bool perl_reads_invocant(const perl_ctx_t *ctx, TSNode invocant) {
if (!ctx->reads_invocant)
return false;
if (perl_shifts_arguments(ctx, invocant))
return ts_node_start_byte(invocant) == ctx->first_shift;
uint32_t start = ts_node_start_byte(invocant);
return ts_node_end_byte(invocant) - start == 5 &&
!strncmp(ctx->walk->source + start, "$_[0]", 5) && start < ctx->first_shift;
}
static void perl_visit_method_call(perl_ctx_t *ctx, TSNode node, const char *invocant_var) {
TSNode method = cbm_source_field(node, "method");
TSNode invocant = cbm_source_field(node, "invocant");
@@ -666,6 +830,8 @@ static void perl_visit_method_call(perl_ctx_t *ctx, TSNode node, const char *inv
if (text && !strcmp(text, invocant_var))
start = ctx->package;
}
if (!start && perl_reads_invocant(ctx, invocant))
start = ctx->package;
if (!start) {
perl_emit(ctx, node, NULL, CBM_RESOLVE_RULE_NONE, NULL, "perl_invocant_class_unknown");
return;
@@ -743,10 +909,46 @@ static void perl_visit(perl_ctx_t *ctx, TSNode node, const char *invocant_var, u
if (conventional && !ts_node_is_null(body) &&
perl_body_rebinds(ctx, child, body, variable))
conventional = false;
if (!ts_node_is_null(body))
if (!ts_node_is_null(body)) {
uint32_t saved_shift = ctx->first_shift;
bool saved_reads = ctx->reads_invocant;
const char *name = perl_field_text(ctx, child, "name");
ctx->first_shift = perl_first_shift(ctx, body, depth + 1);
ctx->reads_invocant = name && strcmp(ctx->package, "main") &&
!strstr(name, "::") && !perl_has_signature(child) &&
perl_only_a_method(ctx, name);
perl_visit(ctx, body, conventional ? variable : NULL, depth + 1);
ctx->first_shift = saved_shift;
ctx->reads_invocant = saved_reads;
}
continue;
}
/* An anonymous sub closes over $self, but its @_ is its own. */
if (cbm_source_is(child, "anonymous_subroutine_expression")) {
bool saved_reads = ctx->reads_invocant, saved_list = ctx->overload_list;
uint32_t saved_shift = ctx->first_shift;
TSNode body = ts_node_named_child(child, 0);
ctx->reads_invocant = saved_list && strcmp(ctx->package, "main");
ctx->overload_list = false;
ctx->first_shift = ctx->reads_invocant && !ts_node_is_null(body)
? perl_first_shift(ctx, body, depth + 1)
: saved_shift;
perl_visit(ctx, child, invocant_var, depth + 1);
ctx->reads_invocant = saved_reads;
ctx->overload_list = saved_list;
ctx->first_shift = saved_shift;
continue;
}
if (cbm_source_is(child, "use_statement")) {
const char *pragma = cbm_source_text(ctx->walk, ts_node_named_child(child, 0));
bool saved_list = ctx->overload_list;
ctx->overload_list = pragma && !strcmp(pragma, "overload");
perl_visit(ctx, child, invocant_var, depth + 1);
ctx->overload_list = saved_list;
continue;
}
if (cbm_source_is(child, "refgen_expression"))
perl_visit_code_reference(ctx, child);
if (cbm_source_is(child, "function_call_expression") ||
cbm_source_is(child, "ambiguous_function_call_expression"))
perl_visit_function_call(ctx, child);
@@ -765,5 +967,8 @@ static void perl_resolve(cbm_source_walk_t *walk) {
perl_visit(&ctx, walk->root, NULL, 0);
}
const cbm_source_adapter_t cbm_source_adapter_perl = {
.language = CBM_LANG_PERL, .collect = perl_collect, .resolve = perl_resolve};
const cbm_source_adapter_t cbm_source_adapter_perl = {.language = CBM_LANG_PERL,
.collect = perl_collect,
.resolve = perl_resolve,
.decides_references = true,
.contains_parse_damage = true};
File diff suppressed because it is too large. Load diff
+449 -115
View File
@@ -19,14 +19,28 @@
* so a member found on the chain is the one selected, and an absent one is not guessed.
* Overloads are told apart by arity; those that share a graph node need no more.
*
* Known gaps, each abstaining: inferred return types, type members and path-dependent
* types, generic element types, infix and postfix calls, and calls to Java classes. */
* A pattern `case e: Foo` and a parameter `(y: Foo) =>` declare a type too.
*
* `a op b` is `a.op(b)`, and `a op: b` is `b.op:(a)`. `x.m` with m a method taking no
* arguments is a call of it, so `x.m.n()` reads n on m's return type.
*
* `def f = new Foo(...)` and `val v = new Foo(...)` have type Foo, as has a block ending
* in that. A type written in another file is read with that file's imports, which the
* table holds, so a base class or return type imported there is not left unknown there.
* An explicit import binds its name even when it leaves the project.
*
* `new Foo { ... }` opens a scope whose members are its own and those on Foo's chain. They
* hide every enclosing template, so a Foo outside the project leaves its bare names open.
*
* Known gaps, each abstaining: other inferred return types, type members and path-dependent
* types, generic element types, postfix calls, and calls to Java classes. */
#include "source_adapter_internal.h"
#include <stdio.h>
#include <stdlib.h>
#define SCALA_RECORD "&template"
#define SCALA_PACKAGE "&package"
#define SCALA_IMPORT "&import"
#define SCALA_TYPE_NAME "type:"
#define SCALA_OBJECT_NAME "object:"
@@ -45,6 +59,12 @@ typedef struct scala_import {
struct scala_import *next;
} scala_import_t;
/* An anonymous class around this point. */
typedef struct scala_anonymous {
const char *base; /* its template key, NULL when it is outside the project */
struct scala_anonymous *next;
} scala_anonymous_t;
typedef struct {
cbm_source_walk_t *walk;
const char *packages[SCALA_MAX_PACKAGES]; /* innermost first */
@@ -54,6 +74,11 @@ typedef struct {
scala_local_t *locals;
scala_import_t *imports; /* the file's */
scala_import_t *block_imports; /* in the blocks around this point */
scala_anonymous_t *anonymous;
TSNode callee; /* the function of the call being walked */
bool child_of_block; /* the node being visited stands directly in a block */
bool child_of_template; /* or directly in a template's body */
CBMHashTable *type_memo; /* (file, owner, name) to template key, for this file's walk */
} scala_ctx_t;
typedef enum {
@@ -72,6 +97,7 @@ typedef struct {
const char *package; /* SCALA_PACKAGE_REF */
const cbm_source_decl_t *decl; /* SCALA_FUNCTION; SCALA_CLASS: the class */
const cbm_source_decl_t *object_decl;
const cbm_source_decl_t *value_decl; /* the val or var a name read reaches */
bool via_import, via_receiver, from_super;
const char *reason;
} scala_ref_t;
@@ -151,7 +177,8 @@ static const char *scala_path(cbm_source_walk_t *walk, TSNode node) {
/* ── Phase one ─────────────────────────────────────────────────────── */
static void scala_note_params(cbm_source_walk_t *walk, TSNode function, cbm_source_decl_t *decl) {
TSNode parameters = cbm_source_field(function, "parameters");
/* The grammar names this field on a plain def but not on a generic one. */
TSNode parameters = scala_child_of_kind(function, "parameters");
decl->arity_min = decl->arity_max = 0;
if (ts_node_is_null(parameters))
return;
@@ -182,21 +209,37 @@ static uint32_t scala_flags(cbm_source_walk_t *walk, TSNode node) {
static void scala_collect_template(cbm_source_walk_t *walk, const char *package, const char *outer,
TSNode node);
/* The type of a right-hand side that says it: `new Foo(...)`, alone or ending a block. */
static const char *scala_constructed_type(cbm_source_walk_t *walk, TSNode value,
const char *type_params) {
while (cbm_source_is(value, "block")) {
uint32_t count = ts_node_named_child_count(value);
TSNode last = (TSNode){0};
for (uint32_t i = count; i > 0 && ts_node_is_null(last); i--) {
TSNode child = ts_node_named_child(value, i - 1);
if (!cbm_source_is(child, "comment") && !cbm_source_is(child, "block_comment"))
last = child;
}
value = last;
}
return cbm_source_is(value, "instance_expression")
? scala_type_text(walk, ts_node_named_child(value, 0), type_params)
: NULL;
}
static void scala_collect_value(cbm_source_walk_t *walk, const char *owner, TSNode node,
const char *type_params) {
TSNode pattern = cbm_source_field(node, "pattern");
if (!cbm_source_is(pattern, "identifier"))
return;
const char *type = scala_type_text(walk, cbm_source_field(node, "type"), type_params);
if (ts_node_is_null(cbm_source_field(node, "type"))) {
TSNode value = cbm_source_field(node, "value");
/* `val x = new Foo(...)`: the type it constructs. */
if (cbm_source_is(value, "instance_expression"))
type = scala_type_text(walk, ts_node_named_child(value, 0), type_params);
}
cbm_source_decl_t *decl = cbm_source_declare(walk, owner, cbm_source_text(walk, pattern), NULL,
CBM_SOURCE_DECL_F_VALUE | scala_flags(walk, node),
ts_node_start_point(node).row + 1);
if (ts_node_is_null(cbm_source_field(node, "type")))
type = scala_constructed_type(walk, cbm_source_field(node, "value"), type_params);
const char *name = cbm_source_text(walk, pattern);
uint32_t line = ts_node_start_point(pattern).row + 1;
cbm_source_decl_t *decl =
cbm_source_declare(walk, owner, name, cbm_source_def_qn(walk, name, line),
CBM_SOURCE_DECL_F_VALUE | scala_flags(walk, node), line);
if (decl && type)
decl->type = cbm_source_table_intern(walk, type);
}
@@ -213,8 +256,11 @@ static void scala_collect_function(cbm_source_walk_t *walk, const char *owner, T
if (!decl)
return;
scala_note_params(walk, node, decl);
const char *type = scala_type_text(walk, cbm_source_field(node, "return_type"),
scala_type_params(walk, node, type_params));
const char *params = scala_type_params(walk, node, type_params);
TSNode returned = cbm_source_field(node, "return_type");
const char *type = ts_node_is_null(returned)
? scala_constructed_type(walk, cbm_source_field(node, "body"), params)
: scala_type_text(walk, returned, params);
if (type)
decl->type = cbm_source_table_intern(walk, type);
}
@@ -275,9 +321,10 @@ static void scala_collect_class_parameters(cbm_source_walk_t *walk, const char *
continue;
const char *name = cbm_source_text(walk, cbm_source_field(param, "name"));
const char *type = scala_type_text(walk, cbm_source_field(param, "type"), type_params);
uint32_t line = ts_node_start_point(param).row + 1;
cbm_source_decl_t *decl =
name ? cbm_source_declare(walk, key, name, NULL, CBM_SOURCE_DECL_F_VALUE,
ts_node_start_point(param).row + 1)
name ? cbm_source_declare(walk, key, name, cbm_source_def_qn(walk, name, line),
CBM_SOURCE_DECL_F_VALUE, line)
: NULL;
if (decl && type)
decl->type = cbm_source_table_intern(walk, type);
@@ -341,7 +388,41 @@ static int scala_file_packages(cbm_source_walk_t *walk, const char **out) {
return used;
}
static void scala_add_import(scala_ctx_t *ctx, TSNode node, scala_import_t **list);
/* A file's imports stay in the table: a type written in that file is read through them. */
static const char *scala_imports_owner(CBMArena *arena, const char *file) {
return cbm_arena_sprintf(arena, "i:%s", file);
}
static void scala_collect_imports(cbm_source_walk_t *walk) {
scala_ctx_t ctx = {.walk = walk};
scala_import_t *imports = NULL;
TSTreeCursor cursor = ts_tree_cursor_new(walk->root);
if (ts_tree_cursor_goto_first_child(&cursor)) {
do {
TSNode child = ts_tree_cursor_current_node(&cursor);
if (cbm_source_is(child, "import_declaration"))
scala_add_import(&ctx, child, &imports);
} while (ts_tree_cursor_goto_next_sibling(&cursor));
}
ts_tree_cursor_delete(&cursor);
const char *owner = imports ? scala_imports_owner(walk->arena, walk->file) : NULL;
for (const scala_import_t *import = imports; import && owner; import = import->next) {
cbm_source_decl_t *decl =
cbm_source_declare(walk, owner, SCALA_IMPORT, NULL, CBM_SOURCE_DECL_F_TYPE, 1);
const char **parts = cbm_source_table_alloc(walk, 3 * sizeof(*parts));
if (!decl || !parts)
return;
decl->type = cbm_source_table_intern(walk, import->path);
parts[0] = cbm_source_table_intern(walk, import->member);
parts[1] = cbm_source_table_intern(walk, import->alias);
decl->bases = parts;
}
}
static void scala_collect(cbm_source_walk_t *walk) {
scala_collect_imports(walk);
const char *packages[SCALA_MAX_PACKAGES];
(void)scala_file_packages(walk, packages);
const char *package = packages[0];
@@ -447,7 +528,8 @@ static scala_ref_t scala_resolve_path(scala_ctx_t *ctx, const char *path) {
}
/* A written type name, seen from a declaring owner's packages. */
static const char *scala_lookup_type(scala_ctx_t *ctx, const char *owner, const char *text);
static const char *scala_lookup_type(scala_ctx_t *ctx, const char *owner, const char *text,
const char *file);
/* ── Phase two: templates ──────────────────────────────────────────── */
@@ -504,7 +586,7 @@ static scala_lookup_t scala_find_member(scala_ctx_t *ctx, const char *key, const
while (record->bases && record->bases[count])
count++;
for (size_t step = count; step > 0; step--) {
const char *parent = scala_lookup_type(ctx, key, record->bases[step - 1]);
const char *parent = scala_lookup_type(ctx, key, record->bases[step - 1], record->file);
if (!parent)
return SCALA_OPEN;
scala_lookup_t found = scala_find_member(ctx, parent, name, arguments, false, chain, out);
@@ -529,23 +611,80 @@ static const char *scala_class_key_of(const scala_ref_t *ref) {
return NULL;
}
/* Imports a declaring file made are not in this table; the current file's are. For a
* type named in another file, its own package and the enclosing templates decide, and
* a name that only that file's import would reach stays unknown. */
static const char *scala_lookup_type(scala_ctx_t *ctx, const char *owner, const char *text) {
if (!owner || !text)
return NULL;
cbm_source_walk_t *walk = ctx->walk;
const char *dot = strrchr(text, '.');
if (dot) {
scala_ref_t path = scala_resolve_path(ctx, text);
return scala_class_key_of(&path);
/* A path written in a file resolves from that file's package. */
static scala_ref_t scala_resolve_path_in(scala_ctx_t *ctx, const char *path, const char *owner,
const char *file) {
if (!strcmp(file, ctx->walk->file))
return scala_resolve_path(ctx, path);
const char *held = ctx->packages[0];
int count = ctx->package_count;
ctx->packages[0] = scala_key_package(ctx->walk, owner);
ctx->package_count = 1;
scala_ref_t found = scala_resolve_path(ctx, path);
ctx->packages[0] = held;
ctx->package_count = count;
return found;
}
/* A type or object a template inherits. Each template on the chain is read once. */
static scala_ref_t scala_inherited_names(scala_ctx_t *ctx, const char *key, const char *text,
cbm_source_chain_t *chain) {
scala_ref_t none = {.reason = "scala_name_not_in_project"};
if (!cbm_source_chain_first_visit(chain, key))
return none;
const cbm_source_decl_t *record = cbm_source_lookup(ctx->walk, key, SCALA_RECORD);
if (!record || record->next)
return none;
for (size_t i = 0; record->bases && record->bases[i]; i++) {
const char *parent = scala_lookup_type(ctx, key, record->bases[i], record->file);
if (!parent)
continue;
scala_ref_t found = scala_member_names(ctx, parent, text);
if (found.kind == SCALA_UNKNOWN)
found = scala_inherited_names(ctx, parent, text, chain);
if (found.kind != SCALA_UNKNOWN)
return found;
}
/* Templates from inside out. */
return none;
}
/* A type name as `file` reads it from `owner`: the templates around it, that file's explicit
* imports, its package, then its wildcard imports. */
static const char *scala_lookup_type_uncached(scala_ctx_t *ctx, const char *owner, const char *text,
const char *file) {
cbm_source_walk_t *walk = ctx->walk;
const char *imports_owner = scala_imports_owner(walk->arena, file);
const cbm_source_decl_t *imports =
imports_owner ? cbm_source_lookup(walk, imports_owner, SCALA_IMPORT) : NULL;
const char *dot = strchr(text, '.');
if (dot) {
scala_ref_t path = scala_resolve_path_in(ctx, text, owner, file);
const char *key = scala_class_key_of(&path);
if (key)
return key;
/* The first segment may be a name that file imported. */
size_t head = (size_t)(dot - text);
for (const cbm_source_decl_t *import = imports; import; import = import->next) {
const char *alias = import->bases[1];
if (!alias || strlen(alias) != head || strncmp(alias, text, head))
continue;
scala_ref_t full = scala_resolve_path_in(
ctx, cbm_arena_sprintf(walk->arena, "%s.%s%s", import->type, import->bases[0], dot),
owner, file);
return scala_class_key_of(&full);
}
return NULL;
}
/* Templates from inside out, each with what it inherits. */
char *scope = !strncmp(owner, "t:", 2) ? cbm_arena_strdup(walk->arena, owner) : NULL;
while (scope) {
scala_ref_t nested = scala_member_names(ctx, scope, text);
const char *key = scala_class_key_of(&nested);
if (!key) {
cbm_source_chain_t chain = {0};
scala_ref_t inherited = scala_inherited_names(ctx, scope, text, &chain);
key = scala_class_key_of(&inherited);
}
if (key)
return key;
char *up = strrchr(scope, '.');
@@ -554,51 +693,63 @@ static const char *scala_lookup_type(scala_ctx_t *ctx, const char *owner, const
break;
*up = '\0';
}
const char *package = scala_key_package(walk, owner);
/* From this file: its imports come before other files of the package. */
bool here = false;
for (int i = 0; i < ctx->package_count; i++)
here |= !strcmp(ctx->packages[i], package);
if (here) {
for (const scala_import_t *import = ctx->imports; import; import = import->next) {
if (!import->member || strcmp(import->alias, text))
continue;
scala_ref_t path = scala_resolve_path(
ctx, cbm_arena_sprintf(walk->arena, "%s.%s", import->path, import->member));
const char *key = scala_class_key_of(&path);
if (key)
return key;
}
/* An explicit import binds the name, also when it leaves the project. */
for (const cbm_source_decl_t *import = imports; import; import = import->next) {
if (!import->bases[0] || strcmp(import->bases[1], text))
continue;
scala_ref_t path = scala_resolve_path_in(
ctx, cbm_arena_sprintf(walk->arena, "%s.%s", import->type, import->bases[0]), owner,
file);
return scala_class_key_of(&path);
}
const char *package_owner = scala_package_owner(ctx, package);
const char *package_owner = scala_package_owner(ctx, scala_key_package(walk, owner));
scala_ref_t own =
package_owner ? scala_member_names(ctx, package_owner, text) : (scala_ref_t){0};
const char *key = scala_class_key_of(&own);
if (key)
return key;
if (here) {
for (const scala_import_t *import = ctx->imports; import; import = import->next) {
if (import->member)
continue;
scala_ref_t from = scala_resolve_path(ctx, import->path);
if (from.kind != SCALA_PACKAGE_REF)
continue;
scala_ref_t found =
scala_member_names(ctx, scala_package_owner(ctx, from.package), text);
key = scala_class_key_of(&found);
if (key)
return key;
}
for (const cbm_source_decl_t *import = imports; import; import = import->next) {
if (import->bases[0])
continue;
scala_ref_t from = scala_resolve_path_in(ctx, import->type, owner, file);
const char *inside = from.kind == SCALA_PACKAGE_REF
? scala_package_owner(ctx, from.package)
: (from.kind == SCALA_TEMPLATE ? from.key : NULL);
scala_ref_t found = inside ? scala_member_names(ctx, inside, text) : (scala_ref_t){0};
key = scala_class_key_of(&found);
if (key)
return key;
}
return NULL;
}
static const char *scala_lookup_type(scala_ctx_t *ctx, const char *owner, const char *text,
const char *file) {
if (!owner || !text || !file)
return NULL;
const char *memo_key =
ctx->type_memo ? cbm_arena_sprintf(ctx->walk->arena, "%s\x1f%s\x1f%s", file, owner, text)
: NULL;
const char *known = memo_key ? cbm_ht_get(ctx->type_memo, memo_key) : NULL;
if (known)
return known[0] ? known : NULL;
/* A base named through what it inherits itself finds nothing there while it is read. */
if (memo_key)
cbm_ht_set(ctx->type_memo, memo_key, (void *)"");
const char *key = scala_lookup_type_uncached(ctx, owner, text, file);
if (memo_key)
cbm_ht_set(ctx->type_memo, memo_key, (void *)(key ? key : ""));
return key;
}
static scala_ref_t scala_ref_of_member(scala_ctx_t *ctx, const cbm_source_decl_t *decl) {
scala_ref_t ref = {.reason = "scala_name_not_in_project"};
if (decl->flags & CBM_SOURCE_DECL_F_VALUE) {
const char *type = decl->type ? scala_lookup_type(ctx, decl->owner, decl->type) : NULL;
const char *type =
decl->type ? scala_lookup_type(ctx, decl->owner, decl->type, decl->file) : NULL;
ref.kind = type ? SCALA_VALUE : SCALA_UNKNOWN;
ref.key = type;
ref.value_decl = decl;
ref.reason = type ? "scala_call_of_a_value" : "scala_value_of_unknown_type";
return ref;
}
@@ -744,6 +895,22 @@ static scala_ref_t scala_name(scala_ctx_t *ctx, const char *name, int arguments)
ref.reason = "scala_outside_import_may_provide_name";
return ref;
}
for (const scala_anonymous_t *anonymous = ctx->anonymous; anonymous;
anonymous = anonymous->next) {
const cbm_source_decl_t *member = NULL;
cbm_source_chain_t chain = {0};
scala_lookup_t found =
anonymous->base
? scala_find_member(ctx, anonymous->base, name, arguments, false, &chain, &member)
: SCALA_OPEN;
if (found == SCALA_FOUND)
return scala_ref_of_member(ctx, member);
if (found != SCALA_ABSENT) {
ref.reason = found == SCALA_OPEN ? "scala_inheritance_leaves_project"
: "scala_overload_not_decided";
return ref;
}
}
/* Enclosing templates, inner to outer. */
char *scope = ctx->template_key ? cbm_arena_strdup(ctx->walk->arena, ctx->template_key) : NULL;
while (scope) {
@@ -769,6 +936,10 @@ static scala_ref_t scala_name(scala_ctx_t *ctx, const char *name, int arguments)
break;
}
scala_ref_t nested = scala_member_names(ctx, scope, name);
if (nested.kind != SCALA_UNKNOWN)
return nested;
cbm_source_chain_t names = {0};
nested = scala_inherited_names(ctx, scope, name, &names);
if (nested.kind != SCALA_UNKNOWN)
return nested;
char *up = strrchr(scope, '.');
@@ -834,6 +1005,8 @@ static int scala_argument_count(TSNode call) {
return (int)ts_node_named_child_count(arguments);
}
static scala_ref_t scala_call_result(scala_ctx_t *ctx, const scala_ref_t *callee, int arguments);
static scala_ref_t scala_member_of(scala_ctx_t *ctx, const scala_ref_t *target, const char *name,
int arguments) {
scala_ref_t ref = {.reason = "scala_receiver_type_unknown"};
@@ -852,6 +1025,18 @@ static scala_ref_t scala_member_of(scala_ctx_t *ctx, const scala_ref_t *target,
return scala_template_member(ctx, target->key, name, arguments, target->from_super);
if (target->kind == SCALA_TEMPLATE && target->key)
return scala_template_member(ctx, target->key, name, arguments, false);
/* `x.m.n`: a method read without arguments is called. */
if (target->kind == SCALA_FUNCTION && target->decl && target->decl->arity_min == 0) {
scala_ref_t value = scala_call_result(ctx, target, 0);
if (value.kind == SCALA_VALUE && value.key)
return scala_template_member(ctx, value.key, name, arguments, false);
if (value.reason)
ref.reason = value.reason;
return ref;
}
/* Why the receiver is unknown is why this member is. */
if (target->reason)
ref.reason = target->reason;
return ref;
}
@@ -862,7 +1047,8 @@ static scala_ref_t scala_call_result(scala_ctx_t *ctx, const scala_ref_t *callee
scala_ref_t result = {.reason = "scala_return_type_unknown"};
if (callee->kind == SCALA_FUNCTION) {
const char *type = callee->decl->type
? scala_lookup_type(ctx, callee->decl->owner, callee->decl->type)
? scala_lookup_type(ctx, callee->decl->owner, callee->decl->type,
callee->decl->file)
: NULL;
result.kind = type ? SCALA_VALUE : SCALA_UNKNOWN;
result.key = type;
@@ -886,6 +1072,23 @@ static scala_ref_t scala_call_result(scala_ctx_t *ctx, const scala_ref_t *callee
return result;
}
/* `f[T]` in `f[T](x)` is still f. */
static TSNode scala_written_callee(TSNode function) {
return cbm_source_is(function, "generic_function") ? cbm_source_field(function, "function")
: function;
}
/* `a op b` selects op on a, and `a op: b` selects it on b. */
static scala_ref_t scala_infix_member(scala_ctx_t *ctx, TSNode node, unsigned depth) {
const char *name = cbm_source_text(ctx->walk, cbm_source_field(node, "operator"));
if (!name || !name[0])
return (scala_ref_t){.reason = "scala_computed_receiver"};
bool right = name[strlen(name) - 1] == ':';
TSNode operand = cbm_source_field(node, right ? "right" : "left");
scala_ref_t target = scala_expr(ctx, operand, -1, depth);
return scala_member_of(ctx, &target, name, 1);
}
static scala_ref_t scala_expr(scala_ctx_t *ctx, TSNode node, int arguments, unsigned depth) {
scala_ref_t unknown = {.reason = "scala_computed_receiver"};
if (ts_node_is_null(node) || depth > 64)
@@ -911,15 +1114,20 @@ static scala_ref_t scala_expr(scala_ctx_t *ctx, TSNode node, int arguments, unsi
}
if (cbm_source_is(node, "call_expression")) {
int count = scala_argument_count(node);
scala_ref_t callee = scala_expr(ctx, cbm_source_field(node, "function"), count, depth + 1);
scala_ref_t callee = scala_expr(
ctx, scala_written_callee(cbm_source_field(node, "function")), count, depth + 1);
return scala_call_result(ctx, &callee, count);
}
if (cbm_source_is(node, "infix_expression")) {
scala_ref_t callee = scala_infix_member(ctx, node, depth + 1);
return scala_call_result(ctx, &callee, 1);
}
if (cbm_source_is(node, "instance_expression")) {
const char *type =
scala_type_text(ctx->walk, ts_node_named_child(node, 0), ctx->type_params);
const char *owner =
ctx->template_key ? ctx->template_key : scala_package_owner(ctx, ctx->packages[0]);
const char *key = type ? scala_lookup_type(ctx, owner, type) : NULL;
const char *key = type ? scala_lookup_type(ctx, owner, type, ctx->walk->file) : NULL;
if (key)
return (scala_ref_t){.kind = SCALA_VALUE, .key = key};
return unknown;
@@ -976,18 +1184,17 @@ static void scala_emit(scala_ctx_t *ctx, TSNode site, const scala_ref_t *ref, in
/* A name read as a value or a type follows the same lookup as a call's. A template
* reached by a type name is the class; by a term, the object. */
static void scala_visit_reference(scala_ctx_t *ctx, TSNode node, bool type_position) {
const char *name = cbm_source_text(ctx->walk, node);
if (!name || !strcmp(name, "this") || !strcmp(name, "super"))
return;
scala_ref_t ref = scala_name(ctx, name, -1);
static void scala_emit_reference(scala_ctx_t *ctx, TSNode node, const scala_ref_t *ref,
bool type_position) {
const cbm_source_decl_t *decl = NULL;
if (ref.kind == SCALA_FUNCTION && !type_position)
decl = ref.decl;
else if (ref.kind == SCALA_CLASS)
decl = ref.decl;
else if (ref.kind == SCALA_TEMPLATE)
decl = type_position ? ref.decl : ref.object_decl;
if (ref->kind == SCALA_FUNCTION && !type_position)
decl = ref->decl;
else if (ref->kind == SCALA_CLASS)
decl = ref->decl;
else if (ref->kind == SCALA_TEMPLATE)
decl = type_position ? ref->decl : ref->object_decl;
else if (ref->value_decl && !type_position)
decl = ref->value_decl;
if (!decl)
return;
const char *identity = NULL;
@@ -995,9 +1202,41 @@ static void scala_visit_reference(scala_ctx_t *ctx, TSNode node, bool type_posit
single.next = NULL;
const char *target = cbm_source_unique_qn(&single, &identity);
cbm_source_emit_node(ctx->walk, node, CBM_SOURCE_SITE_REFERENCE, target,
ref.via_import ? CBM_RESOLVE_RULE_IMPORT_BINDING
: CBM_RESOLVE_RULE_SCOPE_BINDING,
decl->owner, target ? ref.reason : identity);
ref->via_import ? CBM_RESOLVE_RULE_IMPORT_BINDING
: ref->via_receiver ? CBM_RESOLVE_RULE_TYPE_MEMBER
: CBM_RESOLVE_RULE_SCOPE_BINDING,
decl->owner, target ? ref->reason : identity);
}
static void scala_visit_reference(scala_ctx_t *ctx, TSNode node, bool type_position) {
const char *name = cbm_source_text(ctx->walk, node);
if (!name || !strcmp(name, "this") || !strcmp(name, "super"))
return;
scala_ref_t ref = scala_name(ctx, name, -1);
scala_emit_reference(ctx, node, &ref, type_position);
}
/* `x.name` read as a value, when it is no call's function. */
static void scala_visit_selection(scala_ctx_t *ctx, TSNode node) {
TSNode field = cbm_source_field(node, "field");
const char *name = cbm_source_text(ctx->walk, field);
if (!name)
return;
scala_ref_t target = scala_expr(ctx, cbm_source_field(node, "value"), -1, 0);
scala_ref_t ref = scala_member_of(ctx, &target, name, -1);
scala_emit_reference(ctx, field, &ref, false);
}
/* `a.b.Foo` in a type: the path names it, never a Foo in scope here. */
static void scala_visit_type_path(scala_ctx_t *ctx, TSNode node) {
const char *text = scala_type_text(ctx->walk, node, ctx->type_params);
uint32_t count = ts_node_named_child_count(node);
if (!text || !count)
return;
scala_ref_t ref = scala_resolve_path(ctx, text);
/* A mention sits on the last name, a type use on the whole path. */
scala_emit_reference(ctx, ts_node_named_child(node, count - 1), &ref, true);
scala_emit_reference(ctx, node, &ref, true);
}
/* ── Phase two: the walk ───────────────────────────────────────────── */
@@ -1005,6 +1244,8 @@ static void scala_visit_reference(scala_ctx_t *ctx, TSNode node, bool type_posit
static void scala_visit(scala_ctx_t *ctx, TSNode node, unsigned depth);
static void scala_visit_children(scala_ctx_t *ctx, TSNode node, unsigned depth) {
bool in_block = cbm_source_is(node, "block");
bool in_template = cbm_source_is(node, "template_body");
TSTreeCursor cursor = ts_tree_cursor_new(node);
if (ts_tree_cursor_goto_first_child(&cursor)) {
do {
@@ -1014,14 +1255,19 @@ static void scala_visit_children(scala_ctx_t *ctx, TSNode node, unsigned depth)
/* A field's name and a named argument's are spelled, not read. */
const char *field = ts_tree_cursor_current_field_name(&cursor);
if (cbm_source_is(child, "identifier") && field &&
(!strcmp(field, "field") || !strcmp(field, "left") || !strcmp(field, "name")))
(!strcmp(field, "field") || !strcmp(field, "name") || !strcmp(field, "operator") ||
(!strcmp(field, "left") && cbm_source_is(node, "assignment_expression"))))
continue;
ctx->child_of_block = in_block;
ctx->child_of_template = in_template;
scala_visit(ctx, child, depth + 1);
} while (ts_tree_cursor_goto_next_sibling(&cursor));
}
ts_tree_cursor_delete(&cursor);
}
static const char *scala_type_key_here(scala_ctx_t *ctx, TSNode type_node);
static void scala_bind_pattern(scala_ctx_t *ctx, TSNode node, unsigned depth) {
if (!cbm_source_step(ctx->walk, depth))
return;
@@ -1029,6 +1275,15 @@ static void scala_bind_pattern(scala_ctx_t *ctx, TSNode node, unsigned depth) {
scala_bind(ctx, cbm_source_text(ctx->walk, node), NULL);
return;
}
/* `case e: Foo` and `(y: Foo) =>` say the type of what they bind. */
if (cbm_source_is(node, "typed_pattern") || cbm_source_is(node, "binding")) {
TSNode name = ts_node_named_child(node, 0);
if (cbm_source_is(name, "identifier")) {
scala_bind(ctx, cbm_source_text(ctx->walk, name),
scala_type_key_here(ctx, ts_node_named_child(node, 1)));
return;
}
}
uint32_t count = ts_node_named_child_count(node);
for (uint32_t i = 0; i < count; i++)
scala_bind_pattern(ctx, ts_node_named_child(node, i), depth + 1);
@@ -1038,24 +1293,35 @@ static const char *scala_type_key_here(scala_ctx_t *ctx, TSNode type_node) {
const char *type = scala_type_text(ctx->walk, type_node, ctx->type_params);
const char *owner =
ctx->template_key ? ctx->template_key : scala_package_owner(ctx, ctx->packages[0]);
return type ? scala_lookup_type(ctx, owner, type) : NULL;
return type ? scala_lookup_type(ctx, owner, type, ctx->walk->file) : NULL;
}
static void scala_bind_parameters(scala_ctx_t *ctx, TSNode function) {
uint32_t count = ts_node_named_child_count(function);
for (uint32_t i = 0; i < count; i++) {
TSNode list = ts_node_named_child(function, i);
if (!cbm_source_is(list, "parameters"))
continue;
uint32_t params = ts_node_named_child_count(list);
for (uint32_t p = 0; p < params; p++) {
TSNode param = ts_node_named_child(list, p);
if (!cbm_source_is(param, "parameter"))
/* One parameter list: its types and defaults are read before its names come into scope,
* which the lists after it, and the body, see. A class's parameters go the same way. */
static void scala_visit_parameter_list(scala_ctx_t *ctx, TSNode list, unsigned depth) {
TSTreeCursor cursor = ts_tree_cursor_new(list);
if (ts_tree_cursor_goto_first_child(&cursor)) {
do {
TSNode param = ts_tree_cursor_current_node(&cursor);
if (!cbm_source_is(param, "parameter") && !cbm_source_is(param, "class_parameter"))
continue;
scala_bind(ctx, cbm_source_text(ctx->walk, cbm_source_field(param, "name")),
scala_type_key_here(ctx, cbm_source_field(param, "type")));
}
TSNode type = cbm_source_field(param, "type");
if (!ts_node_is_null(type))
scala_visit(ctx, type, depth + 1);
TSNode value = cbm_source_field(param, "default_value");
if (!ts_node_is_null(value))
scala_visit(ctx, value, depth + 1);
} while (ts_tree_cursor_goto_next_sibling(&cursor));
ts_tree_cursor_reset(&cursor, list);
(void)ts_tree_cursor_goto_first_child(&cursor);
do {
TSNode param = ts_tree_cursor_current_node(&cursor);
if (cbm_source_is(param, "parameter") || cbm_source_is(param, "class_parameter"))
scala_bind(ctx, cbm_source_text(ctx->walk, cbm_source_field(param, "name")),
scala_type_key_here(ctx, cbm_source_field(param, "type")));
} while (ts_tree_cursor_goto_next_sibling(&cursor));
}
ts_tree_cursor_delete(&cursor);
}
static void scala_add_import(scala_ctx_t *ctx, TSNode node, scala_import_t **list) {
@@ -1133,20 +1399,18 @@ static void scala_visit_function(scala_ctx_t *ctx, TSNode node, unsigned depth,
scala_import_t *saved_imports = ctx->block_imports;
const char *saved_params = ctx->type_params;
ctx->type_params = scala_type_params(ctx->walk, node, ctx->type_params);
uint32_t parts = ts_node_named_child_count(node);
for (uint32_t i = 0; i < parts; i++) {
TSNode list = ts_node_named_child(node, i);
uint32_t params = cbm_source_is(list, "parameters") ? ts_node_named_child_count(list) : 0;
for (uint32_t p = 0; p < params; p++) {
TSNode type = cbm_source_field(ts_node_named_child(list, p), "type");
if (!ts_node_is_null(type))
scala_visit(ctx, type, depth + 1);
}
TSTreeCursor cursor = ts_tree_cursor_new(node);
if (ts_tree_cursor_goto_first_child(&cursor)) {
do {
TSNode list = ts_tree_cursor_current_node(&cursor);
if (cbm_source_is(list, "parameters"))
scala_visit_parameter_list(ctx, list, depth);
} while (ts_tree_cursor_goto_next_sibling(&cursor));
}
ts_tree_cursor_delete(&cursor);
TSNode returned = cbm_source_field(node, "return_type");
if (!ts_node_is_null(returned))
scala_visit(ctx, returned, depth + 1);
scala_bind_parameters(ctx, node);
TSNode body = cbm_source_field(node, "body");
if (!ts_node_is_null(body))
scala_visit(ctx, body, depth + 1);
@@ -1164,11 +1428,24 @@ static void scala_visit_template(scala_ctx_t *ctx, TSNode node, unsigned depth)
const char *saved_params = ctx->type_params;
scala_local_t *saved_locals = ctx->locals;
scala_import_t *saved_imports = ctx->block_imports;
ctx->type_params = scala_type_params(ctx->walk, node, NULL);
/* A class's parameters and its parents' arguments are read outside its members. */
TSTreeCursor cursor = ts_tree_cursor_new(node);
if (ts_tree_cursor_goto_first_child(&cursor)) {
do {
TSNode part = ts_tree_cursor_current_node(&cursor);
if (cbm_source_is(part, "class_parameters"))
scala_visit_parameter_list(ctx, part, depth);
} while (ts_tree_cursor_goto_next_sibling(&cursor));
}
ts_tree_cursor_delete(&cursor);
TSNode extend = cbm_source_field(node, "extend");
if (!ts_node_is_null(extend))
scala_visit_children(ctx, extend, depth + 1);
ctx->template_key =
saved ? cbm_arena_sprintf(ctx->walk->arena, "%s.%s%s", saved, object ? "$" : "", name)
: cbm_arena_sprintf(ctx->walk->arena, "t:%s#%s%s", ctx->packages[0],
object ? "$" : "", name);
ctx->type_params = scala_type_params(ctx->walk, node, NULL);
ctx->locals = NULL;
TSNode body = cbm_source_field(node, "body");
if (!ts_node_is_null(body))
@@ -1196,11 +1473,11 @@ static void scala_visit(scala_ctx_t *ctx, TSNode node, unsigned depth) {
}
if (!strcmp(kind, "function_definition")) {
/* A member's name is declared in the template; a local one binds here. */
TSNode parent_is_body = ts_node_parent(node);
scala_visit_function(ctx, node, depth, cbm_source_is(parent_is_body, "block"));
scala_visit_function(ctx, node, depth, ctx->child_of_block);
return;
}
if (!strcmp(kind, "val_definition") || !strcmp(kind, "var_definition")) {
bool in_block = ctx->child_of_block;
TSNode value = cbm_source_field(node, "value");
TSNode pattern = cbm_source_field(node, "pattern");
TSNode type = cbm_source_field(node, "type");
@@ -1212,7 +1489,7 @@ static void scala_visit(scala_ctx_t *ctx, TSNode node, unsigned depth) {
}
if (!ts_node_is_null(value))
scala_visit(ctx, value, depth + 1);
if (ctx->template_key && !cbm_source_is(ts_node_parent(node), "block"))
if (ctx->template_key && !in_block)
return; /* A template's own member, declared in the table. */
if (cbm_source_is(pattern, "identifier"))
scala_bind(ctx, cbm_source_text(ctx->walk, pattern), key);
@@ -1221,13 +1498,32 @@ static void scala_visit(scala_ctx_t *ctx, TSNode node, unsigned depth) {
return;
}
if (!strcmp(kind, "call_expression")) {
TSNode function = cbm_source_field(node, "function");
TSNode function = scala_written_callee(cbm_source_field(node, "function"));
if (cbm_source_is(function, "identifier") || cbm_source_is(function, "field_expression")) {
int count = scala_argument_count(node);
scala_ref_t ref = scala_expr(ctx, function, count, 0);
scala_emit(ctx, node, &ref, count);
}
TSNode saved = ctx->callee;
ctx->callee = function;
scala_visit_children(ctx, node, depth);
ctx->callee = saved;
return;
}
if (!strcmp(kind, "infix_expression")) {
scala_ref_t ref = scala_infix_member(ctx, node, 0);
scala_emit(ctx, node, &ref, 1);
scala_visit_children(ctx, node, depth);
return;
}
if (!strcmp(kind, "field_expression") &&
(ts_node_is_null(ctx->callee) || !ts_node_eq(node, ctx->callee))) {
scala_visit_selection(ctx, node);
scala_visit_children(ctx, node, depth);
return;
}
if (!strcmp(kind, "stable_type_identifier")) {
scala_visit_type_path(ctx, node);
return;
}
if (!strcmp(kind, "instance_expression")) {
@@ -1243,7 +1539,41 @@ static void scala_visit(scala_ctx_t *ctx, TSNode node, unsigned depth) {
ref = (scala_ref_t){.reason = "scala_class_not_in_project"};
scala_emit(ctx, node, &ref, -1);
}
scala_visit_children(ctx, node, depth);
TSNode body = scala_child_of_kind(node, "template_body");
if (ts_node_is_null(body)) {
scala_visit_children(ctx, node, depth);
return;
}
TSTreeCursor cursor = ts_tree_cursor_new(node);
if (ts_tree_cursor_goto_first_child(&cursor)) {
do {
TSNode part = ts_tree_cursor_current_node(&cursor);
if (ts_node_is_named(part) && !ts_node_eq(part, body))
scala_visit(ctx, part, depth + 1);
} while (ts_tree_cursor_goto_next_sibling(&cursor));
}
ts_tree_cursor_delete(&cursor);
scala_anonymous_t anonymous = {.next = ctx->anonymous};
anonymous.base = scala_type_key_here(ctx, ts_node_named_child(node, 0));
scala_local_t *saved = ctx->locals;
ctx->anonymous = &anonymous;
/* Its own members are in no table, and hide what is around it. */
cursor = ts_tree_cursor_new(body);
if (ts_tree_cursor_goto_first_child(&cursor)) {
do {
TSNode member = ts_tree_cursor_current_node(&cursor);
if (cbm_source_is(member, "function_definition"))
scala_bind(ctx, cbm_source_text(ctx->walk, cbm_source_field(member, "name")),
NULL);
else if (cbm_source_is(member, "val_definition") ||
cbm_source_is(member, "var_definition"))
scala_bind_pattern(ctx, cbm_source_field(member, "pattern"), depth + 1);
} while (ts_tree_cursor_goto_next_sibling(&cursor));
}
ts_tree_cursor_delete(&cursor);
scala_visit_children(ctx, body, depth + 1);
ctx->anonymous = anonymous.next;
ctx->locals = saved;
return;
}
if (!strcmp(kind, "lambda_expression")) {
@@ -1274,15 +1604,18 @@ static void scala_visit(scala_ctx_t *ctx, TSNode node, unsigned depth) {
}
if (!strcmp(kind, "for_expression")) {
scala_local_t *saved = ctx->locals;
TSNode enumerators = cbm_source_field(node, "enumerators");
TSNode enumerators = scala_child_of_kind(node, "enumerators");
uint32_t count = ts_node_is_null(enumerators) ? 0 : ts_node_named_child_count(enumerators);
for (uint32_t i = 0; i < count; i++) {
TSNode enumerator = ts_node_named_child(enumerators, i);
uint32_t parts = ts_node_named_child_count(enumerator);
for (uint32_t p = 1; p < parts; p++)
scala_visit(ctx, ts_node_named_child(enumerator, p), depth + 1);
/* The generator reads what is outside; its guards see what it binds. */
if (parts > 1)
scala_visit(ctx, ts_node_named_child(enumerator, 1), depth + 1);
if (parts > 0)
scala_bind_pattern(ctx, ts_node_named_child(enumerator, 0), depth + 1);
for (uint32_t p = 2; p < parts; p++)
scala_visit(ctx, ts_node_named_child(enumerator, p), depth + 1);
}
TSNode body = cbm_source_field(node, "body");
if (!ts_node_is_null(body))
@@ -1291,8 +1624,7 @@ static void scala_visit(scala_ctx_t *ctx, TSNode node, unsigned depth) {
return;
}
if (!strcmp(kind, "import_declaration")) {
if (cbm_source_is(ts_node_parent(node), "block") ||
cbm_source_is(ts_node_parent(node), "template_body"))
if (ctx->child_of_block || ctx->child_of_template)
scala_add_import(ctx, node, &ctx->block_imports);
return;
}
@@ -1308,7 +1640,7 @@ static void scala_visit(scala_ctx_t *ctx, TSNode node, unsigned depth) {
}
static void scala_resolve(cbm_source_walk_t *walk) {
scala_ctx_t ctx = {.walk = walk};
scala_ctx_t ctx = {.walk = walk, .type_memo = cbm_ht_create(64)};
ctx.package_count = scala_file_packages(walk, ctx.packages);
uint32_t count = ts_node_named_child_count(walk->root);
for (uint32_t i = 0; i < count; i++) {
@@ -1317,10 +1649,12 @@ static void scala_resolve(cbm_source_walk_t *walk) {
scala_add_import(&ctx, child, &ctx.imports);
}
scala_visit_children(&ctx, walk->root, 0);
cbm_ht_free(ctx.type_memo);
}
const cbm_source_adapter_t cbm_source_adapter_scala = {.language = CBM_LANG_SCALA,
.collect = scala_collect,
.resolve = scala_resolve,
.one_name_one_node = true,
.decides_references = true};
.decides_references = true,
.contains_parse_damage = true};
+93 -21
View File
@@ -12,8 +12,11 @@
* x's container is known from the declared type of a parameter, variable or field
* (through pointers and optionals, which auto-dereference on member access), from
* a struct literal, or from the declared return type of the function it was
* initialized by. `@This()` is the enclosing container. Anything computed at
* comptime, including a generic `Foo(T)` and `anytype`, stays unknown.
* initialized by. `x orelse y`, `x catch y` and `x.*` have x's container, since the
* other arm must have that type or leave. `if (x) |m|` and `while (x) |m|` give m x's
* container: the capture unwraps an optional or error union. `@This()` is the
* enclosing container. Anything computed at comptime, including a generic `Foo(T)` and
* `anytype`, stays unknown, as does an element of a `for`.
*
* The graph's Zig QN holds the file but not the struct, so members of two structs
* in one file sharing a name share a node. That identity cannot name either one,
@@ -47,6 +50,10 @@ typedef struct {
zig_local_t *locals;
size_t local_count, local_capacity;
unsigned local_struct_depth;
TSNode callee; /* the function of the call being walked: its member is the call's site */
/* The `|x|` right after an if or while condition, and what that condition unwraps. */
TSNode capture;
const char *capture_container;
bool in_function;
} zig_ctx_t;
@@ -258,9 +265,24 @@ static const char *zig_parent(zig_ctx_t *ctx, const char *owner) {
static zig_ref_t zig_resolve_encoded(zig_ctx_t *ctx, const char *scope, const char *encoded,
unsigned depth);
/* What (container, name) denotes, from a site in `from`'s file. */
static zig_ref_t zig_member(zig_ctx_t *ctx, const char *container, const char *name,
unsigned depth) {
static size_t zig_file_length(const char *scope) {
const char *root_end = strchr(scope, '#');
return root_end ? (size_t)(root_end - scope) : strlen(scope);
}
/* A declaration without `pub` is visible only in its own file. `reader` is any scope of the
* file the name is written in, which for a field's type is the file that declares the field. */
static bool zig_visible_from(const char *reader, const char *container,
const cbm_source_decl_t *decl) {
size_t file_length = zig_file_length(container);
bool same_file = reader && zig_file_length(reader) == file_length &&
!strncmp(reader, container, file_length);
return same_file || (decl->flags & CBM_SOURCE_DECL_F_PUBLIC);
}
/* What (container, name) denotes, as written in `reader`'s file. */
static zig_ref_t zig_member(zig_ctx_t *ctx, const char *reader, const char *container,
const char *name, unsigned depth) {
zig_ref_t ref = {.reason = "zig_member_absent_from_container"};
const cbm_source_decl_t *decl = cbm_source_lookup(ctx->walk, container, name);
if (!decl)
@@ -269,11 +291,7 @@ static zig_ref_t zig_member(zig_ctx_t *ctx, const char *container, const char *n
ref.reason = "zig_member_not_one_declaration";
return ref;
}
const char *root_end = strchr(container, '#');
size_t file_length = root_end ? (size_t)(root_end - container) : strlen(container);
bool same_file = ctx->walk->file && strlen(ctx->walk->file) == file_length &&
!strncmp(ctx->walk->file, container, file_length);
if (!same_file && !(decl->flags & CBM_SOURCE_DECL_F_PUBLIC)) {
if (!zig_visible_from(reader, container, decl)) {
ref.reason = "zig_declaration_not_pub_in_other_file";
return ref;
}
@@ -292,7 +310,7 @@ static zig_ref_t zig_member(zig_ctx_t *ctx, const char *container, const char *n
static zig_ref_t zig_lexical(zig_ctx_t *ctx, const char *scope, const char *name, unsigned depth) {
for (const char *owner = scope; owner; owner = zig_parent(ctx, owner)) {
if (cbm_source_lookup(ctx->walk, owner, name))
return zig_member(ctx, owner, name, depth);
return zig_member(ctx, scope, owner, name, depth);
}
return (zig_ref_t){.reason = "zig_name_not_declared_in_enclosing_containers"};
}
@@ -341,7 +359,7 @@ static zig_ref_t zig_resolve_encoded(zig_ctx_t *ctx, const char *scope, const ch
next = strchr(segment, '|');
if (next)
*next++ = '\0';
ref = zig_member(ctx, ref.container, segment, depth + 1);
ref = zig_member(ctx, scope, ref.container, segment, depth + 1);
}
return ref;
}
@@ -392,13 +410,21 @@ static const char *zig_expr_container(zig_ctx_t *ctx, TSNode node, unsigned dept
zig_ref_t typed = zig_resolve_encoded(ctx, owner, decl->type, depth + 1);
return typed.kind == ZIG_CONTAINER ? typed.container : NULL;
}
zig_ref_t ref = zig_member(ctx, owner, name, depth + 1);
zig_ref_t ref = zig_member(ctx, walk->file, owner, name, depth + 1);
return ref.kind == ZIG_CONTAINER ? ref.container : NULL;
}
return NULL;
}
/* The grammar reads `*A.B` and `?A.B` as `(*A).B`: the same type path. */
if (cbm_source_is(node, "pointer_type") || cbm_source_is(node, "nullable_type")) {
uint32_t count = ts_node_named_child_count(node);
return count ? zig_expr_container(ctx, ts_node_named_child(node, count - 1), depth + 1)
: NULL;
}
if (cbm_source_is(node, "parenthesized_expression") || cbm_source_is(node, "try_expression") ||
cbm_source_is(node, "null_coercion_expression")) {
cbm_source_is(node, "null_coercion_expression") ||
cbm_source_is(node, "catch_expression") || cbm_source_is(node, "dereference_expression") ||
(cbm_source_is(node, "binary_expression") && cbm_source_token(node, "orelse"))) {
return ts_node_named_child_count(node)
? zig_expr_container(ctx, ts_node_named_child(node, 0), depth + 1)
: NULL;
@@ -427,7 +453,7 @@ static const char *zig_expr_container(zig_ctx_t *ctx, TSNode node, unsigned dept
zig_ref_t typed = zig_resolve_encoded(ctx, decl->owner, decl->type, depth + 1);
return typed.kind == ZIG_CONTAINER ? typed.container : NULL;
}
zig_ref_t ref = zig_member(ctx, object, member, depth + 1);
zig_ref_t ref = zig_member(ctx, walk->file, object, member, depth + 1);
return ref.kind == ZIG_CONTAINER ? ref.container : NULL;
}
if (cbm_source_is(node, "call_expression")) {
@@ -443,7 +469,7 @@ static const char *zig_expr_container(zig_ctx_t *ctx, TSNode node, unsigned dept
zig_expr_container(ctx, cbm_source_field(function, "object"), depth + 1);
const char *member = cbm_source_text(walk, cbm_source_field(function, "member"));
if (object && member)
callee = zig_member(ctx, object, member, depth + 1);
callee = zig_member(ctx, walk->file, object, member, depth + 1);
}
if (callee.kind != ZIG_FUNCTION || !callee.function->type)
return NULL;
@@ -515,7 +541,7 @@ static void zig_visit_call(zig_ctx_t *ctx, TSNode call) {
NULL, "zig_receiver_container_unknown");
return;
}
zig_ref_t ref = zig_member(ctx, object, member, 0);
zig_ref_t ref = zig_member(ctx, ctx->walk->file, object, member, 0);
zig_emit_ref(ctx, call, &ref, CBM_RESOLVE_RULE_TYPE_MEMBER);
return;
}
@@ -544,6 +570,25 @@ static void zig_visit_reference(zig_ctx_t *ctx, TSNode node) {
}
}
/* `Container.name` read as a value or type is that container's declaration. Zig forbids a
* field and a declaration of one name, and a field has no node, so a field read abstains. */
static void zig_visit_member(zig_ctx_t *ctx, TSNode node) {
if (ctx->local_struct_depth)
return;
const char *object = zig_expr_container(ctx, cbm_source_field(node, "object"), 0);
TSNode member = cbm_source_field(node, "member");
const char *name = cbm_source_text(ctx->walk, member);
const cbm_source_decl_t *decl =
object && name ? cbm_source_lookup(ctx->walk, object, name) : NULL;
if (!decl || !zig_visible_from(ctx->walk->file, object, decl))
return;
const char *identity = NULL;
const char *target = cbm_source_unique_qn(decl, &identity);
cbm_source_emit_node(ctx->walk, member, CBM_SOURCE_SITE_REFERENCE, target,
CBM_RESOLVE_RULE_TYPE_MEMBER, decl->owner,
target ? "zig_container_declaration_through_a_path" : identity);
}
static void zig_visit_children(zig_ctx_t *ctx, TSNode node, unsigned depth) {
/* These hold names being bound or spelled, not read. */
if (cbm_source_is(node, "block_label") || cbm_source_is(node, "break_label") ||
@@ -635,17 +680,34 @@ static void zig_visit(zig_ctx_t *ctx, TSNode node, unsigned depth) {
cbm_source_is(node, "if_expression") || cbm_source_is(node, "for_expression") ||
cbm_source_is(node, "while_expression") || cbm_source_is(node, "switch_case")) {
size_t saved = ctx->local_count;
TSNode saved_capture = ctx->capture;
const char *saved_container = ctx->capture_container;
/* Only the capture beside the condition unwraps it: `else |err|` is the error. */
if (cbm_source_is(node, "if_statement") || cbm_source_is(node, "if_expression") ||
cbm_source_is(node, "while_statement") || cbm_source_is(node, "while_expression")) {
TSNode capture = ts_node_named_child(node, 1);
if (cbm_source_is(capture, "payload")) {
ctx->capture = capture;
ctx->capture_container = zig_expr_container(ctx, ts_node_named_child(node, 0), 0);
}
}
zig_visit_children(ctx, node, depth);
ctx->local_count = saved;
ctx->capture = saved_capture;
ctx->capture_container = saved_container;
return;
}
if (cbm_source_is(node, "payload")) {
/* A capture's type comes from the expression it unwraps: not modelled. */
const char *unwrapped = !ts_node_is_null(ctx->capture) && ts_node_eq(node, ctx->capture)
? ctx->capture_container
: NULL;
uint32_t count = ts_node_named_child_count(node);
/* `|a, b|` destructures, which this walk has no type for. */
for (uint32_t i = 0; i < count; i++) {
TSNode child = ts_node_named_child(node, i);
if (cbm_source_is(child, "identifier"))
zig_push_local(ctx, cbm_source_text(ctx->walk, child), NULL);
zig_push_local(ctx, cbm_source_text(ctx->walk, child),
count == 1 ? unwrapped : NULL);
}
return;
}
@@ -675,8 +737,17 @@ static void zig_visit(zig_ctx_t *ctx, TSNode node, unsigned depth) {
}
return;
}
if (cbm_source_is(node, "call_expression"))
if (cbm_source_is(node, "call_expression")) {
zig_visit_call(ctx, node);
TSNode saved = ctx->callee;
ctx->callee = cbm_source_field(node, "function");
zig_visit_children(ctx, node, depth);
ctx->callee = saved;
return;
}
if (cbm_source_is(node, "field_expression") &&
(ts_node_is_null(ctx->callee) || !ts_node_eq(node, ctx->callee)))
zig_visit_member(ctx, node);
zig_visit_children(ctx, node, depth);
}
@@ -688,4 +759,5 @@ static void zig_resolve(cbm_source_walk_t *walk) {
const cbm_source_adapter_t cbm_source_adapter_zig = {.language = CBM_LANG_ZIG,
.collect = zig_collect,
.resolve = zig_resolve,
.decides_references = true};
.decides_references = true,
.contains_parse_damage = true};
+47 -26
View File
@@ -70,21 +70,26 @@ static const edge_row_t edge_rows[] = {
"Exact declared namespace members; unproved aliases and competing candidates abstain."},
{CBM_LANG_PHP, TYPED_EDGES | EDGE(USAGE),
"Declared namespaces, Composer PSR-4 and literal includes; dynamic paths abstain."},
{CBM_LANG_ELIXIR, EDGE(IMPORTS) | EDGE(CALLS),
{CBM_LANG_ELIXIR, EDGE(IMPORTS) | CALLABLE_EDGES | EDGE(USAGE),
"alias, import, require and use of a module declared once in the project; a module declared "
"twice or outside the project abstains."},
{CBM_LANG_ERLANG, EDGE(IMPORTS) | EDGE(CALLS),
{CBM_LANG_ERLANG, EDGE(IMPORTS) | CALLABLE_EDGES,
"-import and -behaviour of the module whose file bears its name, and -include next to the "
"source; a name borne by two files, -include_lib and macro paths abstain."},
{CBM_LANG_ZIG, EDGE(IMPORTS) | EDGE(CALLS) | EDGE(USAGE) | EDGE(USES_TYPE),
"@import of a literal .zig path relative to the importing file; names mapped by the build "
"abstain."},
{CBM_LANG_PERL, EDGE(IMPORTS) | EDGE(CALLS),
{CBM_LANG_PERL, EDGE(IMPORTS) | CALLABLE_EDGES,
"use of a module found under the lib directory of an ancestor of the importing file; other "
"@INC entries are not known and abstain."},
{CBM_LANG_HASKELL, EDGE(CALLS) | EDGE(USAGE), NULL},
{CBM_LANG_OCAML, EDGE(CALLS) | EDGE(USAGE), NULL},
{CBM_LANG_RUBY, EDGE(IMPORTS) | EDGE(CALLS),
{CBM_LANG_HASKELL, EDGE(IMPORTS) | EDGE(CALLS) | EDGE(USAGE),
"The file whose header declares the imported module; a module declared by two files and "
"modules outside the project abstain."},
{CBM_LANG_OCAML, EDGE(CALLS) | EDGE(USAGE),
"Units beside the file, under include_subdirs, and libraries by their dune name; opens, "
"includes and functor results of what those reach, as far as an interface shows them. What "
"a functor's parameter or a module outside the project holds abstains."},
{CBM_LANG_RUBY, EDGE(IMPORTS) | EDGE(CALLS) | EDGE(USAGE) | EDGE(WRITES),
"require_relative of a literal path, and require of a path under the lib directory of an "
"ancestor of the importing file; other load paths and interpolation abstain."},
{CBM_LANG_RUST, TYPED_EDGES,
@@ -122,7 +127,8 @@ static const edge_row_t edge_rows[] = {
"traversal, keyword arguments, conditions and prior unknown effects abstain."},
{CBM_LANG_SCALA, EDGE(IMPORTS) | EDGE(CALLS) | EDGE(USAGE) | EDGE(USES_TYPE),
"Packages, imports by precedence, templates and their linearized parents; declared receiver "
"types and `new`. Inferred return types, infix calls and Java classes abstain."},
"types and `new`, infix calls and value members. Other inferred return types and Java classes "
"abstain."},
{CBM_LANG_HCL, EDGE(IMPORTS) | EDGE(USAGE),
"Terraform/OpenTofu literal local module sources and Terragrunt literal paths with audited "
"parent-directory evaluation; remote, dynamic and ambiguous inputs abstain."},
@@ -333,14 +339,17 @@ bool cbm_resolve_capabilities(CBMLanguage language, cbm_resolve_capabilities_t *
out->adapter = "elixir_source";
out->proof_subsets = CBM_RESOLVE_CAP_SCOPE | CBM_RESOLVE_CAP_IMPORT_EXPORT;
out->limitation =
"Module functions by name and arity through defmodule, alias and import; quote, "
"protocols, computed modules and a graph node shared by two modules abstain.";
"Module functions by name and arity through defmodule, defprotocol, defimpl, alias, "
"import and what a `use` is known to return, captures, module names and exported "
"functions of an Erlang module; quote, computed modules, a __using__ that does not end "
"in a quote and a graph node shared by two modules abstain.";
break;
case CBM_LANG_ERLANG:
out->adapter = "erlang_source";
out->proof_subsets = CBM_RESOLVE_CAP_SCOPE | CBM_RESOLVE_CAP_IMPORT_EXPORT;
out->limitation = "Functions by name and arity, -import and exported m:f and ?MODULE:f; a "
"variable module or function and macros abstain.";
out->limitation = "Functions by name and arity, -import, exported m:f and ?MODULE:f, and "
"fun references to them; a variable module or function and macros "
"abstain.";
break;
case CBM_LANG_ZIG:
out->adapter = "zig_source";
@@ -348,40 +357,50 @@ bool cbm_resolve_capabilities(CBMLanguage language, cbm_resolve_capabilities_t *
CBM_RESOLVE_CAP_DECLARED_TYPE | CBM_RESOLVE_CAP_MEMBER |
CBM_RESOLVE_CAP_TYPE_FLOW;
out->limitation =
"Container declarations, relative @import and members through declared types and "
"return types; build-mapped imports, comptime types, captures and a graph node shared "
"by two structs of a file abstain.";
"Container declarations, relative @import and members through declared types, return "
"types, orelse, catch, dereference and if or while captures; build-mapped imports, "
"comptime types and for and switch captures abstain.";
break;
case CBM_LANG_PERL:
out->adapter = "perl_source";
out->proof_subsets =
CBM_RESOLVE_CAP_SCOPE | CBM_RESOLVE_CAP_IMPORT_EXPORT | CBM_RESOLVE_CAP_MEMBER;
out->limitation =
"Package subs, qualified subs, Exporter imports and Class->m or $self->m through @ISA; "
"builtins, computed names and a graph node shared by two packages abstain. A glob "
"assigned through a computed name is unbounded and is not modelled.";
"Package subs, qualified subs, Exporter imports and re-exports, code references, and "
"Class->m, $self->m, shift->m or $_[0]->m through @ISA, also in the operator subs of "
"use overload; builtins, computed names, a sub the project also calls as a function "
"and a graph node shared by two packages abstain. A glob assigned through a computed "
"name is unbounded and is not modelled.";
break;
case CBM_LANG_RUBY:
out->adapter = "ruby_source";
out->proof_subsets = CBM_RESOLVE_CAP_SCOPE | CBM_RESOLVE_CAP_MEMBER;
out->limitation =
"Implicit self, self and Const receivers on the lexical class's ancestor chain; blocks "
"that can rebind self, metaprogrammed names, unknown ancestors and reopened duplicates "
"abstain. Constants inherited from an ancestor are not modelled.";
"Implicit self, self, self.class, Const and Const.new receivers on the lexical class's "
"ancestor chain, super from a class, attribute writers, index operators, aliases and "
"module functions; "
"constant mentions by lexical scope and ancestors. A block handed to anything but a "
"core iterator or a "
"project method that keeps its self, metaprogrammed names, unknown ancestors, reopened "
"duplicates and locals with no node abstain.";
break;
case CBM_LANG_HASKELL:
out->adapter = "haskell_source";
out->proof_subsets = CBM_RESOLVE_CAP_SCOPE | CBM_RESOLVE_CAP_IMPORT_EXPORT;
out->limitation =
"Local bindings, top-level declarations, import lists and qualified imports against "
"the export list; re-exports, type-class dispatch and two providers abstain.";
"Local bindings, top-level declarations, data constructors, class methods, import "
"lists "
"and qualified imports against the export list; re-exports, which instance runs and "
"two providers abstain.";
break;
case CBM_LANG_OCAML:
out->adapter = "ocaml_source";
out->proof_subsets = CBM_RESOLVE_CAP_SCOPE | CBM_RESOLVE_CAP_IMPORT_EXPORT;
out->limitation =
"Sequential let scope, local shadowing, module aliases and compilation units in the "
"same directory; an open or include of an unknown module and functors abstain.";
"Sequential let scope, local shadowing, module aliases, scoped opens, includes, "
"functor results, module types and interfaces, compilation units beside the file or "
"under include_subdirs and dune libraries by name; an open or include of an unknown "
"module, and a value of a functor's parameter or of the standard library, abstain.";
break;
case CBM_LANG_JAVA:
case CBM_LANG_KOTLIN:
@@ -444,8 +463,10 @@ bool cbm_resolve_capabilities(CBMLanguage language, cbm_resolve_capabilities_t *
out->adapter = "scala_source";
out->proof_subsets = CBM_RESOLVE_CAP_SCOPE | CBM_RESOLVE_CAP_IMPORT_EXPORT |
CBM_RESOLVE_CAP_DECLARED_TYPE | CBM_RESOLVE_CAP_MEMBER;
out->limitation = "Packages, imports by precedence, linearized parents; declared receiver "
"types; inferred return types and infix calls abstain.";
out->limitation =
"Packages, imports by precedence with each file's own imports, linearized parents; "
"declared and `new` types, named infix calls, generic calls and value members; other "
"inferred return types, a chain leaving the project and Java classes abstain.";
break;
case CBM_LANG_SWIFT:
out->adapter = "swift_source";
+185 -16
View File
@@ -17,11 +17,17 @@ struct cbm_source_table {
bool failed;
};
typedef struct {
uint32_t start, end;
} source_damage_t;
struct cbm_source_unit {
cbm_source_site_t *sites;
size_t count, capacity;
cbm_source_extra_call_t *extras;
size_t extra_count, extra_capacity;
source_damage_t *damage; /* sorted, disjoint: no site overlapping one is decided */
size_t damage_count, damage_capacity;
const char *failure;
};
@@ -62,6 +68,15 @@ void cbm_source_table_set_packages(cbm_source_table_t *table, const CBMHashTable
table->packages = packages;
}
const char *cbm_source_table_base(const cbm_source_table_t *table, CBMLanguage language,
const char *qualified_name, size_t offset) {
const cbm_source_adapter_t *adapter = adapter_for(language);
if (!table || !adapter || !adapter->base || !qualified_name)
return NULL;
const cbm_source_decl_t *decl = cbm_ht_get(table->qn_owners, qualified_name);
return decl ? adapter->base(table, decl, offset) : NULL;
}
const char *cbm_source_table_package(const cbm_source_table_t *table, const char *name) {
return table && table->packages && name ? cbm_ht_get(table->packages, name) : NULL;
}
@@ -206,6 +221,19 @@ const char *cbm_source_def_qn_label(cbm_source_walk_t *walk, const char *name, u
return found;
}
const char *cbm_source_def_qn_of_name(cbm_source_walk_t *walk, const char *name) {
if (!name || !source_def_index(walk))
return NULL;
const char *found = NULL;
for (const source_def_link_t *link = cbm_ht_get(walk->def_index, name); link;
link = link->next) {
if (found && strcmp(found, link->def->qualified_name))
return NULL;
found = link->def->qualified_name;
}
return found;
}
const char *cbm_source_def_qn(cbm_source_walk_t *walk, const char *name, uint32_t line) {
return cbm_source_def_qn_label(walk, name, line, NULL);
}
@@ -279,25 +307,29 @@ cbm_source_decl_t *cbm_source_declare(cbm_source_walk_t *walk, const char *owner
}
return decl;
}
while (head->next)
head = head->next;
head->next = decl;
(head->last ? head->last : head)->next = decl;
head->last = decl;
return decl;
}
const cbm_source_decl_t *cbm_source_lookup(const cbm_source_walk_t *walk, const char *owner,
const char *name) {
const cbm_source_decl_t *cbm_source_lookup_in(const cbm_source_walk_t *walk, CBMLanguage language,
const char *owner, const char *name) {
if (!walk->table || !name)
return NULL;
/* Short keys avoid arena growth in the hottest path of the adapters. */
char buffer[768];
int length = snprintf(buffer, sizeof(buffer), "%d\x1f%s\x1f%s", (int)walk->language,
owner ? owner : "", name);
int length =
snprintf(buffer, sizeof(buffer), "%d\x1f%s\x1f%s", (int)language, owner ? owner : "", name);
if (length < 0 || (size_t)length >= sizeof(buffer))
return NULL;
return cbm_ht_get(walk->table->decls, buffer);
}
const cbm_source_decl_t *cbm_source_lookup(const cbm_source_walk_t *walk, const char *owner,
const char *name) {
return cbm_source_lookup_in(walk, walk->language, owner, name);
}
const char *cbm_source_unique_qn(const cbm_source_decl_t *first, const char **reason) {
*reason = "source_declaration_absent";
if (!first)
@@ -350,8 +382,13 @@ void cbm_source_emit_facts(cbm_source_walk_t *walk, uint32_t start, uint32_t end
unit->extras = extras;
unit->extra_capacity = capacity;
}
unit->extras[unit->extra_count++] = (cbm_source_extra_call_t){
walk->extra_scope ? walk->extra_scope : "", walk->extra_name, start, end};
unit->extras[unit->extra_count++] =
(cbm_source_extra_call_t){.scope = walk->extra_scope ? walk->extra_scope : "",
.name = walk->extra_name,
.start = start,
.end = end,
.kind = kind,
.line = walk->extra_line};
}
if (unit->count == unit->capacity) {
size_t capacity = unit->capacity ? unit->capacity * 2 : 64;
@@ -392,28 +429,147 @@ static TSTree *parse_source(const CBMFileResult *result, CBMLanguage language, c
return tree;
}
static const char *source_gate(const CBMFileResult *result, const char *source, size_t source_len) {
static const char *source_gate(const cbm_source_adapter_t *adapter, const CBMFileResult *result,
const char *source, size_t source_len) {
if (!result || !source || !result->module_qn || source_len > UINT32_MAX)
return "source_adapter_input_unavailable";
if (result->has_error || result->parse_incomplete || result->parse_unusable)
if (result->has_error || result->parse_unusable ||
(result->parse_incomplete && !adapter->contains_parse_damage))
return "source_adapter_parse_incomplete";
return NULL;
}
/* A grammar rejects a little valid code, and one rejection must not cost the whole file.
* What a rejection can break decides how far its damage reaches. Inside a function it can
* hide locals, so that whole function decides nothing. At class or file level it hides
* only what it spans, unless it may have changed the structure around it: an unbalanced
* delimiter or block keyword moves everything after it to another scope, a declaration
* keyword means a declaration was lost, and a MISSING token means an opener never closed.
* Those leave the file as a whole undecided, as before. */
enum { SOURCE_DAMAGE_MAX_DEPTH = 400 };
typedef struct {
CBMArena *arena; /* NULL: only ask whether the file can be trusted at all */
cbm_source_unit_t *unit;
const char *source;
const CBMLangSpec *spec;
} source_damage_scan_t;
static bool source_word_in(const char *text, size_t length, const char *const *words) {
for (size_t at = 0; at < length;) {
if (!isalpha((unsigned char)text[at]) && text[at] != '_') {
at++;
continue;
}
size_t end = at;
while (end < length && (isalnum((unsigned char)text[end]) || text[end] == '_'))
end++;
for (size_t w = 0; words[w]; w++)
if (strlen(words[w]) == end - at && !strncmp(text + at, words[w], end - at))
return true;
at = end;
}
return false;
}
static bool source_delimiters_balance(const char *text, size_t length) {
int round = 0, square = 0, curly = 0;
for (size_t i = 0; i < length; i++) {
round += (text[i] == '(') - (text[i] == ')');
square += (text[i] == '[') - (text[i] == ']');
curly += (text[i] == '{') - (text[i] == '}');
if (round < 0 || square < 0 || curly < 0)
return false;
}
return !round && !square && !curly;
}
static bool source_damage_add(source_damage_scan_t *scan, TSNode node) {
if (!scan->arena)
return true;
cbm_source_unit_t *unit = scan->unit;
if (unit->damage_count == unit->damage_capacity) {
size_t capacity = unit->damage_capacity ? unit->damage_capacity * 2 : 8;
source_damage_t *items = cbm_arena_alloc(scan->arena, capacity * sizeof(*items));
if (!items)
return false;
if (unit->damage_count)
memcpy(items, unit->damage, unit->damage_count * sizeof(*items));
unit->damage = items;
unit->damage_capacity = capacity;
}
unit->damage[unit->damage_count++] =
(source_damage_t){ts_node_start_byte(node), ts_node_end_byte(node)};
return true;
}
/* True when the damage below this node is contained. Reads only the nodes that hold an
* error, so a clean subtree costs nothing. */
static bool source_damage_scan(source_damage_scan_t *scan, TSNode node, bool in_function,
unsigned depth) {
static const char *const blocks[] = {"end", NULL};
static const char *const declarations[] = {
"class", "object", "trait", "def", "defp", "defmodule",
"module", "sub", "struct", "fn", "impl", "interface",
"package", "function", "defmacro", "enum", NULL};
if (depth > SOURCE_DAMAGE_MAX_DEPTH || ts_node_is_missing(node))
return false;
if (ts_node_is_error(node)) {
const char *text = scan->source + ts_node_start_byte(node);
size_t length = ts_node_end_byte(node) - ts_node_start_byte(node);
if (!source_delimiters_balance(text, length) || source_word_in(text, length, blocks) ||
(!in_function && source_word_in(text, length, declarations)))
return false;
return in_function || source_damage_add(scan, node);
}
if (!ts_node_has_error(node))
return true;
bool function = !in_function && cbm_kind_in_set(node, scan->spec->function_node_types);
if (function && !source_damage_add(scan, node))
return false;
bool contained = true;
TSTreeCursor cursor = ts_tree_cursor_new(node);
if (ts_tree_cursor_goto_first_child(&cursor)) {
do {
contained = source_damage_scan(scan, ts_tree_cursor_current_node(&cursor),
in_function || function, depth + 1);
} while (contained && ts_tree_cursor_goto_next_sibling(&cursor));
}
ts_tree_cursor_delete(&cursor);
return contained;
}
static bool source_tree_usable(const cbm_source_adapter_t *adapter, TSTree *tree, CBMArena *arena,
cbm_source_unit_t *unit, const char *source) {
if (!tree)
return false;
TSNode root = ts_tree_root_node(tree);
if (!ts_node_has_error(root))
return true;
if (!adapter->contains_parse_damage)
return false;
source_damage_scan_t scan = {
.arena = arena, .unit = unit, .source = source, .spec = cbm_lang_spec(adapter->language)};
return scan.spec && source_damage_scan(&scan, root, false, 0);
}
bool cbm_source_collect(cbm_source_table_t *table, const CBMFileResult *result,
CBMLanguage language, const char *file, const char *source,
size_t source_len) {
const cbm_source_adapter_t *adapter = adapter_for(language);
if (!table || !adapter)
return false;
if (source_gate(result, source, source_len)) {
bool gated = source_gate(adapter, result, source, source_len) != NULL;
if (gated && (!adapter->collect_unusable || !result || !source || !result->module_qn ||
source_len > UINT32_MAX)) {
/* Its declarations remain unknown competitors of every other file. */
table->incomplete[language] = true;
return !table->failed;
}
TSTree *owned = NULL;
TSTree *tree = parse_source(result, language, source, source_len, &owned);
if (!tree || ts_node_has_error(ts_tree_root_node(tree))) {
bool usable = !gated && source_tree_usable(adapter, tree, NULL, NULL, source);
if (!usable && (!tree || !adapter->collect_unusable)) {
table->incomplete[language] = true;
if (owned)
ts_tree_delete(owned);
@@ -429,7 +585,7 @@ bool cbm_source_collect(cbm_source_table_t *table, const CBMFileResult *result,
.source_len = source_len,
.writable = table,
.root = ts_tree_root_node(tree)};
adapter->collect(&walk);
(usable ? adapter->collect : adapter->collect_unusable)(&walk);
cbm_ht_free(walk.def_index);
if (walk.failure)
table->incomplete[language] = true;
@@ -461,12 +617,12 @@ cbm_source_unit_t *cbm_source_unit_build(CBMArena *arena, const cbm_source_table
unit->failure = "source_adapter_unavailable";
return unit;
}
unit->failure = source_gate(result, source, source_len);
unit->failure = source_gate(adapter, result, source, source_len);
if (unit->failure)
return unit;
TSTree *owned = NULL;
TSTree *tree = parse_source(result, language, source, source_len, &owned);
if (!tree || ts_node_has_error(ts_tree_root_node(tree))) {
if (!source_tree_usable(adapter, tree, arena, unit, source)) {
unit->failure = "source_adapter_parse_incomplete";
if (owned)
ts_tree_delete(owned);
@@ -509,6 +665,19 @@ void cbm_source_unit_site(const cbm_source_unit_t *unit, uint32_t start, uint32_
out->reason = unit->failure;
return;
}
/* The first damage ending after this site's start is the only one it can overlap. */
size_t low = 0, high = unit->damage_count;
while (low < high) {
size_t middle = low + (high - low) / 2;
if (unit->damage[middle].end <= start)
low = middle + 1;
else
high = middle;
}
if (low < unit->damage_count && unit->damage[low].start < end) {
out->reason = "source_adapter_parse_incomplete";
return;
}
out->reason = "source_occurrence_not_modelled";
if (!unit->count)
return;
+13 -3
View File
@@ -56,6 +56,12 @@ bool cbm_source_collect(cbm_source_table_t *table, const CBMFileResult *result,
size_t source_len);
bool cbm_source_table_incomplete(const cbm_source_table_t *table, CBMLanguage language);
/* The QN of a declared type's base class by this language's rules, for the graph's
* inheritance edges. offset counts the bases this language's rules can name; NULL means
* there are no more. */
const char *cbm_source_table_base(const cbm_source_table_t *table, CBMLanguage language,
const char *qualified_name, size_t offset);
/* Phase two. The table is read only and may be shared by parallel workers.
* NULL only on allocation failure; an unanalyzable file yields a unit whose
* sites all abstain with its failure reason. */
@@ -63,13 +69,17 @@ cbm_source_unit_t *cbm_source_unit_build(CBMArena *arena, const cbm_source_table
const CBMFileResult *result, CBMLanguage language,
const char *file, const char *source, size_t source_len);
/* Calls this language's rules prove at sites extraction has no carrier for, such as
* Ruby's bare method name or a call deep in a Dart selector chain. scope is the
* enclosing definition's QN, empty for the file. Only decided sites are listed. */
/* Occurrences this language's rules prove at sites extraction has no carrier for, such as
* Ruby's bare method name, a Ruby constant or a call deep in a Dart selector chain. scope
* is the enclosing definition's QN, empty for the file. kind says whether the site calls
* or mentions its target, line is the site's line when the adapter stated one, else 0.
* Only decided sites are listed. */
typedef struct {
const char *scope;
const char *name;
uint32_t start, end;
cbm_source_site_kind_t kind;
uint32_t line;
} cbm_source_extra_call_t;
const cbm_source_extra_call_t *cbm_source_unit_extra_calls(const cbm_source_unit_t *unit,
size_t *count);
@@ -51,6 +51,7 @@ typedef struct cbm_source_decl {
uint32_t flags;
uint32_t line;
struct cbm_source_decl *next;
struct cbm_source_decl *last; /* on a chain's head: its end, so appending stays constant */
} cbm_source_decl_t;
typedef struct {
@@ -76,16 +77,25 @@ typedef struct cbm_source_walk {
/* Set around an emit for a site extraction has no carrier for. */
const char *extra_scope;
const char *extra_name;
uint32_t extra_line;
} cbm_source_walk_t;
typedef void (*cbm_source_pass_fn)(cbm_source_walk_t *walk);
typedef const char *(*cbm_source_base_fn)(const cbm_source_table_t *table,
const cbm_source_decl_t *decl, size_t offset);
typedef struct {
CBMLanguage language;
cbm_source_pass_fn collect;
cbm_source_pass_fn resolve;
cbm_source_base_fn base; /* optional: a language whose inheritance only its rules can read */
bool decides_references;
bool one_name_one_node;
bool before_binding_rules;
/* A grammar's parse error stays where it is: see source_damage_scan. */
bool contains_parse_damage;
/* optional: collects a file whose tree is unusable, when the adapter has another way to
* read it. It sets walk->failure when it has none. */
cbm_source_pass_fn collect_unusable;
} cbm_source_adapter_t;
/* Per-language rule tables. A row here is what makes a language supported. */
@@ -143,6 +153,8 @@ bool cbm_source_token(TSNode node, const char *token);
/* Join one declaration to the definition extraction actually produced for it.
* The line must fall inside that definition's span. NULL means not materialized. */
const char *cbm_source_def_qn(cbm_source_walk_t *walk, const char *name, uint32_t line);
/* The one QN this file's definitions of a name share, wherever they stand, or NULL. */
const char *cbm_source_def_qn_of_name(cbm_source_walk_t *walk, const char *name);
const char *cbm_source_def_qn_label(cbm_source_walk_t *walk, const char *name, uint32_t line,
const char *label);
@@ -156,6 +168,10 @@ void *cbm_source_table_alloc(cbm_source_walk_t *walk, size_t size);
* the whole project, for this language. */
const cbm_source_decl_t *cbm_source_lookup(const cbm_source_walk_t *walk, const char *owner,
const char *name);
/* The same, in another language's declarations, for a call one language's rules state
* across to another: Elixir's `:mod.f()` names an Erlang module. */
const cbm_source_decl_t *cbm_source_lookup_in(const cbm_source_walk_t *walk, CBMLanguage language,
const char *owner, const char *name);
const char *cbm_source_table_package(const cbm_source_table_t *table, const char *name);
/* Phase two. Later rows for the same exact site replace earlier ones. */
+12
View File
@@ -4086,6 +4086,18 @@ static void resolve_def_inherits(resolve_ctx_t *rc, resolve_worker_state_t *ws,
ws->semantic_resolved++;
}
}
for (size_t b = 0; rc->pctx; b++) {
const char *base_qn = cbm_shared_resolver_source_base(rc->pctx->shared_resolver, fs->lang,
def->qualified_name, b);
if (!base_qn)
break;
const cbm_gbuf_node_t *bn = cbm_gbuf_find_by_qn(rc->main_gbuf, base_qn);
if (bn && node->id != bn->id) {
cbm_gbuf_insert_edge(ws->local_edge_buf, node->id, bn->id,
cbm_semantic_base_edge_type(bn), "{}");
ws->semantic_resolved++;
}
}
if (!def->base_classes) {
return;
}
+192 -8
View File
@@ -1626,6 +1626,108 @@ static void parse_python_manifest(const char *basename, const char *source, int
cbm_free(CBM_MEM_CLASS_SEMANTIC, directory);
}
/* dune: which directories hold which library, and how their subdirectories count. Reads
* the S-expressions as data: a form's head, and the `(name x)` and `(wrapped false)` of a
* library. Anything else in the file is skipped by its parentheses. */
static void dune_push(cbm_pkg_entries_t *entries, const char *kind, const char *key,
const char *value) {
char name[PKGMAP_PATH_BUF];
int length = snprintf(name, sizeof(name), "%s%s:%s", CBM_PKGMAP_RAW_PREFIX, kind, key);
if (length <= 0 || (size_t)length >= sizeof(name))
return;
pkg_entries_push(entries, cbm_mem_strdup(CBM_MEM_CLASS_SEMANTIC, name),
cbm_mem_strdup(CBM_MEM_CLASS_SEMANTIC, value));
}
static bool dune_is_library_name(const char *text, int length) {
for (int i = 0; i < length; i++)
if (!isalnum((unsigned char)text[i]) && text[i] != '_')
return false;
return length > 0;
}
/* A directory named x.t is a cram test: data to the build around it, whatever it holds. */
static bool dune_in_cram_directory(const char *rel_path) {
for (const char *mark = strstr(rel_path, ".t/"); mark; mark = strstr(mark + 1, ".t/"))
if (mark != rel_path && mark[-1] != '/')
return true;
return false;
}
static void parse_dune(const char *source, int source_len, const char *rel_path,
cbm_pkg_entries_t *entries) {
if (dune_in_cram_directory(rel_path))
return;
char *dir = path_dirname(rel_path);
const char *where = dir && dir[0] ? dir : ".";
char top[32] = "", field[32] = "", library[128] = "";
bool unwrapped = false, named = false;
int depth = 0, atoms_at_depth1 = 0, atoms_at_depth2 = 0;
for (const char *p = source, *end = source + source_len; p < end;) {
char c = *p;
if (c == ';') {
while (p < end && *p != '\n')
p++;
} else if (c == '"') {
for (p++; p < end && *p != '"'; p++)
if (*p == '\\')
p++;
p++;
} else if (c == '(') {
depth++;
if (depth == 1)
atoms_at_depth1 = 0;
if (depth == 2)
atoms_at_depth2 = 0;
p++;
} else if (c == ')') {
if (depth == 1) {
if (!strcmp(top, "library") && library[0]) {
dune_push(entries, "ocaml-lib", library, where);
dune_push(entries, "ocaml-dir", where, library);
if (unwrapped)
dune_push(entries, "ocaml-unwrapped", library, "1");
} else if (!strcmp(top, "executable") || !strcmp(top, "executables") ||
!strcmp(top, "test") || !strcmp(top, "tests")) {
dune_push(entries, "ocaml-dir", where, "executable");
}
top[0] = library[0] = '\0';
unwrapped = named = false;
}
depth--;
p++;
} else if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
p++;
} else {
const char *start = p;
while (p < end && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r' && *p != '(' &&
*p != ')' && *p != ';' && *p != '"')
p++;
int length = (int)(p - start);
if (depth == 1 && atoms_at_depth1++ == 0) {
snprintf(top, sizeof(top), "%.*s", length, start);
} else if (depth == 1 && !strcmp(top, "include_subdirs")) {
char mode[32];
snprintf(mode, sizeof(mode), "%.*s", length, start);
dune_push(entries, "ocaml-subdirs", where, mode);
} else if (depth == 2 && atoms_at_depth2++ == 0) {
snprintf(field, sizeof(field), "%.*s", length, start);
} else if (depth == 2 && atoms_at_depth2 == 2 && !strcmp(top, "library")) {
if (!strcmp(field, "name") && length < (int)sizeof(library)) {
snprintf(library, sizeof(library), "%.*s", length, start);
named = true;
} else if (!strcmp(field, "public_name") && !named &&
length < (int)sizeof(library) && dune_is_library_name(start, length)) {
/* With no `name`, a library takes this one, when it can be a module's. */
snprintf(library, sizeof(library), "%.*s", length, start);
} else if (!strcmp(field, "wrapped") && length == 5 && !strncmp(start, "false", 5))
unwrapped = true;
}
}
}
cbm_free(CBM_MEM_CLASS_SEMANTIC, dir);
}
/* ── Public: manifest detection + parsing ──────────────────────── */
bool cbm_pkgmap_try_parse(const char *basename, const char *rel_path, const char *source,
@@ -1687,6 +1789,10 @@ bool cbm_pkgmap_try_parse(const char *basename, const char *rel_path, const char
parse_package_swift(source, source_len, rel_path, entries);
return true;
}
if (strcmp(basename, "dune") == 0) {
parse_dune(source, source_len, rel_path, entries);
return true;
}
return false;
}
@@ -2100,12 +2206,19 @@ static bool is_pkgmap_manifest_basename(const char *basename) {
strcmp(basename, "pyproject.toml") == 0 || strcmp(basename, "composer.json") == 0 ||
strcmp(basename, "pubspec.yaml") == 0 || strcmp(basename, "pom.xml") == 0 ||
strcmp(basename, "build.gradle") == 0 || strcmp(basename, "build.gradle.kts") == 0 ||
strcmp(basename, "mix.exs") == 0 || strcmp(basename, "Package.swift") == 0) {
strcmp(basename, "mix.exs") == 0 || strcmp(basename, "Package.swift") == 0 ||
strcmp(basename, "dune") == 0) {
return true;
}
return ends_with(basename, ".gemspec");
}
/* `dune` also names directories, in repositories with nothing to do with OCaml. Only
* the file is a manifest, so anything else of that name withholds nothing. */
static bool pkgmap_name_must_be_a_manifest(const char *basename) {
return is_pkgmap_manifest_basename(basename) && strcmp(basename, "dune") != 0;
}
static void node_excluded_path(cbm_pkg_entries_t *entries, const char *path) {
node_collect(entries, "excluded", path, NULL, "1");
if (path_basename(path)[0] == '.')
@@ -2169,7 +2282,7 @@ static int pkgmap_walk_dir(const char *abs_dir, const char *rel_dir, cbm_pkg_ent
if (cbm_should_skip_dir(name, CBM_MODE_FULL) ||
cbm_pipeline_relpath_is_excluded(rel_path, excluded_dirs, excluded_count)) {
node_excluded_path(entries, rel_path);
if (is_pkgmap_manifest_basename(name))
if (pkgmap_name_must_be_a_manifest(name))
entries->incomplete = true;
continue;
}
@@ -2180,12 +2293,12 @@ static int pkgmap_walk_dir(const char *abs_dir, const char *rel_dir, cbm_pkg_ent
}
if (info.is_symlink) {
node_excluded_path(entries, rel_path);
if (is_pkgmap_manifest_basename(name))
if (pkgmap_name_must_be_a_manifest(name))
entries->incomplete = true;
continue;
}
if (info.is_directory) {
if (is_pkgmap_manifest_basename(name)) {
if (pkgmap_name_must_be_a_manifest(name)) {
entries->incomplete = true;
continue;
}
@@ -2199,13 +2312,16 @@ static int pkgmap_walk_dir(const char *abs_dir, const char *rel_dir, cbm_pkg_ent
if (!is_pkgmap_manifest_basename(name) && !optional)
continue;
if (!info.is_regular) {
entries->incomplete = entries->incomplete || !optional;
if (pkgmap_name_must_be_a_manifest(name))
entries->incomplete = true;
continue;
}
int source_len = 0;
char *source = pkgmap_read_file(abs_path, &source_len);
if (!source) {
entries->incomplete = entries->incomplete || !optional;
/* An empty dune file is valid and says nothing. */
if (pkgmap_name_must_be_a_manifest(name))
entries->incomplete = true;
continue;
}
if (cbm_pkgmap_try_parse(name, rel_path, source, source_len, entries)) {
@@ -2341,7 +2457,8 @@ CBMHashTable *cbm_pkgmap_build_from_files(const cbm_file_info_t *files, int file
int source_len = 0;
char *source = pkgmap_read_file(files[i].path, &source_len);
if (!source) {
entries.incomplete = true;
if (pkgmap_name_must_be_a_manifest(basename))
entries.incomplete = true;
continue;
}
cbm_pkgmap_try_parse(basename, files[i].rel_path, source, source_len, &entries);
@@ -2377,7 +2494,8 @@ CBMHashTable *cbm_pkgmap_build_from_repo(const char *repo_path, const cbm_file_i
int source_len = 0;
char *source = pkgmap_read_file(files[i].path, &source_len);
if (!source) {
entries.incomplete = true;
if (pkgmap_name_must_be_a_manifest(basename))
entries.incomplete = true;
continue;
}
cbm_pkgmap_try_parse(basename, files[i].rel_path, source, source_len, &entries);
@@ -3594,9 +3712,71 @@ static const char *elixir_full_module(const CBMFileResult *result, int index, ch
return length > 0 && (size_t)length < capacity ? out : NULL;
}
/* The name in a Haskell file's `module A.B` header, past comments and pragmas. A file
* with no header is Main, which nothing imports. */
static bool haskell_header_module(const char *source, size_t size, char *out, size_t capacity) {
size_t at = 0;
unsigned nested = 0;
while (at < size) {
if (nested) {
if (at + 1 < size && source[at] == '-' && source[at + 1] == '}') {
nested--;
at += 2;
} else if (at + 1 < size && source[at] == '{' && source[at + 1] == '-') {
nested++;
at += 2;
} else {
at++;
}
} else if (at + 1 < size && source[at] == '{' && source[at + 1] == '-') {
nested = 1;
at += 2;
} else if (at + 1 < size && source[at] == '-' && source[at + 1] == '-') {
while (at < size && source[at] != '\n')
at++;
} else if (source[at] == ' ' || source[at] == '\t' || source[at] == '\n' ||
source[at] == '\r') {
at++;
} else {
break;
}
}
if (size - at < 7 || strncmp(source + at, "module", 6) ||
(source[at + 6] != ' ' && source[at + 6] != '\t' && source[at + 6] != '\n' &&
source[at + 6] != '\r'))
return false;
at += 6;
while (at < size &&
(source[at] == ' ' || source[at] == '\t' || source[at] == '\n' || source[at] == '\r'))
at++;
size_t length = 0;
while (at + length < size &&
(isalnum((unsigned char)source[at + length]) || source[at + length] == '.' ||
source[at + length] == '_' || source[at + length] == '\''))
length++;
if (!length || length >= capacity)
return false;
memcpy(out, source + at, length);
out[length] = '\0';
return true;
}
static bool source_module_facts(const cbm_file_info_t *files, int count, CBMFileResult **cache,
CBMHashTable *map) {
for (int i = 0; i < count; i++) {
if (files[i].language == CBM_LANG_HASKELL) {
int size = 0;
char *source = pkgmap_read_file(files[i].path, &size);
char module[PKGMAP_PATH_BUF];
bool named = source && size > 0 &&
haskell_header_module(source, (size_t)size, module, sizeof(module));
bool put =
!named || source_module_put(map, "haskell_module", module, files[i].rel_path);
cbm_free(CBM_MEM_CLASS_EXTRACT, source);
if (!put)
return false;
continue;
}
if (files[i].language == CBM_LANG_ERLANG) {
char module[PKGMAP_PATH_BUF];
const char *leaf = path_leaf(files[i].rel_path);
@@ -5449,6 +5629,10 @@ static bool resolve_source_language_import(const cbm_pipeline_ctx_t *ctx, const
*reason = "elixir_declared_module";
*out = resolve_source_module(ctx, namespace_map, "elixir_module", path, ambiguous);
return true;
case CBM_LANG_HASKELL:
*reason = "haskell_declared_module";
*out = resolve_source_module(ctx, namespace_map, "haskell_module", path, ambiguous);
return true;
default:
return false;
}
+31 -5
View File
@@ -473,6 +473,11 @@ static bool add_target(cbm_shared_resolver_t *resolver, const CBMDefinition *def
bool same_owner = same_file && !strcmp(prior->label, definition->label);
bool same_span =
prior->start_line == definition->start_line && prior->end_line == definition->end_line;
/* `const Foo = struct {...}` is extracted as a variable and as its struct. */
if (language == CBM_LANG_ZIG && same_file && same_span) {
prior->one_name = true;
return true;
}
prior->cross_file |= !same_file;
if (same_owner &&
(language == CBM_LANG_C || language == CBM_LANG_CPP || language == CBM_LANG_CUDA)) {
@@ -3233,6 +3238,14 @@ void cbm_shared_resolver_fact(const cbm_shared_resolver_t *resolver, const char
cbm_resolve_solver_get(resolver ? resolver->solver : NULL, key, out);
}
const char *cbm_shared_resolver_source_base(const cbm_shared_resolver_t *resolver,
CBMLanguage language, const char *qualified_name,
size_t offset) {
return resolver
? cbm_source_table_base(resolver->source_table, language, qualified_name, offset)
: NULL;
}
const char *cbm_shared_resolver_python_base(const cbm_shared_resolver_t *resolver,
const char *qualified_name, size_t offset) {
return resolver ? cbm_py_members_base(resolver->python_members, qualified_name, offset) : NULL;
@@ -4418,7 +4431,9 @@ static bool record_result(const file_resolver_t *resolver, const char *scope, co
target->label && !strcmp(target->label, "Class")) ||
((resolver->file->language == CBM_LANG_DART ||
resolver->file->language == CBM_LANG_SWIFT ||
resolver->file->language == CBM_LANG_SCALA) &&
resolver->file->language == CBM_LANG_SCALA ||
resolver->file->language == CBM_LANG_RUBY ||
resolver->file->language == CBM_LANG_HASKELL) &&
target->label &&
(!strcmp(target->label, "Class") ||
(resolver->file->language == CBM_LANG_SWIFT && !strcmp(target->label, "Struct")))) ||
@@ -4549,10 +4564,20 @@ static bool resolve_calls(file_resolver_t *resolver, CBMArena *arena) {
cbm_source_unit_extra_calls(resolver->source_unit, &extra_count);
for (size_t e = 0; e < extra_count; e++) {
const cbm_source_extra_call_t *extra = &extras[e];
bool invocation = extra->kind == CBM_SOURCE_SITE_CALL;
cbm_resolve_result_t outcome = source_adapter_result(
resolver, extra->name, extra->start, extra->end, CBM_SOURCE_ORIGIN_RAW, true);
if (!record_result(resolver, extra->scope, extra->name, "CALLS", extra->start, extra->end,
CBM_SOURCE_ORIGIN_RAW, outcome, NULL))
resolver, extra->name, extra->start, extra->end, CBM_SOURCE_ORIGIN_RAW, invocation);
CBMCall site = {.start_line = (int)extra->line};
/* Naming something callable without calling it is a call reference. */
const char *relation =
invocation ? "CALLS"
: callable_node(outcome.target
? cbm_gbuf_find_by_qn(resolver->file->graph, outcome.target)
: NULL)
? "CALL_REFERENCE"
: "USAGE";
if (!record_result(resolver, extra->scope, extra->name, relation, extra->start, extra->end,
CBM_SOURCE_ORIGIN_RAW, outcome, &site))
return false;
}
return true;
@@ -4634,7 +4659,8 @@ static bool resolve_usages(file_resolver_t *resolver, CBMArena *arena) {
/* Source binding can establish a direct callable argument without a
* hybrid row. Composite expressions remain USAGE: the extraction
* carrier deliberately leaves may_be_call_reference false for them. */
if (!site && usage->may_be_call_reference && cbm_resolve_admit(&outcome) &&
if (!site && (usage->may_be_call_reference || resolver->file->language == CBM_LANG_PERL) &&
cbm_resolve_admit(&outcome) &&
callable_node(cbm_gbuf_find_by_qn(resolver->file->graph, outcome.target)))
relation = "CALL_REFERENCE";
/* Reading a property hands over its result, not the function. */
+12
View File
@@ -1895,6 +1895,18 @@ static void sem_process_def_edges(cbm_pipeline_ctx_t *ctx, const cbm_semantic_bi
(*inherits_count)++;
}
}
for (size_t b = 0;; b++) {
const char *base_qn =
cbm_shared_resolver_source_base(ctx->shared_resolver, lang, def->qualified_name, b);
if (!base_qn)
break;
const cbm_gbuf_node_t *base_node = cbm_gbuf_find_by_qn(ctx->gbuf, base_qn);
if (base_node && node->id != base_node->id) {
cbm_gbuf_insert_edge(ctx->gbuf, node->id, base_node->id,
cbm_semantic_base_edge_type(base_node), "{}");
(*inherits_count)++;
}
}
if (def->decorators) {
for (int dc = 0; def->decorators[dc]; dc++) {
resolve_decorator(ctx, node, def->decorators[dc], module_qn, imp_keys, imp_vals,
+4
View File
@@ -40,6 +40,10 @@ bool cbm_shared_resolver_exports_complete(const cbm_shared_resolver_t *resolver)
void cbm_shared_resolver_fact(const cbm_shared_resolver_t *, const char *,
cbm_resolve_solver_result_t *);
/* A Python class's proven project base, in source order: NULL past the end. */
/* A base class only a source adapter's rules can name. NULL once there are no more. */
const char *cbm_shared_resolver_source_base(const cbm_shared_resolver_t *resolver,
CBMLanguage language, const char *qualified_name,
size_t offset);
const char *cbm_shared_resolver_python_base(const cbm_shared_resolver_t *resolver,
const char *qualified_name, size_t offset);
bool cbm_shared_resolver_owns_proof(const cbm_shared_resolver_t *resolver,
+5 -2
View File
@@ -34,8 +34,11 @@ typedef struct cbm_store_read_scope cbm_store_read_scope_t;
* __image__<repository as written>, registry "unknown" -- no longer
* __image__docker.io/[library/]<repository>.
* 4 #1913: Go method QNs are receiver-qualified (pkg.Recv.method, generic
* receivers unwrapped); free functions stay pkg.func. */
#define CBM_INDEX_FORMAT_VERSION 4
* receivers unwrapped); free functions stay pkg.func.
* 5 OCaml functions carry their module path (file.Module.f), Zig functions
* their container (file.Foo.init), Scala class-body vals their class
* (file.Foo.x); same-named ones were one node. */
#define CBM_INDEX_FORMAT_VERSION 5
#define CBM_STORE_CANCELLED (-3)
#define CBM_STORE_SCAN_LIMIT (-4)
#define CBM_STORE_CALLBACK_ERR (-5)
+8 -4
View File
@@ -359,6 +359,10 @@ TEST(resolver_pipeline_ruby_parenthesized_call_binds_its_top_level_def) {
ASSERT_EQ(ruby_target_edges("def target\n 1\nend\ndef caller\n target\n target = 2\nend\n",
"main.rb"),
1);
/* A file runs top to bottom, so the later of two definitions is the one that stands. */
ASSERT_EQ(ruby_target_edges("def target\n 1\nend\ndef target\n 2\nend\ndef caller\n target()\nend\n",
"main.rb"),
1);
/* A local of that name cannot capture a call with parentheses. */
ASSERT_EQ(
ruby_target_edges("def target\n 1\nend\ndef caller\n target = 2\n target()\nend\n", "main.rb"),
@@ -374,13 +378,13 @@ TEST(resolver_pipeline_ruby_redefinition_and_metaprogramming_abstain) {
"def target\n 1\nend\ndef caller(target)\n target\nend\n",
"def target\n 1\nend\ndef caller\n [1].each { |target| target }\nend\n",
"def target\n 1\nend\ndef caller(s)\n /(?<target>x)/ =~ s\n target\nend\n",
/* Two definitions of one name. */
"def target\n 1\nend\ndef target\n 2\nend\ndef caller\n target()\nend\n",
/* Two definitions of one name, one of which may not run. */
"def target\n 1\nend\nif x\n def target\n 2\n end\nend\ndef caller\n target()\nend\n",
/* `define_method` replaces the literal name in the class. */
"class A\n def target\n 1\n end\n define_method(:target) { 2 }\n def caller\n target()\n "
"end\nend\n",
/* An attribute macro declares it too. */
"class A\n attr_reader :target\n def target\n 1\n end\n def caller\n target()\n "
/* An attribute macro after the def replaces it, and has no node of its own. */
"class A\n def target\n 1\n end\n attr_reader :target\n def caller\n target()\n "
"end\nend\n",
/* A prepended module outside the project comes before the class. */
"class A\n prepend Outside\n def target\n 1\n end\n def caller\n target()\n end\nend\n",
File diff suppressed because it is too large. Load diff