mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
The mixin walk recursed into prepend and include at every level with only the depth cap of 16 as a bound, so a module that both prepends and includes the next one doubled the paths per level: a miss on a chain of n such modules cost 2^n visits (measured 4 -> 20,744 for n = 2..14). Each lookup is now one walk that carries the fail-closed blocked flag and a visit stamp. Ruby linearizes ancestors, so a module reached a second time in one walk was already searched and missed (a hit or a block would have ended the walk); skipping the repeat keeps every lookup linear in the ancestors the file declares. Lookup order, hits and the fail-closed rules are unchanged. The stamps live on the per-file class table, so the walk allocates nothing. The walk entries feed CBM_LSP_WORK (seam builds only; product builds compile it to nothing). rubylsp_mixin_walk_visits_each_module_once pins linear work for the doubled chain on the instance and the class side (before: 66 -> 4,098 visits for n = 6 -> 12; after: 14 -> 26) and keeps a diamond and a prepend-plus-include pair resolving exactly once. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
177 lines
7.7 KiB
C
177 lines
7.7 KiB
C
#ifndef CBM_LSP_RUBY_LSP_H
|
|
#define CBM_LSP_RUBY_LSP_H
|
|
|
|
#include "type_rep.h"
|
|
#include "scope.h"
|
|
#include "type_registry.h"
|
|
#include "../cbm.h"
|
|
|
|
/* Ruby mixin kinds. Lookup order for an instance method on class C:
|
|
* prepended modules (last prepended first) → C's own methods →
|
|
* included modules (last included first) → superclass chain.
|
|
* `extend` adds a module's instance methods to C's singleton (class-side). */
|
|
typedef enum {
|
|
RUBY_MIXIN_INCLUDE = 0,
|
|
RUBY_MIXIN_PREPEND,
|
|
RUBY_MIXIN_EXTEND,
|
|
} RubyMixinKind;
|
|
|
|
/* One class/module discovered in the file (or, cross-file, in the project).
|
|
* `path` is the dotted constant path used ONLY for constant lookup ("Admin.User"
|
|
* for both `module Admin; class User` and `class Admin::User`). `qn` is the
|
|
* graph QN built exactly the way the structural extractor builds it: each
|
|
* declaration's raw name text appended to the enclosing class QN (or the
|
|
* module QN at top level), so a compact `class Admin::User` yields
|
|
* "<module_qn>.Admin::User" while the lexically nested form yields
|
|
* "<module_qn>.Admin.User". Emitted rows must carry the extractor spelling
|
|
* or the pipeline's exact-name join silently drops them. `superclass_qn` is
|
|
* resolved against the class table + stdlib after collection; NULL when
|
|
* unknown. */
|
|
typedef struct {
|
|
const char *path;
|
|
const char *qn;
|
|
const char *superclass_ref; /* raw dotted superclass spelling, or NULL */
|
|
const char *superclass_qn; /* resolved QN (table/stdlib), or NULL */
|
|
bool is_module; /* `module` (mixin source) vs `class` */
|
|
/* Method-lookup visit marks: the stamp of the last walk that searched
|
|
* this class on the instance / singleton side (ruby_walk_seen). */
|
|
unsigned int seen_instance;
|
|
unsigned int seen_singleton;
|
|
} RubyClassInfo;
|
|
|
|
/* One `include`/`prepend`/`extend` record: owner class QN → module ref. */
|
|
typedef struct {
|
|
const char *owner_qn; /* class/module whose body held the call */
|
|
const char *module_ref; /* raw dotted module spelling */
|
|
const char *module_qn; /* resolved QN, or NULL */
|
|
RubyMixinKind kind;
|
|
} RubyMixinInfo;
|
|
|
|
/* One inferred instance-variable type: (class QN, @name) → type QN.
|
|
* `conflicted` latches when two assignments disagree — lookup then returns
|
|
* nothing (zero-edge guarantee beats a coin-flip type). */
|
|
typedef struct {
|
|
const char *class_qn;
|
|
const char *ivar_name; /* including the leading '@' */
|
|
const char *type_qn;
|
|
bool conflicted;
|
|
} RubyIvarInfo;
|
|
|
|
/* RubyLSPContext — per-file state for Ruby type-aware call resolution.
|
|
* Mirrors PerlLSPContext / PHPLSPContext structure.
|
|
*
|
|
* Ruby specifics that shape this context:
|
|
* - Classes/modules nest lexically (`module A; class B`), and constants
|
|
* resolve through the lexical nesting outward (A::B sees A's constants
|
|
* before top-level ones). `nesting` tracks the current dotted path.
|
|
* - Inheritance is `class C < Base`; mixins arrive via include/prepend/
|
|
* extend and participate in method lookup (see RubyMixinKind).
|
|
* - `Foo.new` constructs a Foo and dispatches to Foo#initialize.
|
|
* - `def m` defines an instance method; `def self.m` a singleton (class)
|
|
* method. Singleton methods are registered under the receiver key
|
|
* "<class_qn>.self" — constants cannot be lowercase, so the suffix can
|
|
* never collide with a real nested-constant path. */
|
|
typedef struct {
|
|
CBMArena *arena;
|
|
const char *source;
|
|
int source_len;
|
|
const CBMTypeRegistry *registry;
|
|
/* Mutable alias of `registry`, set only while PASS 1 populates method
|
|
* tables (mirrors perl_lsp's mutable-reg build phase). NULL afterwards. */
|
|
CBMTypeRegistry *build_reg;
|
|
CBMScope *current_scope;
|
|
|
|
/* Lexical nesting: one frame per enclosing class/module declaration,
|
|
* frames joined by '.', each frame keeping its raw source spelling ("" at
|
|
* top level, "M.A::B" inside `module M; class A::B`). Ruby's constant
|
|
* lookup walks the frames outward — inside `class A::B` the nesting is
|
|
* [A::B] only, so A's constants are NOT probed. */
|
|
const char *nesting;
|
|
|
|
/* Class/module table (pass 1). */
|
|
RubyClassInfo *classes;
|
|
int class_count;
|
|
int class_cap;
|
|
|
|
/* Mixin table (pass 1). */
|
|
RubyMixinInfo *mixins;
|
|
int mixin_count;
|
|
int mixin_cap;
|
|
|
|
/* Instance-variable type table (pass 1). */
|
|
RubyIvarInfo *ivars;
|
|
int ivar_count;
|
|
int ivar_cap;
|
|
|
|
/* Cross-file import map (require/require_relative resolved by the
|
|
* pipeline). Unused per-file: Ruby constants are file-global. */
|
|
const char **import_names;
|
|
const char **import_qns;
|
|
int import_count;
|
|
|
|
/* Current enclosing context during the resolution walk. */
|
|
const char *enclosing_class_qn; /* class QN, or NULL at top level */
|
|
const char *enclosing_func_qn; /* enclosing method QN, or NULL */
|
|
bool in_singleton_method; /* `def self.m` / class << self scope */
|
|
/* Inside a block that rebinds `self` (instance_eval/exec, class_eval/
|
|
* exec, module_eval/exec, define_method, Class.new/Module.new): the
|
|
* receiver of bare and `self.` calls is NOT the enclosing class, and
|
|
* the resolver cannot know what it is — so they emit nothing. */
|
|
bool self_unknown;
|
|
/* Number of enclosing blocks (`{ }`, `do end`, `-> { }`) since the
|
|
* nearest class/module body. A `def` met here is NOT a method of the
|
|
* enclosing class — ActiveSupport's `class_methods do … end`,
|
|
* `included do … end`, RSpec groups — so PASS 1 registers nothing for
|
|
* it and collects no mixins from it. */
|
|
int block_depth;
|
|
const char *module_qn;
|
|
|
|
/* Output: resolved calls accumulate here. */
|
|
CBMResolvedCallArray *resolved_calls;
|
|
|
|
/* Recursion guards. */
|
|
int eval_depth;
|
|
int walk_depth;
|
|
|
|
/* Stamp of the current method-lookup walk; each walk takes a fresh one
|
|
* so marks left by earlier walks never match. */
|
|
unsigned int lookup_stamp;
|
|
|
|
/* Debug mode (CBM_LSP_DEBUG env). */
|
|
bool debug;
|
|
} RubyLSPContext;
|
|
|
|
/* Initialize a RubyLSPContext for processing one file. */
|
|
void ruby_lsp_init(RubyLSPContext *ctx, CBMArena *arena, const char *source, int source_len,
|
|
const CBMTypeRegistry *registry, const char *module_qn,
|
|
CBMResolvedCallArray *out);
|
|
|
|
/* Resolve a dotted constant path (e.g. "Foo", "A.B") against the class
|
|
* table using the current lexical nesting, then the stdlib. Returns the
|
|
* resolved QN or NULL. */
|
|
const char *ruby_resolve_constant(RubyLSPContext *ctx, const char *path);
|
|
|
|
/* Look up an instance method on a class, walking prepends → own → includes →
|
|
* superclass chain. Returns the resolved CBMRegisteredFunc or NULL. */
|
|
const CBMRegisteredFunc *ruby_lookup_instance_method(RubyLSPContext *ctx, const char *class_qn,
|
|
const char *method_name);
|
|
|
|
/* Look up a singleton (class-side) method: own singleton methods →
|
|
* `extend`ed modules' instance methods → superclass singleton chain. */
|
|
const CBMRegisteredFunc *ruby_lookup_singleton_method(RubyLSPContext *ctx, const char *class_qn,
|
|
const char *method_name);
|
|
|
|
/* Evaluate a Ruby expression's type. May return CBM_TYPE_UNKNOWN. */
|
|
const CBMType *ruby_eval_expr_type(RubyLSPContext *ctx, TSNode node);
|
|
|
|
/* Entry point: build registry from file defs + stdlib, then run resolution.
|
|
* Called from cbm_extract_file() via the language dispatch in cbm.c. */
|
|
void cbm_run_ruby_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len,
|
|
TSNode root);
|
|
|
|
/* Register Ruby core stdlib types/methods + a curated Rails surface
|
|
* (ActiveRecord/ActiveSupport/ActionController) into a registry. */
|
|
void cbm_ruby_stdlib_register(CBMTypeRegistry *reg, CBMArena *arena);
|
|
|
|
#endif /* CBM_LSP_RUBY_LSP_H */
|