mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
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:
1 parent
c3dbdf21f8
commit
c1eb542277
25 files changed
+6920
-592
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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};
|
||||
@@ -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};
|
||||
@@ -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};
|
||||
+1145
-102
File diff suppressed because it is too large.
Load diff
@@ -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};
|
||||
+1171
-182
File diff suppressed because it is too large.
Load diff
@@ -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};
|
||||
@@ -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};
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
+2556
-10
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user