@cstart/coldstart 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +236 -0
- package/dist/cache/disk-cache.d.ts +14 -0
- package/dist/cache/disk-cache.d.ts.map +1 -0
- package/dist/cache/disk-cache.js +371 -0
- package/dist/cache/disk-cache.js.map +1 -0
- package/dist/cli.d.ts +18 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +206 -0
- package/dist/cli.js.map +1 -0
- package/dist/constants.d.ts +8 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +111 -0
- package/dist/constants.js.map +1 -0
- package/dist/daemon-lock.d.ts +63 -0
- package/dist/daemon-lock.d.ts.map +1 -0
- package/dist/daemon-lock.js +218 -0
- package/dist/daemon-lock.js.map +1 -0
- package/dist/daemon-log.d.ts +26 -0
- package/dist/daemon-log.d.ts.map +1 -0
- package/dist/daemon-log.js +110 -0
- package/dist/daemon-log.js.map +1 -0
- package/dist/index-manager.d.ts +103 -0
- package/dist/index-manager.d.ts.map +1 -0
- package/dist/index-manager.js +361 -0
- package/dist/index-manager.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +724 -0
- package/dist/index.js.map +1 -0
- package/dist/indexer/content-tokens.d.ts +74 -0
- package/dist/indexer/content-tokens.d.ts.map +1 -0
- package/dist/indexer/content-tokens.js +406 -0
- package/dist/indexer/content-tokens.js.map +1 -0
- package/dist/indexer/cpp-include-roots.d.ts +7 -0
- package/dist/indexer/cpp-include-roots.d.ts.map +1 -0
- package/dist/indexer/cpp-include-roots.js +166 -0
- package/dist/indexer/cpp-include-roots.js.map +1 -0
- package/dist/indexer/csharp-synthetic.d.ts +9 -0
- package/dist/indexer/csharp-synthetic.d.ts.map +1 -0
- package/dist/indexer/csharp-synthetic.js +51 -0
- package/dist/indexer/csharp-synthetic.js.map +1 -0
- package/dist/indexer/django-synthetic.d.ts +12 -0
- package/dist/indexer/django-synthetic.d.ts.map +1 -0
- package/dist/indexer/django-synthetic.js +49 -0
- package/dist/indexer/django-synthetic.js.map +1 -0
- package/dist/indexer/extractors/angularjs.d.ts +16 -0
- package/dist/indexer/extractors/angularjs.d.ts.map +1 -0
- package/dist/indexer/extractors/angularjs.js +49 -0
- package/dist/indexer/extractors/angularjs.js.map +1 -0
- package/dist/indexer/extractors/cpp.d.ts +9 -0
- package/dist/indexer/extractors/cpp.d.ts.map +1 -0
- package/dist/indexer/extractors/cpp.js +254 -0
- package/dist/indexer/extractors/cpp.js.map +1 -0
- package/dist/indexer/extractors/csharp.d.ts +19 -0
- package/dist/indexer/extractors/csharp.d.ts.map +1 -0
- package/dist/indexer/extractors/csharp.js +327 -0
- package/dist/indexer/extractors/csharp.js.map +1 -0
- package/dist/indexer/extractors/env.d.ts +9 -0
- package/dist/indexer/extractors/env.d.ts.map +1 -0
- package/dist/indexer/extractors/env.js +43 -0
- package/dist/indexer/extractors/env.js.map +1 -0
- package/dist/indexer/extractors/go.d.ts +9 -0
- package/dist/indexer/extractors/go.d.ts.map +1 -0
- package/dist/indexer/extractors/go.js +227 -0
- package/dist/indexer/extractors/go.js.map +1 -0
- package/dist/indexer/extractors/graphql.d.ts +22 -0
- package/dist/indexer/extractors/graphql.d.ts.map +1 -0
- package/dist/indexer/extractors/graphql.js +88 -0
- package/dist/indexer/extractors/graphql.js.map +1 -0
- package/dist/indexer/extractors/groovy.d.ts +9 -0
- package/dist/indexer/extractors/groovy.d.ts.map +1 -0
- package/dist/indexer/extractors/groovy.js +356 -0
- package/dist/indexer/extractors/groovy.js.map +1 -0
- package/dist/indexer/extractors/java.d.ts +10 -0
- package/dist/indexer/extractors/java.d.ts.map +1 -0
- package/dist/indexer/extractors/java.js +586 -0
- package/dist/indexer/extractors/java.js.map +1 -0
- package/dist/indexer/extractors/kotlin.d.ts +10 -0
- package/dist/indexer/extractors/kotlin.d.ts.map +1 -0
- package/dist/indexer/extractors/kotlin.js +398 -0
- package/dist/indexer/extractors/kotlin.js.map +1 -0
- package/dist/indexer/extractors/node-helpers.d.ts +13 -0
- package/dist/indexer/extractors/node-helpers.d.ts.map +1 -0
- package/dist/indexer/extractors/node-helpers.js +17 -0
- package/dist/indexer/extractors/node-helpers.js.map +1 -0
- package/dist/indexer/extractors/parser-factory.d.ts +4 -0
- package/dist/indexer/extractors/parser-factory.d.ts.map +1 -0
- package/dist/indexer/extractors/parser-factory.js +28 -0
- package/dist/indexer/extractors/parser-factory.js.map +1 -0
- package/dist/indexer/extractors/php.d.ts +17 -0
- package/dist/indexer/extractors/php.d.ts.map +1 -0
- package/dist/indexer/extractors/php.js +363 -0
- package/dist/indexer/extractors/php.js.map +1 -0
- package/dist/indexer/extractors/python.d.ts +14 -0
- package/dist/indexer/extractors/python.d.ts.map +1 -0
- package/dist/indexer/extractors/python.js +449 -0
- package/dist/indexer/extractors/python.js.map +1 -0
- package/dist/indexer/extractors/ruby.d.ts +10 -0
- package/dist/indexer/extractors/ruby.d.ts.map +1 -0
- package/dist/indexer/extractors/ruby.js +996 -0
- package/dist/indexer/extractors/ruby.js.map +1 -0
- package/dist/indexer/extractors/rust.d.ts +9 -0
- package/dist/indexer/extractors/rust.d.ts.map +1 -0
- package/dist/indexer/extractors/rust.js +322 -0
- package/dist/indexer/extractors/rust.js.map +1 -0
- package/dist/indexer/extractors/toml.d.ts +9 -0
- package/dist/indexer/extractors/toml.d.ts.map +1 -0
- package/dist/indexer/extractors/toml.js +84 -0
- package/dist/indexer/extractors/toml.js.map +1 -0
- package/dist/indexer/extractors/xml.d.ts +9 -0
- package/dist/indexer/extractors/xml.d.ts.map +1 -0
- package/dist/indexer/extractors/xml.js +163 -0
- package/dist/indexer/extractors/xml.js.map +1 -0
- package/dist/indexer/extractors/yaml.d.ts +9 -0
- package/dist/indexer/extractors/yaml.d.ts.map +1 -0
- package/dist/indexer/extractors/yaml.js +117 -0
- package/dist/indexer/extractors/yaml.js.map +1 -0
- package/dist/indexer/git.d.ts +19 -0
- package/dist/indexer/git.d.ts.map +1 -0
- package/dist/indexer/git.js +55 -0
- package/dist/indexer/git.js.map +1 -0
- package/dist/indexer/graph.d.ts +7 -0
- package/dist/indexer/graph.d.ts.map +1 -0
- package/dist/indexer/graph.js +20 -0
- package/dist/indexer/graph.js.map +1 -0
- package/dist/indexer/indexed-file.d.ts +15 -0
- package/dist/indexer/indexed-file.d.ts.map +1 -0
- package/dist/indexer/indexed-file.js +50 -0
- package/dist/indexer/indexed-file.js.map +1 -0
- package/dist/indexer/invariants.d.ts +15 -0
- package/dist/indexer/invariants.d.ts.map +1 -0
- package/dist/indexer/invariants.js +96 -0
- package/dist/indexer/invariants.js.map +1 -0
- package/dist/indexer/laravel-synthetic.d.ts +10 -0
- package/dist/indexer/laravel-synthetic.d.ts.map +1 -0
- package/dist/indexer/laravel-synthetic.js +78 -0
- package/dist/indexer/laravel-synthetic.js.map +1 -0
- package/dist/indexer/parser.d.ts +4 -0
- package/dist/indexer/parser.d.ts.map +1 -0
- package/dist/indexer/parser.js +499 -0
- package/dist/indexer/parser.js.map +1 -0
- package/dist/indexer/patch.d.ts +3 -0
- package/dist/indexer/patch.d.ts.map +1 -0
- package/dist/indexer/patch.js +291 -0
- package/dist/indexer/patch.js.map +1 -0
- package/dist/indexer/rails-synthetic.d.ts +11 -0
- package/dist/indexer/rails-synthetic.d.ts.map +1 -0
- package/dist/indexer/rails-synthetic.js +80 -0
- package/dist/indexer/rails-synthetic.js.map +1 -0
- package/dist/indexer/reconcile.d.ts +9 -0
- package/dist/indexer/reconcile.d.ts.map +1 -0
- package/dist/indexer/reconcile.js +96 -0
- package/dist/indexer/reconcile.js.map +1 -0
- package/dist/indexer/resolvers/cpp.d.ts +15 -0
- package/dist/indexer/resolvers/cpp.d.ts.map +1 -0
- package/dist/indexer/resolvers/cpp.js +35 -0
- package/dist/indexer/resolvers/cpp.js.map +1 -0
- package/dist/indexer/resolvers/csharp.d.ts +2 -0
- package/dist/indexer/resolvers/csharp.d.ts.map +1 -0
- package/dist/indexer/resolvers/csharp.js +89 -0
- package/dist/indexer/resolvers/csharp.js.map +1 -0
- package/dist/indexer/resolvers/generic.d.ts +10 -0
- package/dist/indexer/resolvers/generic.d.ts.map +1 -0
- package/dist/indexer/resolvers/generic.js +35 -0
- package/dist/indexer/resolvers/generic.js.map +1 -0
- package/dist/indexer/resolvers/go.d.ts +2 -0
- package/dist/indexer/resolvers/go.d.ts.map +1 -0
- package/dist/indexer/resolvers/go.js +171 -0
- package/dist/indexer/resolvers/go.js.map +1 -0
- package/dist/indexer/resolvers/index.d.ts +26 -0
- package/dist/indexer/resolvers/index.d.ts.map +1 -0
- package/dist/indexer/resolvers/index.js +301 -0
- package/dist/indexer/resolvers/index.js.map +1 -0
- package/dist/indexer/resolvers/java.d.ts +17 -0
- package/dist/indexer/resolvers/java.d.ts.map +1 -0
- package/dist/indexer/resolvers/java.js +104 -0
- package/dist/indexer/resolvers/java.js.map +1 -0
- package/dist/indexer/resolvers/php.d.ts +2 -0
- package/dist/indexer/resolvers/php.d.ts.map +1 -0
- package/dist/indexer/resolvers/php.js +108 -0
- package/dist/indexer/resolvers/php.js.map +1 -0
- package/dist/indexer/resolvers/python.d.ts +17 -0
- package/dist/indexer/resolvers/python.d.ts.map +1 -0
- package/dist/indexer/resolvers/python.js +63 -0
- package/dist/indexer/resolvers/python.js.map +1 -0
- package/dist/indexer/resolvers/ruby.d.ts +13 -0
- package/dist/indexer/resolvers/ruby.d.ts.map +1 -0
- package/dist/indexer/resolvers/ruby.js +199 -0
- package/dist/indexer/resolvers/ruby.js.map +1 -0
- package/dist/indexer/resolvers/rust.d.ts +12 -0
- package/dist/indexer/resolvers/rust.d.ts.map +1 -0
- package/dist/indexer/resolvers/rust.js +61 -0
- package/dist/indexer/resolvers/rust.js.map +1 -0
- package/dist/indexer/resolvers/shared.d.ts +18 -0
- package/dist/indexer/resolvers/shared.d.ts.map +1 -0
- package/dist/indexer/resolvers/shared.js +71 -0
- package/dist/indexer/resolvers/shared.js.map +1 -0
- package/dist/indexer/rust-workspace.d.ts +25 -0
- package/dist/indexer/rust-workspace.d.ts.map +1 -0
- package/dist/indexer/rust-workspace.js +155 -0
- package/dist/indexer/rust-workspace.js.map +1 -0
- package/dist/indexer/symbol-edges.d.ts +19 -0
- package/dist/indexer/symbol-edges.d.ts.map +1 -0
- package/dist/indexer/symbol-edges.js +117 -0
- package/dist/indexer/symbol-edges.js.map +1 -0
- package/dist/indexer/tokenize.d.ts +26 -0
- package/dist/indexer/tokenize.d.ts.map +1 -0
- package/dist/indexer/tokenize.js +200 -0
- package/dist/indexer/tokenize.js.map +1 -0
- package/dist/indexer/ts-parser.d.ts +10 -0
- package/dist/indexer/ts-parser.d.ts.map +1 -0
- package/dist/indexer/ts-parser.js +605 -0
- package/dist/indexer/ts-parser.js.map +1 -0
- package/dist/indexer/walker.d.ts +9 -0
- package/dist/indexer/walker.d.ts.map +1 -0
- package/dist/indexer/walker.js +91 -0
- package/dist/indexer/walker.js.map +1 -0
- package/dist/init.d.ts +111 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +787 -0
- package/dist/init.js.map +1 -0
- package/dist/kb/cli.d.ts +2 -0
- package/dist/kb/cli.d.ts.map +1 -0
- package/dist/kb/cli.js +377 -0
- package/dist/kb/cli.js.map +1 -0
- package/dist/kb/commit.d.ts +6 -0
- package/dist/kb/commit.d.ts.map +1 -0
- package/dist/kb/commit.js +84 -0
- package/dist/kb/commit.js.map +1 -0
- package/dist/kb/fold.d.ts +42 -0
- package/dist/kb/fold.d.ts.map +1 -0
- package/dist/kb/fold.js +285 -0
- package/dist/kb/fold.js.map +1 -0
- package/dist/kb/freshness.d.ts +9 -0
- package/dist/kb/freshness.d.ts.map +1 -0
- package/dist/kb/freshness.js +52 -0
- package/dist/kb/freshness.js.map +1 -0
- package/dist/kb/git.d.ts +4 -0
- package/dist/kb/git.d.ts.map +1 -0
- package/dist/kb/git.js +28 -0
- package/dist/kb/git.js.map +1 -0
- package/dist/kb/ids.d.ts +8 -0
- package/dist/kb/ids.d.ts.map +1 -0
- package/dist/kb/ids.js +42 -0
- package/dist/kb/ids.js.map +1 -0
- package/dist/kb/lint.d.ts +23 -0
- package/dist/kb/lint.d.ts.map +1 -0
- package/dist/kb/lint.js +102 -0
- package/dist/kb/lint.js.map +1 -0
- package/dist/kb/lookup.d.ts +55 -0
- package/dist/kb/lookup.d.ts.map +1 -0
- package/dist/kb/lookup.js +92 -0
- package/dist/kb/lookup.js.map +1 -0
- package/dist/kb/notes-index.d.ts +31 -0
- package/dist/kb/notes-index.d.ts.map +1 -0
- package/dist/kb/notes-index.js +0 -0
- package/dist/kb/notes-index.js.map +1 -0
- package/dist/kb/raw-log.d.ts +34 -0
- package/dist/kb/raw-log.d.ts.map +1 -0
- package/dist/kb/raw-log.js +106 -0
- package/dist/kb/raw-log.js.map +1 -0
- package/dist/kb/render.d.ts +17 -0
- package/dist/kb/render.d.ts.map +1 -0
- package/dist/kb/render.js +101 -0
- package/dist/kb/render.js.map +1 -0
- package/dist/kb/search.d.ts +110 -0
- package/dist/kb/search.d.ts.map +1 -0
- package/dist/kb/search.js +453 -0
- package/dist/kb/search.js.map +1 -0
- package/dist/kb/store.d.ts +35 -0
- package/dist/kb/store.d.ts.map +1 -0
- package/dist/kb/store.js +160 -0
- package/dist/kb/store.js.map +1 -0
- package/dist/kb/types.d.ts +142 -0
- package/dist/kb/types.d.ts.map +1 -0
- package/dist/kb/types.js +13 -0
- package/dist/kb/types.js.map +1 -0
- package/dist/kb/view-template.d.ts +8 -0
- package/dist/kb/view-template.d.ts.map +1 -0
- package/dist/kb/view-template.js +8 -0
- package/dist/kb/view-template.js.map +1 -0
- package/dist/kb/view.d.ts +42 -0
- package/dist/kb/view.d.ts.map +1 -0
- package/dist/kb/view.js +118 -0
- package/dist/kb/view.js.map +1 -0
- package/dist/kb/write.d.ts +72 -0
- package/dist/kb/write.d.ts.map +1 -0
- package/dist/kb/write.js +345 -0
- package/dist/kb/write.js.map +1 -0
- package/dist/keeper-state.d.ts +29 -0
- package/dist/keeper-state.d.ts.map +1 -0
- package/dist/keeper-state.js +89 -0
- package/dist/keeper-state.js.map +1 -0
- package/dist/keeper.d.ts +23 -0
- package/dist/keeper.d.ts.map +1 -0
- package/dist/keeper.js +105 -0
- package/dist/keeper.js.map +1 -0
- package/dist/migrate.d.ts +9 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/migrate.js +102 -0
- package/dist/migrate.js.map +1 -0
- package/dist/restart.d.ts +15 -0
- package/dist/restart.d.ts.map +1 -0
- package/dist/restart.js +48 -0
- package/dist/restart.js.map +1 -0
- package/dist/server/find.d.ts +39 -0
- package/dist/server/find.d.ts.map +1 -0
- package/dist/server/find.js +979 -0
- package/dist/server/find.js.map +1 -0
- package/dist/server/mcp.d.ts +117 -0
- package/dist/server/mcp.d.ts.map +1 -0
- package/dist/server/mcp.js +301 -0
- package/dist/server/mcp.js.map +1 -0
- package/dist/server/searcher.d.ts +14 -0
- package/dist/server/searcher.d.ts.map +1 -0
- package/dist/server/searcher.js +146 -0
- package/dist/server/searcher.js.map +1 -0
- package/dist/server/tools.d.ts +17 -0
- package/dist/server/tools.d.ts.map +1 -0
- package/dist/server/tools.js +875 -0
- package/dist/server/tools.js.map +1 -0
- package/dist/status.d.ts +12 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +157 -0
- package/dist/status.js.map +1 -0
- package/dist/types.d.ts +138 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/watcher.d.ts +11 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +67 -0
- package/dist/watcher.js.map +1 -0
- package/hooks/canonical-find-key.mjs +68 -0
- package/hooks/codex-find-nudge.mjs +15 -0
- package/hooks/codex-find-preguard.mjs +14 -0
- package/hooks/codex-kb-elicit.mjs +329 -0
- package/hooks/codex-kb-recall.mjs +151 -0
- package/hooks/codex-nudge-handler.mjs +370 -0
- package/hooks/codex-preguard-handler.mjs +75 -0
- package/hooks/codex-run-hook.mjs +132 -0
- package/hooks/coldstart-call.mjs +58 -0
- package/hooks/cursor-find-nudge.mjs +15 -0
- package/hooks/cursor-find-preguard.mjs +14 -0
- package/hooks/cursor-input.mjs +72 -0
- package/hooks/cursor-kb-elicit.mjs +335 -0
- package/hooks/cursor-kb-recall.mjs +138 -0
- package/hooks/cursor-nudge-handler.mjs +33 -0
- package/hooks/cursor-preguard-handler.mjs +39 -0
- package/hooks/cursor-run-hook.mjs +129 -0
- package/hooks/find-nudge.mjs +15 -0
- package/hooks/find-preguard.mjs +14 -0
- package/hooks/kb-elicit.mjs +334 -0
- package/hooks/kb-recall.mjs +151 -0
- package/hooks/nudge-handler.mjs +370 -0
- package/hooks/preguard-handler.mjs +75 -0
- package/hooks/run-hook.mjs +132 -0
- package/package.json +69 -0
- package/templates/coldstart.cli.md +60 -0
- package/templates/coldstart.mcp.md +59 -0
|
@@ -0,0 +1,996 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree-sitter based parser for Ruby.
|
|
3
|
+
* Extracts symbol-level nodes (classes, modules, methods, constants)
|
|
4
|
+
* and their relationships (calls, extends, includes/implements).
|
|
5
|
+
*
|
|
6
|
+
* Follows the same interface and patterns as ts-parser.ts.
|
|
7
|
+
*/
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
import { childrenOfType, firstChildOfType, firstChildOfTypes } from './node-helpers.js';
|
|
10
|
+
import { makeParser } from './parser-factory.js';
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
const rubyGrammar = require('tree-sitter-ruby');
|
|
14
|
+
const getParser = makeParser(rubyGrammar);
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Node helpers
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
/** Collect all call node method names + first-seen line in a subtree */
|
|
19
|
+
function collectCalls(node, results) {
|
|
20
|
+
if (node.type === 'call') {
|
|
21
|
+
// call grammar: field('receiver', ...) '.' field('method', identifier|constant) field('arguments', ...)?
|
|
22
|
+
// Use the field accessor — `find(...)` returns the receiver when it's a bare
|
|
23
|
+
// identifier/constant, not the method name.
|
|
24
|
+
const methodNode = node.childForFieldName('method');
|
|
25
|
+
if (methodNode && !results.has(methodNode.text)) {
|
|
26
|
+
results.set(methodNode.text, node.startPosition.row + 1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else if (node.type === 'method_call' || node.type === 'command') {
|
|
30
|
+
// bare method call: method_name args
|
|
31
|
+
const methodNode = node.namedChildren[0];
|
|
32
|
+
if (methodNode?.type === 'identifier' && !results.has(methodNode.text)) {
|
|
33
|
+
results.set(methodNode.text, node.startPosition.row + 1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
for (const child of node.namedChildren) {
|
|
37
|
+
collectCalls(child, results);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function callsFromMap(calls, exclude) {
|
|
41
|
+
const out = [];
|
|
42
|
+
for (const [name, line] of calls) {
|
|
43
|
+
if (exclude && name === exclude)
|
|
44
|
+
continue;
|
|
45
|
+
out.push({ name, line });
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
/** Get constant name from a scope_resolution or constant node */
|
|
50
|
+
function getConstantName(node) {
|
|
51
|
+
if (!node)
|
|
52
|
+
return null;
|
|
53
|
+
if (node.type === 'constant')
|
|
54
|
+
return node.text;
|
|
55
|
+
if (node.type === 'scope_resolution') {
|
|
56
|
+
// Foo::Bar — return the full path
|
|
57
|
+
return node.text;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Rails DSL detection helpers
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
const RAILS_ASSOCIATION_METHODS = new Set(['has_many', 'has_one', 'belongs_to', 'has_and_belongs_to_many']);
|
|
65
|
+
const RAILS_CALLBACK_METHODS = new Set(['before_action', 'after_action', 'around_action', 'before_filter', 'after_filter', 'validates', 'validate']);
|
|
66
|
+
const RAILS_DSL_METHODS = new Set(['attr_accessor', 'attr_reader', 'attr_writer', 'delegate', 'scope', 'attribute', 'enum']);
|
|
67
|
+
// Stoplist for Rails autoload constant resolution — framework/stdlib constants we never resolve
|
|
68
|
+
const CONSTANT_STOPLIST_TOP_LEVEL = new Set([
|
|
69
|
+
// Ruby builtins
|
|
70
|
+
'Object', 'Kernel', 'Module', 'Class', 'BasicObject', 'Proc', 'Method', 'UnboundMethod',
|
|
71
|
+
'String', 'Symbol', 'Integer', 'Float', 'Numeric', 'Rational', 'Complex',
|
|
72
|
+
'TrueClass', 'FalseClass', 'NilClass', 'Array', 'Hash', 'Set', 'Range',
|
|
73
|
+
'Regexp', 'MatchData', 'Time', 'Date', 'DateTime', 'IO', 'File', 'Dir',
|
|
74
|
+
'Pathname', 'URI', 'Tempfile', 'Struct', 'OpenStruct', 'Comparable',
|
|
75
|
+
'Enumerable', 'Enumerator', 'StandardError', 'RuntimeError', 'ArgumentError',
|
|
76
|
+
'TypeError', 'NameError', 'NoMethodError', 'NotImplementedError', 'IOError',
|
|
77
|
+
'EOFError', 'SystemCallError', 'ZeroDivisionError', 'FloatDomainError',
|
|
78
|
+
'IndexError', 'KeyError', 'RangeError', 'StopIteration', 'ThreadError',
|
|
79
|
+
'FiberError', 'LocalJumpError', 'SignalException', 'Errno', 'Encoding',
|
|
80
|
+
'Thread', 'Fiber', 'Mutex', 'Queue', 'ConditionVariable', 'GC',
|
|
81
|
+
'ObjectSpace', 'Process', 'Signal', 'Math', 'Random', 'JSON', 'YAML',
|
|
82
|
+
'CSV', 'Base64', 'Digest', 'OpenSSL', 'Net', 'ERB', 'CGI', 'Logger',
|
|
83
|
+
'SecureRandom', 'FileUtils', 'Forwardable', 'Singleton', 'BigDecimal',
|
|
84
|
+
'Marshal',
|
|
85
|
+
// Rails framework
|
|
86
|
+
'Rails', 'ActiveRecord', 'ActiveModel', 'ActiveSupport', 'ActionController',
|
|
87
|
+
'ActionView', 'ActionDispatch', 'ActionMailer', 'ActionCable', 'ActionPack',
|
|
88
|
+
'ActiveJob', 'ActiveStorage', 'ActionText', 'ActionMailbox',
|
|
89
|
+
'Concern', 'Mime', 'MIME', 'I18n', 'Migration', 'Schema', 'Devise',
|
|
90
|
+
'Doorkeeper', 'Sidekiq', 'Paperclip', 'CarrierWave', 'Pundit', 'Kaminari',
|
|
91
|
+
'WillPaginate', 'RSpec', 'Minitest', 'Faker', 'FactoryBot', 'Webpacker',
|
|
92
|
+
'RSolr', 'Redis', 'Memcached', 'Resque', 'GoodJob', 'Que', 'DelayedJob',
|
|
93
|
+
'Aws', 'Azure', 'Google', 'GraphQL', 'Stripe', 'Twilio', 'Twitter',
|
|
94
|
+
'OmniAuth', 'Bundler', 'Gem', 'Rake', 'Rack', 'Sprockets',
|
|
95
|
+
'Mail', 'Nokogiri', 'HTTP', 'HTTParty', 'Faraday', 'Chewy', 'Elasticsearch',
|
|
96
|
+
]);
|
|
97
|
+
const CONSTANT_STOPLIST_PREFIXES = [
|
|
98
|
+
'ActiveRecord::', 'ActiveModel::', 'ActiveSupport::', 'ActionController::',
|
|
99
|
+
'ActionView::', 'ActionDispatch::', 'ActionMailer::', 'ActionCable::',
|
|
100
|
+
'ActiveJob::', 'ActiveStorage::', 'Rails::', 'Concern::',
|
|
101
|
+
'Devise::', 'Doorkeeper::', 'Sidekiq::', 'Pundit::', 'Kaminari::',
|
|
102
|
+
'OmniAuth::', 'OAuth::', 'OAuth2::', 'JWT::', 'OpenSSL::', 'Net::',
|
|
103
|
+
'URI::', 'Errno::', 'Encoding::', 'Process::', 'Math::', 'File::',
|
|
104
|
+
'IO::', 'Dir::', 'Thread::', 'JSON::', 'YAML::', 'CSV::', 'ERB::',
|
|
105
|
+
'Digest::', 'Base64::', 'Mime::', 'MIME::', 'I18n::', 'Aws::',
|
|
106
|
+
'Google::', 'GraphQL::', 'Stripe::', 'Mail::', 'Nokogiri::',
|
|
107
|
+
'HTTP::', 'HTTParty::', 'Faraday::', 'Chewy::', 'Sprockets::',
|
|
108
|
+
'Bundler::', 'Gem::', 'Rake::', 'Rack::', 'Logger::', 'FactoryBot::', 'RSpec::', 'Minitest::',
|
|
109
|
+
];
|
|
110
|
+
function isConstantStoplisted(fqcn) {
|
|
111
|
+
const head = fqcn.split('::')[0];
|
|
112
|
+
if (CONSTANT_STOPLIST_TOP_LEVEL.has(head))
|
|
113
|
+
return true;
|
|
114
|
+
for (const p of CONSTANT_STOPLIST_PREFIXES)
|
|
115
|
+
if (fqcn.startsWith(p))
|
|
116
|
+
return true;
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
/** Convert snake_case association name to CamelCase model name */
|
|
120
|
+
function associationToModel(name) {
|
|
121
|
+
// has_many :comments → Comment; has_one :profile → Profile
|
|
122
|
+
// Singularize the name if it's plural, convert to CamelCase
|
|
123
|
+
const singular = singularize(name);
|
|
124
|
+
return singular.replace(/_([a-z])/g, (_, c) => c.toUpperCase())
|
|
125
|
+
.replace(/^(.)/, (_, c) => c.toUpperCase());
|
|
126
|
+
}
|
|
127
|
+
/** Singularize plural English nouns (basic rules for Rails conventions) */
|
|
128
|
+
function singularize(word) {
|
|
129
|
+
// Leave already-singular words alone: -us (status, virus, focus), -ss (address, class),
|
|
130
|
+
// -is (basis, axis), -ous (famous). Rails Inflector also treats these as uncountable/no-op.
|
|
131
|
+
if (word.endsWith('us') || word.endsWith('ss') || word.endsWith('is'))
|
|
132
|
+
return word;
|
|
133
|
+
if (word.endsWith('ies'))
|
|
134
|
+
return word.slice(0, -3) + 'y';
|
|
135
|
+
if (word.endsWith('ches') || word.endsWith('shes') || word.endsWith('sses') || word.endsWith('xes') || word.endsWith('zes'))
|
|
136
|
+
return word.slice(0, -2);
|
|
137
|
+
if (word.endsWith('es') && (word.endsWith('oes') || word.endsWith('zes') || word[word.length - 3] === 's' || word[word.length - 3] === 'x' || word[word.length - 3] === 'z' || word[word.length - 3] === 'h'))
|
|
138
|
+
return word.slice(0, -2);
|
|
139
|
+
if (word.endsWith('s'))
|
|
140
|
+
return word.slice(0, -1);
|
|
141
|
+
return word;
|
|
142
|
+
}
|
|
143
|
+
/** Pluralize singular English nouns (basic rules for Rails conventions) */
|
|
144
|
+
function pluralize(word) {
|
|
145
|
+
if (word.endsWith('y'))
|
|
146
|
+
return word.slice(0, -1) + 'ies';
|
|
147
|
+
if (word.endsWith('s') || word.endsWith('x') || word.endsWith('z') || word.endsWith('ch') || word.endsWith('sh'))
|
|
148
|
+
return word + 'es';
|
|
149
|
+
return word + 's';
|
|
150
|
+
}
|
|
151
|
+
/** Extract Rails model association file imports from config/routes.rb */
|
|
152
|
+
function extractRoutesImports(rootNode, _fileId) {
|
|
153
|
+
const routeImports = [];
|
|
154
|
+
function walkNode(node) {
|
|
155
|
+
if (node.type === 'call' || node.type === 'command') {
|
|
156
|
+
const methodNode = node.type === 'call'
|
|
157
|
+
? node.namedChildren.find((c) => c.type === 'identifier')
|
|
158
|
+
: node.namedChildren[0];
|
|
159
|
+
if (!methodNode || methodNode.type !== 'identifier') {
|
|
160
|
+
for (const child of node.namedChildren)
|
|
161
|
+
walkNode(child);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const methodName = methodNode.text;
|
|
165
|
+
if (methodName === 'resources' || methodName === 'resource') {
|
|
166
|
+
const args = firstChildOfType(node, 'argument_list') ?? firstChildOfType(node, 'arguments');
|
|
167
|
+
const firstArg = args?.namedChildren[0] ?? node.namedChildren[1];
|
|
168
|
+
if (firstArg?.type === 'simple_symbol') {
|
|
169
|
+
const resourceName = firstArg.text.replace(/^:/, '');
|
|
170
|
+
const controllerName = methodName === 'resource' ? pluralize(resourceName) : resourceName;
|
|
171
|
+
routeImports.push(`../app/controllers/${controllerName}_controller`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else if (['get', 'post', 'put', 'patch', 'delete'].includes(methodName)) {
|
|
175
|
+
const toTargetRef = { value: null };
|
|
176
|
+
function findToValue(n) {
|
|
177
|
+
if (toTargetRef.value !== null)
|
|
178
|
+
return;
|
|
179
|
+
if (n.type === 'pair' && n.namedChildren.length >= 2) {
|
|
180
|
+
const firstChild = n.namedChildren[0];
|
|
181
|
+
const secondChild = n.namedChildren[1];
|
|
182
|
+
const keyText = firstChild.type === 'hash_key_symbol' ? firstChild.text : (firstChild.text.startsWith(':') ? firstChild.text.slice(1) : firstChild.text);
|
|
183
|
+
if ((firstChild.type === 'hash_key_symbol' || firstChild.type === 'simple_symbol') && keyText === 'to') {
|
|
184
|
+
if (secondChild.type === 'string' || secondChild.type === 'string_literal') {
|
|
185
|
+
const content = firstChildOfType(secondChild, 'string_content');
|
|
186
|
+
toTargetRef.value = content?.text ?? secondChild.text.replace(/^['"]|['"]$/g, '');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
for (const child of n.namedChildren) {
|
|
191
|
+
findToValue(child);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
findToValue(node);
|
|
195
|
+
const toTarget = toTargetRef.value;
|
|
196
|
+
if (toTarget && toTarget.includes('#')) {
|
|
197
|
+
const [controller, _action] = toTarget.split('#');
|
|
198
|
+
const parts = controller.split('/');
|
|
199
|
+
const fileName = parts[parts.length - 1];
|
|
200
|
+
const dir = parts.slice(0, -1).join('/');
|
|
201
|
+
const controllerPath = dir ? `../app/controllers/${dir}/${fileName}_controller` : `../app/controllers/${fileName}_controller`;
|
|
202
|
+
routeImports.push(controllerPath);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
for (const child of node.namedChildren) {
|
|
207
|
+
walkNode(child);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
walkNode(rootNode);
|
|
211
|
+
return routeImports;
|
|
212
|
+
}
|
|
213
|
+
/** Extract DSL method names from command node arguments (bare method call form) */
|
|
214
|
+
function extractDslMethodsFromCommand(methodName, parentName, node, ctx, symbols) {
|
|
215
|
+
if (!parentName)
|
|
216
|
+
return;
|
|
217
|
+
const startLine = node.startPosition.row + 1;
|
|
218
|
+
const endLine = node.endPosition.row + 1;
|
|
219
|
+
const argList = node.namedChildren.slice(1);
|
|
220
|
+
if (methodName === 'attr_accessor') {
|
|
221
|
+
for (const arg of argList) {
|
|
222
|
+
if (arg.type === 'simple_symbol') {
|
|
223
|
+
const propName = arg.text.replace(/^:/, '');
|
|
224
|
+
symbols.push({
|
|
225
|
+
id: `${ctx.fileId}#${parentName}.${propName}`,
|
|
226
|
+
name: `${parentName}.${propName}`,
|
|
227
|
+
kind: 'method',
|
|
228
|
+
startLine,
|
|
229
|
+
endLine,
|
|
230
|
+
isExported: false,
|
|
231
|
+
calls: [],
|
|
232
|
+
implementsNames: [],
|
|
233
|
+
});
|
|
234
|
+
symbols.push({
|
|
235
|
+
id: `${ctx.fileId}#${parentName}.${propName}=`,
|
|
236
|
+
name: `${parentName}.${propName}=`,
|
|
237
|
+
kind: 'method',
|
|
238
|
+
startLine,
|
|
239
|
+
endLine,
|
|
240
|
+
isExported: false,
|
|
241
|
+
calls: [],
|
|
242
|
+
implementsNames: [],
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else if (methodName === 'attr_reader') {
|
|
248
|
+
for (const arg of argList) {
|
|
249
|
+
if (arg.type === 'simple_symbol') {
|
|
250
|
+
const propName = arg.text.replace(/^:/, '');
|
|
251
|
+
symbols.push({
|
|
252
|
+
id: `${ctx.fileId}#${parentName}.${propName}`,
|
|
253
|
+
name: `${parentName}.${propName}`,
|
|
254
|
+
kind: 'method',
|
|
255
|
+
startLine,
|
|
256
|
+
endLine,
|
|
257
|
+
isExported: false,
|
|
258
|
+
calls: [],
|
|
259
|
+
implementsNames: [],
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
else if (methodName === 'attr_writer') {
|
|
265
|
+
for (const arg of argList) {
|
|
266
|
+
if (arg.type === 'simple_symbol') {
|
|
267
|
+
const propName = arg.text.replace(/^:/, '');
|
|
268
|
+
symbols.push({
|
|
269
|
+
id: `${ctx.fileId}#${parentName}.${propName}=`,
|
|
270
|
+
name: `${parentName}.${propName}=`,
|
|
271
|
+
kind: 'method',
|
|
272
|
+
startLine,
|
|
273
|
+
endLine,
|
|
274
|
+
isExported: false,
|
|
275
|
+
calls: [],
|
|
276
|
+
implementsNames: [],
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
else if (methodName === 'delegate') {
|
|
282
|
+
for (const arg of argList) {
|
|
283
|
+
if (arg.type === 'simple_symbol') {
|
|
284
|
+
const delegatedName = arg.text.replace(/^:/, '');
|
|
285
|
+
symbols.push({
|
|
286
|
+
id: `${ctx.fileId}#${parentName}.${delegatedName}`,
|
|
287
|
+
name: `${parentName}.${delegatedName}`,
|
|
288
|
+
kind: 'method',
|
|
289
|
+
startLine,
|
|
290
|
+
endLine,
|
|
291
|
+
isExported: false,
|
|
292
|
+
calls: [],
|
|
293
|
+
implementsNames: [],
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
else if (methodName === 'scope') {
|
|
302
|
+
const firstArg = argList[0];
|
|
303
|
+
if (firstArg?.type === 'simple_symbol') {
|
|
304
|
+
const scopeName = firstArg.text.replace(/^:/, '');
|
|
305
|
+
symbols.push({
|
|
306
|
+
id: `${ctx.fileId}#${parentName}.${scopeName}`,
|
|
307
|
+
name: `${parentName}.${scopeName}`,
|
|
308
|
+
kind: 'method',
|
|
309
|
+
startLine,
|
|
310
|
+
endLine,
|
|
311
|
+
isExported: false,
|
|
312
|
+
calls: [],
|
|
313
|
+
implementsNames: [],
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
else if (methodName === 'attribute') {
|
|
318
|
+
const firstArg = argList[0];
|
|
319
|
+
if (firstArg?.type === 'simple_symbol') {
|
|
320
|
+
const attrName = firstArg.text.replace(/^:/, '');
|
|
321
|
+
symbols.push({
|
|
322
|
+
id: `${ctx.fileId}#${parentName}.${attrName}`,
|
|
323
|
+
name: `${parentName}.${attrName}`,
|
|
324
|
+
kind: 'method',
|
|
325
|
+
startLine,
|
|
326
|
+
endLine,
|
|
327
|
+
isExported: false,
|
|
328
|
+
calls: [],
|
|
329
|
+
implementsNames: [],
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
else if (methodName === 'enum') {
|
|
334
|
+
const firstArg = argList[0];
|
|
335
|
+
if (firstArg?.type === 'pair') {
|
|
336
|
+
const keyNode = firstChildOfTypes(firstArg, ['hash_key_symbol', 'simple_symbol', 'symbol']);
|
|
337
|
+
if (keyNode) {
|
|
338
|
+
const enumName = keyNode.text.replace(/^:/, '');
|
|
339
|
+
symbols.push({
|
|
340
|
+
id: `${ctx.fileId}#${parentName}.${enumName}`,
|
|
341
|
+
name: `${parentName}.${enumName}`,
|
|
342
|
+
kind: 'method',
|
|
343
|
+
startLine,
|
|
344
|
+
endLine,
|
|
345
|
+
isExported: false,
|
|
346
|
+
calls: [],
|
|
347
|
+
implementsNames: [],
|
|
348
|
+
});
|
|
349
|
+
const valueNode = firstChildOfType(firstArg, 'hash');
|
|
350
|
+
if (valueNode) {
|
|
351
|
+
const valuePairs = childrenOfType(valueNode, 'pair');
|
|
352
|
+
for (const pair of valuePairs) {
|
|
353
|
+
const pairKey = firstChildOfTypes(pair, ['hash_key_symbol', 'simple_symbol', 'symbol']);
|
|
354
|
+
if (pairKey) {
|
|
355
|
+
const valueName = pairKey.text.replace(/^:/, '');
|
|
356
|
+
symbols.push({
|
|
357
|
+
id: `${ctx.fileId}#${parentName}.${valueName}?`,
|
|
358
|
+
name: `${parentName}.${valueName}?`,
|
|
359
|
+
kind: 'method',
|
|
360
|
+
startLine,
|
|
361
|
+
endLine,
|
|
362
|
+
isExported: false,
|
|
363
|
+
calls: [],
|
|
364
|
+
implementsNames: [],
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
/** Extract synthesized method names from Rails DSL calls and emit SymbolNode for each */
|
|
374
|
+
function extractDslMethods(node, methodName, parentName, ctx, symbols) {
|
|
375
|
+
if (!parentName)
|
|
376
|
+
return;
|
|
377
|
+
const startLine = node.startPosition.row + 1;
|
|
378
|
+
const endLine = node.endPosition.row + 1;
|
|
379
|
+
const args = firstChildOfType(node, 'argument_list') ?? firstChildOfType(node, 'arguments');
|
|
380
|
+
if (!args)
|
|
381
|
+
return;
|
|
382
|
+
const argList = args.namedChildren;
|
|
383
|
+
if (methodName === 'attr_accessor') {
|
|
384
|
+
// attr_accessor :name, :age → emit name, name=, age, age=
|
|
385
|
+
for (const arg of argList) {
|
|
386
|
+
if (arg.type === 'simple_symbol') {
|
|
387
|
+
const propName = arg.text.replace(/^:/, '');
|
|
388
|
+
symbols.push({
|
|
389
|
+
id: `${ctx.fileId}#${parentName}.${propName}`,
|
|
390
|
+
name: `${parentName}.${propName}`,
|
|
391
|
+
kind: 'method',
|
|
392
|
+
startLine,
|
|
393
|
+
endLine,
|
|
394
|
+
isExported: false,
|
|
395
|
+
calls: [],
|
|
396
|
+
implementsNames: [],
|
|
397
|
+
});
|
|
398
|
+
symbols.push({
|
|
399
|
+
id: `${ctx.fileId}#${parentName}.${propName}=`,
|
|
400
|
+
name: `${parentName}.${propName}=`,
|
|
401
|
+
kind: 'method',
|
|
402
|
+
startLine,
|
|
403
|
+
endLine,
|
|
404
|
+
isExported: false,
|
|
405
|
+
calls: [],
|
|
406
|
+
implementsNames: [],
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
else if (methodName === 'attr_reader') {
|
|
412
|
+
// attr_reader :name, :age → emit name, age
|
|
413
|
+
for (const arg of argList) {
|
|
414
|
+
if (arg.type === 'simple_symbol') {
|
|
415
|
+
const propName = arg.text.replace(/^:/, '');
|
|
416
|
+
symbols.push({
|
|
417
|
+
id: `${ctx.fileId}#${parentName}.${propName}`,
|
|
418
|
+
name: `${parentName}.${propName}`,
|
|
419
|
+
kind: 'method',
|
|
420
|
+
startLine,
|
|
421
|
+
endLine,
|
|
422
|
+
isExported: false,
|
|
423
|
+
calls: [],
|
|
424
|
+
implementsNames: [],
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
else if (methodName === 'attr_writer') {
|
|
430
|
+
// attr_writer :name, :age → emit name=, age=
|
|
431
|
+
for (const arg of argList) {
|
|
432
|
+
if (arg.type === 'simple_symbol') {
|
|
433
|
+
const propName = arg.text.replace(/^:/, '');
|
|
434
|
+
symbols.push({
|
|
435
|
+
id: `${ctx.fileId}#${parentName}.${propName}=`,
|
|
436
|
+
name: `${parentName}.${propName}=`,
|
|
437
|
+
kind: 'method',
|
|
438
|
+
startLine,
|
|
439
|
+
endLine,
|
|
440
|
+
isExported: false,
|
|
441
|
+
calls: [],
|
|
442
|
+
implementsNames: [],
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
else if (methodName === 'delegate') {
|
|
448
|
+
// delegate :foo, :bar, to: :user → emit foo, bar (stop at first non-symbol arg)
|
|
449
|
+
for (const arg of argList) {
|
|
450
|
+
if (arg.type === 'simple_symbol') {
|
|
451
|
+
const delegatedName = arg.text.replace(/^:/, '');
|
|
452
|
+
symbols.push({
|
|
453
|
+
id: `${ctx.fileId}#${parentName}.${delegatedName}`,
|
|
454
|
+
name: `${parentName}.${delegatedName}`,
|
|
455
|
+
kind: 'method',
|
|
456
|
+
startLine,
|
|
457
|
+
endLine,
|
|
458
|
+
isExported: false,
|
|
459
|
+
calls: [],
|
|
460
|
+
implementsNames: [],
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
// Stop at first non-symbol arg (hash with to:)
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
else if (methodName === 'scope') {
|
|
470
|
+
// scope :recent, -> { ... } → emit recent (class method)
|
|
471
|
+
const firstArg = argList[0];
|
|
472
|
+
if (firstArg?.type === 'simple_symbol') {
|
|
473
|
+
const scopeName = firstArg.text.replace(/^:/, '');
|
|
474
|
+
symbols.push({
|
|
475
|
+
id: `${ctx.fileId}#${parentName}.${scopeName}`,
|
|
476
|
+
name: `${parentName}.${scopeName}`,
|
|
477
|
+
kind: 'method',
|
|
478
|
+
startLine,
|
|
479
|
+
endLine,
|
|
480
|
+
isExported: false,
|
|
481
|
+
calls: [],
|
|
482
|
+
implementsNames: [],
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
else if (methodName === 'attribute') {
|
|
487
|
+
// attribute :webfinger, :string → emit webfinger
|
|
488
|
+
const firstArg = argList[0];
|
|
489
|
+
if (firstArg?.type === 'simple_symbol') {
|
|
490
|
+
const attrName = firstArg.text.replace(/^:/, '');
|
|
491
|
+
symbols.push({
|
|
492
|
+
id: `${ctx.fileId}#${parentName}.${attrName}`,
|
|
493
|
+
name: `${parentName}.${attrName}`,
|
|
494
|
+
kind: 'method',
|
|
495
|
+
startLine,
|
|
496
|
+
endLine,
|
|
497
|
+
isExported: false,
|
|
498
|
+
calls: [],
|
|
499
|
+
implementsNames: [],
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
else if (methodName === 'enum') {
|
|
504
|
+
// enum status: { active: 0, inactive: 1 } → emit status, active?, inactive?
|
|
505
|
+
const firstArg = argList[0];
|
|
506
|
+
if (firstArg?.type === 'pair') {
|
|
507
|
+
const keyNode = firstChildOfTypes(firstArg, ['hash_key_symbol', 'simple_symbol', 'symbol']);
|
|
508
|
+
if (keyNode) {
|
|
509
|
+
const enumName = keyNode.text.replace(/^:/, '');
|
|
510
|
+
symbols.push({
|
|
511
|
+
id: `${ctx.fileId}#${parentName}.${enumName}`,
|
|
512
|
+
name: `${parentName}.${enumName}`,
|
|
513
|
+
kind: 'method',
|
|
514
|
+
startLine,
|
|
515
|
+
endLine,
|
|
516
|
+
isExported: false,
|
|
517
|
+
calls: [],
|
|
518
|
+
implementsNames: [],
|
|
519
|
+
});
|
|
520
|
+
const valueNode = firstChildOfType(firstArg, 'hash');
|
|
521
|
+
if (valueNode) {
|
|
522
|
+
const valuePairs = childrenOfType(valueNode, 'pair');
|
|
523
|
+
for (const pair of valuePairs) {
|
|
524
|
+
const pairKey = firstChildOfTypes(pair, ['hash_key_symbol', 'simple_symbol', 'symbol']);
|
|
525
|
+
if (pairKey) {
|
|
526
|
+
const valueName = pairKey.text.replace(/^:/, '');
|
|
527
|
+
symbols.push({
|
|
528
|
+
id: `${ctx.fileId}#${parentName}.${valueName}?`,
|
|
529
|
+
name: `${parentName}.${valueName}?`,
|
|
530
|
+
kind: 'method',
|
|
531
|
+
startLine,
|
|
532
|
+
endLine,
|
|
533
|
+
isExported: false,
|
|
534
|
+
calls: [],
|
|
535
|
+
implementsNames: [],
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
// ---------------------------------------------------------------------------
|
|
545
|
+
// Extract method definition
|
|
546
|
+
// ---------------------------------------------------------------------------
|
|
547
|
+
function extractMethod(node, ctx, parentName) {
|
|
548
|
+
const startLine = node.startPosition.row + 1;
|
|
549
|
+
const endLine = node.endPosition.row + 1;
|
|
550
|
+
let methodName;
|
|
551
|
+
let isSingleton = false;
|
|
552
|
+
if (node.type === 'singleton_method') {
|
|
553
|
+
// def self.foo; singleton_method has: object '.' name body_statement
|
|
554
|
+
isSingleton = true;
|
|
555
|
+
const nameNode = node.namedChildren.find((c) => c.type === 'identifier');
|
|
556
|
+
if (!nameNode)
|
|
557
|
+
return null;
|
|
558
|
+
methodName = nameNode.text;
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
// method: 'def' name body_statement 'end'
|
|
562
|
+
const nameNode = firstChildOfTypes(node, ['identifier', 'operator']);
|
|
563
|
+
if (!nameNode)
|
|
564
|
+
return null;
|
|
565
|
+
methodName = nameNode.text;
|
|
566
|
+
}
|
|
567
|
+
const fullName = parentName
|
|
568
|
+
? isSingleton
|
|
569
|
+
? `${parentName}.self.${methodName}`
|
|
570
|
+
: `${parentName}.${methodName}`
|
|
571
|
+
: methodName;
|
|
572
|
+
const calls = new Map();
|
|
573
|
+
const body = firstChildOfTypes(node, ['body_statement', 'do_block']);
|
|
574
|
+
if (body)
|
|
575
|
+
collectCalls(body, calls);
|
|
576
|
+
// also scan the whole node for calls
|
|
577
|
+
if (!body)
|
|
578
|
+
collectCalls(node, calls);
|
|
579
|
+
return {
|
|
580
|
+
id: `${ctx.fileId}#${fullName}`,
|
|
581
|
+
name: fullName,
|
|
582
|
+
kind: parentName ? 'method' : 'function',
|
|
583
|
+
startLine,
|
|
584
|
+
endLine,
|
|
585
|
+
isExported: !parentName, // top-level methods are "exported" by default in Ruby
|
|
586
|
+
calls: callsFromMap(calls, methodName),
|
|
587
|
+
implementsNames: [],
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
// ---------------------------------------------------------------------------
|
|
591
|
+
// Extract a class or module body
|
|
592
|
+
// ---------------------------------------------------------------------------
|
|
593
|
+
function extractBody(bodyNode, ctx, parentName, symbols) {
|
|
594
|
+
if (!bodyNode)
|
|
595
|
+
return;
|
|
596
|
+
for (const child of bodyNode.namedChildren) {
|
|
597
|
+
extractNode(child, ctx, parentName, symbols);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
// ---------------------------------------------------------------------------
|
|
601
|
+
// Extract a single node
|
|
602
|
+
// ---------------------------------------------------------------------------
|
|
603
|
+
function extractNode(node, ctx, parentName, symbols) {
|
|
604
|
+
const startLine = node.startPosition.row + 1;
|
|
605
|
+
const endLine = node.endPosition.row + 1;
|
|
606
|
+
switch (node.type) {
|
|
607
|
+
case 'class': {
|
|
608
|
+
// class Foo [< Bar] ... end
|
|
609
|
+
const nameNode = firstChildOfTypes(node, ['constant', 'scope_resolution']);
|
|
610
|
+
if (!nameNode)
|
|
611
|
+
return;
|
|
612
|
+
const name = parentName ? `${parentName}::${nameNode.text}` : nameNode.text;
|
|
613
|
+
// superclass: class Foo < Bar — tree-sitter exposes the superclass as a child
|
|
614
|
+
let extendsName;
|
|
615
|
+
const superclassNode = firstChildOfType(node, 'superclass');
|
|
616
|
+
if (superclassNode) {
|
|
617
|
+
const superNode = firstChildOfTypes(superclassNode, ['constant', 'scope_resolution']);
|
|
618
|
+
if (superNode)
|
|
619
|
+
extendsName = superNode.text;
|
|
620
|
+
}
|
|
621
|
+
const implementsNames = [];
|
|
622
|
+
const classSymbol = {
|
|
623
|
+
id: `${ctx.fileId}#${name}`,
|
|
624
|
+
name,
|
|
625
|
+
kind: 'class',
|
|
626
|
+
startLine,
|
|
627
|
+
endLine,
|
|
628
|
+
isExported: true, // Ruby classes are always public
|
|
629
|
+
calls: [],
|
|
630
|
+
extendsName,
|
|
631
|
+
implementsNames,
|
|
632
|
+
};
|
|
633
|
+
symbols.push(classSymbol);
|
|
634
|
+
// Extract body
|
|
635
|
+
const body = firstChildOfType(node, 'body_statement');
|
|
636
|
+
if (body)
|
|
637
|
+
extractBody(body, ctx, name, symbols);
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
case 'module': {
|
|
641
|
+
const nameNode = firstChildOfTypes(node, ['constant', 'scope_resolution']);
|
|
642
|
+
if (!nameNode)
|
|
643
|
+
return;
|
|
644
|
+
const name = parentName ? `${parentName}::${nameNode.text}` : nameNode.text;
|
|
645
|
+
const modSymbol = {
|
|
646
|
+
id: `${ctx.fileId}#${name}`,
|
|
647
|
+
name,
|
|
648
|
+
kind: 'class', // modules modeled as class
|
|
649
|
+
startLine,
|
|
650
|
+
endLine,
|
|
651
|
+
isExported: true,
|
|
652
|
+
calls: [],
|
|
653
|
+
implementsNames: [],
|
|
654
|
+
};
|
|
655
|
+
symbols.push(modSymbol);
|
|
656
|
+
const body = firstChildOfType(node, 'body_statement');
|
|
657
|
+
if (body)
|
|
658
|
+
extractBody(body, ctx, name, symbols);
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
case 'method':
|
|
662
|
+
case 'singleton_method': {
|
|
663
|
+
const sym = extractMethod(node, ctx, parentName);
|
|
664
|
+
if (sym)
|
|
665
|
+
symbols.push(sym);
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
case 'assignment': {
|
|
669
|
+
// Constant assignment: FOO = ...
|
|
670
|
+
const leftNode = node.namedChildren[0];
|
|
671
|
+
if (!leftNode)
|
|
672
|
+
return;
|
|
673
|
+
// Constants start with uppercase
|
|
674
|
+
if (leftNode.type === 'constant' && /^[A-Z]/.test(leftNode.text)) {
|
|
675
|
+
const constName = parentName ? `${parentName}::${leftNode.text}` : leftNode.text;
|
|
676
|
+
symbols.push({
|
|
677
|
+
id: `${ctx.fileId}#${constName}`,
|
|
678
|
+
name: constName,
|
|
679
|
+
kind: 'constant',
|
|
680
|
+
startLine,
|
|
681
|
+
endLine,
|
|
682
|
+
isExported: true,
|
|
683
|
+
calls: [],
|
|
684
|
+
implementsNames: [],
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
case 'call': {
|
|
690
|
+
// Detect Rails DSLs: include/extend/prepend (implement edges)
|
|
691
|
+
// and has_many/belongs_to/before_action etc.
|
|
692
|
+
if (!parentName)
|
|
693
|
+
return;
|
|
694
|
+
const receiver = node.namedChildren[0];
|
|
695
|
+
const methodNode = node.namedChildren.find((c) => c.type === 'identifier');
|
|
696
|
+
if (!methodNode)
|
|
697
|
+
return;
|
|
698
|
+
const methodName = methodNode.text;
|
|
699
|
+
if (methodName === 'include' || methodName === 'extend' || methodName === 'prepend') {
|
|
700
|
+
// include Foo → implements edge
|
|
701
|
+
const args = firstChildOfType(node, 'argument_list') ?? firstChildOfType(node, 'arguments');
|
|
702
|
+
if (args) {
|
|
703
|
+
for (const arg of args.namedChildren) {
|
|
704
|
+
const modName = getConstantName(arg);
|
|
705
|
+
if (modName) {
|
|
706
|
+
// Add to parent class's implementsNames (post-process)
|
|
707
|
+
ctx.extraCalls.push({ fromId: `${ctx.fileId}#${parentName}`, toName: modName });
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
else if (RAILS_ASSOCIATION_METHODS.has(methodName)) {
|
|
713
|
+
// has_many :comments → Comment model
|
|
714
|
+
const args = firstChildOfType(node, 'argument_list') ?? firstChildOfType(node, 'arguments');
|
|
715
|
+
const firstArg = args?.namedChildren[0];
|
|
716
|
+
if (firstArg?.type === 'simple_symbol') {
|
|
717
|
+
const assocName = firstArg.text.replace(/^:/, '');
|
|
718
|
+
const modelName = associationToModel(assocName);
|
|
719
|
+
ctx.extraCalls.push({ fromId: `${ctx.fileId}#${parentName}`, toName: modelName });
|
|
720
|
+
ctx.associationTargets.push(modelName);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
else if (RAILS_CALLBACK_METHODS.has(methodName)) {
|
|
724
|
+
// before_action :method_name → calls edge within same class
|
|
725
|
+
const args = firstChildOfType(node, 'argument_list') ?? firstChildOfType(node, 'arguments');
|
|
726
|
+
const firstArg = args?.namedChildren[0];
|
|
727
|
+
if (firstArg?.type === 'simple_symbol') {
|
|
728
|
+
const refMethod = firstArg.text.replace(/^:/, '');
|
|
729
|
+
ctx.extraCalls.push({ fromId: `${ctx.fileId}#${parentName}`, toName: refMethod });
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
else if (RAILS_DSL_METHODS.has(methodName)) {
|
|
733
|
+
// attr_accessor, delegate, scope, enum, etc. → emit method symbols
|
|
734
|
+
extractDslMethods(node, methodName, parentName, ctx, symbols);
|
|
735
|
+
}
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
// Handle bare method calls at class scope (include, has_many, etc.)
|
|
739
|
+
case 'command': {
|
|
740
|
+
if (!parentName)
|
|
741
|
+
return;
|
|
742
|
+
const methodNode = node.namedChildren[0];
|
|
743
|
+
if (!methodNode || methodNode.type !== 'identifier')
|
|
744
|
+
return;
|
|
745
|
+
const methodName = methodNode.text;
|
|
746
|
+
if (methodName === 'include' || methodName === 'extend' || methodName === 'prepend') {
|
|
747
|
+
const args = node.namedChildren.slice(1);
|
|
748
|
+
for (const arg of args) {
|
|
749
|
+
const modName = getConstantName(arg);
|
|
750
|
+
if (modName) {
|
|
751
|
+
ctx.extraCalls.push({ fromId: `${ctx.fileId}#${parentName}`, toName: modName });
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
else if (RAILS_ASSOCIATION_METHODS.has(methodName)) {
|
|
756
|
+
const firstArg = node.namedChildren[1];
|
|
757
|
+
if (firstArg?.type === 'simple_symbol') {
|
|
758
|
+
const assocName = firstArg.text.replace(/^:/, '');
|
|
759
|
+
const modelName = associationToModel(assocName);
|
|
760
|
+
ctx.extraCalls.push({ fromId: `${ctx.fileId}#${parentName}`, toName: modelName });
|
|
761
|
+
ctx.associationTargets.push(modelName);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
else if (RAILS_CALLBACK_METHODS.has(methodName)) {
|
|
765
|
+
const firstArg = node.namedChildren[1];
|
|
766
|
+
if (firstArg?.type === 'simple_symbol') {
|
|
767
|
+
const refMethod = firstArg.text.replace(/^:/, '');
|
|
768
|
+
ctx.extraCalls.push({ fromId: `${ctx.fileId}#${parentName}`, toName: refMethod });
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
else if (RAILS_DSL_METHODS.has(methodName)) {
|
|
772
|
+
extractDslMethodsFromCommand(methodName, parentName, node, ctx, symbols);
|
|
773
|
+
}
|
|
774
|
+
break;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
// ---------------------------------------------------------------------------
|
|
779
|
+
// Constant and render collection (Rails autoload + render)
|
|
780
|
+
// ---------------------------------------------------------------------------
|
|
781
|
+
function isConstantDefinitionContext(node) {
|
|
782
|
+
// True if this constant is a definition (class/module name, assignment LHS), not a reference.
|
|
783
|
+
const p = node.parent;
|
|
784
|
+
if (!p)
|
|
785
|
+
return false;
|
|
786
|
+
// class Foo / module Foo — constant is first named child, don't resolve
|
|
787
|
+
if ((p.type === 'class' || p.type === 'module') && p.namedChildren[0] === node)
|
|
788
|
+
return true;
|
|
789
|
+
// Assignment LHS (FOO = 1) — don't resolve
|
|
790
|
+
if (p.type === 'assignment' && p.namedChildren[0] === node)
|
|
791
|
+
return true;
|
|
792
|
+
return false;
|
|
793
|
+
}
|
|
794
|
+
function shouldSkipConstantContext(node) {
|
|
795
|
+
// Skip if we're inside include/extend/prepend (already handled) or has_many/etc.
|
|
796
|
+
let p = node.parent;
|
|
797
|
+
while (p) {
|
|
798
|
+
if (p.type === 'call' || p.type === 'command') {
|
|
799
|
+
const mNode = p.type === 'call'
|
|
800
|
+
? p.childForFieldName?.('method')
|
|
801
|
+
: p.namedChildren[0];
|
|
802
|
+
const m = mNode?.text;
|
|
803
|
+
if (m === 'include' || m === 'extend' || m === 'prepend')
|
|
804
|
+
return true;
|
|
805
|
+
}
|
|
806
|
+
// Don't walk past statement boundaries
|
|
807
|
+
if (p.type === 'body_statement' || p.type === 'program' || p.type === 'method' || p.type === 'class' || p.type === 'module')
|
|
808
|
+
break;
|
|
809
|
+
p = p.parent;
|
|
810
|
+
}
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Collect constant references as ordered candidate groups, mirroring Ruby's
|
|
815
|
+
* lexical-nesting constant lookup. For a reference `R` seen inside `module A;
|
|
816
|
+
* module B`, Ruby tries `A::B::R`, `A::R`, then top-level `R` — so we emit
|
|
817
|
+
* `[A::B::R, A::R, R]` (innermost-first, bare last). The resolver tries each in
|
|
818
|
+
* order and takes the first that maps to a real file. This recovers references
|
|
819
|
+
* to namespaced files (e.g. bare `Invite` → `app/models/members/invite.rb`)
|
|
820
|
+
* that a literal-only lookup misses, and avoids wrong edges to top-level
|
|
821
|
+
* homonyms.
|
|
822
|
+
*
|
|
823
|
+
* `nesting` is the enclosing class/module FQCNs, innermost-first (= Module.nesting).
|
|
824
|
+
* Compact definitions (`class A::B`) don't add intermediate namespaces, matching Ruby.
|
|
825
|
+
*/
|
|
826
|
+
function collectConstantReferences(root) {
|
|
827
|
+
const out = [];
|
|
828
|
+
const seen = new Set();
|
|
829
|
+
function emit(ref, nesting) {
|
|
830
|
+
const candidates = [...nesting.map(n => `${n}::${ref}`), ref];
|
|
831
|
+
const key = candidates.join('>');
|
|
832
|
+
if (seen.has(key))
|
|
833
|
+
return;
|
|
834
|
+
seen.add(key);
|
|
835
|
+
out.push(candidates);
|
|
836
|
+
}
|
|
837
|
+
function visit(node, nesting) {
|
|
838
|
+
if (node.type === 'class' || node.type === 'module') {
|
|
839
|
+
// Compute this scope's FQCN the same way extractNode does (parentName::name).
|
|
840
|
+
const nameNode = firstChildOfTypes(node, ['constant', 'scope_resolution']);
|
|
841
|
+
const text = nameNode?.text;
|
|
842
|
+
const newName = text ? (nesting.length ? `${nesting[0]}::${text}` : text) : nesting[0];
|
|
843
|
+
const newNesting = newName ? [newName, ...nesting] : nesting;
|
|
844
|
+
// Body resolves under the new nesting; the name/superclass resolve in the
|
|
845
|
+
// OUTER nesting (Ruby resolves `class Foo < Bar` and `Foo::X` before Foo is open).
|
|
846
|
+
const body = firstChildOfType(node, 'body_statement');
|
|
847
|
+
for (const c of node.namedChildren) {
|
|
848
|
+
visit(c, c === body ? newNesting : nesting);
|
|
849
|
+
}
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
if (node.type === 'constant' || node.type === 'scope_resolution') {
|
|
853
|
+
if (!isConstantDefinitionContext(node) && !shouldSkipConstantContext(node)) {
|
|
854
|
+
const txt = node.text;
|
|
855
|
+
if (!isConstantStoplisted(txt))
|
|
856
|
+
emit(txt, nesting);
|
|
857
|
+
}
|
|
858
|
+
// Don't descend into scope_resolution children — we want the full path.
|
|
859
|
+
if (node.type === 'scope_resolution')
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
for (const c of node.namedChildren)
|
|
863
|
+
visit(c, nesting);
|
|
864
|
+
}
|
|
865
|
+
visit(root, []);
|
|
866
|
+
return out;
|
|
867
|
+
}
|
|
868
|
+
const RUBY_MAX_STRING = 32000;
|
|
869
|
+
const RUBY_CHUNK_SIZE = 4096;
|
|
870
|
+
function parseContent(parser, content) {
|
|
871
|
+
if (content.length <= RUBY_MAX_STRING) {
|
|
872
|
+
return parser.parse(content);
|
|
873
|
+
}
|
|
874
|
+
return parser.parse((startIndex) => {
|
|
875
|
+
if (startIndex >= content.length)
|
|
876
|
+
return null;
|
|
877
|
+
return content.slice(startIndex, startIndex + RUBY_CHUNK_SIZE);
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
export function parseRubyContent(content, fileId) {
|
|
881
|
+
const parser = getParser();
|
|
882
|
+
let tree;
|
|
883
|
+
try {
|
|
884
|
+
tree = parseContent(parser, content);
|
|
885
|
+
}
|
|
886
|
+
catch (err) {
|
|
887
|
+
throw new Error(`Tree-sitter failed to parse Ruby (${content.length} chars): ${err}`);
|
|
888
|
+
}
|
|
889
|
+
const root = tree.rootNode;
|
|
890
|
+
const imports = [];
|
|
891
|
+
const exports = [];
|
|
892
|
+
const rawSymbols = [];
|
|
893
|
+
const ctx = {
|
|
894
|
+
fileId,
|
|
895
|
+
allSymbolNames: new Set(),
|
|
896
|
+
extraCalls: [],
|
|
897
|
+
associationTargets: [],
|
|
898
|
+
};
|
|
899
|
+
// First pass: collect all require/require_relative calls
|
|
900
|
+
function collectRequires(node) {
|
|
901
|
+
if (node.type === 'call') {
|
|
902
|
+
const methodNode = node.namedChildren.find((c) => c.type === 'identifier');
|
|
903
|
+
const isRelative = methodNode?.text === 'require_relative';
|
|
904
|
+
if (isRelative || methodNode?.text === 'require') {
|
|
905
|
+
const args = firstChildOfType(node, 'argument_list') ?? firstChildOfType(node, 'arguments');
|
|
906
|
+
const strNode = args
|
|
907
|
+
? firstChildOfTypes(args, ['string', 'string_literal'])
|
|
908
|
+
: firstChildOfTypes(node, ['string', 'string_literal']);
|
|
909
|
+
if (strNode) {
|
|
910
|
+
const content = firstChildOfType(strNode, 'string_content');
|
|
911
|
+
const raw = content?.text ?? strNode.text.replace(/^['"]|['"]$/g, '');
|
|
912
|
+
if (raw)
|
|
913
|
+
imports.push(normalizeRequire(raw, isRelative));
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
else if (node.type === 'command') {
|
|
918
|
+
const methodNode = node.namedChildren[0];
|
|
919
|
+
const isRelative = methodNode?.text === 'require_relative';
|
|
920
|
+
if (methodNode?.type === 'identifier' && (isRelative || methodNode.text === 'require')) {
|
|
921
|
+
const strNode = node.namedChildren.find((c) => c.type === 'string' || c.type === 'string_literal');
|
|
922
|
+
if (strNode) {
|
|
923
|
+
const content = firstChildOfType(strNode, 'string_content');
|
|
924
|
+
const raw = content?.text ?? strNode.text.replace(/^['"]|['"]$/g, '');
|
|
925
|
+
if (raw)
|
|
926
|
+
imports.push(normalizeRequire(raw, isRelative));
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
for (const child of node.namedChildren) {
|
|
931
|
+
collectRequires(child);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
function normalizeRequire(raw, isRelative) {
|
|
935
|
+
if (isRelative && !raw.startsWith('.') && !raw.startsWith('/')) {
|
|
936
|
+
return './' + raw;
|
|
937
|
+
}
|
|
938
|
+
return raw;
|
|
939
|
+
}
|
|
940
|
+
collectRequires(root);
|
|
941
|
+
// Second pass: extract symbols from top-level nodes
|
|
942
|
+
for (const node of root.namedChildren) {
|
|
943
|
+
extractNode(node, ctx, null, rawSymbols);
|
|
944
|
+
}
|
|
945
|
+
// Build exports from public symbols
|
|
946
|
+
for (const sym of rawSymbols) {
|
|
947
|
+
if (sym.isExported && sym.kind !== 'method') {
|
|
948
|
+
exports.push(sym.name);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
// Apply extra calls (include/extend/prepend → implementsNames, Rails associations → calls)
|
|
952
|
+
const symbolById = new Map(rawSymbols.map(s => [s.id, s]));
|
|
953
|
+
for (const { fromId, toName } of ctx.extraCalls) {
|
|
954
|
+
const sym = symbolById.get(fromId);
|
|
955
|
+
if (sym) {
|
|
956
|
+
// For include/extend/prepend: add to implementsNames
|
|
957
|
+
// For Rails associations/callbacks: add to calls
|
|
958
|
+
sym.implementsNames.push(toName);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
// For Rails models (files under app/models/), convert association names to file-level imports
|
|
962
|
+
if (fileId.includes('app/models/')) {
|
|
963
|
+
for (const modelName of ctx.associationTargets) {
|
|
964
|
+
const modelSnake = modelName.replace(/([A-Z])/g, '_$1').slice(1).toLowerCase();
|
|
965
|
+
const importPath = `./${modelSnake}`;
|
|
966
|
+
if (!imports.includes(importPath)) {
|
|
967
|
+
imports.push(importPath);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
// Extract routes.rb controller imports
|
|
972
|
+
if (fileId.endsWith('config/routes.rb')) {
|
|
973
|
+
const routeImports = extractRoutesImports(root, fileId);
|
|
974
|
+
for (const imp of routeImports) {
|
|
975
|
+
if (!imports.includes(imp)) {
|
|
976
|
+
imports.push(imp);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
// Collect Rails autoload constant references
|
|
981
|
+
const constantReferences = collectConstantReferences(root);
|
|
982
|
+
// Resolve intra-file calls: replace plain names with full IDs
|
|
983
|
+
const symbolIdByName = new Map(rawSymbols.map(s => [s.name, s.id]));
|
|
984
|
+
const resolvedSymbols = rawSymbols.map(sym => ({
|
|
985
|
+
...sym,
|
|
986
|
+
calls: sym.calls.map(c => ({ name: symbolIdByName.get(c.name) ?? c.name, line: c.line })),
|
|
987
|
+
}));
|
|
988
|
+
return {
|
|
989
|
+
imports: [...new Set(imports)],
|
|
990
|
+
exports: [...new Set(exports)],
|
|
991
|
+
hasDefaultExport: false,
|
|
992
|
+
symbols: resolvedSymbols,
|
|
993
|
+
constantReferences: constantReferences.length > 0 ? constantReferences : undefined,
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
//# sourceMappingURL=ruby.js.map
|