/* * test_ruby_lsp.c — Tests for the Ruby Light Semantic Pass. * * Coverage mirrors tests/test_perl_lsp.c / tests/test_php_lsp.c, exercising * the foundational Ruby resolution scenarios: * 1. QN contract (defs carry module_qn.ClassPath.method — the join key * the resolver's emitted rows depend on) * 2. Constructor typing + edge (a = Animal.new; a.speak) * 3. Constructor edge targets CLASS (textual `Widget.new` -> "Widget") * 4. Implicit/explicit self dispatch * 5. Singleton (class) methods (def self.m; Animal.m) * 6. Superclass chain lookup (class B < A) * 7. include + prepend mixin lookup (prepend shadows the class's own) * 8. extend mixin (class-side) lookup * 9. super dispatch * 10. Instance-variable typing (@thing = Foo.new; @thing.bar) * 11. Chained call typing (Foo.new.bar) * 12. Nested modules (lexical nesting + `A::B` references) * 12b. Compact `class A::B` names (exact extractor spelling, scope) * 13. ActiveRecord model typing (User.find(1).full_name) * 14. Top-level function calls * 15. Unresolvable receiver emits NO spurious edge (negative) * 15b. Scope gates (def, params, blocks, reassignment) * 16. send() / the whole dynamic-dispatch family emits NO edge (negative) * 16c. Blocks that rebind self emit NO self edge (negative) * Fail-closed ancestry (7c) and block-nested defs (7d) sit with mixins. * * Scope: every row here is SINGLE-FILE, driven through cbm_extract_file so it * exercises the per-file resolver exactly as the extraction pipeline calls it. * Cross-file resolution (cbm_run_ruby_lsp_cross, wired through * pass_lsp_cross.c) is a separate tier and is covered by its own rows. * * The resolver populates result->resolved_calls with CBMResolvedCall rows. * The pipeline joins a row to its caller def by EXACT QN equality, so every * row that pins a class-path spelling (nested or compact) asserts the full * caller and callee QN with strcmp ("test"/"main.rb" -> module QN test.main). * Substring helpers remain for rows where only presence/absence matters. */ #include "test_framework.h" #include "cbm.h" #include "../src/pipeline/lsp_resolve.h" #include "lsp/ruby_lsp.h" #include "lsp/lsp_work.h" #include #include #include /* ── Helpers (mirror test_perl_lsp.c) ──────────────────────────── */ static CBMFileResult *extract_ruby(const char *source) { return cbm_extract_file(source, (int)strlen(source), CBM_LANG_RUBY, "test", "main.rb", 0, NULL, NULL); } static int find_resolved(const CBMFileResult *r, const char *callerSub, const char *calleeSub) { for (int i = 0; i < r->resolved_calls.count; i++) { const CBMResolvedCall *rc = &r->resolved_calls.items[i]; if (rc->caller_qn && strstr(rc->caller_qn, callerSub) && rc->callee_qn && strstr(rc->callee_qn, calleeSub)) return i; } return -1; } static int require_resolved(const CBMFileResult *r, const char *callerSub, const char *calleeSub) { int idx = find_resolved(r, callerSub, calleeSub); if (idx < 0) { printf(" MISSING resolved call: caller~%s -> callee~%s (have %d)\n", callerSub, calleeSub, r->resolved_calls.count); for (int i = 0; i < r->resolved_calls.count; i++) { const CBMResolvedCall *rc = &r->resolved_calls.items[i]; printf(" %s -> %s [%s %.2f]\n", rc->caller_qn ? rc->caller_qn : "(null)", rc->callee_qn ? rc->callee_qn : "(null)", rc->strategy ? rc->strategy : "(null)", rc->confidence); } } return idx; } static const CBMResolvedCall *find_resolved_with_strategy(const CBMFileResult *r, const char *callerSub, const char *calleeSub, const char *strategy) { for (int i = 0; i < r->resolved_calls.count; i++) { const CBMResolvedCall *rc = &r->resolved_calls.items[i]; if (!rc->caller_qn || !rc->callee_qn) continue; if (!strstr(rc->caller_qn, callerSub)) continue; if (!strstr(rc->callee_qn, calleeSub)) continue; if (strategy && (!rc->strategy || strcmp(rc->strategy, strategy) != 0)) continue; return rc; } return NULL; } /* Exact-name match on BOTH ends. The pipeline joins a resolved row to its * caller def by exact QN equality, so a row whose caller is spelled even * slightly differently from the extractor's def is silently dropped — * substring matching would hide exactly that failure. */ static int find_resolved_exact(const CBMFileResult *r, const char *caller_qn, const char *callee_qn) { for (int i = 0; i < r->resolved_calls.count; i++) { const CBMResolvedCall *rc = &r->resolved_calls.items[i]; if (rc->caller_qn && rc->callee_qn && strcmp(rc->caller_qn, caller_qn) == 0 && strcmp(rc->callee_qn, callee_qn) == 0) return i; } return -1; } static void dump_resolved(const CBMFileResult *r) { for (int i = 0; i < r->resolved_calls.count; i++) { const CBMResolvedCall *rc = &r->resolved_calls.items[i]; printf(" %s -> %s [%s %.2f]\n", rc->caller_qn ? rc->caller_qn : "(null)", rc->callee_qn ? rc->callee_qn : "(null)", rc->strategy ? rc->strategy : "(null)", rc->confidence); } } static int require_resolved_exact(const CBMFileResult *r, const char *caller_qn, const char *callee_qn) { int idx = find_resolved_exact(r, caller_qn, callee_qn); if (idx < 0) { printf(" MISSING exact resolved call: %s -> %s (have %d)\n", caller_qn, callee_qn, r->resolved_calls.count); dump_resolved(r); } return idx; } /* Number of rows with exactly this caller and callee. */ static int count_resolved_exact(const CBMFileResult *r, const char *caller_qn, const char *callee_qn) { int n = 0; for (int i = 0; i < r->resolved_calls.count; i++) { const CBMResolvedCall *rc = &r->resolved_calls.items[i]; if (rc->caller_qn && rc->callee_qn && strcmp(rc->caller_qn, caller_qn) == 0 && strcmp(rc->callee_qn, callee_qn) == 0) n++; } return n; } static const CBMDefinition *find_def(const CBMFileResult *r, const char *label, const char *name) { for (int i = 0; i < r->defs.count; i++) { const CBMDefinition *d = &r->defs.items[i]; if (d->label && d->name && strcmp(d->label, label) == 0 && strcmp(d->name, name) == 0) return d; } return NULL; } /* ── 1. QN contract: defs weave the class path into method QNs ─── */ TEST(rubylsp_qn_contract) { const char *src = "class Animal\n" " def speak\n" " 'woof'\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); const CBMDefinition *m = find_def(r, "Method", "speak"); if (!m) { printf(" no Method speak def; defs:\n"); for (int i = 0; i < r->defs.count; i++) printf(" [%s] %s qn=%s parent=%s\n", r->defs.items[i].label, r->defs.items[i].name, r->defs.items[i].qualified_name, r->defs.items[i].parent_class ? r->defs.items[i].parent_class : "(null)"); } ASSERT(m); if (strcmp(m->qualified_name, "test.main.Animal.speak") != 0) { printf(" QN scheme mismatch: got %s (resolver assumes module_qn.ClassPath.method)\n", m->qualified_name); } ASSERT(strcmp(m->qualified_name, "test.main.Animal.speak") == 0); cbm_free_result(r); PASS(); } /* ── 2. Constructor typing: a = Animal.new; a.speak ────────────── */ TEST(rubylsp_method_via_constructor_assignment) { const char *src = "class Animal\n" " def initialize(name)\n" " @name = name\n" " end\n" " def speak\n" " @name\n" " end\n" "end\n" "class Runner\n" " def run\n" " a = Animal.new('rex')\n" " a.speak\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); ASSERT(require_resolved(r, "Runner.run", "Animal.speak") >= 0); ASSERT(find_resolved_with_strategy(r, "Runner.run", "Animal.speak", "ruby_method_typed")); cbm_free_result(r); PASS(); } /* ── 3. Constructor edge targets the CLASS node ─────────────────── */ TEST(rubylsp_constructor_edge_targets_class) { const char *src = "class Widget\n" " def initialize\n" " end\n" "end\n" "class Maker\n" " def build\n" " Widget.new\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); const CBMResolvedCall *rc = find_resolved_with_strategy(r, "Maker.build", "Widget", "ruby_constructor"); ASSERT(rc); /* Callee is the class QN (leaf "Widget"), matching the textual * extractor's Widget.new -> "Widget" rewrite so the row joins. */ ASSERT(strcmp(rc->callee_qn, "test.main.Widget") == 0); ASSERT(rc->confidence >= CBM_LSP_CONFIDENCE_FLOOR); cbm_free_result(r); PASS(); } /* ── 4. Implicit + explicit self dispatch ───────────────────────── */ TEST(rubylsp_self_dispatch) { const char *src = "class Greeter\n" " def greet\n" " build_greeting('hi')\n" " self.build_greeting('yo')\n" " end\n" " def build_greeting(word)\n" " word\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); ASSERT(require_resolved(r, "Greeter.greet", "Greeter.build_greeting") >= 0); ASSERT(find_resolved_with_strategy(r, "Greeter.greet", "Greeter.build_greeting", "ruby_self_dispatch")); cbm_free_result(r); PASS(); } /* ── 5. Singleton (class) methods ───────────────────────────────── */ TEST(rubylsp_singleton_method_dispatch) { const char *src = "class Registry\n" " def self.register(key)\n" " key\n" " end\n" "end\n" "class App\n" " def boot\n" " Registry.register(:db)\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); ASSERT(require_resolved(r, "App.boot", "Registry.register") >= 0); ASSERT(find_resolved_with_strategy(r, "App.boot", "Registry.register", "ruby_class_method")); cbm_free_result(r); PASS(); } /* Singleton methods must NOT satisfy instance dispatch (and vice versa). */ TEST(rubylsp_singleton_instance_split) { const char *src = "class Config\n" " def self.load\n" " 1\n" " end\n" "end\n" "class App\n" " def boot\n" " c = Config.new\n" " c.load\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); /* c.load is an INSTANCE call; Config only defines a singleton `load`. * Zero-edge guarantee: no resolved row may bind them. */ ASSERT(find_resolved(r, "App.boot", "Config.load") < 0); cbm_free_result(r); PASS(); } /* ── 6. Superclass chain ────────────────────────────────────────── */ TEST(rubylsp_inheritance) { const char *src = "class Base\n" " def helper\n" " 1\n" " end\n" "end\n" "class Child < Base\n" " def work\n" " helper\n" " self.helper\n" " end\n" "end\n" "class Driver\n" " def drive\n" " c = Child.new\n" " c.helper\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); /* Inherited method resolves to Base.helper from both dispatch shapes. */ ASSERT(require_resolved(r, "Child.work", "Base.helper") >= 0); ASSERT(require_resolved(r, "Driver.drive", "Base.helper") >= 0); cbm_free_result(r); PASS(); } /* ── 7. include mixin ───────────────────────────────────────────── */ TEST(rubylsp_include_mixin) { const char *src = "module Greetable\n" " def greet\n" " 'hello'\n" " end\n" "end\n" "class Person\n" " include Greetable\n" " def hail\n" " greet()\n" " end\n" "end\n" "class Caller\n" " def run\n" " p = Person.new\n" " p.greet\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); ASSERT(require_resolved(r, "Person.hail", "Greetable.greet") >= 0); ASSERT(require_resolved(r, "Caller.run", "Greetable.greet") >= 0); cbm_free_result(r); PASS(); } /* ── 7b. prepend mixin shadows the class's own method ───────────── */ /* Ruby's lookup order puts prepended modules AHEAD of the class itself, which * is the whole point of `prepend` over `include`. Both an own method and a * prepended one exist here, so a resolver that walked the class first would * still emit an edge — just the wrong one. Pin the winner, not merely that * something resolved. */ TEST(rubylsp_prepend_mixin) { const char *src = "module Audited\n" " def save\n" " 'audited'\n" " end\n" "end\n" "class Record\n" " prepend Audited\n" " def save\n" " 'plain'\n" " end\n" "end\n" "class App\n" " def run\n" " r = Record.new\n" " r.save\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); /* The prepended module wins. */ ASSERT(require_resolved(r, "App.run", "Audited.save") >= 0); /* And the shadowed own method must NOT also be emitted. */ ASSERT(find_resolved(r, "App.run", "Record.save") < 0); cbm_free_result(r); PASS(); } /* ── 7c. Unresolved ancestry fails closed ───────────────────────── */ /* Three cases where an ancestor the resolver cannot see could define (or * shadow) the method being called. Each one has a tempting local answer — * the class's own method, a superclass method, a same-named top-level def — * and each answer may be wrong, so none may be emitted. */ TEST(rubylsp_unresolved_ancestry_fails_closed) { const char *src = /* (a) unresolved PREPEND blocks an own-method hit: the * prepended module may shadow `save`. */ "class Record\n" " prepend UnseenAudit\n" " def save\n" " 1\n" " end\n" " def persist\n" " self.save\n" " end\n" "end\n" "class RecordUser\n" " def run\n" " r = Record.new\n" " r.save\n" " end\n" "end\n" /* (b) unresolved INCLUDE stops the walk before the superclass chain: * the included module may define `helper` ahead of Base. */ "class Base\n" " def helper\n" " 1\n" " end\n" "end\n" "class Child < Base\n" " include UnseenConcern\n" " def work\n" " self.helper\n" " end\n" "end\n" "class ChildUser\n" " def run\n" " c = Child.new\n" " c.helper\n" " end\n" "end\n" /* (c) a bare call in a class with an unresolved SUPERCLASS must not * fall through to a same-named top-level def. */ "def util\n" " 1\n" "end\n" "class Orphan < UnseenBase\n" " def go\n" " util()\n" " end\n" " def again\n" " super\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); /* (a) */ ASSERT(find_resolved(r, "Record.persist", "save") < 0); ASSERT(find_resolved(r, "RecordUser.run", "save") < 0); /* (b) */ ASSERT(find_resolved(r, "Child.work", "helper") < 0); ASSERT(find_resolved(r, "ChildUser.run", "helper") < 0); /* (c) */ ASSERT(find_resolved(r, "Orphan.go", "util") < 0); ASSERT(find_resolved(r, "Orphan.again", "again") < 0); cbm_free_result(r); PASS(); } /* Positive controls for the rule above: resolved ancestry still reaches the * superclass and still falls through to a top-level def, and a hit found * BEFORE the unseen ancestor in Ruby's lookup order is still emitted. */ TEST(rubylsp_resolved_ancestry_still_resolves) { const char *src = "module Seen\n" " def greet\n" " 1\n" " end\n" "end\n" "def util\n" " 1\n" "end\n" "class Base\n" " def helper\n" " 1\n" " end\n" "end\n" "class Child < Base\n" " include Seen\n" " def work\n" " self.helper\n" " util()\n" " end\n" "end\n" "class Mixed < UnseenBase\n" " include UnseenConcern\n" " include Seen\n" " def own\n" " 2\n" " end\n" " def work\n" " self.own\n" " self.greet\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); ASSERT(require_resolved_exact(r, "test.main.Child.work", "test.main.Base.helper") >= 0); ASSERT(require_resolved_exact(r, "test.main.Child.work", "test.main.util") >= 0); /* Own method precedes includes and the superclass in lookup order. */ ASSERT(require_resolved_exact(r, "test.main.Mixed.work", "test.main.Mixed.own") >= 0); /* `include Seen` comes AFTER `include UnseenConcern`, so Seen is searched * first and its hit is trustworthy. */ ASSERT(require_resolved_exact(r, "test.main.Mixed.work", "test.main.Seen.greet") >= 0); cbm_free_result(r); PASS(); } /* ── 7d. Methods defined inside blocks are not members of the module ── */ /* ActiveSupport's `class_methods do … end` / `included do … end` define * methods on whatever class includes the concern (class-side, for * class_methods), never instance methods of the concern module itself. A * resolver that registered them on the module would then find them through * `include Concern` and emit `Foo.new.build` -> Concern.build — an edge to a * def the extractor does not even emit. Same for `include X` written INSIDE * such a block: it applies to the includer, not the concern. */ TEST(rubylsp_block_defs_not_module_members) { const char *src = "module Other\n" " def other_m\n" " 1\n" " end\n" "end\n" "module Concern\n" " class_methods do\n" " def build\n" " 1\n" " end\n" " end\n" " included do\n" " include Other\n" " def injected\n" " 2\n" " end\n" " end\n" " def real\n" " 3\n" " end\n" "end\n" "class Foo\n" " include Concern\n" " def initialize\n" " end\n" "end\n" "class User\n" " def run\n" " f = Foo.new\n" " f.build\n" " f.injected\n" " f.other_m\n" " f.real\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); ASSERT(find_resolved(r, "User.run", "build") < 0); ASSERT(find_resolved(r, "User.run", "injected") < 0); ASSERT(find_resolved(r, "User.run", "other_m") < 0); /* Positive control: a def directly in the module body IS a member. */ ASSERT(require_resolved_exact(r, "test.main.User.run", "test.main.Concern.real") >= 0); cbm_free_result(r); PASS(); } /* A def inside a top-level block (an RSpec group) has whatever `self` the * DSL gave it: bare calls in its body emit nothing, and it is not a * file-level function other top-level code can reach. */ TEST(rubylsp_top_level_block_defs_fail_closed) { const char *src = "def util\n" " 1\n" "end\n" "describe 'x' do\n" " def helper\n" " util()\n" " end\n" "end\n" "def caller_fn\n" " helper()\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); ASSERT(find_resolved(r, "helper", "util") < 0); ASSERT(find_resolved(r, "caller_fn", "helper") < 0); cbm_free_result(r); PASS(); } /* ── 8. extend mixin (class-side) ───────────────────────────────── */ TEST(rubylsp_extend_mixin) { const char *src = "module Findable\n" " def locate(id)\n" " id\n" " end\n" "end\n" "class Record\n" " extend Findable\n" "end\n" "class App\n" " def run\n" " Record.locate(7)\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); ASSERT(require_resolved(r, "App.run", "Findable.locate") >= 0); cbm_free_result(r); PASS(); } /* ── 9. super dispatch ──────────────────────────────────────────── */ TEST(rubylsp_super_dispatch) { const char *src = "class Base\n" " def setup\n" " 1\n" " end\n" "end\n" "class Child < Base\n" " def setup\n" " super\n" " 2\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); const CBMResolvedCall *rc = find_resolved_with_strategy(r, "Child.setup", "Base.setup", "ruby_method_super"); if (!rc) (void)require_resolved(r, "Child.setup", "Base.setup"); ASSERT(rc); cbm_free_result(r); PASS(); } /* ── 10. Instance-variable typing ───────────────────────────────── */ TEST(rubylsp_ivar_typing) { const char *src = "class Engine\n" " def start\n" " 1\n" " end\n" "end\n" "class Car\n" " def initialize\n" " @engine = Engine.new\n" " end\n" " def drive\n" " @engine.start\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); ASSERT(require_resolved(r, "Car.drive", "Engine.start") >= 0); ASSERT(find_resolved_with_strategy(r, "Car.drive", "Engine.start", "ruby_ivar_method")); cbm_free_result(r); PASS(); } /* Conflicting ivar assignments must suppress the type (zero-edge). */ TEST(rubylsp_ivar_conflict_no_edge) { const char *src = "class A\n" " def go\n" " 1\n" " end\n" "end\n" "class B\n" " def go\n" " 2\n" " end\n" "end\n" "class Holder\n" " def initialize(flag)\n" " @x = A.new\n" " @x = B.new\n" " end\n" " def run\n" " @x.go\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); ASSERT(find_resolved(r, "Holder.run", ".go") < 0); cbm_free_result(r); PASS(); } /* Any write to @x that is not a resolved constructor (or nil) must latch the * conflict, the same way two different constructors do — a method result, a * parameter, an unresolved constant's `.new`, a compound operator, or a * multiple assignment. Otherwise `@x = Foo.new` in one method silently types * every other method's `@x` even after it was reassigned to who-knows-what. */ TEST(rubylsp_ivar_non_constructor_write_latches) { const char *src = "class Engine\n" " def start\n" " 1\n" " end\n" "end\n" "class ViaMethod\n" " def initialize\n" " @e = Engine.new\n" " end\n" " def reset\n" " @e = compute\n" " end\n" " def go\n" " @e.start\n" " end\n" "end\n" "class ViaParam\n" " def initialize(e)\n" " @e = Engine.new\n" " @e = e\n" " end\n" " def go\n" " @e.start\n" " end\n" "end\n" "class ViaUnknownCtor\n" " def initialize\n" " @e = Engine.new\n" " @e = Mystery.new\n" " end\n" " def go\n" " @e.start\n" " end\n" "end\n" "class ViaCompound\n" " def initialize\n" " @e = Engine.new\n" " @e += 1\n" " end\n" " def go\n" " @e.start\n" " end\n" "end\n" "class ViaMulti\n" " def initialize(a)\n" " @e = Engine.new\n" " @e, @f = a\n" " end\n" " def go\n" " @e.start\n" " end\n" "end\n" "class ViaOrUntyped\n" " def initialize\n" " @e = Engine.new\n" " end\n" " def lazy\n" " @e ||= build\n" " end\n" " def go\n" " @e.start\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); ASSERT(find_resolved(r, "ViaMethod.go", "Engine.start") < 0); ASSERT(find_resolved(r, "ViaParam.go", "Engine.start") < 0); ASSERT(find_resolved(r, "ViaUnknownCtor.go", "Engine.start") < 0); ASSERT(find_resolved(r, "ViaCompound.go", "Engine.start") < 0); ASSERT(find_resolved(r, "ViaMulti.go", "Engine.start") < 0); ASSERT(find_resolved(r, "ViaOrUntyped.go", "Engine.start") < 0); cbm_free_result(r); PASS(); } /* `@x ||= Const.new` is the memoised-constructor idiom and counts as a * constructor assignment; a `nil` reset says nothing about the type. */ TEST(rubylsp_ivar_or_assign_constructor_types) { const char *src = "class Engine\n" " def start\n" " 1\n" " end\n" "end\n" "class Car\n" " def engine\n" " @engine ||= Engine.new\n" " end\n" " def clear\n" " @engine = nil\n" " end\n" " def drive\n" " @engine.start\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); ASSERT(require_resolved_exact(r, "test.main.Car.drive", "test.main.Engine.start") >= 0); ASSERT(find_resolved_with_strategy(r, "Car.drive", "Engine.start", "ruby_ivar_method")); cbm_free_result(r); PASS(); } /* ── 11. Chained calls ──────────────────────────────────────────── */ TEST(rubylsp_chained_constructor_call) { const char *src = "class Builder\n" " def finish\n" " 1\n" " end\n" "end\n" "class App\n" " def run\n" " Builder.new.finish\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); ASSERT(require_resolved(r, "App.run", "Builder.finish") >= 0); cbm_free_result(r); PASS(); } /* ── 12. Nested modules + lexical nesting ───────────────────────── */ TEST(rubylsp_nested_modules) { const char *src = "module Admin\n" " class User\n" " def self.lookup(id)\n" " id\n" " end\n" " def name\n" " 'n'\n" " end\n" " end\n" " class Panel\n" " def show\n" " User.lookup(1)\n" " u = User.new\n" " u.name\n" " end\n" " end\n" "end\n" "class Outside\n" " def probe\n" " Admin::User.lookup(2)\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); /* Exact spellings: lexically nested declarations join with '.', matching * the extractor's `.` — a substring match would let * a wrongly-spelled row pass. */ /* Lexical reference from sibling class inside the module. */ ASSERT(require_resolved_exact(r, "test.main.Admin.Panel.show", "test.main.Admin.User.lookup") >= 0); ASSERT(require_resolved_exact(r, "test.main.Admin.Panel.show", "test.main.Admin.User") >= 0); ASSERT(require_resolved_exact(r, "test.main.Admin.Panel.show", "test.main.Admin.User.name") >= 0); /* Fully-qualified reference from outside. */ ASSERT(require_resolved_exact(r, "test.main.Outside.probe", "test.main.Admin.User.lookup") >= 0); cbm_free_result(r); PASS(); } /* ── 12b. Compact `class A::B` names match the extractor byte-for-byte ── */ /* The structural extractor names a compact declaration with its RAW name text * (`Admin::User`), and the pipeline joins a resolved row to its caller def by * exact QN equality. A resolver that normalised the name to `Admin.User` would * have every row from or to such a class dropped at the join — silently, since * substring assertions would still pass. Pin the exact spelling on both ends, * and pin that the extractor's def really is spelled that way. */ TEST(rubylsp_compact_class_name_exact) { const char *src = "class Admin::User\n" " def initialize\n" " end\n" " def name\n" " 'n'\n" " end\n" " def shout\n" " self.name\n" " end\n" "end\n" "class Outside\n" " def probe\n" " u = Admin::User.new\n" " u.name\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); /* The extractor's contract this resolver must reproduce. */ const CBMDefinition *m = find_def(r, "Method", "name"); ASSERT(m); ASSERT(strcmp(m->qualified_name, "test.main.Admin::User.name") == 0); ASSERT(m->parent_class && strcmp(m->parent_class, "test.main.Admin::User") == 0); /* Caller inside the compact class, callee inside it. */ ASSERT(require_resolved_exact(r, "test.main.Admin::User.shout", "test.main.Admin::User.name") >= 0); /* Caller outside: constructor row lands on the class QN, typed call on * the method QN — both in the extractor's spelling. */ ASSERT(require_resolved_exact(r, "test.main.Outside.probe", "test.main.Admin::User") >= 0); ASSERT(require_resolved_exact(r, "test.main.Outside.probe", "test.main.Admin::User.name") >= 0); /* And the dotted spelling must NOT appear anywhere. */ ASSERT(find_resolved(r, "Outside.probe", "Admin.User") < 0); cbm_free_result(r); PASS(); } /* Compact declaration nested inside a lexical module: the extractor joins the * enclosing class QN with the raw name, giving `Admin.Foo::Bar`. */ TEST(rubylsp_compact_class_inside_module_exact) { const char *src = "module Admin\n" " class Foo::Bar\n" " def m\n" " 1\n" " end\n" " end\n" " class Panel\n" " def show\n" " Foo::Bar.new.m\n" " end\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); const CBMDefinition *m = find_def(r, "Method", "m"); ASSERT(m); ASSERT(strcmp(m->qualified_name, "test.main.Admin.Foo::Bar.m") == 0); ASSERT(require_resolved_exact(r, "test.main.Admin.Panel.show", "test.main.Admin.Foo::Bar") >= 0); ASSERT(require_resolved_exact(r, "test.main.Admin.Panel.show", "test.main.Admin.Foo::Bar.m") >= 0); cbm_free_result(r); PASS(); } /* Inside `class Admin::User`, Ruby's lexical nesting is [Admin::User] only — * `Admin` is not opened, so a bare `Helper` does NOT see Admin::Helper. A * resolver that probed the dotted prefixes (Admin.User, Admin, "") would * invent that constant and emit a wrong constructor edge. The explicit * `Admin::Helper` still resolves. */ TEST(rubylsp_compact_class_does_not_open_outer_scope) { const char *src = "module Admin\n" " class Helper\n" " def initialize\n" " end\n" " def assist\n" " 1\n" " end\n" " end\n" "end\n" "class Admin::User\n" " def bare\n" " h = Helper.new\n" " h.assist\n" " end\n" " def explicit\n" " h = Admin::Helper.new\n" " h.assist\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); /* Bare `Helper` inside the compact class: no edge at all. */ ASSERT(find_resolved(r, "Admin::User.bare", "Helper") < 0); /* Fully qualified reference resolves, in exact extractor spelling. */ ASSERT(require_resolved_exact(r, "test.main.Admin::User.explicit", "test.main.Admin.Helper") >= 0); ASSERT(require_resolved_exact(r, "test.main.Admin::User.explicit", "test.main.Admin.Helper.assist") >= 0); cbm_free_result(r); PASS(); } /* ── 13. ActiveRecord model typing ──────────────────────────────── */ TEST(rubylsp_activerecord_model_typing) { const char *src = "class User < ApplicationRecord\n" " def full_name\n" " 'x'\n" " end\n" "end\n" "class UsersController < ApplicationController\n" " def show\n" " u = User.find(1)\n" " u.full_name\n" " end\n" " def index\n" " User.where(active: true).first.full_name\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); /* User.find returns User (AR query typing); u.full_name resolves. */ ASSERT(require_resolved(r, "UsersController.show", "User.full_name") >= 0); /* Relation approximation keeps the model type through where/first. */ ASSERT(require_resolved(r, "UsersController.index", "User.full_name") >= 0); cbm_free_result(r); PASS(); } /* ── 14. Top-level functions ────────────────────────────────────── */ TEST(rubylsp_top_level_function) { const char *src = "def helper(x)\n" " x\n" "end\n" "def run\n" " helper(1)\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); const CBMResolvedCall *rc = find_resolved_with_strategy(r, "main.run", "main.helper", "ruby_function_local"); if (!rc) (void)require_resolved(r, "main.run", "main.helper"); ASSERT(rc); cbm_free_result(r); PASS(); } /* ── 15. Negative: unresolvable receiver emits no edge ──────────── */ TEST(rubylsp_unknown_receiver_no_edge) { const char *src = "class Safe\n" " def process(payload)\n" " payload.transform\n" " mystery = fetch_thing\n" " mystery.explode\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); /* Neither the untyped parameter nor the unresolved local may produce * a resolved row. */ ASSERT(find_resolved(r, "Safe.process", "transform") < 0); ASSERT(find_resolved(r, "Safe.process", "explode") < 0); cbm_free_result(r); PASS(); } /* ── 15b. `def` is a scope gate ─────────────────────────────────── */ /* Ruby locals do not cross a `def`: a typed local in the enclosing body (or * a sibling method) is invisible inside, and a parameter of the same name is * a brand-new, untyped binding. A resolver that chained the method scope to * its parent would type `a` from the outside and emit a wrong edge. */ TEST(rubylsp_def_is_scope_gate) { const char *src = "class Foo\n" " def bar\n" " 1\n" " end\n" "end\n" "a = Foo.new\n" "def top\n" " a.bar\n" "end\n" "class Worker\n" " def run\n" " a = Foo.new\n" " a.bar\n" " helper(a)\n" " end\n" " def helper(a)\n" " a.bar\n" " end\n" " def later\n" " a.bar\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); /* Positive control: the typed local inside its own method resolves. */ ASSERT(require_resolved_exact(r, "test.main.Worker.run", "test.main.Foo.bar") >= 0); /* Top-level `a` is not visible inside `def top`. */ ASSERT(find_resolved(r, "main.top", "Foo.bar") < 0); /* Parameter `a` is untyped, whatever the caller passed. */ ASSERT(find_resolved(r, "Worker.helper", "Foo.bar") < 0); /* Sibling method's local is not visible either. */ ASSERT(find_resolved(r, "Worker.later", "Foo.bar") < 0); cbm_free_result(r); PASS(); } /* Block parameters shadow the enclosing local for the block's extent only: * `|a|` inside the block is untyped, and `a` after the block is still Foo. */ TEST(rubylsp_block_param_shadows_local) { const char *src = "class Foo\n" " def bar\n" " 1\n" " end\n" "end\n" "class Worker\n" " def run\n" " a = Foo.new\n" " [1].each { |a| a.bar }\n" " [1].each do |x, (y, z), *rest; loc|\n" " x.bar\n" " y.bar\n" " rest.bar\n" " loc.bar\n" " end\n" " f = ->(a) { a.bar }\n" " a.bar\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); /* Exactly ONE row: the `a.bar` after the blocks. Every `.bar` inside a * block is on an untyped block parameter. */ if (count_resolved_exact(r, "test.main.Worker.run", "test.main.Foo.bar") != 1) dump_resolved(r); ASSERT(count_resolved_exact(r, "test.main.Worker.run", "test.main.Foo.bar") == 1); cbm_free_result(r); PASS(); } /* Reassigning a typed local to anything the resolver cannot type must drop * the old type. `a = Foo.new; a = x; a.bar` is NOT a call on Foo. Covers * plain reassignment, multiple assignment, `||=` and other compound forms. */ TEST(rubylsp_untyped_reassignment_drops_type) { const char *src = "class Foo\n" " def bar\n" " 1\n" " end\n" "end\n" "class Worker\n" " def plain(x)\n" " a = Foo.new\n" " a = x\n" " a.bar\n" " end\n" " def multi(x)\n" " a = Foo.new\n" " a, b = x, 1\n" " a.bar\n" " end\n" " def nested_multi(x)\n" " a = Foo.new\n" " (a, b), c = x, 1\n" " a.bar\n" " end\n" " def splat_multi(x)\n" " a = Foo.new\n" " b, *a = x\n" " a.bar\n" " end\n" " def or_assign(x)\n" " a = Foo.new\n" " a ||= x\n" " a.bar\n" " end\n" " def compound(x)\n" " a = Foo.new\n" " a += x\n" " a.bar\n" " end\n" " def in_block(x)\n" " a = Foo.new\n" " [1].each { a = x }\n" " a.bar\n" " end\n" " def fresh_or_assign\n" " a ||= Foo.new\n" " a.bar\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); ASSERT(find_resolved(r, "Worker.plain", "Foo.bar") < 0); ASSERT(find_resolved(r, "Worker.multi", "Foo.bar") < 0); ASSERT(find_resolved(r, "Worker.nested_multi", "Foo.bar") < 0); ASSERT(find_resolved(r, "Worker.splat_multi", "Foo.bar") < 0); ASSERT(find_resolved(r, "Worker.or_assign", "Foo.bar") < 0); ASSERT(find_resolved(r, "Worker.compound", "Foo.bar") < 0); /* Assignment inside a block rebinds the enclosing local (closure). */ ASSERT(find_resolved(r, "Worker.in_block", "Foo.bar") < 0); /* Positive control: `||=` on a fresh name with a typed RHS is a * constructor assignment. */ ASSERT(require_resolved_exact(r, "test.main.Worker.fresh_or_assign", "test.main.Foo.bar") >= 0); cbm_free_result(r); PASS(); } /* ── 16. Negative: dynamic dispatch emits no edge ───────────────── */ TEST(rubylsp_send_no_edge) { const char *src = "class Target\n" " def hidden\n" " 1\n" " end\n" "end\n" "class Meta\n" " def invoke\n" " t = Target.new\n" " t.send(:hidden)\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); ASSERT(find_resolved(r, "Meta.invoke", "hidden") < 0); cbm_free_result(r); PASS(); } /* ── 16b. Negative: the whole dynamic-dispatch family emits no edge ── */ /* `send` has its own row above. The zero-edge guarantee is advertised for the * whole reflective family, so pin the rest of it too: a resolver that treated * any of these as an ordinary call would invent an edge to `hidden`. * * `Meta` deliberately defines its OWN `hidden`: inside `instance_eval { }` * self is the Target, not the Meta, so a resolver that dispatched the bare * `hidden()` on the enclosing class would emit Meta.via_instance_eval -> * Meta.hidden — a wrong edge that an empty Meta would never reveal. The * bare calls are written with `()` because tree-sitter-ruby parses a lone * `hidden` as an identifier, which the extractor never emits as a call. */ TEST(rubylsp_dynamic_dispatch_family_no_edge) { const char *src = "class Target\n" " def hidden\n" " 1\n" " end\n" "end\n" "class Meta\n" " def hidden\n" " 0\n" " end\n" " def via_public_send\n" " Target.new.public_send(:hidden)\n" " end\n" " def via_underscore_send\n" " Target.new.__send__(:hidden)\n" " end\n" " def via_define_method\n" " Target.define_method(:hidden) { 2 }\n" " end\n" " def via_instance_eval\n" " Target.new.instance_eval { hidden() }\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); ASSERT(find_resolved(r, "Meta.via_public_send", "hidden") < 0); ASSERT(find_resolved(r, "Meta.via_underscore_send", "hidden") < 0); ASSERT(find_resolved(r, "Meta.via_define_method", "hidden") < 0); ASSERT(find_resolved(r, "Meta.via_instance_eval", "hidden") < 0); cbm_free_result(r); PASS(); } /* ── 16c. Negative: blocks that rebind `self` ───────────────────── */ /* instance_eval/exec, class_eval/exec, module_eval/exec, define_method and * Class.new/Module.new run their block with a different `self`. Bare calls, * `self.` calls and `super` inside them must not dispatch on the enclosing * class. Every enclosing class here HAS the method being called, so the old * behaviour would emit a wrong edge in each row. Receiver and argument * expressions of the same call are ordinary and still resolve. */ TEST(rubylsp_self_rebinding_blocks_no_edge) { const char *src = "class Target\n" " def hidden\n" " 1\n" " end\n" "end\n" "class Meta\n" " def hidden\n" " 0\n" " end\n" " def target\n" " Target.new\n" " end\n" " def via_instance_exec\n" " target().instance_exec(1) { |x| hidden(); self.hidden }\n" " end\n" " def via_class_eval\n" " Target.class_eval do\n" " hidden()\n" " end\n" " end\n" " def via_class_exec\n" " Target.class_exec { hidden() }\n" " end\n" " def via_module_eval\n" " Target.module_eval { self.hidden }\n" " end\n" " def via_module_exec\n" " Target.module_exec { hidden() }\n" " end\n" " def via_define_method_body\n" " Target.define_method(:other) { hidden() }\n" " end\n" " def via_class_new\n" " Class.new do\n" " hidden()\n" " def hidden\n" " super\n" " end\n" " end\n" " end\n" " def via_module_new\n" " Module.new { self.hidden }\n" " end\n" " def after_block\n" " Target.new.instance_eval { hidden() }\n" " self.hidden\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); ASSERT(find_resolved(r, "Meta.via_instance_exec", "hidden") < 0); /* ...but the receiver expression of that same call still resolves. */ ASSERT(require_resolved_exact(r, "test.main.Meta.via_instance_exec", "test.main.Meta.target") >= 0); ASSERT(find_resolved(r, "Meta.via_class_eval", "hidden") < 0); ASSERT(find_resolved(r, "Meta.via_class_exec", "hidden") < 0); ASSERT(find_resolved(r, "Meta.via_module_eval", "hidden") < 0); ASSERT(find_resolved(r, "Meta.via_module_exec", "hidden") < 0); ASSERT(find_resolved(r, "Meta.via_define_method_body", "hidden") < 0); ASSERT(find_resolved(r, "Meta.via_class_new", "hidden") < 0); ASSERT(find_resolved(r, "Meta.via_module_new", "hidden") < 0); /* The suppression ends with the block: the following statement is an * ordinary self dispatch on Meta. */ ASSERT(count_resolved_exact(r, "test.main.Meta.after_block", "test.main.Meta.hidden") == 1); cbm_free_result(r); PASS(); } /* ── 17. Bounded resolver work ──────────────────────────────────── * * Measured with counters, never with a clock (O9): CBM_LSP_WORK * (lsp_work.h, seam builds) for lookup work, and the result arena's * cumulative allocation for bytes built. Each fixture is measured at n and * 2n: linear work doubles (ratio about 2); anything worse lands above the * gate of 3. */ static uint64_t rubylsp_counted_work(const char *src, size_t *arena_bytes) { (void)cbm_lsp_work_take(); CBMFileResult *r = extract_ruby(src); uint64_t work = cbm_lsp_work_take(); if (arena_bytes) *arena_bytes = r ? cbm_arena_total(&r->arena) : 0; if (r) cbm_free_result(r); return work; } /* n modules where every module both prepends and includes the next one; * the last defines one method. `Probe` reaches M0 through `include` * (instance side) or `extend` (class side) and calls a method nobody * defines, so the lookup must miss after searching every ancestor. */ static char *rubylsp_mixin_chain_src(int n, bool class_side) { size_t cap = (size_t)n * 96 + 256; char *src = (char *)malloc(cap); if (!src) return NULL; size_t off = 0; for (int i = 0; i < n; i++) { if (i + 1 < n) off += (size_t)snprintf(src + off, cap - off, "module M%d\n prepend M%d\n include M%d\nend\n", i, i + 1, i + 1); else off += (size_t)snprintf(src + off, cap - off, "module M%d\n def leaf\n 1\n end\nend\n", i); } if (class_side) (void)snprintf(src + off, cap - off, "class Probe\n extend M0\n def self.run\n nobody_defines_this()\n" " end\nend\n"); else (void)snprintf(src + off, cap - off, "class Probe\n include M0\n def run\n nobody_defines_this()\n" " end\nend\n"); return src; } static uint64_t rubylsp_mixin_chain_work(int n, bool class_side) { char *src = rubylsp_mixin_chain_src(n, class_side); if (!src) return 0; uint64_t work = rubylsp_counted_work(src, NULL); free(src); return work; } /* Review ask (a): Ruby linearizes ancestors, so a module reached through * two paths (`prepend M; include M`, a diamond) is searched once. Without a * visited set every level of the chain doubles the paths: 2^n visits * (measured 4 -> 20,744 for n = 2..14). With it a miss costs one visit per * module. Positive controls: the doubled pair still resolves through the * prepend, and a diamond resolves to the shared module exactly once. */ TEST(rubylsp_mixin_walk_visits_each_module_once) { uint64_t inst_n = rubylsp_mixin_chain_work(6, false); uint64_t inst_2n = rubylsp_mixin_chain_work(12, false); uint64_t cls_n = rubylsp_mixin_chain_work(6, true); uint64_t cls_2n = rubylsp_mixin_chain_work(12, true); printf(" mixin chain work: instance n=6 %llu, n=12 %llu; class side n=6 %llu, n=12 %llu\n", (unsigned long long)inst_n, (unsigned long long)inst_2n, (unsigned long long)cls_n, (unsigned long long)cls_2n); ASSERT(inst_n > 0 && cls_n > 0); ASSERT(inst_2n <= 3 * inst_n); ASSERT(cls_2n <= 3 * cls_n); const char *src = "module Shared\n" " def hello\n" " 1\n" " end\n" "end\n" "module Left\n" " include Shared\n" "end\n" "module Right\n" " include Shared\n" "end\n" "module Twice\n" " def twice\n" " 2\n" " end\n" "end\n" "class Diamond\n" " include Left\n" " include Right\n" " prepend Twice\n" " include Twice\n" " def run\n" " hello()\n" " twice()\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); ASSERT(count_resolved_exact(r, "test.main.Diamond.run", "test.main.Shared.hello") == 1); ASSERT(count_resolved_exact(r, "test.main.Diamond.run", "test.main.Twice.twice") == 1); cbm_free_result(r); PASS(); } /* `class Probe; def run; S0::S1::...::S.new; end; end` */ static char *rubylsp_deep_const_src(int n) { size_t cap = (size_t)n * 12 + 128; char *src = (char *)malloc(cap); if (!src) return NULL; size_t off = (size_t)snprintf(src, cap, "class Probe\n def run\n "); for (int i = 0; i < n; i++) off += (size_t)snprintf(src + off, cap - off, i ? "::S%d" : "S%d", i); (void)snprintf(src + off, cap - off, ".new\n end\nend\n"); return src; } static size_t rubylsp_deep_const_bytes(int n) { char *src = rubylsp_deep_const_src(n); if (!src) return 0; size_t bytes = 0; (void)rubylsp_counted_work(src, &bytes); free(src); return bytes; } /* Review ask (b): a constant path is built in one pass. Recursing once per * `::` and re-formatting the growing prefix at every level costs one stack * frame per segment and quadratic bytes; the loop has no recursion and * writes the path once. The positive control pins the exact class QN for a * deep path and for a leading `::`. */ TEST(rubylsp_const_path_is_linear) { size_t b_n = rubylsp_deep_const_bytes(800); size_t b_2n = rubylsp_deep_const_bytes(1600); printf(" const path arena bytes: n=800 %zu, n=1600 %zu\n", b_n, b_2n); ASSERT(b_n > 0); ASSERT(b_2n <= 3 * b_n); const char *src = "module L0\n" " module L1\n" " module L2\n" " module L3\n" " module L4\n" " class Leaf\n" " end\n" " end\n" " end\n" " end\n" " end\n" "end\n" "class Top\n" " class Inner\n" " end\n" "end\n" "class Caller\n" " def deep\n" " L0::L1::L2::L3::L4::Leaf.new\n" " end\n" " def rooted\n" " ::Top::Inner.new\n" " end\n" "end\n"; CBMFileResult *r = extract_ruby(src); ASSERT(r); ASSERT(require_resolved_exact(r, "test.main.Caller.deep", "test.main.L0.L1.L2.L3.L4.Leaf") >= 0); ASSERT(require_resolved_exact(r, "test.main.Caller.rooted", "test.main.Top.Inner") >= 0); cbm_free_result(r); PASS(); } /* Review ask (c): the stdlib seed must survive allocation failure. A * zero-initialised arena has no blocks and no grow size, so every * allocation from it returns NULL. The seed must not write through a NULL * return-type vector, and must not register an entry whose formatted name * came back NULL (a NULL QN would reach strcmp in finalize and in every later * lookup). */ TEST(rubylsp_stdlib_seed_survives_alloc_failure) { CBMArena reg_arena; cbm_arena_init(®_arena); CBMTypeRegistry reg; cbm_registry_init(®, ®_arena); CBMArena dead; memset(&dead, 0, sizeof(dead)); cbm_ruby_stdlib_register(®, &dead); ASSERT(reg.type_count > 0); ASSERT(reg.func_count > 0); /* Kernel built-ins carry literal names */ for (int i = 0; i < reg.func_count; i++) { ASSERT(reg.funcs[i].qualified_name != NULL); ASSERT(reg.funcs[i].short_name != NULL); } cbm_registry_finalize(®); ASSERT(cbm_registry_lookup_func(®, "puts") != NULL); cbm_arena_destroy(®_arena); PASS(); } /* ── suite ──────────────────────────────────────────────────────── */ void suite_ruby_lsp(void) { RUN_TEST(rubylsp_qn_contract); RUN_TEST(rubylsp_method_via_constructor_assignment); RUN_TEST(rubylsp_constructor_edge_targets_class); RUN_TEST(rubylsp_self_dispatch); RUN_TEST(rubylsp_singleton_method_dispatch); RUN_TEST(rubylsp_singleton_instance_split); RUN_TEST(rubylsp_inheritance); RUN_TEST(rubylsp_include_mixin); RUN_TEST(rubylsp_prepend_mixin); RUN_TEST(rubylsp_unresolved_ancestry_fails_closed); RUN_TEST(rubylsp_resolved_ancestry_still_resolves); RUN_TEST(rubylsp_block_defs_not_module_members); RUN_TEST(rubylsp_top_level_block_defs_fail_closed); RUN_TEST(rubylsp_extend_mixin); RUN_TEST(rubylsp_super_dispatch); RUN_TEST(rubylsp_ivar_typing); RUN_TEST(rubylsp_ivar_conflict_no_edge); RUN_TEST(rubylsp_ivar_non_constructor_write_latches); RUN_TEST(rubylsp_ivar_or_assign_constructor_types); RUN_TEST(rubylsp_chained_constructor_call); RUN_TEST(rubylsp_nested_modules); RUN_TEST(rubylsp_compact_class_name_exact); RUN_TEST(rubylsp_compact_class_inside_module_exact); RUN_TEST(rubylsp_compact_class_does_not_open_outer_scope); RUN_TEST(rubylsp_activerecord_model_typing); RUN_TEST(rubylsp_top_level_function); RUN_TEST(rubylsp_unknown_receiver_no_edge); RUN_TEST(rubylsp_def_is_scope_gate); RUN_TEST(rubylsp_block_param_shadows_local); RUN_TEST(rubylsp_untyped_reassignment_drops_type); RUN_TEST(rubylsp_send_no_edge); RUN_TEST(rubylsp_dynamic_dispatch_family_no_edge); RUN_TEST(rubylsp_self_rebinding_blocks_no_edge); RUN_TEST(rubylsp_mixin_walk_visits_each_module_once); RUN_TEST(rubylsp_const_path_is_linear); RUN_TEST(rubylsp_stdlib_seed_survives_alloc_failure); }