diff --git a/internal/cbm/extract_calls.c b/internal/cbm/extract_calls.c index a0760ca27..8a8d3b192 100644 --- a/internal/cbm/extract_calls.c +++ b/internal/cbm/extract_calls.c @@ -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); diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index 9c33d3741..f2ec41f92 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -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"); } diff --git a/internal/cbm/extract_unified.c b/internal/cbm/extract_unified.c index 8fd86cb6d..44c840322 100644 --- a/internal/cbm/extract_unified.c +++ b/internal/cbm/extract_unified.c @@ -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 diff --git a/internal/cbm/helpers.c b/internal/cbm/helpers.c index aaec9597e..e180227ce 100644 --- a/internal/cbm/helpers.c +++ b/internal/cbm/helpers.c @@ -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; } diff --git a/internal/cbm/lang_specs.c b/internal/cbm/lang_specs.c index a81b5d171..7576ea665 100644 --- a/internal/cbm/lang_specs.c +++ b/internal/cbm/lang_specs.c @@ -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", diff --git a/internal/cbm/resolve/adapter_elixir.c b/internal/cbm/resolve/adapter_elixir.c index 1aef1ce5f..45a80086a 100644 --- a/internal/cbm/resolve/adapter_elixir.c +++ b/internal/cbm/resolve/adapter_elixir.c @@ -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}; diff --git a/internal/cbm/resolve/adapter_erlang.c b/internal/cbm/resolve/adapter_erlang.c index 1950d212b..dbf38b17e 100644 --- a/internal/cbm/resolve/adapter_erlang.c +++ b/internal/cbm/resolve/adapter_erlang.c @@ -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}; diff --git a/internal/cbm/resolve/adapter_haskell.c b/internal/cbm/resolve/adapter_haskell.c index 45eea67ef..173dc772d 100644 --- a/internal/cbm/resolve/adapter_haskell.c +++ b/internal/cbm/resolve/adapter_haskell.c @@ -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 #include #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}; diff --git a/internal/cbm/resolve/adapter_ocaml.c b/internal/cbm/resolve/adapter_ocaml.c index 39118b048..7d8c15099 100644 --- a/internal/cbm/resolve/adapter_ocaml.c +++ b/internal/cbm/resolve/adapter_ocaml.c @@ -9,9 +9,35 @@ * * A file foo.ml is the compilation unit Foo. Within one directory a build's modules * see each other by name, unless something in scope defines a module of that name. - * `M.f` therefore looks through local module aliases, then at the unit beside this - * file, and only when no open in scope is open-ended. A functor argument, a first - * class module and anything behind `include` of an unknown module are not followed. */ + * `M.f` therefore looks through the modules opens in scope bring in and local module + * aliases, then at the unit beside this file, then at a library of that name. + * + * A dune file is a fact about this: `(library (name foo))` makes foo.ml in that + * directory the module Foo everywhere in the project, or with no foo.ml makes each unit + * there a member of Foo; `(include_subdirs unqualified)` makes the whole subtree one + * directory; `qualified` lets a unit see the directories above it and reads a + * subdirectory as a module. + * + * `include M` gives a unit what M has from that line on, and a later binding of one + * name replaces an earlier one. M can be a path, a literal `struct`, or an application + * of a functor, whose result has what the functor's own `struct` has. A functor's + * parameter has what its module type says and no more: a name there has no declaration + * to point at, but what is not there cannot come from it. A unit is closed when + * everything it includes is known; a module outside the project is not, and an open of + * such a unit leaves any name uncertain. `module X = Y` reads Y where it is written, + * before X exists, under the opens and includes of the `struct` around it. A unit is not + * in scope in its own body, so `include List` in list.ml is some other List. A functor + * argument's own names are its own; a first class module is not followed. + * + * A unit's interface says what it shows: a name a closed foo.mli does not list cannot be + * reached from outside, whatever foo.ml has, and a unit whose interface is closed is closed + * whatever foo.ml includes. The declarations are still the .ml's. + * + * The standard library's modules are known by name, and so is which modules each holds. + * Its values are not listed, so a name read through one abstains. A module type's name + * is kept apart from a module's by a `^`, which no identifier can begin with. + * + * `M.(e)` and `let open M in e` hold M open for e alone. */ #include "source_adapter_internal.h" #include #include @@ -19,6 +45,22 @@ #define OCAML_OPEN_ENDED "&open_ended" #define OCAML_MODULE_PREFIX "&module:" +#define OCAML_DECL_RECURSIVE (1u << 20) /* `module rec`: in scope in its own body */ +/* A functor's parameter: its module type is read where the functor is written, whose line is + * in arity_min, because the name itself is in scope from the functor's first line. */ +#define OCAML_DECL_PARAMETER (1u << 21) +#define OCAML_INCLUDE "&include" +/* A directory read as a module: its units are its members. */ +#define OCAML_DIRECTORY_PREFIX "&dir:" +/* Under `include_subdirs unqualified`, the units of a whole subtree by module name. */ +#define OCAML_SUBTREE_PREFIX "&subtree:" +#define OCAML_LIBRARY_KEY "@cbm:raw:ocaml-lib:" +#define OCAML_DIR_KEY "@cbm:raw:ocaml-dir:" +#define OCAML_SUBDIRS_KEY "@cbm:raw:ocaml-subdirs:" +#define OCAML_BLOCKED_PREFIX "@cbm:" +#define OCAML_STDLIB_PREFIX "&stdlib:" + +enum { OCAML_MAX_OPENS = 32 }; typedef struct ocaml_name { const char *name; @@ -28,6 +70,7 @@ typedef struct ocaml_name { typedef struct ocaml_open { const char *unit; /* a project unit's key, NULL when the module is not known */ uint32_t at, line; + bool include; struct ocaml_open *next; } ocaml_open_t; @@ -38,8 +81,17 @@ typedef struct { const char *unit; ocaml_name_t *locals; ocaml_open_t *opens; + /* This file's answers. Every site reads the same few opens, each with a closure of + * includes behind it, so each (unit, name) is worked out once per file. */ + CBMHashTable *closed_units; + CBMHashTable *member_modules; + CBMHashTable *member_values; + CBMHashTable *decl_paths; + bool in_functor_argument; /* a `let` here binds a name of the argument's own */ } ocaml_ctx_t; +typedef enum { OCAML_FOUND, OCAML_ABSENT, OCAML_UNKNOWN } ocaml_lookup_t; + /* src/foo/bar.ml is unit key src/foo/bar; its module name is Bar. */ static const char *ocaml_unit_key(cbm_source_walk_t *walk) { const char *file = walk->file ? walk->file : ""; @@ -47,22 +99,241 @@ static const char *ocaml_unit_key(cbm_source_walk_t *walk) { return dot ? cbm_arena_strndup(walk->arena, file, (size_t)(dot - file)) : file; } -/* The unit a module name denotes beside this file. */ -static const char *ocaml_sibling_unit(const ocaml_ctx_t *ctx, const char *module) { - char lowered[256]; - size_t length = strlen(module); - if (!length || length >= sizeof(lowered)) +static const char *ocaml_manifest(cbm_source_walk_t *walk, const char *prefix, const char *key) { + const cbm_source_table_t *table = walk->table ? walk->table : walk->writable; + char name[640]; + int length = snprintf(name, sizeof(name), "%s%s", prefix, key); + const char *value = + length > 0 && (size_t)length < sizeof(name) ? cbm_source_table_package(table, name) : NULL; + return value && strncmp(value, OCAML_BLOCKED_PREFIX, strlen(OCAML_BLOCKED_PREFIX)) ? value + : NULL; +} + +/* The directory holding a unit key: "." at the root, so a manifest can name it. */ +static const char *ocaml_directory_of(cbm_source_walk_t *walk, const char *unit) { + const char *hash = strchr(unit, '#'); + size_t length = hash ? (size_t)(hash - unit) : strlen(unit); + const char *slash = NULL; + for (size_t i = 0; i < length; i++) + if (unit[i] == '/') + slash = unit + i; + return slash ? cbm_arena_strndup(walk->arena, unit, (size_t)(slash - unit)) : "."; +} + +static const char *ocaml_parent_directory(cbm_source_walk_t *walk, const char *directory) { + const char *slash = strrchr(directory, '/'); + if (slash) + return cbm_arena_strndup(walk->arena, directory, (size_t)(slash - directory)); + return strcmp(directory, ".") ? "." : NULL; +} + +static const char *ocaml_lowered(cbm_source_walk_t *walk, const char *module) { + char *copy = cbm_arena_strdup(walk->arena, module); + if (copy) + copy[0] = (char)tolower((unsigned char)copy[0]); + return copy; +} + +/* A unit of this directory, by its file or, under `unqualified`, anywhere below it. */ +static const char *ocaml_directory_member(const ocaml_ctx_t *ctx, const char *directory, + const char *lowered) { + const char *key = strcmp(directory, ".") + ? cbm_arena_sprintf(ctx->walk->arena, "%s/%s", directory, lowered) + : lowered; + if (key && cbm_source_lookup(ctx->walk, key, "")) + return key; + const char *subtree = + cbm_arena_sprintf(ctx->walk->arena, "%s%s", OCAML_SUBTREE_PREFIX, directory); + const cbm_source_decl_t *found = + subtree ? cbm_source_lookup(ctx->walk, subtree, lowered) : NULL; + return found && !found->next ? found->type : NULL; +} + +/* The module a name denotes from a unit's place in the tree, with nothing in scope. */ +static const char *ocaml_sibling_unit(const ocaml_ctx_t *ctx, const char *from, + const char *module) { + cbm_source_walk_t *walk = ctx->walk; + const char *lowered = ocaml_lowered(walk, module); + if (!lowered) return NULL; - memcpy(lowered, module, length + 1); - lowered[0] = (char)tolower((unsigned char)lowered[0]); - const char *slash = strrchr(ctx->unit, '/'); - const char *key = slash ? cbm_arena_sprintf(ctx->walk->arena, "%.*s/%s", - (int)(slash - ctx->unit), ctx->unit, lowered) - : cbm_arena_strdup(ctx->walk->arena, lowered); - return key && cbm_source_lookup(ctx->walk, key, "") ? key : NULL; + for (const char *directory = ocaml_directory_of(walk, from); directory; + directory = ocaml_parent_directory(walk, directory)) { + const char *unit = ocaml_directory_member(ctx, directory, lowered); + if (unit) + return unit; + const char *subdirs = ocaml_manifest(walk, OCAML_SUBDIRS_KEY, directory); + /* `qualified` reads a subdirectory as a module, and looks up through its parents + * as far as the directory that declares it. Everything else stops here. */ + if (subdirs && !strcmp(subdirs, "qualified")) { + const char *below = strcmp(directory, ".") + ? cbm_arena_sprintf(walk->arena, "%s/%s", directory, lowered) + : lowered; + if (below && + cbm_source_lookup( + walk, cbm_arena_sprintf(walk->arena, "%s%s", OCAML_DIRECTORY_PREFIX, below), + "")) + return cbm_arena_sprintf(walk->arena, "%s%s", OCAML_DIRECTORY_PREFIX, below); + break; + } + /* Below an include_subdirs the units are still that library's. */ + const char *parent = ocaml_parent_directory(walk, directory); + if (!parent || !ocaml_manifest(walk, OCAML_SUBDIRS_KEY, parent)) + break; + } + return NULL; +} + +/* A library's module: its main unit, or with none its directory, whose units are then + * the members. A library can name one outside what the project built only by its name. */ +static const char *ocaml_library_module(const ocaml_ctx_t *ctx, const char *module) { + cbm_source_walk_t *walk = ctx->walk; + const char *lowered = ocaml_lowered(walk, module); + const char *directory = lowered ? ocaml_manifest(walk, OCAML_LIBRARY_KEY, lowered) : NULL; + if (!directory) + return NULL; + const char *main_unit = ocaml_directory_member(ctx, directory, lowered); + return main_unit ? main_unit + : cbm_arena_sprintf(walk->arena, "%s%s", OCAML_DIRECTORY_PREFIX, directory); +} + +static bool ocaml_is_directory_module(const char *container) { + return !strncmp(container, OCAML_DIRECTORY_PREFIX, strlen(OCAML_DIRECTORY_PREFIX)); +} + +/* From the manual's chapter on the standard library, sorted for bsearch. A module that holds + * modules is followed by them; a functor's result holds none. Those with holdings this does + * not need (Bigarray, Ephemeron, Obj) are left out, and so stay unknown. */ +static const char *const OCAML_STDLIB_MODULES[] = { + "Arg", + "Array", + "ArrayLabels", + "Atomic", + "Bool", + "Buffer", + "Bytes", + "BytesLabels", + "Callback", + "Char", + "Complex", + "Condition", + "Digest", + "Digest.BLAKE128", + "Digest.BLAKE256", + "Digest.BLAKE512", + "Digest.MD5", + "Domain", + "Domain.DLS", + "Dynarray", + "Effect", + "Effect.Deep", + "Effect.Shallow", + "Either", + "Filename", + "Float", + "Float.Array", + "Float.ArrayLabels", + "Format", + "Fun", + "Gc", + "Gc.Memprof", + "Hashtbl", + "Hashtbl.Make", + "Hashtbl.MakeSeeded", + "In_channel", + "Int", + "Int32", + "Int64", + "Lazy", + "Lexing", + "List", + "ListLabels", + "Map", + "Map.Make", + "Marshal", + "MoreLabels", + "MoreLabels.Hashtbl", + "MoreLabels.Hashtbl.Make", + "MoreLabels.Hashtbl.MakeSeeded", + "MoreLabels.Map", + "MoreLabels.Map.Make", + "MoreLabels.Set", + "MoreLabels.Set.Make", + "Mutex", + "Nativeint", + "Oo", + "Option", + "Out_channel", + "Parsing", + "Printexc", + "Printexc.Slot", + "Printf", + "Queue", + "Random", + "Random.State", + "Result", + "Scanf", + "Scanf.Scanning", + "Semaphore", + "Semaphore.Binary", + "Semaphore.Counting", + "Seq", + "Set", + "Set.Make", + "Stack", + "StdLabels", + "StdLabels.Array", + "StdLabels.Bytes", + "StdLabels.List", + "StdLabels.String", + "String", + "StringLabels", + "Sys", + "Sys.Immediate64", + "Type", + "Type.Id", + "Uchar", + "Unit", + "Weak", + "Weak.Make", +}; + +static int ocaml_stdlib_compare(const void *key, const void *entry) { + return strcmp((const char *)key, *(const char *const *)entry); +} + +static bool ocaml_is_stdlib(const char *container) { + return !strncmp(container, OCAML_STDLIB_PREFIX, strlen(OCAML_STDLIB_PREFIX)); +} + +/* A module of the standard library by its path below Stdlib, or NULL. */ +static const char *ocaml_stdlib_module(const ocaml_ctx_t *ctx, const char *path) { + bool known = bsearch(path, OCAML_STDLIB_MODULES, + sizeof(OCAML_STDLIB_MODULES) / sizeof(OCAML_STDLIB_MODULES[0]), + sizeof(OCAML_STDLIB_MODULES[0]), ocaml_stdlib_compare) != NULL; + return known ? cbm_arena_sprintf(ctx->walk->arena, "%s%s", OCAML_STDLIB_PREFIX, path) : NULL; +} + +/* `from`, a module inside it, or its interface, is this unit. */ +static bool ocaml_is_own_unit(const char *unit, const char *from) { + size_t length = strlen(unit); + return !strncmp(unit, from, length) && + (from[length] == '\0' || from[length] == '#' || from[length] == '.'); +} + +/* The module a name denotes by place alone. A unit is not in scope in its own body. */ +static const char *ocaml_unit_by_place(const ocaml_ctx_t *ctx, const char *from, + const char *module) { + const char *unit = ocaml_sibling_unit(ctx, from, module); + if (!unit || ocaml_is_own_unit(unit, from)) + unit = ocaml_library_module(ctx, module); + if (unit && !ocaml_is_own_unit(unit, from)) + return unit; + return !strcmp(module, "Stdlib") ? OCAML_STDLIB_PREFIX : ocaml_stdlib_module(ctx, module); } static TSNode ocaml_child(TSNode node, const char *kind) { + if (ts_node_is_null(node)) + return (TSNode){0}; uint32_t count = ts_node_named_child_count(node); for (uint32_t i = 0; i < count; i++) if (cbm_source_is(ts_node_named_child(node, i), kind)) @@ -86,17 +357,266 @@ static void ocaml_collect_binding(cbm_source_walk_t *walk, const char *owner, TS } static void ocaml_collect_structure(cbm_source_walk_t *walk, const char *owner, TSNode structure, + const char **opens, size_t *open_count, unsigned depth); + +/* A module path is read where it is written, under the opens in force there. Each is kept + * as "line:path", or "line+path" for an include, because a binding after an open comes before + * it. */ +static const char *const *ocaml_opens_copy(cbm_source_walk_t *walk, const char *const *opens, + size_t count) { + if (!count) + return NULL; + const char **copy = cbm_source_table_alloc(walk, (count + 1) * sizeof(*copy)); + for (size_t o = 0; copy && o < count; o++) + copy[o] = cbm_source_table_intern(walk, opens[o]); + return copy; +} + +/* What is open at this point of a struct. Past what can be kept, the last one reads as a + * module no one knows, and so does an empty path. */ +static void ocaml_push_open(cbm_source_walk_t *walk, const char **opens, size_t *count, + uint32_t line, const char *path, bool include) { + const char *entry = + cbm_arena_sprintf(walk->arena, "%u%c%s", line, include ? '+' : ':', path ? path : ""); + if (!entry) + return; + if (*count < OCAML_MAX_OPENS) + opens[(*count)++] = entry; + else + opens[OCAML_MAX_OPENS - 1] = cbm_arena_sprintf(walk->arena, "%u:", line); +} + +/* A literal struct inlined here: its opens end with it, and what it includes stays. */ +static void ocaml_collect_inlined(cbm_source_walk_t *walk, const char *owner, TSNode structure, + const char **opens, size_t *open_count, unsigned depth) { + const char *inner[OCAML_MAX_OPENS]; + size_t inner_count = *open_count; + memcpy(inner, opens, inner_count * sizeof(*inner)); + ocaml_collect_structure(walk, owner, structure, inner, &inner_count, depth + 1); + for (size_t i = *open_count; i < inner_count; i++) { + char *rest = NULL; + unsigned long line = strtoul(inner[i], &rest, 10); + if (rest && *rest == '+') + ocaml_push_open(walk, opens, open_count, (uint32_t)line, rest + 1, true); + } +} + +/* The path a module expression reads: M, M of `(M : S)`, F of `F (A) (B)`. */ +static TSNode ocaml_module_read(TSNode body) { + while (cbm_source_is(body, "typed_module_expression") || + cbm_source_is(body, "module_application") || + cbm_source_is(body, "parenthesized_module_expression")) + body = ts_node_named_child_count(body) ? ts_node_named_child(body, 0) : (TSNode){0}; + return cbm_source_is(body, "module_path") ? body : (TSNode){0}; +} + +/* `include M`: what it adds is known only through M, and is in scope below it. */ +static void ocaml_collect_include(cbm_source_walk_t *walk, const char *owner, TSNode item, + const char **outer_opens, size_t *outer_count, unsigned depth) { + uint32_t line = ts_node_start_point(item).row + 1; + TSNode body = ts_node_named_child_count(item) ? ts_node_named_child(item, 0) : (TSNode){0}; + if (cbm_source_is(body, "typed_module_expression") && ts_node_named_child_count(body)) + body = ts_node_named_child(body, 0); + if (cbm_source_is(body, "structure")) { + ocaml_collect_inlined(walk, owner, body, outer_opens, outer_count, depth); + return; + } + const char *path = cbm_source_text(walk, ocaml_module_read(body)); + if (path) { + cbm_source_decl_t *decl = + cbm_source_declare(walk, owner, OCAML_INCLUDE, NULL, CBM_SOURCE_DECL_F_VALUE, line); + if (decl) { + decl->type = cbm_source_table_intern(walk, path); + decl->bases = ocaml_opens_copy(walk, outer_opens, *outer_count); + } + } else { + (void)cbm_source_declare(walk, owner, OCAML_OPEN_ENDED, NULL, CBM_SOURCE_DECL_F_OPEN, line); + } + ocaml_push_open(walk, outer_opens, outer_count, line, path, true); +} + +/* `A.S` as a module type: the last name is kept apart from a module's. */ +static const char *ocaml_module_type_path(cbm_source_walk_t *walk, TSNode path) { + const char *text = cbm_source_text(walk, path); + const char *dot = text ? strrchr(text, '.') : NULL; + if (!text) + return NULL; + return dot ? cbm_arena_sprintf(walk->arena, "%.*s.^%s", (int)(dot - text), text, dot + 1) + : cbm_arena_sprintf(walk->arena, "^%s", text); +} + +/* `(S with type t = int)` promises what S does. */ +static TSNode ocaml_module_type_read(TSNode type) { + while (cbm_source_is(type, "module_type_constraint") || + cbm_source_is(type, "parenthesized_module_type")) + type = ts_node_named_child_count(type) ? ts_node_named_child(type, 0) : (TSNode){0}; + return type; +} + +static void ocaml_collect_signature(cbm_source_walk_t *walk, const char *owner, TSNode signature, + const char *const *outer_opens, size_t outer_count, + unsigned depth); + +/* What a module type says goes on the declaration: its own scope for a `sig`, a path for a + * name. Anything else leaves the type NULL, which is a module no one can read. */ +static void ocaml_declare_module_type(cbm_source_walk_t *walk, cbm_source_decl_t *decl, + const char *scope, TSNode type, const char *const *opens, + size_t open_count, unsigned depth) { + if (!decl) + return; + type = ocaml_module_type_read(type); + if (cbm_source_is(type, "signature") && scope) { + decl->type = cbm_source_table_intern(walk, scope); + (void)cbm_source_declare(walk, scope, "", NULL, CBM_SOURCE_DECL_F_TYPE, 0); + ocaml_collect_signature(walk, scope, type, opens, open_count, depth + 1); + } else if (cbm_source_is(type, "module_type_path")) { + decl->flags |= CBM_SOURCE_DECL_F_VALUE; + decl->type = cbm_source_table_intern(walk, ocaml_module_type_path(walk, type)); + decl->bases = ocaml_opens_copy(walk, opens, open_count); + } +} + +static void ocaml_collect_module_type(cbm_source_walk_t *walk, const char *owner, TSNode item, + const char *const *opens, size_t open_count, unsigned depth) { + const char *name = cbm_source_text(walk, ocaml_child(item, "module_type_name")); + uint32_t parts = ts_node_named_child_count(item); + if (!name) + return; + cbm_source_decl_t *decl = cbm_source_declare( + walk, owner, cbm_arena_sprintf(walk->arena, "%s^%s", OCAML_MODULE_PREFIX, name), NULL, + CBM_SOURCE_DECL_F_TYPE, ts_node_start_point(item).row + 1); + if (parts > 1) + ocaml_declare_module_type(walk, decl, cbm_arena_sprintf(walk->arena, "%s#^%s", owner, name), + ts_node_named_child(item, parts - 1), opens, open_count, depth); +} + +/* A signature's names have no declaration behind them. Knowing them shows what cannot come + * from there. */ +static void ocaml_collect_signature(cbm_source_walk_t *walk, const char *owner, TSNode signature, + const char *const *outer_opens, size_t outer_count, unsigned depth) { if (!cbm_source_step(walk, depth)) return; - uint32_t count = ts_node_named_child_count(structure); - for (uint32_t i = 0; i < count; i++) { - TSNode item = ts_node_named_child(structure, i); + const char *opens[OCAML_MAX_OPENS]; + size_t open_count = outer_count; + if (outer_count) + memcpy(opens, outer_opens, outer_count * sizeof(*opens)); + TSTreeCursor cursor = ts_tree_cursor_new(signature); + bool more = ts_tree_cursor_goto_first_child(&cursor); + for (; more; more = ts_tree_cursor_goto_next_sibling(&cursor)) { + TSNode item = ts_tree_cursor_current_node(&cursor); + uint32_t line = ts_node_start_point(item).row + 1; + if (cbm_source_is(item, "value_specification") || cbm_source_is(item, "external")) { + const char *name = cbm_source_text(walk, ocaml_child(item, "value_name")); + if (name) + (void)cbm_source_declare(walk, owner, name, NULL, 0, line); + } else if (cbm_source_is(item, "module_definition")) { + TSNode binding = ocaml_child(item, "module_binding"); + const char *name = cbm_source_text(walk, ocaml_child(binding, "module_name")); + uint32_t parts = ts_node_is_null(binding) ? 0 : ts_node_named_child_count(binding); + if (!name || parts < 2) + continue; + TSNode body = ts_node_named_child(binding, parts - 1); + cbm_source_decl_t *decl = cbm_source_declare( + walk, owner, cbm_arena_sprintf(walk->arena, "%s%s", OCAML_MODULE_PREFIX, name), + NULL, CBM_SOURCE_DECL_F_TYPE, line); + if (cbm_source_is(body, "module_path") && decl) { + decl->flags |= CBM_SOURCE_DECL_F_VALUE; + decl->type = cbm_source_table_intern(walk, cbm_source_text(walk, body)); + decl->bases = ocaml_opens_copy(walk, opens, open_count); + } else { + ocaml_declare_module_type(walk, decl, + cbm_arena_sprintf(walk->arena, "%s#%s", owner, name), + body, opens, open_count, depth); + } + } else if (cbm_source_is(item, "module_type_definition")) { + ocaml_collect_module_type(walk, owner, item, opens, open_count, depth); + } else if (cbm_source_is(item, "include_module_type")) { + TSNode type = ts_node_named_child_count(item) + ? ocaml_module_type_read(ts_node_named_child(item, 0)) + : (TSNode){0}; + const char *path = + cbm_source_is(type, "module_type_path") ? ocaml_module_type_path(walk, type) : NULL; + /* `include module type of M`, with or without `struct include M end`, has what M + * has. */ + TSNode of = cbm_source_is(type, "module_type_of") && ts_node_named_child_count(type) + ? ts_node_named_child(type, 0) + : (TSNode){0}; + if (cbm_source_is(of, "structure")) { + ocaml_collect_inlined(walk, owner, of, opens, &open_count, depth); + continue; + } + if (!ts_node_is_null(of)) + path = cbm_source_text(walk, ocaml_module_read(of)); + if (path) { + cbm_source_decl_t *decl = cbm_source_declare(walk, owner, OCAML_INCLUDE, NULL, + CBM_SOURCE_DECL_F_VALUE, line); + if (decl) { + decl->type = cbm_source_table_intern(walk, path); + decl->bases = ocaml_opens_copy(walk, opens, open_count); + } + } else { + (void)cbm_source_declare(walk, owner, OCAML_OPEN_ENDED, NULL, + CBM_SOURCE_DECL_F_OPEN, line); + } + ocaml_push_open(walk, opens, &open_count, line, path, true); + } else if (cbm_source_is(item, "open_module")) { + ocaml_push_open( + walk, opens, &open_count, line, + cbm_source_text(walk, ocaml_module_read(cbm_source_field(item, "module"))), false); + } + } + ts_tree_cursor_delete(&cursor); +} + +/* `module Make (X : S) = struct`: inside, X is a module with what S says, whatever else goes + * by that name. */ +static void ocaml_collect_parameters(cbm_source_walk_t *walk, const char *nested, TSNode binding, + const char *const *opens, size_t open_count, uint32_t line, + unsigned depth) { + TSTreeCursor cursor = ts_tree_cursor_new(binding); + bool more = ts_tree_cursor_goto_first_child(&cursor); + for (; more; more = ts_tree_cursor_goto_next_sibling(&cursor)) { + TSNode child = ts_tree_cursor_current_node(&cursor); + if (!cbm_source_is(child, "module_parameter")) + continue; + const char *name = cbm_source_text(walk, ocaml_child(child, "module_name")); + uint32_t parts = ts_node_named_child_count(child); + if (!name || parts < 2) + continue; + cbm_source_decl_t *decl = cbm_source_declare( + walk, nested, cbm_arena_sprintf(walk->arena, "%s%s", OCAML_MODULE_PREFIX, name), NULL, + CBM_SOURCE_DECL_F_TYPE | OCAML_DECL_PARAMETER, 0); + if (decl) + decl->arity_min = (int)line; + ocaml_declare_module_type(walk, decl, + cbm_arena_sprintf(walk->arena, "%s#^(%s", nested, name), + ts_node_named_child(child, parts - 1), opens, open_count, depth); + } + ts_tree_cursor_delete(&cursor); +} + +static void ocaml_collect_structure(cbm_source_walk_t *walk, const char *owner, TSNode structure, + const char **opens, size_t *open_count, unsigned depth) { + if (!cbm_source_step(walk, depth)) + return; + TSTreeCursor cursor = ts_tree_cursor_new(structure); + bool more = ts_tree_cursor_goto_first_child(&cursor); + for (; more; more = ts_tree_cursor_goto_next_sibling(&cursor)) { + TSNode item = ts_tree_cursor_current_node(&cursor); + if (!ts_node_is_named(item)) + continue; if (cbm_source_is(item, "value_definition")) { uint32_t parts = ts_node_named_child_count(item); for (uint32_t p = 0; p < parts; p++) if (cbm_source_is(ts_node_named_child(item, p), "let_binding")) ocaml_collect_binding(walk, owner, ts_node_named_child(item, p)); + } else if (cbm_source_is(item, "external")) { + const char *name = cbm_source_text(walk, ocaml_child(item, "value_name")); + uint32_t line = ts_node_start_point(item).row + 1; + if (name) + (void)cbm_source_declare(walk, owner, name, cbm_source_def_qn(walk, name, line), 0, + line); } else if (cbm_source_is(item, "module_definition")) { TSNode binding = ocaml_child(item, "module_binding"); const char *name = cbm_source_text(walk, ocaml_child(binding, "module_name")); @@ -105,31 +625,142 @@ static void ocaml_collect_structure(cbm_source_walk_t *walk, const char *owner, continue; const char *key = cbm_arena_sprintf(walk->arena, "%s%s", OCAML_MODULE_PREFIX, name); cbm_source_decl_t *decl = cbm_source_declare( - walk, owner, key, NULL, CBM_SOURCE_DECL_F_TYPE, ts_node_start_point(item).row + 1); + walk, owner, key, NULL, + CBM_SOURCE_DECL_F_TYPE | (cbm_source_token(item, "rec") ? OCAML_DECL_RECURSIVE : 0), + ts_node_start_point(item).row + 1); if (cbm_source_is(body, "structure")) { const char *nested = cbm_arena_sprintf(walk->arena, "%s#%s", owner, name); if (decl) decl->type = cbm_source_table_intern(walk, nested); (void)cbm_source_declare(walk, nested, "", NULL, CBM_SOURCE_DECL_F_TYPE, 0); - ocaml_collect_structure(walk, nested, body, depth + 1); - } else if (cbm_source_is(body, "module_path") && decl) { - /* `module C = Config`: stored as the spelling, read beside the file. */ + ocaml_collect_parameters(walk, nested, binding, opens, *open_count, + ts_node_start_point(item).row + 1, depth); + /* What is open here stays open inside the struct. */ + const char *inner_opens[OCAML_MAX_OPENS]; + size_t inner_count = *open_count; + memcpy(inner_opens, opens, inner_count * sizeof(*inner_opens)); + ocaml_collect_structure(walk, nested, body, inner_opens, &inner_count, depth + 1); + } else if (decl && !ts_node_is_null(ocaml_module_read(body))) { + /* `module C = Config` or `module C = Make (T)`: stored as the spelling, read + * where it is written: under the opens of this struct, and before C exists. */ decl->flags |= CBM_SOURCE_DECL_F_VALUE; - decl->type = cbm_source_table_intern(walk, cbm_source_text(walk, body)); + decl->type = + cbm_source_table_intern(walk, cbm_source_text(walk, ocaml_module_read(body))); + decl->bases = ocaml_opens_copy(walk, opens, *open_count); + } + } else if (cbm_source_is(item, "module_type_definition")) { + ocaml_collect_module_type(walk, owner, item, opens, *open_count, depth); + } else if (cbm_source_is(item, "include_module")) { + ocaml_collect_include(walk, owner, item, opens, open_count, depth); + } else if (cbm_source_is(item, "open_module")) { + TSNode opened = cbm_source_field(item, "module"); + if (cbm_source_is(opened, "structure")) { + /* `open struct`: what it has is in scope below, as an include's is. */ + ocaml_collect_inlined(walk, owner, opened, opens, open_count, depth); + } else { + ocaml_push_open(walk, opens, open_count, ts_node_start_point(item).row + 1, + cbm_source_text(walk, ocaml_module_read(opened)), false); } - } else if (cbm_source_is(item, "include_module") || cbm_source_is(item, "open_module")) { - /* What this unit's `include` adds is only known if the module is. */ - if (cbm_source_is(item, "include_module")) - (void)cbm_source_declare(walk, owner, OCAML_OPEN_ENDED, NULL, - CBM_SOURCE_DECL_F_OPEN, 0); } } } +/* Where this file stands among the modules of its build. */ +static void ocaml_collect_place(cbm_source_walk_t *walk, const char *unit) { + const char *leaf = strrchr(unit, '/'); + leaf = leaf ? leaf + 1 : unit; + char lowered[256]; + if (strlen(leaf) >= sizeof(lowered)) + return; + strcpy(lowered, leaf); + lowered[0] = (char)tolower((unsigned char)lowered[0]); + uint32_t line = 1; + const char *directory = ocaml_directory_of(walk, unit); + for (const char *up = directory; up; up = ocaml_parent_directory(walk, up)) { + const char *subdirs = ocaml_manifest(walk, OCAML_SUBDIRS_KEY, up); + if (subdirs && !strcmp(subdirs, "unqualified")) { + cbm_source_decl_t *decl = cbm_source_declare( + walk, cbm_arena_sprintf(walk->arena, "%s%s", OCAML_SUBTREE_PREFIX, up), lowered, + NULL, CBM_SOURCE_DECL_F_VALUE, line); + if (decl) + decl->type = cbm_source_table_intern(walk, unit); + } + if (subdirs && !strcmp(subdirs, "qualified")) { + /* Each directory on the way down from here is a module of its parent. */ + for (const char *group = directory; group && strcmp(group, up); + group = ocaml_parent_directory(walk, group)) + (void)cbm_source_declare( + walk, cbm_arena_sprintf(walk->arena, "%s%s", OCAML_DIRECTORY_PREFIX, group), "", + NULL, CBM_SOURCE_DECL_F_TYPE, line); + } + if (ocaml_manifest(walk, OCAML_DIR_KEY, up)) + break; + } +} + +/* foo.mli is the signature of unit foo, kept under its own name. The grammar here reads + * implementations, where `val` is an error, so the file is read inside `sig ... end`, which + * is what it is. Rows stay right because the opening adds none. One that still does not + * parse says nothing. */ +static void ocaml_collect_interface(cbm_source_walk_t *walk, const char *unit) { + static const char opening[] = "module type I = sig ", closing[] = "\nend\n"; + const char *interface = cbm_arena_sprintf(walk->arena, "%s.mli", unit); + if (!interface) + return; + (void)cbm_source_declare(walk, interface, "", NULL, CBM_SOURCE_DECL_F_TYPE, 1); + size_t length = strlen(opening) + walk->source_len + strlen(closing); + char *text = cbm_arena_alloc(walk->arena, length + 1); + TSParser *parser = ts_parser_new(); + TSTree *tree = NULL; + if (text && parser && ts_parser_set_language(parser, ts_node_language(walk->root))) { + snprintf(text, length + 1, "%s%.*s%s", opening, (int)walk->source_len, walk->source, + closing); + tree = ts_parser_parse_string(parser, NULL, text, (uint32_t)length); + } + TSNode root = tree ? ts_tree_root_node(tree) : (TSNode){0}; + TSNode signature = ts_node_is_null(root) || ts_node_has_error(root) + ? (TSNode){0} + : ocaml_child(ocaml_child(root, "module_type_definition"), "signature"); + if (cbm_source_is(signature, "signature")) { + const char *source = walk->source; + size_t source_len = walk->source_len; + walk->source = text; + walk->source_len = length; + ocaml_collect_signature(walk, interface, signature, NULL, 0, 0); + walk->source = source; + walk->source_len = source_len; + } else { + (void)cbm_source_declare(walk, interface, OCAML_OPEN_ENDED, NULL, CBM_SOURCE_DECL_F_OPEN, + 1); + } + if (tree) + ts_tree_delete(tree); + if (parser) + ts_parser_delete(parser); +} + static void ocaml_collect(cbm_source_walk_t *walk) { const char *unit = ocaml_unit_key(walk); (void)cbm_source_declare(walk, unit, "", NULL, CBM_SOURCE_DECL_F_TYPE, 1); - ocaml_collect_structure(walk, unit, walk->root, 0); + /* A unit's members and its place are the .ml's. */ + size_t length = strlen(walk->file); + if (length > 4 && !strcmp(walk->file + length - 4, ".mli")) { + ocaml_collect_interface(walk, unit); + return; + } + ocaml_collect_place(walk, unit); + const char *opens[OCAML_MAX_OPENS]; + size_t open_count = 0; + ocaml_collect_structure(walk, unit, walk->root, opens, &open_count, 0); +} + +/* An interface is what the implementation grammar cannot read, and has its own way in. */ +static void ocaml_collect_unusable(cbm_source_walk_t *walk) { + size_t length = strlen(walk->file); + if (length > 4 && !strcmp(walk->file + length - 4, ".mli")) + ocaml_collect(walk); + else + walk->failure = "ocaml_unreadable"; } /* ── Phase two ─────────────────────────────────────────────────────── */ @@ -155,65 +786,417 @@ static void ocaml_bind_pattern(ocaml_ctx_t *ctx, TSNode node, unsigned depth) { return; if (cbm_source_is(node, "value_pattern") || cbm_source_is(node, "value_name")) ocaml_bind(ctx, cbm_source_text(ctx->walk, node)); - uint32_t count = ts_node_named_child_count(node); - for (uint32_t i = 0; i < count; i++) - ocaml_bind_pattern(ctx, ts_node_named_child(node, i), depth + 1); + /* `{ a; M.b }` binds a and b; `{ a = c }` binds only c. */ + if (cbm_source_is(node, "field_pattern") && ts_node_named_child_count(node) == 1) { + TSNode path = ts_node_named_child(node, 0); + ocaml_bind(ctx, cbm_source_text(ctx->walk, ocaml_child(path, "field_name"))); + 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 (ts_node_is_named(child)) + ocaml_bind_pattern(ctx, child, depth + 1); + } while (ts_tree_cursor_goto_next_sibling(&cursor)); + } + ts_tree_cursor_delete(&cursor); } -/* A module alias in this unit, or the unit beside this file. */ -static const char *ocaml_module_unit(ocaml_ctx_t *ctx, const char *owner, const char *module, - unsigned depth) { - if (depth > 8 || !module) - return NULL; +static const char *ocaml_decl_path(ocaml_ctx_t *ctx, const cbm_source_decl_t *decl, unsigned depth); +static bool ocaml_unit_closed(ocaml_ctx_t *ctx, const char *unit); + +/* The latest binding of a module name in one structure that is in scope before a line. + * UINT32_MAX reads a finished structure, where every binding has happened. */ +static const cbm_source_decl_t *ocaml_module_binding(const ocaml_ctx_t *ctx, const char *scope, + const char *module, uint32_t before_line) { char key[320]; int length = snprintf(key, sizeof(key), "%s%s", OCAML_MODULE_PREFIX, module); if (length < 0 || (size_t)length >= sizeof(key)) return NULL; - const char *scope = owner; - while (scope) { - const cbm_source_decl_t *alias = cbm_source_lookup(ctx->walk, scope, key); - /* A module binding is sequential and not recursive: it is in scope only after - * its definition ends, so the same name inside its body or before it is another - * module. A structure's own key is a prefix of everything inside it. */ - if (alias && alias->type && !alias->next) { - size_t own = strlen(alias->type); - bool inside = !(alias->flags & CBM_SOURCE_DECL_F_VALUE) && - !strncmp(ctx->unit, alias->type, own) && - (ctx->unit[own] == '\0' || ctx->unit[own] == '#'); - if (inside || alias->line > ctx->site_line) - alias = NULL; - } - if (alias) { - if (alias->next || !alias->type) - return NULL; - return alias->flags & CBM_SOURCE_DECL_F_VALUE - ? ocaml_module_unit(ctx, scope, alias->type, depth + 1) - : alias->type; - } - const char *hash = strrchr(scope, '#'); - scope = hash ? cbm_arena_strndup(ctx->walk->arena, scope, (size_t)(hash - scope)) : NULL; - } - return ocaml_sibling_unit(ctx, module); + const cbm_source_decl_t *latest = NULL; + for (const cbm_source_decl_t *decl = cbm_source_lookup(ctx->walk, scope, key); decl; + decl = decl->next) + if (decl->line < before_line && (!latest || decl->line >= latest->line)) + latest = decl; + return latest; } -static void ocaml_note_open(ocaml_ctx_t *ctx, TSNode item) { - TSNode path = cbm_source_field(item, "module"); +/* `within#head` holds `unit`: a site in unit stands in the body of that very binding, where + * the name, unless it is `rec`, still means what it meant before the binding. */ +static bool ocaml_in_own_body(const cbm_source_decl_t *binding, const char *unit, + const char *within, const char *head) { + size_t scope = strlen(within), name = strlen(head); + return binding && !(binding->flags & OCAML_DECL_RECURSIVE) && !strncmp(unit, within, scope) && + unit[scope] == '#' && !strncmp(unit + scope + 1, head, name) && + (unit[scope + 1 + name] == '\0' || unit[scope + 1 + name] == '#'); +} + +/* What a module binding denotes. A binding with nothing readable behind it is NULL. */ +static const char *ocaml_binding_target(ocaml_ctx_t *ctx, const cbm_source_decl_t *decl, + unsigned depth) { + if (!decl->type) + return NULL; + return decl->flags & CBM_SOURCE_DECL_F_VALUE ? ocaml_decl_path(ctx, decl, depth + 1) + : decl->type; +} + +/* A module inside a module: its own binding, or one of what it includes. */ +static const char *ocaml_member_module_uncached(ocaml_ctx_t *ctx, const char *container, + const char *module, unsigned depth); + +static const char *ocaml_member_module(ocaml_ctx_t *ctx, const char *container, const char *module, + unsigned depth) { + if (depth > 32) + return NULL; + char key[640]; + int length = snprintf(key, sizeof(key), "%s\x1f%s", container, module); + bool keyed = length > 0 && (size_t)length < sizeof(key) && ctx->member_modules; + const char *known = keyed ? cbm_ht_get(ctx->member_modules, key) : NULL; + if (known) + return known[0] ? known : NULL; + /* Asked again while working it out, the answer is no. */ + const char *stored = keyed ? cbm_arena_strdup(ctx->walk->arena, key) : NULL; + if (stored) + cbm_ht_set(ctx->member_modules, stored, (void *)""); + const char *found = ocaml_member_module_uncached(ctx, container, module, depth); + if (stored) + cbm_ht_set(ctx->member_modules, stored, (void *)(found ? found : "")); + return found; +} + +/* A unit's interface, when it has one. */ +static const char *ocaml_interface_of(const ocaml_ctx_t *ctx, const char *unit) { + const char *key = cbm_arena_sprintf(ctx->walk->arena, "%s.mli", unit); + return key && cbm_source_lookup(ctx->walk, key, "") ? key : NULL; +} + +static const char *ocaml_member_module_uncached(ocaml_ctx_t *ctx, const char *container, + const char *module, unsigned depth) { + if (ocaml_is_stdlib(container)) { + const char *parent = container + strlen(OCAML_STDLIB_PREFIX); + const char *path = + *parent ? cbm_arena_sprintf(ctx->walk->arena, "%s.%s", parent, module) : module; + return path ? ocaml_stdlib_module(ctx, path) : NULL; + } + if (ocaml_is_directory_module(container)) { + const char *directory = container + strlen(OCAML_DIRECTORY_PREFIX); + const char *lowered = ocaml_lowered(ctx->walk, module); + const char *unit = lowered ? ocaml_directory_member(ctx, directory, lowered) : NULL; + if (unit) + return unit; + const char *group = lowered ? cbm_arena_sprintf(ctx->walk->arena, "%s%s/%s", + OCAML_DIRECTORY_PREFIX, directory, lowered) + : NULL; + return group && cbm_source_lookup(ctx->walk, group, "") ? group : NULL; + } + /* What an interface lists can have a module type no one reads: it is still listed. */ + const char *interface = ocaml_interface_of(ctx, container); + if (interface && !ocaml_module_binding(ctx, interface, module, UINT32_MAX) && + !ocaml_member_module(ctx, interface, module, depth + 1) && + ocaml_unit_closed(ctx, interface)) + return NULL; + const cbm_source_decl_t *own = ocaml_module_binding(ctx, container, module, UINT32_MAX); + const cbm_source_decl_t *include = cbm_source_lookup(ctx->walk, container, OCAML_INCLUDE); + /* The latest include after the binding replaces it, if it has the module at all. */ + const char *found = NULL; + uint32_t found_line = 0; + for (; include; include = include->next) { + if (own && include->line < own->line) + continue; + const char *included = ocaml_decl_path(ctx, include, depth + 1); + const char *inner = included ? ocaml_member_module(ctx, included, module, depth + 1) : NULL; + if (inner && include->line >= found_line) { + found = inner; + found_line = include->line; + } + } + return found ? found : own ? ocaml_binding_target(ctx, own, depth + 1) : NULL; +} + +/* The opens above a path, read in order, each under the ones before it. */ +typedef struct { + const char *unit; /* NULL: a module no one can read */ + uint32_t line; +} ocaml_read_open_t; + +/* `A.B.C` read from a structure: the latest of its opens and enclosing bindings, then the + * unit's place, then libraries. An open that could hold A and cannot be read leaves it + * unknown. */ +static const char *ocaml_resolve_path(ocaml_ctx_t *ctx, const char *scope, const char *path, + uint32_t before_line, const ocaml_read_open_t *opens, + size_t open_count, unsigned depth) { + if (depth > 32 || !path || !*path) + return NULL; + const char *dot = strchr(path, '.'); + const char *head = dot ? cbm_arena_strndup(ctx->walk->arena, path, (size_t)(dot - path)) : path; + const cbm_source_decl_t *binding = NULL; + for (const char *within = scope; !binding && within;) { + binding = ocaml_module_binding(ctx, within, head, before_line); + if (ocaml_in_own_body(binding, scope, within, head)) + binding = NULL; + const char *hash = strrchr(within, '#'); + within = hash ? cbm_arena_strndup(ctx->walk->arena, within, (size_t)(hash - within)) : NULL; + } + const char *container = NULL; + for (size_t i = open_count; !container && i > 0; i--) { + if (binding && opens[i - 1].line < binding->line) + break; + if (!opens[i - 1].unit) + return NULL; + container = ocaml_member_module(ctx, opens[i - 1].unit, head, depth + 1); + if (!container && !ocaml_unit_closed(ctx, opens[i - 1].unit)) + return NULL; + } + if (!container && binding) { + container = ocaml_binding_target(ctx, binding, depth + 1); + if (!container) + return NULL; + } + if (!container) + container = ocaml_unit_by_place(ctx, scope, head); + while (container && dot) { + const char *rest = dot + 1; + dot = strchr(rest, '.'); + const char *part = + dot ? cbm_arena_strndup(ctx->walk->arena, rest, (size_t)(dot - rest)) : rest; + container = part ? ocaml_member_module(ctx, container, part, depth + 1) : NULL; + } + return container; +} + +/* What a declaration's path denotes. Worked out once per file; a path that needs itself + * has no answer. */ +static const char *ocaml_decl_path(ocaml_ctx_t *ctx, const cbm_source_decl_t *decl, + unsigned depth) { + char key[32]; + snprintf(key, sizeof(key), "%p", (const void *)decl); + const char *known = cbm_ht_get(ctx->decl_paths, key); + if (known) + return known[0] ? known : NULL; + const char *stored = cbm_arena_strdup(ctx->walk->arena, key); + if (stored) + cbm_ht_set(ctx->decl_paths, stored, (void *)""); + const char *scope = decl->owner; + uint32_t line = decl->line; + if (decl->flags & OCAML_DECL_PARAMETER) { + const char *hash = strrchr(scope, '#'); + scope = hash ? cbm_arena_strndup(ctx->walk->arena, scope, (size_t)(hash - scope)) : scope; + line = (uint32_t)decl->arity_min; + } + size_t count = 0; + while (decl->bases && decl->bases[count]) + count++; + ocaml_read_open_t *opens = + count ? cbm_arena_alloc(ctx->walk->arena, count * sizeof(*opens)) : NULL; + size_t kept = 0; + for (size_t i = 0; opens && i < count; i++) { + char *rest = NULL; + unsigned long open_line = strtoul(decl->bases[i], &rest, 10); + opens[i].line = (uint32_t)open_line; + opens[i].unit = ocaml_resolve_path(ctx, scope, rest && *rest ? rest + 1 : NULL, + (uint32_t)open_line, opens, i, depth + 1); + kept = i + 1; + } + const char *found = ocaml_resolve_path(ctx, scope, decl->type, line, opens, kept, depth); + if (stored) + cbm_ht_set(ctx->decl_paths, stored, (void *)(found ? found : "")); + return found; +} + +/* Everything a unit brings in is known. Answers are kept for this file: a closure is + * shared by every open of it, and each unit is read once per file. */ +static bool ocaml_unit_closed_walk(ocaml_ctx_t *ctx, const char *unit, cbm_source_chain_t *chain) { + if (ocaml_is_directory_module(unit) || ocaml_is_stdlib(unit)) + return true; + if (!cbm_source_chain_first_visit(chain, unit)) + return !chain->overflow; + /* An interface can only hide, so either side being closed closes the unit. */ + const char *interface = ocaml_interface_of(ctx, unit); + if (interface && ocaml_unit_closed(ctx, interface)) + return true; + if (cbm_source_lookup(ctx->walk, unit, OCAML_OPEN_ENDED)) + return false; + for (const cbm_source_decl_t *include = cbm_source_lookup(ctx->walk, unit, OCAML_INCLUDE); + include; include = include->next) { + const char *included = ocaml_decl_path(ctx, include, 0); + if (!included || !ocaml_unit_closed_walk(ctx, included, chain)) + return false; + } + return true; +} + +static bool ocaml_unit_closed(ocaml_ctx_t *ctx, const char *unit) { + if (!unit) + return false; + const char *memo = ctx->closed_units ? cbm_ht_get(ctx->closed_units, unit) : NULL; + if (memo) + return memo[0] == 'y'; + /* Asked again while working it out, the answer is no. */ + const char *key = cbm_arena_strdup(ctx->walk->arena, unit); + if (ctx->closed_units && key) + cbm_ht_set(ctx->closed_units, key, "n"); + cbm_source_chain_t chain = {0}; + bool closed = ocaml_unit_closed_walk(ctx, unit, &chain); + if (ctx->closed_units && key) + cbm_ht_set(ctx->closed_units, key, closed ? "y" : "n"); + return closed; +} + +/* A value of a closed or open-ended unit: the latest binding of it the unit makes. */ +static ocaml_lookup_t ocaml_unit_value(ocaml_ctx_t *ctx, const char *unit, const char *name, + cbm_source_chain_t *chain, const cbm_source_decl_t **out) { + if (ocaml_is_stdlib(unit)) + return OCAML_UNKNOWN; + if (ocaml_is_directory_module(unit)) + return OCAML_ABSENT; + if (!cbm_source_chain_first_visit(chain, unit)) + return chain->overflow ? OCAML_UNKNOWN : OCAML_ABSENT; + const char *interface = ocaml_interface_of(ctx, unit); + if (interface) { + const cbm_source_decl_t *listed = NULL; + if (ocaml_unit_value(ctx, interface, name, chain, &listed) == OCAML_ABSENT) + return OCAML_ABSENT; + } + const cbm_source_decl_t *own = cbm_source_lookup(ctx->walk, unit, name); + uint32_t own_line = 0; + for (const cbm_source_decl_t *decl = own; decl; decl = decl->next) + if (decl->line >= own_line) + own_line = decl->line; + const cbm_source_decl_t *best = own; + uint32_t best_line = own_line, unknown_line = 0; + bool unknown = false; + for (const cbm_source_decl_t *include = cbm_source_lookup(ctx->walk, unit, OCAML_INCLUDE); + include; include = include->next) { + if (include->line < best_line) + continue; + const char *included = ocaml_decl_path(ctx, include, 0); + const cbm_source_decl_t *inner = NULL; + ocaml_lookup_t found = + included ? ocaml_unit_value(ctx, included, name, chain, &inner) : OCAML_UNKNOWN; + if (found == OCAML_UNKNOWN) { + unknown = true; + if (include->line > unknown_line) + unknown_line = include->line; + } else if (found == OCAML_FOUND) { + best = inner; + best_line = include->line; + } + } + /* `include (val m)` can replace what stands above it. */ + for (const cbm_source_decl_t *ended = cbm_source_lookup(ctx->walk, unit, OCAML_OPEN_ENDED); + ended; ended = ended->next) { + unknown = true; + if (ended->line > unknown_line) + unknown_line = ended->line; + } + /* What is unknown matters only below the latest binding known. */ + if (unknown && (!best || unknown_line >= best_line)) + return OCAML_UNKNOWN; + if (!best) + return OCAML_ABSENT; + *out = best; + return OCAML_FOUND; +} + +typedef struct { + ocaml_lookup_t found; + const cbm_source_decl_t *decl; +} ocaml_value_memo_t; + +static ocaml_lookup_t ocaml_value_in(ocaml_ctx_t *ctx, const char *unit, const char *name, + const cbm_source_decl_t **out) { + char key[640]; + int length = snprintf(key, sizeof(key), "%s\x1f%s", unit, name); + bool keyed = length > 0 && (size_t)length < sizeof(key) && ctx->member_values; + const ocaml_value_memo_t *known = keyed ? cbm_ht_get(ctx->member_values, key) : NULL; + if (known) { + *out = known->decl; + return known->found; + } + cbm_source_chain_t chain = {0}; + const cbm_source_decl_t *decl = NULL; + ocaml_lookup_t found = ocaml_unit_value(ctx, unit, name, &chain, &decl); + ocaml_value_memo_t *memo = keyed ? cbm_arena_alloc(ctx->walk->arena, sizeof(*memo)) : NULL; + if (memo) { + *memo = (ocaml_value_memo_t){found, decl}; + cbm_ht_set(ctx->member_values, cbm_arena_strdup(ctx->walk->arena, key), memo); + } + *out = decl; + return found; +} + +/* A module name at a site: an open since any binding of it can bring in another. */ +typedef enum { OCAML_MODULE_FOUND, OCAML_MODULE_ABSENT, OCAML_MODULE_UNCERTAIN } ocaml_module_t; + +static ocaml_module_t ocaml_module_at_site(ocaml_ctx_t *ctx, const char *path, uint32_t at, + const char **out) { + const char *dot = strchr(path, '.'); + const char *head = dot ? cbm_arena_strndup(ctx->walk->arena, path, (size_t)(dot - path)) : path; + if (!head) + return OCAML_MODULE_ABSENT; + const cbm_source_decl_t *binding = NULL; + for (const char *within = ctx->unit; !binding && within;) { + binding = ocaml_module_binding(ctx, within, head, ctx->site_line); + if (ocaml_in_own_body(binding, ctx->unit, within, head)) + binding = NULL; + const char *hash = strrchr(within, '#'); + within = hash ? cbm_arena_strndup(ctx->walk->arena, within, (size_t)(hash - within)) : NULL; + } + const char *container = NULL; + for (const ocaml_open_t *open = ctx->opens; !container && open; open = open->next) { + if (open->at >= at) + continue; + if (binding && open->line < binding->line) + break; + if (!ocaml_unit_closed(ctx, open->unit)) + return OCAML_MODULE_UNCERTAIN; + container = ocaml_member_module(ctx, open->unit, head, 0); + } + if (!container && binding) + container = ocaml_binding_target(ctx, binding, 0); + else if (!container) + container = ocaml_unit_by_place(ctx, ctx->unit, head); + while (container && dot) { + const char *rest = dot + 1; + dot = strchr(rest, '.'); + const char *part = + dot ? cbm_arena_strndup(ctx->walk->arena, rest, (size_t)(dot - rest)) : rest; + container = part ? ocaml_member_module(ctx, container, part, 0) : NULL; + } + *out = container; + return container ? OCAML_MODULE_FOUND : OCAML_MODULE_ABSENT; +} + +/* A literal struct has ended: its opens end with it, and what it includes stays. */ +static ocaml_open_t *ocaml_keep_includes(ocaml_open_t *open, ocaml_open_t *saved) { + if (open == saved) + return saved; + ocaml_open_t *below = ocaml_keep_includes(open->next, saved); + if (!open->include) + return below; + open->next = below; + return open; +} + +static void ocaml_note_open(ocaml_ctx_t *ctx, TSNode item, TSNode path) { const char *text = cbm_source_text(ctx->walk, path); ocaml_open_t *open = cbm_arena_alloc(ctx->walk->arena, sizeof(*open)); if (!open) return; - *open = (ocaml_open_t){ - .unit = text && !strchr(text, '.') ? ocaml_module_unit(ctx, ctx->unit, text, 0) : NULL, - .at = ts_node_start_byte(item), - .line = ts_node_start_point(item).row + 1, - .next = ctx->opens}; + const char *unit = NULL; + ctx->site_line = ts_node_start_point(item).row + 1; + if (text && + ocaml_module_at_site(ctx, text, ts_node_start_byte(item), &unit) != OCAML_MODULE_FOUND) + unit = NULL; + *open = (ocaml_open_t){.unit = unit, + .at = ts_node_start_byte(item), + .line = ts_node_start_point(item).row + 1, + .include = cbm_source_is(item, "include_module"), + .next = ctx->opens}; ctx->opens = open; } -static bool ocaml_unit_closed(const ocaml_ctx_t *ctx, const char *unit) { - return unit && !cbm_source_lookup(ctx->walk, unit, OCAML_OPEN_ENDED); -} - static void ocaml_emit(ocaml_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, @@ -258,9 +1241,10 @@ static void ocaml_visit_application(ocaml_ctx_t *ctx, TSNode site, TSNode functi } /* Any open since the declaration could have replaced the name. */ for (const ocaml_open_t *open = ctx->opens; open; open = open->next) { + const cbm_source_decl_t *replacing = NULL; if (open->at < at && open->line > decl->line && (!ocaml_unit_closed(ctx, open->unit) || - cbm_source_lookup(ctx->walk, open->unit, name))) { + ocaml_value_in(ctx, open->unit, name, &replacing) != OCAML_ABSENT)) { ocaml_emit(ctx, site, NULL, CBM_RESOLVE_RULE_NONE, NULL, "ocaml_open_may_replace_name"); return; @@ -279,8 +1263,14 @@ static void ocaml_visit_application(ocaml_ctx_t *ctx, TSNode site, TSNode functi "ocaml_open_of_unknown_module"); return; } - const cbm_source_decl_t *opened = cbm_source_lookup(ctx->walk, open->unit, name); - if (opened) { + const cbm_source_decl_t *opened = NULL; + ocaml_lookup_t from_open = ocaml_value_in(ctx, open->unit, name, &opened); + if (from_open == OCAML_UNKNOWN) { + ocaml_emit(ctx, site, NULL, CBM_RESOLVE_RULE_NONE, NULL, + "ocaml_open_of_unknown_module"); + return; + } + if (from_open == OCAML_FOUND) { const char *qn = cbm_source_unique_qn(opened, &reason); ocaml_emit(ctx, site, qn, CBM_RESOLVE_RULE_IMPORT_BINDING, open->unit, qn ? "ocaml_name_from_open" : reason); @@ -291,31 +1281,29 @@ static void ocaml_visit_application(ocaml_ctx_t *ctx, TSNode site, TSNode functi return; } const char *module = cbm_source_text(ctx->walk, path); - if (!module || strchr(module, '.')) { - ocaml_emit(ctx, site, NULL, CBM_RESOLVE_RULE_NONE, NULL, "ocaml_nested_module_path"); + const char *unit = NULL; + /* An open-ended open could define a module of this name. */ + ocaml_module_t where = + module ? ocaml_module_at_site(ctx, module, at, &unit) : OCAML_MODULE_ABSENT; + if (where == OCAML_MODULE_UNCERTAIN) { + ocaml_emit(ctx, site, NULL, CBM_RESOLVE_RULE_NONE, NULL, "ocaml_open_may_define_module"); return; } - const char *unit = ocaml_module_unit(ctx, ctx->unit, module, 0); - if (!unit) { + if (where == OCAML_MODULE_ABSENT) { ocaml_emit(ctx, site, NULL, CBM_RESOLVE_RULE_NONE, NULL, "ocaml_module_not_in_project"); return; } - /* An open-ended open could define a module of this name. */ - for (const ocaml_open_t *open = ctx->opens; open; open = open->next) { - if (open->at >= at) - continue; - char key[320]; - int length = snprintf(key, sizeof(key), "%s%s", OCAML_MODULE_PREFIX, module); - bool defines = length > 0 && (size_t)length < sizeof(key) && open->unit && - cbm_source_lookup(ctx->walk, open->unit, key); - if (!ocaml_unit_closed(ctx, open->unit) || defines) { - ocaml_emit(ctx, site, NULL, CBM_RESOLVE_RULE_NONE, NULL, - "ocaml_open_may_define_module"); - return; - } + const cbm_source_decl_t *decl = NULL; + ocaml_lookup_t found = ocaml_value_in(ctx, unit, name, &decl); + if (found == OCAML_UNKNOWN) { + ocaml_emit(ctx, site, NULL, CBM_RESOLVE_RULE_NONE, NULL, + ocaml_is_stdlib(unit) ? "ocaml_standard_library_value" + : "ocaml_module_is_open_ended"); + return; } - const cbm_source_decl_t *decl = cbm_source_lookup(ctx->walk, unit, name); - const char *qn = cbm_source_unique_qn(decl, &reason); + const char *qn = found == OCAML_FOUND ? cbm_source_unique_qn(decl, &reason) : NULL; + if (!qn && !reason) + reason = "source_declaration_absent"; ocaml_emit(ctx, site, qn, CBM_RESOLVE_RULE_IMPORT_BINDING, unit, qn ? "ocaml_compilation_unit_member" : reason); } @@ -389,11 +1377,54 @@ static void ocaml_visit(ocaml_ctx_t *ctx, TSNode node, unsigned depth) { return; } if (!strcmp(kind, "open_module")) { - ocaml_note_open(ctx, node); + TSNode opened = cbm_source_field(node, "module"); + if (cbm_source_is(opened, "structure")) { + ocaml_open_t *saved_opens = ctx->opens; + ocaml_visit_children(ctx, opened, depth); + ctx->opens = ocaml_keep_includes(ctx->opens, saved_opens); + } else { + ocaml_note_open(ctx, node, ocaml_module_read(opened)); + } + return; + } + /* What an include adds is in scope after it, as an open's is, and may replace a name. A + * literal struct is walked. */ + if (!strcmp(kind, "include_module")) { + TSNode body = ts_node_named_child_count(node) ? ts_node_named_child(node, 0) : (TSNode){0}; + if (cbm_source_is(body, "typed_module_expression") && ts_node_named_child_count(body)) + body = ts_node_named_child(body, 0); + ocaml_open_t *saved_opens = ctx->opens; + bool literal = cbm_source_is(body, "structure"); + if (!literal) + ocaml_note_open(ctx, node, ocaml_module_read(body)); + ocaml_visit_children(ctx, node, depth); + if (literal) + ctx->opens = ocaml_keep_includes(ctx->opens, saved_opens); + return; + } + /* `F (struct ... end)`: what the argument binds is its own, and ends with it. */ + if (!strcmp(kind, "module_application")) { + ocaml_name_t *saved = ctx->locals; + ocaml_open_t *saved_opens = ctx->opens; + bool saved_argument = ctx->in_functor_argument; + ctx->in_functor_argument = true; + ocaml_visit_children(ctx, node, depth); + ctx->in_functor_argument = saved_argument; + ctx->locals = saved; + ctx->opens = saved_opens; + return; + } + /* `M.(e)` and `let open M in e`: M is open for e and no further. */ + if (!strcmp(kind, "local_open_expression") || !strcmp(kind, "let_open_expression")) { + ocaml_open_t *saved_opens = ctx->opens; + if (!strcmp(kind, "local_open_expression")) + ocaml_note_open(ctx, node, ocaml_child(node, "module_path")); + ocaml_visit_children(ctx, node, depth); + ctx->opens = saved_opens; return; } if (!strcmp(kind, "value_definition")) { - ocaml_visit_value_definition(ctx, node, false, depth); + ocaml_visit_value_definition(ctx, node, ctx->in_functor_argument, depth); return; } if (!strcmp(kind, "let_expression")) { @@ -417,7 +1448,7 @@ static void ocaml_visit(ocaml_ctx_t *ctx, TSNode node, unsigned depth) { uint32_t count = ts_node_named_child_count(node); for (uint32_t i = 0; i < count; i++) { TSNode child = ts_node_named_child(node, i); - if (cbm_source_is(child, "parameter")) + if (cbm_source_is(child, "parameter") || cbm_source_is(child, "value_pattern")) ocaml_bind_pattern(ctx, child, depth + 1); } TSNode pattern = cbm_source_field(node, "pattern"); @@ -428,8 +1459,7 @@ static void ocaml_visit(ocaml_ctx_t *ctx, TSNode node, unsigned depth) { return; } if (!strcmp(kind, "module_definition")) { - /* A nested structure has names and opens of its own, which this walk does - * not scope; what runs in it decides nothing. */ + /* A nested structure has names and opens of its own, kept under its key. */ TSNode binding = ocaml_child(node, "module_binding"); TSNode body = cbm_source_field(binding, "body"); if (cbm_source_is(body, "structure")) { @@ -441,6 +1471,8 @@ static void ocaml_visit(ocaml_ctx_t *ctx, TSNode node, unsigned depth) { ocaml_visit_children(ctx, body, depth); ctx->unit = saved; ctx->opens = saved_opens; + } else if (!ts_node_is_null(body)) { + ocaml_visit(ctx, body, depth + 1); } return; } @@ -451,11 +1483,22 @@ static void ocaml_visit(ocaml_ctx_t *ctx, TSNode node, unsigned depth) { } static void ocaml_resolve(cbm_source_walk_t *walk) { - ocaml_ctx_t ctx = {.walk = walk, .unit = ocaml_unit_key(walk)}; + ocaml_ctx_t ctx = {.walk = walk, + .unit = ocaml_unit_key(walk), + .closed_units = cbm_ht_create(16), + .member_modules = cbm_ht_create(64), + .member_values = cbm_ht_create(64), + .decl_paths = cbm_ht_create(16)}; ocaml_visit_children(&ctx, walk->root, 0); + cbm_ht_free(ctx.closed_units); + cbm_ht_free(ctx.member_modules); + cbm_ht_free(ctx.member_values); + cbm_ht_free(ctx.decl_paths); } const cbm_source_adapter_t cbm_source_adapter_ocaml = {.language = CBM_LANG_OCAML, .collect = ocaml_collect, + .collect_unusable = ocaml_collect_unusable, .resolve = ocaml_resolve, - .decides_references = true}; + .decides_references = true, + .contains_parse_damage = true}; diff --git a/internal/cbm/resolve/adapter_perl.c b/internal/cbm/resolve/adapter_perl.c index 966c4e7af..12e072e2a 100644 --- a/internal/cbm/resolve/adapter_perl.c +++ b/internal/cbm/resolve/adapter_perl.c @@ -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}; diff --git a/internal/cbm/resolve/adapter_ruby.c b/internal/cbm/resolve/adapter_ruby.c index 540949728..67f38ae65 100644 --- a/internal/cbm/resolve/adapter_ruby.c +++ b/internal/cbm/resolve/adapter_ruby.c @@ -13,24 +13,69 @@ * superclass, finally Object. A subclass overriding it is runtime dispatch, which * CALLS does not model. In a `def self.x` or `class << self` body, or directly in * a class body, self is the class, so singleton methods are searched instead, - * then extended modules, then the superclass's. `Const.m` searches Const's - * singleton chain the same way and `Const.new` selects `initialize`. + * then extended modules, then the superclass's. `self.class.m` from an instance + * searches that same chain, and `module_function` copies a module's instance + * methods onto it. `Const.m` searches Const's singleton chain. + * + * `Const.new` allocates and runs `initialize`, unless the project's part of the + * singleton chain declares its own `new`. It selects that `initialize`, or the + * class itself when the project's part of the chain has none. An ancestor outside + * the project may override `new`, but an override still constructs this class, so + * leaving the project does not change what is selected. + * + * `a.b = c` is a call of `b=`, never of the reader `b`, and `a.b ||= c` calls both. * * Reopening is the language's normal mode. So all files declaring a method of the - * same class count, and two declarations of one method are no single target. - * `define_method`, `alias_method`, `attr_*`, `undef_method` and `remove_method` - * with a literal name replace that name, which then abstains. A block can run - * with another self through instance_eval and friends, so anything in those, and - * every block written directly in a class body or at the top of a file (the shape - * of a DSL), abstains. A constant resolves lexically, innermost first, then at the - * top level; constants inherited from an ancestor are a known limit. */ + * same class count. Within one file Ruby runs top to bottom, so the last declaration + * stands; across files the load order decides, so two declarations are no single + * target, and neither is one that may not run. `define_method`, `attr_*`, + * `undef_method` and `remove_method` with a literal name declare that name with no + * node, which abstains if it is the one that stands. `alias` and `alias_method` name + * the earlier method of the same file, and otherwise abstain. + * + * A block keeps the self it was written under unless the method it is handed to + * rebinds that. So it keeps self only for a method known not to: a project method + * this rule selected, or a core iterator, and in both cases only if no project + * `def` of that name hands its block on to anything but a core iterator, or keeps + * it. Everything else is the shape of a DSL and abstains. A lambda in a class body + * or at the top of a file is handed to something the same way, and abstains too. + * A `define_method` block is the body of an instance method of the lexical class. + * + * A constant resolves in each lexically enclosing scope's own table, innermost + * first, then the ancestors of the innermost one, then the top level. Blocks never + * change this, so a mention resolves under an unknown self as well. A mention of a + * constant declared once is a USAGE of it; a reopened namespace is no single + * declaration and abstains. A file-level local is the one local the graph has a node + * for, so it alone can be read or written; every other bare identifier is a local + * with no node or a call of self. */ #include "source_adapter_internal.h" #include #include enum { RUBY_MAX_NEST = 32, RUBY_MAX_BASES = 64 }; +/* Declaration facts of this adapter, above the shared flags. */ +enum { + RUBY_DECL_ALIAS = 1u << 20, /* `type` names the method this is another name of */ + RUBY_DECL_MODULE_FUNCTION = 1u << 21, /* `type` names the instance method this copies */ + RUBY_DECL_CONSTANT_ALIAS = 1u << 22 /* `type` is "cref|text" of the constant it holds */ +}; + #define RUBY_OBJECT "Object" +/* Name sets sharing the table: no constant path can spell these owners. One holds a + * name for each `def` that can rebind its caller's block, with the method it forwards + * that block to in `type` when forwarding is why. The same notes sit under the def's own + * class, RUBY_REBINDING_OF + owner, for a call that selected that very def. The last set + * holds every `def` name. */ +#define RUBY_REBINDING_DEFS \ + "\x01" \ + "rebinding" +#define RUBY_REBINDING_OF \ + "\x01" \ + "of:" +#define RUBY_ALL_DEFS \ + "\x01" \ + "defs" typedef enum { RUBY_SELF_UNKNOWN, @@ -44,7 +89,8 @@ typedef struct { cbm_source_walk_t *walk; const char *nest[RUBY_MAX_NEST]; int depth; - bool singleton; /* inside class << self */ + bool singleton; /* inside class << self */ + bool module_function; /* after a bare module_function in this module body */ } ruby_ctx_t; static const char *ruby_current(const ruby_ctx_t *ctx) { @@ -69,6 +115,15 @@ static const char *ruby_declared_owner(ruby_ctx_t *ctx, TSNode name) { : text; } +/* The lexically enclosing scopes as stored with a deferred constant, innermost last. */ +static const char *ruby_cref(ruby_ctx_t *ctx) { + const char *joined = ""; + for (int i = 0; joined && i < ctx->depth; i++) + joined = + i ? cbm_arena_sprintf(ctx->walk->arena, "%s;%s", joined, ctx->nest[i]) : ctx->nest[i]; + return joined; +} + static const char *ruby_literal_symbol(cbm_source_walk_t *walk, TSNode node) { if (cbm_source_is(node, "simple_symbol")) { const char *text = cbm_source_text(walk, node); @@ -115,6 +170,128 @@ static bool ruby_rebinds_self(const char *name) { return ruby_name_in(name, names); } +/* Ruby core's block-taking methods: Enumerable, Array, Hash, String, Integer, Kernel, + * Comparable, IO, File, Dir, Mutex and Monitor. Each yields to its block and none + * rebinds self. A project `def` of one of these names is audited separately. */ +static bool ruby_core_block_method(const char *name) { + static const char *const names[] = {"all?", + "any?", + "at_exit", + "catch", + "chdir", + "chunk", + "chunk_while", + "collect", + "collect!", + "collect_concat", + "count", + "cycle", + "delete_if", + "detect", + "downto", + "drop_while", + "each", + "each_byte", + "each_char", + "each_cons", + "each_entry", + "each_index", + "each_key", + "each_line", + "each_pair", + "each_slice", + "each_value", + "each_with_index", + "each_with_object", + "fetch", + "filter", + "filter!", + "filter_map", + "find", + "find_all", + "find_index", + "flat_map", + "fork", + "foreach", + "group_by", + "gsub", + "gsub!", + "index", + "inject", + "keep_if", + "lambda", + "loop", + "map", + "map!", + "max", + "max_by", + "merge", + "merge!", + "min", + "min_by", + "minmax", + "minmax_by", + "mktmpdir", + "mon_synchronize", + "none?", + "one?", + "open", + "partition", + "popen", + "popen2", + "popen2e", + "popen3", + "proc", + "reduce", + "reject", + "reject!", + "reverse_each", + "rindex", + "scan", + "select", + "select!", + "slice_after", + "slice_before", + "slice_when", + "sort", + "sort!", + "sort_by", + "sort_by!", + "step", + "sub", + "sub!", + "sum", + "synchronize", + "take_while", + "tally_by", + "tap", + "then", + "times", + "to_h", + "transform_keys", + "transform_keys!", + "transform_values", + "transform_values!", + "trap", + "uniq", + "uniq!", + "update", + "upto", + "with_index", + "with_object", + "yield_self", + "zip", + NULL}; + return ruby_name_in(name, names); +} + +/* `Thread.new { }` and these run their block as written; `Class.new { }` does not. */ +static bool ruby_core_class_keeps_block_self(const char *name) { + static const char *const names[] = {"Array", "Enumerator", "Fiber", "Hash", + "Proc", "Set", "Thread", NULL}; + return ruby_name_in(name, names); +} + /* ── Phase one ─────────────────────────────────────────────────────── */ static void ruby_declare_key(ruby_ctx_t *ctx, const char *name, uint32_t flags, const char *qn, @@ -125,40 +302,105 @@ static void ruby_declare_key(ruby_ctx_t *ctx, const char *name, uint32_t flags, (void)cbm_source_declare(ctx->walk, owner, key, qn, flags, line); } -static void ruby_note_body_call(ruby_ctx_t *ctx, TSNode call) { +static void ruby_collect_method(ruby_ctx_t *ctx, TSNode method, bool conditional); +static void ruby_collect_singleton_method(ruby_ctx_t *ctx, TSNode method, bool conditional); + +/* `alias_method :a, :b`, `alias a b`: a is whatever b was when this ran. */ +static void ruby_declare_alias(ruby_ctx_t *ctx, const char *name, const char *original, + uint32_t line) { + const char *owner = ctx->depth ? ruby_current(ctx) : RUBY_OBJECT; + const char *key = ctx->singleton ? cbm_arena_sprintf(ctx->walk->arena, "self.%s", name) : name; + cbm_source_decl_t *decl = + key ? cbm_source_declare(ctx->walk, owner, key, NULL, + original ? RUBY_DECL_ALIAS : CBM_SOURCE_DECL_F_OPEN, line) + : NULL; + if (decl && original) + decl->type = cbm_source_table_intern( + ctx->walk, + ctx->singleton ? cbm_arena_sprintf(ctx->walk->arena, "self.%s", original) : original); +} + +/* `module_function` copies the instance method onto the module. The copy is one + * declaration with one node, so it names the method and carries no QN of its own. */ +static void ruby_declare_module_function(ruby_ctx_t *ctx, const char *name, uint32_t line) { + const char *key = cbm_arena_sprintf(ctx->walk->arena, "self.%s", name); + cbm_source_decl_t *decl = key && ctx->depth + ? cbm_source_declare(ctx->walk, ruby_current(ctx), key, NULL, + RUBY_DECL_MODULE_FUNCTION, line) + : NULL; + if (decl) + decl->type = cbm_source_table_intern(ctx->walk, name); +} + +static void ruby_note_body_call(ruby_ctx_t *ctx, TSNode call, bool conditional) { const char *name = ruby_call_name(ctx->walk, call); if (!name) return; TSNode arguments = cbm_source_field(call, "arguments"); + if (ts_node_is_null(arguments)) + return; uint32_t line = ts_node_start_point(call).row + 1; - uint32_t count = ts_node_is_null(arguments) ? 0 : ts_node_named_child_count(arguments); bool attribute = ruby_attribute_macro(name); bool rewrite = ruby_rewrites_method(name); - if (!attribute && !rewrite && strcmp(name, "alias_method") != 0) - return; - bool only_first = !strcmp(name, "alias_method"); - for (uint32_t i = 0; i < count && (!only_first || i == 0); i++) { - const char *literal = ruby_literal_symbol(ctx->walk, ts_node_named_child(arguments, i)); - if (!literal) - continue; - ruby_declare_key(ctx, literal, CBM_SOURCE_DECL_F_OPEN, NULL, line); - if (attribute && (!strcmp(name, "attr_accessor") || !strcmp(name, "attr_writer"))) { - const char *writer = cbm_arena_sprintf(ctx->walk->arena, "%s=", literal); - if (writer) - ruby_declare_key(ctx, writer, CBM_SOURCE_DECL_F_OPEN, NULL, line); - } + bool alias = !strcmp(name, "alias_method"); + bool module_function = !strcmp(name, "module_function"); + const char *first = NULL; + uint32_t index = 0; + TSTreeCursor cursor = ts_tree_cursor_new(arguments); + if (ts_tree_cursor_goto_first_child(&cursor)) { + do { + TSNode argument = ts_tree_cursor_current_node(&cursor); + if (!ts_node_is_named(argument)) + continue; + /* `private def x`: the def runs in the class body like any other. */ + if (cbm_source_is(argument, "method")) { + ruby_collect_method(ctx, argument, conditional); + if (module_function) { + const char *text = + cbm_source_text(ctx->walk, cbm_source_field(argument, "name")); + if (text) + ruby_declare_module_function(ctx, text, line); + } + } else if (cbm_source_is(argument, "singleton_method")) { + ruby_collect_singleton_method(ctx, argument, conditional); + } else if (cbm_source_is(argument, "call")) { + ruby_note_body_call(ctx, argument, conditional); /* `private attr_reader :x` */ + } else { + const char *literal = ruby_literal_symbol(ctx->walk, argument); + if (literal && module_function) + ruby_declare_module_function(ctx, literal, line); + else if (literal && alias && index == 0) + first = literal; + else if (literal && alias && index == 1 && first) + ruby_declare_alias(ctx, first, literal, line); + else if (literal && (attribute || rewrite)) + ruby_declare_key(ctx, literal, CBM_SOURCE_DECL_F_OPEN, NULL, line); + if (literal && attribute && + (!strcmp(name, "attr_accessor") || !strcmp(name, "attr_writer"))) { + const char *writer = cbm_arena_sprintf(ctx->walk->arena, "%s=", literal); + if (writer) + ruby_declare_key(ctx, writer, CBM_SOURCE_DECL_F_OPEN, NULL, line); + } + } + index++; + } while (ts_tree_cursor_goto_next_sibling(&cursor)); } + ts_tree_cursor_delete(&cursor); + if (alias && first && index < 2) + ruby_declare_alias(ctx, first, NULL, line); /* A computed original abstains. */ } /* Each mixin keeps the lexical path that its constant is read under. */ -static void ruby_gather_mixin(ruby_ctx_t *ctx, TSNode call, const char **bases, size_t *used) { +static void ruby_gather_mixin(ruby_ctx_t *ctx, TSNode call, bool in_singleton_class, + const char **bases, size_t *used) { const char *name = ruby_call_name(ctx->walk, call); char tag = 0; + /* Inside `class << self`, include is what extend is outside it. */ if (name && !strcmp(name, "include")) - tag = 'I'; - else if (name && !strcmp(name, "prepend")) + tag = in_singleton_class ? 'E' : 'I'; + else if (name && !strcmp(name, "prepend") && !in_singleton_class) tag = 'P'; - else if (name && !strcmp(name, "extend")) + else if (name && !strcmp(name, "extend") && !in_singleton_class) tag = 'E'; if (!tag) return; @@ -171,8 +413,8 @@ static void ruby_gather_mixin(ruby_ctx_t *ctx, TSNode call, const char **bases, cbm_source_is(argument, "self")) text = cbm_source_text(ctx->walk, argument); /* A computed mixin is an unknown ancestor; the '?' keeps that visible. */ - bases[(*used)++] = cbm_arena_sprintf(ctx->walk->arena, "%c:%s|%s", tag, ruby_current(ctx), - text ? text : "?"); + bases[(*used)++] = + cbm_arena_sprintf(ctx->walk->arena, "%c:%s|%s", tag, ruby_cref(ctx), text ? text : "?"); } } @@ -186,6 +428,8 @@ static void ruby_collect_method(ruby_ctx_t *ctx, TSNode method, bool conditional uint32_t line = ts_node_start_point(method).row + 1; ruby_declare_key(ctx, text, conditional ? CBM_SOURCE_DECL_F_CONDITIONAL : 0, cbm_source_def_qn(ctx->walk, text, line), line); + if (ctx->module_function && !ctx->singleton) + ruby_declare_module_function(ctx, text, line); } static void ruby_collect_singleton_method(ruby_ctx_t *ctx, TSNode method, bool conditional) { @@ -206,7 +450,14 @@ static void ruby_collect_class(ruby_ctx_t *ctx, TSNode node, bool conditional, u if (cbm_source_is(node, "module")) flags |= CBM_SOURCE_DECL_F_INTERFACE; /* Modules have no Object step. */ uint32_t line = ts_node_start_point(node).row + 1; - cbm_source_decl_t *record = cbm_source_declare(ctx->walk, owner, "", NULL, flags, line); + /* The class's node carries the name as its header spells it. */ + const char *spelled = cbm_source_text(ctx->walk, cbm_source_field(node, "name")); + const char *qn = NULL; + if (spelled && spelled[0] == ':' && spelled[1] == ':') + spelled += 2; + if (spelled) + qn = cbm_source_def_qn(ctx->walk, spelled, line); + cbm_source_decl_t *record = cbm_source_declare(ctx->walk, owner, "", qn, flags, line); const char *bases[RUBY_MAX_BASES]; size_t used = 0; TSNode superclass = cbm_source_field(node, "superclass"); @@ -218,19 +469,43 @@ static void ruby_collect_class(ruby_ctx_t *ctx, TSNode node, bool conditional, u : NULL; /* Read in the enclosing nest: the class is not in scope for its own header. */ bases[used++] = - cbm_arena_sprintf(ctx->walk->arena, "S:%s|%s", ruby_current(ctx), text ? text : "?"); + cbm_arena_sprintf(ctx->walk->arena, "S:%s|%s", ruby_cref(ctx), text ? text : "?"); } if (!ruby_push(ctx, owner)) { cbm_source_fail(ctx->walk, "ruby_nesting_limit"); return; } bool saved = ctx->singleton; + bool saved_function = ctx->module_function; ctx->singleton = false; + ctx->module_function = false; TSNode body = cbm_source_field(node, "body"); - uint32_t count = ts_node_is_null(body) ? 0 : ts_node_named_child_count(body); - for (uint32_t i = 0; i < count; i++) - if (cbm_source_is(ts_node_named_child(body, i), "call")) - ruby_gather_mixin(ctx, ts_node_named_child(body, i), bases, &used); + if (!ts_node_is_null(body)) { + TSTreeCursor cursor = ts_tree_cursor_new(body); + if (ts_tree_cursor_goto_first_child(&cursor)) { + do { + TSNode child = ts_tree_cursor_current_node(&cursor); + if (cbm_source_is(child, "call")) { + ruby_gather_mixin(ctx, child, false, bases, &used); + } else if (cbm_source_is(child, "singleton_class") && + cbm_source_is(cbm_source_field(child, "value"), "self")) { + TSNode inner = cbm_source_field(child, "body"); + if (ts_node_is_null(inner)) + continue; + TSTreeCursor walker = ts_tree_cursor_new(inner); + if (ts_tree_cursor_goto_first_child(&walker)) { + do { + TSNode statement = ts_tree_cursor_current_node(&walker); + if (cbm_source_is(statement, "call")) + ruby_gather_mixin(ctx, statement, true, bases, &used); + } while (ts_tree_cursor_goto_next_sibling(&walker)); + } + ts_tree_cursor_delete(&walker); + } + } while (ts_tree_cursor_goto_next_sibling(&cursor)); + } + ts_tree_cursor_delete(&cursor); + } if (record) { const char **copy = cbm_source_table_alloc(ctx->walk, (used + 1) * sizeof(*copy)); if (copy) { @@ -243,9 +518,39 @@ static void ruby_collect_class(ruby_ctx_t *ctx, TSNode node, bool conditional, u if (!ts_node_is_null(body)) ruby_collect_body(ctx, body, conditional, depth + 1); ctx->singleton = saved; + ctx->module_function = saved_function; ctx->depth--; } +/* `NAME = value`. A constant holding another constant is that constant's second name. */ +static void ruby_collect_constant(ruby_ctx_t *ctx, TSNode assignment, bool conditional) { + TSNode left = cbm_source_field(assignment, "left"); + if (!cbm_source_is(left, "constant")) + return; + const char *name = cbm_source_text(ctx->walk, left); + if (!name) + return; + const char *path = + ctx->depth ? cbm_arena_sprintf(ctx->walk->arena, "%s::%s", ruby_current(ctx), name) : name; + uint32_t line = ts_node_start_point(assignment).row + 1; + if (!path) + return; + cbm_source_decl_t *decl = + cbm_source_declare(ctx->walk, path, "", cbm_source_def_qn(ctx->walk, name, line), + conditional ? CBM_SOURCE_DECL_F_VALUE | CBM_SOURCE_DECL_F_CONDITIONAL + : CBM_SOURCE_DECL_F_VALUE, + line); + TSNode right = cbm_source_field(assignment, "right"); + const char *held = cbm_source_is(right, "constant") || cbm_source_is(right, "scope_resolution") + ? cbm_source_text(ctx->walk, right) + : NULL; + if (decl && held && !conditional) { + decl->flags |= RUBY_DECL_CONSTANT_ALIAS; + decl->type = cbm_source_table_intern( + ctx->walk, cbm_arena_sprintf(ctx->walk->arena, "%s|%s", ruby_cref(ctx), held)); + } +} + static bool ruby_block_node(TSNode node) { return cbm_source_is(node, "block") || cbm_source_is(node, "do_block") || cbm_source_is(node, "lambda"); @@ -277,11 +582,18 @@ static void ruby_collect_body(ruby_ctx_t *ctx, TSNode body, bool conditional, un ruby_collect_singleton_method(ctx, child, conditional); } else if (cbm_source_is(child, "alias")) { const char *name = cbm_source_text(ctx->walk, cbm_source_field(child, "name")); + const char *original = cbm_source_text(ctx->walk, cbm_source_field(child, "alias")); if (name) - ruby_declare_key(ctx, name, CBM_SOURCE_DECL_F_OPEN, NULL, - ts_node_start_point(child).row + 1); + ruby_declare_alias(ctx, name, original, ts_node_start_point(child).row + 1); } else if (cbm_source_is(child, "call")) { - ruby_note_body_call(ctx, child); + ruby_note_body_call(ctx, child, conditional); + } else if (cbm_source_is(child, "identifier")) { + const char *word = ctx->depth ? cbm_source_text(ctx->walk, child) : NULL; + if (word && !strcmp(word, "module_function")) + ctx->module_function = true; + } else if (cbm_source_is(child, "assignment")) { + ruby_collect_constant(ctx, child, conditional); + ruby_collect_body(ctx, child, true, depth + 1); } else if (ruby_block_node(child)) { /* A def in a block belongs to whatever the block runs against. */ continue; @@ -294,78 +606,345 @@ static void ruby_collect_body(ruby_ctx_t *ctx, TSNode body, bool conditional, un ts_tree_cursor_delete(&cursor); } +/* ── Phase one: which defs can rebind their caller's block ─────────── */ + +typedef struct { + ruby_ctx_t ctx; + cbm_source_walk_t *walk; + const char *block_param; /* the `&name` of the def being read */ + const char *def_name; + const char *def_key; /* the def's own table key */ + const char *def_owner; /* NULL for a def in a block, which no chain reaches */ + uint32_t def_line; + bool in_def; + bool in_block; +} ruby_def_audit_t; + +static void ruby_note_rebinding(ruby_def_audit_t *audit, const char *forwarded_to) { + cbm_source_decl_t *global = cbm_source_declare(audit->walk, RUBY_REBINDING_DEFS, + audit->def_name, NULL, 0, audit->def_line); + const char *own = audit->def_owner ? cbm_arena_sprintf(audit->walk->arena, "%s%s", + RUBY_REBINDING_OF, audit->def_owner) + : NULL; + cbm_source_decl_t *local = + own ? cbm_source_declare(audit->walk, own, audit->def_key, NULL, 0, audit->def_line) : NULL; + if (forwarded_to) { + if (global) + global->type = cbm_source_table_intern(audit->walk, forwarded_to); + if (local) + local->type = cbm_source_table_intern(audit->walk, forwarded_to); + } +} + +/* Asking about a block or running it keeps self; anything else lets it be kept. */ +static bool ruby_block_read_is_harmless(cbm_source_walk_t *walk, TSNode read, TSNode parent) { + if (cbm_source_is(parent, "call") && ts_node_eq(cbm_source_field(parent, "receiver"), read)) { + static const char *const names[] = {"call", "yield", "arity", "parameters", "nil?", + "lambda?", "present?", "blank?", NULL}; + TSNode method = cbm_source_field(parent, "method"); + return ts_node_is_null(method) || ruby_name_in(cbm_source_text(walk, method), names); + } + if (cbm_source_is(parent, "element_reference") || cbm_source_is(parent, "unary")) + return true; + if (cbm_source_is(parent, "binary") && cbm_source_token(parent, "&&")) + return true; + return ts_node_eq(cbm_source_field(parent, "condition"), read); +} + +static void ruby_audit_block_use(ruby_def_audit_t *audit, TSNode node, TSNode parent, TSNode call) { + cbm_source_walk_t *walk = audit->walk; + if (cbm_source_is(node, "super")) { + ruby_note_rebinding(audit, NULL); /* A bare super hands the block on, to whatever. */ + } else if (cbm_source_is(node, "block_argument")) { + TSNode passed = + ts_node_named_child_count(node) ? ts_node_named_child(node, 0) : (TSNode){0}; + const char *text = + cbm_source_is(passed, "identifier") ? cbm_source_text(walk, passed) : NULL; + bool forwarded = ts_node_is_null(passed) || + (text && audit->block_param && !strcmp(text, audit->block_param)); + if (forwarded) { + /* The block goes wherever the callee takes it. A callee on self can be read + * in the project; on any other receiver it can be anything's. */ + const char *target = cbm_source_text(walk, cbm_source_field(call, "method")); + TSNode receiver = cbm_source_field(call, "receiver"); + bool on_self = ts_node_is_null(receiver) || cbm_source_is(receiver, "self"); + if (!target || !ruby_core_block_method(target)) + ruby_note_rebinding(audit, on_self ? target : NULL); + } + } else if (cbm_source_is(node, "identifier") && audit->block_param && + !cbm_source_is(parent, "block_argument") && + !cbm_source_is(parent, "block_parameter")) { + const char *text = cbm_source_text(walk, node); + if (text && !strcmp(text, audit->block_param) && + !ruby_block_read_is_harmless(walk, node, parent)) + ruby_note_rebinding(audit, NULL); + } +} + +static const char *ruby_block_parameter(cbm_source_walk_t *walk, TSNode def) { + TSNode parameters = cbm_source_field(def, "parameters"); + if (ts_node_is_null(parameters)) + return NULL; + const char *name = NULL; + TSTreeCursor cursor = ts_tree_cursor_new(parameters); + if (ts_tree_cursor_goto_first_child(&cursor)) { + do { + TSNode child = ts_tree_cursor_current_node(&cursor); + if (cbm_source_is(child, "block_parameter")) + name = cbm_source_text(walk, cbm_source_field(child, "name")); + } while (ts_tree_cursor_goto_next_sibling(&cursor)); + } + ts_tree_cursor_delete(&cursor); + return name; +} + +/* Every def counts, even one in a block no chain can reach: its name can rebind. */ +static void ruby_audit_defs(ruby_def_audit_t *audit, TSNode node, TSNode parent, TSNode call, + unsigned depth) { + if (!cbm_source_step(audit->walk, depth)) + return; + bool def = cbm_source_is(node, "method") || cbm_source_is(node, "singleton_method"); + ruby_def_audit_t saved = *audit; + if (def) { + bool singleton = audit->ctx.singleton || cbm_source_is(node, "singleton_method"); + audit->def_name = cbm_source_text(audit->walk, cbm_source_field(node, "name")); + audit->def_key = singleton && audit->def_name + ? cbm_arena_sprintf(audit->walk->arena, "self.%s", audit->def_name) + : audit->def_name; + audit->def_owner = audit->in_block ? NULL + : audit->ctx.depth ? ruby_current(&audit->ctx) + : RUBY_OBJECT; + audit->def_line = ts_node_start_point(node).row + 1; + audit->in_def = audit->def_name != NULL; + audit->block_param = ruby_block_parameter(audit->walk, node); + if (audit->def_name) + (void)cbm_source_declare(audit->walk, RUBY_ALL_DEFS, audit->def_name, NULL, 0, + audit->def_line); + } else if (audit->in_def) { + ruby_audit_block_use(audit, node, parent, call); + } else if (ruby_block_node(node)) { + audit->in_block = true; + } else if (cbm_source_is(node, "class") || cbm_source_is(node, "module")) { + const char *owner = ruby_declared_owner(&audit->ctx, cbm_source_field(node, "name")); + if (owner && ruby_push(&audit->ctx, owner)) + audit->ctx.singleton = false; + } else if (cbm_source_is(node, "singleton_class")) { + audit->ctx.singleton = cbm_source_is(cbm_source_field(node, "value"), "self"); + } + TSNode next_call = cbm_source_is(node, "call") ? node : call; + TSTreeCursor cursor = ts_tree_cursor_new(node); + if (ts_tree_cursor_goto_first_child(&cursor)) { + do { + TSNode child = ts_tree_cursor_current_node(&cursor); + if (ts_node_is_named(child)) + ruby_audit_defs(audit, child, node, next_call, depth + 1); + } while (ts_tree_cursor_goto_next_sibling(&cursor)); + } + ts_tree_cursor_delete(&cursor); + /* A def's own notes are written as it goes; the rest of this state is its scope. */ + if (def || ruby_block_node(node) || cbm_source_is(node, "class") || + cbm_source_is(node, "module") || cbm_source_is(node, "singleton_class")) + *audit = saved; +} + static void ruby_collect(cbm_source_walk_t *walk) { ruby_ctx_t ctx = {.walk = walk}; ruby_collect_body(&ctx, walk->root, false, 0); + ruby_def_audit_t audit = {.ctx = {.walk = walk}, .walk = walk}; + ruby_audit_defs(&audit, walk->root, (TSNode){0}, (TSNode){0}, 0); } /* ── Phase two: constants and ancestor chains ──────────────────────── */ -static const char *ruby_resolve_constant_in(const ruby_ctx_t *ctx, const char *cref, - const char *text) { - if (!text || !text[0] || !strcmp(text, "?")) - return NULL; - if (text[0] == ':' && text[1] == ':') - return cbm_source_lookup(ctx->walk, text + 2, "") ? text + 2 : NULL; - const char *colons = strstr(text, "::"); - size_t first = colons ? (size_t)(colons - text) : strlen(text); - char prefix[512]; - if (cref && strlen(cref) < sizeof(prefix)) { - strcpy(prefix, cref); - while (prefix[0]) { - char candidate[1024]; - int length = - snprintf(candidate, sizeof(candidate), "%s::%.*s", prefix, (int)first, text); - if (length > 0 && (size_t)length < sizeof(candidate) && - cbm_source_lookup(ctx->walk, candidate, "")) { - /* The first segment decides; the rest must exist under it. */ - const char *full = cbm_arena_sprintf(ctx->walk->arena, "%s::%s", prefix, text); - return full && cbm_source_lookup(ctx->walk, full, "") ? full : NULL; - } - char *cut = strrchr(prefix, ':'); - if (!cut || cut == prefix) - break; - cut[-1] = '\0'; - } - } - char head[512]; - if (first < sizeof(head)) { - memcpy(head, text, first); - head[first] = '\0'; - if (cbm_source_lookup(ctx->walk, head, "")) - return cbm_source_lookup(ctx->walk, text, "") ? text : NULL; - } - return NULL; +enum { RUBY_CONSTANT_BUFFER = 1024 }; + +static const cbm_source_decl_t *ruby_constant_at(const ruby_ctx_t *ctx, const char *owner, + const char *name, size_t name_length) { + char candidate[RUBY_CONSTANT_BUFFER]; + int length = + owner && owner[0] + ? snprintf(candidate, sizeof(candidate), "%s::%.*s", owner, (int)name_length, name) + : snprintf(candidate, sizeof(candidate), "%.*s", (int)name_length, name); + return length > 0 && (size_t)length < sizeof(candidate) + ? cbm_source_lookup(ctx->walk, candidate, "") + : NULL; } -/* Reads a stored "tag:cref|text" base. */ +static bool ruby_is_type(const cbm_source_decl_t *records) { + for (const cbm_source_decl_t *record = records; record; record = record->next) + if (record->flags & CBM_SOURCE_DECL_F_TYPE) + return true; + return false; +} + +static const cbm_source_decl_t *ruby_constant_path(const ruby_ctx_t *ctx, const char *const *nest, + int depth, const char *text, bool ancestors); + +/* Splits a stored "a;a::b" in place; returns how many scopes it holds. */ +static int ruby_split_cref(char *cref, const char **nest) { + int depth = 0; + for (char *part = cref; *part && depth < RUBY_MAX_NEST;) { + nest[depth++] = part; + char *end = strchr(part, ';'); + if (!end) + break; + *end = '\0'; + part = end + 1; + } + return depth; +} + +/* A constant written once as another constant reads as that one. A cycle of these + * names no class; the walk visits each name at most once. */ +static const cbm_source_decl_t *ruby_follow_constant(const ruby_ctx_t *ctx, + const cbm_source_decl_t *records) { + cbm_source_chain_t seen = {0}; + while (records && !records->next && (records->flags & RUBY_DECL_CONSTANT_ALIAS)) { + if (!cbm_source_chain_first_visit(&seen, records->owner)) + return NULL; + const char *bar = strchr(records->type, '|'); + char cref[RUBY_CONSTANT_BUFFER]; + size_t length = bar ? (size_t)(bar - records->type) : sizeof(cref); + if (length >= sizeof(cref)) + return NULL; + memcpy(cref, records->type, length); + cref[length] = '\0'; + const char *nest[RUBY_MAX_NEST]; + int depth = ruby_split_cref(cref, nest); + records = ruby_constant_path(ctx, nest, depth, bar + 1, false); + } + return records; +} + +/* Reads a stored "tag:cref|text" base. Lexical and top level only: looking through an + * ancestor here would need the ancestors this is computing. */ static const char *ruby_base_owner(const ruby_ctx_t *ctx, const char *base) { const char *bar = strchr(base, '|'); if (!bar || !base[1]) return NULL; - char cref[512]; + char cref[RUBY_CONSTANT_BUFFER]; size_t length = (size_t)(bar - (base + 2)); if (length >= sizeof(cref)) return NULL; memcpy(cref, base + 2, length); cref[length] = '\0'; - return ruby_resolve_constant_in(ctx, cref, bar + 1); + const char *nest[RUBY_MAX_NEST]; + int depth = ruby_split_cref(cref, nest); + const cbm_source_decl_t *records = + ruby_follow_constant(ctx, ruby_constant_path(ctx, nest, depth, bar + 1, false)); + return ruby_is_type(records) ? records->owner : NULL; +} + +/* A constant is inherited from prepended modules, included modules and the superclass. + * Extending a class gives it methods, not constants. An unknown ancestor can only be + * passed over: its table cannot be read. */ +static const cbm_source_decl_t *ruby_constant_of_ancestors(const ruby_ctx_t *ctx, const char *owner, + const char *name, size_t length, + cbm_source_chain_t *chain) { + if (!cbm_source_chain_first_visit(chain, owner)) + return NULL; + static const char tags[] = {'P', 'I', 'S'}; + const cbm_source_decl_t *records = cbm_source_lookup(ctx->walk, owner, ""); + for (size_t t = 0; t < sizeof(tags); t++) { + for (const cbm_source_decl_t *record = records; record; record = record->next) { + size_t count = 0; + while (record->bases && record->bases[count]) + count++; + for (size_t i = count; i > 0; i--) { + if (record->bases[i - 1][0] != tags[t]) + continue; + const char *parent = ruby_base_owner(ctx, record->bases[i - 1]); + if (!parent) + continue; + const cbm_source_decl_t *found = ruby_constant_at(ctx, parent, name, length); + if (!found) + found = ruby_constant_of_ancestors(ctx, parent, name, length, chain); + if (found) + return found; + } + } + } + return NULL; +} + +/* Reading `::A`, `A` or `A::B::C` with these scopes enclosing it, innermost last. */ +static const cbm_source_decl_t *ruby_constant_path(const ruby_ctx_t *ctx, const char *const *nest, + int depth, const char *text, bool ancestors) { + if (!text || !text[0] || !strcmp(text, "?")) + return NULL; + bool absolute = text[0] == ':' && text[1] == ':'; + if (absolute) + text += 2; + const char *colons = strstr(text, "::"); + size_t first = colons ? (size_t)(colons - text) : strlen(text); + const cbm_source_decl_t *found = NULL; + if (!absolute) { + for (int i = depth - 1; !found && i >= 0; i--) + found = ruby_constant_at(ctx, nest[i], text, first); + if (!found && ancestors && depth > 0) { + cbm_source_chain_t chain = {0}; + found = ruby_constant_of_ancestors(ctx, nest[depth - 1], text, first, &chain); + } + } + if (!found) + found = ruby_constant_at(ctx, "", text, first); + /* `class A::B` declares a path whose head can lie outside the project. */ + if (!found) + return colons ? cbm_source_lookup(ctx->walk, text, "") : NULL; + while (colons) { + const char *rest = colons + 2; + colons = strstr(rest, "::"); + size_t length = colons ? (size_t)(colons - rest) : strlen(rest); + const cbm_source_decl_t *holder = ruby_follow_constant(ctx, found); + if (!holder) + return NULL; + const cbm_source_decl_t *member = ruby_constant_at(ctx, holder->owner, rest, length); + if (!member) { + cbm_source_chain_t chain = {0}; + member = ruby_constant_of_ancestors(ctx, holder->owner, rest, length, &chain); + } + if (!member) + return ruby_constant_at(ctx, holder->owner, rest, strlen(rest)); + found = member; + } + return found; } typedef enum { RUBY_FOUND, RUBY_ABSENT, RUBY_OPEN, RUBY_AMBIGUOUS } ruby_lookup_t; +/* The declaration of a name that stands once its file has run, if one is certain. */ +static const cbm_source_decl_t *ruby_standing_declaration(const cbm_source_decl_t *first) { + const cbm_source_decl_t *last = first; + for (const cbm_source_decl_t *decl = first; decl; decl = decl->next) { + if (strcmp(decl->file, first->file) || (decl->flags & CBM_SOURCE_DECL_F_CONDITIONAL)) + return NULL; + if (decl->line >= last->line) + last = decl; + } + return last; +} + static ruby_lookup_t ruby_own(const ruby_ctx_t *ctx, const char *owner, const char *key, const cbm_source_decl_t **out) { const cbm_source_decl_t *first = cbm_source_lookup(ctx->walk, owner, key); if (!first) return RUBY_ABSENT; - const char *reason = NULL; - if (first->next || !cbm_source_unique_qn(first, &reason)) - return RUBY_AMBIGUOUS; - for (const cbm_source_decl_t *decl = first; decl; decl = decl->next) - if (decl->flags & (CBM_SOURCE_DECL_F_CONDITIONAL | CBM_SOURCE_DECL_F_OPEN)) + const cbm_source_decl_t *standing = ruby_standing_declaration(first); + /* An alias must name an earlier method of its own file, which is what it copied. + * Each step moves to an earlier line, so a chain of these ends. */ + while (standing && (standing->flags & (RUBY_DECL_ALIAS | RUBY_DECL_MODULE_FUNCTION))) { + const cbm_source_decl_t *named = cbm_source_lookup(ctx->walk, owner, standing->type); + const cbm_source_decl_t *original = named ? ruby_standing_declaration(named) : NULL; + if (!original || strcmp(original->file, standing->file)) return RUBY_AMBIGUOUS; - *out = first; + if ((standing->flags & RUBY_DECL_ALIAS) && original->line >= standing->line) + return RUBY_AMBIGUOUS; + standing = original; + } + if (!standing || !standing->qn || + (standing->flags & (CBM_SOURCE_DECL_F_OPEN | CBM_SOURCE_DECL_F_QN_SHARED))) + return RUBY_AMBIGUOUS; + *out = standing; return RUBY_FOUND; } @@ -448,8 +1027,9 @@ static ruby_lookup_t ruby_find_instance(const ruby_ctx_t *ctx, const char *owner return found; if (has_superclass) return ruby_find_instance(ctx, superclass, name, chain, out); - if (records->flags & CBM_SOURCE_DECL_F_INTERFACE) - return RUBY_ABSENT; /* A module has no Object step of its own. */ + for (const cbm_source_decl_t *record = records; record; record = record->next) + if (record->flags & CBM_SOURCE_DECL_F_INTERFACE) + return RUBY_ABSENT; /* A module has no Object step of its own. */ return ruby_own(ctx, RUBY_OBJECT, name, out); } @@ -489,6 +1069,34 @@ static ruby_lookup_t ruby_find_singleton(const ruby_ctx_t *ctx, const char *owne return ruby_find_singleton_on(ctx, owner, name, &chain, &extended, out); } +/* `super` in a method of owner: what follows owner's own step on its chain. Where a module's + * method goes next depends on what includes it, so that stays unknown. */ +static ruby_lookup_t ruby_find_super(const ruby_ctx_t *ctx, const char *owner, const char *name, + bool singleton, const cbm_source_decl_t **out) { + const cbm_source_decl_t *records = cbm_source_lookup(ctx->walk, owner, ""); + if (!records) + return RUBY_OPEN; + for (const cbm_source_decl_t *record = records; record; record = record->next) + if (!singleton && (record->flags & CBM_SOURCE_DECL_F_INTERFACE)) + return RUBY_OPEN; + cbm_source_chain_t chain = {0}, extended = {0}; + (void)cbm_source_chain_first_visit(&chain, owner); + ruby_lookup_t found = singleton + ? ruby_search_mixins(ctx, records, 'E', owner, name, &extended, out) + : ruby_search_mixins(ctx, records, 'I', owner, name, &chain, out); + if (found != RUBY_ABSENT) + return found; + const char *superclass = NULL; + bool has_superclass = false; + found = ruby_search_superclass(ctx, records, &superclass, &has_superclass); + if (found != RUBY_ABSENT) + return found; + if (!has_superclass) + return RUBY_ABSENT; + return singleton ? ruby_find_singleton_on(ctx, superclass, name, &chain, &extended, out) + : ruby_find_instance(ctx, superclass, name, &chain, out); +} + static ruby_lookup_t ruby_find_instance_from(const ruby_ctx_t *ctx, const char *owner, const char *name, const cbm_source_decl_t **out) { cbm_source_chain_t chain = {0}; @@ -502,6 +1110,7 @@ typedef struct ruby_local { TSNode value; uint32_t available_from; uint32_t bound_from; /* where the first assignment's name is, as the parser sees it */ + uint32_t first_line; /* the line of that assignment */ int writes; bool blocked; struct ruby_local *next; @@ -511,7 +1120,10 @@ typedef struct { ruby_ctx_t base; ruby_self_t self; ruby_local_t *locals; - const char *scope; /* the enclosing def's QN; NULL in a class body or unknown */ + const char *scope; /* the enclosing def's QN; NULL in a class body or unknown */ + const char *def_name; /* the def around this point, which `super` selects by */ + bool def_singleton; /* and whether it is a method of the class itself */ + const cbm_source_decl_t *selected; /* what the call just visited selected, or NULL */ } ruby_visit_t; static ruby_local_t *ruby_local(ruby_visit_t *visit, const char *name) { @@ -530,18 +1142,37 @@ static ruby_local_t *ruby_local(ruby_visit_t *visit, const char *name) { return local; } +static void ruby_block_local_name(ruby_visit_t *visit, TSNode node) { + const char *name = cbm_source_text(visit->base.walk, node); + ruby_local_t *local = name ? ruby_local(visit, name) : NULL; + if (local) + local->blocked = true; +} + +/* The names a parameter list, pattern or assignment target binds. A receiver or index + * in `a.b, c[i] = 1, 2` is read rather than bound, and a default value runs as code. */ static void ruby_block_locals(ruby_visit_t *visit, TSNode node, unsigned depth) { if (!cbm_source_step(visit->base.walk, depth)) return; if (cbm_source_is(node, "identifier")) { - const char *name = cbm_source_text(visit->base.walk, node); - ruby_local_t *local = name ? ruby_local(visit, name) : NULL; - if (local) - local->blocked = true; + ruby_block_local_name(visit, node); + return; } - uint32_t count = ts_node_named_child_count(node); - for (uint32_t i = 0; i < count; i++) - ruby_block_locals(visit, ts_node_named_child(node, i), depth + 1); + if (cbm_source_is(node, "call") || cbm_source_is(node, "element_reference")) + return; + if (cbm_source_is(node, "optional_parameter") || cbm_source_is(node, "keyword_parameter")) { + ruby_block_local_name(visit, cbm_source_field(node, "name")); + 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 (ts_node_is_named(child)) + ruby_block_locals(visit, child, depth + 1); + } while (ts_tree_cursor_goto_next_sibling(&cursor)); + } + ts_tree_cursor_delete(&cursor); } static void ruby_audit_locals(ruby_visit_t *visit, TSNode node, unsigned depth) { @@ -560,8 +1191,10 @@ static void ruby_audit_locals(ruby_visit_t *visit, TSNode node, unsigned depth) local->writes++; local->value = cbm_source_field(node, "right"); local->available_from = ts_node_end_byte(node); - if (ts_node_start_byte(left) < local->bound_from) + if (ts_node_start_byte(left) < local->bound_from) { local->bound_from = ts_node_start_byte(left); + local->first_line = ts_node_start_point(left).row + 1; + } } } else { ruby_block_locals(visit, left, depth + 1); @@ -594,17 +1227,32 @@ static void ruby_audit_locals(ruby_visit_t *visit, TSNode node, unsigned depth) local->blocked = true; } } - uint32_t count = ts_node_named_child_count(node); - for (uint32_t i = 0; i < count; i++) - ruby_audit_locals(visit, ts_node_named_child(node, i), depth + 1); + TSTreeCursor cursor = ts_tree_cursor_new(node); + if (ts_tree_cursor_goto_first_child(&cursor)) { + do { + TSNode child = ts_tree_cursor_current_node(&cursor); + if (ts_node_is_named(child)) + ruby_audit_locals(visit, child, depth + 1); + } while (ts_tree_cursor_goto_next_sibling(&cursor)); + } + ts_tree_cursor_delete(&cursor); } -static const char *ruby_constant_owner(ruby_visit_t *visit, TSNode node) { +static void ruby_visit(ruby_visit_t *visit, TSNode node, unsigned depth); + +static const cbm_source_decl_t *ruby_constant_records(ruby_visit_t *visit, TSNode node) { if (!cbm_source_is(node, "constant") && !cbm_source_is(node, "scope_resolution")) return NULL; - const char *text = cbm_source_text(visit->base.walk, node); /* A qualified constant may not have a project prefix at all. */ - return ruby_resolve_constant_in(&visit->base, ruby_current(&visit->base), text); + return ruby_constant_path(&visit->base, visit->base.nest, visit->base.depth, + cbm_source_text(visit->base.walk, node), true); +} + +/* The class a constant receiver names; a constant holding some value names none. */ +static const char *ruby_constant_owner(ruby_visit_t *visit, TSNode node) { + const cbm_source_decl_t *records = + ruby_follow_constant(&visit->base, ruby_constant_records(visit, node)); + return ruby_is_type(records) ? records->owner : NULL; } static void ruby_emit(ruby_visit_t *visit, TSNode node, const char *target, cbm_resolve_rule_t rule, @@ -613,7 +1261,7 @@ static void ruby_emit(ruby_visit_t *visit, TSNode node, const char *target, cbm_ reason); } -static void ruby_emit_lookup(ruby_visit_t *visit, TSNode node, ruby_lookup_t found, +static bool ruby_emit_lookup(ruby_visit_t *visit, TSNode node, ruby_lookup_t found, const cbm_source_decl_t *decl, const char *domain) { switch (found) { case RUBY_FOUND: { @@ -621,7 +1269,8 @@ static void ruby_emit_lookup(ruby_visit_t *visit, TSNode node, ruby_lookup_t fou ? CBM_RESOLVE_RULE_SCOPE_BINDING : CBM_RESOLVE_RULE_LANGUAGE_BINDING; ruby_emit(visit, node, decl->qn, rule, domain, "ruby_ancestor_chain_declaration"); - break; + visit->selected = decl; + return true; } case RUBY_ABSENT: ruby_emit(visit, node, NULL, CBM_RESOLVE_RULE_NONE, NULL, @@ -636,9 +1285,48 @@ static void ruby_emit_lookup(ruby_visit_t *visit, TSNode node, ruby_lookup_t fou "ruby_method_not_one_static_declaration"); break; } + return false; } -static void ruby_visit_implicit_self(ruby_visit_t *visit, TSNode node, const char *name) { +/* An occurrence extraction has no carrier for. */ +static void ruby_extra_begin(ruby_visit_t *visit, TSNode node, const char *name) { + cbm_source_walk_t *walk = visit->base.walk; + /* With no enclosing def it belongs where a carrier's would: the file's module. */ + walk->extra_scope = visit->scope ? visit->scope : walk->result->module_qn; + walk->extra_name = name; + walk->extra_line = ts_node_start_point(node).row + 1; +} + +static void ruby_extra_end(ruby_visit_t *visit) { + cbm_source_walk_t *walk = visit->base.walk; + walk->extra_scope = NULL; + walk->extra_name = NULL; + walk->extra_line = 0; +} + +/* `Const.new`, as the comment at the top of this file states it. */ +static bool ruby_visit_constructor(ruby_visit_t *visit, TSNode node, const char *owner) { + const cbm_source_decl_t *decl = NULL; + ruby_lookup_t custom = ruby_find_singleton(&visit->base, owner, "new", &decl); + if (custom == RUBY_FOUND || custom == RUBY_AMBIGUOUS) + return ruby_emit_lookup(visit, node, custom, decl, owner); + ruby_lookup_t initialize = ruby_find_instance_from(&visit->base, owner, "initialize", &decl); + if (initialize == RUBY_FOUND || initialize == RUBY_AMBIGUOUS) + return ruby_emit_lookup(visit, node, initialize, decl, owner); + const char *reason = NULL; + const char *qn = cbm_source_unique_qn(cbm_source_lookup(visit->base.walk, owner, ""), &reason); + if (!qn) { + ruby_emit(visit, node, NULL, CBM_RESOLVE_RULE_NONE, NULL, + "ruby_method_not_one_static_declaration"); + return false; + } + cbm_source_emit_facts(visit->base.walk, ts_node_start_byte(node), ts_node_end_byte(node), + CBM_SOURCE_SITE_CALL, qn, CBM_RESOLVE_RULE_LANGUAGE_BINDING, owner, + "ruby_new_constructs_the_class", CBM_RESOLVE_FACT_CLASS_CALLABLE); + return true; +} + +static bool ruby_visit_implicit_self(ruby_visit_t *visit, TSNode node, const char *name) { const cbm_source_decl_t *decl = NULL; ruby_lookup_t found = RUBY_ABSENT; const char *current = ruby_current(&visit->base); @@ -646,7 +1334,7 @@ static void ruby_visit_implicit_self(ruby_visit_t *visit, TSNode node, const cha case RUBY_SELF_UNKNOWN: ruby_emit(visit, node, NULL, CBM_RESOLVE_RULE_NONE, NULL, "ruby_block_can_run_with_another_self"); - return; + return false; case RUBY_SELF_MAIN: case RUBY_SELF_TOP_DEF: current = RUBY_OBJECT; @@ -656,10 +1344,26 @@ static void ruby_visit_implicit_self(ruby_visit_t *visit, TSNode node, const cha found = ruby_find_instance_from(&visit->base, current, name, &decl); break; case RUBY_SELF_CLASS: + if (!strcmp(name, "new")) + return ruby_visit_constructor(visit, node, current); found = ruby_find_singleton(&visit->base, current, name, &decl); break; } - ruby_emit_lookup(visit, node, found, decl, current); + return ruby_emit_lookup(visit, node, found, decl, current); +} + +/* The graph has a node for a local only at the top of a file. */ +static const char *ruby_file_local_qn(ruby_visit_t *visit, const ruby_local_t *local) { + if (visit->scope || visit->base.depth || visit->self != RUBY_SELF_MAIN || local->blocked) + return NULL; + return cbm_source_def_qn_label(visit->base.walk, local->name, local->first_line, "Variable"); +} + +static void ruby_emit_local(ruby_visit_t *visit, TSNode node, const ruby_local_t *local) { + const char *qn = local ? ruby_file_local_qn(visit, local) : NULL; + cbm_source_emit_node(visit->base.walk, node, CBM_SOURCE_SITE_REFERENCE, qn, + qn ? CBM_RESOLVE_RULE_SCOPE_BINDING : CBM_RESOLVE_RULE_NONE, NULL, + qn ? "ruby_file_level_local" : "ruby_local_variable_has_no_node"); } /* Ruby's parser reads a bare identifier as a local only when an assignment to it @@ -668,40 +1372,55 @@ static void ruby_visit_implicit_self(ruby_visit_t *visit, TSNode node, const cha static void ruby_visit_bare_identifier(ruby_visit_t *visit, TSNode node) { cbm_source_walk_t *walk = visit->base.walk; const char *name = cbm_source_text(walk, node); - if (!name || (visit->self != RUBY_SELF_MAIN && !visit->scope)) + if (!name) return; uint32_t at = ts_node_start_byte(node); for (const ruby_local_t *local = visit->locals; local; local = local->next) - if (!strcmp(local->name, name) && (local->blocked || at >= local->bound_from)) + if (!strcmp(local->name, name) && (local->blocked || at >= local->bound_from)) { + ruby_emit_local(visit, node, local); return; - walk->extra_scope = visit->scope; - walk->extra_name = name; - ruby_visit_implicit_self(visit, node, name); - walk->extra_scope = NULL; - walk->extra_name = NULL; + } + cbm_source_emit_node(walk, node, CBM_SOURCE_SITE_REFERENCE, NULL, CBM_RESOLVE_RULE_NONE, NULL, + "ruby_bare_name_is_a_call"); + ruby_extra_begin(visit, node, name); + (void)ruby_visit_implicit_self(visit, node, name); + ruby_extra_end(visit); } -static void ruby_visit_constant_call(ruby_visit_t *visit, TSNode node, const char *owner, +/* A mention of a constant, whichever way it is then used. Each prefix of `A::B::C` is + * read on the way, so each is a mention of its own. */ +static void ruby_visit_constant(ruby_visit_t *visit, TSNode node, unsigned depth) { + if (cbm_source_is(node, "scope_resolution")) { + TSNode scope = cbm_source_field(node, "scope"); + if (!ts_node_is_null(scope)) { + ruby_visit(visit, scope, depth + 1); + /* `thing::Name` looks in a value this walk has no type for. */ + if (!cbm_source_is(scope, "constant") && !cbm_source_is(scope, "scope_resolution")) + return; + } + } + const cbm_source_decl_t *records = ruby_constant_records(visit, node); + const char *reason = NULL; + const char *qn = records ? cbm_source_unique_qn(records, &reason) : NULL; + if (!qn) + return; + ruby_extra_begin(visit, node, cbm_source_text(visit->base.walk, node)); + cbm_source_emit_node(visit->base.walk, node, CBM_SOURCE_SITE_REFERENCE, qn, + CBM_RESOLVE_RULE_LANGUAGE_BINDING, records->owner, "ruby_constant_lookup"); + ruby_extra_end(visit); +} + +static bool ruby_visit_constant_call(ruby_visit_t *visit, TSNode node, const char *owner, const char *method) { + if (!strcmp(method, "new")) + return ruby_visit_constructor(visit, node, owner); const cbm_source_decl_t *decl = NULL; ruby_lookup_t found = ruby_find_singleton(&visit->base, owner, method, &decl); - if (found == RUBY_ABSENT && !strcmp(method, "new")) { - /* Class#new allocates and calls initialize; initialize is how the - * declaration is reached. */ - found = ruby_find_instance_from(&visit->base, owner, "initialize", &decl); - } - ruby_emit_lookup(visit, node, found, decl, owner); + return ruby_emit_lookup(visit, node, found, decl, owner); } -static const char *ruby_local_class(ruby_visit_t *visit, const char *name, uint32_t at) { - ruby_local_t *local = NULL; - for (ruby_local_t *candidate = visit->locals; candidate; candidate = candidate->next) - if (!strcmp(candidate->name, name)) - local = candidate; - /* One assignment of `Const.new` is the only value the local can hold. */ - if (!local || local->blocked || local->writes != 1 || at < local->available_from) - return NULL; - TSNode value = local->value; +/* The class `Const.new` makes, when that is what its `new` does. */ +static const char *ruby_constructed_class(ruby_visit_t *visit, TSNode value) { if (!cbm_source_is(value, "call")) return NULL; const char *method = cbm_source_text(visit->base.walk, cbm_source_field(value, "method")); @@ -712,53 +1431,211 @@ static const char *ruby_local_class(ruby_visit_t *visit, const char *name, uint3 return NULL; /* A class can define its own `new`, which returns anything. */ const cbm_source_decl_t *custom = NULL; - return ruby_find_singleton(&visit->base, owner, "new", &custom) == RUBY_ABSENT ? owner : NULL; + ruby_lookup_t found = ruby_find_singleton(&visit->base, owner, "new", &custom); + return found == RUBY_FOUND || found == RUBY_AMBIGUOUS ? NULL : owner; } -static void ruby_visit_call(ruby_visit_t *visit, TSNode node) { - cbm_source_walk_t *walk = visit->base.walk; +static const char *ruby_local_class(ruby_visit_t *visit, const char *name, uint32_t at) { + ruby_local_t *local = NULL; + for (ruby_local_t *candidate = visit->locals; candidate; candidate = candidate->next) + if (!strcmp(candidate->name, name)) + local = candidate; + /* One assignment of `Const.new` is the only value the local can hold. */ + if (!local || local->blocked || local->writes != 1 || at < local->available_from) + return NULL; + return ruby_constructed_class(visit, local->value); +} + +/* The exact class of a receiver's value, when one can be read off it. */ +static const char *ruby_value_class(ruby_visit_t *visit, TSNode receiver, uint32_t at) { + if (cbm_source_is(receiver, "identifier")) { + const char *local = cbm_source_text(visit->base.walk, receiver); + return local ? ruby_local_class(visit, local, at) : NULL; + } + return ruby_constructed_class(visit, receiver); +} + +/* `self.class` from an instance. */ +static bool ruby_receiver_is_own_class(ruby_visit_t *visit, TSNode receiver) { + if (visit->self != RUBY_SELF_INSTANCE || !cbm_source_is(receiver, "call") || + !cbm_source_is(cbm_source_field(receiver, "receiver"), "self")) + return false; + const char *method = cbm_source_text(visit->base.walk, cbm_source_field(receiver, "method")); + return method && !strcmp(method, "class"); +} + +/* One method name on one receiver. True when a project declaration was selected. */ +static bool ruby_dispatch(ruby_visit_t *visit, TSNode node, TSNode receiver, const char *name) { + if (ts_node_is_null(receiver) || cbm_source_is(receiver, "self")) + return ruby_visit_implicit_self(visit, node, name); + const char *owner = ruby_constant_owner(visit, receiver); + if (owner) + return ruby_visit_constant_call(visit, node, owner, name); + if (cbm_source_is(receiver, "constant") || cbm_source_is(receiver, "scope_resolution")) { + ruby_emit(visit, node, NULL, CBM_RESOLVE_RULE_NONE, NULL, "ruby_constant_not_in_project"); + return false; + } + if (ruby_receiver_is_own_class(visit, receiver)) + return ruby_visit_constant_call(visit, node, ruby_current(&visit->base), name); + const char *cls = ruby_value_class(visit, receiver, ts_node_start_byte(node)); + if (cls) { + const cbm_source_decl_t *decl = NULL; + ruby_lookup_t found = ruby_find_instance_from(&visit->base, cls, name, &decl); + if (found == RUBY_FOUND) { + ruby_emit(visit, node, decl->qn, CBM_RESOLVE_RULE_TYPE_MEMBER, cls, + "ruby_receiver_class_from_new"); + visit->selected = decl; + return true; + } + return ruby_emit_lookup(visit, node, found, decl, cls); + } + ruby_emit(visit, node, NULL, CBM_RESOLVE_RULE_NONE, NULL, "ruby_receiver_class_unknown"); + return false; +} + +static bool ruby_visit_call(ruby_visit_t *visit, TSNode node) { TSNode method = cbm_source_field(node, "method"); - TSNode receiver = cbm_source_field(node, "receiver"); const char *name = cbm_source_is(method, "identifier") || cbm_source_is(method, "constant") || cbm_source_is(method, "operator") - ? cbm_source_text(walk, method) + ? cbm_source_text(visit->base.walk, method) : NULL; if (!name) { ruby_emit(visit, node, NULL, CBM_RESOLVE_RULE_NONE, NULL, "ruby_dynamic_method_name"); - return; + return false; } - if (ts_node_is_null(receiver) || cbm_source_is(receiver, "self")) { - ruby_visit_implicit_self(visit, node, name); - return; - } - const char *owner = ruby_constant_owner(visit, receiver); - if (owner) { - ruby_visit_constant_call(visit, node, owner, name); - return; - } - if (cbm_source_is(receiver, "constant") || cbm_source_is(receiver, "scope_resolution")) { - ruby_emit(visit, node, NULL, CBM_RESOLVE_RULE_NONE, NULL, "ruby_constant_not_in_project"); - return; - } - if (cbm_source_is(receiver, "identifier")) { - const char *local = cbm_source_text(walk, receiver); - const char *cls = local ? ruby_local_class(visit, local, ts_node_start_byte(node)) : NULL; - if (cls) { - const cbm_source_decl_t *decl = NULL; - ruby_lookup_t found = ruby_find_instance_from(&visit->base, cls, name, &decl); - if (found == RUBY_FOUND) { - ruby_emit(visit, node, decl->qn, CBM_RESOLVE_RULE_TYPE_MEMBER, cls, - "ruby_receiver_assigned_once_from_new"); - return; - } - ruby_emit_lookup(visit, node, found, decl, cls); - return; - } - } - ruby_emit(visit, node, NULL, CBM_RESOLVE_RULE_NONE, NULL, "ruby_receiver_class_unknown"); + TSNode receiver = cbm_source_field(node, "receiver"); + /* Extraction keeps no call for a bare `new(...)`, which is self.new. */ + bool bare_new = ts_node_is_null(receiver) && !strcmp(name, "new"); + if (bare_new) + ruby_extra_begin(visit, node, name); + bool selected = ruby_dispatch(visit, node, receiver, name); + if (bare_new) + ruby_extra_end(visit); + return selected; } -static void ruby_visit(ruby_visit_t *visit, TSNode node, unsigned depth); +/* `a[i]` calls [] and `a[i] = b` calls []=. Neither has a carrier. */ +static void ruby_visit_index_operator(ruby_visit_t *visit, TSNode node, const char *method) { + ruby_extra_begin(visit, node, method); + (void)ruby_dispatch(visit, node, cbm_source_field(node, "object"), method); + ruby_extra_end(visit); +} + +static void ruby_visit_index_writer(ruby_visit_t *visit, TSNode left, TSNode site) { + ruby_extra_begin(visit, site, "[]="); + (void)ruby_dispatch(visit, site, cbm_source_field(left, "object"), "[]="); + ruby_extra_end(visit); +} + +/* `a.b = c` calls b=. The carrier extraction records for `a.b` is that call, so the + * reader is not selected. `a.b ||= c` reads first, so the carrier is the reader and the + * writer is a second occurrence, on the method's name. */ +static void ruby_visit_attribute_target(ruby_visit_t *visit, TSNode left, bool reads, + unsigned depth) { + cbm_source_walk_t *walk = visit->base.walk; + TSNode receiver = cbm_source_field(left, "receiver"); + TSNode method = cbm_source_field(left, "method"); + const char *name = cbm_source_is(method, "identifier") ? cbm_source_text(walk, method) : NULL; + const char *writer = name ? cbm_arena_sprintf(walk->arena, "%s=", name) : NULL; + if (!writer) { + ruby_emit(visit, left, NULL, CBM_RESOLVE_RULE_NONE, NULL, "ruby_dynamic_method_name"); + } else if (reads) { + (void)ruby_dispatch(visit, left, receiver, name); + ruby_extra_begin(visit, method, writer); + (void)ruby_dispatch(visit, method, receiver, writer); + ruby_extra_end(visit); + } else { + (void)ruby_dispatch(visit, left, receiver, writer); + } + ruby_visit(visit, receiver, depth + 1); +} + +/* Follows forwarded blocks through the project's defs. A cycle adds nothing: the walk + * reads each name once, so its cost is the forwarding defs reachable from this name. */ +static bool ruby_name_may_rebind(const ruby_ctx_t *ctx, const char *name, + cbm_source_chain_t *chain) { + const cbm_source_decl_t *notes = cbm_source_lookup(ctx->walk, RUBY_REBINDING_DEFS, name); + if (!notes) + return false; + if (!cbm_source_chain_first_visit(chain, name)) + return chain->overflow; + for (const cbm_source_decl_t *note = notes; note; note = note->next) { + if (!note->type || !cbm_source_lookup(ctx->walk, RUBY_ALL_DEFS, note->type)) + return true; + if (ruby_name_may_rebind(ctx, note->type, chain)) + return true; + } + return false; +} + +static bool ruby_method_may_rebind(const ruby_visit_t *visit, const char *name) { + cbm_source_chain_t chain = {0}; + return ruby_name_may_rebind(&visit->base, name, &chain); +} + +/* The same question about one def: what it forwards to is what its own chain selects. + * Each def is read once, so the walk costs the defs reachable by forwarding. */ +static bool ruby_decl_may_rebind(const ruby_ctx_t *ctx, const cbm_source_decl_t *decl, + cbm_source_chain_t *chain) { + char own[RUBY_CONSTANT_BUFFER]; + int length = snprintf(own, sizeof(own), "%s%s", RUBY_REBINDING_OF, decl->owner); + if (length < 0 || (size_t)length >= sizeof(own)) + return true; + const cbm_source_decl_t *notes = cbm_source_lookup(ctx->walk, own, decl->name); + if (!notes) + return false; + if (!cbm_source_chain_first_visit(chain, decl->qn ? decl->qn : decl->name)) + return chain->overflow; + bool singleton = !strncmp(decl->name, "self.", 5); + for (const cbm_source_decl_t *note = notes; note; note = note->next) { + if (!note->type) + return true; + const cbm_source_decl_t *next = NULL; + ruby_lookup_t found = singleton + ? ruby_find_singleton(ctx, decl->owner, note->type, &next) + : ruby_find_instance_from(ctx, decl->owner, note->type, &next); + if (found != RUBY_FOUND || ruby_decl_may_rebind(ctx, next, chain)) + return true; + } + return false; +} + +/* What self is inside the block handed to this call. */ +static ruby_self_t ruby_block_self(ruby_visit_t *visit, TSNode call, const char *method, + const cbm_source_decl_t *selected) { + ruby_self_t self = visit->self; + if (self == RUBY_SELF_UNKNOWN || !method) + return RUBY_SELF_UNKNOWN; + cbm_source_walk_t *walk = visit->base.walk; + TSNode receiver = cbm_source_field(call, "receiver"); + bool own_receiver = ts_node_is_null(receiver) || cbm_source_is(receiver, "self"); + if (self == RUBY_SELF_CLASS && own_receiver && !strcmp(method, "define_method")) + return RUBY_SELF_INSTANCE; + if (self == RUBY_SELF_CLASS && own_receiver && !strcmp(method, "define_singleton_method")) + return RUBY_SELF_CLASS; + cbm_source_chain_t chain = {0}; + if (!strcmp(method, "new")) { + const cbm_source_decl_t *records = ruby_constant_records(visit, receiver); + if (records) + return selected && !ruby_decl_may_rebind(&visit->base, selected, &chain) + ? self + : RUBY_SELF_UNKNOWN; + const char *core = + cbm_source_is(receiver, "constant") ? cbm_source_text(walk, receiver) : NULL; + return core && ruby_core_class_keeps_block_self(core) ? self : RUBY_SELF_UNKNOWN; + } + if (ruby_rebinds_self(method)) + return RUBY_SELF_UNKNOWN; + /* A proc made here is handed over like a lambda literal is. */ + if ((self == RUBY_SELF_CLASS || self == RUBY_SELF_MAIN) && + (!strcmp(method, "lambda") || !strcmp(method, "proc"))) + return RUBY_SELF_UNKNOWN; + if (selected) + return ruby_decl_may_rebind(&visit->base, selected, &chain) ? RUBY_SELF_UNKNOWN : self; + return ruby_core_block_method(method) && !ruby_method_may_rebind(visit, method) + ? self + : RUBY_SELF_UNKNOWN; +} static void ruby_visit_children(ruby_visit_t *visit, TSNode node, unsigned depth) { TSTreeCursor cursor = ts_tree_cursor_new(node); @@ -773,6 +1650,13 @@ static void ruby_visit_children(ruby_visit_t *visit, TSNode node, unsigned depth } static void ruby_visit_def(ruby_visit_t *visit, TSNode node, ruby_self_t self, unsigned depth) { + const char *saved_def = visit->def_name; + bool saved_singleton = visit->def_singleton; + /* A def inside a block has no known owner, so nothing follows it either. */ + visit->def_name = self == RUBY_SELF_UNKNOWN + ? NULL + : cbm_source_text(visit->base.walk, cbm_source_field(node, "name")); + visit->def_singleton = self == RUBY_SELF_CLASS; ruby_self_t saved_self = visit->self; ruby_local_t *saved_locals = visit->locals; const char *saved_scope = visit->scope; @@ -785,8 +1669,10 @@ static void ruby_visit_def(ruby_visit_t *visit, TSNode node, ruby_self_t self, u visit->locals = NULL; TSNode parameters = cbm_source_field(node, "parameters"); TSNode body = cbm_source_field(node, "body"); - if (!ts_node_is_null(parameters)) + if (!ts_node_is_null(parameters)) { ruby_block_locals(visit, parameters, depth + 1); + ruby_visit(visit, parameters, depth + 1); + } if (!ts_node_is_null(body)) { ruby_audit_locals(visit, body, depth + 1); ruby_visit(visit, body, depth + 1); @@ -794,6 +1680,21 @@ static void ruby_visit_def(ruby_visit_t *visit, TSNode node, ruby_self_t self, u visit->self = saved_self; visit->locals = saved_locals; visit->scope = saved_scope; + visit->def_name = saved_def; + visit->def_singleton = saved_singleton; +} + +/* `super` selects by the def it stands in, whatever self is by then. */ +static void ruby_visit_super(ruby_visit_t *visit, TSNode node) { + if (!visit->def_name || !visit->base.depth) + return; + const char *owner = ruby_current(&visit->base); + const cbm_source_decl_t *decl = NULL; + ruby_lookup_t found = + ruby_find_super(&visit->base, owner, visit->def_name, visit->def_singleton, &decl); + ruby_extra_begin(visit, node, visit->def_name); + (void)ruby_emit_lookup(visit, node, found, decl, owner); + ruby_extra_end(visit); } static void ruby_visit(ruby_visit_t *visit, TSNode node, unsigned depth) { @@ -804,30 +1705,80 @@ static void ruby_visit(ruby_visit_t *visit, TSNode node, unsigned depth) { ruby_visit_bare_identifier(visit, node); return; } + if (cbm_source_is(node, "super")) { + ruby_visit_super(visit, node); + return; + } /* These hold names being bound or spelled, not values being read. */ const char *syntax = ts_node_type(node); - if (!strcmp(syntax, "block_parameters") || !strcmp(syntax, "lambda_parameters") || - !strcmp(syntax, "method_parameters") || !strcmp(syntax, "left_assignment_list") || - !strcmp(syntax, "exception_variable") || !strcmp(syntax, "alias") || - !strcmp(syntax, "undef") || strstr(syntax, "pattern") || + if (!strcmp(syntax, "left_assignment_list") || !strcmp(syntax, "exception_variable") || + !strcmp(syntax, "alias") || !strcmp(syntax, "undef") || strstr(syntax, "pattern") || (!strcmp(syntax, "unary") && cbm_source_token(node, "defined?"))) return; + /* A parameter's name is bound; its default value runs. */ + if (!strcmp(syntax, "block_parameters") || !strcmp(syntax, "lambda_parameters") || + !strcmp(syntax, "method_parameters")) { + 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, "optional_parameter") || + cbm_source_is(child, "keyword_parameter")) + ruby_visit(visit, cbm_source_field(child, "value"), depth + 1); + } while (ts_tree_cursor_goto_next_sibling(&cursor)); + } + ts_tree_cursor_delete(&cursor); + return; + } if (!strcmp(syntax, "assignment") || !strcmp(syntax, "operator_assignment")) { TSNode left = cbm_source_field(node, "left"); - if (!cbm_source_is(left, "identifier")) + if (cbm_source_is(left, "identifier")) { + const char *name = cbm_source_text(walk, left); + const ruby_local_t *found = NULL; + for (const ruby_local_t *local = visit->locals; name && local; local = local->next) + if (!strcmp(local->name, name)) + found = local; + ruby_emit_local(visit, left, found); + } else if (cbm_source_is(left, "call")) { + ruby_visit_attribute_target(visit, left, !strcmp(syntax, "operator_assignment"), depth); + } else if (cbm_source_is(left, "element_reference")) { + if (!strcmp(syntax, "operator_assignment")) { + /* It reads and then writes, and one span holds one site: the writer goes on + * the bracket, as an attribute's goes on its name. */ + ruby_visit_index_operator(visit, left, "[]"); + TSNode bracket = ts_node_next_sibling(cbm_source_field(left, "object")); + if (!ts_node_is_null(bracket)) + ruby_visit_index_writer(visit, left, bracket); + } else { + ruby_visit_index_writer(visit, left, left); + } + ruby_visit_children(visit, left, depth); + } else if (!cbm_source_is(left, "constant")) { ruby_visit(visit, left, depth + 1); + } ruby_visit(visit, cbm_source_field(node, "right"), depth + 1); return; } - if (!strcmp(syntax, "scope_resolution")) { - ruby_visit(visit, cbm_source_field(node, "scope"), depth + 1); + if (!strcmp(syntax, "constant") || !strcmp(syntax, "scope_resolution")) { + ruby_visit_constant(visit, node, depth); return; } + if (!strcmp(syntax, "element_reference")) + ruby_visit_index_operator(visit, node, "[]"); if (!strcmp(syntax, "for")) { ruby_visit(visit, cbm_source_field(node, "value"), depth + 1); ruby_visit(visit, cbm_source_field(node, "body"), depth + 1); return; } + /* A lambda in a class body or at the top of a file is handed to something. */ + if (!strcmp(syntax, "lambda") && + (visit->self == RUBY_SELF_CLASS || visit->self == RUBY_SELF_MAIN)) { + ruby_self_t saved = visit->self; + visit->self = RUBY_SELF_UNKNOWN; + ruby_visit_children(visit, node, depth); + visit->self = saved; + return; + } if (cbm_source_is(node, "class") || cbm_source_is(node, "module")) { const char *owner = ruby_declared_owner(&visit->base, cbm_source_field(node, "name")); TSNode body = cbm_source_field(node, "body"); @@ -835,11 +1786,19 @@ static void ruby_visit(ruby_visit_t *visit, TSNode node, unsigned depth) { return; ruby_self_t saved = visit->self; bool saved_singleton = visit->base.singleton; + ruby_local_t *saved_locals = visit->locals; + const char *saved_scope = visit->scope; visit->self = RUBY_SELF_CLASS; visit->base.singleton = false; + visit->scope = NULL; + /* A class body is a new scope for locals. */ + visit->locals = NULL; + ruby_audit_locals(visit, body, depth + 1); ruby_visit_children(visit, body, depth); visit->self = saved; visit->base.singleton = saved_singleton; + visit->locals = saved_locals; + visit->scope = saved_scope; visit->base.depth--; return; } @@ -873,7 +1832,9 @@ static void ruby_visit(ruby_visit_t *visit, TSNode node, unsigned depth) { return; } if (cbm_source_is(node, "call")) { - ruby_visit_call(visit, node); + visit->selected = NULL; + (void)ruby_visit_call(visit, node); + const cbm_source_decl_t *selected = visit->selected; TSNode block = cbm_source_field(node, "block"); const char *method = cbm_source_text(walk, cbm_source_field(node, "method")); ruby_self_t saved = visit->self; @@ -881,13 +1842,15 @@ static void ruby_visit(ruby_visit_t *visit, TSNode node, unsigned depth) { if (ts_tree_cursor_goto_first_child(&cursor)) { do { TSNode child = ts_tree_cursor_current_node(&cursor); - if (!ts_node_is_named(child) || - (ts_node_eq(child, cbm_source_field(node, "method")))) + if (!ts_node_is_named(child)) continue; - if (!ts_node_is_null(block) && ts_node_eq(child, block) && - (visit->self == RUBY_SELF_CLASS || visit->self == RUBY_SELF_MAIN || - (method && ruby_rebinds_self(method)))) - visit->self = RUBY_SELF_UNKNOWN; + if (ts_node_eq(child, cbm_source_field(node, "method"))) { + if (cbm_source_is(child, "super")) + ruby_visit_super(visit, child); + continue; + } + if (!ts_node_is_null(block) && ts_node_eq(child, block)) + visit->self = ruby_block_self(visit, node, method, selected); ruby_visit(visit, child, depth + 1); visit->self = saved; } while (ts_tree_cursor_goto_next_sibling(&cursor)); @@ -904,5 +1867,31 @@ static void ruby_resolve(cbm_source_walk_t *walk) { ruby_visit_children(&visit, walk->root, 0); } -const cbm_source_adapter_t cbm_source_adapter_ruby = { - .language = CBM_LANG_RUBY, .collect = ruby_collect, .resolve = ruby_resolve}; +/* `class A < B`: the constant in the header, read where the header is written. Only the + * superclass, which is what the graph's INHERITS has always meant for Ruby. */ +static const char *ruby_table_base(const cbm_source_table_t *table, const cbm_source_decl_t *decl, + size_t offset) { + if (offset || !(decl->flags & CBM_SOURCE_DECL_F_TYPE)) + return NULL; + cbm_source_walk_t walk = {.table = table, .language = CBM_LANG_RUBY}; + ruby_ctx_t ctx = {.walk = &walk}; + for (const cbm_source_decl_t *record = cbm_source_lookup(&walk, decl->owner, ""); record; + record = record->next) { + for (size_t i = 0; record->bases && record->bases[i]; i++) { + if (record->bases[i][0] != 'S') + continue; + const char *parent = ruby_base_owner(&ctx, record->bases[i]); + const char *reason = NULL; + return parent ? cbm_source_unique_qn(cbm_source_lookup(&walk, parent, ""), &reason) + : NULL; + } + } + return NULL; +} + +const cbm_source_adapter_t cbm_source_adapter_ruby = {.language = CBM_LANG_RUBY, + .collect = ruby_collect, + .resolve = ruby_resolve, + .base = ruby_table_base, + .decides_references = true, + .contains_parse_damage = true}; diff --git a/internal/cbm/resolve/adapter_scala.c b/internal/cbm/resolve/adapter_scala.c index 0e43db644..fbd32b6df 100644 --- a/internal/cbm/resolve/adapter_scala.c +++ b/internal/cbm/resolve/adapter_scala.c @@ -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 #include #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}; diff --git a/internal/cbm/resolve/adapter_zig.c b/internal/cbm/resolve/adapter_zig.c index 189f1f4dc..cac78171a 100644 --- a/internal/cbm/resolve/adapter_zig.c +++ b/internal/cbm/resolve/adapter_zig.c @@ -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}; diff --git a/internal/cbm/resolve/capabilities.c b/internal/cbm/resolve/capabilities.c index 51bfb1fea..64dd8021e 100644 --- a/internal/cbm/resolve/capabilities.c +++ b/internal/cbm/resolve/capabilities.c @@ -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"; diff --git a/internal/cbm/resolve/source_adapter.c b/internal/cbm/resolve/source_adapter.c index a1847825e..fc03affa3 100644 --- a/internal/cbm/resolve/source_adapter.c +++ b/internal/cbm/resolve/source_adapter.c @@ -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; diff --git a/internal/cbm/resolve/source_adapter.h b/internal/cbm/resolve/source_adapter.h index 9f96fb48c..12e119c9f 100644 --- a/internal/cbm/resolve/source_adapter.h +++ b/internal/cbm/resolve/source_adapter.h @@ -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); diff --git a/internal/cbm/resolve/source_adapter_internal.h b/internal/cbm/resolve/source_adapter_internal.h index 2fbf6dbac..422243924 100644 --- a/internal/cbm/resolve/source_adapter_internal.h +++ b/internal/cbm/resolve/source_adapter_internal.h @@ -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. */ diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index fa1cb9565..160536af1 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -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; } diff --git a/src/pipeline/pass_pkgmap.c b/src/pipeline/pass_pkgmap.c index 55bd143a5..d888ae456 100644 --- a/src/pipeline/pass_pkgmap.c +++ b/src/pipeline/pass_pkgmap.c @@ -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; } diff --git a/src/pipeline/pass_resolver.c b/src/pipeline/pass_resolver.c index 7fece6085..5680b3921 100644 --- a/src/pipeline/pass_resolver.c +++ b/src/pipeline/pass_resolver.c @@ -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. */ diff --git a/src/pipeline/pass_semantic.c b/src/pipeline/pass_semantic.c index 23e1ed9c7..e71c39b69 100644 --- a/src/pipeline/pass_semantic.c +++ b/src/pipeline/pass_semantic.c @@ -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, diff --git a/src/pipeline/shared_resolver.h b/src/pipeline/shared_resolver.h index ce7b4d6f5..c7d6300c9 100644 --- a/src/pipeline/shared_resolver.h +++ b/src/pipeline/shared_resolver.h @@ -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, diff --git a/src/store/store.h b/src/store/store.h index eed6f746d..adf27ded2 100644 --- a/src/store/store.h +++ b/src/store/store.h @@ -34,8 +34,11 @@ typedef struct cbm_store_read_scope cbm_store_read_scope_t; * __image__, registry "unknown" -- no longer * __image__docker.io/[library/]. * 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) diff --git a/tests/test_resolver_pipeline.c b/tests/test_resolver_pipeline.c index 40d90de49..570a84b15 100644 --- a/tests/test_resolver_pipeline.c +++ b/tests/test_resolver_pipeline.c @@ -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 /(?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", diff --git a/tests/test_resolver_source_langs.c b/tests/test_resolver_source_langs.c index 21338f82a..3e5bd1b1b 100644 --- a/tests/test_resolver_source_langs.c +++ b/tests/test_resolver_source_langs.c @@ -91,10 +91,19 @@ static bool sl_check(const char *project, const char *filler_name, const char *f } cbm_store_t *store = ok ? cbm_store_open_path_existing(database) : NULL; RQ_Report report = {0}; - ok = store && - rq_check(store, project, project, sources, source_count, expected, expected_count, + /* A build manifest has no node; the oracle's sources are the ones that do. */ + RQ_Source *indexed = calloc(source_count, sizeof(*indexed)); + size_t indexed_count = 0; + for (size_t i = 0; indexed && i < source_count; i++) { + const char *slash = strrchr(sources[i].path, '/'); + if (strcmp(slash ? slash + 1 : sources[i].path, "dune")) + indexed[indexed_count++] = sources[i]; + } + ok = store && indexed && + rq_check(store, project, project, indexed, indexed_count, expected, expected_count, forbidden, forbidden_count, &report, stderr) && ok; + free(indexed); if (store) cbm_store_close(store); th_cleanup(root); @@ -377,7 +386,7 @@ static const char ZIG_SHARED[] = "const A = struct {\n" #define ZIG_CALL(id, method, text, tfile, tsuffix, tline, tdecl) \ SL_CALL_AT(id, ZIG_FILE, "main." method, text, 0, tfile, tsuffix, "Function", tline, tdecl) #define ZIG_GO(id, method, text) \ - ZIG_CALL(id, method, text, "src/sub/other.zig", "other.go", 6, ZIG_THING_GO) + ZIG_CALL(id, method, text, "src/sub/other.zig", "other.Thing.go", 6, ZIG_THING_GO) #define ZIG_DENY(id, method, text) SL_DENY_AT(id, ZIG_FILE, "main." method, text, 0) TEST(resolver_source_zig_containers_imports_and_declared_receivers) { @@ -400,31 +409,33 @@ TEST(resolver_source_zig_containers_imports_and_declared_receivers) { ZIG_GO("declared-field-type", "callsField", "self.field.go()"), ZIG_GO("declared-return-type", "callsReturn", "Thing.init().go()"), ZIG_CALL("container-function-through-alias", "callsReturn", "Thing.init()", - "src/sub/other.zig", "other.init", 4, " pub fn init() Thing { return .{}; }"), + "src/sub/other.zig", "other.Thing.init", 4, + " pub fn init() Thing { return .{}; }"), ZIG_GO("declared-local", "callsTyped", "v.go()"), ZIG_GO("struct-literal-local", "callsLiteral", "w.go()"), ZIG_GO("error-union-return-through-try", "callsTry", "x.go()"), ZIG_CALL("container-function-with-error-union", "callsTry", "Thing.make()", - "src/sub/other.zig", "other.make", 5, " pub fn make() !Thing { return .{}; }"), - SL_CALL_AT("nested-container-sees-outer", ZIG_FILE, "main.callsOuter", "local()", 2, + "src/sub/other.zig", "other.Thing.make", 5, + " pub fn make() !Thing { return .{}; }"), + SL_CALL_AT("nested-container-sees-outer", ZIG_FILE, "Nested.callsOuter", "local()", 2, ZIG_FILE, "main.local", "Function", 10, "fn local() void {}"), SL_USE_AT("name-read-from-the-enclosing-container", ZIG_FILE, "main.usesConst", "LIMIT", 1, ZIG_FILE, "main.LIMIT", "Variable", 35, "const LIMIT = 3;"), + SL_CALL_AT("a-function-is-its-containers-own", "src/shared.zig", "A.callsSame", "same()", 1, + "src/shared.zig", "shared.A.same", "Function", 2, " pub fn same() void {}"), ZIG_CALL("comptime-function-itself-is-a-call", "noGeneric", "Box(u8)", ZIG_FILE, "main.Box", 34, "fn Box(comptime T: type) type { return struct { pub fn make() void {} }; }"), }; static const RQ_Forbidden forbidden[] = { ZIG_DENY("build-mapped-import", "noBuild", "zls.helper()"), ZIG_DENY("standard-library", "noStd", "std.debug.print(\"x\", .{})"), - ZIG_DENY("capture-type-is-not-modelled", "noCapture", "p.decoyOnly()"), + ZIG_DENY("capture-has-no-member-of-that-name", "noCapture", "p.decoyOnly()"), ZIG_DENY("anytype-has-no-container", "noAnytype", "a.go()"), ZIG_DENY("non-pub-from-another-file", "noPrivate", "other.hidden()"), ZIG_DENY("absent-from-named-file", "noAbsent", "other.absent()"), ZIG_DENY("name-in-unimported-file", "noUndeclared", "unknown_name()"), ZIG_DENY("comptime-generic-result", "noGeneric", "Box(u8).make()"), SL_NO_USE_AT("member-name-is-not-a-read", ZIG_FILE, "main.noMemberName", "LIMIT", 2), - SL_DENY_AT("graph-node-shared-by-two-structs", "src/shared.zig", "shared.callsSame", - "same()", 1), }; ASSERT(sl_check("zig-rules", ".zig", "pub fn f() void {}\n", sources, sizeof(sources) / sizeof(sources[0]), expected, @@ -433,6 +444,186 @@ TEST(resolver_source_zig_containers_imports_and_declared_receivers) { PASS(); } +static const char ZIG_FLOW[] = + "const other = @import(\"sub/other.zig\");\n" + "const Thing = other.Thing;\n" + "fn maybe() ?*Thing { return null; }\n" + "fn fails() !Thing { return .{}; }\n" + "fn list() ?[]Thing { return null; }\n" + "pub fn viaOrelse() void { const a = maybe() orelse return; a.go(); }\n" + "pub fn viaCatch() void { const b = fails() catch return; b.go(); }\n" + "pub fn viaDeref(p: *Thing) void { const c = p.*; c.go(); }\n" + "pub fn viaIf() void { if (maybe()) |d| { d.go(); } }\n" + "pub fn viaWhile() void { while (maybe()) |e| { e.go(); } }\n" + "pub fn noElse() void { if (fails()) |_| {} else |err| { err.go(); } }\n" + "pub fn noCatchCapture() void { const q = fails() catch |bad| bad.go(); _ = q; }\n" + "pub fn noSlice() void { if (list()) |s| { s.go(); } }\n" + "pub fn noFor(items: []Thing) void { for (items) |i| { i.go(); } }\n" + "pub fn noSwitch(u: anytype) void { switch (u) { .x => |v| v.go(), } }\n" + "pub fn readsPath() u8 { return other.LIMIT; }\n" + "pub fn readsType(x: other.Thing) void { _ = x; }\n" + "pub fn readsFunction() void { const f = other.helper; _ = f; }\n" + "pub fn noPrivateRead() u8 { return other.SECRET; }\n" + "pub fn noFieldRead(t: Thing) u8 { return t.size; }\n" + "pub fn noAnytypeRead(t: anytype) u8 { return t.LIMIT; }\n" + "pub fn callIsNotARead() void { other.helper(); }\n" + "pub fn readsBehindAPointer(x: *other.Thing, y: ?other.Thing) void { _ = x; _ = y; }\n"; +static const char ZIG_FLOW_OTHER[] = "pub fn helper() void {}\n" + "pub const LIMIT = 3;\n" + "const SECRET = 4;\n" + "pub const Thing = struct {\n" + " size: u8,\n" + " pub fn go(self: Thing) void { _ = self; }\n" + "};\n"; +static const char ZIG_FLOW_DECOY[] = "pub const size = 1;\n" + "pub const SECRET = 2;\n"; +#define ZIG_FLOW_FILE "src/flow.zig" +#define ZIG_FLOW_GO(id, method, text) \ + SL_CALL_AT(id, ZIG_FLOW_FILE, "flow." method, text, 0, "src/sub/other.zig", \ + "other.Thing.go", "Function", 6, ZIG_THING_GO) +#define ZIG_FLOW_DENY(id, method, text) SL_DENY_AT(id, ZIG_FLOW_FILE, "flow." method, text, 0) + +#define ZIG_FLOW_OTHER_READ(id, method, at) \ + SL_USE_AT(id, ZIG_FLOW_FILE, "flow." method, "other", at, ZIG_FLOW_FILE, "flow.other", \ + "Variable", 1, "const other = @import(\"sub/other.zig\");") +#define ZIG_FLOW_FAILS(id, method, at) \ + SL_CALL_AT(id, ZIG_FLOW_FILE, "flow." method, "fails()", at, ZIG_FLOW_FILE, "flow.fails", \ + "Function", 4, "fn fails() !Thing { return .{}; }") +TEST(resolver_source_zig_unwraps_captures_and_member_reads) { + static const RQ_Source sources[] = { + {.path = ZIG_FLOW_FILE, .text = ZIG_FLOW}, + {.path = "src/sub/other.zig", .text = ZIG_FLOW_OTHER}, + {.path = "src/decoy.zig", .text = ZIG_FLOW_DECOY}, + }; + static const RQ_Expected expected[] = { + ZIG_FLOW_GO("orelse-keeps-the-optional-payload", "viaOrelse", "a.go()"), + SL_CALL_AT("orelse-operand-is-still-a-call", ZIG_FLOW_FILE, "flow.viaOrelse", "maybe()", 1, + ZIG_FLOW_FILE, "flow.maybe", "Function", 3, + "fn maybe() ?*Thing { return null; }"), + ZIG_FLOW_GO("catch-keeps-the-error-union-payload", "viaCatch", "b.go()"), + SL_CALL_AT("catch-operand-is-still-a-call", ZIG_FLOW_FILE, "flow.viaCatch", "fails()", 1, + ZIG_FLOW_FILE, "flow.fails", "Function", 4, "fn fails() !Thing { return .{}; }"), + SL_CALL_AT("if-condition-is-still-a-call", ZIG_FLOW_FILE, "flow.viaIf", "maybe()", 2, + ZIG_FLOW_FILE, "flow.maybe", "Function", 3, + "fn maybe() ?*Thing { return null; }"), + SL_CALL_AT("while-condition-is-still-a-call", ZIG_FLOW_FILE, "flow.viaWhile", "maybe()", 3, + ZIG_FLOW_FILE, "flow.maybe", "Function", 3, + "fn maybe() ?*Thing { return null; }"), + ZIG_FLOW_GO("dereference-keeps-the-pointee", "viaDeref", "c.go()"), + ZIG_FLOW_GO("if-capture-unwraps-the-condition", "viaIf", "d.go()"), + ZIG_FLOW_GO("while-capture-unwraps-the-condition", "viaWhile", "e.go()"), + SL_USE_AT("value-through-an-import", ZIG_FLOW_FILE, "flow.readsPath", "LIMIT", 0, + "src/sub/other.zig", "other.LIMIT", "Variable", 2, "pub const LIMIT = 3;"), + ZIG_FLOW_OTHER_READ("the-import-is-read-on-the-way", "readsPath", 3), + ZIG_FLOW_OTHER_READ("the-import-is-read-for-a-private-name", "noPrivateRead", 6), + ZIG_FLOW_OTHER_READ("the-import-is-read-for-a-call", "callIsNotARead", 7), + ZIG_FLOW_FAILS("condition-beside-an-else-capture", "noElse", 2), + ZIG_FLOW_FAILS("operand-beside-a-catch-capture", "noCatchCapture", 3), + SL_CALL_AT("condition-of-a-slice-capture", ZIG_FLOW_FILE, "flow.noSlice", "list()", 1, + ZIG_FLOW_FILE, "flow.list", "Function", 5, + "fn list() ?[]Thing { return null; }"), + SL_USE_AT("type-path-behind-a-pointer", ZIG_FLOW_FILE, "flow.readsBehindAPointer", "Thing", + 9, "src/sub/other.zig", "other.Thing", "Variable", 4, + "pub const Thing = struct {"), + SL_USE_AT("type-path-behind-an-optional", ZIG_FLOW_FILE, "flow.readsBehindAPointer", + "Thing", 10, "src/sub/other.zig", "other.Thing", "Variable", 4, + "pub const Thing = struct {"), + ZIG_FLOW_OTHER_READ("the-import-is-read-behind-a-pointer", "readsBehindAPointer", 8), + ZIG_FLOW_OTHER_READ("the-import-is-read-behind-an-optional", "readsBehindAPointer", 9), + SL_USE_AT("field-read-has-a-type-read", ZIG_FLOW_FILE, "flow.noFieldRead", "Thing", 8, + ZIG_FLOW_FILE, "flow.Thing", "Variable", 2, "const Thing = other.Thing;"), + }; + static const RQ_Forbidden forbidden[] = { + ZIG_FLOW_DENY("else-capture-is-the-error", "noElse", "err.go()"), + ZIG_FLOW_DENY("catch-capture-is-the-error", "noCatchCapture", "bad.go()"), + ZIG_FLOW_DENY("slice-is-not-its-element", "noSlice", "s.go()"), + ZIG_FLOW_DENY("for-element-is-not-modelled", "noFor", "i.go()"), + ZIG_FLOW_DENY("switch-capture-is-not-modelled", "noSwitch", "v.go()"), + SL_NO_USE_AT("private-through-an-import", ZIG_FLOW_FILE, "flow.noPrivateRead", "SECRET", 0), + SL_NO_USE_AT("field-has-no-node", ZIG_FLOW_FILE, "flow.noFieldRead", "size", 0), + SL_NO_USE_AT("anytype-has-no-container", ZIG_FLOW_FILE, "flow.noAnytypeRead", "LIMIT", 1), + SL_NO_USE_AT("called-member-is-not-also-a-read", ZIG_FLOW_FILE, "flow.callIsNotARead", + "helper", 1), + }; + ASSERT(sl_check("zig-flow", ".zig", "pub fn f() void {}\n", sources, + sizeof(sources) / sizeof(sources[0]), expected, + sizeof(expected) / sizeof(expected[0]), forbidden, + sizeof(forbidden) / sizeof(forbidden[0]))); + PASS(); +} + +/* A type written in a declaration is read in the file of the declaration. */ +static const char ZIG_SERVER[] = "const Server = @This();\n" + "const DocumentStore = @import(\"DocumentStore.zig\");\n" + "const Analyser = @import(\"analysis.zig\");\n" + "\n" + "document_store: DocumentStore,\n" + "\n" + "pub fn initAnalyser(server: *Server) Analyser {\n" + " _ = server;\n" + " return .{};\n" + "}\n"; +static const char ZIG_STORE[] = "const DocumentStore = @This();\n" + "pub fn getHandle(self: *DocumentStore) void {\n" + " _ = self;\n" + "}\n" + "fn hidden(self: *DocumentStore) void {\n" + " _ = self;\n" + "}\n"; +static const char ZIG_ANALYSIS[] = "const Analyser = @This();\n" + "pub fn getSymbol(analyser: *Analyser) void {\n" + " _ = analyser;\n" + "}\n"; +static const char ZIG_REFERENCES[] = + "const Server = @import(\"../Server.zig\");\n" + "pub fn fieldTypedInItsOwnFile(server: *Server) void {\n" + " server.document_store.getHandle();\n" + "}\n" + "pub fn returnTypedInItsOwnFile(server: *Server) void {\n" + " var analyser = server.initAnalyser();\n" + " analyser.getSymbol();\n" + "}\n" + "pub fn aMethodStillNeedsPub(server: *Server) void {\n" + " server.document_store.hidden();\n" + "}\n" + "pub fn aConstStillNeedsPubHere(store: *Server.DocumentStore) void {\n" + " store.getHandle();\n" + "}\n"; + +TEST(resolver_source_zig_a_type_is_read_in_the_file_that_writes_it) { + static const RQ_Source sources[] = { + {.path = "src/Server.zig", .text = ZIG_SERVER}, + {.path = "src/DocumentStore.zig", .text = ZIG_STORE}, + {.path = "src/analysis.zig", .text = ZIG_ANALYSIS}, + {.path = "src/features/references.zig", .text = ZIG_REFERENCES}, + }; + static const RQ_Expected expected[] = { + SL_CALL_AT("a-field-type-written-in-its-own-file", "src/features/references.zig", + "references.fieldTypedInItsOwnFile", "server.document_store.getHandle()", 0, + "src/DocumentStore.zig", "DocumentStore.getHandle", "Function", 2, + "pub fn getHandle(self: *DocumentStore) void {"), + SL_CALL_AT("a-method-reached-through-a-parameter", "src/features/references.zig", + "references.returnTypedInItsOwnFile", "server.initAnalyser()", 0, + "src/Server.zig", "Server.initAnalyser", "Function", 7, + "pub fn initAnalyser(server: *Server) Analyser {"), + SL_CALL_AT("a-return-type-written-in-its-own-file", "src/features/references.zig", + "references.returnTypedInItsOwnFile", "analyser.getSymbol()", 0, + "src/analysis.zig", "analysis.getSymbol", "Function", 2, + "pub fn getSymbol(analyser: *Analyser) void {"), + }; + static const RQ_Forbidden forbidden[] = { + SL_DENY_AT("a-method-from-another-file-needs-pub", "src/features/references.zig", + "references.aMethodStillNeedsPub", "server.document_store.hidden()", 0), + SL_DENY_AT("a-const-from-another-file-needs-pub", "src/features/references.zig", + "references.aConstStillNeedsPubHere", "store.getHandle()", 0), + }; + ASSERT(sl_check("zig-written-file", ".zig", "pub fn f() void {}\n", sources, + sizeof(sources) / sizeof(sources[0]), expected, + sizeof(expected) / sizeof(expected[0]), forbidden, + sizeof(forbidden) / sizeof(forbidden[0]))); + PASS(); +} + /* ── Perl ──────────────────────────────────────────────────────────── */ static const char PL_BASE[] = "package Foo::Base;\n" @@ -682,7 +873,13 @@ static const char RB_BARE[] = "class Bare\n" #define RB_CALL(id, caller_name, text, tsuffix, tline, tdecl) \ SL_CALL_AT(id, RB_FILE, caller_name, text, 0, RB_FILE, tsuffix, "Method", tline, tdecl) #define RB_OWN " def own" +#define RB_CLASS(id, caller_name, text, at) \ + SL_CALL_AT(id, RB_FILE, caller_name, text, at, RB_FILE, "Sinatra.Other", "Class", 89, \ + " class Other") +#define RB_MENTION(id, caller_name, at) \ + SL_USE_AT(id, RB_FILE, caller_name, "Other", at, RB_FILE, "Sinatra.Other", "Class", 89, \ + " class Other") TEST(resolver_source_ruby_ancestor_chain_and_self_rules) { static const RQ_Source sources[] = {{.path = RB_FILE, .text = RB_BASE}, {.path = RB_BARE_FILE, .text = RB_BARE}}; @@ -699,6 +896,12 @@ TEST(resolver_source_ruby_ancestor_chain_and_self_rules) { 21, " def self.settings"), RB_CALL("new-selects-initialize", "Base.call_new", "Sinatra::Base.new(1)", "Base.initialize", 25, " def initialize(app)"), + /* Other declares no initialize, so `new` constructs the class itself. */ + RB_CLASS("new-with-no-initialize-is-the-class", "Base.call_local", "Other.new", 0), + RB_CLASS("new-of-a-reassigned-local-is-still-the-class", "Base.no_reassigned", "Other.new", + 1), + RB_MENTION("constant-of-a-new-is-a-mention", "Base.call_local", 0), + RB_MENTION("constant-of-a-new-of-a-reassigned-local", "Base.no_reassigned", 1), RB_CALL("local-assigned-once-from-new", "Base.call_local", "made.other_m()", "Other.other_m", 90, " def other_m"), SL_CALL_AT("block-keeps-self-in-a-method", RB_FILE, "Base.call_in_block", "own()", 2, @@ -729,6 +932,2079 @@ TEST(resolver_source_ruby_ancestor_chain_and_self_rules) { PASS(); } +static const char RB2_MAIN[] = + "module App\n" + " module PathUtil\n" + " def before_function\n" + " 0\n" + " end\n" + "\n" + " module_function\n" + "\n" + " def smart_path(path)\n" + " path\n" + " end\n" + " end\n" + "\n" + " module Explicit\n" + " def listed\n" + " 1\n" + " end\n" + " module_function :listed\n" + " end\n" + "\n" + " class Corrector < ::Parser::Source::TreeRewriter\n" + " def initialize(source)\n" + " @source = source\n" + " end\n" + "\n" + " def fix\n" + " 2\n" + " end\n" + " end\n" + "\n" + " class NoInit\n" + " def work\n" + " 3\n" + " end\n" + " end\n" + "\n" + " class Factory\n" + " def self.new\n" + " Object.new\n" + " end\n" + "\n" + " def work\n" + " 4\n" + " end\n" + " end\n" + "\n" + " class Parent\n" + " PARENT_LIMIT = 9\n" + "\n" + " class Inner\n" + " def inner_m\n" + " 5\n" + " end\n" + " end\n" + " end\n" + "\n" + " class Cop < Parent\n" + " MSG = 'x'\n" + " DEFAULT = NoInit\n" + "\n" + " class << self\n" + " attr_accessor :debug\n" + "\n" + " def registry\n" + " 6\n" + " end\n" + "\n" + " def registry=(value)\n" + " @registry = value\n" + " end\n" + " end\n" + "\n" + " attr_accessor :name\n" + "\n" + " def name\n" + " @name\n" + " end\n" + "\n" + " def resource=(value)\n" + " @resource = value\n" + " end\n" + "\n" + " def resource\n" + " @resource\n" + " end\n" + "\n" + " def reader_only\n" + " 7\n" + " end\n" + "\n" + " attr_accessor :accessor_only\n" + "\n" + " private def hidden\n" + " 8\n" + " end\n" + "\n" + " def old_m\n" + " 9\n" + " end\n" + " alias_method :new_m, :old_m\n" + " alias also_m old_m\n" + "\n" + " alias_method :orig_wrapped, :wrapped\n" + " def wrapped\n" + " orig_wrapped()\n" + " end\n" + "\n" + " def helper_obj\n" + " 10\n" + " end\n" + "\n" + " def calls_module_function\n" + " PathUtil.smart_path(1)\n" + " end\n" + "\n" + " def no_def_before_module_function\n" + " PathUtil.before_function\n" + " end\n" + "\n" + " def calls_listed_function\n" + " Explicit.listed\n" + " end\n" + "\n" + " def calls_private_def\n" + " hidden()\n" + " end\n" + "\n" + " def calls_alias_method\n" + " new_m()\n" + " end\n" + "\n" + " def calls_alias_keyword\n" + " also_m()\n" + " end\n" + "\n" + " def calls_later_def_over_attr\n" + " name()\n" + " end\n" + "\n" + " def calls_new_outside_parent\n" + " Corrector.new(1)\n" + " end\n" + "\n" + " def calls_new_chain\n" + " Corrector.new(2).fix\n" + " end\n" + "\n" + " def calls_new_no_initialize\n" + " NoInit.new\n" + " end\n" + "\n" + " def calls_new_through_constant_alias\n" + " DEFAULT.new\n" + " end\n" + "\n" + " def calls_writer\n" + " self.resource = 1\n" + " end\n" + "\n" + " def calls_reader_and_writer\n" + " self.resource ||= 1\n" + " end\n" + "\n" + " def calls_class_chain\n" + " self.class.registry\n" + " end\n" + "\n" + " def calls_constant_writer\n" + " Cop.registry = 1\n" + " end\n" + "\n" + " def receiver_of_attribute_write(value)\n" + " helper_obj.thing = value\n" + " end\n" + "\n" + " def no_writer_is_not_its_reader\n" + " self.reader_only = 1\n" + " end\n" + "\n" + " def no_accessor_node\n" + " self.accessor_only = 1\n" + " end\n" + "\n" + " def no_custom_new\n" + " Factory.new.work\n" + " end\n" + "\n" + " def no_value_constant\n" + " MSG.size\n" + " end\n" + "\n" + " def inherited_constant\n" + " Inner.new.inner_m\n" + " end\n" + " end\n" + "end\n" + "\n" + "class App::Compact\n" + " def lexical_scope_skips_app\n" + " Lexical.pick\n" + " end\n" + "end\n" + "\n" + "class Lexical\n" + " def self.pick\n" + " 11\n" + " end\n" + "end\n" + "\n" + "module App\n" + " class Lexical\n" + " def self.pick\n" + " 12\n" + " end\n" + " end\n" + "end\n"; + +static const char RB3_BLOCKS[] = + "module App\n" + " class Worker\n" + " def add_offense(node, &block)\n" + " correct(node, &block)\n" + " end\n" + "\n" + " def correct(node)\n" + " yield node\n" + " end\n" + "\n" + " def configure(&block)\n" + " instance_eval(&block)\n" + " end\n" + "\n" + " def relay(&block)\n" + " configure(&block)\n" + " end\n" + "\n" + " def keep(&block)\n" + " @kept = block\n" + " end\n" + "\n" + " def splice(&block)\n" + " lots.map(&block)\n" + " end\n" + "\n" + " def helper\n" + " 1\n" + " end\n" + "\n" + " def self.class_helper\n" + " 2\n" + " end\n" + "\n" + " def in_core_iterator(items)\n" + " items.each { |item| helper() }\n" + " end\n" + "\n" + " def in_core_chain(items)\n" + " items.map { |item| item }.select { |item| helper() }\n" + " end\n" + "\n" + " def in_project_method\n" + " add_offense(1) { |x| helper() }\n" + " end\n" + "\n" + " def in_forward_to_core\n" + " splice { |x| helper() }\n" + " end\n" + "\n" + " def no_instance_eval\n" + " configure { helper() }\n" + " end\n" + "\n" + " def no_forward_to_instance_eval\n" + " relay { helper() }\n" + " end\n" + "\n" + " def no_kept_block\n" + " keep { helper() }\n" + " end\n" + "\n" + " def no_unknown_method(thing)\n" + " thing.frobnicate { helper() }\n" + " end\n" + "\n" + " %w[a].each { |name| class_helper() }\n" + "\n" + " define_method(:made) { helper() }\n" + "\n" + " scope :recent, -> { class_helper() }\n" + "\n" + " get '/' do\n" + " class_helper()\n" + " end\n" + " end\n" + "end\n"; + +static const char RB4_OWN_EACH[] = + "class Dsl\n" + " def each(&block)\n" + " instance_eval(&block)\n" + " end\n" + "end\n" + "\n" + "class Uses\n" + " def helper\n" + " 1\n" + " end\n" + "\n" + " def no_project_each(items)\n" + " items.each { |item| helper() }\n" + " end\n" + "\n" + " def map_is_still_core(items)\n" + " items.map { |item| helper() }\n" + " end\n" + "end\n"; + +static const char RB5_REFS[] = + "require_relative 'other'\n" + "\n" + "limit = 1\n" + "puts limit\n" + "\n" + "module App\n" + " class Thing\n" + " KIND = 'k'\n" + "\n" + " def mentions_class\n" + " Solo\n" + " end\n" + "\n" + " def mentions_path\n" + " Inner::Deep\n" + " end\n" + "\n" + " def mentions_value\n" + " KIND\n" + " end\n" + "\n" + " def no_local\n" + " limit = 2\n" + " limit\n" + " end\n" + "\n" + " def no_reopened_namespace\n" + " Spread\n" + " end\n" + "\n" + " def no_outside\n" + " Gemfile::Thing\n" + " end\n" + "\n" + " def mentions_in_rescue\n" + " 1\n" + " rescue Solo\n" + " 2\n" + " end\n" + " end\n" + "\n" + " class Solo\n" + " end\n" + "\n" + " module Inner\n" + " class Deep\n" + " end\n" + " end\n" + "\n" + " class Spread\n" + " end\n" + "end\n"; + +static const char RB5_OTHER[] = + "module App\n" + " class Spread\n" + " end\n" + "end\n"; + +static const char RB6_INDEX[] = + "class Store\n" + " def initialize(a = 1)\n" + " @a = a\n" + " end\n" + "\n" + " def [](key)\n" + " key\n" + " end\n" + "\n" + " def []=(key, value)\n" + " value\n" + " end\n" + "\n" + " def self.build(a)\n" + " new(a)\n" + " end\n" + "\n" + " def self.memo(a)\n" + " @memo ||= new(a)\n" + " end\n" + "\n" + " def no_instance_new(a)\n" + " new(a)\n" + " end\n" + "\n" + " def reads_self(key)\n" + " self[key]\n" + " end\n" + "\n" + " def writes_self(key)\n" + " self[key] = 1\n" + " end\n" + "\n" + " def both_self(key)\n" + " self[key] ||= 1\n" + " end\n" + "\n" + " def reads_typed\n" + " other = Store.new\n" + " other[1]\n" + " end\n" + "\n" + " def no_unknown_receiver(thing)\n" + " thing[1]\n" + " end\n" + "\n" + " def no_array_literal\n" + " [1][0]\n" + " end\n" + "end\n" + "\n" + "class Plain\n" + " def no_index_on_a_class_without_it\n" + " self[1]\n" + " end\n" + "end\n"; + +static const char RB7_CORRECTOR[] = "module Helper\n" + " def from_helper\n" + " 1\n" + " end\n" + "end\n" + "\n" + "class Corrector\n" + " class << self\n" + " include Helper\n" + "\n" + " def correct\n" + " from_helper()\n" + " end\n" + " end\n" + "\n" + " def no_instance_reach\n" + " from_helper()\n" + " end\n" + "end\n"; + +static const char RB8_SUPER[] = "module Mixed\n" + " def through_mixin\n" + " 1\n" + " end\n" + "end\n" + "\n" + "class Parent\n" + " def go(a)\n" + " a\n" + " end\n" + "\n" + " def self.make\n" + " 1\n" + " end\n" + "\n" + " def through_mixin\n" + " 2\n" + " end\n" + "end\n" + "\n" + "class Child < Parent\n" + " include Mixed\n" + "\n" + " def go(a)\n" + " super\n" + " end\n" + "\n" + " def through_mixin\n" + " super()\n" + " end\n" + "\n" + " def self.make\n" + " super\n" + " end\n" + "\n" + " def no_parent_method\n" + " super\n" + " end\n" + "end\n" + "\n" + "class Outsider < Gem::Thing\n" + " def no_chain_out\n" + " super\n" + " end\n" + "end\n" + "\n" + "module Mod\n" + " def no_module_knows\n" + " super\n" + " end\n" + "end\n"; + +#define RB2_FILE "lib/app/cop.rb" +#define RB6_FILE "lib/app/store.rb" +#define RB6_CALL(id, caller, text, at, tsuffix, line, decl) \ + SL_CALL_AT(id, RB6_FILE, "Store." caller, text, at, RB6_FILE, "Store." tsuffix, "Method", \ + line, decl) +#define RB3_FILE "lib/app/workers.rb" +#define RB4_FILE "lib/app/uses.rb" +#define RB5_FILE "lib/app/refs.rb" +#define SL_WRITE_AT(id, sfile, scaller, text, at, tfile, tsuffix, tlabel, tline, tdecl) \ + { \ + .case_id = id, \ + .site = {.file = sfile, .caller = scaller, .needle = text, .occurrence_index = at}, \ + .relation = "WRITES", \ + .target = {.file = tfile, \ + .suffix = tsuffix, \ + .label = tlabel, \ + .declaration_line = tline, \ + .declaration_text = tdecl}, \ + } + +/* Forms from devise, vagrant and rubocop. */ +TEST(resolver_source_ruby_writers_constructors_aliases_and_constants) { + static const RQ_Source sources[] = {{.path = RB2_FILE, .text = RB2_MAIN}}; + static const RQ_Expected expected[] = { + SL_CALL_AT("module-function-copies-onto-the-module", RB2_FILE, "Cop.calls_module_function", "PathUtil.smart_path(1)", 0, RB2_FILE, "PathUtil.smart_path", "Method", 9, + " def smart_path(path)"), + SL_CALL_AT("module-function-with-a-literal-name", RB2_FILE, "Cop.calls_listed_function", "Explicit.listed", 0, RB2_FILE, "Explicit.listed", "Method", 15, + " def listed"), + SL_CALL_AT("private-def-is-declared", RB2_FILE, "Cop.calls_private_def", "hidden()", 0, RB2_FILE, "Cop.hidden", "Method", 93, + " private def hidden"), + SL_CALL_AT("alias-method-names-the-earlier-def", RB2_FILE, "Cop.calls_alias_method", "new_m()", 0, RB2_FILE, "Cop.old_m", "Method", 97, + " def old_m"), + SL_CALL_AT("alias-keyword-names-the-earlier-def", RB2_FILE, "Cop.calls_alias_keyword", "also_m()", 0, RB2_FILE, "Cop.old_m", "Method", 97, + " def old_m"), + SL_CALL_AT("a-later-def-replaces-an-attribute-macro", RB2_FILE, "Cop.calls_later_def_over_attr", "name()", 0, RB2_FILE, "Cop.name", "Method", 75, + " def name"), + SL_CALL_AT("new-selects-initialize-with-an-outside-parent", RB2_FILE, "Cop.calls_new_outside_parent", "Corrector.new(1)", 0, RB2_FILE, "Corrector.initialize", "Method", 22, + " def initialize(source)"), + SL_CALL_AT("new-gives-the-chain-its-class-initialize", RB2_FILE, "Cop.calls_new_chain", "Corrector.new(2)", 0, RB2_FILE, "Corrector.initialize", "Method", 22, + " def initialize(source)"), + SL_CALL_AT("new-gives-the-chain-its-class-method", RB2_FILE, "Cop.calls_new_chain", "Corrector.new(2).fix", 0, RB2_FILE, "Corrector.fix", "Method", 26, + " def fix"), + SL_CALL_AT("new-with-no-initialize-is-the-class", RB2_FILE, "Cop.calls_new_no_initialize", "NoInit.new", 0, RB2_FILE, "App.NoInit", "Class", 31, + " class NoInit"), + SL_USE_AT("constant-receiver-of-new-is-a-mention", RB2_FILE, "Cop.calls_new_no_initialize", + "NoInit", 2, RB2_FILE, "App.NoInit", "Class", 31, " class NoInit"), + SL_USE_AT("inherited-constant-is-a-mention", RB2_FILE, "Cop.inherited_constant", "Inner", 1, + RB2_FILE, "Parent.Inner", "Class", 50, " class Inner"), + SL_CALL_AT("constant-holding-a-class-reads-as-it", RB2_FILE, "Cop.calls_new_through_constant_alias", "DEFAULT.new", 0, RB2_FILE, "App.NoInit", "Class", 31, + " class NoInit"), + SL_CALL_AT("custom-new-is-what-new-selects", RB2_FILE, "Cop.no_custom_new", "Factory.new", 0, RB2_FILE, "Factory.new", "Method", 38, + " def self.new"), + SL_CALL_AT("attribute-write-calls-the-writer", RB2_FILE, "Cop.calls_writer", "self.resource", 0, RB2_FILE, "Cop.resource=", "Method", 79, + " def resource=(value)"), + SL_CALL_AT("operator-write-reads-through-the-reader", RB2_FILE, "Cop.calls_reader_and_writer", "self.resource", 1, RB2_FILE, "Cop.resource", "Method", 83, + " def resource"), + SL_CALL_AT("operator-write-then-calls-the-writer", RB2_FILE, "Cop.calls_reader_and_writer", "resource", 5, RB2_FILE, "Cop.resource=", "Method", 79, + " def resource=(value)"), + SL_CALL_AT("self-class-searches-the-singleton-chain", RB2_FILE, "Cop.calls_class_chain", "self.class.registry", 0, RB2_FILE, "Cop.registry", "Method", 64, + " def registry"), + SL_CALL_AT("constant-attribute-write-calls-the-singleton-writer", RB2_FILE, "Cop.calls_constant_writer", "Cop.registry", 0, RB2_FILE, "Cop.registry=", "Method", 68, + " def registry=(value)"), + SL_CALL_AT("receiver-of-an-attribute-write-stays-a-call", RB2_FILE, "Cop.receiver_of_attribute_write", "helper_obj", 1, RB2_FILE, "Cop.helper_obj", "Method", 108, + " def helper_obj"), + SL_CALL_AT("constant-inherited-from-a-superclass", RB2_FILE, "Cop.inherited_constant", "Inner.new", 0, RB2_FILE, "Parent.Inner", "Class", 50, + " class Inner"), + SL_CALL_AT("class-of-an-inherited-constant", RB2_FILE, "Cop.inherited_constant", "Inner.new.inner_m", 0, RB2_FILE, "Inner.inner_m", "Method", 51, + " def inner_m"), + SL_CALL_AT("compact-class-header-skips-the-enclosing-module", RB2_FILE, "App::Compact.lexical_scope_skips_app", "Lexical.pick", 0, RB2_FILE, "Lexical.pick", "Method", 205, + " def self.pick"), + }; + static const RQ_Forbidden forbidden[] = { + SL_DENY_AT("alias-of-a-later-def-is-what-it-replaced", RB2_FILE, "Cop.wrapped", "orig_wrapped()", 0), + SL_DENY_AT("def-before-module-function-is-not-copied", RB2_FILE, "Cop.no_def_before_module_function", "PathUtil.before_function", 0), + SL_DENY_AT("writer-is-not-its-reader", RB2_FILE, "Cop.no_writer_is_not_its_reader", "self.reader_only", 0), + SL_DENY_AT("attribute-macro-has-no-node", RB2_FILE, "Cop.no_accessor_node", "self.accessor_only", 0), + SL_DENY_AT("custom-new-returns-anything", RB2_FILE, "Cop.no_custom_new", "Factory.new.work", 0), + SL_DENY_AT("constant-holding-a-value-has-no-class", RB2_FILE, "Cop.no_value_constant", "MSG.size", 0), + }; + ASSERT(sl_check("ruby-members", ".rb", "def filler\n 1\nend\n", sources, + sizeof(sources) / sizeof(sources[0]), expected, + sizeof(expected) / sizeof(expected[0]), forbidden, + sizeof(forbidden) / sizeof(forbidden[0]))); + PASS(); +} + +TEST(resolver_source_ruby_block_keeps_self_only_for_a_method_that_lets_it) { + static const RQ_Source sources[] = {{.path = RB3_FILE, .text = RB3_BLOCKS}}; + static const RQ_Expected expected[] = { + SL_CALL_AT("project-call-with-a-block", RB3_FILE, "Worker.in_project_method", + "add_offense(1) { |x| helper() }", 0, RB3_FILE, "Worker.add_offense", "Method", 3, + " def add_offense(node, &block)"), + SL_CALL_AT("project-call-forwarding-to-core", RB3_FILE, "Worker.in_forward_to_core", + "splice { |x| helper() }", 0, RB3_FILE, "Worker.splice", "Method", 23, + " def splice(&block)"), + SL_CALL_AT("project-call-that-rebinds-is-still-a-call", RB3_FILE, "Worker.no_instance_eval", + "configure { helper() }", 0, RB3_FILE, "Worker.configure", "Method", 11, + " def configure(&block)"), + SL_CALL_AT("project-call-that-forwards-is-still-a-call", RB3_FILE, + "Worker.no_forward_to_instance_eval", "relay { helper() }", 0, RB3_FILE, + "Worker.relay", "Method", 15, " def relay(&block)"), + SL_CALL_AT("project-call-that-keeps-is-still-a-call", RB3_FILE, "Worker.no_kept_block", + "keep { helper() }", 0, RB3_FILE, "Worker.keep", "Method", 19, + " def keep(&block)"), + SL_CALL_AT("core-iterator-keeps-self", RB3_FILE, "Worker.in_core_iterator", "helper()", 0, RB3_FILE, "Worker.helper", "Method", 27, + " def helper"), + SL_CALL_AT("core-iterators-in-a-chain-keep-self", RB3_FILE, "Worker.in_core_chain", "helper()", 1, RB3_FILE, "Worker.helper", "Method", 27, + " def helper"), + SL_CALL_AT("project-method-that-only-yields-keeps-self", RB3_FILE, "Worker.in_project_method", "helper()", 2, RB3_FILE, "Worker.helper", "Method", 27, + " def helper"), + SL_CALL_AT("project-method-forwarding-to-a-core-iterator", RB3_FILE, "Worker.in_forward_to_core", "helper()", 3, RB3_FILE, "Worker.helper", "Method", 27, + " def helper"), + SL_CALL_AT("core-iterator-in-a-class-body-keeps-the-class", RB3_FILE, "workers", "class_helper()", 0, RB3_FILE, "Worker.class_helper", "Method", 31, + " def self.class_helper"), + SL_CALL_AT("define-method-block-is-an-instance-method-body", RB3_FILE, "workers", "helper()", 9, RB3_FILE, "Worker.helper", "Method", 27, + " def helper"), + }; + static const RQ_Forbidden forbidden[] = { + SL_DENY_AT("instance-eval-in-the-project-def", RB3_FILE, "Worker.no_instance_eval", "helper()", 4), + SL_DENY_AT("forwarded-into-instance-eval", RB3_FILE, "Worker.no_forward_to_instance_eval", "helper()", 5), + SL_DENY_AT("project-def-keeps-the-block", RB3_FILE, "Worker.no_kept_block", "helper()", 6), + SL_DENY_AT("unknown-method-can-rebind", RB3_FILE, "Worker.no_unknown_method", "helper()", 7), + SL_DENY_AT("lambda-in-a-class-body-is-handed-over", RB3_FILE, "workers", "class_helper()", 1), + SL_DENY_AT("dsl-block-in-a-class-body", RB3_FILE, "workers", "class_helper()", 2), + }; + ASSERT(sl_check("ruby-blocks", ".rb", "def filler\n 1\nend\n", sources, + sizeof(sources) / sizeof(sources[0]), expected, + sizeof(expected) / sizeof(expected[0]), forbidden, + sizeof(forbidden) / sizeof(forbidden[0]))); + PASS(); +} + +/* The project defines each with instance_eval, so no `.each` receiver can be trusted. */ +TEST(resolver_source_ruby_project_each_that_rebinds_blocks_the_core_rule) { + static const RQ_Source sources[] = {{.path = RB4_FILE, .text = RB4_OWN_EACH}}; + static const RQ_Expected expected[] = { + SL_CALL_AT("other-core-iterators-still-keep-self", RB4_FILE, "Uses.map_is_still_core", "helper()", 1, RB4_FILE, "Uses.helper", "Method", 8, + " def helper"), + }; + static const RQ_Forbidden forbidden[] = { + SL_DENY_AT("each-is-the-projects-own", RB4_FILE, "Uses.no_project_each", "helper()", 0), + }; + ASSERT(sl_check("ruby-own-each", ".rb", "def filler\n 1\nend\n", sources, + sizeof(sources) / sizeof(sources[0]), expected, + sizeof(expected) / sizeof(expected[0]), forbidden, + sizeof(forbidden) / sizeof(forbidden[0]))); + PASS(); +} + +TEST(resolver_source_ruby_constants_and_file_locals_are_references) { + static const RQ_Source sources[] = {{.path = RB5_FILE, .text = RB5_REFS}, + {.path = "lib/app/other.rb", .text = RB5_OTHER}}; + static const RQ_Expected expected[] = { + SL_USE_AT("class-mention", RB5_FILE, "Thing.mentions_class", "Solo", 0, RB5_FILE, "App.Solo", "Class", 42, + " class Solo"), + SL_USE_AT("whole-path-mention", RB5_FILE, "Thing.mentions_path", "Inner::Deep", 0, RB5_FILE, "Inner.Deep", "Class", 46, + " class Deep"), + SL_USE_AT("path-prefix-is-read-on-the-way", RB5_FILE, "Thing.mentions_path", "Inner", 0, RB5_FILE, "App.Inner", "Class", 45, + " module Inner"), + SL_USE_AT("value-constant-mention", RB5_FILE, "Thing.mentions_value", "KIND", 1, RB5_FILE, "refs.KIND", "Variable", 8, + " KIND = 'k'"), + SL_USE_AT("rescue-names-a-class", RB5_FILE, "Thing.mentions_in_rescue", "Solo", 1, RB5_FILE, "App.Solo", "Class", 42, + " class Solo"), + SL_USE_AT("file-level-local-is-read", RB5_FILE, "refs", "limit", 1, RB5_FILE, "refs.limit", "Variable", 3, + "limit = 1"), + SL_WRITE_AT("file-level-local-is-written", RB5_FILE, "refs", "limit", 0, RB5_FILE, "refs.limit", "Variable", 3, + "limit = 1"), + }; + static const RQ_Forbidden forbidden[] = { + SL_NO_USE_AT("local-in-a-def-has-no-node", RB5_FILE, "Thing.no_local", "limit", 3), + SL_NO_USE_AT("reopened-namespace-is-no-single-declaration", RB5_FILE, "Thing.no_reopened_namespace", "Spread", 0), + SL_NO_USE_AT("constant-outside-the-project", RB5_FILE, "Thing.no_outside", "Gemfile::Thing", 0), + }; + ASSERT(sl_check("ruby-references", ".rb", "def filler\n 1\nend\n", sources, + sizeof(sources) / sizeof(sources[0]), expected, + sizeof(expected) / sizeof(expected[0]), forbidden, + sizeof(forbidden) / sizeof(forbidden[0]))); + PASS(); +} + +#define SL_REF_AT(id, sfile, scaller, text, at, tfile, tsuffix, tlabel, tline, tdecl) \ + { \ + .case_id = id, \ + .site = {.file = sfile, .caller = scaller, .needle = text, .occurrence_index = at}, \ + .relation = "CALL_REFERENCE", \ + .target = {.file = tfile, \ + .suffix = tsuffix, \ + .label = tlabel, \ + .declaration_line = tline, \ + .declaration_text = tdecl}, \ + } +#define SL_NO_REF_AT(id, sfile, scaller, text, at) \ + { \ + .case_id = id, \ + .site = {.file = sfile, .caller = scaller, .needle = text, .occurrence_index = at}, \ + .relation = "CALL_REFERENCE", .any_target = true \ + } + +/* ── Elixir: references and what the first round left out ──────────── */ + +static const char EX2_PROTOCOL[] = + "defprotocol Plug.Exception do\n" + " def status(exception)\n" + "end\n"; +static const char EX2_IMPL[] = + "defimpl Plug.Exception, for: Any do\n" + " def status(_exception), do: fallback()\n" + " defp fallback, do: 500\n" + "end\n"; +static const char EX2_OTHER[] = + "defmodule Plug.Other do\n" + " def pub(x), do: x\n" + " defp hid(x), do: x\n" + "end\n"; +static const char EX2_ERL[] = + "-module(plug_multipart).\n" + "-export([parse_headers/2]).\n" + "parse_headers(A, B) -> {A, B}.\n" + "hidden(A) -> A.\n"; +static const char EX2_REFS[] = + "defmodule Plug.Refs do\n" + " alias Plug.Other, as: O\n" + " def fetch_it(conn), do: conn\n" + " def chained(conn), do: fetch_it(conn).params\n" + " def proto(e), do: Plug.Exception.status(e)\n" + " def erl(data), do: :plug_multipart.parse_headers(data, 1)\n" + " def no_erl_private(data), do: :plug_multipart.hidden(data)\n" + " def no_erl_outside(data), do: :binary.match(data, 1)\n" + " def piped(conn), do: conn |> fetch_it\n" + " def no_variable(fetch_it), do: fetch_it\n" + " def local_capture, do: run(&fetch_it/1)\n" + " def remote_capture, do: run(&Plug.Other.pub/1)\n" + " def alias_capture, do: run(&O.pub/1)\n" + " def no_private_capture, do: run(&Plug.Other.hid/1)\n" + " def no_arity_capture, do: run(&fetch_it/2)\n" + " def mentions, do: Plug.Other\n" + " def mentions_alias, do: O\n" + " def no_outside_module, do: Some.Outside\n" + " defp run(fun), do: fun\n" + "end\n"; + +#define EX2_FILE "lib/plug/refs.ex" +#define EX2_ERL_FILE "src/plug_multipart.erl" + +TEST(resolver_source_elixir_captures_mentions_protocols_and_erlang_atoms) { + static const RQ_Source sources[] = { + {.path = "lib/plug/exception.ex", .text = EX2_PROTOCOL}, + {.path = "lib/plug/exception_any.ex", .text = EX2_IMPL}, + {.path = "lib/plug/other.ex", .text = EX2_OTHER}, + {.path = EX2_ERL_FILE, .text = EX2_ERL}, + {.path = EX2_FILE, .text = EX2_REFS}, + }; + static const RQ_Expected expected[] = { + SL_CALL_AT("receiver-of-a-field-is-read-first", EX2_FILE, "refs.chained", "fetch_it(conn)", 1, EX2_FILE, "refs.fetch_it", "Function", 3, + " def fetch_it(conn), do: conn"), + SL_CALL_AT("protocol-function-is-what-the-call-names", EX2_FILE, "refs.proto", "Plug.Exception.status(e)", 0, "lib/plug/exception.ex", "exception.status", "Function", 2, + " def status(exception)"), + SL_CALL_AT("impl-calls-its-own-function", "lib/plug/exception_any.ex", "exception_any.status", "fallback()", 0, "lib/plug/exception_any.ex", "exception_any.fallback", "Function", 3, + " defp fallback, do: 500"), + SL_CALL_AT("atom-receiver-is-an-erlang-module", EX2_FILE, "refs.erl", ":plug_multipart.parse_headers(data, 1)", 0, EX2_ERL_FILE, "plug_multipart.parse_headers", "Function", 3, + "parse_headers(A, B) -> {A, B}."), + SL_CALL_AT("pipe-into-a-bare-name-calls-it", EX2_FILE, "refs.piped", "fetch_it", 2, EX2_FILE, "refs.fetch_it", "Function", 3, + " def fetch_it(conn), do: conn"), + SL_REF_AT("local-capture", EX2_FILE, "refs.local_capture", "&fetch_it/1", 0, EX2_FILE, "refs.fetch_it", "Function", 3, + " def fetch_it(conn), do: conn"), + SL_REF_AT("remote-capture", EX2_FILE, "refs.remote_capture", "&Plug.Other.pub/1", 0, "lib/plug/other.ex", "other.pub", "Function", 2, + " def pub(x), do: x"), + SL_REF_AT("capture-through-an-alias", EX2_FILE, "refs.alias_capture", "&O.pub/1", 0, "lib/plug/other.ex", "other.pub", "Function", 2, + " def pub(x), do: x"), + SL_USE_AT("module-name-is-a-mention", EX2_FILE, "refs.mentions", "Plug.Other", 3, "lib/plug/other.ex", "Plug.Other", "Class", 1, + "defmodule Plug.Other do"), + SL_USE_AT("aliased-module-name-is-a-mention", EX2_FILE, "refs.mentions_alias", "O", 6, "lib/plug/other.ex", "Plug.Other", "Class", 1, + "defmodule Plug.Other do"), + }; + static const RQ_Forbidden forbidden[] = { + SL_DENY_AT("erlang-function-not-exported", EX2_FILE, "refs.no_erl_private", ":plug_multipart.hidden(data)", 0), + SL_DENY_AT("erlang-module-outside-the-project", EX2_FILE, "refs.no_erl_outside", ":binary.match(data, 1)", 0), + SL_DENY_AT("bare-name-is-a-variable", EX2_FILE, "refs.no_variable", "fetch_it", 4), + SL_NO_REF_AT("private-function-does-not-capture", EX2_FILE, "refs.no_private_capture", "&Plug.Other.hid/1", 0), + SL_NO_REF_AT("capture-of-an-absent-arity", EX2_FILE, "refs.no_arity_capture", "&fetch_it/2", 0), + SL_NO_USE_AT("module-outside-the-project", EX2_FILE, "refs.no_outside_module", "Some.Outside", 0), + }; + ASSERT(sl_check("elixir-references", ".ex", "defmodule Filler do\n def f, do: 1\nend\n", + sources, sizeof(sources) / sizeof(sources[0]), expected, + sizeof(expected) / sizeof(expected[0]), forbidden, + sizeof(forbidden) / sizeof(forbidden[0]))); + PASS(); +} + +/* ── Elixir: what `use` is known to bring ──────────────────────────── */ + +static const char EX3_CONN[] = "defmodule My.Conn do\n" + " def assign(conn, key), do: {conn, key}\n" + " def halt(conn), do: conn\n" + "end\n"; +static const char EX3_BUILDER[] = + "defmodule My.Builder do\n" + " defmacro __using__(_opts) do\n" + " quote do\n" + " import My.Conn, except: [halt: 1]\n" + " import My.Builder, only: [plug: 1]\n" + " def call(conn, opts), do: {conn, opts}\n" + " end\n" + " end\n" + " defmacro plug(name), do: name\n" + " defmacro not_listed(name), do: name\n" + " def call(conn), do: conn\n" + "end\n"; +static const char EX3_ROUTER[] = "defmodule My.Router do\n" + " defmacro __using__(opts) do\n" + " quote location: :keep do\n" + " import unquote(__MODULE__)\n" + " use My.Builder, unquote(opts)\n" + " end\n" + " end\n" + " defmacro get(path, contents), do: {path, contents}\n" + "end\n"; +static const char EX3_DISCARDS[] = "defmodule My.Discards do\n" + " defmacro __using__(_opts) do\n" + " _ = quote do\n" + " import My.Conn\n" + " end\n" + " :ok\n" + " end\n" + "end\n"; +static const char EX3_APP[] = + "defmodule My.App do\n" + " use My.Router\n" + " plug :first\n" + " get \"/\" do\n" + " :ok\n" + " end\n" + " def through_two_uses(conn), do: assign(conn, 1)\n" + " def no_except(conn), do: halt(conn)\n" + " def no_only(name), do: not_listed(name)\n" + " def no_injected(conn, opts), do: call(conn, opts)\n" + "end\n"; +static const char EX3_PLAIN[] = "defmodule My.Plain do\n" + " def no_use(conn), do: assign(conn, 1)\n" + "end\n"; +static const char EX3_USES_DISCARDS[] = "defmodule My.UsesDiscards do\n" + " use My.Discards\n" + " def no_discarded(conn), do: assign(conn, 1)\n" + "end\n"; +static const char EX3_SCOPED[] = "defmodule My.Scoped do\n" + " def in_block(conn) do\n" + " if conn do\n" + " use My.Builder\n" + " assign(conn, 1)\n" + " end\n" + " assign(conn, 2)\n" + " end\n" + "end\n"; + +static const char EX3_OUTER[] = "defmodule My.Outer do\n" + " import My.Conn\n" + " defmodule Nested do\n" + " use My.Router\n" + " def in_nested(conn), do: assign(conn, 3)\n" + " def halts_in_nested(conn), do: halt(conn)\n" + " end\n" + " def after_nested(conn), do: halt(conn)\n" + " def no_macro_after_nested(name), do: plug(name)\n" + "end\n"; + +TEST(resolver_source_elixir_use_brings_what_using_returns) { + static const RQ_Source sources[] = { + {.path = "lib/my/conn.ex", .text = EX3_CONN}, + {.path = "lib/my/builder.ex", .text = EX3_BUILDER}, + {.path = "lib/my/router.ex", .text = EX3_ROUTER}, + {.path = "lib/my/discards.ex", .text = EX3_DISCARDS}, + {.path = "lib/my/app.ex", .text = EX3_APP}, + {.path = "lib/my/plain.ex", .text = EX3_PLAIN}, + {.path = "lib/my/uses_discards.ex", .text = EX3_USES_DISCARDS}, + {.path = "lib/my/scoped.ex", .text = EX3_SCOPED}, + {.path = "lib/my/outer.ex", .text = EX3_OUTER}, + }; + static const RQ_Expected expected[] = { + SL_CALL_AT("import-of-the-macro-module-itself", "lib/my/app.ex", "my.app", + "get \"/\" do\n :ok\n end", 0, "lib/my/router.ex", "router.get", "Function", + 8, " defmacro get(path, contents), do: {path, contents}"), + SL_CALL_AT("import-with-only-through-a-second-use", "lib/my/app.ex", "my.app", + "plug :first", 0, "lib/my/builder.ex", "builder.plug", "Function", 9, + " defmacro plug(name), do: name"), + SL_CALL_AT("import-through-a-second-use", "lib/my/app.ex", "app.through_two_uses", + "assign(conn, 1)", 0, "lib/my/conn.ex", "conn.assign", "Function", 2, + " def assign(conn, key), do: {conn, key}"), + SL_CALL_AT("nested-module-keeps-the-imports-around-it", "lib/my/outer.ex", + "outer.halts_in_nested", "halt(conn)", 0, "lib/my/conn.ex", "conn.halt", + "Function", 3, " def halt(conn), do: conn"), + SL_CALL_AT("use-in-a-nested-module", "lib/my/outer.ex", "outer.in_nested", + "assign(conn, 3)", 0, "lib/my/conn.ex", "conn.assign", "Function", 2, + " def assign(conn, key), do: {conn, key}"), + SL_CALL_AT("import-survives-a-nested-modules-use", "lib/my/outer.ex", "outer.after_nested", + "halt(conn)", 1, "lib/my/conn.ex", "conn.halt", "Function", 3, + " def halt(conn), do: conn"), + SL_CALL_AT("use-holds-to-the-end-of-its-block", "lib/my/scoped.ex", "scoped.in_block", + "assign(conn, 1)", 0, "lib/my/conn.ex", "conn.assign", "Function", 2, + " def assign(conn, key), do: {conn, key}"), + }; + static const RQ_Forbidden forbidden[] = { + SL_DENY_AT("except-is-kept", "lib/my/app.ex", "app.no_except", "halt(conn)", 0), + SL_DENY_AT("what-a-nested-module-adds-ends-with-it", "lib/my/outer.ex", + "outer.no_macro_after_nested", "plug(name)", 0), + SL_DENY_AT("only-is-kept", "lib/my/app.ex", "app.no_only", "not_listed(name)", 0), + SL_DENY_AT("function-put-here-by-use", "lib/my/app.ex", "app.no_injected", + "call(conn, opts)", 0), + SL_DENY_AT("no-use-no-import", "lib/my/plain.ex", "plain.no_use", "assign(conn, 1)", 0), + SL_DENY_AT("quote-that-is-not-returned", "lib/my/uses_discards.ex", + "uses_discards.no_discarded", "assign(conn, 1)", 0), + SL_DENY_AT("use-ended-with-its-block", "lib/my/scoped.ex", "scoped.in_block", + "assign(conn, 2)", 0), + }; + ASSERT(sl_check("elixir-use", ".ex", "defmodule Filler do\n def f, do: 1\nend\n", sources, + sizeof(sources) / sizeof(sources[0]), expected, + sizeof(expected) / sizeof(expected[0]), forbidden, + sizeof(forbidden) / sizeof(forbidden[0]))); + PASS(); +} + +/* ── Erlang: fun references ────────────────────────────────────────── */ + +static const char ERL2_OTHER[] = + "-module(other).\n" + "-export([pub/1, imp/1]).\n" + "pub(X) -> X.\n" + "imp(X) -> X.\n" + "hid(X) -> X.\n"; +static const char ERL2_REFS[] = + "-module(refs).\n" + "-export([local_ref/0, remote_ref/0, imported_ref/0, no_private/0, no_arity/0, no_variable/1]).\n" + "-import(other, [imp/1]).\n" + "helper(X) -> X.\n" + "local_ref() -> fun helper/1.\n" + "remote_ref() -> fun other:pub/1.\n" + "imported_ref() -> fun imp/1.\n" + "no_private() -> fun other:hid/1.\n" + "no_arity() -> fun helper/2.\n" + "no_variable(M) -> fun M:pub/1.\n"; + +TEST(resolver_source_erlang_fun_references) { + static const RQ_Source sources[] = {{.path = "src/other.erl", .text = ERL2_OTHER}, + {.path = "src/refs.erl", .text = ERL2_REFS}}; + static const RQ_Expected expected[] = { + SL_REF_AT("local-fun-reference", "src/refs.erl", "refs.local_ref", "fun helper/1", 0, "src/refs.erl", "refs.helper", "Function", 4, + "helper(X) -> X."), + SL_REF_AT("remote-fun-reference", "src/refs.erl", "refs.remote_ref", "fun other:pub/1", 0, "src/other.erl", "other.pub", "Function", 3, + "pub(X) -> X."), + SL_REF_AT("imported-fun-reference", "src/refs.erl", "refs.imported_ref", "fun imp/1", 0, "src/other.erl", "other.imp", "Function", 4, + "imp(X) -> X."), + }; + static const RQ_Forbidden forbidden[] = { + SL_NO_REF_AT("fun-of-a-function-not-exported", "src/refs.erl", "refs.no_private", "fun other:hid/1", 0), + SL_NO_REF_AT("fun-of-an-absent-arity", "src/refs.erl", "refs.no_arity", "fun helper/2", 0), + SL_NO_REF_AT("fun-through-a-variable-module", "src/refs.erl", "refs.no_variable", "fun M:pub/1", 0), + }; + ASSERT(sl_check("erlang-references", ".erl", "-module(filler).\nf() -> 1.\n", sources, + sizeof(sources) / sizeof(sources[0]), expected, + sizeof(expected) / sizeof(expected[0]), forbidden, + sizeof(forbidden) / sizeof(forbidden[0]))); + PASS(); +} + +/* ── Perl: the invocant read directly, re-exports and code references ─ */ + +static const char PL2_BASEUTIL[] = + "package My::BaseUtil;\n" + "use Exporter 'import';\n" + "our @EXPORT_OK = qw(patch);\n" + "sub patch { return 1; }\n" + "1;\n"; +static const char PL2_UTIL[] = + "package My::Util;\n" + "use Exporter 'import';\n" + "use My::BaseUtil qw(patch);\n" + "our @EXPORT_OK = qw(patch other);\n" + "sub other { return 2; }\n" + "1;\n"; +static const char PL2_ROOT[] = + "package My::Root;\n" + "sub root_m { return 3; }\n" + "1;\n"; +static const char PL2_THING[] = + "package My::Thing;\n" + "use parent -norequire, 'My::Root';\n" + "use My::Util qw(patch);\n" + "\n" + "sub own { return 4; }\n" + "sub _select { $_[1] ? $_[0]->own : $_[0] }\n" + "sub via_shift { shift->own }\n" + "sub via_shift_paren { shift()->own }\n" + "sub via_arg { $_[0]->own }\n" + "sub via_inherited { shift->root_m(@_) }\n" + "sub after_shift { my $x = shift; $_[0]->own }\n" + "sub second_shift { my $x = shift; shift->own }\n" + "sub anon_has_its_own_args { my $cb = sub { shift->own }; $cb }\n" + "sub calls_select { _select(1, 2) }\n" + "sub calls_reexport { patch() }\n" + "sub takes_ref { my $r = \\&own; $r }\n" + "sub takes_qualified_ref { keep(\\&My::Root::root_m) }\n" + "sub no_absent_ref { my $r = \\&absent; $r }\n" + "1;\n"; + +static const char PL3_THING[] = + "package My::Over;\n" + "use overload bool => sub {1}, '\"\"' => sub { shift->to_text }, fallback => 1;\n" + "use other_pragma '+' => sub { shift->to_text };\n" + "\n" + "sub to_text { return 'x'; }\n" + "sub list_shift { my ($self, $name) = (shift, shift); $self->to_text; }\n" + "sub list_shift_second { my ($first, $other) = (shift, shift); $other->to_text; }\n" + "sub list_not_shift { my ($self, $name) = (1, shift); $self->to_text; }\n" + "sub list_rebound { my ($self, $name) = (shift, shift); $self = 1; $self->to_text; }\n" + "1;\n"; +static const char PL3_MAIN[] = "use overload '+' => sub { shift->to_text };\n" + "sub to_text { return 'main'; }\n"; + +TEST(resolver_source_perl_list_shift_and_overload) { + static const RQ_Source sources[] = { + {.path = "lib/My/Over.pm", .text = PL3_THING}, + {.path = "script/main.pl", .text = PL3_MAIN}, + }; + static const RQ_Expected expected[] = { + SL_CALL_AT("operator-sub-of-use-overload-gets-the-object", "lib/My/Over.pm", "My.Over", + "shift->to_text", 0, "lib/My/Over.pm", "Over.to_text", "Function", 5, + "sub to_text { return 'x'; }"), + SL_CALL_AT("list-of-shifts-takes-the-first-argument", "lib/My/Over.pm", "Over.list_shift", + "$self->to_text", 0, "lib/My/Over.pm", "Over.to_text", "Function", 5, + "sub to_text { return 'x'; }"), + }; + static const RQ_Forbidden forbidden[] = { + SL_DENY_AT("another-pragma-promises-nothing", "lib/My/Over.pm", "My.Over", + "shift->to_text", 1), + SL_DENY_AT("second-of-the-list-is-no-invocant", "lib/My/Over.pm", "Over.list_shift_second", + "$other->to_text", 0), + SL_DENY_AT("list-not-led-by-shift", "lib/My/Over.pm", "Over.list_not_shift", + "$self->to_text", 1), + SL_DENY_AT("rebound-after-the-list", "lib/My/Over.pm", "Over.list_rebound", + "$self->to_text", 2), + SL_DENY_AT("overload-in-main-has-no-class", "script/main.pl", "script.main", "shift->to_text", + 0), + }; + ASSERT(sl_check("perl-list-shift", ".pl", "sub filler { return 1; }\n", sources, + sizeof(sources) / sizeof(sources[0]), expected, + sizeof(expected) / sizeof(expected[0]), forbidden, + sizeof(forbidden) / sizeof(forbidden[0]))); + PASS(); +} + +TEST(resolver_source_perl_invocant_reexports_and_code_references) { + static const RQ_Source sources[] = { + {.path = "lib/My/BaseUtil.pm", .text = PL2_BASEUTIL}, + {.path = "lib/My/Util.pm", .text = PL2_UTIL}, + {.path = "lib/My/Root.pm", .text = PL2_ROOT}, + {.path = "lib/My/Thing.pm", .text = PL2_THING}, + }; + static const RQ_Expected expected[] = { + SL_CALL_AT("shift-is-the-invocant", "lib/My/Thing.pm", "Thing.via_shift", "shift->own", 0, "lib/My/Thing.pm", "Thing.own", "Function", 5, + "sub own { return 4; }"), + SL_CALL_AT("shift-with-parens-is-the-invocant", "lib/My/Thing.pm", "Thing.via_shift_paren", "shift()->own", 0, "lib/My/Thing.pm", "Thing.own", "Function", 5, + "sub own { return 4; }"), + SL_CALL_AT("first-argument-is-the-invocant", "lib/My/Thing.pm", "Thing.via_arg", "$_[0]->own", 1, "lib/My/Thing.pm", "Thing.own", "Function", 5, + "sub own { return 4; }"), + SL_CALL_AT("shift-walks-the-parents", "lib/My/Thing.pm", "Thing.via_inherited", "shift->root_m(@_)", 0, "lib/My/Root.pm", "Root.root_m", "Function", 2, + "sub root_m { return 3; }"), + SL_CALL_AT("import-of-a-reexported-name", "lib/My/Thing.pm", "Thing.calls_reexport", "patch()", 0, "lib/My/BaseUtil.pm", "BaseUtil.patch", "Function", 4, + "sub patch { return 1; }"), + SL_REF_AT("code-reference-to-a-sub-of-the-package", "lib/My/Thing.pm", "Thing.takes_ref", "\\&own", 0, "lib/My/Thing.pm", "Thing.own", "Function", 5, + "sub own { return 4; }"), + SL_REF_AT("code-reference-with-its-package", "lib/My/Thing.pm", "Thing.takes_qualified_ref", "\\&My::Root::root_m", 0, "lib/My/Root.pm", "Root.root_m", "Function", 2, + "sub root_m { return 3; }"), + }; + static const RQ_Forbidden forbidden[] = { + SL_DENY_AT("plain-function-takes-whatever-it-is-given", "lib/My/Thing.pm", "Thing._select", "$_[0]->own", 0), + SL_DENY_AT("first-argument-after-a-shift-is-the-second", "lib/My/Thing.pm", "Thing.after_shift", "$_[0]->own", 2), + SL_DENY_AT("second-shift-is-the-second-argument", "lib/My/Thing.pm", "Thing.second_shift", "shift->own", 1), + SL_DENY_AT("anonymous-sub-has-its-own-arguments", "lib/My/Thing.pm", "Thing.anon_has_its_own_args", "shift->own", 2), + SL_NO_REF_AT("code-reference-to-nothing", "lib/My/Thing.pm", "Thing.no_absent_ref", "\\&absent", 0), + }; + ASSERT(sl_check("perl-invocant", ".pm", "package Filler;\nsub f { return 1; }\n1;\n", sources, + sizeof(sources) / sizeof(sources[0]), expected, + sizeof(expected) / sizeof(expected[0]), forbidden, + sizeof(forbidden) / sizeof(forbidden[0]))); + PASS(); +} + +/* ── Ruby: inheritance through the header's constant ───────────────── */ + +static const char RB6_BASE[] = "module App\n" + " class Base\n" + " def run\n" + " 1\n" + " end\n" + " end\n" + "end\n"; +static const char RB6_CHILD[] = "module App\n" + " module Style\n" + " class Child < Base\n" + " def run\n" + " 2\n" + " end\n" + " end\n" + " class Outside < Gem::Base\n" + " end\n" + " end\n" + "end\n"; + +TEST(resolver_source_ruby_bare_new_and_index_operators) { + static const RQ_Source sources[] = {{.path = RB6_FILE, .text = RB6_INDEX}, + {.path = "lib/app/corrector.rb", .text = RB7_CORRECTOR}}; + static const RQ_Expected expected[] = { + SL_CALL_AT("include-in-a-singleton-class-is-extend", "lib/app/corrector.rb", + "Corrector.correct", "from_helper()", 0, "lib/app/corrector.rb", + "Helper.from_helper", "Method", 2, " def from_helper"), + RB6_CALL("bare-new-in-a-class-method", "build", "new(a)", 0, "initialize", 2, + " def initialize(a = 1)"), + RB6_CALL("bare-new-under-operator-assignment", "memo", "new(a)", 1, "initialize", 2, + " def initialize(a = 1)"), + RB6_CALL("index-reads-self", "reads_self", "self[key]", 0, "[]", 6, " def [](key)"), + RB6_CALL("index-writes-self", "writes_self", "self[key]", 1, "[]=", 10, + " def []=(key, value)"), + RB6_CALL("index-or-assign-reads", "both_self", "self[key]", 2, "[]", 6, " def [](key)"), + RB6_CALL("index-or-assign-writes", "both_self", "[", 4, "[]=", 10, + " def []=(key, value)"), + RB6_CALL("new-of-a-constant", "reads_typed", "Store.new", 0, "initialize", 2, + " def initialize(a = 1)"), + RB6_CALL("index-of-a-local-from-new", "reads_typed", "other[1]", 0, "[]", 6, + " def [](key)"), + SL_USE_AT("constant-in-new", RB6_FILE, "Store.reads_typed", "Store", 1, RB6_FILE, + "app.store.Store", "Class", 1, "class Store"), + }; + static const RQ_Forbidden forbidden[] = { + SL_DENY_AT("an-instance-does-not-have-it", "lib/app/corrector.rb", + "Corrector.no_instance_reach", "from_helper()", 1), + SL_DENY_AT("new-is-no-instance-method", RB6_FILE, "Store.no_instance_new", "new(a)", 3), + SL_DENY_AT("index-of-an-unknown-value", RB6_FILE, "Store.no_unknown_receiver", "thing[1]", + 0), + SL_DENY_AT("index-of-a-literal", RB6_FILE, "Store.no_array_literal", "[1][0]", 0), + SL_DENY_AT("index-absent-from-the-chain", RB6_FILE, "Plain.no_index_on_a_class_without_it", + "self[1]", 0), + }; + ASSERT(sl_check("ruby-index", ".rb", "class Filler\nend\n", sources, + sizeof(sources) / sizeof(sources[0]), expected, + sizeof(expected) / sizeof(expected[0]), forbidden, + sizeof(forbidden) / sizeof(forbidden[0]))); + PASS(); +} + +TEST(resolver_source_ruby_super_selects_what_follows_the_own_step) { + static const RQ_Source sources[] = {{.path = "lib/app/kin.rb", .text = RB8_SUPER}}; + static const RQ_Expected expected[] = { + SL_CALL_AT("super-reaches-the-superclass", "lib/app/kin.rb", "Child.go", "super", 0, + "lib/app/kin.rb", "Parent.go", "Method", 8, " def go(a)"), + SL_CALL_AT("super-with-parens-reaches-an-included-module-first", "lib/app/kin.rb", + "Child.through_mixin", "super", 1, "lib/app/kin.rb", "Mixed.through_mixin", + "Method", 2, " def through_mixin"), + SL_CALL_AT("super-of-a-class-method-reaches-the-superclasss", "lib/app/kin.rb", "Child.make", + "super", 2, "lib/app/kin.rb", "Parent.make", "Method", 12, " def self.make"), + }; + static const RQ_Forbidden forbidden[] = { + SL_DENY_AT("nothing-on-the-chain", "lib/app/kin.rb", "Child.no_parent_method", "super", 3), + SL_DENY_AT("chain-leaves-the-project", "lib/app/kin.rb", "Outsider.no_chain_out", "super", 4), + SL_DENY_AT("a-module-method-goes-where-its-includer-says", "lib/app/kin.rb", + "Mod.no_module_knows", "super", 5), + }; + ASSERT(sl_check("ruby-super", ".rb", "class Filler\nend\n", sources, + sizeof(sources) / sizeof(sources[0]), expected, + sizeof(expected) / sizeof(expected[0]), forbidden, + sizeof(forbidden) / sizeof(forbidden[0]))); + PASS(); +} + +TEST(resolver_source_ruby_superclass_reaches_the_graph_on_both_routes) { + for (int driver = 0; driver < 2; driver++) { + const char *root = th_mktempdir("cbm_source_langs"); + ASSERT_NOT_NULL(root); + char repo[768], database[768]; + snprintf(repo, sizeof(repo), "%s/repo", root); + snprintf(database, sizeof(database), "%s/index.db", root); + ASSERT(cbm_mkdir_p(repo, 0755)); + RQ_Source base = {.path = "lib/app/base.rb", .text = RB6_BASE}; + RQ_Source child = {.path = "lib/app/style/child.rb", .text = RB6_CHILD}; + ASSERT(sl_write(repo, &base) && sl_write(repo, &child)); + for (int i = 0; i < 55; i++) { + char path[64]; + snprintf(path, sizeof(path), "filler/f%02d.rb", i); + RQ_Source filler = {.path = path, .text = "def filler\n 1\nend\n"}; + ASSERT(sl_write(repo, &filler)); + } + const char *old_workers = getenv("CBM_WORKERS"); + char *saved_workers = old_workers ? strdup(old_workers) : NULL; + cbm_setenv("CBM_WORKERS", driver ? "4" : "1", 1); + cbm_pipeline_t *pipeline = cbm_pipeline_new(repo, database, CBM_MODE_FULL); + ASSERT(pipeline && cbm_pipeline_set_project_name(pipeline, "ruby-inherit") && + cbm_pipeline_run(pipeline) == 0); + cbm_pipeline_free(pipeline); + if (saved_workers) { + cbm_setenv("CBM_WORKERS", saved_workers, 1); + free(saved_workers); + } else { + cbm_unsetenv("CBM_WORKERS"); + } + cbm_store_t *store = cbm_store_open_path_existing(database); + ASSERT_NOT_NULL(store); + /* Child inherits Base through the enclosing module; Outside's parent is a gem's. */ + ASSERT_EQ(cbm_store_count_edges_by_type(store, "ruby-inherit", "INHERITS"), 1); + ASSERT_EQ(cbm_store_count_edges_by_type(store, "ruby-inherit", "OVERRIDE"), 1); + cbm_store_close(store); + th_cleanup(root); + } + PASS(); +} + +/* ── OCaml: dune libraries, includes and scoped opens ──────────────── */ + +static const char ML_LIB_DUNE[] = + "(include_subdirs qualified)\n" + "\n" + "(library\n" + " (name corelib)\n" + " (public_name corelib)\n" + " (synopsis \"A library.\\nWith a string (holding parentheses.\")\n" + " (libraries unix))\n"; +static const char ML_CORELIB[] = + "let printfn x = x\n" + "module Path = Path\n" + "module Local = struct\n" + " let inner x = x\n" + "end\n"; +static const char ML_PATH[] = + "let to_string x = x\n"; +static const char ML_NOMAIN_DUNE[] = + "(library\n" + " (name nomain))\n"; +static const char ML_NOMAIN_UNIT[] = + "let member x = x\n"; +static const char ML_FUNCTORED_DUNE[] = + "(library\n" + " (name functored))\n"; +static const char ML_FUNCTORED[] = + "include Make (Base)\n" + "\n" + "let known x = x\n"; +static const char ML_RULES_DUNE[] = + "(include_subdirs unqualified)\n" + "\n" + "(library\n" + " (name rules)\n" + " (libraries corelib nomain functored))\n"; +static const char ML_IMPORT[] = + "include Corelib\n" + "\n" + "include struct\n" + " open Nomain\n" + " module Unit_one = Unit_one\n" + "end\n" + "\n" + "let helper x = x\n"; +static const char ML_DEEP[] = + "let deep x = x\n"; +static const char ML_DUNE_MAIN[] = + "open Import\n" + "\n" + "let bare_from_open x = helper x\n" + "let bare_through_include x = printfn x\n" + "let module_through_include x = Path.to_string x\n" + "let module_from_subtree x = Deep.deep x\n" + "let library_path x = Corelib.Local.inner x\n" + "let wrapped_library x = Nomain.Unit_one.member x\n" + "let alias_under_struct_open x = Unit_one.member x\n" + "let no_module_outside x = Outside.thing x\n" + "let no_local_record_field { helper; _ } = helper 1\n" + "let no_local_parameter ~helper = helper 1\n" + "let no_shadowed_by_local_open x = Corelib.(printfn x)\n" + "let local_open_is_scoped x = helper x\n"; +static const char ML_OTHER[] = + "open Functored\n" + "\n" + "let no_after_open_ended x = known x\n"; +static const char ML_SHADOW[] = + "let helper x = x\n" + "let call_before x = helper x\n" + "\n" + "open Corelib\n" + "\n" + "let printfn x = x\n" + "let own_after_open x = printfn x\n"; + +TEST(resolver_source_ocaml_dune_libraries_includes_and_scoped_opens) { + static const RQ_Source sources[] = { + {.path = "libs/corelib/dune", .text = ML_LIB_DUNE}, + {.path = "libs/corelib/corelib.ml", .text = ML_CORELIB}, + {.path = "libs/corelib/path.ml", .text = ML_PATH}, + {.path = "libs/nomain/dune", .text = ML_NOMAIN_DUNE}, + {.path = "libs/nomain/unit_one.ml", .text = ML_NOMAIN_UNIT}, + {.path = "libs/functored/dune", .text = ML_FUNCTORED_DUNE}, + {.path = "libs/functored/functored.ml", .text = ML_FUNCTORED}, + {.path = "src/rules/dune", .text = ML_RULES_DUNE}, + {.path = "src/rules/import.ml", .text = ML_IMPORT}, + {.path = "src/rules/sub/deep.ml", .text = ML_DEEP}, + {.path = "src/rules/main.ml", .text = ML_DUNE_MAIN}, + {.path = "src/other/other.ml", .text = ML_OTHER}, + {.path = "src/rules/shadow.ml", .text = ML_SHADOW}, + }; + static const RQ_Expected expected[] = { + SL_CALL_AT("name-from-an-open-sibling", "src/rules/main.ml", "main.bare_from_open", "helper x", 0, "src/rules/import.ml", "import.helper", "Function", 8, + "let helper x = x"), + SL_CALL_AT("name-through-an-include-of-a-library", "src/rules/main.ml", "main.bare_through_include", "printfn x", 0, "libs/corelib/corelib.ml", "corelib.printfn", "Function", 1, + "let printfn x = x"), + SL_CALL_AT("module-through-an-include-of-a-library", "src/rules/main.ml", "main.module_through_include", "Path.to_string x", 0, "libs/corelib/path.ml", "path.to_string", "Function", 1, + "let to_string x = x"), + SL_CALL_AT("unit-of-an-unqualified-subtree", "src/rules/main.ml", "main.module_from_subtree", "Deep.deep x", 0, "src/rules/sub/deep.ml", "deep.deep", "Function", 1, + "let deep x = x"), + SL_CALL_AT("nested-module-of-a-library", "src/rules/main.ml", "main.library_path", "Corelib.Local.inner x", 0, "libs/corelib/corelib.ml", "corelib.Local.inner", "Function", 4, + " let inner x = x"), + SL_CALL_AT("unit-of-a-library-with-no-main-module", "src/rules/main.ml", "main.wrapped_library", "Nomain.Unit_one.member x", 0, "libs/nomain/unit_one.ml", "unit_one.member", "Function", 1, + "let member x = x"), + SL_CALL_AT("alias-read-under-its-structs-open", "src/rules/main.ml", "main.alias_under_struct_open", "Unit_one.member x", 1, "libs/nomain/unit_one.ml", "unit_one.member", "Function", 1, + "let member x = x"), + SL_CALL_AT("local-open-selects-from-its-module", "src/rules/main.ml", "main.no_shadowed_by_local_open", "printfn x", 1, "libs/corelib/corelib.ml", "corelib.printfn", "Function", 1, + "let printfn x = x"), + SL_CALL_AT("local-open-ends-with-its-expression", "src/rules/main.ml", "main.local_open_is_scoped", "helper x", 1, "src/rules/import.ml", "import.helper", "Function", 8, + "let helper x = x"), + SL_CALL_AT("let-before-an-open-it-is-not-in", "src/rules/shadow.ml", "shadow.call_before", "helper x", 1, "src/rules/shadow.ml", "shadow.helper", "Function", 1, + "let helper x = x"), + SL_CALL_AT("let-after-an-open-replaces-its-name", "src/rules/shadow.ml", "shadow.own_after_open", "printfn x", 1, "src/rules/shadow.ml", "shadow.printfn", "Function", 6, + "let printfn x = x"), + }; + static const RQ_Forbidden forbidden[] = { + SL_DENY_AT("module-outside-the-project", "src/rules/main.ml", "main.no_module_outside", "Outside.thing x", 0), + SL_DENY_AT("record-pun-binds-the-field", "src/rules/main.ml", "main.no_local_record_field", "helper 1", 0), + SL_DENY_AT("labelled-parameter-binds", "src/rules/main.ml", "main.no_local_parameter", "helper 1", 1), + SL_DENY_AT("open-of-a-functor-application-is-unknown", "src/other/other.ml", "other.no_after_open_ended", "known x", 0), + }; + ASSERT(sl_check("ocaml-dune", ".ml", "let filler x = x\n", sources, + sizeof(sources) / sizeof(sources[0]), expected, + sizeof(expected) / sizeof(expected[0]), forbidden, + sizeof(forbidden) / sizeof(forbidden[0]))); + PASS(); +} + +static const char ML2_BELOW[] = "open Rootimport\n" + "let up_from_a_subdirectory x = at_the_root x\n" + "let public_name_is_the_library x = Publiclib.from_public x\n" + "let no_dotted_public_name x = Dotted.from_dotted x\n" + "let no_public_beside_a_name x = Other_name.from_real x\n" + "let name_beside_a_public_name x = Real_name.from_real x\n"; + +TEST(resolver_source_ocaml_subdirectories_and_public_names) { + static const RQ_Source sources[] = { + {.path = "libs/publiclib/dune", .text = "(library\n (public_name publiclib))\n"}, + {.path = "libs/publiclib/publiclib.ml", .text = "let from_public x = x\n"}, + {.path = "libs/dotted/dune", .text = "(library\n (public_name some.dotted))\n"}, + {.path = "libs/dotted/dotted.ml", .text = "let from_dotted x = x\n"}, + {.path = "libs/named/dune", + .text = "(library\n (name real_name)\n (public_name other_name))\n"}, + {.path = "libs/named/real_name.ml", .text = "let from_real x = x\n"}, + {.path = "src/rules/dune", .text = "(include_subdirs unqualified)\n\n(library\n (name rules))\n"}, + {.path = "src/rules/rootimport.ml", .text = "let at_the_root x = x\n"}, + {.path = "src/rules/sub/below.ml", .text = ML2_BELOW}, + {.path = "src/rules/sub/paired.ml", .text = "let in_a_subdirectory x = x\n"}, + {.path = "src/rules/sub/paired.mli", .text = "val in_a_subdirectory : int -> int\n"}, + {.path = "src/rules/top.ml", + .text = "let down_to_a_unit_with_an_interface x = Paired.in_a_subdirectory x\n"}, + {.path = "src/plain/dune", .text = "(library\n (name plain))\n"}, + {.path = "src/plain/top.ml", .text = "let at_top x = x\n"}, + {.path = "src/plain/sub/inner.ml", .text = "let no_climb_without_subdirs x = Top.at_top x\n"}, + }; + static const RQ_Expected expected[] = { + SL_CALL_AT("subdirectory-reads-the-librarys-root", "src/rules/sub/below.ml", + "below.up_from_a_subdirectory", "at_the_root x", 0, "src/rules/rootimport.ml", + "rootimport.at_the_root", "Function", 1, "let at_the_root x = x"), + SL_CALL_AT("unit-with-an-interface-under-unqualified", "src/rules/top.ml", + "top.down_to_a_unit_with_an_interface", "Paired.in_a_subdirectory x", 0, + "src/rules/sub/paired.ml", "paired.in_a_subdirectory", "Function", 1, + "let in_a_subdirectory x = x"), + SL_CALL_AT("public-name-with-no-name", "src/rules/sub/below.ml", + "below.public_name_is_the_library", "Publiclib.from_public x", 0, + "libs/publiclib/publiclib.ml", "publiclib.from_public", "Function", 1, + "let from_public x = x"), + SL_CALL_AT("name-beside-a-public-name", "src/rules/sub/below.ml", + "below.name_beside_a_public_name", "Real_name.from_real x", 0, + "libs/named/real_name.ml", "real_name.from_real", "Function", 1, + "let from_real x = x"), + }; + static const RQ_Forbidden forbidden[] = { + SL_DENY_AT("dotted-public-name-is-no-module", "src/rules/sub/below.ml", + "below.no_dotted_public_name", "Dotted.from_dotted x", 0), + SL_DENY_AT("public-name-beside-a-name", "src/rules/sub/below.ml", + "below.no_public_beside_a_name", "Other_name.from_real x", 0), + SL_DENY_AT("no-climb-without-include-subdirs", "src/plain/sub/inner.ml", + "inner.no_climb_without_subdirs", "Top.at_top x", 0), + }; + ASSERT(sl_check("ocaml-subdirs", ".ml", "let filler x = x\n", sources, + sizeof(sources) / sizeof(sources[0]), expected, + sizeof(expected) / sizeof(expected[0]), forbidden, + sizeof(forbidden) / sizeof(forbidden[0]))); + PASS(); +} + +/* An include is read under the opens in force. One in an interface that cannot be read leaves + * the implementation to answer. */ +static const char ML4_BELOW[] = "open Base_lib\n" + "\n" + "include Util\n" + "\n" + "module Util = struct\n" + " include Util\n" + " let own_after x = x\n" + "end\n" + "\n" + "let through_an_opened_include x = from_util x\n" + "let through_a_nested_include x = Util.from_util x\n" + "let declared_after_the_include x = Util.own_after x\n"; +static const char ML4_IFACE_ML[] = "let declared x = x\n"; +static const char ML4_IFACE_MLI[] = "include Some_sig with type t := t\n" + "val declared : int -> int\n"; +static const char ML4_USER[] = "let through_the_implementation x = Iface.declared x\n" + "let no_such_member x = Iface.absent x\n"; + +TEST(resolver_source_ocaml_includes_under_opens_and_interfaces) { + static const RQ_Source sources[] = { + {.path = "libs/base_lib/dune", .text = "(library\n (name base_lib))\n"}, + {.path = "libs/base_lib/base_lib.ml", .text = "module Util = Util\n"}, + {.path = "libs/base_lib/util.ml", .text = "let from_util x = x\n"}, + {.path = "src/below.ml", .text = ML4_BELOW}, + {.path = "src/iface.ml", .text = ML4_IFACE_ML}, + {.path = "src/iface.mli", .text = ML4_IFACE_MLI}, + {.path = "src/user.ml", .text = ML4_USER}, + }; + static const RQ_Expected expected[] = { + SL_CALL_AT("include-read-under-an-open", "src/below.ml", "below.through_an_opened_include", + "from_util x", 0, "libs/base_lib/util.ml", "util.from_util", "Function", 1, + "let from_util x = x"), + SL_CALL_AT("nested-include-of-its-own-name", "src/below.ml", + "below.through_a_nested_include", "Util.from_util x", 0, "libs/base_lib/util.ml", + "util.from_util", "Function", 1, "let from_util x = x"), + SL_CALL_AT("declared-after-an-include", "src/below.ml", + "below.declared_after_the_include", "Util.own_after x", 0, "src/below.ml", + "below.Util.own_after", "Function", 7, " let own_after x = x"), + SL_CALL_AT("interface-include-adds-no-doubt", "src/user.ml", + "user.through_the_implementation", "Iface.declared x", 0, "src/iface.ml", + "iface.declared", "Function", 1, "let declared x = x"), + }; + static const RQ_Forbidden forbidden[] = { + SL_DENY_AT("member-absent-from-the-implementation", "src/user.ml", "user.no_such_member", + "Iface.absent x", 0), + }; + ASSERT(sl_check("ocaml-includes", ".ml", "let filler x = x\n", sources, + sizeof(sources) / sizeof(sources[0]), expected, + sizeof(expected) / sizeof(expected[0]), forbidden, + sizeof(forbidden) / sizeof(forbidden[0]))); + PASS(); +} + +/* What a functor's result has, what its parameter has, what an interface shows. */ +static const char ML5_MONAD[] = "module type Basic = Monad_intf.Basic\n" + "\n" + "module Make (M : Basic) = struct\n" + " include M\n" + "\n" + " let map t ~f = bind t ~f:(fun x -> return (f x))\n" + "\n" + " module O = struct\n" + " let ( let* ) t f = bind t ~f\n" + " let both a b = map a ~f:(fun x -> x, b)\n" + " end\n" + "end\n"; +static const char ML5_MONAD_INTF[] = "module type Basic = sig\n" + " type 'a t\n" + "\n" + " val return : 'a -> 'a t\n" + " val bind : 'a t -> f:('a -> 'b t) -> 'b t\n" + "end\n"; +static const char ML5_RESULT_LIKE[] = "open Core_lib\n" + "\n" + "let return x = Ok x\n" + "let bind t ~f = Result.bind t f\n" + "\n" + "include Monad.Make (struct\n" + " type nonrec 'a t = ('a, string) result\n" + "\n" + " let return = return\n" + " let bind = bind\n" + " end)\n" + "\n" + "let after_the_application x = Helper.run x\n" + "let what_the_functor_made x = map x ~f:Helper.run\n"; +static const char ML5_WRAP[] = "module Make (Helper : sig val run : int -> int end) = struct\n" + " let through_the_parameter x = Helper.run x\n" + "end\n"; +static const char ML5_COMPARED[] = "let compare a b = a - b\n" + "\n" + "include Core_lib.Comparable.Make (struct\n" + " type t = int\n" + "\n" + " let compare a b = a + b\n" + " let uses_its_own a b = compare a b\n" + " end)\n"; +static const char ML5_APPLIED[] = "module C = Core_lib.Comparable.Make (struct\n" + " type t = int\n" + "\n" + " let compare = Stdlib.compare\n" + " let uses_a_helper x = Core_lib.Helper.run x\n" + " end)\n" + "\n" + "let through_an_applied_module a b = C.max_of a b\n"; +static const char ML5_LATER[] = "open Core_lib\n" + "module Helper = struct let run x = x + 1 end\n" + "module Alias = Helper\n" + "\n" + "let through_the_binding_below_the_open x = Alias.run x\n"; +static const char ML5_STRUCT[] = "open struct\n" + " open Outside\n" + " let private_helper x = x\n" + "end\n" + "\n" + "let after_an_open_struct x = private_helper x\n" + "let nothing_is_left_open x = Core_lib.Helper.run x\n" + "\n" + "include struct\n" + " open Outside\n" + " let another x = x\n" + "end\n" + "\n" + "let an_inlined_structs_open_ends x = Core_lib.Helper.run x\n" + "\n" + "include struct\n" + " include Outside\n" + "end\n" + "\n" + "let what_it_includes_stays x = Core_lib.Helper.run x\n"; +#define ML5_HELPER_RUN(id, file, caller, text, at) \ + SL_CALL_AT(id, file, caller, text, at, "libs/core_lib/helper.ml", "helper.run", "Function", 1, \ + "let run x = x") + +TEST(resolver_source_ocaml_functors_module_types_and_interfaces) { + static const RQ_Source sources[] = { + {.path = "libs/core_lib/dune", .text = "(library\n (name core_lib))\n"}, + {.path = "libs/core_lib/core_lib.ml", + .text = "module Comparable = Comparable\n" + "module Helper = Helper\n" + "module Hidden = Hidden\n" + "module List = List\n" + "module Listed = Listed\n" + "module Loosely = Loosely\n" + "module Monad = Monad\n" + "module Opaque = Opaque\n" + "module Replaced = Replaced\n" + "module Holds = Holds\n" + "module Pp = Pp\n" + "module Sealed = Sealed\n" + "module Unsealed = Unsealed\n" + "module Sexp0 = Sexp0\n"}, + {.path = "libs/core_lib/helper.ml", .text = "let run x = x\n"}, + {.path = "libs/core_lib/monad.ml", .text = ML5_MONAD}, + {.path = "libs/core_lib/monad_intf.ml", .text = ML5_MONAD_INTF}, + {.path = "libs/core_lib/comparable.ml", + .text = "module Make (Key : sig type t val compare : t -> t -> int end) = struct\n" + " let max_of a b = if Key.compare a b > 0 then a else b\n" + "end\n"}, + {.path = "libs/core_lib/wrap.ml", .text = ML5_WRAP}, + {.path = "libs/core_lib/opaque.ml", + .text = "module Make (X : Outside.S) = struct\n include X\nend\n"}, + {.path = "libs/core_lib/holds.ml", + .text = "module Make (X : sig module Helper : sig val run : int -> int end end) = struct\n" + " include X\n" + "end\n"}, + {.path = "libs/core_lib/hidden.ml", + .text = "module Helper = struct let run x = x + 1 end\n" + "let shown x = x\n" + "let not_shown x = x\n"}, + {.path = "libs/core_lib/hidden.mli", .text = "val shown : int -> int\n"}, + {.path = "libs/core_lib/sealed.ml", .text = "include Outside\nlet shown x = x\n"}, + {.path = "libs/core_lib/sealed.mli", .text = "val shown : int -> int\n"}, + {.path = "libs/core_lib/unsealed.ml", .text = "include Outside\nlet shown x = x\n"}, + {.path = "libs/core_lib/listed.ml", + .text = "module Inner = struct let in_a_listed_module x = x end\n"}, + {.path = "libs/core_lib/listed.mli", + .text = "module Inner : module type of Somewhere_else\n"}, + {.path = "src/listed_user.ml", + .text = "let a_listed_module_of_no_known_type x = Core_lib.Listed.Inner.in_a_listed_module " + "x\n"}, + {.path = "libs/core_lib/loosely.ml", .text = "let shown x = x\n"}, + {.path = "libs/core_lib/loosely.mli", + .text = "include Some_sig with type t := t\nval shown : int -> int\n"}, + {.path = "src/loosely_user.ml", + .text = "open Core_lib\n" + "open Loosely\n" + "\n" + "let closed_by_what_it_implements x = Helper.run x\n"}, + {.path = "libs/core_lib/replaced.ml", + .text = "let early x = x\n" + "include String\n" + "include Comparable.Make (struct type t = int let compare a b = a - b end)\n" + "let late x = x\n"}, + {.path = "src/replaced_user.ml", + .text = "let the_latest_binding_is_known a b = Core_lib.Replaced.max_of a b\n" + "let declared_below_what_is_unknown x = Core_lib.Replaced.late x\n" + "let an_unknown_include_may_replace_it x = Core_lib.Replaced.early x\n"}, + {.path = "libs/core_lib/sexp0.ml", + .text = "module Decoder = struct let decode x = x end\n"}, + {.path = "libs/core_lib/list.ml", .text = "include List\nlet own_list x = x\n"}, + {.path = "libs/core_lib/pp.ml", .text = "include Pp\nlet own_pp x = x\n"}, + {.path = "libs/pp/dune", .text = "(library\n (name pp))\n"}, + {.path = "libs/pp/pp.ml", .text = "let text x = x\n"}, + /* A cram test's directory holds a project of its own, by any name. */ + {.path = "test/cases/one.t/dune", .text = "(library\n (name core_lib))\n"}, + {.path = "test/cases/one.t/core_lib.ml", .text = "let in_a_cram_test x = x\n"}, + {.path = "src/result_like.ml", .text = ML5_RESULT_LIKE}, + {.path = "src/user.ml", + .text = "open Core_lib\n" + "open Result_like.O\n" + "\n" + "let below_a_functors_module x = Helper.run x\n" + "let from_a_functors_module a b = both a b\n"}, + {.path = "src/opaque_user.ml", + .text = "open Core_lib\n" + "include Opaque.Make (struct end)\n" + "\n" + "let a_parameter_no_one_can_read x = Helper.run x\n"}, + {.path = "src/holds_user.ml", + .text = "open Core_lib\n" + "include Holds.Make (struct module Helper = struct let run x = x end end)\n" + "\n" + "let a_parameter_that_says_it_has_it x = Helper.run x\n"}, + {.path = "src/compared.ml", .text = ML5_COMPARED}, + {.path = "src/applied.ml", .text = ML5_APPLIED}, + {.path = "src/later.ml", .text = ML5_LATER}, + {.path = "src/opened_struct.ml", .text = ML5_STRUCT}, + {.path = "src/startup.ml", + .text = "let twice x = Core_lib.Helper.run x\n" + "let twice x = Core_lib.Helper.run x\n"}, + {.path = "src/hidden_user.ml", + .text = "open Core_lib\n" + "let not_shown x = x\n" + "open Hidden\n" + "\n" + "let past_what_an_interface_keeps x = Helper.run x\n" + "let earlier_name_is_not_replaced x = not_shown x\n" + "let what_an_interface_shows x = shown x\n"}, + {.path = "src/sealed_user.ml", + .text = "open Core_lib\n" + "open Sealed\n" + "\n" + "let through_a_sealed_open x = Helper.run x\n"}, + {.path = "src/unsealed_user.ml", + .text = "open Core_lib\n" + "open Unsealed\n" + "\n" + "let an_include_with_no_interface x = Helper.run x\n"}, + {.path = "src/import.ml", + .text = "include Core_lib\n" + "\n" + "include struct\n" + " open Sexp0\n" + " module Decoder = Decoder\n" + "end\n"}, + {.path = "src/decoding.ml", + .text = "open Import\n" + "open Decoder\n" + "\n" + "let through_an_include_above x = decode x\n" + "let past_it x = Helper.run x\n"}, + {.path = "src/pp_user.ml", .text = "let not_the_unit_itself x = Core_lib.Pp.text x\n"}, + {.path = "src/memprof.ml", .text = "let start x = x\n"}, + {.path = "src/printing.ml", + .text = "open Core_lib\n" + "open Helper\n" + "open Printf\n" + "\n" + "let a_standard_module_holds_no_module x = Helper.run x\n" + "let its_values_are_not_listed x = run x\n"}, + {.path = "src/collecting.ml", + .text = "open Gc\n" + "\n" + "let a_standard_module_holds_this_one x = Memprof.start x\n"}, + {.path = "src/list_user.ml", + .text = "open Core_lib\n" + "open List\n" + "\n" + "let through_a_list_of_its_own x = Helper.run x\n" + "let its_own_value x = own_list x\n"}, + }; + static const RQ_Expected expected[] = { + ML5_HELPER_RUN("functor-result-holds-no-such-module", "src/result_like.ml", + "result_like.after_the_application", "Helper.run x", 0), + SL_CALL_AT("functor-result-value", "src/result_like.ml", + "result_like.what_the_functor_made", "map x ~f:Helper.run", 0, + "libs/core_lib/monad.ml", "monad.Make.map", "Function", 6, + " let map t ~f = bind t ~f:(fun x -> return (f x))"), + ML5_HELPER_RUN("open-of-a-module-a-functor-makes", "src/user.ml", + "user.below_a_functors_module", "Helper.run x", 0), + SL_CALL_AT("value-of-a-module-a-functor-makes", "src/user.ml", + "user.from_a_functors_module", "both a b", 0, "libs/core_lib/monad.ml", + "monad.Make.O.both", "Function", 10, + " let both a b = map a ~f:(fun x -> x, b)"), + SL_CALL_AT("applied-module", "src/applied.ml", "applied.through_an_applied_module", + "C.max_of a b", 0, "libs/core_lib/comparable.ml", "comparable.Make.max_of", + "Function", 2, " let max_of a b = if Key.compare a b > 0 then a else b"), + ML5_HELPER_RUN("inside-a-functors-argument", "src/applied.ml", "applied.C.uses_a_helper", + "Core_lib.Helper.run x", 0), + SL_CALL_AT("binding-below-an-open-comes-first", "src/later.ml", + "later.through_the_binding_below_the_open", "Alias.run x", 0, "src/later.ml", + "later.Helper.run", "Function", 2, + "module Helper = struct let run x = x + 1 end"), + SL_CALL_AT("open-struct-names", "src/opened_struct.ml", + "opened_struct.after_an_open_struct", "private_helper x", 1, + "src/opened_struct.ml", "opened_struct.private_helper", "Function", 3, + " let private_helper x = x"), + ML5_HELPER_RUN("an-inlined-structs-open-ends", "src/opened_struct.ml", + "opened_struct.an_inlined_structs_open_ends", "Core_lib.Helper.run x", 1), + ML5_HELPER_RUN("open-struct-leaves-nothing-open", "src/opened_struct.ml", + "opened_struct.nothing_is_left_open", "Core_lib.Helper.run x", 0), + ML5_HELPER_RUN("past-what-an-interface-keeps", "src/hidden_user.ml", + "hidden_user.past_what_an_interface_keeps", "Helper.run x", 0), + SL_CALL_AT("earlier-name-not-replaced-by-a-hidden-one", "src/hidden_user.ml", + "hidden_user.earlier_name_is_not_replaced", "not_shown x", 1, + "src/hidden_user.ml", "hidden_user.not_shown", "Function", 2, + "let not_shown x = x"), + SL_CALL_AT("what-an-interface-shows", "src/hidden_user.ml", + "hidden_user.what_an_interface_shows", "shown x", 2, "libs/core_lib/hidden.ml", + "hidden.shown", "Function", 2, "let shown x = x"), + SL_CALL_AT("a-listed-module-of-no-known-type", "src/listed_user.ml", + "listed_user.a_listed_module_of_no_known_type", + "Core_lib.Listed.Inner.in_a_listed_module x", 0, "libs/core_lib/listed.ml", + "listed.Inner.in_a_listed_module", "Function", 1, + "module Inner = struct let in_a_listed_module x = x end"), + ML5_HELPER_RUN("a-closed-implementation-closes-the-unit", "src/loosely_user.ml", + "loosely_user.closed_by_what_it_implements", "Helper.run x", 0), + ML5_HELPER_RUN("a-closed-interface-closes-the-unit", "src/sealed_user.ml", + "sealed_user.through_a_sealed_open", "Helper.run x", 0), + SL_CALL_AT("an-include-above-reaches-an-alias", "src/decoding.ml", + "decoding.through_an_include_above", "decode x", 0, "libs/core_lib/sexp0.ml", + "sexp0.Decoder.decode", "Function", 1, + "module Decoder = struct let decode x = x end"), + ML5_HELPER_RUN("past-the-opens-of-an-include", "src/decoding.ml", "decoding.past_it", + "Helper.run x", 0), + SL_CALL_AT("a-unit-is-not-in-scope-in-itself", "src/pp_user.ml", + "pp_user.not_the_unit_itself", "Core_lib.Pp.text x", 0, "libs/pp/pp.ml", + "pp.text", "Function", 1, "let text x = x"), + SL_CALL_AT("a-functors-binding-below-an-unknown-include", "src/replaced_user.ml", + "replaced_user.the_latest_binding_is_known", "Core_lib.Replaced.max_of a b", 0, + "libs/core_lib/comparable.ml", "comparable.Make.max_of", "Function", 2, + " let max_of a b = if Key.compare a b > 0 then a else b"), + SL_CALL_AT("own-binding-below-an-unknown-include", "src/replaced_user.ml", + "replaced_user.declared_below_what_is_unknown", "Core_lib.Replaced.late x", 0, + "libs/core_lib/replaced.ml", "replaced.late", "Function", 4, "let late x = x"), + ML5_HELPER_RUN("standard-module-holds-no-module", "src/printing.ml", + "printing.a_standard_module_holds_no_module", "Helper.run x", 0), + ML5_HELPER_RUN("include-of-the-standard-one", "src/list_user.ml", + "list_user.through_a_list_of_its_own", "Helper.run x", 0), + SL_CALL_AT("own-value-beside-it", "src/list_user.ml", "list_user.its_own_value", + "own_list x", 0, "libs/core_lib/list.ml", "list.own_list", "Function", 2, + "let own_list x = x"), + }; + static const RQ_Forbidden forbidden[] = { + SL_DENY_AT("a-parameter-is-not-the-unit-of-its-name", "libs/core_lib/wrap.ml", + "wrap.Make.through_the_parameter", "Helper.run x", 0), + SL_DENY_AT("a-parameter-no-one-can-read", "src/opaque_user.ml", + "opaque_user.a_parameter_no_one_can_read", "Helper.run x", 0), + SL_DENY_AT("a-parameter-that-says-it-has-it", "src/holds_user.ml", + "holds_user.a_parameter_that_says_it_has_it", "Helper.run x", 0), + SL_DENY_AT("a-functor-arguments-names-are-its-own", "src/compared.ml", + "compared.uses_its_own", "compare a b", 2), + SL_DENY_AT("two-lets-of-one-name-are-still-two", "src/startup.ml", "startup.twice", + "Core_lib.Helper.run x", 0), + SL_DENY_AT("the-second-of-two-lets-of-one-name", "src/startup.ml", "startup.twice", + "Core_lib.Helper.run x", 1), + SL_DENY_AT("what-an-inlined-struct-includes-stays", "src/opened_struct.ml", + "opened_struct.what_it_includes_stays", "Core_lib.Helper.run x", 2), + SL_DENY_AT("an-unknown-include-below-a-binding", "src/replaced_user.ml", + "replaced_user.an_unknown_include_may_replace_it", "Core_lib.Replaced.early x", + 0), + SL_DENY_AT("an-include-with-no-interface", "src/unsealed_user.ml", + "unsealed_user.an_include_with_no_interface", "Helper.run x", 0), + SL_DENY_AT("standard-values-are-not-listed", "src/printing.ml", + "printing.its_values_are_not_listed", "run x", 0), + SL_DENY_AT("a-standard-module-holds-this-one", "src/collecting.ml", + "collecting.a_standard_module_holds_this_one", "Memprof.start x", 0), + }; + ASSERT(sl_check("ocaml-functors", ".ml", "let filler x = x\n", sources, + sizeof(sources) / sizeof(sources[0]), expected, + sizeof(expected) / sizeof(expected[0]), forbidden, + sizeof(forbidden) / sizeof(forbidden[0]))); + PASS(); +} + +/* A function's module is part of its name. */ +static const char ML3_ARCHIVE[] = "module Name = struct\n" + " let dll_file x = x\n" + " let in_name x = dll_file x\n" + " module Deep = struct\n" + " let dll_file x = x\n" + " let in_deep x = dll_file x\n" + " let up_to_a_module x = in_name x\n" + " end\n" + "end\n" + "\n" + "module Make (X : sig end) = struct\n" + " let dll_file x = x\n" + " let in_functor x = dll_file x\n" + "end\n" + "\n" + "let dll_file x = Name.dll_file x\n" + "let at_the_top x = dll_file x\n" + "let through_two_modules x = Name.Deep.dll_file x\n" + "module Applied = Make (struct end)\n" + "let through_a_functor_application x = Applied.dll_file x\n" + "let no_such_member_of_a_result x = Applied.not_there x\n"; +static const char ML3_USER[] = "let qualified x = Archive.Name.dll_file x\n" + "let top_of_the_unit x = Archive.dll_file x\n"; +#define ML3_FILE "src/archive.ml" +#define ML3_CALL(id, caller, text, at, tsuffix, line, decl) \ + SL_CALL_AT(id, ML3_FILE, caller, text, at, ML3_FILE, tsuffix, "Function", line, decl) + +TEST(resolver_source_ocaml_a_module_is_part_of_a_functions_name) { + static const RQ_Source sources[] = { + {.path = ML3_FILE, .text = ML3_ARCHIVE}, + {.path = "src/user.ml", .text = ML3_USER}, + }; + static const RQ_Expected expected[] = { + ML3_CALL("own-module", "Name.in_name", "dll_file x", 1, "archive.Name.dll_file", 2, + " let dll_file x = x"), + ML3_CALL("inner-module-shadows-its-parent", "Deep.in_deep", "dll_file x", 3, + "Name.Deep.dll_file", 5, " let dll_file x = x"), + ML3_CALL("enclosing-module-from-a-nested-one", "Deep.up_to_a_module", "in_name x", 1, + "archive.Name.in_name", 3, " let in_name x = dll_file x"), + ML3_CALL("functor-body-reads-its-own", "Make.in_functor", "dll_file x", 5, + "archive.Make.dll_file", 12, " let dll_file x = x"), + ML3_CALL("the-unit-reads-a-nested-module", "src.archive.dll_file", "Name.dll_file x", 0, + "archive.Name.dll_file", 2, " let dll_file x = x"), + ML3_CALL("top-level-let-of-the-same-name", "src.archive.at_the_top", "dll_file x", 8, + "src.archive.dll_file", 16, "let dll_file x = Name.dll_file x"), + ML3_CALL("two-modules-down", "src.archive.through_two_modules", "Name.Deep.dll_file x", 0, + "Name.Deep.dll_file", 5, " let dll_file x = x"), + ML3_CALL("a-functor-result-holds-what-its-struct-holds", + "src.archive.through_a_functor_application", "Applied.dll_file x", 0, + "archive.Make.dll_file", 12, " let dll_file x = x"), + SL_CALL_AT("from-another-unit", "src/user.ml", "user.qualified", "Archive.Name.dll_file x", + 0, ML3_FILE, "archive.Name.dll_file", "Function", 2, " let dll_file x = x"), + SL_CALL_AT("the-units-own-from-another-unit", "src/user.ml", "user.top_of_the_unit", + "Archive.dll_file x", 0, ML3_FILE, "src.archive.dll_file", "Function", 16, + "let dll_file x = Name.dll_file x"), + }; + static const RQ_Forbidden forbidden[] = { + SL_DENY_AT("a-functor-result-holds-nothing-else", ML3_FILE, + "archive.no_such_member_of_a_result", "Applied.not_there x", 0), + }; + ASSERT(sl_check("ocaml-module-names", ".ml", "let filler = 1\n", sources, + sizeof(sources) / sizeof(sources[0]), expected, + sizeof(expected) / sizeof(expected[0]), forbidden, + sizeof(forbidden) / sizeof(forbidden[0]))); + PASS(); +} + +/* ── Haskell: imports by header, constructors ──────────────────────── */ + +static const char HS_TYPES[] = + "module Lib.Types\n" + " ( Open(..)\n" + " , Abstract\n" + " , mk\n" + " ) where\n" + "\n" + "data Open = Open Int | Other Int\n" + "\n" + "data Abstract = Abstract Int\n" + "\n" + "mk :: Int -> Abstract\n" + "mk = Abstract\n"; +static const char HS_USER[] = + "module Lib.User where\n" + "\n" + "import Lib.Types (Open(..), Abstract, mk)\n" + "import Lib.Missing\n" + "\n" + "data Local = Local Int | Renamed Int\n" + "\n" + "ownConstructor :: Int -> Local\n" + "ownConstructor x = Local x\n" + "\n" + "importedConstructor :: Int -> Open\n" + "importedConstructor x = Open x\n" + "\n" + "noConstructorWithoutNode :: Int -> Local\n" + "noConstructorWithoutNode x = Renamed x\n" + "\n" + "noOtherConstructor :: Int -> Open\n" + "noOtherConstructor x = Other x\n" + "\n" + "noPatternIsACall :: Local -> Int\n" + "noPatternIsACall (Local x) = x\n" + "noPatternIsACall (Renamed x) = x\n" + "\n" + "noCaseIsACall :: Local -> Int\n" + "noCaseIsACall v = case v of\n" + " Local x -> x\n" + " Renamed x -> x\n"; + +TEST(resolver_source_haskell_constructors) { + static const RQ_Source sources[] = {{.path = "src/Lib/Types.hs", .text = HS_TYPES}, + {.path = "src/Lib/User.hs", .text = HS_USER}}; + static const RQ_Expected expected[] = { + SL_CALL_AT("constructor-named-as-its-type", "src/Lib/User.hs", "User.ownConstructor", "Local x", 0, "src/Lib/User.hs", "User.Local", "Class", 6, + "data Local = Local Int | Renamed Int"), + SL_CALL_AT("imported-constructor-listed-with-children", "src/Lib/User.hs", "User.importedConstructor", "Open x", 0, "src/Lib/Types.hs", "Types.Open", "Class", 7, + "data Open = Open Int | Other Int"), + {.case_id = "import-names-the-module-its-header-declares", + .site = {.file = "src/Lib/User.hs", .caller = "User.hs.__file__", .needle = "Lib.Types"}, + .relation = "IMPORTS", + .target = {.file = "src/Lib/Types.hs", + .suffix = "Types.hs.__file__", + .label = "File", + .declaration_line = 1, + .declaration_text = "module Lib.Types"}}, + }; + static const RQ_Forbidden forbidden[] = { + SL_DENY_AT("constructor-with-no-node", "src/Lib/User.hs", "User.noConstructorWithoutNode", "Renamed x", 0), + SL_DENY_AT("other-constructor-with-no-node", "src/Lib/User.hs", "User.noOtherConstructor", "Other x", 0), + SL_DENY_AT("function-pattern-matches", "src/Lib/User.hs", "User.noPatternIsACall", "Local x", 1), + SL_DENY_AT("case-pattern-matches", "src/Lib/User.hs", "User.noCaseIsACall", "Local x", 2), + }; + ASSERT(sl_check("haskell-constructors", ".hs", "module Filler where\nf = 1\n", sources, + sizeof(sources) / sizeof(sources[0]), expected, + sizeof(expected) / sizeof(expected[0]), forbidden, + sizeof(forbidden) / sizeof(forbidden[0]))); + PASS(); +} + +static const char HS_KINDS[] = "module Lib.Kinds\n" + " ( Kind(..)\n" + " , Partial(one)\n" + " , Hidden\n" + " , localUse\n" + " ) where\n" + "\n" + "class Kind a where\n" + " status :: a -> Int\n" + " code, level :: a -> Int\n" + " level x = 1\n" + "\n" + "class Partial a where\n" + " one :: a -> Int\n" + " two :: a -> Int\n" + "\n" + "class Hidden a where\n" + " hid :: a -> Int\n" + "\n" + "localUse :: Kind a => a -> Int\n" + "localUse x = status x\n" + "\n" + "instance Kind Int where\n" + " status x = x\n" + " code x = x\n" + "\n" + "instance Partial Int where\n" + " one x = x\n" + " two x = x\n" + "\n" + "instance Hidden Int where\n" + " hid x = x\n"; +static const char HS_USE_KINDS[] = "module Lib.UseKinds where\n" + "\n" + "import Lib.Kinds (Kind(..), Partial(one))\n" + "\n" + "viaAll x = status x\n" + "viaAllBinding x = level x\n" + "viaListed x = one x\n" + "noNotListed x = two x\n" + "noHidden x = hid x\n"; +static const char HS_WHOLE[] = "module Lib.Whole where\n" + "\n" + "import Lib.Kinds\n" + "\n" + "viaWhole x = code x\n" + "noPartialChild x = two x\n" + "noHiddenWhole x = hid x\n"; +#define HS_KINDS_FILE "src/Lib/Kinds.hs" +#define HS_METHOD(id, file, caller, text, method, line, decl) \ + SL_CALL_AT(id, file, caller, text, 0, HS_KINDS_FILE, "Kinds." method, "Function", line, decl) + +TEST(resolver_source_haskell_class_methods) { + static const RQ_Source sources[] = {{.path = HS_KINDS_FILE, .text = HS_KINDS}, + {.path = "src/Lib/UseKinds.hs", .text = HS_USE_KINDS}, + {.path = "src/Lib/Whole.hs", .text = HS_WHOLE}}; + static const RQ_Expected expected[] = { + HS_METHOD("method-in-its-own-module", HS_KINDS_FILE, "Kinds.localUse", "status x", "status", + 24, " status x = x"), + HS_METHOD("method-through-children-all", "src/Lib/UseKinds.hs", "UseKinds.viaAll", + "status x", "status", 24, " status x = x"), + HS_METHOD("method-of-a-binding-list", "src/Lib/UseKinds.hs", "UseKinds.viaAllBinding", + "level x", "level", 11, " level x = 1"), + HS_METHOD("method-listed-by-name", "src/Lib/UseKinds.hs", "UseKinds.viaListed", "one x", + "one", 28, " one x = x"), + HS_METHOD("method-through-a-whole-import", "src/Lib/Whole.hs", "Whole.viaWhole", "code x", + "code", 25, " code x = x"), + }; + static const RQ_Forbidden forbidden[] = { + SL_DENY_AT("method-not-in-the-import-list", "src/Lib/UseKinds.hs", "UseKinds.noNotListed", + "two x", 0), + SL_DENY_AT("class-exported-without-children", "src/Lib/UseKinds.hs", "UseKinds.noHidden", + "hid x", 0), + SL_DENY_AT("method-not-in-the-export-list", "src/Lib/Whole.hs", "Whole.noPartialChild", + "two x", 0), + SL_DENY_AT("hidden-method-with-a-whole-import", "src/Lib/Whole.hs", "Whole.noHiddenWhole", + "hid x", 0), + }; + ASSERT(sl_check("haskell-class-methods", ".hs", "module Filler where\nf = 1\n", sources, + sizeof(sources) / sizeof(sources[0]), expected, + sizeof(expected) / sizeof(expected[0]), forbidden, + sizeof(forbidden) / sizeof(forbidden[0]))); + PASS(); +} + +/* ── A grammar's rejection stays where it is ───────────────────────── */ + +/* `@Inject() (...)` is the shape tree-sitter-scala rejects most in playframework: the + * rejection is in the class header, so the rest of the file still decides. */ +static const char SC_DAMAGED[] = + "package demo\n" + "\n" + "class Damaged @Inject() (a: Int)(implicit b: Long) {\n" + " def clean(): Int = helper()\n" + " def helper(): Int = 1\n" + " def damaged(): Int = {\n" + " val = 1\n" + " helper()\n" + " }\n" + "}\n"; +/* A stray closer moves everything after it to another scope. */ +static const char SC_UNBALANCED[] = + "package demo\n" + "\n" + "class Unbalanced {\n" + " def one(): Int = two()\n" + " def two(): Int = 1\n" + " }\n" + "}\n" + "class After {\n" + " def three(): Int = four()\n" + " def four(): Int = 1\n" + "}\n"; + +TEST(resolver_source_parse_damage_stays_where_it_is) { + static const RQ_Source sources[] = {{.path = "src/Damaged.scala", .text = SC_DAMAGED}, + {.path = "src/Unbalanced.scala", .text = SC_UNBALANCED}}; + static const RQ_Expected expected[] = { + SL_CALL_AT("clean-method-of-a-damaged-file-decides", "src/Damaged.scala", "Damaged.clean", + "helper()", 0, "src/Damaged.scala", "Damaged.helper", "Method", 5, + " def helper(): Int = 1"), + }; + static const RQ_Forbidden forbidden[] = { + SL_DENY_AT("damaged-method-decides-nothing", "src/Damaged.scala", "Damaged.damaged", + "helper()", 2), + SL_DENY_AT("unbalanced-file-decides-nothing", "src/Unbalanced.scala", "After.three", "four()", + 0), + }; + ASSERT(sl_check("parse-damage", ".scala", "package filler\nobject F { def f(): Int = 1 }\n", + sources, sizeof(sources) / sizeof(sources[0]), expected, + sizeof(expected) / sizeof(expected[0]), forbidden, + sizeof(forbidden) / sizeof(forbidden[0]))); + PASS(); +} + /* ── Dart ──────────────────────────────────────────────────────────── */ static const char DART_MAIN[] = @@ -1307,6 +3583,253 @@ TEST(resolver_source_scala_packages_precedence_and_receivers) { PASS(); } +/* ── Scala: positions read, types written in other files ───────────── */ + +static const char SC2_LIB[] = + "package lib\n" + "\n" + "import lib.deep.Imported\n" + "import external.Elsewhere\n" + "\n" + "class Base {\n" + " def inherited(a: Int): Int = a\n" + " val limit: Int = 1\n" + "}\n" + "class FromImport extends Imported\n" + "class FromOutside extends Elsewhere\n" + "class Made {\n" + " def imported(): Imported = new Imported\n" + " def outside(): Elsewhere = null\n" + " def fresh = new Thing\n" + " def freshBlock = { val a = 1; new Thing }\n" + " def noMethod(a: Int) = new Thing\n" + "}\n" + "class Thing {\n" + " def go(): Int = 1\n" + " def gen[T](a: T): T = a\n" + " def +(other: Thing): Thing = this\n" + " def merge(other: Thing): Thing = this\n" + " def ::(other: Thing): Thing = this\n" + " def self: Thing = this\n" + " val mine: Int = 1\n" + " var counted: Int = 2\n" + "}\n" + "object Things {\n" + " val Shared: Int = 3\n" + " def empty: Thing = new Thing\n" + "}\n" + "object Mode {\n" + " case object Dev\n" + "}\n" + "class Elsewhere\n"; +static const char SC2_DEEP[] = "package lib.deep\n" + "\n" + "class Imported {\n" + " def deepMember(): Int = 1\n" + "}\n"; +static const char SC2_APP[] = + "package app\n" + "\n" + "import lib._\n" + "import lib.deep.Imported\n" + "\n" + "case class Conf(a: Int = 1)\n" + "class Host(conf: Conf = Conf(1)) extends Base {\n" + " def own(a: Int): Int = a\n" + " def own2(a: Int): Int = a\n" + " def shadow(a: Int): Int = a\n" + " val here: Int = 4\n" + " def viaDefault(c: Any = Conf(2)): Int = 1\n" + " def viaFor = for { a <- Option(own(1)); b = own(2) if b > own2(3) } yield a\n" + " def viaGeneric(t: Thing) = t.gen[Int](1)\n" + " def viaBareGeneric = make[Int](1)\n" + " def make[T](a: T): T = a\n" + " def viaInfix(a: Thing, b: Thing) = a merge b\n" + " def viaSymbol(a: Thing, b: Thing) = a + b\n" + " def viaRight(a: Thing, b: Thing) = a :: b\n" + " def viaInfixResult(a: Thing, b: Thing) = (a + b).go()\n" + " def viaImportedReturn(m: Made) = m.imported().deepMember()\n" + " def viaImportedBase(f: FromImport) = f.deepMember()\n" + " def viaInferredDef(m: Made) = m.fresh.go()\n" + " def viaInferredBlock(m: Made) = m.freshBlock.go()\n" + " def viaInferredCall(m: Made) = m.noMethod(1).go()\n" + " def viaEmpty = Things.empty.go()\n" + " def viaSelf(t: Thing) = t.self.go()\n" + " def thing(): Thing = new Thing\n" + " def readsMember = thing().mine\n" + " def readsVar = thing().counted\n" + " def readsObjectValue = Things.Shared\n" + " def readsCaseObject = Mode.Dev\n" + " def readsOwnValue = here\n" + " def readsInherited = limit\n" + " def readsTypePath(a: lib.deep.Imported): Int = 1\n" + " def noOutsideReturn(m: Made) = m.outside().deepMember()\n" + " def noOutsideBase(f: FromOutside) = f.deepMember()\n" + " def noMethodWithArgs(m: Made) = m.noMethod.go()\n" + " def noInfixAbsent(a: Thing, b: Thing) = a absent b\n" + " def noInfixUnknown(a: String, b: Thing) = a merge b\n" + " def noLocalMember(mine: Any) = mine\n" + " def noForBinding = for { here <- Option(1) } yield here\n" + " def noMemberOfOutside(s: String) = s.mine\n" + " def noAnonymousOwn = new Thing { def shadow(a: Int): Int = a; val v = shadow(1) }\n" + " def anonymousInherits = new Thing { val g = go() }\n" + " def noAnonymousOutside = new Elsewhere2 { val x = shadow(1) }\n" + " def anonymousArgument = new Thing2(shadow(2)) { }\n" + " def noCalledRead = thing().go()\n" + " def assignsNamed = own(a = 1)\n" + " def viaTypedPattern(x: Any) = x match { case e: Thing => e.go(); case _ => 0 }\n" + " def viaTypedLambda(l: Seq[Thing]) = l.map((y: Thing) => y.go())\n" + " def noUntypedLambda(l: Seq[Thing]) = l.map(z => z.go())\n" + "}\n" + "class Thing2(a: Int)\n"; +static const char SC2_SUITE[] = "package suite\n" + "\n" + "trait Common {\n" + " trait Tester {\n" + " def run(): Int = 1\n" + " }\n" + " def helper(): Int = 2\n" + "}\n" + "class Impl extends Common {\n" + " def make = new Tester {\n" + " val v = helper()\n" + " val w = run()\n" + " }\n" + " def typed(t: Tester) = t.run()\n" + "}\n"; +static const char SC2_SHADOW[] = "package app\n" + "\n" + "class Imported {\n" + " def deepMember(): Int = 2\n" + "}\n" + "class Local {\n" + " def readsDecoyPath(a: lib.deep.Imported): Int = 1\n" + "}\n"; +#define SC2_FILE "src/app/Host.scala" +#define SC2_LIB_FILE "src/lib/Lib.scala" +#define SC2_DEEP_FILE "src/lib/deep/Deep.scala" +#define SC2_CALL(id, method, text, tfile, tsuffix, label, tline, tdecl) \ + SL_CALL_AT(id, SC2_FILE, "Host." method, text, 0, tfile, tsuffix, label, tline, tdecl) +#define SC2_GO(id, method, text) \ + SC2_CALL(id, method, text, SC2_LIB_FILE, "Thing.go", "Method", 20, " def go(): Int = 1") +#define SC2_DEEP_MEMBER(id, method, text) \ + SC2_CALL(id, method, text, SC2_DEEP_FILE, "Imported.deepMember", "Method", 4, \ + " def deepMember(): Int = 1") +#define SC2_DENY(id, method, text) SL_DENY_AT(id, SC2_FILE, "Host." method, text, 0) + +TEST(resolver_source_scala_positions_and_types_written_in_other_files) { + static const RQ_Source sources[] = { + {.path = SC2_FILE, .text = SC2_APP}, + {.path = SC2_LIB_FILE, .text = SC2_LIB}, + {.path = SC2_DEEP_FILE, .text = SC2_DEEP}, + {.path = "src/app/Shadow.scala", .text = SC2_SHADOW}, + {.path = "src/suite/Suite.scala", .text = SC2_SUITE}, + }; + static const RQ_Expected expected[] = { + SC2_CALL("default-of-a-function-parameter", "viaDefault", "Conf(2)", SC2_FILE, "app.Host.Conf", + "Class", 6, "case class Conf(a: Int = 1)"), + SC2_CALL("generator-of-a-for", "viaFor", "own(1)", SC2_FILE, "Host.own", "Method", 8, + " def own(a: Int): Int = a"), + SC2_CALL("value-of-a-for", "viaFor", "own(2)", SC2_FILE, "Host.own", "Method", 8, + " def own(a: Int): Int = a"), + SC2_CALL("guard-of-a-for", "viaFor", "own2(3)", SC2_FILE, "Host.own2", "Method", 9, + " def own2(a: Int): Int = a"), + SC2_CALL("generic-member-call", "viaGeneric", "t.gen[Int](1)", SC2_LIB_FILE, "Thing.gen", + "Method", 21, " def gen[T](a: T): T = a"), + SC2_CALL("generic-bare-call", "viaBareGeneric", "make[Int](1)", SC2_FILE, "Host.make", + "Method", 16, " def make[T](a: T): T = a"), + SC2_CALL("named-infix", "viaInfix", "a merge b", SC2_LIB_FILE, "Thing.merge", "Method", 23, + " def merge(other: Thing): Thing = this"), + SC2_CALL("symbolic-infix", "viaSymbol", "a + b", SC2_LIB_FILE, "Thing.+", "Method", 22, + " def +(other: Thing): Thing = this"), + SC2_CALL("right-associative-infix", "viaRight", "a :: b", SC2_LIB_FILE, "Thing.::", "Method", + 24, " def ::(other: Thing): Thing = this"), + SL_CALL_AT("enclosing-template-from-its-anonymous-class", "src/suite/Suite.scala", + "Impl.make", "helper()", 1, "src/suite/Suite.scala", "Common.helper", "Method", 7, + " def helper(): Int = 2"), + SL_CALL_AT("member-of-an-inherited-type-in-its-anonymous-class", "src/suite/Suite.scala", + "Impl.make", "run()", 1, "src/suite/Suite.scala", "Tester.run", "Method", 5, + " def run(): Int = 1"), + SL_CALL_AT("parameter-of-an-inherited-type", "src/suite/Suite.scala", "Impl.typed", "t.run()", + 0, "src/suite/Suite.scala", "Tester.run", "Method", 5, " def run(): Int = 1"), + SC2_GO("result-of-an-infix", "viaInfixResult", "(a + b).go()"), + SC2_GO("pattern-says-its-type", "viaTypedPattern", "e.go()"), + SL_CALL_AT("lambda-parameter-says-its-type", SC2_FILE, "Host.viaTypedLambda", "y.go()", 1, + SC2_LIB_FILE, "Thing.go", "Method", 20, " def go(): Int = 1"), + SC2_DEEP_MEMBER("return-type-imported-in-its-file", "viaImportedReturn", + "m.imported().deepMember()"), + SC2_DEEP_MEMBER("base-class-imported-in-its-file", "viaImportedBase", "f.deepMember()"), + SC2_GO("def-of-a-new", "viaInferredDef", "m.fresh.go()"), + SC2_GO("def-of-a-block-ending-in-new", "viaInferredBlock", "m.freshBlock.go()"), + SC2_GO("call-of-a-def-of-a-new", "viaInferredCall", "m.noMethod(1).go()"), + SC2_GO("method-without-arguments-is-called", "viaEmpty", "Things.empty.go()"), + SC2_GO("member-without-arguments-is-called", "viaSelf", "t.self.go()"), + SL_CALL_AT("anonymous-class-inherits-its-base", SC2_FILE, "Host.anonymousInherits", "go()", + 7, SC2_LIB_FILE, "Thing.go", "Method", 20, " def go(): Int = 1"), + SC2_CALL("new-of-an-anonymous-class", "anonymousInherits", "new Thing { val g = go() }", + SC2_LIB_FILE, "lib.Lib.Thing", "Class", 19, "class Thing {"), + SC2_CALL("argument-of-an-anonymous-class-is-outside-it", "anonymousArgument", "shadow(2)", + SC2_FILE, "Host.shadow", "Method", 10, " def shadow(a: Int): Int = a"), + SC2_CALL("new-with-an-argument-and-a-body", "anonymousArgument", + "new Thing2(shadow(2)) { }", SC2_FILE, "app.Host.Thing2", "Class", 54, + "class Thing2(a: Int)"), + SC2_CALL("receiver-of-an-imported-return", "viaImportedReturn", "m.imported()", SC2_LIB_FILE, + "Made.imported", "Method", 13, " def imported(): Imported = new Imported"), + SC2_CALL("receiver-of-a-def-of-a-new", "viaInferredCall", "m.noMethod(1)", SC2_LIB_FILE, + "Made.noMethod", "Method", 17, " def noMethod(a: Int) = new Thing"), + SL_CALL_AT("infix-as-a-receiver", SC2_FILE, "Host.viaInfixResult", "a + b", 1, SC2_LIB_FILE, + "Thing.+", "Method", 22, " def +(other: Thing): Thing = this"), + SL_USE_AT("value-member-through-a-receiver", SC2_FILE, "Host.readsMember", "mine", 0, + SC2_LIB_FILE, "Lib.Thing.mine", "Variable", 26, " val mine: Int = 1"), + SL_USE_AT("var-member-through-a-receiver", SC2_FILE, "Host.readsVar", "counted", 0, + SC2_LIB_FILE, "Lib.Thing.counted", "Variable", 27, " var counted: Int = 2"), + SC2_CALL("new-of-an-anonymous-class-with-its-own-member", "noAnonymousOwn", + "new Thing { def shadow(a: Int): Int = a; val v = shadow(1) }", SC2_LIB_FILE, + "lib.Lib.Thing", "Class", 19, "class Thing {"), + SC2_CALL("receiver-of-an-outside-return", "noOutsideReturn", "m.outside()", SC2_LIB_FILE, + "Made.outside", "Method", 14, " def outside(): Elsewhere = null"), + SL_USE_AT("own-value-by-bare-name", SC2_FILE, "Host.readsOwnValue", "here", 1, SC2_FILE, + "Host.here", "Variable", 11, " val here: Int = 4"), + SL_USE_AT("inherited-value-by-bare-name", SC2_FILE, "Host.readsInherited", "limit", 0, + SC2_LIB_FILE, "Lib.Base.limit", "Variable", 8, " val limit: Int = 1"), + SL_USE_AT("a-type-path-names-its-own-type", SC2_FILE, "Host.readsTypePath", "Imported", 3, + SC2_DEEP_FILE, "deep.Deep.Imported", "Class", 3, "class Imported {"), + {.case_id = "a-type-path-is-a-type-use", + .site = {.file = SC2_FILE, + .caller = "Host.readsTypePath", + .needle = "lib.deep.Imported", + .occurrence_index = 1}, + .relation = "USES_TYPE", + .target = {.file = SC2_DEEP_FILE, + .suffix = "deep.Deep.Imported", + .label = "Class", + .declaration_line = 3, + .declaration_text = "class Imported {"}}, + }; + static const RQ_Forbidden forbidden[] = { + SC2_DENY("untyped-lambda-parameter", "noUntypedLambda", "z.go()"), + SC2_DENY("return-type-from-an-outside-import", "noOutsideReturn", + "m.outside().deepMember()"), + SL_DENY_AT("base-class-from-an-outside-import", SC2_FILE, "Host.noOutsideBase", + "f.deepMember()", 1), + SC2_DENY("a-method-with-arguments-is-no-receiver", "noMethodWithArgs", "m.noMethod.go()"), + SC2_DENY("infix-member-absent", "noInfixAbsent", "a absent b"), + SL_DENY_AT("infix-on-an-unknown-type", SC2_FILE, "Host.noInfixUnknown", "a merge b", 1), + SL_DENY_AT("bare-call-in-an-anonymous-class-with-an-outside-base", SC2_FILE, + "Host.noAnonymousOutside", "shadow(1)", 1), + SL_NO_USE_AT("parameter-hides-a-member", SC2_FILE, "Host.noLocalMember", "mine", 2), + SL_NO_USE_AT("for-binding-hides-a-member", SC2_FILE, "Host.noForBinding", "here", 3), + SL_NO_USE_AT("member-of-an-outside-type", SC2_FILE, "Host.noMemberOfOutside", "mine", 3), + SC2_DENY("own-member-of-an-anonymous-class", "noAnonymousOwn", "shadow(1)"), + SL_NO_USE_AT("a-called-member-is-not-also-a-read", SC2_FILE, "Host.noCalledRead", "go", 8), + }; + ASSERT(sl_check("scala-positions", ".scala", "package f\nobject F\n", sources, + sizeof(sources) / sizeof(sources[0]), expected, + sizeof(expected) / sizeof(expected[0]), forbidden, + sizeof(forbidden) / sizeof(forbidden[0]))); + PASS(); +} + /* ── Haskell ───────────────────────────────────────────────────────── */ static const char HS_MAIN[] = "module Main where\n" @@ -1436,7 +3959,7 @@ TEST(resolver_source_ocaml_sequential_scope_and_modules) { "let exported x = x"), ML_CALL("open-of-a-known-unit", "calls_open", "opened 6", "src/other.ml", "other.opened", 1, "let opened x = x"), - SL_USE_AT("module-is-not-in-scope-in-its-own-body", ML_FILE, "main.exported", + SL_USE_AT("module-is-not-in-scope-in-its-own-body", ML_FILE, "main.Lib.exported", "Lib.exported", 1, "src/lib.ml", "lib.exported", "Function", 1, "let exported x = x"), SL_USE_AT("function-read-as-a-value", ML_FILE, "main.uses_value", "local", 3, ML_FILE, @@ -1460,8 +3983,31 @@ SUITE(resolver_source_langs) { RUN_TEST(resolver_source_elixir_modules_aliases_imports_and_arity); RUN_TEST(resolver_source_erlang_module_arity_export_and_import); RUN_TEST(resolver_source_zig_containers_imports_and_declared_receivers); + RUN_TEST(resolver_source_zig_unwraps_captures_and_member_reads); + RUN_TEST(resolver_source_zig_a_type_is_read_in_the_file_that_writes_it); + RUN_TEST(resolver_source_scala_positions_and_types_written_in_other_files); RUN_TEST(resolver_source_perl_packages_exporter_and_method_resolution); RUN_TEST(resolver_source_ruby_ancestor_chain_and_self_rules); + RUN_TEST(resolver_source_ruby_writers_constructors_aliases_and_constants); + RUN_TEST(resolver_source_ruby_block_keeps_self_only_for_a_method_that_lets_it); + RUN_TEST(resolver_source_ruby_project_each_that_rebinds_blocks_the_core_rule); + RUN_TEST(resolver_source_ruby_constants_and_file_locals_are_references); + RUN_TEST(resolver_source_ruby_bare_new_and_index_operators); + RUN_TEST(resolver_source_ruby_super_selects_what_follows_the_own_step); + RUN_TEST(resolver_source_ruby_superclass_reaches_the_graph_on_both_routes); + RUN_TEST(resolver_source_elixir_captures_mentions_protocols_and_erlang_atoms); + RUN_TEST(resolver_source_elixir_use_brings_what_using_returns); + RUN_TEST(resolver_source_erlang_fun_references); + RUN_TEST(resolver_source_perl_invocant_reexports_and_code_references); + RUN_TEST(resolver_source_perl_list_shift_and_overload); + RUN_TEST(resolver_source_ocaml_dune_libraries_includes_and_scoped_opens); + RUN_TEST(resolver_source_ocaml_subdirectories_and_public_names); + RUN_TEST(resolver_source_ocaml_a_module_is_part_of_a_functions_name); + RUN_TEST(resolver_source_ocaml_includes_under_opens_and_interfaces); + RUN_TEST(resolver_source_ocaml_functors_module_types_and_interfaces); + RUN_TEST(resolver_source_haskell_constructors); + RUN_TEST(resolver_source_haskell_class_methods); + RUN_TEST(resolver_source_parse_damage_stays_where_it_is); RUN_TEST(resolver_source_dart_lexical_scope_libraries_and_receivers); RUN_TEST(resolver_source_swift_modules_lookup_and_receivers); RUN_TEST(resolver_source_scala_packages_precedence_and_receivers);