mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Merge pull request #1711 from benrfairless/feat/ruby-lsp-per-file
feat(lsp): add the Ruby per-file resolver
This commit is contained in:
9 files changed
+4046
-2
No files matched your search
+2
-1
@@ -887,6 +887,7 @@ TEST_CS_LSP_SRCS = tests/test_cs_lsp.c
|
||||
TEST_CS_LSP_BENCH_SRCS = tests/test_cs_lsp_bench.c
|
||||
|
||||
TEST_PERL_LSP_SRCS = tests/test_perl_lsp.c
|
||||
TEST_RUBY_LSP_SRCS = tests/test_ruby_lsp.c
|
||||
|
||||
TEST_SCOPE_SRCS = tests/test_scope.c
|
||||
|
||||
@@ -1010,7 +1011,7 @@ TEST_REPRO_SRCS = \
|
||||
tests/repro/repro_lsp_java_cs.c \
|
||||
tests/repro/repro_lsp_kt_php_rust.c
|
||||
|
||||
ALL_TEST_SRCS =$(TEST_FOUNDATION_SRCS) $(TEST_EXTRACTION_SRCS) $(TEST_STORE_SRCS) $(TEST_CYPHER_SRCS) $(TEST_MCP_SRCS) $(TEST_DAEMON_SRCS) $(TEST_DISCOVER_SRCS) $(TEST_GRAPH_BUFFER_SRCS) $(TEST_PIPELINE_SRCS) $(TEST_WATCHER_SRCS) $(TEST_LZ4_SRCS) $(TEST_ZSTD_SRCS) $(TEST_ARTIFACT_SRCS) $(TEST_SQLITE_WRITER_SRCS) $(TEST_GO_LSP_SRCS) $(TEST_C_LSP_SRCS) $(TEST_PHP_LSP_SRCS) $(TEST_CS_LSP_SRCS) $(TEST_CS_LSP_BENCH_SRCS) $(TEST_PERL_LSP_SRCS) $(TEST_SCOPE_SRCS) $(TEST_TYPE_REP_SRCS) $(TEST_PY_LSP_SRCS) $(TEST_PY_LSP_BENCH_SRCS) $(TEST_PY_LSP_STRESS_SRCS) $(TEST_PY_LSP_SCALE_SRCS) $(TEST_TS_LSP_SRCS) $(TEST_JAVA_LSP_SRCS) $(TEST_KOTLIN_LSP_SRCS) $(TEST_RUST_LSP_SRCS) $(TEST_TRACES_SRCS) $(TEST_CLI_SRCS) $(TEST_MEM_SRCS) $(TEST_UI_SRCS) $(TEST_HTTPD_SRCS) $(TEST_SECURITY_SRCS) $(TEST_YAML_SRCS) $(TEST_SEMANTIC_SRCS) $(TEST_AST_PROFILE_SRCS) $(TEST_SLAB_ALLOC_SRCS) $(TEST_SIMHASH_SRCS) $(TEST_STACK_OVERFLOW_SRCS) $(TEST_INTEGRATION_SRCS)
|
||||
ALL_TEST_SRCS =$(TEST_FOUNDATION_SRCS) $(TEST_EXTRACTION_SRCS) $(TEST_STORE_SRCS) $(TEST_CYPHER_SRCS) $(TEST_MCP_SRCS) $(TEST_DAEMON_SRCS) $(TEST_DISCOVER_SRCS) $(TEST_GRAPH_BUFFER_SRCS) $(TEST_PIPELINE_SRCS) $(TEST_WATCHER_SRCS) $(TEST_LZ4_SRCS) $(TEST_ZSTD_SRCS) $(TEST_ARTIFACT_SRCS) $(TEST_SQLITE_WRITER_SRCS) $(TEST_GO_LSP_SRCS) $(TEST_C_LSP_SRCS) $(TEST_PHP_LSP_SRCS) $(TEST_CS_LSP_SRCS) $(TEST_CS_LSP_BENCH_SRCS) $(TEST_PERL_LSP_SRCS) $(TEST_RUBY_LSP_SRCS) $(TEST_SCOPE_SRCS) $(TEST_TYPE_REP_SRCS) $(TEST_PY_LSP_SRCS) $(TEST_PY_LSP_BENCH_SRCS) $(TEST_PY_LSP_STRESS_SRCS) $(TEST_PY_LSP_SCALE_SRCS) $(TEST_TS_LSP_SRCS) $(TEST_JAVA_LSP_SRCS) $(TEST_KOTLIN_LSP_SRCS) $(TEST_RUST_LSP_SRCS) $(TEST_TRACES_SRCS) $(TEST_CLI_SRCS) $(TEST_MEM_SRCS) $(TEST_UI_SRCS) $(TEST_HTTPD_SRCS) $(TEST_SECURITY_SRCS) $(TEST_YAML_SRCS) $(TEST_SEMANTIC_SRCS) $(TEST_AST_PROFILE_SRCS) $(TEST_SLAB_ALLOC_SRCS) $(TEST_SIMHASH_SRCS) $(TEST_STACK_OVERFLOW_SRCS) $(TEST_INTEGRATION_SRCS)
|
||||
|
||||
|
||||
# ── Build directories ────────────────────────────────────────────
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "lsp/php_lsp.h"
|
||||
#include "lsp/perl_lsp.h"
|
||||
#include "lsp/py_lsp.h"
|
||||
#include "lsp/ruby_lsp.h"
|
||||
#include "lsp/ts_lsp.h"
|
||||
#include "lsp/cs_lsp.h"
|
||||
#include "lsp/java_lsp.h"
|
||||
@@ -3951,6 +3952,9 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C
|
||||
if (language == CBM_LANG_PERL) {
|
||||
cbm_run_perl_lsp(a, result, source, source_len, root);
|
||||
}
|
||||
if (language == CBM_LANG_RUBY) {
|
||||
cbm_run_ruby_lsp(a, result, source, source_len, root);
|
||||
}
|
||||
if (language == CBM_LANG_PYTHON) {
|
||||
cbm_run_py_lsp(a, result, source, source_len, root);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,465 @@
|
||||
/*
|
||||
* ruby_stdlib_data.c — hand-written Ruby core + curated Rails type data.
|
||||
*
|
||||
* Strategy mirrors perl_stdlib_data.c / php_stdlib_data.c:
|
||||
* 1. Ruby core classes (String, Array, Hash, Integer, ...) registered as
|
||||
* types with receiver-keyed instance methods whose return types enable
|
||||
* call-chain typing ("a,b".split(",").first). Method QNs are bare
|
||||
* ("String.upcase") — no project prefix — so they can never collide
|
||||
* with an indexed def; they exist for TYPING, not for edge emission
|
||||
* (the resolver only emits edges to project-defined methods).
|
||||
* 2. Kernel built-ins (puts, require, raise, ...) as global functions so
|
||||
* bare calls to them type-check without producing spurious edges.
|
||||
* 3. A curated Rails surface: ActiveRecord::Base class-side query methods
|
||||
* and instance persistence methods, ActionController::Base helpers,
|
||||
* and ActiveSupport-style predicates on Object. Registered under
|
||||
* dotted QNs ("ActiveRecord.Base") to match ruby path dotting
|
||||
* (Foo::Bar -> Foo.Bar). The resolver special-cases ActiveRecord
|
||||
* query methods to return the *receiving model's* type (User.find ->
|
||||
* User), which these generic entries cannot express.
|
||||
*
|
||||
* Return types are left UNKNOWN where the real type is polymorphic; the
|
||||
* seed only needs to be right where chaining matters.
|
||||
*/
|
||||
|
||||
#include "../type_rep.h"
|
||||
#include "../type_registry.h"
|
||||
#include "../../arena.h"
|
||||
#include "../ruby_lsp.h"
|
||||
#include <string.h>
|
||||
|
||||
#define MIXED cbm_type_unknown()
|
||||
|
||||
/* Register a type by bare dotted QN (e.g. "String", "ActiveRecord.Base"). */
|
||||
#define REG_TYPE(qn_) \
|
||||
do { \
|
||||
CBMRegisteredType rt; \
|
||||
memset(&rt, 0, sizeof(rt)); \
|
||||
rt.qualified_name = (qn_); \
|
||||
rt.short_name = (qn_); \
|
||||
cbm_registry_add_type(reg, rt); \
|
||||
} while (0)
|
||||
|
||||
/* Signature `() -> ret`. Every seed allocation can fail (arena exhausted):
|
||||
* a NULL return vector is left out, so the signature types as unknown (the
|
||||
* same fallback ruby_register_method uses) instead of being written through. */
|
||||
static const CBMType *ruby_seed_signature(CBMArena *arena, const CBMType *ret) {
|
||||
const CBMType **rets = (const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(*rets));
|
||||
if (rets) {
|
||||
rets[0] = ret;
|
||||
rets[1] = NULL;
|
||||
}
|
||||
return cbm_type_func(arena, NULL, NULL, rets);
|
||||
}
|
||||
|
||||
/* Register an instance method on receiver type `recv_` returning `ret_`.
|
||||
* An entry whose formatted QN came back NULL is not registered: a NULL QN
|
||||
* would reach strcmp in finalize and in every later lookup. */
|
||||
#define REG_METHOD(recv_, name_, ret_) \
|
||||
do { \
|
||||
memset(&rf, 0, sizeof(rf)); \
|
||||
rf.min_params = -1; \
|
||||
rf.qualified_name = cbm_arena_sprintf(arena, "%s.%s", (recv_), (name_)); \
|
||||
if (!rf.qualified_name) \
|
||||
break; \
|
||||
rf.short_name = (name_); \
|
||||
rf.receiver_type = (recv_); \
|
||||
rf.signature = ruby_seed_signature(arena, (ret_)); \
|
||||
cbm_registry_add_func(reg, rf); \
|
||||
} while (0)
|
||||
|
||||
/* Register a singleton (class-side) method: receiver key "<qn>.self"
|
||||
* matches ruby_lookup_singleton_method's keying convention. Skipped when
|
||||
* either formatted name came back NULL (see REG_METHOD). */
|
||||
#define REG_SMETHOD(recv_, name_, ret_) \
|
||||
do { \
|
||||
memset(&rf, 0, sizeof(rf)); \
|
||||
rf.min_params = -1; \
|
||||
rf.qualified_name = cbm_arena_sprintf(arena, "%s.%s", (recv_), (name_)); \
|
||||
rf.receiver_type = cbm_arena_sprintf(arena, "%s.self", (recv_)); \
|
||||
if (!rf.qualified_name || !rf.receiver_type) \
|
||||
break; \
|
||||
rf.short_name = (name_); \
|
||||
rf.signature = ruby_seed_signature(arena, (ret_)); \
|
||||
cbm_registry_add_func(reg, rf); \
|
||||
} while (0)
|
||||
|
||||
/* Register a global (Kernel) built-in function returning `ret_`. */
|
||||
#define REG_BUILTIN(name_, ret_) \
|
||||
do { \
|
||||
memset(&rf, 0, sizeof(rf)); \
|
||||
rf.min_params = -1; \
|
||||
rf.qualified_name = (name_); \
|
||||
rf.short_name = (name_); \
|
||||
rf.signature = ruby_seed_signature(arena, (ret_)); \
|
||||
cbm_registry_add_func(reg, rf); \
|
||||
} while (0)
|
||||
|
||||
void cbm_ruby_stdlib_register(CBMTypeRegistry *reg, CBMArena *arena) {
|
||||
CBMRegisteredFunc rf;
|
||||
|
||||
const CBMType *T_STRING = cbm_type_named(arena, "String");
|
||||
const CBMType *T_ARRAY = cbm_type_named(arena, "Array");
|
||||
const CBMType *T_HASH = cbm_type_named(arena, "Hash");
|
||||
const CBMType *T_INT = cbm_type_named(arena, "Integer");
|
||||
const CBMType *T_FLOAT = cbm_type_named(arena, "Float");
|
||||
const CBMType *T_SYM = cbm_type_named(arena, "Symbol");
|
||||
const CBMType *T_BOOL = cbm_type_builtin(arena, "bool");
|
||||
|
||||
/* ── Core class constants (resolvable as superclasses / receivers) ── */
|
||||
static const char *core_types[] = {"Object",
|
||||
"BasicObject",
|
||||
"String",
|
||||
"Array",
|
||||
"Hash",
|
||||
"Integer",
|
||||
"Float",
|
||||
"Numeric",
|
||||
"Symbol",
|
||||
"Range",
|
||||
"Regexp",
|
||||
"Time",
|
||||
"Date",
|
||||
"DateTime",
|
||||
"File",
|
||||
"IO",
|
||||
"Dir",
|
||||
"Struct",
|
||||
"OpenStruct",
|
||||
"StringIO",
|
||||
"Set",
|
||||
"Proc",
|
||||
"Method",
|
||||
"Thread",
|
||||
"Mutex",
|
||||
"Queue",
|
||||
"Rational",
|
||||
"Complex",
|
||||
"NilClass",
|
||||
"TrueClass",
|
||||
"FalseClass",
|
||||
"Exception",
|
||||
"StandardError",
|
||||
"RuntimeError",
|
||||
"ArgumentError",
|
||||
"TypeError",
|
||||
"NameError",
|
||||
"NoMethodError",
|
||||
"IOError",
|
||||
"KeyError",
|
||||
"IndexError",
|
||||
"RangeError",
|
||||
"NotImplementedError",
|
||||
"StopIteration",
|
||||
"Comparable",
|
||||
"Enumerable",
|
||||
"Kernel",
|
||||
"Math",
|
||||
"JSON",
|
||||
"ERB",
|
||||
"Logger",
|
||||
"Enumerator",
|
||||
NULL};
|
||||
for (int i = 0; core_types[i]; i++)
|
||||
REG_TYPE(core_types[i]);
|
||||
|
||||
/* ── String ── */
|
||||
REG_METHOD("String", "upcase", T_STRING);
|
||||
REG_METHOD("String", "downcase", T_STRING);
|
||||
REG_METHOD("String", "capitalize", T_STRING);
|
||||
REG_METHOD("String", "strip", T_STRING);
|
||||
REG_METHOD("String", "chomp", T_STRING);
|
||||
REG_METHOD("String", "gsub", T_STRING);
|
||||
REG_METHOD("String", "sub", T_STRING);
|
||||
REG_METHOD("String", "split", T_ARRAY);
|
||||
REG_METHOD("String", "chars", T_ARRAY);
|
||||
REG_METHOD("String", "lines", T_ARRAY);
|
||||
REG_METHOD("String", "bytes", T_ARRAY);
|
||||
REG_METHOD("String", "to_s", T_STRING);
|
||||
REG_METHOD("String", "to_str", T_STRING);
|
||||
REG_METHOD("String", "to_i", T_INT);
|
||||
REG_METHOD("String", "to_f", T_FLOAT);
|
||||
REG_METHOD("String", "to_sym", T_SYM);
|
||||
REG_METHOD("String", "length", T_INT);
|
||||
REG_METHOD("String", "size", T_INT);
|
||||
REG_METHOD("String", "reverse", T_STRING);
|
||||
REG_METHOD("String", "include?", T_BOOL);
|
||||
REG_METHOD("String", "start_with?", T_BOOL);
|
||||
REG_METHOD("String", "end_with?", T_BOOL);
|
||||
REG_METHOD("String", "empty?", T_BOOL);
|
||||
REG_METHOD("String", "match?", T_BOOL);
|
||||
REG_METHOD("String", "freeze", T_STRING);
|
||||
REG_METHOD("String", "dup", T_STRING);
|
||||
|
||||
/* ── Array ── */
|
||||
REG_METHOD("Array", "map", T_ARRAY);
|
||||
REG_METHOD("Array", "flat_map", T_ARRAY);
|
||||
REG_METHOD("Array", "select", T_ARRAY);
|
||||
REG_METHOD("Array", "filter", T_ARRAY);
|
||||
REG_METHOD("Array", "reject", T_ARRAY);
|
||||
REG_METHOD("Array", "sort", T_ARRAY);
|
||||
REG_METHOD("Array", "sort_by", T_ARRAY);
|
||||
REG_METHOD("Array", "uniq", T_ARRAY);
|
||||
REG_METHOD("Array", "compact", T_ARRAY);
|
||||
REG_METHOD("Array", "flatten", T_ARRAY);
|
||||
REG_METHOD("Array", "reverse", T_ARRAY);
|
||||
REG_METHOD("Array", "concat", T_ARRAY);
|
||||
REG_METHOD("Array", "push", T_ARRAY);
|
||||
REG_METHOD("Array", "join", T_STRING);
|
||||
REG_METHOD("Array", "first", MIXED);
|
||||
REG_METHOD("Array", "last", MIXED);
|
||||
REG_METHOD("Array", "sample", MIXED);
|
||||
REG_METHOD("Array", "length", T_INT);
|
||||
REG_METHOD("Array", "size", T_INT);
|
||||
REG_METHOD("Array", "count", T_INT);
|
||||
REG_METHOD("Array", "sum", MIXED);
|
||||
REG_METHOD("Array", "min", MIXED);
|
||||
REG_METHOD("Array", "max", MIXED);
|
||||
REG_METHOD("Array", "include?", T_BOOL);
|
||||
REG_METHOD("Array", "empty?", T_BOOL);
|
||||
REG_METHOD("Array", "any?", T_BOOL);
|
||||
REG_METHOD("Array", "all?", T_BOOL);
|
||||
REG_METHOD("Array", "none?", T_BOOL);
|
||||
REG_METHOD("Array", "each", T_ARRAY);
|
||||
REG_METHOD("Array", "each_with_index", T_ARRAY);
|
||||
REG_METHOD("Array", "to_a", T_ARRAY);
|
||||
REG_METHOD("Array", "group_by", T_HASH);
|
||||
REG_METHOD("Array", "each_slice", MIXED);
|
||||
REG_METHOD("Array", "zip", T_ARRAY);
|
||||
|
||||
/* ── Hash ── */
|
||||
REG_METHOD("Hash", "keys", T_ARRAY);
|
||||
REG_METHOD("Hash", "values", T_ARRAY);
|
||||
REG_METHOD("Hash", "merge", T_HASH);
|
||||
REG_METHOD("Hash", "merge!", T_HASH);
|
||||
REG_METHOD("Hash", "fetch", MIXED);
|
||||
REG_METHOD("Hash", "dig", MIXED);
|
||||
REG_METHOD("Hash", "key?", T_BOOL);
|
||||
REG_METHOD("Hash", "has_key?", T_BOOL);
|
||||
REG_METHOD("Hash", "include?", T_BOOL);
|
||||
REG_METHOD("Hash", "empty?", T_BOOL);
|
||||
REG_METHOD("Hash", "size", T_INT);
|
||||
REG_METHOD("Hash", "length", T_INT);
|
||||
REG_METHOD("Hash", "count", T_INT);
|
||||
REG_METHOD("Hash", "each", T_HASH);
|
||||
REG_METHOD("Hash", "map", T_ARRAY);
|
||||
REG_METHOD("Hash", "select", T_HASH);
|
||||
REG_METHOD("Hash", "reject", T_HASH);
|
||||
REG_METHOD("Hash", "to_a", T_ARRAY);
|
||||
REG_METHOD("Hash", "to_h", T_HASH);
|
||||
REG_METHOD("Hash", "transform_values", T_HASH);
|
||||
REG_METHOD("Hash", "transform_keys", T_HASH);
|
||||
REG_METHOD("Hash", "symbolize_keys", T_HASH); /* ActiveSupport */
|
||||
REG_METHOD("Hash", "stringify_keys", T_HASH); /* ActiveSupport */
|
||||
REG_METHOD("Hash", "deep_symbolize_keys", T_HASH);
|
||||
REG_METHOD("Hash", "with_indifferent_access", T_HASH);
|
||||
|
||||
/* ── Integer / Float ── */
|
||||
REG_METHOD("Integer", "to_s", T_STRING);
|
||||
REG_METHOD("Integer", "to_i", T_INT);
|
||||
REG_METHOD("Integer", "to_f", T_FLOAT);
|
||||
REG_METHOD("Integer", "times", MIXED);
|
||||
REG_METHOD("Integer", "upto", MIXED);
|
||||
REG_METHOD("Integer", "abs", T_INT);
|
||||
REG_METHOD("Integer", "zero?", T_BOOL);
|
||||
REG_METHOD("Integer", "positive?", T_BOOL);
|
||||
REG_METHOD("Integer", "negative?", T_BOOL);
|
||||
REG_METHOD("Integer", "even?", T_BOOL);
|
||||
REG_METHOD("Integer", "odd?", T_BOOL);
|
||||
REG_METHOD("Float", "to_s", T_STRING);
|
||||
REG_METHOD("Float", "to_i", T_INT);
|
||||
REG_METHOD("Float", "round", T_INT);
|
||||
REG_METHOD("Float", "ceil", T_INT);
|
||||
REG_METHOD("Float", "floor", T_INT);
|
||||
REG_METHOD("Float", "abs", T_FLOAT);
|
||||
|
||||
/* ── Symbol / misc ── */
|
||||
REG_METHOD("Symbol", "to_s", T_STRING);
|
||||
REG_METHOD("Symbol", "to_sym", T_SYM);
|
||||
REG_METHOD("Symbol", "to_proc", cbm_type_named(arena, "Proc"));
|
||||
REG_METHOD("Range", "to_a", T_ARRAY);
|
||||
REG_METHOD("Range", "each", MIXED);
|
||||
REG_METHOD("Range", "map", T_ARRAY);
|
||||
REG_METHOD("Range", "include?", T_BOOL);
|
||||
REG_METHOD("Time", "to_s", T_STRING);
|
||||
REG_METHOD("Time", "to_i", T_INT);
|
||||
REG_METHOD("Time", "strftime", T_STRING);
|
||||
REG_METHOD("Time", "year", T_INT);
|
||||
REG_METHOD("Time", "month", T_INT);
|
||||
REG_METHOD("Time", "day", T_INT);
|
||||
REG_SMETHOD("Time", "now", cbm_type_named(arena, "Time"));
|
||||
REG_SMETHOD("Time", "at", cbm_type_named(arena, "Time"));
|
||||
REG_SMETHOD("Time", "parse", cbm_type_named(arena, "Time"));
|
||||
REG_SMETHOD("Time", "current", cbm_type_named(arena, "Time")); /* ActiveSupport */
|
||||
REG_SMETHOD("Time", "zone", MIXED);
|
||||
REG_SMETHOD("File", "read", T_STRING);
|
||||
REG_SMETHOD("File", "readlines", T_ARRAY);
|
||||
REG_SMETHOD("File", "open", cbm_type_named(arena, "File"));
|
||||
REG_SMETHOD("File", "exist?", T_BOOL);
|
||||
REG_SMETHOD("File", "basename", T_STRING);
|
||||
REG_SMETHOD("File", "dirname", T_STRING);
|
||||
REG_SMETHOD("File", "join", T_STRING);
|
||||
REG_SMETHOD("File", "expand_path", T_STRING);
|
||||
REG_SMETHOD("JSON", "parse", MIXED);
|
||||
REG_SMETHOD("JSON", "generate", T_STRING);
|
||||
REG_SMETHOD("JSON", "pretty_generate", T_STRING);
|
||||
REG_SMETHOD("Math", "sqrt", T_FLOAT);
|
||||
REG_SMETHOD("Math", "pow", T_FLOAT);
|
||||
REG_SMETHOD("Math", "sin", T_FLOAT);
|
||||
REG_SMETHOD("Math", "cos", T_FLOAT);
|
||||
REG_SMETHOD("Math", "log", T_FLOAT);
|
||||
|
||||
/* ── Object (universal receivers; ActiveSupport predicates) ── */
|
||||
REG_METHOD("Object", "to_s", T_STRING);
|
||||
REG_METHOD("Object", "inspect", T_STRING);
|
||||
REG_METHOD("Object", "freeze", MIXED);
|
||||
REG_METHOD("Object", "frozen?", T_BOOL);
|
||||
REG_METHOD("Object", "dup", MIXED);
|
||||
REG_METHOD("Object", "clone", MIXED);
|
||||
REG_METHOD("Object", "tap", MIXED);
|
||||
REG_METHOD("Object", "then", MIXED);
|
||||
REG_METHOD("Object", "nil?", T_BOOL);
|
||||
REG_METHOD("Object", "is_a?", T_BOOL);
|
||||
REG_METHOD("Object", "kind_of?", T_BOOL);
|
||||
REG_METHOD("Object", "instance_of?", T_BOOL);
|
||||
REG_METHOD("Object", "respond_to?", T_BOOL);
|
||||
REG_METHOD("Object", "blank?", T_BOOL); /* ActiveSupport */
|
||||
REG_METHOD("Object", "present?", T_BOOL); /* ActiveSupport */
|
||||
REG_METHOD("Object", "presence", MIXED); /* ActiveSupport */
|
||||
REG_METHOD("Object", "to_json", T_STRING); /* ActiveSupport / json */
|
||||
|
||||
/* ── Kernel built-ins (global functions) ── */
|
||||
REG_BUILTIN("puts", MIXED);
|
||||
REG_BUILTIN("print", MIXED);
|
||||
REG_BUILTIN("p", MIXED);
|
||||
REG_BUILTIN("pp", MIXED);
|
||||
REG_BUILTIN("require", T_BOOL);
|
||||
REG_BUILTIN("require_relative", T_BOOL);
|
||||
REG_BUILTIN("raise", MIXED);
|
||||
REG_BUILTIN("fail", MIXED);
|
||||
REG_BUILTIN("loop", MIXED);
|
||||
REG_BUILTIN("sleep", T_INT);
|
||||
REG_BUILTIN("rand", MIXED);
|
||||
REG_BUILTIN("format", T_STRING);
|
||||
REG_BUILTIN("sprintf", T_STRING);
|
||||
REG_BUILTIN("gets", T_STRING);
|
||||
REG_BUILTIN("exit", MIXED);
|
||||
REG_BUILTIN("abort", MIXED);
|
||||
REG_BUILTIN("lambda", cbm_type_named(arena, "Proc"));
|
||||
REG_BUILTIN("proc", cbm_type_named(arena, "Proc"));
|
||||
REG_BUILTIN("binding", MIXED);
|
||||
REG_BUILTIN("catch", MIXED);
|
||||
REG_BUILTIN("throw", MIXED);
|
||||
REG_BUILTIN("attr_accessor", MIXED);
|
||||
REG_BUILTIN("attr_reader", MIXED);
|
||||
REG_BUILTIN("attr_writer", MIXED);
|
||||
|
||||
/* ── Rails: ActiveRecord::Base ──────────────────────────────────
|
||||
* Class-side query interface. Return types here are generic; the
|
||||
* resolver's AR special case substitutes the receiving model's type
|
||||
* (User.find -> User) when the receiver's ancestry reaches
|
||||
* ActiveRecord.Base / ApplicationRecord. */
|
||||
REG_TYPE("ActiveRecord.Base");
|
||||
REG_TYPE("ApplicationRecord");
|
||||
static const char *ar_class_methods[] = {"find",
|
||||
"find_by",
|
||||
"find_by!",
|
||||
"find_or_create_by",
|
||||
"find_or_initialize_by",
|
||||
"where",
|
||||
"all",
|
||||
"first",
|
||||
"last",
|
||||
"take",
|
||||
"create",
|
||||
"create!",
|
||||
"new",
|
||||
"count",
|
||||
"order",
|
||||
"limit",
|
||||
"offset",
|
||||
"joins",
|
||||
"left_joins",
|
||||
"includes",
|
||||
"preload",
|
||||
"eager_load",
|
||||
"group",
|
||||
"having",
|
||||
"distinct",
|
||||
"pluck",
|
||||
"ids",
|
||||
"exists?",
|
||||
"none",
|
||||
"unscoped",
|
||||
"select",
|
||||
"sum",
|
||||
"maximum",
|
||||
"minimum",
|
||||
"average",
|
||||
"destroy_all",
|
||||
"delete_all",
|
||||
"update_all",
|
||||
"transaction",
|
||||
"find_each",
|
||||
NULL};
|
||||
for (int i = 0; ar_class_methods[i]; i++)
|
||||
REG_SMETHOD("ActiveRecord.Base", ar_class_methods[i], MIXED);
|
||||
REG_METHOD("ActiveRecord.Base", "save", T_BOOL);
|
||||
REG_METHOD("ActiveRecord.Base", "save!", T_BOOL);
|
||||
REG_METHOD("ActiveRecord.Base", "update", T_BOOL);
|
||||
REG_METHOD("ActiveRecord.Base", "update!", T_BOOL);
|
||||
REG_METHOD("ActiveRecord.Base", "destroy", MIXED);
|
||||
REG_METHOD("ActiveRecord.Base", "destroy!", MIXED);
|
||||
REG_METHOD("ActiveRecord.Base", "delete", MIXED);
|
||||
REG_METHOD("ActiveRecord.Base", "reload", MIXED);
|
||||
REG_METHOD("ActiveRecord.Base", "valid?", T_BOOL);
|
||||
REG_METHOD("ActiveRecord.Base", "invalid?", T_BOOL);
|
||||
REG_METHOD("ActiveRecord.Base", "persisted?", T_BOOL);
|
||||
REG_METHOD("ActiveRecord.Base", "new_record?", T_BOOL);
|
||||
REG_METHOD("ActiveRecord.Base", "errors", MIXED);
|
||||
REG_METHOD("ActiveRecord.Base", "attributes", T_HASH);
|
||||
REG_METHOD("ActiveRecord.Base", "assign_attributes", MIXED);
|
||||
REG_METHOD("ActiveRecord.Base", "update_attribute", T_BOOL);
|
||||
REG_METHOD("ActiveRecord.Base", "touch", T_BOOL);
|
||||
REG_METHOD("ActiveRecord.Base", "id", T_INT);
|
||||
|
||||
/* ── Rails: ActionController::Base / ::API ── */
|
||||
REG_TYPE("ActionController.Base");
|
||||
REG_TYPE("ActionController.API");
|
||||
static const char *ac_receivers[] = {"ActionController.Base", "ActionController.API", NULL};
|
||||
for (int i = 0; ac_receivers[i]; i++) {
|
||||
const char *acr = ac_receivers[i];
|
||||
REG_METHOD(acr, "render", MIXED);
|
||||
REG_METHOD(acr, "redirect_to", MIXED);
|
||||
REG_METHOD(acr, "head", MIXED);
|
||||
REG_METHOD(acr, "params", MIXED);
|
||||
REG_METHOD(acr, "session", MIXED);
|
||||
REG_METHOD(acr, "cookies", MIXED);
|
||||
REG_METHOD(acr, "request", MIXED);
|
||||
REG_METHOD(acr, "response", MIXED);
|
||||
REG_METHOD(acr, "respond_to", MIXED);
|
||||
REG_METHOD(acr, "flash", MIXED);
|
||||
}
|
||||
REG_SMETHOD("ActionController.Base", "before_action", MIXED);
|
||||
REG_SMETHOD("ActionController.Base", "after_action", MIXED);
|
||||
REG_SMETHOD("ActionController.Base", "skip_before_action", MIXED);
|
||||
REG_SMETHOD("ActionController.API", "before_action", MIXED);
|
||||
REG_SMETHOD("ActionController.API", "after_action", MIXED);
|
||||
|
||||
/* ── Rails: jobs / mailers (constants only, for superclass chains) ── */
|
||||
REG_TYPE("ActiveJob.Base");
|
||||
REG_TYPE("ApplicationJob");
|
||||
REG_TYPE("ActionMailer.Base");
|
||||
REG_TYPE("ApplicationMailer");
|
||||
REG_TYPE("ApplicationController");
|
||||
REG_SMETHOD("ActiveJob.Base", "perform_later", MIXED);
|
||||
REG_SMETHOD("ActiveJob.Base", "perform_now", MIXED);
|
||||
REG_SMETHOD("ActiveJob.Base", "set", MIXED);
|
||||
REG_METHOD("ActionMailer.Base", "mail", MIXED);
|
||||
REG_SMETHOD("ActionMailer.Base", "with", MIXED);
|
||||
|
||||
/* ── Minitest/RSpec anchors (superclass chains in test files) ── */
|
||||
REG_TYPE("Minitest.Test");
|
||||
REG_TYPE("ActiveSupport.TestCase");
|
||||
REG_TYPE("ActionDispatch.IntegrationTest");
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,176 @@
|
||||
#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 */
|
||||
@@ -14,6 +14,8 @@
|
||||
#include "lsp/generated/php_stdlib_data.c"
|
||||
#include "lsp/perl_lsp.c"
|
||||
#include "lsp/generated/perl_stdlib_data.c"
|
||||
#include "lsp/ruby_lsp.c"
|
||||
#include "lsp/generated/ruby_stdlib_data.c"
|
||||
#include "lsp/generated/python_stdlib_data.c"
|
||||
#include "lsp/py_lsp.c"
|
||||
#include "lsp/ts_lsp.c"
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
# USAGE
|
||||
# bash scripts/check-lsp-originality.sh [--lang NAME] [--refresh]
|
||||
# [--list-candidates] [--help]
|
||||
# --lang NAME scan against ONE reference only (py|ts|go|cs|c|java|kotlin|rust|php|perl)
|
||||
# --lang NAME scan against ONE reference only (py|ts|go|cs|c|java|kotlin|rust|php|perl|ruby)
|
||||
# --refresh re-fetch reference sources even if cached
|
||||
# --list-candidates print the extracted local tokens and exit (no fetch; self-test)
|
||||
# Exit 0 = no verbatim overlap found. Exit 1 = overlap(s) to review by a human.
|
||||
@@ -64,6 +64,10 @@ REFS=(
|
||||
# Perl (PR #461) was authored clean-room. We scan against PerlNavigator (MIT)
|
||||
# — the leading OSS Perl language server — for defensive copy-detection.
|
||||
"perl|https://github.com/bscan/PerlNavigator|server/src"
|
||||
# Ruby was authored clean-room from the tree-sitter-ruby grammar + Ruby
|
||||
# language semantics. We scan against Shopify's ruby-lsp (MIT) — the
|
||||
# leading OSS Ruby language server — for defensive copy-detection.
|
||||
"ruby|https://github.com/Shopify/ruby-lsp|lib"
|
||||
# Infrastructure hybrid LSPs (K8s YAML, Kustomize bundles, Flux/Argo deploy
|
||||
# scopes, the Go operator extension) were written from the Kubernetes, Flux,
|
||||
# Argo, Cluster API, cert-manager and Kubermatic API types. Scanned against
|
||||
|
||||
@@ -1802,6 +1802,7 @@ extern void suite_php_lsp(void);
|
||||
extern void suite_cs_lsp(void);
|
||||
extern void suite_cs_lsp_bench(void);
|
||||
extern void suite_perl_lsp(void);
|
||||
extern void suite_ruby_lsp(void);
|
||||
extern void suite_scope(void);
|
||||
extern void suite_type_rep(void);
|
||||
extern void suite_py_lsp(void);
|
||||
@@ -2435,6 +2436,7 @@ int main(int argc, char **argv) {
|
||||
RUN_SELECTED_SUITE(cs_lsp);
|
||||
RUN_SELECTED_SUITE_PERF(cs_lsp_bench);
|
||||
RUN_SELECTED_SUITE(perl_lsp);
|
||||
RUN_SELECTED_SUITE(ruby_lsp);
|
||||
RUN_SELECTED_SUITE(py_lsp);
|
||||
RUN_SELECTED_SUITE(kotlin_lsp);
|
||||
RUN_SELECTED_SUITE(kotlin_typed_calls);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user