spec-gen-cli 1.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/LICENSE +21 -0
- package/README.md +1078 -0
- package/dist/api/analyze.d.ts +17 -0
- package/dist/api/analyze.d.ts.map +1 -0
- package/dist/api/analyze.js +109 -0
- package/dist/api/analyze.js.map +1 -0
- package/dist/api/drift.d.ts +21 -0
- package/dist/api/drift.d.ts.map +1 -0
- package/dist/api/drift.js +145 -0
- package/dist/api/drift.js.map +1 -0
- package/dist/api/generate.d.ts +18 -0
- package/dist/api/generate.d.ts.map +1 -0
- package/dist/api/generate.js +251 -0
- package/dist/api/generate.js.map +1 -0
- package/dist/api/index.d.ts +39 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +32 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/init.d.ts +18 -0
- package/dist/api/init.d.ts.map +1 -0
- package/dist/api/init.js +82 -0
- package/dist/api/init.js.map +1 -0
- package/dist/api/run.d.ts +19 -0
- package/dist/api/run.d.ts.map +1 -0
- package/dist/api/run.js +291 -0
- package/dist/api/run.js.map +1 -0
- package/dist/api/specs.d.ts +49 -0
- package/dist/api/specs.d.ts.map +1 -0
- package/dist/api/specs.js +136 -0
- package/dist/api/specs.js.map +1 -0
- package/dist/api/types.d.ts +176 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +9 -0
- package/dist/api/types.js.map +1 -0
- package/dist/api/verify.d.ts +20 -0
- package/dist/api/verify.d.ts.map +1 -0
- package/dist/api/verify.js +117 -0
- package/dist/api/verify.js.map +1 -0
- package/dist/cli/commands/analyze.d.ts +27 -0
- package/dist/cli/commands/analyze.d.ts.map +1 -0
- package/dist/cli/commands/analyze.js +485 -0
- package/dist/cli/commands/analyze.js.map +1 -0
- package/dist/cli/commands/drift.d.ts +9 -0
- package/dist/cli/commands/drift.d.ts.map +1 -0
- package/dist/cli/commands/drift.js +540 -0
- package/dist/cli/commands/drift.js.map +1 -0
- package/dist/cli/commands/generate.d.ts +9 -0
- package/dist/cli/commands/generate.d.ts.map +1 -0
- package/dist/cli/commands/generate.js +633 -0
- package/dist/cli/commands/generate.js.map +1 -0
- package/dist/cli/commands/init.d.ts +9 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +171 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/mcp.d.ts +638 -0
- package/dist/cli/commands/mcp.d.ts.map +1 -0
- package/dist/cli/commands/mcp.js +574 -0
- package/dist/cli/commands/mcp.js.map +1 -0
- package/dist/cli/commands/run.d.ts +24 -0
- package/dist/cli/commands/run.d.ts.map +1 -0
- package/dist/cli/commands/run.js +546 -0
- package/dist/cli/commands/run.js.map +1 -0
- package/dist/cli/commands/verify.d.ts +9 -0
- package/dist/cli/commands/verify.d.ts.map +1 -0
- package/dist/cli/commands/verify.js +417 -0
- package/dist/cli/commands/verify.js.map +1 -0
- package/dist/cli/commands/view.d.ts +9 -0
- package/dist/cli/commands/view.d.ts.map +1 -0
- package/dist/cli/commands/view.js +511 -0
- package/dist/cli/commands/view.js.map +1 -0
- package/dist/cli/index.d.ts +9 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +83 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/analyzer/architecture-writer.d.ts +67 -0
- package/dist/core/analyzer/architecture-writer.d.ts.map +1 -0
- package/dist/core/analyzer/architecture-writer.js +209 -0
- package/dist/core/analyzer/architecture-writer.js.map +1 -0
- package/dist/core/analyzer/artifact-generator.d.ts +222 -0
- package/dist/core/analyzer/artifact-generator.d.ts.map +1 -0
- package/dist/core/analyzer/artifact-generator.js +726 -0
- package/dist/core/analyzer/artifact-generator.js.map +1 -0
- package/dist/core/analyzer/call-graph.d.ts +83 -0
- package/dist/core/analyzer/call-graph.d.ts.map +1 -0
- package/dist/core/analyzer/call-graph.js +827 -0
- package/dist/core/analyzer/call-graph.js.map +1 -0
- package/dist/core/analyzer/code-shaper.d.ts +33 -0
- package/dist/core/analyzer/code-shaper.d.ts.map +1 -0
- package/dist/core/analyzer/code-shaper.js +149 -0
- package/dist/core/analyzer/code-shaper.js.map +1 -0
- package/dist/core/analyzer/dependency-graph.d.ts +179 -0
- package/dist/core/analyzer/dependency-graph.d.ts.map +1 -0
- package/dist/core/analyzer/dependency-graph.js +574 -0
- package/dist/core/analyzer/dependency-graph.js.map +1 -0
- package/dist/core/analyzer/duplicate-detector.d.ts +52 -0
- package/dist/core/analyzer/duplicate-detector.d.ts.map +1 -0
- package/dist/core/analyzer/duplicate-detector.js +279 -0
- package/dist/core/analyzer/duplicate-detector.js.map +1 -0
- package/dist/core/analyzer/embedding-service.d.ts +50 -0
- package/dist/core/analyzer/embedding-service.d.ts.map +1 -0
- package/dist/core/analyzer/embedding-service.js +104 -0
- package/dist/core/analyzer/embedding-service.js.map +1 -0
- package/dist/core/analyzer/file-walker.d.ts +78 -0
- package/dist/core/analyzer/file-walker.d.ts.map +1 -0
- package/dist/core/analyzer/file-walker.js +531 -0
- package/dist/core/analyzer/file-walker.js.map +1 -0
- package/dist/core/analyzer/import-parser.d.ts +91 -0
- package/dist/core/analyzer/import-parser.d.ts.map +1 -0
- package/dist/core/analyzer/import-parser.js +720 -0
- package/dist/core/analyzer/import-parser.js.map +1 -0
- package/dist/core/analyzer/index.d.ts +10 -0
- package/dist/core/analyzer/index.d.ts.map +1 -0
- package/dist/core/analyzer/index.js +10 -0
- package/dist/core/analyzer/index.js.map +1 -0
- package/dist/core/analyzer/refactor-analyzer.d.ts +80 -0
- package/dist/core/analyzer/refactor-analyzer.d.ts.map +1 -0
- package/dist/core/analyzer/refactor-analyzer.js +339 -0
- package/dist/core/analyzer/refactor-analyzer.js.map +1 -0
- package/dist/core/analyzer/repository-mapper.d.ts +150 -0
- package/dist/core/analyzer/repository-mapper.d.ts.map +1 -0
- package/dist/core/analyzer/repository-mapper.js +731 -0
- package/dist/core/analyzer/repository-mapper.js.map +1 -0
- package/dist/core/analyzer/signature-extractor.d.ts +31 -0
- package/dist/core/analyzer/signature-extractor.d.ts.map +1 -0
- package/dist/core/analyzer/signature-extractor.js +387 -0
- package/dist/core/analyzer/signature-extractor.js.map +1 -0
- package/dist/core/analyzer/significance-scorer.d.ts +79 -0
- package/dist/core/analyzer/significance-scorer.d.ts.map +1 -0
- package/dist/core/analyzer/significance-scorer.js +407 -0
- package/dist/core/analyzer/significance-scorer.js.map +1 -0
- package/dist/core/analyzer/subgraph-extractor.d.ts +43 -0
- package/dist/core/analyzer/subgraph-extractor.d.ts.map +1 -0
- package/dist/core/analyzer/subgraph-extractor.js +129 -0
- package/dist/core/analyzer/subgraph-extractor.js.map +1 -0
- package/dist/core/analyzer/vector-index.d.ts +63 -0
- package/dist/core/analyzer/vector-index.d.ts.map +1 -0
- package/dist/core/analyzer/vector-index.js +169 -0
- package/dist/core/analyzer/vector-index.js.map +1 -0
- package/dist/core/drift/drift-detector.d.ts +102 -0
- package/dist/core/drift/drift-detector.d.ts.map +1 -0
- package/dist/core/drift/drift-detector.js +597 -0
- package/dist/core/drift/drift-detector.js.map +1 -0
- package/dist/core/drift/git-diff.d.ts +55 -0
- package/dist/core/drift/git-diff.d.ts.map +1 -0
- package/dist/core/drift/git-diff.js +356 -0
- package/dist/core/drift/git-diff.js.map +1 -0
- package/dist/core/drift/index.d.ts +12 -0
- package/dist/core/drift/index.d.ts.map +1 -0
- package/dist/core/drift/index.js +9 -0
- package/dist/core/drift/index.js.map +1 -0
- package/dist/core/drift/spec-mapper.d.ts +73 -0
- package/dist/core/drift/spec-mapper.d.ts.map +1 -0
- package/dist/core/drift/spec-mapper.js +353 -0
- package/dist/core/drift/spec-mapper.js.map +1 -0
- package/dist/core/generator/adr-generator.d.ts +32 -0
- package/dist/core/generator/adr-generator.d.ts.map +1 -0
- package/dist/core/generator/adr-generator.js +192 -0
- package/dist/core/generator/adr-generator.js.map +1 -0
- package/dist/core/generator/index.d.ts +9 -0
- package/dist/core/generator/index.d.ts.map +1 -0
- package/dist/core/generator/index.js +12 -0
- package/dist/core/generator/index.js.map +1 -0
- package/dist/core/generator/mapping-generator.d.ts +54 -0
- package/dist/core/generator/mapping-generator.d.ts.map +1 -0
- package/dist/core/generator/mapping-generator.js +239 -0
- package/dist/core/generator/mapping-generator.js.map +1 -0
- package/dist/core/generator/openspec-compat.d.ts +160 -0
- package/dist/core/generator/openspec-compat.d.ts.map +1 -0
- package/dist/core/generator/openspec-compat.js +523 -0
- package/dist/core/generator/openspec-compat.js.map +1 -0
- package/dist/core/generator/openspec-format-generator.d.ts +111 -0
- package/dist/core/generator/openspec-format-generator.d.ts.map +1 -0
- package/dist/core/generator/openspec-format-generator.js +817 -0
- package/dist/core/generator/openspec-format-generator.js.map +1 -0
- package/dist/core/generator/openspec-writer.d.ts +131 -0
- package/dist/core/generator/openspec-writer.d.ts.map +1 -0
- package/dist/core/generator/openspec-writer.js +379 -0
- package/dist/core/generator/openspec-writer.js.map +1 -0
- package/dist/core/generator/prompts.d.ts +35 -0
- package/dist/core/generator/prompts.d.ts.map +1 -0
- package/dist/core/generator/prompts.js +212 -0
- package/dist/core/generator/prompts.js.map +1 -0
- package/dist/core/generator/spec-pipeline.d.ts +94 -0
- package/dist/core/generator/spec-pipeline.d.ts.map +1 -0
- package/dist/core/generator/spec-pipeline.js +474 -0
- package/dist/core/generator/spec-pipeline.js.map +1 -0
- package/dist/core/generator/stages/stage1-survey.d.ts +19 -0
- package/dist/core/generator/stages/stage1-survey.d.ts.map +1 -0
- package/dist/core/generator/stages/stage1-survey.js +105 -0
- package/dist/core/generator/stages/stage1-survey.js.map +1 -0
- package/dist/core/generator/stages/stage2-entities.d.ts +11 -0
- package/dist/core/generator/stages/stage2-entities.d.ts.map +1 -0
- package/dist/core/generator/stages/stage2-entities.js +67 -0
- package/dist/core/generator/stages/stage2-entities.js.map +1 -0
- package/dist/core/generator/stages/stage3-services.d.ts +11 -0
- package/dist/core/generator/stages/stage3-services.d.ts.map +1 -0
- package/dist/core/generator/stages/stage3-services.js +75 -0
- package/dist/core/generator/stages/stage3-services.js.map +1 -0
- package/dist/core/generator/stages/stage4-api.d.ts +11 -0
- package/dist/core/generator/stages/stage4-api.d.ts.map +1 -0
- package/dist/core/generator/stages/stage4-api.js +65 -0
- package/dist/core/generator/stages/stage4-api.js.map +1 -0
- package/dist/core/generator/stages/stage5-architecture.d.ts +10 -0
- package/dist/core/generator/stages/stage5-architecture.d.ts.map +1 -0
- package/dist/core/generator/stages/stage5-architecture.js +62 -0
- package/dist/core/generator/stages/stage5-architecture.js.map +1 -0
- package/dist/core/generator/stages/stage6-adr.d.ts +8 -0
- package/dist/core/generator/stages/stage6-adr.d.ts.map +1 -0
- package/dist/core/generator/stages/stage6-adr.js +41 -0
- package/dist/core/generator/stages/stage6-adr.js.map +1 -0
- package/dist/core/services/chat-agent.d.ts +45 -0
- package/dist/core/services/chat-agent.d.ts.map +1 -0
- package/dist/core/services/chat-agent.js +310 -0
- package/dist/core/services/chat-agent.js.map +1 -0
- package/dist/core/services/chat-tools.d.ts +32 -0
- package/dist/core/services/chat-tools.d.ts.map +1 -0
- package/dist/core/services/chat-tools.js +270 -0
- package/dist/core/services/chat-tools.js.map +1 -0
- package/dist/core/services/config-manager.d.ts +61 -0
- package/dist/core/services/config-manager.d.ts.map +1 -0
- package/dist/core/services/config-manager.js +143 -0
- package/dist/core/services/config-manager.js.map +1 -0
- package/dist/core/services/gitignore-manager.d.ts +29 -0
- package/dist/core/services/gitignore-manager.d.ts.map +1 -0
- package/dist/core/services/gitignore-manager.js +106 -0
- package/dist/core/services/gitignore-manager.js.map +1 -0
- package/dist/core/services/index.d.ts +8 -0
- package/dist/core/services/index.d.ts.map +1 -0
- package/dist/core/services/index.js +8 -0
- package/dist/core/services/index.js.map +1 -0
- package/dist/core/services/llm-service.d.ts +336 -0
- package/dist/core/services/llm-service.d.ts.map +1 -0
- package/dist/core/services/llm-service.js +1155 -0
- package/dist/core/services/llm-service.js.map +1 -0
- package/dist/core/services/mcp-handlers/analysis.d.ts +42 -0
- package/dist/core/services/mcp-handlers/analysis.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/analysis.js +300 -0
- package/dist/core/services/mcp-handlers/analysis.js.map +1 -0
- package/dist/core/services/mcp-handlers/graph.d.ts +65 -0
- package/dist/core/services/mcp-handlers/graph.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/graph.js +509 -0
- package/dist/core/services/mcp-handlers/graph.js.map +1 -0
- package/dist/core/services/mcp-handlers/semantic.d.ts +38 -0
- package/dist/core/services/mcp-handlers/semantic.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/semantic.js +172 -0
- package/dist/core/services/mcp-handlers/semantic.js.map +1 -0
- package/dist/core/services/mcp-handlers/utils.d.ts +21 -0
- package/dist/core/services/mcp-handlers/utils.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/utils.js +62 -0
- package/dist/core/services/mcp-handlers/utils.js.map +1 -0
- package/dist/core/services/project-detector.d.ts +32 -0
- package/dist/core/services/project-detector.d.ts.map +1 -0
- package/dist/core/services/project-detector.js +111 -0
- package/dist/core/services/project-detector.js.map +1 -0
- package/dist/core/verifier/index.d.ts +5 -0
- package/dist/core/verifier/index.d.ts.map +1 -0
- package/dist/core/verifier/index.js +5 -0
- package/dist/core/verifier/index.js.map +1 -0
- package/dist/core/verifier/verification-engine.d.ts +226 -0
- package/dist/core/verifier/verification-engine.d.ts.map +1 -0
- package/dist/core/verifier/verification-engine.js +681 -0
- package/dist/core/verifier/verification-engine.js.map +1 -0
- package/dist/types/index.d.ts +252 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/pipeline.d.ts +148 -0
- package/dist/types/pipeline.d.ts.map +1 -0
- package/dist/types/pipeline.js +5 -0
- package/dist/types/pipeline.js.map +1 -0
- package/dist/utils/errors.d.ts +51 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +128 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/logger.d.ts +149 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +331 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/progress.d.ts +142 -0
- package/dist/utils/progress.d.ts.map +1 -0
- package/dist/utils/progress.js +280 -0
- package/dist/utils/progress.js.map +1 -0
- package/dist/utils/prompts.d.ts +53 -0
- package/dist/utils/prompts.d.ts.map +1 -0
- package/dist/utils/prompts.js +199 -0
- package/dist/utils/prompts.js.map +1 -0
- package/dist/utils/shutdown.d.ts +89 -0
- package/dist/utils/shutdown.d.ts.map +1 -0
- package/dist/utils/shutdown.js +237 -0
- package/dist/utils/shutdown.js.map +1 -0
- package/package.json +114 -0
- package/src/viewer/InteractiveGraphViewer.jsx +1486 -0
- package/src/viewer/app/index.html +17 -0
- package/src/viewer/app/main.jsx +13 -0
- package/src/viewer/components/ArchitectureView.jsx +177 -0
- package/src/viewer/components/ChatPanel.jsx +448 -0
- package/src/viewer/components/ClusterGraph.jsx +441 -0
- package/src/viewer/components/FilterBar.jsx +179 -0
- package/src/viewer/components/FlatGraph.jsx +275 -0
- package/src/viewer/components/MicroComponents.jsx +83 -0
- package/src/viewer/hooks/usePanZoom.js +79 -0
- package/src/viewer/utils/constants.js +47 -0
- package/src/viewer/utils/graph-helpers.js +291 -0
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Call Graph Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Performs static analysis of function calls across source files using tree-sitter.
|
|
5
|
+
* Supports TypeScript/JavaScript, Python, Go, Rust, Ruby, Java — no LLM, pure AST.
|
|
6
|
+
*
|
|
7
|
+
* Produces:
|
|
8
|
+
* - FunctionNode[] — all identified functions/methods
|
|
9
|
+
* - CallEdge[] — resolved function→function call relationships
|
|
10
|
+
* - Hub functions — high-fanIn nodes (called by many others)
|
|
11
|
+
* - Entry points — functions with no internal callers
|
|
12
|
+
* - Layer violations — cross-layer calls in the wrong direction
|
|
13
|
+
*/
|
|
14
|
+
import Parser from 'tree-sitter';
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// CONSTANTS
|
|
17
|
+
// ============================================================================
|
|
18
|
+
const HUB_THRESHOLD = 5;
|
|
19
|
+
/** Common builtins and stdlib names to ignore as call targets (across all languages) */
|
|
20
|
+
const IGNORED_CALLEES = new Set([
|
|
21
|
+
// Python builtins
|
|
22
|
+
'print', 'len', 'range', 'str', 'int', 'float', 'list', 'dict', 'set', 'tuple',
|
|
23
|
+
'bool', 'type', 'isinstance', 'issubclass', 'hasattr', 'getattr', 'setattr',
|
|
24
|
+
'enumerate', 'zip', 'map', 'filter', 'sorted', 'reversed', 'sum', 'min', 'max',
|
|
25
|
+
'open', 'input', 'format', 'repr', 'id', 'hash', 'abs', 'round', 'pow',
|
|
26
|
+
'super', 'object', 'property', 'staticmethod', 'classmethod',
|
|
27
|
+
// JS/TS common
|
|
28
|
+
'console', 'log', 'error', 'warn', 'JSON', 'parse', 'stringify',
|
|
29
|
+
'Promise', 'resolve', 'reject', 'then', 'catch', 'finally',
|
|
30
|
+
'Array', 'Object', 'String', 'Number', 'Boolean', 'Math', 'Date',
|
|
31
|
+
'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
|
|
32
|
+
'require', 'import', 'exports',
|
|
33
|
+
// Python control flow (used like functions sometimes)
|
|
34
|
+
'assert', 'raise', 'return', 'yield', 'await', 'pass', 'del',
|
|
35
|
+
// Node.js common
|
|
36
|
+
'readFile', 'writeFile', 'mkdir', 'join', 'resolve', 'basename', 'dirname',
|
|
37
|
+
'existsSync', 'readFileSync', 'writeFileSync',
|
|
38
|
+
// Go builtins
|
|
39
|
+
'make', 'new', 'append', 'copy', 'delete', 'close', 'panic', 'recover',
|
|
40
|
+
'println', 'printf', 'sprintf', 'errorf', 'fprintf',
|
|
41
|
+
// Rust macros / common stdlib
|
|
42
|
+
'println', 'eprintln', 'format', 'vec', 'assert', 'unwrap', 'expect',
|
|
43
|
+
'ok', 'err', 'some', 'none',
|
|
44
|
+
// Ruby builtins
|
|
45
|
+
'puts', 'print', 'p', 'raise', 'require', 'require_relative', 'include',
|
|
46
|
+
'extend', 'attr_accessor', 'attr_reader', 'attr_writer',
|
|
47
|
+
// Java common
|
|
48
|
+
'toString', 'equals', 'hashCode', 'getClass', 'println', 'printf',
|
|
49
|
+
]);
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// PARSER SINGLETONS (lazy init)
|
|
52
|
+
// ============================================================================
|
|
53
|
+
let _tsParser;
|
|
54
|
+
let _pyParser;
|
|
55
|
+
let _goParser;
|
|
56
|
+
let _rustParser;
|
|
57
|
+
let _rubyParser;
|
|
58
|
+
let _javaParser;
|
|
59
|
+
let _TsLanguage;
|
|
60
|
+
let _PyLanguage;
|
|
61
|
+
let _GoLanguage;
|
|
62
|
+
let _RustLanguage;
|
|
63
|
+
let _RubyLanguage;
|
|
64
|
+
let _JavaLanguage;
|
|
65
|
+
async function getTSParser() {
|
|
66
|
+
if (!_tsParser) {
|
|
67
|
+
const tsModule = await import('tree-sitter-typescript');
|
|
68
|
+
_TsLanguage = tsModule.default.typescript;
|
|
69
|
+
_tsParser = new Parser();
|
|
70
|
+
_tsParser.setLanguage(_TsLanguage);
|
|
71
|
+
}
|
|
72
|
+
return { parser: _tsParser, lang: _TsLanguage };
|
|
73
|
+
}
|
|
74
|
+
async function getPyParser() {
|
|
75
|
+
if (!_pyParser) {
|
|
76
|
+
const pyModule = await import('tree-sitter-python');
|
|
77
|
+
_PyLanguage = pyModule.default;
|
|
78
|
+
_pyParser = new Parser();
|
|
79
|
+
_pyParser.setLanguage(_PyLanguage);
|
|
80
|
+
}
|
|
81
|
+
return { parser: _pyParser, lang: _PyLanguage };
|
|
82
|
+
}
|
|
83
|
+
async function getGoParser() {
|
|
84
|
+
if (!_goParser) {
|
|
85
|
+
const goModule = await import('tree-sitter-go');
|
|
86
|
+
_GoLanguage = goModule.default;
|
|
87
|
+
_goParser = new Parser();
|
|
88
|
+
_goParser.setLanguage(_GoLanguage);
|
|
89
|
+
}
|
|
90
|
+
return { parser: _goParser, lang: _GoLanguage };
|
|
91
|
+
}
|
|
92
|
+
async function getRustParser() {
|
|
93
|
+
if (!_rustParser) {
|
|
94
|
+
const rustModule = await import('tree-sitter-rust');
|
|
95
|
+
_RustLanguage = rustModule.default;
|
|
96
|
+
_rustParser = new Parser();
|
|
97
|
+
_rustParser.setLanguage(_RustLanguage);
|
|
98
|
+
}
|
|
99
|
+
return { parser: _rustParser, lang: _RustLanguage };
|
|
100
|
+
}
|
|
101
|
+
async function getRubyParser() {
|
|
102
|
+
if (!_rubyParser) {
|
|
103
|
+
const rubyModule = await import('tree-sitter-ruby');
|
|
104
|
+
_RubyLanguage = rubyModule.default;
|
|
105
|
+
_rubyParser = new Parser();
|
|
106
|
+
_rubyParser.setLanguage(_RubyLanguage);
|
|
107
|
+
}
|
|
108
|
+
return { parser: _rubyParser, lang: _RubyLanguage };
|
|
109
|
+
}
|
|
110
|
+
async function getJavaParser() {
|
|
111
|
+
if (!_javaParser) {
|
|
112
|
+
const javaModule = await import('tree-sitter-java');
|
|
113
|
+
_JavaLanguage = javaModule.default;
|
|
114
|
+
_javaParser = new Parser();
|
|
115
|
+
_javaParser.setLanguage(_JavaLanguage);
|
|
116
|
+
}
|
|
117
|
+
return { parser: _javaParser, lang: _JavaLanguage };
|
|
118
|
+
}
|
|
119
|
+
// ============================================================================
|
|
120
|
+
// ATTRIBUTION HELPER
|
|
121
|
+
// ============================================================================
|
|
122
|
+
/**
|
|
123
|
+
* Given a list of function nodes (with startIndex/endIndex) and a call position,
|
|
124
|
+
* find the narrowest enclosing function node.
|
|
125
|
+
*/
|
|
126
|
+
function findEnclosingFunction(nodes, callPos) {
|
|
127
|
+
let best;
|
|
128
|
+
let bestSize = Infinity;
|
|
129
|
+
for (const n of nodes) {
|
|
130
|
+
if (n.startIndex <= callPos && callPos < n.endIndex) {
|
|
131
|
+
const size = n.endIndex - n.startIndex;
|
|
132
|
+
if (size < bestSize) {
|
|
133
|
+
bestSize = size;
|
|
134
|
+
best = n;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return best;
|
|
139
|
+
}
|
|
140
|
+
// ============================================================================
|
|
141
|
+
// TYPESCRIPT EXTRACTOR
|
|
142
|
+
// ============================================================================
|
|
143
|
+
const TS_FN_QUERY = `
|
|
144
|
+
(function_declaration
|
|
145
|
+
name: (identifier) @fn.name) @fn.node
|
|
146
|
+
|
|
147
|
+
(export_statement
|
|
148
|
+
declaration: (function_declaration
|
|
149
|
+
name: (identifier) @fn.name)) @fn.node
|
|
150
|
+
|
|
151
|
+
(method_definition
|
|
152
|
+
name: (property_identifier) @fn.name) @fn.node
|
|
153
|
+
|
|
154
|
+
(lexical_declaration
|
|
155
|
+
(variable_declarator
|
|
156
|
+
name: (identifier) @fn.name
|
|
157
|
+
value: [(arrow_function) (function_expression)] @fn.value)) @fn.node
|
|
158
|
+
`;
|
|
159
|
+
const TS_CALL_QUERY = `
|
|
160
|
+
(call_expression
|
|
161
|
+
function: [(identifier) @call.name
|
|
162
|
+
(member_expression
|
|
163
|
+
property: (property_identifier) @call.name)]) @call.node
|
|
164
|
+
`;
|
|
165
|
+
async function extractTSGraph(filePath, content) {
|
|
166
|
+
const { parser, lang } = await getTSParser();
|
|
167
|
+
const tree = parser.parse(content);
|
|
168
|
+
const fnQuery = new Parser.Query(lang, TS_FN_QUERY);
|
|
169
|
+
const callQuery = new Parser.Query(lang, TS_CALL_QUERY);
|
|
170
|
+
// --- Extract function nodes ---
|
|
171
|
+
const nodes = [];
|
|
172
|
+
const fnMatches = fnQuery.matches(tree.rootNode);
|
|
173
|
+
for (const match of fnMatches) {
|
|
174
|
+
const nameCapture = match.captures.find(c => c.name === 'fn.name');
|
|
175
|
+
const nodeCapture = match.captures.find(c => c.name === 'fn.node');
|
|
176
|
+
if (!nameCapture || !nodeCapture)
|
|
177
|
+
continue;
|
|
178
|
+
const name = nameCapture.node.text;
|
|
179
|
+
const fnNode = nodeCapture.node;
|
|
180
|
+
// Find enclosing class (walk up — skip class_body, its children are methods not the name)
|
|
181
|
+
let className;
|
|
182
|
+
let cursor = fnNode.parent;
|
|
183
|
+
while (cursor) {
|
|
184
|
+
if (cursor.type === 'class_declaration') {
|
|
185
|
+
const classNameNode = cursor.children.find(c => c.type === 'type_identifier' || c.type === 'identifier');
|
|
186
|
+
if (classNameNode)
|
|
187
|
+
className = classNameNode.text;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
cursor = cursor.parent;
|
|
191
|
+
}
|
|
192
|
+
// Detect async (method_definition has 'async' as first named child keyword)
|
|
193
|
+
const isAsync = fnNode.children.some(c => c.type === 'async') ||
|
|
194
|
+
fnNode.text.startsWith('async ');
|
|
195
|
+
const id = className
|
|
196
|
+
? `${filePath}::${className}.${name}`
|
|
197
|
+
: `${filePath}::${name}`;
|
|
198
|
+
nodes.push({
|
|
199
|
+
id,
|
|
200
|
+
name,
|
|
201
|
+
filePath,
|
|
202
|
+
className,
|
|
203
|
+
isAsync,
|
|
204
|
+
language: 'TypeScript',
|
|
205
|
+
startIndex: fnNode.startIndex,
|
|
206
|
+
endIndex: fnNode.endIndex,
|
|
207
|
+
fanIn: 0,
|
|
208
|
+
fanOut: 0,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
// --- Extract calls ---
|
|
212
|
+
const rawEdges = [];
|
|
213
|
+
const callMatches = callQuery.matches(tree.rootNode);
|
|
214
|
+
for (const match of callMatches) {
|
|
215
|
+
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
216
|
+
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
217
|
+
if (!nameCapture || !nodeCapture)
|
|
218
|
+
continue;
|
|
219
|
+
const calleeName = nameCapture.node.text;
|
|
220
|
+
if (IGNORED_CALLEES.has(calleeName))
|
|
221
|
+
continue;
|
|
222
|
+
const callPos = nodeCapture.node.startIndex;
|
|
223
|
+
const caller = findEnclosingFunction(nodes, callPos);
|
|
224
|
+
if (!caller)
|
|
225
|
+
continue;
|
|
226
|
+
rawEdges.push({
|
|
227
|
+
callerId: caller.id,
|
|
228
|
+
calleeName,
|
|
229
|
+
line: nodeCapture.node.startPosition.row + 1,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return { nodes, rawEdges };
|
|
233
|
+
}
|
|
234
|
+
// ============================================================================
|
|
235
|
+
// PYTHON EXTRACTOR
|
|
236
|
+
// ============================================================================
|
|
237
|
+
const PY_FN_QUERY = `
|
|
238
|
+
(function_definition
|
|
239
|
+
name: (identifier) @fn.name) @fn.node
|
|
240
|
+
|
|
241
|
+
(decorated_definition
|
|
242
|
+
(function_definition
|
|
243
|
+
name: (identifier) @fn.name)) @fn.node
|
|
244
|
+
`;
|
|
245
|
+
/**
|
|
246
|
+
* Direct function calls: foo(), bar(x)
|
|
247
|
+
* We keep this separate from attribute calls so we can filter attribute calls
|
|
248
|
+
* by object name (only self/cls are resolved to internal functions).
|
|
249
|
+
*/
|
|
250
|
+
const PY_DIRECT_CALL_QUERY = `
|
|
251
|
+
(call
|
|
252
|
+
function: (identifier) @call.name) @call.node
|
|
253
|
+
`;
|
|
254
|
+
/**
|
|
255
|
+
* Method calls on an object: obj.method()
|
|
256
|
+
* We capture the object name so we can restrict resolution to self/cls.
|
|
257
|
+
* Calls like redis.get(), dict.get(), os.environ.get() are NOT resolved —
|
|
258
|
+
* only self.method() and cls.method() are tracked as internal edges.
|
|
259
|
+
*/
|
|
260
|
+
const PY_METHOD_CALL_QUERY = `
|
|
261
|
+
(call
|
|
262
|
+
function: (attribute
|
|
263
|
+
object: (identifier) @call.object
|
|
264
|
+
attribute: (identifier) @call.name)) @call.node
|
|
265
|
+
`;
|
|
266
|
+
async function extractPyGraph(filePath, content) {
|
|
267
|
+
const { parser, lang } = await getPyParser();
|
|
268
|
+
const tree = parser.parse(content);
|
|
269
|
+
const fnQuery = new Parser.Query(lang, PY_FN_QUERY);
|
|
270
|
+
// --- Extract function nodes ---
|
|
271
|
+
const nodes = [];
|
|
272
|
+
const seen = new Set(); // avoid duplicates from decorated_definition + function_definition
|
|
273
|
+
const fnMatches = fnQuery.matches(tree.rootNode);
|
|
274
|
+
for (const match of fnMatches) {
|
|
275
|
+
const nameCapture = match.captures.find(c => c.name === 'fn.name');
|
|
276
|
+
const nodeCapture = match.captures.find(c => c.name === 'fn.node');
|
|
277
|
+
if (!nameCapture || !nodeCapture)
|
|
278
|
+
continue;
|
|
279
|
+
const name = nameCapture.node.text;
|
|
280
|
+
const fnNode = nodeCapture.node;
|
|
281
|
+
// Deduplicate by name node position (decorated_definition wraps the function_definition)
|
|
282
|
+
if (seen.has(nameCapture.node.startIndex))
|
|
283
|
+
continue;
|
|
284
|
+
seen.add(nameCapture.node.startIndex);
|
|
285
|
+
// Find enclosing class
|
|
286
|
+
let className;
|
|
287
|
+
let cursor = fnNode.parent;
|
|
288
|
+
while (cursor) {
|
|
289
|
+
if (cursor.type === 'class_definition') {
|
|
290
|
+
const classNameNode = cursor.children.find(c => c.type === 'identifier');
|
|
291
|
+
if (classNameNode)
|
|
292
|
+
className = classNameNode.text;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
cursor = cursor.parent;
|
|
296
|
+
}
|
|
297
|
+
// Skip private methods (underscore prefix) unless they're __init__ or there are very few nodes
|
|
298
|
+
if (name.startsWith('_') && name !== '__init__')
|
|
299
|
+
continue;
|
|
300
|
+
const isAsync = fnNode.text.startsWith('async ') ||
|
|
301
|
+
(fnNode.type === 'function_definition' && fnNode.children[0]?.text === 'async');
|
|
302
|
+
const id = className
|
|
303
|
+
? `${filePath}::${className}.${name}`
|
|
304
|
+
: `${filePath}::${name}`;
|
|
305
|
+
nodes.push({
|
|
306
|
+
id,
|
|
307
|
+
name,
|
|
308
|
+
filePath,
|
|
309
|
+
className,
|
|
310
|
+
isAsync,
|
|
311
|
+
language: 'Python',
|
|
312
|
+
startIndex: fnNode.startIndex,
|
|
313
|
+
endIndex: fnNode.endIndex,
|
|
314
|
+
fanIn: 0,
|
|
315
|
+
fanOut: 0,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
// --- Extract calls ---
|
|
319
|
+
const rawEdges = [];
|
|
320
|
+
const directCallQuery = new Parser.Query(lang, PY_DIRECT_CALL_QUERY);
|
|
321
|
+
const methodCallQuery = new Parser.Query(lang, PY_METHOD_CALL_QUERY);
|
|
322
|
+
// Direct calls: foo(), bar(x) — resolve across all files
|
|
323
|
+
for (const match of directCallQuery.matches(tree.rootNode)) {
|
|
324
|
+
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
325
|
+
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
326
|
+
if (!nameCapture || !nodeCapture)
|
|
327
|
+
continue;
|
|
328
|
+
const calleeName = nameCapture.node.text;
|
|
329
|
+
if (IGNORED_CALLEES.has(calleeName))
|
|
330
|
+
continue;
|
|
331
|
+
const callPos = nodeCapture.node.startIndex;
|
|
332
|
+
const caller = findEnclosingFunction(nodes, callPos);
|
|
333
|
+
if (!caller)
|
|
334
|
+
continue;
|
|
335
|
+
rawEdges.push({
|
|
336
|
+
callerId: caller.id,
|
|
337
|
+
calleeName,
|
|
338
|
+
line: nodeCapture.node.startPosition.row + 1,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
// Method calls: obj.method() — only resolve self.* and cls.* (internal object methods)
|
|
342
|
+
for (const match of methodCallQuery.matches(tree.rootNode)) {
|
|
343
|
+
const objectCapture = match.captures.find(c => c.name === 'call.object');
|
|
344
|
+
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
345
|
+
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
346
|
+
if (!objectCapture || !nameCapture || !nodeCapture)
|
|
347
|
+
continue;
|
|
348
|
+
const objectName = objectCapture.node.text;
|
|
349
|
+
// Only track self.method() and cls.method() — external objects like
|
|
350
|
+
// redis.get(), dict.get(), os.path.join() would create massive false positives
|
|
351
|
+
if (objectName !== 'self' && objectName !== 'cls')
|
|
352
|
+
continue;
|
|
353
|
+
const calleeName = nameCapture.node.text;
|
|
354
|
+
if (IGNORED_CALLEES.has(calleeName))
|
|
355
|
+
continue;
|
|
356
|
+
const callPos = nodeCapture.node.startIndex;
|
|
357
|
+
const caller = findEnclosingFunction(nodes, callPos);
|
|
358
|
+
if (!caller)
|
|
359
|
+
continue;
|
|
360
|
+
rawEdges.push({
|
|
361
|
+
callerId: caller.id,
|
|
362
|
+
calleeName,
|
|
363
|
+
line: nodeCapture.node.startPosition.row + 1,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
return { nodes, rawEdges };
|
|
367
|
+
}
|
|
368
|
+
// ============================================================================
|
|
369
|
+
// GO EXTRACTOR
|
|
370
|
+
// ============================================================================
|
|
371
|
+
const GO_FN_QUERY = `
|
|
372
|
+
(function_declaration
|
|
373
|
+
name: (identifier) @fn.name) @fn.node
|
|
374
|
+
|
|
375
|
+
(method_declaration
|
|
376
|
+
name: (field_identifier) @fn.name) @fn.node
|
|
377
|
+
`;
|
|
378
|
+
const GO_CALL_QUERY = `
|
|
379
|
+
(call_expression
|
|
380
|
+
function: (identifier) @call.name) @call.node
|
|
381
|
+
|
|
382
|
+
(call_expression
|
|
383
|
+
function: (selector_expression
|
|
384
|
+
field: (field_identifier) @call.name)) @call.node
|
|
385
|
+
`;
|
|
386
|
+
async function extractGoGraph(filePath, content) {
|
|
387
|
+
const { parser, lang } = await getGoParser();
|
|
388
|
+
const tree = parser.parse(content);
|
|
389
|
+
const fnQuery = new Parser.Query(lang, GO_FN_QUERY);
|
|
390
|
+
const callQuery = new Parser.Query(lang, GO_CALL_QUERY);
|
|
391
|
+
const nodes = [];
|
|
392
|
+
for (const match of fnQuery.matches(tree.rootNode)) {
|
|
393
|
+
const nameCapture = match.captures.find(c => c.name === 'fn.name');
|
|
394
|
+
const nodeCapture = match.captures.find(c => c.name === 'fn.node');
|
|
395
|
+
if (!nameCapture || !nodeCapture)
|
|
396
|
+
continue;
|
|
397
|
+
const name = nameCapture.node.text;
|
|
398
|
+
const fnNode = nodeCapture.node;
|
|
399
|
+
// Receiver type for method_declaration → use as className
|
|
400
|
+
let className;
|
|
401
|
+
if (fnNode.type === 'method_declaration') {
|
|
402
|
+
const receiver = fnNode.children.find(c => c.type === 'parameter_list');
|
|
403
|
+
if (receiver) {
|
|
404
|
+
// Extract type name from receiver: (r *MyStruct) → MyStruct
|
|
405
|
+
const typeNode = receiver.descendantsOfType('type_identifier')[0]
|
|
406
|
+
?? receiver.descendantsOfType('pointer_type')[0];
|
|
407
|
+
if (typeNode)
|
|
408
|
+
className = typeNode.text.replace(/^\*/, '');
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const id = className ? `${filePath}::${className}.${name}` : `${filePath}::${name}`;
|
|
412
|
+
nodes.push({
|
|
413
|
+
id, name, filePath, className,
|
|
414
|
+
isAsync: false, // Go has goroutines, not async/await
|
|
415
|
+
language: 'Go',
|
|
416
|
+
startIndex: fnNode.startIndex,
|
|
417
|
+
endIndex: fnNode.endIndex,
|
|
418
|
+
fanIn: 0, fanOut: 0,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
const rawEdges = [];
|
|
422
|
+
for (const match of callQuery.matches(tree.rootNode)) {
|
|
423
|
+
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
424
|
+
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
425
|
+
if (!nameCapture || !nodeCapture)
|
|
426
|
+
continue;
|
|
427
|
+
const calleeName = nameCapture.node.text;
|
|
428
|
+
if (IGNORED_CALLEES.has(calleeName))
|
|
429
|
+
continue;
|
|
430
|
+
const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
|
|
431
|
+
if (!caller)
|
|
432
|
+
continue;
|
|
433
|
+
rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1 });
|
|
434
|
+
}
|
|
435
|
+
return { nodes, rawEdges };
|
|
436
|
+
}
|
|
437
|
+
// ============================================================================
|
|
438
|
+
// RUST EXTRACTOR
|
|
439
|
+
// ============================================================================
|
|
440
|
+
const RUST_FN_QUERY = `
|
|
441
|
+
(function_item
|
|
442
|
+
name: (identifier) @fn.name) @fn.node
|
|
443
|
+
`;
|
|
444
|
+
const RUST_CALL_QUERY = `
|
|
445
|
+
(call_expression
|
|
446
|
+
function: (identifier) @call.name) @call.node
|
|
447
|
+
|
|
448
|
+
(call_expression
|
|
449
|
+
function: (field_expression
|
|
450
|
+
field: (field_identifier) @call.name)) @call.node
|
|
451
|
+
`;
|
|
452
|
+
async function extractRustGraph(filePath, content) {
|
|
453
|
+
const { parser, lang } = await getRustParser();
|
|
454
|
+
const tree = parser.parse(content);
|
|
455
|
+
const fnQuery = new Parser.Query(lang, RUST_FN_QUERY);
|
|
456
|
+
const callQuery = new Parser.Query(lang, RUST_CALL_QUERY);
|
|
457
|
+
const nodes = [];
|
|
458
|
+
for (const match of fnQuery.matches(tree.rootNode)) {
|
|
459
|
+
const nameCapture = match.captures.find(c => c.name === 'fn.name');
|
|
460
|
+
const nodeCapture = match.captures.find(c => c.name === 'fn.node');
|
|
461
|
+
if (!nameCapture || !nodeCapture)
|
|
462
|
+
continue;
|
|
463
|
+
const name = nameCapture.node.text;
|
|
464
|
+
const fnNode = nodeCapture.node;
|
|
465
|
+
// Find enclosing impl block → use as className
|
|
466
|
+
let className;
|
|
467
|
+
let cursor = fnNode.parent;
|
|
468
|
+
while (cursor) {
|
|
469
|
+
if (cursor.type === 'impl_item') {
|
|
470
|
+
const typeNode = cursor.children.find(c => c.type === 'type_identifier');
|
|
471
|
+
if (typeNode)
|
|
472
|
+
className = typeNode.text;
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
cursor = cursor.parent;
|
|
476
|
+
}
|
|
477
|
+
// Rust: async keyword lives inside a function_modifiers child
|
|
478
|
+
const isAsync = fnNode.children.some(c => c.type === 'function_modifiers' && c.text.includes('async'));
|
|
479
|
+
const id = className ? `${filePath}::${className}.${name}` : `${filePath}::${name}`;
|
|
480
|
+
nodes.push({
|
|
481
|
+
id, name, filePath, className,
|
|
482
|
+
isAsync,
|
|
483
|
+
language: 'Rust',
|
|
484
|
+
startIndex: fnNode.startIndex,
|
|
485
|
+
endIndex: fnNode.endIndex,
|
|
486
|
+
fanIn: 0, fanOut: 0,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
const rawEdges = [];
|
|
490
|
+
for (const match of callQuery.matches(tree.rootNode)) {
|
|
491
|
+
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
492
|
+
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
493
|
+
if (!nameCapture || !nodeCapture)
|
|
494
|
+
continue;
|
|
495
|
+
const calleeName = nameCapture.node.text;
|
|
496
|
+
if (IGNORED_CALLEES.has(calleeName))
|
|
497
|
+
continue;
|
|
498
|
+
const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
|
|
499
|
+
if (!caller)
|
|
500
|
+
continue;
|
|
501
|
+
rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1 });
|
|
502
|
+
}
|
|
503
|
+
return { nodes, rawEdges };
|
|
504
|
+
}
|
|
505
|
+
// ============================================================================
|
|
506
|
+
// RUBY EXTRACTOR
|
|
507
|
+
// ============================================================================
|
|
508
|
+
const RUBY_FN_QUERY = `
|
|
509
|
+
(method
|
|
510
|
+
name: (identifier) @fn.name) @fn.node
|
|
511
|
+
|
|
512
|
+
(singleton_method
|
|
513
|
+
name: (identifier) @fn.name) @fn.node
|
|
514
|
+
`;
|
|
515
|
+
// Explicit calls: fetch(), obj.method()
|
|
516
|
+
const RUBY_CALL_QUERY = `
|
|
517
|
+
(call
|
|
518
|
+
method: (identifier) @call.name) @call.node
|
|
519
|
+
`;
|
|
520
|
+
// Bareword calls: Ruby allows calling methods without parentheses.
|
|
521
|
+
// An identifier at statement level inside a body_statement is almost always
|
|
522
|
+
// a method call (variable usage appears in assignments/expressions, not alone).
|
|
523
|
+
const RUBY_BAREWORD_QUERY = `
|
|
524
|
+
(body_statement
|
|
525
|
+
(identifier) @call.name)
|
|
526
|
+
`;
|
|
527
|
+
async function extractRubyGraph(filePath, content) {
|
|
528
|
+
const { parser, lang } = await getRubyParser();
|
|
529
|
+
const tree = parser.parse(content);
|
|
530
|
+
const fnQuery = new Parser.Query(lang, RUBY_FN_QUERY);
|
|
531
|
+
const callQuery = new Parser.Query(lang, RUBY_CALL_QUERY);
|
|
532
|
+
const barewordQuery = new Parser.Query(lang, RUBY_BAREWORD_QUERY);
|
|
533
|
+
const nodes = [];
|
|
534
|
+
for (const match of fnQuery.matches(tree.rootNode)) {
|
|
535
|
+
const nameCapture = match.captures.find(c => c.name === 'fn.name');
|
|
536
|
+
const nodeCapture = match.captures.find(c => c.name === 'fn.node');
|
|
537
|
+
if (!nameCapture || !nodeCapture)
|
|
538
|
+
continue;
|
|
539
|
+
const name = nameCapture.node.text;
|
|
540
|
+
const fnNode = nodeCapture.node;
|
|
541
|
+
// Find enclosing class/module
|
|
542
|
+
let className;
|
|
543
|
+
let cursor = fnNode.parent;
|
|
544
|
+
while (cursor) {
|
|
545
|
+
if (cursor.type === 'class' || cursor.type === 'module') {
|
|
546
|
+
const nameNode = cursor.children.find(c => c.type === 'constant' || c.type === 'scope_resolution');
|
|
547
|
+
if (nameNode)
|
|
548
|
+
className = nameNode.text;
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
cursor = cursor.parent;
|
|
552
|
+
}
|
|
553
|
+
const id = className ? `${filePath}::${className}.${name}` : `${filePath}::${name}`;
|
|
554
|
+
nodes.push({
|
|
555
|
+
id, name, filePath, className,
|
|
556
|
+
isAsync: false,
|
|
557
|
+
language: 'Ruby',
|
|
558
|
+
startIndex: fnNode.startIndex,
|
|
559
|
+
endIndex: fnNode.endIndex,
|
|
560
|
+
fanIn: 0, fanOut: 0,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
const rawEdges = [];
|
|
564
|
+
// Explicit calls: fetch(), obj.method()
|
|
565
|
+
for (const match of callQuery.matches(tree.rootNode)) {
|
|
566
|
+
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
567
|
+
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
568
|
+
if (!nameCapture || !nodeCapture)
|
|
569
|
+
continue;
|
|
570
|
+
const calleeName = nameCapture.node.text;
|
|
571
|
+
if (IGNORED_CALLEES.has(calleeName))
|
|
572
|
+
continue;
|
|
573
|
+
const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
|
|
574
|
+
if (!caller)
|
|
575
|
+
continue;
|
|
576
|
+
rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1 });
|
|
577
|
+
}
|
|
578
|
+
// Bareword calls: fetch (no parens) — identifier at statement level
|
|
579
|
+
for (const match of barewordQuery.matches(tree.rootNode)) {
|
|
580
|
+
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
581
|
+
if (!nameCapture)
|
|
582
|
+
continue;
|
|
583
|
+
const calleeName = nameCapture.node.text;
|
|
584
|
+
if (IGNORED_CALLEES.has(calleeName))
|
|
585
|
+
continue;
|
|
586
|
+
const caller = findEnclosingFunction(nodes, nameCapture.node.startIndex);
|
|
587
|
+
if (!caller)
|
|
588
|
+
continue;
|
|
589
|
+
rawEdges.push({ callerId: caller.id, calleeName, line: nameCapture.node.startPosition.row + 1 });
|
|
590
|
+
}
|
|
591
|
+
return { nodes, rawEdges };
|
|
592
|
+
}
|
|
593
|
+
// ============================================================================
|
|
594
|
+
// JAVA EXTRACTOR
|
|
595
|
+
// ============================================================================
|
|
596
|
+
const JAVA_FN_QUERY = `
|
|
597
|
+
(method_declaration
|
|
598
|
+
name: (identifier) @fn.name) @fn.node
|
|
599
|
+
|
|
600
|
+
(constructor_declaration
|
|
601
|
+
name: (identifier) @fn.name) @fn.node
|
|
602
|
+
`;
|
|
603
|
+
const JAVA_CALL_QUERY = `
|
|
604
|
+
(method_invocation
|
|
605
|
+
name: (identifier) @call.name) @call.node
|
|
606
|
+
`;
|
|
607
|
+
async function extractJavaGraph(filePath, content) {
|
|
608
|
+
const { parser, lang } = await getJavaParser();
|
|
609
|
+
const tree = parser.parse(content);
|
|
610
|
+
const fnQuery = new Parser.Query(lang, JAVA_FN_QUERY);
|
|
611
|
+
const callQuery = new Parser.Query(lang, JAVA_CALL_QUERY);
|
|
612
|
+
const nodes = [];
|
|
613
|
+
for (const match of fnQuery.matches(tree.rootNode)) {
|
|
614
|
+
const nameCapture = match.captures.find(c => c.name === 'fn.name');
|
|
615
|
+
const nodeCapture = match.captures.find(c => c.name === 'fn.node');
|
|
616
|
+
if (!nameCapture || !nodeCapture)
|
|
617
|
+
continue;
|
|
618
|
+
const name = nameCapture.node.text;
|
|
619
|
+
const fnNode = nodeCapture.node;
|
|
620
|
+
// Find enclosing class/interface/enum
|
|
621
|
+
let className;
|
|
622
|
+
let cursor = fnNode.parent;
|
|
623
|
+
while (cursor) {
|
|
624
|
+
if (cursor.type === 'class_declaration' || cursor.type === 'interface_declaration' || cursor.type === 'enum_declaration') {
|
|
625
|
+
const nameNode = cursor.children.find(c => c.type === 'identifier');
|
|
626
|
+
if (nameNode)
|
|
627
|
+
className = nameNode.text;
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
cursor = cursor.parent;
|
|
631
|
+
}
|
|
632
|
+
const isAsync = false; // Java uses Future/CompletableFuture, not async keyword
|
|
633
|
+
const id = className ? `${filePath}::${className}.${name}` : `${filePath}::${name}`;
|
|
634
|
+
nodes.push({
|
|
635
|
+
id, name, filePath, className,
|
|
636
|
+
isAsync,
|
|
637
|
+
language: 'Java',
|
|
638
|
+
startIndex: fnNode.startIndex,
|
|
639
|
+
endIndex: fnNode.endIndex,
|
|
640
|
+
fanIn: 0, fanOut: 0,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
const rawEdges = [];
|
|
644
|
+
for (const match of callQuery.matches(tree.rootNode)) {
|
|
645
|
+
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
646
|
+
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
647
|
+
if (!nameCapture || !nodeCapture)
|
|
648
|
+
continue;
|
|
649
|
+
const calleeName = nameCapture.node.text;
|
|
650
|
+
if (IGNORED_CALLEES.has(calleeName))
|
|
651
|
+
continue;
|
|
652
|
+
const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
|
|
653
|
+
if (!caller)
|
|
654
|
+
continue;
|
|
655
|
+
rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1 });
|
|
656
|
+
}
|
|
657
|
+
return { nodes, rawEdges };
|
|
658
|
+
}
|
|
659
|
+
// ============================================================================
|
|
660
|
+
// CALL GRAPH BUILDER
|
|
661
|
+
// ============================================================================
|
|
662
|
+
export class CallGraphBuilder {
|
|
663
|
+
/**
|
|
664
|
+
* Build a call graph from a list of source files.
|
|
665
|
+
*
|
|
666
|
+
* @param files Source files with path, content, and language
|
|
667
|
+
* @param layers Optional layer map { layerName: [path prefix, ...] }
|
|
668
|
+
* e.g. { api: ['routes/', 'controllers/'], storage: ['models/'] }
|
|
669
|
+
*/
|
|
670
|
+
async build(files, layers) {
|
|
671
|
+
const allNodes = new Map();
|
|
672
|
+
const allRawEdges = [];
|
|
673
|
+
// Pass 1: Extract nodes and raw edges from each file
|
|
674
|
+
for (const file of files) {
|
|
675
|
+
try {
|
|
676
|
+
let result;
|
|
677
|
+
if (file.language === 'Python') {
|
|
678
|
+
result = await extractPyGraph(file.path, file.content);
|
|
679
|
+
}
|
|
680
|
+
else if (file.language === 'TypeScript' || file.language === 'JavaScript') {
|
|
681
|
+
result = await extractTSGraph(file.path, file.content);
|
|
682
|
+
}
|
|
683
|
+
else if (file.language === 'Go') {
|
|
684
|
+
result = await extractGoGraph(file.path, file.content);
|
|
685
|
+
}
|
|
686
|
+
else if (file.language === 'Rust') {
|
|
687
|
+
result = await extractRustGraph(file.path, file.content);
|
|
688
|
+
}
|
|
689
|
+
else if (file.language === 'Ruby') {
|
|
690
|
+
result = await extractRubyGraph(file.path, file.content);
|
|
691
|
+
}
|
|
692
|
+
else if (file.language === 'Java') {
|
|
693
|
+
result = await extractJavaGraph(file.path, file.content);
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
for (const node of result.nodes) {
|
|
699
|
+
allNodes.set(node.id, node);
|
|
700
|
+
}
|
|
701
|
+
allRawEdges.push(...result.rawEdges);
|
|
702
|
+
}
|
|
703
|
+
catch {
|
|
704
|
+
// Skip files that fail to parse (syntax errors, encoding issues, etc.)
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
// Pass 2: Resolve raw edges — find callee FunctionNode by name
|
|
708
|
+
const nodesByName = new Map();
|
|
709
|
+
for (const node of allNodes.values()) {
|
|
710
|
+
const list = nodesByName.get(node.name) ?? [];
|
|
711
|
+
list.push(node);
|
|
712
|
+
nodesByName.set(node.name, list);
|
|
713
|
+
}
|
|
714
|
+
const edges = [];
|
|
715
|
+
for (const raw of allRawEdges) {
|
|
716
|
+
const candidates = nodesByName.get(raw.calleeName);
|
|
717
|
+
if (!candidates || candidates.length === 0)
|
|
718
|
+
continue; // external call
|
|
719
|
+
let calleeNode;
|
|
720
|
+
if (candidates.length === 1) {
|
|
721
|
+
calleeNode = candidates[0];
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
// Prefer same file as caller
|
|
725
|
+
const callerNode = allNodes.get(raw.callerId);
|
|
726
|
+
const sameFile = candidates.find(c => c.filePath === callerNode?.filePath);
|
|
727
|
+
calleeNode = sameFile ?? candidates[0];
|
|
728
|
+
}
|
|
729
|
+
edges.push({
|
|
730
|
+
callerId: raw.callerId,
|
|
731
|
+
calleeId: calleeNode.id,
|
|
732
|
+
calleeName: raw.calleeName,
|
|
733
|
+
line: raw.line,
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
// Pass 3: Calculate fanIn / fanOut (count unique caller→callee pairs, not call sites)
|
|
737
|
+
const seenPairs = new Set();
|
|
738
|
+
for (const edge of edges) {
|
|
739
|
+
const pairKey = `${edge.callerId}\0${edge.calleeId}`;
|
|
740
|
+
if (seenPairs.has(pairKey))
|
|
741
|
+
continue;
|
|
742
|
+
seenPairs.add(pairKey);
|
|
743
|
+
const caller = allNodes.get(edge.callerId);
|
|
744
|
+
const callee = allNodes.get(edge.calleeId);
|
|
745
|
+
if (caller)
|
|
746
|
+
caller.fanOut++;
|
|
747
|
+
if (callee)
|
|
748
|
+
callee.fanIn++;
|
|
749
|
+
}
|
|
750
|
+
// Pass 4: Derive hub functions, entry points, layer violations
|
|
751
|
+
const nodes = Array.from(allNodes.values());
|
|
752
|
+
const hubFunctions = nodes
|
|
753
|
+
.filter(n => n.fanIn >= HUB_THRESHOLD)
|
|
754
|
+
.sort((a, b) => b.fanIn - a.fanIn);
|
|
755
|
+
const calledIds = new Set(edges.map(e => e.calleeId));
|
|
756
|
+
const entryPoints = nodes
|
|
757
|
+
.filter(n => !calledIds.has(n.id))
|
|
758
|
+
.sort((a, b) => b.fanOut - a.fanOut);
|
|
759
|
+
const layerViolations = layers
|
|
760
|
+
? this.detectLayerViolations(edges, allNodes, layers)
|
|
761
|
+
: [];
|
|
762
|
+
const totalFanIn = nodes.reduce((s, n) => s + n.fanIn, 0);
|
|
763
|
+
const totalFanOut = nodes.reduce((s, n) => s + n.fanOut, 0);
|
|
764
|
+
return {
|
|
765
|
+
nodes: allNodes,
|
|
766
|
+
edges,
|
|
767
|
+
hubFunctions,
|
|
768
|
+
entryPoints,
|
|
769
|
+
layerViolations,
|
|
770
|
+
stats: {
|
|
771
|
+
totalNodes: nodes.length,
|
|
772
|
+
totalEdges: edges.length,
|
|
773
|
+
avgFanIn: nodes.length > 0 ? totalFanIn / nodes.length : 0,
|
|
774
|
+
avgFanOut: nodes.length > 0 ? totalFanOut / nodes.length : 0,
|
|
775
|
+
},
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
detectLayerViolations(edges, nodes, layers) {
|
|
779
|
+
// Build ordered layer list (index 0 = top layer, higher index = lower layer)
|
|
780
|
+
const layerOrder = Object.keys(layers);
|
|
781
|
+
const getLayer = (filePath) => {
|
|
782
|
+
for (const [layerName, prefixes] of Object.entries(layers)) {
|
|
783
|
+
if (prefixes.some(p => filePath.includes(p)))
|
|
784
|
+
return layerName;
|
|
785
|
+
}
|
|
786
|
+
return undefined;
|
|
787
|
+
};
|
|
788
|
+
const violations = [];
|
|
789
|
+
for (const edge of edges) {
|
|
790
|
+
const caller = nodes.get(edge.callerId);
|
|
791
|
+
const callee = nodes.get(edge.calleeId);
|
|
792
|
+
if (!caller || !callee)
|
|
793
|
+
continue;
|
|
794
|
+
const callerLayer = getLayer(caller.filePath);
|
|
795
|
+
const calleeLayer = getLayer(callee.filePath);
|
|
796
|
+
if (!callerLayer || !calleeLayer || callerLayer === calleeLayer)
|
|
797
|
+
continue;
|
|
798
|
+
const callerIdx = layerOrder.indexOf(callerLayer);
|
|
799
|
+
const calleeIdx = layerOrder.indexOf(calleeLayer);
|
|
800
|
+
if (callerIdx > calleeIdx) {
|
|
801
|
+
// Lower layer calling upper layer — violation
|
|
802
|
+
violations.push({
|
|
803
|
+
callerId: edge.callerId,
|
|
804
|
+
calleeId: edge.calleeId,
|
|
805
|
+
callerLayer,
|
|
806
|
+
calleeLayer,
|
|
807
|
+
reason: `${callerLayer} calls ${calleeLayer} (${caller.name} → ${callee.name})`,
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return violations;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
// ============================================================================
|
|
815
|
+
// SERIALIZATION HELPER
|
|
816
|
+
// ============================================================================
|
|
817
|
+
export function serializeCallGraph(result) {
|
|
818
|
+
return {
|
|
819
|
+
nodes: Array.from(result.nodes.values()),
|
|
820
|
+
edges: result.edges,
|
|
821
|
+
hubFunctions: result.hubFunctions,
|
|
822
|
+
entryPoints: result.entryPoints,
|
|
823
|
+
layerViolations: result.layerViolations,
|
|
824
|
+
stats: result.stats,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
//# sourceMappingURL=call-graph.js.map
|