openlore 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/LICENSE +21 -0
- package/README.md +268 -0
- package/dist/api/analyze.d.ts +17 -0
- package/dist/api/analyze.d.ts.map +1 -0
- package/dist/api/analyze.js +143 -0
- package/dist/api/analyze.js.map +1 -0
- package/dist/api/audit.d.ts +10 -0
- package/dist/api/audit.d.ts.map +1 -0
- package/dist/api/audit.js +117 -0
- package/dist/api/audit.js.map +1 -0
- package/dist/api/decisions.d.ts +55 -0
- package/dist/api/decisions.d.ts.map +1 -0
- package/dist/api/decisions.js +157 -0
- package/dist/api/decisions.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 +152 -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 +259 -0
- package/dist/api/generate.js.map +1 -0
- package/dist/api/index.d.ts +41 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +34 -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 +83 -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 +312 -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 +137 -0
- package/dist/api/specs.js.map +1 -0
- package/dist/api/types.d.ts +201 -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 +30 -0
- package/dist/cli/commands/analyze.d.ts.map +1 -0
- package/dist/cli/commands/analyze.js +683 -0
- package/dist/cli/commands/analyze.js.map +1 -0
- package/dist/cli/commands/audit.d.ts +9 -0
- package/dist/cli/commands/audit.d.ts.map +1 -0
- package/dist/cli/commands/audit.js +98 -0
- package/dist/cli/commands/audit.js.map +1 -0
- package/dist/cli/commands/decisions.d.ts +16 -0
- package/dist/cli/commands/decisions.d.ts.map +1 -0
- package/dist/cli/commands/decisions.js +864 -0
- package/dist/cli/commands/decisions.js.map +1 -0
- package/dist/cli/commands/digest.d.ts +9 -0
- package/dist/cli/commands/digest.d.ts.map +1 -0
- package/dist/cli/commands/digest.js +61 -0
- package/dist/cli/commands/digest.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +9 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +398 -0
- package/dist/cli/commands/doctor.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 +550 -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 +565 -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 +173 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/mcp.d.ts +2235 -0
- package/dist/cli/commands/mcp.d.ts.map +1 -0
- package/dist/cli/commands/mcp.js +1384 -0
- package/dist/cli/commands/mcp.js.map +1 -0
- package/dist/cli/commands/refresh-stories.d.ts +10 -0
- package/dist/cli/commands/refresh-stories.d.ts.map +1 -0
- package/dist/cli/commands/refresh-stories.js +314 -0
- package/dist/cli/commands/refresh-stories.js.map +1 -0
- package/dist/cli/commands/run.d.ts +9 -0
- package/dist/cli/commands/run.d.ts.map +1 -0
- package/dist/cli/commands/run.js +459 -0
- package/dist/cli/commands/run.js.map +1 -0
- package/dist/cli/commands/setup.d.ts +19 -0
- package/dist/cli/commands/setup.d.ts.map +1 -0
- package/dist/cli/commands/setup.js +355 -0
- package/dist/cli/commands/setup.js.map +1 -0
- package/dist/cli/commands/test.d.ts +22 -0
- package/dist/cli/commands/test.d.ts.map +1 -0
- package/dist/cli/commands/test.js +180 -0
- package/dist/cli/commands/test.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 +383 -0
- package/dist/cli/commands/verify.js.map +1 -0
- package/dist/cli/commands/view.d.ts +13 -0
- package/dist/cli/commands/view.d.ts.map +1 -0
- package/dist/cli/commands/view.js +547 -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 +118 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/tui-approval.d.ts +11 -0
- package/dist/cli/tui-approval.d.ts.map +1 -0
- package/dist/cli/tui-approval.js +129 -0
- package/dist/cli/tui-approval.js.map +1 -0
- package/dist/constants.d.ts +314 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +382 -0
- package/dist/constants.js.map +1 -0
- package/dist/core/analyzer/ai-config-generator.d.ts +54 -0
- package/dist/core/analyzer/ai-config-generator.d.ts.map +1 -0
- package/dist/core/analyzer/ai-config-generator.js +98 -0
- package/dist/core/analyzer/ai-config-generator.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 +261 -0
- package/dist/core/analyzer/artifact-generator.d.ts.map +1 -0
- package/dist/core/analyzer/artifact-generator.js +909 -0
- package/dist/core/analyzer/artifact-generator.js.map +1 -0
- package/dist/core/analyzer/ast-chunker.d.ts +24 -0
- package/dist/core/analyzer/ast-chunker.d.ts.map +1 -0
- package/dist/core/analyzer/ast-chunker.js +198 -0
- package/dist/core/analyzer/ast-chunker.js.map +1 -0
- package/dist/core/analyzer/call-graph.d.ts +162 -0
- package/dist/core/analyzer/call-graph.d.ts.map +1 -0
- package/dist/core/analyzer/call-graph.js +2040 -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 +154 -0
- package/dist/core/analyzer/code-shaper.js.map +1 -0
- package/dist/core/analyzer/codebase-digest.d.ts +40 -0
- package/dist/core/analyzer/codebase-digest.d.ts.map +1 -0
- package/dist/core/analyzer/codebase-digest.js +195 -0
- package/dist/core/analyzer/codebase-digest.js.map +1 -0
- package/dist/core/analyzer/cpp-header-resolver.d.ts +30 -0
- package/dist/core/analyzer/cpp-header-resolver.d.ts.map +1 -0
- package/dist/core/analyzer/cpp-header-resolver.js +71 -0
- package/dist/core/analyzer/cpp-header-resolver.js.map +1 -0
- package/dist/core/analyzer/dependency-graph.d.ts +230 -0
- package/dist/core/analyzer/dependency-graph.d.ts.map +1 -0
- package/dist/core/analyzer/dependency-graph.js +752 -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 +289 -0
- package/dist/core/analyzer/duplicate-detector.js.map +1 -0
- package/dist/core/analyzer/embedding-service.d.ts +56 -0
- package/dist/core/analyzer/embedding-service.d.ts.map +1 -0
- package/dist/core/analyzer/embedding-service.js +118 -0
- package/dist/core/analyzer/embedding-service.js.map +1 -0
- package/dist/core/analyzer/env-extractor.d.ts +33 -0
- package/dist/core/analyzer/env-extractor.d.ts.map +1 -0
- package/dist/core/analyzer/env-extractor.js +196 -0
- package/dist/core/analyzer/env-extractor.js.map +1 -0
- package/dist/core/analyzer/external-packages.d.ts +20 -0
- package/dist/core/analyzer/external-packages.d.ts.map +1 -0
- package/dist/core/analyzer/external-packages.js +175 -0
- package/dist/core/analyzer/external-packages.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 +532 -0
- package/dist/core/analyzer/file-walker.js.map +1 -0
- package/dist/core/analyzer/function-registry-trie.d.ts +21 -0
- package/dist/core/analyzer/function-registry-trie.d.ts.map +1 -0
- package/dist/core/analyzer/function-registry-trie.js +39 -0
- package/dist/core/analyzer/function-registry-trie.js.map +1 -0
- package/dist/core/analyzer/http-route-parser.d.ts +152 -0
- package/dist/core/analyzer/http-route-parser.d.ts.map +1 -0
- package/dist/core/analyzer/http-route-parser.js +971 -0
- package/dist/core/analyzer/http-route-parser.js.map +1 -0
- package/dist/core/analyzer/import-parser.d.ts +100 -0
- package/dist/core/analyzer/import-parser.d.ts.map +1 -0
- package/dist/core/analyzer/import-parser.js +952 -0
- package/dist/core/analyzer/import-parser.js.map +1 -0
- package/dist/core/analyzer/import-resolver-bridge.d.ts +25 -0
- package/dist/core/analyzer/import-resolver-bridge.d.ts.map +1 -0
- package/dist/core/analyzer/import-resolver-bridge.js +99 -0
- package/dist/core/analyzer/import-resolver-bridge.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/middleware-extractor.d.ts +29 -0
- package/dist/core/analyzer/middleware-extractor.d.ts.map +1 -0
- package/dist/core/analyzer/middleware-extractor.js +195 -0
- package/dist/core/analyzer/middleware-extractor.js.map +1 -0
- package/dist/core/analyzer/refactor-analyzer.d.ts +83 -0
- package/dist/core/analyzer/refactor-analyzer.d.ts.map +1 -0
- package/dist/core/analyzer/refactor-analyzer.js +351 -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 +740 -0
- package/dist/core/analyzer/repository-mapper.js.map +1 -0
- package/dist/core/analyzer/schema-extractor.d.ts +41 -0
- package/dist/core/analyzer/schema-extractor.d.ts.map +1 -0
- package/dist/core/analyzer/schema-extractor.js +229 -0
- package/dist/core/analyzer/schema-extractor.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 +675 -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/spec-snapshot-generator.d.ts +17 -0
- package/dist/core/analyzer/spec-snapshot-generator.d.ts.map +1 -0
- package/dist/core/analyzer/spec-snapshot-generator.js +201 -0
- package/dist/core/analyzer/spec-snapshot-generator.js.map +1 -0
- package/dist/core/analyzer/spec-vector-index.d.ts +68 -0
- package/dist/core/analyzer/spec-vector-index.d.ts.map +1 -0
- package/dist/core/analyzer/spec-vector-index.js +340 -0
- package/dist/core/analyzer/spec-vector-index.js.map +1 -0
- package/dist/core/analyzer/subgraph-extractor.d.ts +51 -0
- package/dist/core/analyzer/subgraph-extractor.d.ts.map +1 -0
- package/dist/core/analyzer/subgraph-extractor.js +147 -0
- package/dist/core/analyzer/subgraph-extractor.js.map +1 -0
- package/dist/core/analyzer/type-inference-engine.d.ts +23 -0
- package/dist/core/analyzer/type-inference-engine.d.ts.map +1 -0
- package/dist/core/analyzer/type-inference-engine.js +130 -0
- package/dist/core/analyzer/type-inference-engine.js.map +1 -0
- package/dist/core/analyzer/ui-component-extractor.d.ts +43 -0
- package/dist/core/analyzer/ui-component-extractor.d.ts.map +1 -0
- package/dist/core/analyzer/ui-component-extractor.js +245 -0
- package/dist/core/analyzer/ui-component-extractor.js.map +1 -0
- package/dist/core/analyzer/unified-search.d.ts +116 -0
- package/dist/core/analyzer/unified-search.d.ts.map +1 -0
- package/dist/core/analyzer/unified-search.js +231 -0
- package/dist/core/analyzer/unified-search.js.map +1 -0
- package/dist/core/analyzer/vector-index.d.ts +92 -0
- package/dist/core/analyzer/vector-index.d.ts.map +1 -0
- package/dist/core/analyzer/vector-index.js +451 -0
- package/dist/core/analyzer/vector-index.js.map +1 -0
- package/dist/core/decisions/consolidator.d.ts +14 -0
- package/dist/core/decisions/consolidator.d.ts.map +1 -0
- package/dist/core/decisions/consolidator.js +169 -0
- package/dist/core/decisions/consolidator.js.map +1 -0
- package/dist/core/decisions/extractor.d.ts +26 -0
- package/dist/core/decisions/extractor.d.ts.map +1 -0
- package/dist/core/decisions/extractor.js +156 -0
- package/dist/core/decisions/extractor.js.map +1 -0
- package/dist/core/decisions/index.d.ts +19 -0
- package/dist/core/decisions/index.d.ts.map +1 -0
- package/dist/core/decisions/index.js +16 -0
- package/dist/core/decisions/index.js.map +1 -0
- package/dist/core/decisions/store.d.ts +36 -0
- package/dist/core/decisions/store.d.ts.map +1 -0
- package/dist/core/decisions/store.js +109 -0
- package/dist/core/decisions/store.js.map +1 -0
- package/dist/core/decisions/syncer.d.ts +27 -0
- package/dist/core/decisions/syncer.d.ts.map +1 -0
- package/dist/core/decisions/syncer.js +214 -0
- package/dist/core/decisions/syncer.js.map +1 -0
- package/dist/core/decisions/verifier.d.ts +20 -0
- package/dist/core/decisions/verifier.d.ts.map +1 -0
- package/dist/core/decisions/verifier.js +115 -0
- package/dist/core/decisions/verifier.js.map +1 -0
- package/dist/core/digest/digest-generator.d.ts +29 -0
- package/dist/core/digest/digest-generator.d.ts.map +1 -0
- package/dist/core/digest/digest-generator.js +181 -0
- package/dist/core/digest/digest-generator.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 +598 -0
- package/dist/core/drift/drift-detector.js.map +1 -0
- package/dist/core/drift/git-diff.d.ts +60 -0
- package/dist/core/drift/git-diff.d.ts.map +1 -0
- package/dist/core/drift/git-diff.js +383 -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/drift/test-suggester.d.ts +18 -0
- package/dist/core/drift/test-suggester.d.ts.map +1 -0
- package/dist/core/drift/test-suggester.js +107 -0
- package/dist/core/drift/test-suggester.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 +240 -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 +524 -0
- package/dist/core/generator/openspec-compat.js.map +1 -0
- package/dist/core/generator/openspec-format-generator.d.ts +131 -0
- package/dist/core/generator/openspec-format-generator.d.ts.map +1 -0
- package/dist/core/generator/openspec-format-generator.js +963 -0
- package/dist/core/generator/openspec-format-generator.js.map +1 -0
- package/dist/core/generator/openspec-writer.d.ts +130 -0
- package/dist/core/generator/openspec-writer.d.ts.map +1 -0
- package/dist/core/generator/openspec-writer.js +404 -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/rag-manifest-generator.d.ts +37 -0
- package/dist/core/generator/rag-manifest-generator.d.ts.map +1 -0
- package/dist/core/generator/rag-manifest-generator.js +134 -0
- package/dist/core/generator/rag-manifest-generator.js.map +1 -0
- package/dist/core/generator/schemas.d.ts +365 -0
- package/dist/core/generator/schemas.d.ts.map +1 -0
- package/dist/core/generator/schemas.js +190 -0
- package/dist/core/generator/schemas.js.map +1 -0
- package/dist/core/generator/spec-pipeline.d.ts +123 -0
- package/dist/core/generator/spec-pipeline.d.ts.map +1 -0
- package/dist/core/generator/spec-pipeline.js +699 -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 +171 -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 +74 -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 +85 -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 +72 -0
- package/dist/core/generator/stages/stage4-api.js.map +1 -0
- package/dist/core/generator/stages/stage5-architecture.d.ts +11 -0
- package/dist/core/generator/stages/stage5-architecture.d.ts.map +1 -0
- package/dist/core/generator/stages/stage5-architecture.js +75 -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 +47 -0
- package/dist/core/generator/stages/stage6-adr.js.map +1 -0
- package/dist/core/services/chat-agent.d.ts +50 -0
- package/dist/core/services/chat-agent.d.ts.map +1 -0
- package/dist/core/services/chat-agent.js +369 -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 +494 -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 +149 -0
- package/dist/core/services/config-manager.js.map +1 -0
- package/dist/core/services/edge-store.d.ts +57 -0
- package/dist/core/services/edge-store.d.ts.map +1 -0
- package/dist/core/services/edge-store.js +419 -0
- package/dist/core/services/edge-store.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 +95 -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 +379 -0
- package/dist/core/services/llm-service.d.ts.map +1 -0
- package/dist/core/services/llm-service.js +1553 -0
- package/dist/core/services/llm-service.js.map +1 -0
- package/dist/core/services/mcp-handlers/analysis.d.ts +127 -0
- package/dist/core/services/mcp-handlers/analysis.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/analysis.js +1185 -0
- package/dist/core/services/mcp-handlers/analysis.js.map +1 -0
- package/dist/core/services/mcp-handlers/change.d.ts +14 -0
- package/dist/core/services/mcp-handlers/change.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/change.js +416 -0
- package/dist/core/services/mcp-handlers/change.js.map +1 -0
- package/dist/core/services/mcp-handlers/decisions.d.ts +16 -0
- package/dist/core/services/mcp-handlers/decisions.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/decisions.js +239 -0
- package/dist/core/services/mcp-handlers/decisions.js.map +1 -0
- package/dist/core/services/mcp-handlers/graph.d.ts +94 -0
- package/dist/core/services/mcp-handlers/graph.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/graph.js +693 -0
- package/dist/core/services/mcp-handlers/graph.js.map +1 -0
- package/dist/core/services/mcp-handlers/orient.d.ts +17 -0
- package/dist/core/services/mcp-handlers/orient.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/orient.js +357 -0
- package/dist/core/services/mcp-handlers/orient.js.map +1 -0
- package/dist/core/services/mcp-handlers/semantic.d.ts +66 -0
- package/dist/core/services/mcp-handlers/semantic.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/semantic.js +432 -0
- package/dist/core/services/mcp-handlers/semantic.js.map +1 -0
- package/dist/core/services/mcp-handlers/utils.d.ts +85 -0
- package/dist/core/services/mcp-handlers/utils.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/utils.js +262 -0
- package/dist/core/services/mcp-handlers/utils.js.map +1 -0
- package/dist/core/services/mcp-watcher.d.ts +41 -0
- package/dist/core/services/mcp-watcher.d.ts.map +1 -0
- package/dist/core/services/mcp-watcher.js +254 -0
- package/dist/core/services/mcp-watcher.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 +100 -0
- package/dist/core/services/project-detector.js.map +1 -0
- package/dist/core/test-generator/coverage-analyzer.d.ts +27 -0
- package/dist/core/test-generator/coverage-analyzer.d.ts.map +1 -0
- package/dist/core/test-generator/coverage-analyzer.js +285 -0
- package/dist/core/test-generator/coverage-analyzer.js.map +1 -0
- package/dist/core/test-generator/framework-detector.d.ts +17 -0
- package/dist/core/test-generator/framework-detector.d.ts.map +1 -0
- package/dist/core/test-generator/framework-detector.js +65 -0
- package/dist/core/test-generator/framework-detector.js.map +1 -0
- package/dist/core/test-generator/index.d.ts +14 -0
- package/dist/core/test-generator/index.d.ts.map +1 -0
- package/dist/core/test-generator/index.js +11 -0
- package/dist/core/test-generator/index.js.map +1 -0
- package/dist/core/test-generator/renderers/catch2.d.ts +8 -0
- package/dist/core/test-generator/renderers/catch2.d.ts.map +1 -0
- package/dist/core/test-generator/renderers/catch2.js +47 -0
- package/dist/core/test-generator/renderers/catch2.js.map +1 -0
- package/dist/core/test-generator/renderers/gtest.d.ts +8 -0
- package/dist/core/test-generator/renderers/gtest.d.ts.map +1 -0
- package/dist/core/test-generator/renderers/gtest.js +45 -0
- package/dist/core/test-generator/renderers/gtest.js.map +1 -0
- package/dist/core/test-generator/renderers/index.d.ts +20 -0
- package/dist/core/test-generator/renderers/index.d.ts.map +1 -0
- package/dist/core/test-generator/renderers/index.js +35 -0
- package/dist/core/test-generator/renderers/index.js.map +1 -0
- package/dist/core/test-generator/renderers/playwright.d.ts +8 -0
- package/dist/core/test-generator/renderers/playwright.d.ts.map +1 -0
- package/dist/core/test-generator/renderers/playwright.js +44 -0
- package/dist/core/test-generator/renderers/playwright.js.map +1 -0
- package/dist/core/test-generator/renderers/pytest.d.ts +8 -0
- package/dist/core/test-generator/renderers/pytest.d.ts.map +1 -0
- package/dist/core/test-generator/renderers/pytest.js +44 -0
- package/dist/core/test-generator/renderers/pytest.js.map +1 -0
- package/dist/core/test-generator/renderers/shared.d.ts +21 -0
- package/dist/core/test-generator/renderers/shared.d.ts.map +1 -0
- package/dist/core/test-generator/renderers/shared.js +56 -0
- package/dist/core/test-generator/renderers/shared.js.map +1 -0
- package/dist/core/test-generator/renderers/vitest.d.ts +8 -0
- package/dist/core/test-generator/renderers/vitest.d.ts.map +1 -0
- package/dist/core/test-generator/renderers/vitest.js +52 -0
- package/dist/core/test-generator/renderers/vitest.js.map +1 -0
- package/dist/core/test-generator/scenario-parser.d.ts +33 -0
- package/dist/core/test-generator/scenario-parser.d.ts.map +1 -0
- package/dist/core/test-generator/scenario-parser.js +244 -0
- package/dist/core/test-generator/scenario-parser.js.map +1 -0
- package/dist/core/test-generator/test-generator.d.ts +30 -0
- package/dist/core/test-generator/test-generator.d.ts.map +1 -0
- package/dist/core/test-generator/test-generator.js +174 -0
- package/dist/core/test-generator/test-generator.js.map +1 -0
- package/dist/core/test-generator/test-writer.d.ts +25 -0
- package/dist/core/test-generator/test-writer.d.ts.map +1 -0
- package/dist/core/test-generator/test-writer.js +128 -0
- package/dist/core/test-generator/test-writer.js.map +1 -0
- package/dist/core/test-generator/then-matchers.d.ts +35 -0
- package/dist/core/test-generator/then-matchers.d.ts.map +1 -0
- package/dist/core/test-generator/then-matchers.js +211 -0
- package/dist/core/test-generator/then-matchers.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 +293 -0
- package/dist/core/verifier/verification-engine.d.ts.map +1 -0
- package/dist/core/verifier/verification-engine.js +919 -0
- package/dist/core/verifier/verification-engine.js.map +1 -0
- package/dist/types/index.d.ts +368 -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 +167 -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/types/test-generator.d.ts +103 -0
- package/dist/types/test-generator.d.ts.map +1 -0
- package/dist/types/test-generator.js +17 -0
- package/dist/types/test-generator.js.map +1 -0
- package/dist/utils/command-helpers.d.ts +68 -0
- package/dist/utils/command-helpers.d.ts.map +1 -0
- package/dist/utils/command-helpers.js +150 -0
- package/dist/utils/command-helpers.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 +129 -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 +342 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/misc.d.ts +10 -0
- package/dist/utils/misc.d.ts.map +1 -0
- package/dist/utils/misc.js +21 -0
- package/dist/utils/misc.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 +283 -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 +238 -0
- package/dist/utils/shutdown.js.map +1 -0
- package/examples/bmad/README.md +113 -0
- package/examples/bmad/agents/architect.md +226 -0
- package/examples/bmad/agents/dev-brownfield.md +69 -0
- package/examples/bmad/setup/architect.customize.yaml +14 -0
- package/examples/bmad/tasks/implement-story.md +254 -0
- package/examples/bmad/tasks/onboarding.md +169 -0
- package/examples/bmad/tasks/refactor.md +178 -0
- package/examples/bmad/tasks/sprint-planning.md +168 -0
- package/examples/bmad/templates/story.md +108 -0
- package/examples/cline-workflows/openlore-analyze-codebase.md +101 -0
- package/examples/cline-workflows/openlore-check-spec-drift.md +102 -0
- package/examples/cline-workflows/openlore-execute-refactor.md +212 -0
- package/examples/cline-workflows/openlore-implement-feature.md +266 -0
- package/examples/cline-workflows/openlore-plan-refactor.md +279 -0
- package/examples/cline-workflows/openlore-refactor-codebase.md +16 -0
- package/examples/cline-workflows/openlore-write-tests.md +177 -0
- package/examples/drift-demo/openspec/config.yaml +14 -0
- package/examples/drift-demo/openspec/specs/architecture/spec.md +30 -0
- package/examples/drift-demo/openspec/specs/auth/spec.md +71 -0
- package/examples/drift-demo/openspec/specs/database/spec.md +33 -0
- package/examples/drift-demo/openspec/specs/overview/spec.md +20 -0
- package/examples/drift-demo/openspec/specs/projects/spec.md +55 -0
- package/examples/drift-demo/openspec/specs/tasks/spec.md +78 -0
- package/examples/drift-demo/package.json +21 -0
- package/examples/drift-demo/src/auth/auth-middleware.ts +30 -0
- package/examples/drift-demo/src/auth/auth-routes.ts +29 -0
- package/examples/drift-demo/src/auth/auth-service.ts +45 -0
- package/examples/drift-demo/src/database/connection.ts +27 -0
- package/examples/drift-demo/src/index.ts +16 -0
- package/examples/drift-demo/src/projects/project-model.ts +15 -0
- package/examples/drift-demo/src/projects/project-service.ts +34 -0
- package/examples/drift-demo/src/tasks/task-model.ts +37 -0
- package/examples/drift-demo/src/tasks/task-routes.ts +53 -0
- package/examples/drift-demo/src/tasks/task-service.ts +60 -0
- package/examples/drift-demo/src/utils/validation.ts +11 -0
- package/examples/drift-demo/tests/auth.test.ts +4 -0
- package/examples/drift-demo/tests/tasks.test.ts +4 -0
- package/examples/drift-demo/tsconfig.json +10 -0
- package/examples/drift-test/run-drift-test.sh +1087 -0
- package/examples/gsd/README.md +119 -0
- package/examples/gsd/commands/gsd/openlore-drift.md +111 -0
- package/examples/gsd/commands/gsd/openlore-orient.md +191 -0
- package/examples/mistral-vibe/README.md +101 -0
- package/examples/mistral-vibe/antipatterns-template.md +18 -0
- package/examples/mistral-vibe/skills/openlore-analyze-codebase/SKILL.md +124 -0
- package/examples/mistral-vibe/skills/openlore-brainstorm/SKILL.md +379 -0
- package/examples/mistral-vibe/skills/openlore-debug/SKILL.md +330 -0
- package/examples/mistral-vibe/skills/openlore-execute-refactor/SKILL.md +291 -0
- package/examples/mistral-vibe/skills/openlore-generate/SKILL.md +245 -0
- package/examples/mistral-vibe/skills/openlore-implement-story/SKILL.md +326 -0
- package/examples/mistral-vibe/skills/openlore-plan-refactor/SKILL.md +365 -0
- package/examples/mistral-vibe/skills/openlore-review-changes/SKILL.md +128 -0
- package/examples/mistral-vibe/skills/openlore-write-tests/SKILL.md +261 -0
- package/examples/opencode/agent-guard.ts +170 -0
- package/examples/opencode/plugins/anti-laziness.ts +202 -0
- package/examples/opencode/plugins/lib/openlore-context-injector-helpers.ts +116 -0
- package/examples/opencode/plugins/lib/openlore-decision-extractor-helpers.ts +65 -0
- package/examples/opencode/plugins/openlore-context-injector.test.ts +211 -0
- package/examples/opencode/plugins/openlore-context-injector.ts +165 -0
- package/examples/opencode/plugins/openlore-decision-extractor.test.ts +131 -0
- package/examples/opencode/plugins/openlore-decision-extractor.ts +322 -0
- package/examples/opencode/plugins/openlore-enforcer.ts +227 -0
- package/examples/opencode/prompts/sisyphus-sdd.md +150 -0
- package/examples/opencode-skills/openlore-analyze-codebase/SKILL.md +101 -0
- package/examples/opencode-skills/openlore-brainstorm/SKILL.md +354 -0
- package/examples/opencode-skills/openlore-debug/SKILL.md +291 -0
- package/examples/opencode-skills/openlore-execute-refactor/SKILL.md +241 -0
- package/examples/opencode-skills/openlore-generate/SKILL.md +236 -0
- package/examples/opencode-skills/openlore-implement-story/SKILL.md +251 -0
- package/examples/opencode-skills/openlore-plan-refactor/SKILL.md +298 -0
- package/examples/opencode-skills/openlore-review-changes/SKILL.md +134 -0
- package/examples/opencode-skills/openlore-write-tests/SKILL.md +230 -0
- package/examples/openspec-analysis/README.md +59 -0
- package/examples/openspec-analysis/SUMMARY.md +72 -0
- package/examples/openspec-analysis/config.json +16 -0
- package/examples/openspec-analysis/dependencies.mermaid +35 -0
- package/examples/openspec-analysis/dependency-graph.json +12116 -0
- package/examples/openspec-analysis/llm-context.json +119 -0
- package/examples/openspec-analysis/repo-structure.json +871 -0
- package/examples/openspec-cli/README.md +67 -0
- package/examples/openspec-cli/openspec/config.yaml +26 -0
- package/examples/openspec-cli/openspec/specs/architecture/spec.md +178 -0
- package/examples/openspec-cli/openspec/specs/artifact-graph/spec.md +143 -0
- package/examples/openspec-cli/openspec/specs/cli/spec.md +138 -0
- package/examples/openspec-cli/openspec/specs/overview/spec.md +60 -0
- package/examples/openspec-cli/openspec/specs/parsing/spec.md +123 -0
- package/examples/openspec-cli/openspec/specs/validation/spec.md +108 -0
- package/examples/spec-kit/README.md +104 -0
- package/examples/spec-kit/commands/drift.md +87 -0
- package/examples/spec-kit/commands/orient.md +138 -0
- package/examples/spec-kit/extension.yml +54 -0
- package/package.json +125 -0
- package/src/viewer/InteractiveGraphViewer.jsx +1600 -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 +450 -0
- package/src/viewer/components/ClassGraph.jsx +782 -0
- package/src/viewer/components/ClusterGraph.jsx +469 -0
- package/src/viewer/components/FilterBar.jsx +179 -0
- package/src/viewer/components/FlatGraph.jsx +282 -0
- package/src/viewer/components/MicroComponents.jsx +85 -0
- package/src/viewer/hooks/usePanZoom.js +79 -0
- package/src/viewer/utils/constants.js +64 -0
- package/src/viewer/utils/graph-helpers.js +303 -0
- package/src/viewer/utils/graph-helpers.test.ts +39 -0
- package/src/viewer/utils/themes.js +206 -0
- package/stubs/tree-sitter-cli-stub/package.json +6 -0
|
@@ -0,0 +1,2040 @@
|
|
|
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, Swift — 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 { dirname, resolve as resolvePath } from 'node:path';
|
|
15
|
+
import Parser from 'tree-sitter';
|
|
16
|
+
import { FunctionRegistryTrie } from './function-registry-trie.js';
|
|
17
|
+
import { inferTypesFromSource, resolveViaTypeInference } from './type-inference-engine.js';
|
|
18
|
+
import { extractAllHttpEdges } from './http-route-parser.js';
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// CONSTANTS
|
|
21
|
+
// ============================================================================
|
|
22
|
+
const HUB_THRESHOLD = 5;
|
|
23
|
+
/** Common builtins and stdlib names to ignore as call targets (across all languages) */
|
|
24
|
+
const IGNORED_CALLEES = new Set([
|
|
25
|
+
// Python builtins
|
|
26
|
+
'print', 'len', 'range', 'str', 'int', 'float', 'list', 'dict', 'set', 'tuple',
|
|
27
|
+
'bool', 'type', 'isinstance', 'issubclass', 'hasattr', 'getattr', 'setattr',
|
|
28
|
+
'enumerate', 'zip', 'map', 'filter', 'sorted', 'reversed', 'sum', 'min', 'max',
|
|
29
|
+
'open', 'input', 'format', 'repr', 'id', 'hash', 'abs', 'round', 'pow',
|
|
30
|
+
'super', 'object', 'property', 'staticmethod', 'classmethod',
|
|
31
|
+
// JS/TS common
|
|
32
|
+
'console', 'log', 'error', 'warn', 'JSON', 'parse', 'stringify',
|
|
33
|
+
'Promise', 'resolve', 'reject', 'then', 'catch', 'finally',
|
|
34
|
+
'Array', 'Object', 'String', 'Number', 'Boolean', 'Math', 'Date',
|
|
35
|
+
'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
|
|
36
|
+
'require', 'import', 'exports',
|
|
37
|
+
// Python control flow (used like functions sometimes)
|
|
38
|
+
'assert', 'raise', 'return', 'yield', 'await', 'pass', 'del',
|
|
39
|
+
// Node.js common
|
|
40
|
+
'readFile', 'writeFile', 'mkdir', 'join', 'resolve', 'basename', 'dirname',
|
|
41
|
+
'existsSync', 'readFileSync', 'writeFileSync',
|
|
42
|
+
// Go builtins
|
|
43
|
+
'make', 'new', 'append', 'copy', 'delete', 'close', 'panic', 'recover',
|
|
44
|
+
'println', 'printf', 'sprintf', 'errorf', 'fprintf',
|
|
45
|
+
// Rust macros / common stdlib
|
|
46
|
+
'println', 'eprintln', 'format', 'vec', 'assert', 'unwrap', 'expect',
|
|
47
|
+
'ok', 'err', 'some', 'none',
|
|
48
|
+
// Ruby builtins
|
|
49
|
+
'puts', 'print', 'p', 'raise', 'require', 'require_relative', 'include',
|
|
50
|
+
'extend', 'attr_accessor', 'attr_reader', 'attr_writer',
|
|
51
|
+
// Java common
|
|
52
|
+
'toString', 'equals', 'hashCode', 'getClass', 'println', 'printf',
|
|
53
|
+
// Swift stdlib / builtins
|
|
54
|
+
'print', 'debugPrint', 'dump', 'fatalError', 'precondition', 'preconditionFailure',
|
|
55
|
+
'assert', 'assertionFailure', 'withUnsafePointer', 'withUnsafeMutablePointer',
|
|
56
|
+
'DispatchQueue', 'main', 'async', 'sync', 'append', 'remove', 'insert', 'contains',
|
|
57
|
+
'map', 'filter', 'reduce', 'forEach', 'compactMap', 'flatMap', 'sorted', 'first', 'last',
|
|
58
|
+
// C++ stdlib / builtins
|
|
59
|
+
'cout', 'cin', 'cerr', 'endl', 'malloc', 'free', 'memcpy', 'memset', 'memcmp',
|
|
60
|
+
'strlen', 'strcpy', 'strcat', 'strcmp', 'sprintf', 'snprintf', 'fprintf',
|
|
61
|
+
'push_back', 'pop_back', 'emplace_back', 'begin', 'end', 'size', 'empty',
|
|
62
|
+
'find', 'insert', 'erase', 'at', 'front', 'back', 'clear', 'reserve', 'resize',
|
|
63
|
+
'make_shared', 'make_unique', 'move', 'forward', 'swap',
|
|
64
|
+
'static_cast', 'dynamic_cast', 'reinterpret_cast', 'const_cast',
|
|
65
|
+
]);
|
|
66
|
+
/** Returns true if the name should be skipped as a call target. */
|
|
67
|
+
function isIgnoredCallee(name) {
|
|
68
|
+
if (IGNORED_CALLEES.has(name))
|
|
69
|
+
return true;
|
|
70
|
+
// ALL_CAPS names (3+ chars) are almost certainly C/C++ macros, not functions
|
|
71
|
+
if (/^[A-Z][A-Z0-9_]{2,}$/.test(name))
|
|
72
|
+
return true;
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// PARSER SINGLETONS (lazy init)
|
|
77
|
+
// ============================================================================
|
|
78
|
+
let _tsParser;
|
|
79
|
+
let _pyParser;
|
|
80
|
+
let _goParser;
|
|
81
|
+
let _rustParser;
|
|
82
|
+
let _rubyParser;
|
|
83
|
+
let _javaParser;
|
|
84
|
+
let _cppParser;
|
|
85
|
+
let _swiftParser;
|
|
86
|
+
let _TsLanguage;
|
|
87
|
+
let _PyLanguage;
|
|
88
|
+
let _GoLanguage;
|
|
89
|
+
let _RustLanguage;
|
|
90
|
+
let _RubyLanguage;
|
|
91
|
+
let _JavaLanguage;
|
|
92
|
+
let _CppLanguage;
|
|
93
|
+
let _SwiftLanguage;
|
|
94
|
+
async function getTSParser() {
|
|
95
|
+
if (!_tsParser) {
|
|
96
|
+
const tsModule = await import('tree-sitter-typescript');
|
|
97
|
+
_TsLanguage = tsModule.default.typescript;
|
|
98
|
+
_tsParser = new Parser();
|
|
99
|
+
_tsParser.setLanguage(_TsLanguage);
|
|
100
|
+
}
|
|
101
|
+
return { parser: _tsParser, lang: _TsLanguage };
|
|
102
|
+
}
|
|
103
|
+
async function getPyParser() {
|
|
104
|
+
if (!_pyParser) {
|
|
105
|
+
const pyModule = await import('tree-sitter-python');
|
|
106
|
+
_PyLanguage = pyModule.default;
|
|
107
|
+
_pyParser = new Parser();
|
|
108
|
+
_pyParser.setLanguage(_PyLanguage);
|
|
109
|
+
}
|
|
110
|
+
return { parser: _pyParser, lang: _PyLanguage };
|
|
111
|
+
}
|
|
112
|
+
async function getGoParser() {
|
|
113
|
+
if (!_goParser) {
|
|
114
|
+
const goModule = await import('tree-sitter-go');
|
|
115
|
+
_GoLanguage = goModule.default;
|
|
116
|
+
_goParser = new Parser();
|
|
117
|
+
_goParser.setLanguage(_GoLanguage);
|
|
118
|
+
}
|
|
119
|
+
return { parser: _goParser, lang: _GoLanguage };
|
|
120
|
+
}
|
|
121
|
+
async function getRustParser() {
|
|
122
|
+
if (!_rustParser) {
|
|
123
|
+
const rustModule = await import('tree-sitter-rust');
|
|
124
|
+
_RustLanguage = rustModule.default;
|
|
125
|
+
_rustParser = new Parser();
|
|
126
|
+
_rustParser.setLanguage(_RustLanguage);
|
|
127
|
+
}
|
|
128
|
+
return { parser: _rustParser, lang: _RustLanguage };
|
|
129
|
+
}
|
|
130
|
+
async function getRubyParser() {
|
|
131
|
+
if (!_rubyParser) {
|
|
132
|
+
const rubyModule = await import('tree-sitter-ruby');
|
|
133
|
+
_RubyLanguage = rubyModule.default;
|
|
134
|
+
_rubyParser = new Parser();
|
|
135
|
+
_rubyParser.setLanguage(_RubyLanguage);
|
|
136
|
+
}
|
|
137
|
+
return { parser: _rubyParser, lang: _RubyLanguage };
|
|
138
|
+
}
|
|
139
|
+
async function getJavaParser() {
|
|
140
|
+
if (!_javaParser) {
|
|
141
|
+
const javaModule = await import('tree-sitter-java');
|
|
142
|
+
_JavaLanguage = javaModule.default;
|
|
143
|
+
_javaParser = new Parser();
|
|
144
|
+
_javaParser.setLanguage(_JavaLanguage);
|
|
145
|
+
}
|
|
146
|
+
return { parser: _javaParser, lang: _JavaLanguage };
|
|
147
|
+
}
|
|
148
|
+
async function getCppParser() {
|
|
149
|
+
if (!_cppParser) {
|
|
150
|
+
const cppModule = await import('tree-sitter-cpp');
|
|
151
|
+
_CppLanguage = cppModule.default;
|
|
152
|
+
_cppParser = new Parser();
|
|
153
|
+
_cppParser.setLanguage(_CppLanguage);
|
|
154
|
+
}
|
|
155
|
+
return { parser: _cppParser, lang: _CppLanguage };
|
|
156
|
+
}
|
|
157
|
+
async function getSwiftParser() {
|
|
158
|
+
if (!_swiftParser) {
|
|
159
|
+
const swiftModule = await import('tree-sitter-swift');
|
|
160
|
+
_SwiftLanguage = swiftModule.default;
|
|
161
|
+
_swiftParser = new Parser();
|
|
162
|
+
_swiftParser.setLanguage(_SwiftLanguage);
|
|
163
|
+
}
|
|
164
|
+
return { parser: _swiftParser, lang: _SwiftLanguage };
|
|
165
|
+
}
|
|
166
|
+
// ============================================================================
|
|
167
|
+
// ATTRIBUTION HELPER
|
|
168
|
+
// ============================================================================
|
|
169
|
+
/**
|
|
170
|
+
* Given a list of function nodes (with startIndex/endIndex) and a call position,
|
|
171
|
+
* find the narrowest enclosing function node.
|
|
172
|
+
*/
|
|
173
|
+
function findEnclosingFunction(nodes, callPos) {
|
|
174
|
+
let best;
|
|
175
|
+
let bestSize = Infinity;
|
|
176
|
+
for (const n of nodes) {
|
|
177
|
+
if (n.startIndex <= callPos && callPos < n.endIndex) {
|
|
178
|
+
const size = n.endIndex - n.startIndex;
|
|
179
|
+
if (size < bestSize) {
|
|
180
|
+
bestSize = size;
|
|
181
|
+
best = n;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return best;
|
|
186
|
+
}
|
|
187
|
+
// ============================================================================
|
|
188
|
+
// DOCSTRING / SIGNATURE EXTRACTION HELPERS
|
|
189
|
+
// ============================================================================
|
|
190
|
+
/**
|
|
191
|
+
* Scan backward from `startIndex` in `source` to find the doc comment
|
|
192
|
+
* immediately preceding the function declaration. Skip blank lines.
|
|
193
|
+
*
|
|
194
|
+
* For Python, docstrings are INSIDE the function body — scan forward from
|
|
195
|
+
* `startIndex` past the `def name(...):` colon to find the triple-quoted string.
|
|
196
|
+
*
|
|
197
|
+
* Returns the first meaningful (non-empty, non-decorator) line of the comment.
|
|
198
|
+
*/
|
|
199
|
+
function extractDocstringBefore(source, startIndex, language) {
|
|
200
|
+
// ── Python: scan forward past the colon into the function body ──────────
|
|
201
|
+
if (language === 'Python') {
|
|
202
|
+
// Find the colon that ends the `def` line
|
|
203
|
+
let i = startIndex;
|
|
204
|
+
while (i < source.length && source[i] !== ':')
|
|
205
|
+
i++;
|
|
206
|
+
// Skip past the colon
|
|
207
|
+
i++;
|
|
208
|
+
// Skip whitespace / newline
|
|
209
|
+
while (i < source.length && (source[i] === ' ' || source[i] === '\t' || source[i] === '\n' || source[i] === '\r'))
|
|
210
|
+
i++;
|
|
211
|
+
// Check for triple-quoted docstring
|
|
212
|
+
const tripleDouble = source.startsWith('"""', i);
|
|
213
|
+
const tripleSingle = source.startsWith("'''", i);
|
|
214
|
+
if (tripleDouble || tripleSingle) {
|
|
215
|
+
const quote = tripleDouble ? '"""' : "'''";
|
|
216
|
+
const bodyStart = i + 3;
|
|
217
|
+
const closeIdx = source.indexOf(quote, bodyStart);
|
|
218
|
+
if (closeIdx === -1)
|
|
219
|
+
return undefined;
|
|
220
|
+
const inner = source.slice(bodyStart, closeIdx);
|
|
221
|
+
const firstLine = inner.split('\n').map(l => l.trim()).find(l => l.length > 0);
|
|
222
|
+
return firstLine ?? undefined;
|
|
223
|
+
}
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
// ── All other languages: scan backward from startIndex ─────────────────
|
|
227
|
+
// Move to the character just before startIndex
|
|
228
|
+
let pos = startIndex - 1;
|
|
229
|
+
// Skip trailing whitespace / newlines before the declaration
|
|
230
|
+
while (pos >= 0 && (source[pos] === ' ' || source[pos] === '\t' || source[pos] === '\n' || source[pos] === '\r')) {
|
|
231
|
+
pos--;
|
|
232
|
+
}
|
|
233
|
+
if (pos < 0)
|
|
234
|
+
return undefined;
|
|
235
|
+
// ── TypeScript / JavaScript / Java / C++: JSDoc block /** ... */ ────────
|
|
236
|
+
if (language === 'TypeScript' || language === 'JavaScript' ||
|
|
237
|
+
language === 'Java' || language === 'C++') {
|
|
238
|
+
// Expect closing */ of a JSDoc block
|
|
239
|
+
if (source[pos] === '/' && pos > 0 && source[pos - 1] === '*') {
|
|
240
|
+
const closePos = pos - 1; // points at '*' of closing '*/'
|
|
241
|
+
// Find opening /**
|
|
242
|
+
const openIdx = source.lastIndexOf('/**', closePos);
|
|
243
|
+
if (openIdx === -1)
|
|
244
|
+
return undefined;
|
|
245
|
+
const inner = source.slice(openIdx + 3, closePos - 0);
|
|
246
|
+
// Remove leading * on each line, find first non-empty, non-@ line
|
|
247
|
+
const firstLine = inner
|
|
248
|
+
.split('\n')
|
|
249
|
+
.map(l => l.replace(/^\s*\*\s?/, '').trim())
|
|
250
|
+
.find(l => l.length > 0 && !l.startsWith('@'));
|
|
251
|
+
return firstLine ?? undefined;
|
|
252
|
+
}
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
// ── Go: // comment lines immediately before ──────────────────────────────
|
|
256
|
+
if (language === 'Go') {
|
|
257
|
+
const lines = [];
|
|
258
|
+
// Walk backward line by line
|
|
259
|
+
let lineEnd = pos;
|
|
260
|
+
while (lineEnd >= 0) {
|
|
261
|
+
// Find start of this line
|
|
262
|
+
let lineStart = lineEnd;
|
|
263
|
+
while (lineStart > 0 && source[lineStart - 1] !== '\n')
|
|
264
|
+
lineStart--;
|
|
265
|
+
const line = source.slice(lineStart, lineEnd + 1).trimEnd();
|
|
266
|
+
const trimmed = line.trim();
|
|
267
|
+
if (trimmed.startsWith('//')) {
|
|
268
|
+
lines.unshift(trimmed.slice(2).trim());
|
|
269
|
+
lineEnd = lineStart - 1;
|
|
270
|
+
// Skip over the newline
|
|
271
|
+
while (lineEnd >= 0 && (source[lineEnd] === '\n' || source[lineEnd] === '\r'))
|
|
272
|
+
lineEnd--;
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return lines.find(l => l.length > 0) ?? undefined;
|
|
279
|
+
}
|
|
280
|
+
// ── Rust / Swift: /// doc comment lines immediately before ─────────────
|
|
281
|
+
if (language === 'Rust' || language === 'Swift') {
|
|
282
|
+
const lines = [];
|
|
283
|
+
let lineEnd = pos;
|
|
284
|
+
while (lineEnd >= 0) {
|
|
285
|
+
let lineStart = lineEnd;
|
|
286
|
+
while (lineStart > 0 && source[lineStart - 1] !== '\n')
|
|
287
|
+
lineStart--;
|
|
288
|
+
const line = source.slice(lineStart, lineEnd + 1).trimEnd();
|
|
289
|
+
const trimmed = line.trim();
|
|
290
|
+
if (trimmed.startsWith('///')) {
|
|
291
|
+
lines.unshift(trimmed.slice(3).trim());
|
|
292
|
+
lineEnd = lineStart - 1;
|
|
293
|
+
while (lineEnd >= 0 && (source[lineEnd] === '\n' || source[lineEnd] === '\r'))
|
|
294
|
+
lineEnd--;
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return lines.find(l => l.length > 0) ?? undefined;
|
|
301
|
+
}
|
|
302
|
+
// ── Ruby: # comment lines immediately before ─────────────────────────────
|
|
303
|
+
if (language === 'Ruby') {
|
|
304
|
+
const lines = [];
|
|
305
|
+
let lineEnd = pos;
|
|
306
|
+
while (lineEnd >= 0) {
|
|
307
|
+
let lineStart = lineEnd;
|
|
308
|
+
while (lineStart > 0 && source[lineStart - 1] !== '\n')
|
|
309
|
+
lineStart--;
|
|
310
|
+
const line = source.slice(lineStart, lineEnd + 1).trimEnd();
|
|
311
|
+
const trimmed = line.trim();
|
|
312
|
+
if (trimmed.startsWith('#')) {
|
|
313
|
+
lines.unshift(trimmed.slice(1).trim());
|
|
314
|
+
lineEnd = lineStart - 1;
|
|
315
|
+
while (lineEnd >= 0 && (source[lineEnd] === '\n' || source[lineEnd] === '\r'))
|
|
316
|
+
lineEnd--;
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return lines.find(l => l.length > 0) ?? undefined;
|
|
323
|
+
}
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Extract the function declaration (signature without body) from
|
|
328
|
+
* `source.slice(startIndex, endIndex)`.
|
|
329
|
+
*
|
|
330
|
+
* Strategy:
|
|
331
|
+
* - TS/JS/Java/C++/Go/Rust/Ruby: take everything up to the first `{` at depth 0
|
|
332
|
+
* - Python: take everything up to the first `:` that ends the `def` line
|
|
333
|
+
*
|
|
334
|
+
* Whitespace is normalized (multiple spaces/newlines → single space).
|
|
335
|
+
* Limited to 300 characters max.
|
|
336
|
+
*/
|
|
337
|
+
function extractDeclaration(source, startIndex, endIndex, language) {
|
|
338
|
+
const slice = source.slice(startIndex, Math.min(endIndex, startIndex + 1500));
|
|
339
|
+
let decl;
|
|
340
|
+
if (language === 'Python') {
|
|
341
|
+
// Take up to (not including) the first `:` that ends the def line
|
|
342
|
+
// We scan for `:` while tracking parenthesis depth to avoid matching
|
|
343
|
+
// colons inside type annotations (e.g., def f(x: int) -> dict[str, int]:)
|
|
344
|
+
let depth = 0;
|
|
345
|
+
let end = -1;
|
|
346
|
+
for (let i = 0; i < slice.length; i++) {
|
|
347
|
+
const ch = slice[i];
|
|
348
|
+
if (ch === '(' || ch === '[' || ch === '{')
|
|
349
|
+
depth++;
|
|
350
|
+
else if (ch === ')' || ch === ']' || ch === '}')
|
|
351
|
+
depth--;
|
|
352
|
+
else if (ch === ':' && depth === 0) {
|
|
353
|
+
end = i;
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
decl = end !== -1 ? slice.slice(0, end) : slice.slice(0, 300);
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
// Find first `{` at brace depth 0
|
|
361
|
+
let depth = 0;
|
|
362
|
+
let end = -1;
|
|
363
|
+
for (let i = 0; i < slice.length; i++) {
|
|
364
|
+
const ch = slice[i];
|
|
365
|
+
if (ch === '{') {
|
|
366
|
+
if (depth === 0) {
|
|
367
|
+
end = i;
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
depth++;
|
|
371
|
+
}
|
|
372
|
+
else if (ch === '}') {
|
|
373
|
+
depth--;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
decl = end !== -1 ? slice.slice(0, end) : slice.slice(0, 300);
|
|
377
|
+
}
|
|
378
|
+
// Normalize whitespace
|
|
379
|
+
return decl.replace(/\s+/g, ' ').trim().slice(0, 300);
|
|
380
|
+
}
|
|
381
|
+
// ============================================================================
|
|
382
|
+
// TYPESCRIPT EXTRACTOR
|
|
383
|
+
// ============================================================================
|
|
384
|
+
const TS_FN_QUERY = `
|
|
385
|
+
(function_declaration
|
|
386
|
+
name: (identifier) @fn.name) @fn.node
|
|
387
|
+
|
|
388
|
+
(export_statement
|
|
389
|
+
declaration: (function_declaration
|
|
390
|
+
name: (identifier) @fn.name)) @fn.node
|
|
391
|
+
|
|
392
|
+
(method_definition
|
|
393
|
+
name: (property_identifier) @fn.name) @fn.node
|
|
394
|
+
|
|
395
|
+
(lexical_declaration
|
|
396
|
+
(variable_declarator
|
|
397
|
+
name: (identifier) @fn.name
|
|
398
|
+
value: [(arrow_function) (function_expression)] @fn.value)) @fn.node
|
|
399
|
+
`;
|
|
400
|
+
const TS_CALL_QUERY = `
|
|
401
|
+
(call_expression
|
|
402
|
+
function: [(identifier) @call.name
|
|
403
|
+
(member_expression
|
|
404
|
+
object: (identifier) @call.object
|
|
405
|
+
property: (property_identifier) @call.name)]) @call.node
|
|
406
|
+
`;
|
|
407
|
+
async function extractTSGraph(filePath, content) {
|
|
408
|
+
const { parser, lang } = await getTSParser();
|
|
409
|
+
const tree = parser.parse(content);
|
|
410
|
+
const fnQuery = new Parser.Query(lang, TS_FN_QUERY);
|
|
411
|
+
const callQuery = new Parser.Query(lang, TS_CALL_QUERY);
|
|
412
|
+
// --- Extract function nodes ---
|
|
413
|
+
const nodes = [];
|
|
414
|
+
const fnMatches = fnQuery.matches(tree.rootNode);
|
|
415
|
+
for (const match of fnMatches) {
|
|
416
|
+
const nameCapture = match.captures.find(c => c.name === 'fn.name');
|
|
417
|
+
const nodeCapture = match.captures.find(c => c.name === 'fn.node');
|
|
418
|
+
if (!nameCapture || !nodeCapture)
|
|
419
|
+
continue;
|
|
420
|
+
const name = nameCapture.node.text;
|
|
421
|
+
const fnNode = nodeCapture.node;
|
|
422
|
+
// Find enclosing class (walk up — skip class_body, its children are methods not the name)
|
|
423
|
+
let className;
|
|
424
|
+
let cursor = fnNode.parent;
|
|
425
|
+
while (cursor) {
|
|
426
|
+
if (cursor.type === 'class_declaration') {
|
|
427
|
+
const classNameNode = cursor.children.find(c => c.type === 'type_identifier' || c.type === 'identifier');
|
|
428
|
+
if (classNameNode)
|
|
429
|
+
className = classNameNode.text;
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
cursor = cursor.parent;
|
|
433
|
+
}
|
|
434
|
+
// Detect async (method_definition has 'async' as first named child keyword)
|
|
435
|
+
const isAsync = fnNode.children.some(c => c.type === 'async') ||
|
|
436
|
+
fnNode.text.startsWith('async ');
|
|
437
|
+
const id = className
|
|
438
|
+
? `${filePath}::${className}.${name}`
|
|
439
|
+
: `${filePath}::${name}`;
|
|
440
|
+
nodes.push({
|
|
441
|
+
id,
|
|
442
|
+
name,
|
|
443
|
+
filePath,
|
|
444
|
+
className,
|
|
445
|
+
isAsync,
|
|
446
|
+
language: 'TypeScript',
|
|
447
|
+
startIndex: fnNode.startIndex,
|
|
448
|
+
endIndex: fnNode.endIndex,
|
|
449
|
+
fanIn: 0,
|
|
450
|
+
fanOut: 0,
|
|
451
|
+
docstring: extractDocstringBefore(content, fnNode.startIndex, 'TypeScript'),
|
|
452
|
+
signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'TypeScript'),
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
// --- Extract calls ---
|
|
456
|
+
const rawEdges = [];
|
|
457
|
+
const callMatches = callQuery.matches(tree.rootNode);
|
|
458
|
+
for (const match of callMatches) {
|
|
459
|
+
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
460
|
+
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
461
|
+
const objectCapture = match.captures.find(c => c.name === 'call.object');
|
|
462
|
+
if (!nameCapture || !nodeCapture)
|
|
463
|
+
continue;
|
|
464
|
+
const calleeName = nameCapture.node.text;
|
|
465
|
+
if (isIgnoredCallee(calleeName))
|
|
466
|
+
continue;
|
|
467
|
+
const callPos = nodeCapture.node.startIndex;
|
|
468
|
+
const caller = findEnclosingFunction(nodes, callPos);
|
|
469
|
+
if (!caller)
|
|
470
|
+
continue;
|
|
471
|
+
// Detect call type from AST parent context
|
|
472
|
+
let callType = objectCapture ? 'method' : 'direct';
|
|
473
|
+
const parentType = nodeCapture.node.parent?.type;
|
|
474
|
+
if (parentType === 'await_expression')
|
|
475
|
+
callType = 'awaited';
|
|
476
|
+
else if (parentType === 'new_expression')
|
|
477
|
+
callType = 'constructor';
|
|
478
|
+
rawEdges.push({
|
|
479
|
+
callerId: caller.id,
|
|
480
|
+
calleeName,
|
|
481
|
+
line: nodeCapture.node.startPosition.row + 1,
|
|
482
|
+
calleeObject: objectCapture?.node.text,
|
|
483
|
+
callType,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
return { nodes, rawEdges };
|
|
487
|
+
}
|
|
488
|
+
// ============================================================================
|
|
489
|
+
// PYTHON EXTRACTOR
|
|
490
|
+
// ============================================================================
|
|
491
|
+
const PY_FN_QUERY = `
|
|
492
|
+
(function_definition
|
|
493
|
+
name: (identifier) @fn.name) @fn.node
|
|
494
|
+
|
|
495
|
+
(decorated_definition
|
|
496
|
+
(function_definition
|
|
497
|
+
name: (identifier) @fn.name)) @fn.node
|
|
498
|
+
`;
|
|
499
|
+
/**
|
|
500
|
+
* Direct function calls: foo(), bar(x)
|
|
501
|
+
* We keep this separate from attribute calls so we can filter attribute calls
|
|
502
|
+
* by object name (only self/cls are resolved to internal functions).
|
|
503
|
+
*/
|
|
504
|
+
const PY_DIRECT_CALL_QUERY = `
|
|
505
|
+
(call
|
|
506
|
+
function: (identifier) @call.name) @call.node
|
|
507
|
+
`;
|
|
508
|
+
/**
|
|
509
|
+
* Method calls on an object: obj.method()
|
|
510
|
+
* We capture the object name so we can restrict resolution to self/cls.
|
|
511
|
+
* Calls like redis.get(), dict.get(), os.environ.get() are NOT resolved —
|
|
512
|
+
* only self.method() and cls.method() are tracked as internal edges.
|
|
513
|
+
*/
|
|
514
|
+
const PY_METHOD_CALL_QUERY = `
|
|
515
|
+
(call
|
|
516
|
+
function: (attribute
|
|
517
|
+
object: (identifier) @call.object
|
|
518
|
+
attribute: (identifier) @call.name)) @call.node
|
|
519
|
+
`;
|
|
520
|
+
async function extractPyGraph(filePath, content) {
|
|
521
|
+
const { parser, lang } = await getPyParser();
|
|
522
|
+
const tree = parser.parse(content);
|
|
523
|
+
const fnQuery = new Parser.Query(lang, PY_FN_QUERY);
|
|
524
|
+
// --- Extract function nodes ---
|
|
525
|
+
const nodes = [];
|
|
526
|
+
const seen = new Set(); // avoid duplicates from decorated_definition + function_definition
|
|
527
|
+
const fnMatches = fnQuery.matches(tree.rootNode);
|
|
528
|
+
for (const match of fnMatches) {
|
|
529
|
+
const nameCapture = match.captures.find(c => c.name === 'fn.name');
|
|
530
|
+
const nodeCapture = match.captures.find(c => c.name === 'fn.node');
|
|
531
|
+
if (!nameCapture || !nodeCapture)
|
|
532
|
+
continue;
|
|
533
|
+
const name = nameCapture.node.text;
|
|
534
|
+
const fnNode = nodeCapture.node;
|
|
535
|
+
// Deduplicate by name node position (decorated_definition wraps the function_definition)
|
|
536
|
+
if (seen.has(nameCapture.node.startIndex))
|
|
537
|
+
continue;
|
|
538
|
+
seen.add(nameCapture.node.startIndex);
|
|
539
|
+
// Find enclosing class
|
|
540
|
+
let className;
|
|
541
|
+
let cursor = fnNode.parent;
|
|
542
|
+
while (cursor) {
|
|
543
|
+
if (cursor.type === 'class_definition') {
|
|
544
|
+
const classNameNode = cursor.children.find(c => c.type === 'identifier');
|
|
545
|
+
if (classNameNode)
|
|
546
|
+
className = classNameNode.text;
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
cursor = cursor.parent;
|
|
550
|
+
}
|
|
551
|
+
// Skip private methods (underscore prefix) unless they're __init__ or there are very few nodes
|
|
552
|
+
if (name.startsWith('_') && name !== '__init__')
|
|
553
|
+
continue;
|
|
554
|
+
const isAsync = fnNode.text.startsWith('async ') ||
|
|
555
|
+
(fnNode.type === 'function_definition' && fnNode.children[0]?.text === 'async');
|
|
556
|
+
const id = className
|
|
557
|
+
? `${filePath}::${className}.${name}`
|
|
558
|
+
: `${filePath}::${name}`;
|
|
559
|
+
nodes.push({
|
|
560
|
+
id,
|
|
561
|
+
name,
|
|
562
|
+
filePath,
|
|
563
|
+
className,
|
|
564
|
+
isAsync,
|
|
565
|
+
language: 'Python',
|
|
566
|
+
startIndex: fnNode.startIndex,
|
|
567
|
+
endIndex: fnNode.endIndex,
|
|
568
|
+
fanIn: 0,
|
|
569
|
+
fanOut: 0,
|
|
570
|
+
docstring: extractDocstringBefore(content, fnNode.startIndex, 'Python'),
|
|
571
|
+
signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'Python'),
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
// --- Extract calls ---
|
|
575
|
+
const rawEdges = [];
|
|
576
|
+
const directCallQuery = new Parser.Query(lang, PY_DIRECT_CALL_QUERY);
|
|
577
|
+
const methodCallQuery = new Parser.Query(lang, PY_METHOD_CALL_QUERY);
|
|
578
|
+
// Direct calls: foo(), bar(x) — resolve across all files
|
|
579
|
+
for (const match of directCallQuery.matches(tree.rootNode)) {
|
|
580
|
+
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
581
|
+
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
582
|
+
if (!nameCapture || !nodeCapture)
|
|
583
|
+
continue;
|
|
584
|
+
const calleeName = nameCapture.node.text;
|
|
585
|
+
if (isIgnoredCallee(calleeName))
|
|
586
|
+
continue;
|
|
587
|
+
const callPos = nodeCapture.node.startIndex;
|
|
588
|
+
const caller = findEnclosingFunction(nodes, callPos);
|
|
589
|
+
if (!caller)
|
|
590
|
+
continue;
|
|
591
|
+
// In Python tree-sitter, `await expr` wraps the call: parent type is 'await'
|
|
592
|
+
const callType = nodeCapture.node.parent?.type === 'await' ? 'awaited' : 'direct';
|
|
593
|
+
rawEdges.push({
|
|
594
|
+
callerId: caller.id,
|
|
595
|
+
calleeName,
|
|
596
|
+
line: nodeCapture.node.startPosition.row + 1,
|
|
597
|
+
callType,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
// Method calls: obj.method() — capture receiver for type-inference-based resolution
|
|
601
|
+
for (const match of methodCallQuery.matches(tree.rootNode)) {
|
|
602
|
+
const objectCapture = match.captures.find(c => c.name === 'call.object');
|
|
603
|
+
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
604
|
+
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
605
|
+
if (!objectCapture || !nameCapture || !nodeCapture)
|
|
606
|
+
continue;
|
|
607
|
+
const calleeName = nameCapture.node.text;
|
|
608
|
+
if (isIgnoredCallee(calleeName))
|
|
609
|
+
continue;
|
|
610
|
+
const callPos = nodeCapture.node.startIndex;
|
|
611
|
+
const caller = findEnclosingFunction(nodes, callPos);
|
|
612
|
+
if (!caller)
|
|
613
|
+
continue;
|
|
614
|
+
const methodCallType = nodeCapture.node.parent?.type === 'await' ? 'awaited' : 'method';
|
|
615
|
+
rawEdges.push({
|
|
616
|
+
callerId: caller.id,
|
|
617
|
+
calleeName,
|
|
618
|
+
line: nodeCapture.node.startPosition.row + 1,
|
|
619
|
+
calleeObject: objectCapture.node.text,
|
|
620
|
+
callType: methodCallType,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
return { nodes, rawEdges };
|
|
624
|
+
}
|
|
625
|
+
// ============================================================================
|
|
626
|
+
// GO EXTRACTOR
|
|
627
|
+
// ============================================================================
|
|
628
|
+
const GO_FN_QUERY = `
|
|
629
|
+
(function_declaration
|
|
630
|
+
name: (identifier) @fn.name) @fn.node
|
|
631
|
+
|
|
632
|
+
(method_declaration
|
|
633
|
+
name: (field_identifier) @fn.name) @fn.node
|
|
634
|
+
`;
|
|
635
|
+
const GO_CALL_QUERY = `
|
|
636
|
+
(call_expression
|
|
637
|
+
function: (identifier) @call.name) @call.node
|
|
638
|
+
|
|
639
|
+
(call_expression
|
|
640
|
+
function: (selector_expression
|
|
641
|
+
operand: (identifier) @call.object
|
|
642
|
+
field: (field_identifier) @call.name)) @call.node
|
|
643
|
+
`;
|
|
644
|
+
async function extractGoGraph(filePath, content) {
|
|
645
|
+
const { parser, lang } = await getGoParser();
|
|
646
|
+
const tree = parser.parse(content);
|
|
647
|
+
const fnQuery = new Parser.Query(lang, GO_FN_QUERY);
|
|
648
|
+
const callQuery = new Parser.Query(lang, GO_CALL_QUERY);
|
|
649
|
+
const nodes = [];
|
|
650
|
+
for (const match of fnQuery.matches(tree.rootNode)) {
|
|
651
|
+
const nameCapture = match.captures.find(c => c.name === 'fn.name');
|
|
652
|
+
const nodeCapture = match.captures.find(c => c.name === 'fn.node');
|
|
653
|
+
if (!nameCapture || !nodeCapture)
|
|
654
|
+
continue;
|
|
655
|
+
const name = nameCapture.node.text;
|
|
656
|
+
const fnNode = nodeCapture.node;
|
|
657
|
+
// Receiver type for method_declaration → use as className
|
|
658
|
+
let className;
|
|
659
|
+
if (fnNode.type === 'method_declaration') {
|
|
660
|
+
const receiver = fnNode.children.find(c => c.type === 'parameter_list');
|
|
661
|
+
if (receiver) {
|
|
662
|
+
// Extract type name from receiver: (r *MyStruct) → MyStruct
|
|
663
|
+
const typeNode = receiver.descendantsOfType('type_identifier')[0]
|
|
664
|
+
?? receiver.descendantsOfType('pointer_type')[0];
|
|
665
|
+
if (typeNode)
|
|
666
|
+
className = typeNode.text.replace(/^\*/, '');
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
const id = className ? `${filePath}::${className}.${name}` : `${filePath}::${name}`;
|
|
670
|
+
nodes.push({
|
|
671
|
+
id, name, filePath, className,
|
|
672
|
+
isAsync: false, // Go has goroutines, not async/await
|
|
673
|
+
language: 'Go',
|
|
674
|
+
startIndex: fnNode.startIndex,
|
|
675
|
+
endIndex: fnNode.endIndex,
|
|
676
|
+
fanIn: 0, fanOut: 0,
|
|
677
|
+
docstring: extractDocstringBefore(content, fnNode.startIndex, 'Go'),
|
|
678
|
+
signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'Go'),
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
const rawEdges = [];
|
|
682
|
+
for (const match of callQuery.matches(tree.rootNode)) {
|
|
683
|
+
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
684
|
+
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
685
|
+
const objectCapture = match.captures.find(c => c.name === 'call.object');
|
|
686
|
+
if (!nameCapture || !nodeCapture)
|
|
687
|
+
continue;
|
|
688
|
+
const calleeName = nameCapture.node.text;
|
|
689
|
+
if (isIgnoredCallee(calleeName))
|
|
690
|
+
continue;
|
|
691
|
+
const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
|
|
692
|
+
if (!caller)
|
|
693
|
+
continue;
|
|
694
|
+
rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1, calleeObject: objectCapture?.node.text });
|
|
695
|
+
}
|
|
696
|
+
return { nodes, rawEdges };
|
|
697
|
+
}
|
|
698
|
+
// ============================================================================
|
|
699
|
+
// RUST EXTRACTOR
|
|
700
|
+
// ============================================================================
|
|
701
|
+
const RUST_FN_QUERY = `
|
|
702
|
+
(function_item
|
|
703
|
+
name: (identifier) @fn.name) @fn.node
|
|
704
|
+
`;
|
|
705
|
+
const RUST_CALL_QUERY = `
|
|
706
|
+
(call_expression
|
|
707
|
+
function: (identifier) @call.name) @call.node
|
|
708
|
+
|
|
709
|
+
(call_expression
|
|
710
|
+
function: (field_expression
|
|
711
|
+
value: (identifier) @call.object
|
|
712
|
+
field: (field_identifier) @call.name)) @call.node
|
|
713
|
+
`;
|
|
714
|
+
async function extractRustGraph(filePath, content) {
|
|
715
|
+
const { parser, lang } = await getRustParser();
|
|
716
|
+
const tree = parser.parse(content);
|
|
717
|
+
const fnQuery = new Parser.Query(lang, RUST_FN_QUERY);
|
|
718
|
+
const callQuery = new Parser.Query(lang, RUST_CALL_QUERY);
|
|
719
|
+
const nodes = [];
|
|
720
|
+
for (const match of fnQuery.matches(tree.rootNode)) {
|
|
721
|
+
const nameCapture = match.captures.find(c => c.name === 'fn.name');
|
|
722
|
+
const nodeCapture = match.captures.find(c => c.name === 'fn.node');
|
|
723
|
+
if (!nameCapture || !nodeCapture)
|
|
724
|
+
continue;
|
|
725
|
+
const name = nameCapture.node.text;
|
|
726
|
+
const fnNode = nodeCapture.node;
|
|
727
|
+
// Find enclosing impl block → use as className
|
|
728
|
+
let className;
|
|
729
|
+
let cursor = fnNode.parent;
|
|
730
|
+
while (cursor) {
|
|
731
|
+
if (cursor.type === 'impl_item') {
|
|
732
|
+
const typeNode = cursor.children.find(c => c.type === 'type_identifier');
|
|
733
|
+
if (typeNode)
|
|
734
|
+
className = typeNode.text;
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
737
|
+
cursor = cursor.parent;
|
|
738
|
+
}
|
|
739
|
+
// Rust: async keyword lives inside a function_modifiers child
|
|
740
|
+
const isAsync = fnNode.children.some(c => c.type === 'function_modifiers' && c.text.includes('async'));
|
|
741
|
+
const id = className ? `${filePath}::${className}.${name}` : `${filePath}::${name}`;
|
|
742
|
+
nodes.push({
|
|
743
|
+
id, name, filePath, className,
|
|
744
|
+
isAsync,
|
|
745
|
+
language: 'Rust',
|
|
746
|
+
startIndex: fnNode.startIndex,
|
|
747
|
+
endIndex: fnNode.endIndex,
|
|
748
|
+
fanIn: 0, fanOut: 0,
|
|
749
|
+
docstring: extractDocstringBefore(content, fnNode.startIndex, 'Rust'),
|
|
750
|
+
signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'Rust'),
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
const rawEdges = [];
|
|
754
|
+
for (const match of callQuery.matches(tree.rootNode)) {
|
|
755
|
+
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
756
|
+
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
757
|
+
const objectCapture = match.captures.find(c => c.name === 'call.object');
|
|
758
|
+
if (!nameCapture || !nodeCapture)
|
|
759
|
+
continue;
|
|
760
|
+
const calleeName = nameCapture.node.text;
|
|
761
|
+
if (isIgnoredCallee(calleeName))
|
|
762
|
+
continue;
|
|
763
|
+
const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
|
|
764
|
+
if (!caller)
|
|
765
|
+
continue;
|
|
766
|
+
rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1, calleeObject: objectCapture?.node.text });
|
|
767
|
+
}
|
|
768
|
+
return { nodes, rawEdges };
|
|
769
|
+
}
|
|
770
|
+
// ============================================================================
|
|
771
|
+
// RUBY EXTRACTOR
|
|
772
|
+
// ============================================================================
|
|
773
|
+
const RUBY_FN_QUERY = `
|
|
774
|
+
(method
|
|
775
|
+
name: (identifier) @fn.name) @fn.node
|
|
776
|
+
|
|
777
|
+
(singleton_method
|
|
778
|
+
name: (identifier) @fn.name) @fn.node
|
|
779
|
+
`;
|
|
780
|
+
// Explicit calls: fn(), obj.method()
|
|
781
|
+
const RUBY_CALL_QUERY = `
|
|
782
|
+
(call
|
|
783
|
+
receiver: (identifier) @call.object
|
|
784
|
+
method: (identifier) @call.name) @call.node
|
|
785
|
+
|
|
786
|
+
(call
|
|
787
|
+
method: (identifier) @call.name) @call.node
|
|
788
|
+
`;
|
|
789
|
+
// Bareword calls: Ruby allows calling methods without parentheses.
|
|
790
|
+
// An identifier at statement level inside a body_statement is almost always
|
|
791
|
+
// a method call (variable usage appears in assignments/expressions, not alone).
|
|
792
|
+
const RUBY_BAREWORD_QUERY = `
|
|
793
|
+
(body_statement
|
|
794
|
+
(identifier) @call.name)
|
|
795
|
+
`;
|
|
796
|
+
async function extractRubyGraph(filePath, content) {
|
|
797
|
+
const { parser, lang } = await getRubyParser();
|
|
798
|
+
const tree = parser.parse(content);
|
|
799
|
+
const fnQuery = new Parser.Query(lang, RUBY_FN_QUERY);
|
|
800
|
+
const callQuery = new Parser.Query(lang, RUBY_CALL_QUERY);
|
|
801
|
+
const barewordQuery = new Parser.Query(lang, RUBY_BAREWORD_QUERY);
|
|
802
|
+
const nodes = [];
|
|
803
|
+
for (const match of fnQuery.matches(tree.rootNode)) {
|
|
804
|
+
const nameCapture = match.captures.find(c => c.name === 'fn.name');
|
|
805
|
+
const nodeCapture = match.captures.find(c => c.name === 'fn.node');
|
|
806
|
+
if (!nameCapture || !nodeCapture)
|
|
807
|
+
continue;
|
|
808
|
+
const name = nameCapture.node.text;
|
|
809
|
+
const fnNode = nodeCapture.node;
|
|
810
|
+
// Find enclosing class/module
|
|
811
|
+
let className;
|
|
812
|
+
let cursor = fnNode.parent;
|
|
813
|
+
while (cursor) {
|
|
814
|
+
if (cursor.type === 'class' || cursor.type === 'module') {
|
|
815
|
+
const nameNode = cursor.children.find(c => c.type === 'constant' || c.type === 'scope_resolution');
|
|
816
|
+
if (nameNode)
|
|
817
|
+
className = nameNode.text;
|
|
818
|
+
break;
|
|
819
|
+
}
|
|
820
|
+
cursor = cursor.parent;
|
|
821
|
+
}
|
|
822
|
+
const id = className ? `${filePath}::${className}.${name}` : `${filePath}::${name}`;
|
|
823
|
+
nodes.push({
|
|
824
|
+
id, name, filePath, className,
|
|
825
|
+
isAsync: false,
|
|
826
|
+
language: 'Ruby',
|
|
827
|
+
startIndex: fnNode.startIndex,
|
|
828
|
+
endIndex: fnNode.endIndex,
|
|
829
|
+
fanIn: 0, fanOut: 0,
|
|
830
|
+
docstring: extractDocstringBefore(content, fnNode.startIndex, 'Ruby'),
|
|
831
|
+
signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'Ruby'),
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
const rawEdges = [];
|
|
835
|
+
// Explicit calls: fn(), obj.method()
|
|
836
|
+
for (const match of callQuery.matches(tree.rootNode)) {
|
|
837
|
+
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
838
|
+
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
839
|
+
const objectCapture = match.captures.find(c => c.name === 'call.object');
|
|
840
|
+
if (!nameCapture || !nodeCapture)
|
|
841
|
+
continue;
|
|
842
|
+
const calleeName = nameCapture.node.text;
|
|
843
|
+
if (isIgnoredCallee(calleeName))
|
|
844
|
+
continue;
|
|
845
|
+
const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
|
|
846
|
+
if (!caller)
|
|
847
|
+
continue;
|
|
848
|
+
rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1, calleeObject: objectCapture?.node.text });
|
|
849
|
+
}
|
|
850
|
+
// Bareword calls: identifier at statement level, no parens
|
|
851
|
+
for (const match of barewordQuery.matches(tree.rootNode)) {
|
|
852
|
+
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
853
|
+
if (!nameCapture)
|
|
854
|
+
continue;
|
|
855
|
+
const calleeName = nameCapture.node.text;
|
|
856
|
+
if (isIgnoredCallee(calleeName))
|
|
857
|
+
continue;
|
|
858
|
+
const caller = findEnclosingFunction(nodes, nameCapture.node.startIndex);
|
|
859
|
+
if (!caller)
|
|
860
|
+
continue;
|
|
861
|
+
rawEdges.push({ callerId: caller.id, calleeName, line: nameCapture.node.startPosition.row + 1 });
|
|
862
|
+
}
|
|
863
|
+
return { nodes, rawEdges };
|
|
864
|
+
}
|
|
865
|
+
// ============================================================================
|
|
866
|
+
// JAVA EXTRACTOR
|
|
867
|
+
// ============================================================================
|
|
868
|
+
const JAVA_FN_QUERY = `
|
|
869
|
+
(method_declaration
|
|
870
|
+
name: (identifier) @fn.name) @fn.node
|
|
871
|
+
|
|
872
|
+
(constructor_declaration
|
|
873
|
+
name: (identifier) @fn.name) @fn.node
|
|
874
|
+
`;
|
|
875
|
+
const JAVA_CALL_QUERY = `
|
|
876
|
+
(method_invocation
|
|
877
|
+
object: (identifier) @call.object
|
|
878
|
+
name: (identifier) @call.name) @call.node
|
|
879
|
+
|
|
880
|
+
(method_invocation
|
|
881
|
+
name: (identifier) @call.name) @call.node
|
|
882
|
+
`;
|
|
883
|
+
async function extractJavaGraph(filePath, content) {
|
|
884
|
+
const { parser, lang } = await getJavaParser();
|
|
885
|
+
const tree = parser.parse(content);
|
|
886
|
+
const fnQuery = new Parser.Query(lang, JAVA_FN_QUERY);
|
|
887
|
+
const callQuery = new Parser.Query(lang, JAVA_CALL_QUERY);
|
|
888
|
+
const nodes = [];
|
|
889
|
+
for (const match of fnQuery.matches(tree.rootNode)) {
|
|
890
|
+
const nameCapture = match.captures.find(c => c.name === 'fn.name');
|
|
891
|
+
const nodeCapture = match.captures.find(c => c.name === 'fn.node');
|
|
892
|
+
if (!nameCapture || !nodeCapture)
|
|
893
|
+
continue;
|
|
894
|
+
const name = nameCapture.node.text;
|
|
895
|
+
const fnNode = nodeCapture.node;
|
|
896
|
+
// Find enclosing class/interface/enum
|
|
897
|
+
let className;
|
|
898
|
+
let cursor = fnNode.parent;
|
|
899
|
+
while (cursor) {
|
|
900
|
+
if (cursor.type === 'class_declaration' || cursor.type === 'interface_declaration' || cursor.type === 'enum_declaration') {
|
|
901
|
+
const nameNode = cursor.children.find(c => c.type === 'identifier');
|
|
902
|
+
if (nameNode)
|
|
903
|
+
className = nameNode.text;
|
|
904
|
+
break;
|
|
905
|
+
}
|
|
906
|
+
cursor = cursor.parent;
|
|
907
|
+
}
|
|
908
|
+
const isAsync = false; // Java uses Future/CompletableFuture, not async keyword
|
|
909
|
+
const id = className ? `${filePath}::${className}.${name}` : `${filePath}::${name}`;
|
|
910
|
+
nodes.push({
|
|
911
|
+
id, name, filePath, className,
|
|
912
|
+
isAsync,
|
|
913
|
+
language: 'Java',
|
|
914
|
+
startIndex: fnNode.startIndex,
|
|
915
|
+
endIndex: fnNode.endIndex,
|
|
916
|
+
fanIn: 0, fanOut: 0,
|
|
917
|
+
docstring: extractDocstringBefore(content, fnNode.startIndex, 'Java'),
|
|
918
|
+
signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'Java'),
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
const rawEdges = [];
|
|
922
|
+
for (const match of callQuery.matches(tree.rootNode)) {
|
|
923
|
+
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
924
|
+
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
925
|
+
const objectCapture = match.captures.find(c => c.name === 'call.object');
|
|
926
|
+
if (!nameCapture || !nodeCapture)
|
|
927
|
+
continue;
|
|
928
|
+
const calleeName = nameCapture.node.text;
|
|
929
|
+
if (isIgnoredCallee(calleeName))
|
|
930
|
+
continue;
|
|
931
|
+
const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
|
|
932
|
+
if (!caller)
|
|
933
|
+
continue;
|
|
934
|
+
rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1, calleeObject: objectCapture?.node.text });
|
|
935
|
+
}
|
|
936
|
+
return { nodes, rawEdges };
|
|
937
|
+
}
|
|
938
|
+
// ============================================================================
|
|
939
|
+
// C++ EXTRACTOR
|
|
940
|
+
// ============================================================================
|
|
941
|
+
/**
|
|
942
|
+
* Safely run a tree-sitter query, returning [] if the S-expression is invalid
|
|
943
|
+
* for the grammar. C++ grammar has many edge cases (templates, operators,
|
|
944
|
+
* pointer declarators) that can make certain queries fail.
|
|
945
|
+
*/
|
|
946
|
+
function safeQuery(lang, queryStr, root) {
|
|
947
|
+
try {
|
|
948
|
+
const q = new Parser.Query(lang, queryStr);
|
|
949
|
+
return q.matches(root);
|
|
950
|
+
}
|
|
951
|
+
catch {
|
|
952
|
+
return [];
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
/** Free functions and inline class methods with a simple identifier name */
|
|
956
|
+
const CPP_FN_BASIC_QUERY = `
|
|
957
|
+
(function_definition
|
|
958
|
+
declarator: (function_declarator
|
|
959
|
+
declarator: (identifier) @fn.name)) @fn.node
|
|
960
|
+
|
|
961
|
+
(function_definition
|
|
962
|
+
declarator: (function_declarator
|
|
963
|
+
declarator: (field_identifier) @fn.name)) @fn.node
|
|
964
|
+
`;
|
|
965
|
+
/** Out-of-class definitions: void Foo::bar() {} */
|
|
966
|
+
const CPP_FN_QUALIFIED_QUERY = `
|
|
967
|
+
(function_definition
|
|
968
|
+
declarator: (function_declarator
|
|
969
|
+
declarator: (qualified_identifier
|
|
970
|
+
name: (identifier) @fn.name))) @fn.node
|
|
971
|
+
`;
|
|
972
|
+
/** Plain function calls: foo() */
|
|
973
|
+
const CPP_CALL_DIRECT_QUERY = `
|
|
974
|
+
(call_expression
|
|
975
|
+
function: (identifier) @call.name) @call.node
|
|
976
|
+
`;
|
|
977
|
+
/** Member calls: obj.method() and ptr->method() — captures receiver */
|
|
978
|
+
const CPP_CALL_MEMBER_QUERY = `
|
|
979
|
+
(call_expression
|
|
980
|
+
function: (field_expression
|
|
981
|
+
argument: (identifier) @call.object
|
|
982
|
+
field: (field_identifier) @call.name)) @call.node
|
|
983
|
+
`;
|
|
984
|
+
async function extractCppGraph(filePath, content) {
|
|
985
|
+
const { parser, lang } = await getCppParser();
|
|
986
|
+
const tree = parser.parse(content);
|
|
987
|
+
const nodes = [];
|
|
988
|
+
const seen = new Set(); // deduplicate by name-node start position
|
|
989
|
+
for (const queryStr of [CPP_FN_BASIC_QUERY, CPP_FN_QUALIFIED_QUERY]) {
|
|
990
|
+
for (const match of safeQuery(lang, queryStr, tree.rootNode)) {
|
|
991
|
+
const nameCapture = match.captures.find(c => c.name === 'fn.name');
|
|
992
|
+
const nodeCapture = match.captures.find(c => c.name === 'fn.node');
|
|
993
|
+
if (!nameCapture || !nodeCapture)
|
|
994
|
+
continue;
|
|
995
|
+
if (seen.has(nameCapture.node.startIndex))
|
|
996
|
+
continue;
|
|
997
|
+
seen.add(nameCapture.node.startIndex);
|
|
998
|
+
const name = nameCapture.node.text;
|
|
999
|
+
// Skip ALL_CAPS names — these are almost certainly macros, not functions
|
|
1000
|
+
if (/^[A-Z][A-Z0-9_]{2,}$/.test(name))
|
|
1001
|
+
continue;
|
|
1002
|
+
const fnNode = nodeCapture.node;
|
|
1003
|
+
// Find enclosing class (inline method defined inside class body)
|
|
1004
|
+
let className;
|
|
1005
|
+
let cursor = fnNode.parent;
|
|
1006
|
+
while (cursor) {
|
|
1007
|
+
if (cursor.type === 'class_specifier' || cursor.type === 'struct_specifier') {
|
|
1008
|
+
const nameNode = cursor.children.find(c => c.type === 'type_identifier');
|
|
1009
|
+
if (nameNode)
|
|
1010
|
+
className = nameNode.text;
|
|
1011
|
+
break;
|
|
1012
|
+
}
|
|
1013
|
+
cursor = cursor.parent;
|
|
1014
|
+
}
|
|
1015
|
+
// For out-of-class: void Foo::bar() — extract class from qualified_identifier scope
|
|
1016
|
+
if (!className) {
|
|
1017
|
+
const fnDeclarator = fnNode.children.find(c => c.type === 'function_declarator');
|
|
1018
|
+
if (fnDeclarator) {
|
|
1019
|
+
const qualNode = fnDeclarator.children.find(c => c.type === 'qualified_identifier');
|
|
1020
|
+
if (qualNode) {
|
|
1021
|
+
const scopeNode = qualNode.children.find(c => c.type === 'namespace_identifier' || c.type === 'type_identifier');
|
|
1022
|
+
if (scopeNode)
|
|
1023
|
+
className = scopeNode.text;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
const id = className ? `${filePath}::${className}.${name}` : `${filePath}::${name}`;
|
|
1028
|
+
nodes.push({
|
|
1029
|
+
id, name, filePath, className,
|
|
1030
|
+
isAsync: false, // C++ has no async keyword at language level
|
|
1031
|
+
language: 'C++',
|
|
1032
|
+
startIndex: fnNode.startIndex,
|
|
1033
|
+
endIndex: fnNode.endIndex,
|
|
1034
|
+
fanIn: 0, fanOut: 0,
|
|
1035
|
+
docstring: extractDocstringBefore(content, fnNode.startIndex, 'C++'),
|
|
1036
|
+
signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'C++'),
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
const rawEdges = [];
|
|
1041
|
+
// Plain calls: foo()
|
|
1042
|
+
for (const match of safeQuery(lang, CPP_CALL_DIRECT_QUERY, tree.rootNode)) {
|
|
1043
|
+
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
1044
|
+
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
1045
|
+
if (!nameCapture || !nodeCapture)
|
|
1046
|
+
continue;
|
|
1047
|
+
const calleeName = nameCapture.node.text;
|
|
1048
|
+
if (isIgnoredCallee(calleeName))
|
|
1049
|
+
continue;
|
|
1050
|
+
const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
|
|
1051
|
+
if (!caller)
|
|
1052
|
+
continue;
|
|
1053
|
+
rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1 });
|
|
1054
|
+
}
|
|
1055
|
+
// Member calls: obj.method() / ptr->method()
|
|
1056
|
+
for (const match of safeQuery(lang, CPP_CALL_MEMBER_QUERY, tree.rootNode)) {
|
|
1057
|
+
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
1058
|
+
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
1059
|
+
const objectCapture = match.captures.find(c => c.name === 'call.object');
|
|
1060
|
+
if (!nameCapture || !nodeCapture)
|
|
1061
|
+
continue;
|
|
1062
|
+
const calleeName = nameCapture.node.text;
|
|
1063
|
+
if (isIgnoredCallee(calleeName))
|
|
1064
|
+
continue;
|
|
1065
|
+
const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
|
|
1066
|
+
if (!caller)
|
|
1067
|
+
continue;
|
|
1068
|
+
rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1, calleeObject: objectCapture?.node.text });
|
|
1069
|
+
}
|
|
1070
|
+
return { nodes, rawEdges };
|
|
1071
|
+
}
|
|
1072
|
+
// ============================================================================
|
|
1073
|
+
// SWIFT EXTRACTOR
|
|
1074
|
+
// ============================================================================
|
|
1075
|
+
// function_declaration covers free functions and methods inside class_body
|
|
1076
|
+
const SWIFT_FN_QUERY = `
|
|
1077
|
+
(function_declaration
|
|
1078
|
+
name: (simple_identifier) @fn.name) @fn.node
|
|
1079
|
+
|
|
1080
|
+
(init_declaration) @fn.node
|
|
1081
|
+
`;
|
|
1082
|
+
// Direct calls: foo()
|
|
1083
|
+
const SWIFT_CALL_DIRECT_QUERY = `
|
|
1084
|
+
(call_expression
|
|
1085
|
+
(simple_identifier) @call.name) @call.node
|
|
1086
|
+
`;
|
|
1087
|
+
// Method calls: obj.method() / self.method()
|
|
1088
|
+
const SWIFT_CALL_NAV_QUERY = `
|
|
1089
|
+
(call_expression
|
|
1090
|
+
(navigation_expression
|
|
1091
|
+
(navigation_suffix
|
|
1092
|
+
(simple_identifier) @call.name))) @call.node
|
|
1093
|
+
`;
|
|
1094
|
+
async function extractSwiftGraph(filePath, content) {
|
|
1095
|
+
const { parser, lang } = await getSwiftParser();
|
|
1096
|
+
const tree = parser.parse(content);
|
|
1097
|
+
const fnQuery = new Parser.Query(lang, SWIFT_FN_QUERY);
|
|
1098
|
+
const directCallQuery = new Parser.Query(lang, SWIFT_CALL_DIRECT_QUERY);
|
|
1099
|
+
const navCallQuery = new Parser.Query(lang, SWIFT_CALL_NAV_QUERY);
|
|
1100
|
+
const nodes = [];
|
|
1101
|
+
for (const match of fnQuery.matches(tree.rootNode)) {
|
|
1102
|
+
const nameCapture = match.captures.find(c => c.name === 'fn.name');
|
|
1103
|
+
const nodeCapture = match.captures.find(c => c.name === 'fn.node');
|
|
1104
|
+
if (!nodeCapture)
|
|
1105
|
+
continue;
|
|
1106
|
+
const fnNode = nodeCapture.node;
|
|
1107
|
+
const name = nameCapture?.node.text ?? 'init';
|
|
1108
|
+
// Find enclosing class/struct/actor/enum/extension (all are class_declaration in this grammar)
|
|
1109
|
+
let className;
|
|
1110
|
+
let cursor = fnNode.parent;
|
|
1111
|
+
while (cursor) {
|
|
1112
|
+
if (cursor.type === 'class_declaration') {
|
|
1113
|
+
const nameNode = cursor.children.find(c => c.type === 'type_identifier');
|
|
1114
|
+
if (nameNode)
|
|
1115
|
+
className = nameNode.text;
|
|
1116
|
+
break;
|
|
1117
|
+
}
|
|
1118
|
+
cursor = cursor.parent;
|
|
1119
|
+
}
|
|
1120
|
+
const isAsync = content.slice(fnNode.startIndex, fnNode.endIndex).includes(' async ');
|
|
1121
|
+
const id = className ? `${filePath}::${className}.${name}` : `${filePath}::${name}`;
|
|
1122
|
+
nodes.push({
|
|
1123
|
+
id, name, filePath, className,
|
|
1124
|
+
isAsync,
|
|
1125
|
+
language: 'Swift',
|
|
1126
|
+
startIndex: fnNode.startIndex,
|
|
1127
|
+
endIndex: fnNode.endIndex,
|
|
1128
|
+
fanIn: 0, fanOut: 0,
|
|
1129
|
+
docstring: extractDocstringBefore(content, fnNode.startIndex, 'Swift'),
|
|
1130
|
+
signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'Swift'),
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
const rawEdges = [];
|
|
1134
|
+
// Direct calls: foo()
|
|
1135
|
+
for (const match of directCallQuery.matches(tree.rootNode)) {
|
|
1136
|
+
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
1137
|
+
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
1138
|
+
if (!nameCapture || !nodeCapture)
|
|
1139
|
+
continue;
|
|
1140
|
+
const calleeName = nameCapture.node.text;
|
|
1141
|
+
if (isIgnoredCallee(calleeName))
|
|
1142
|
+
continue;
|
|
1143
|
+
const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
|
|
1144
|
+
if (!caller)
|
|
1145
|
+
continue;
|
|
1146
|
+
rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1 });
|
|
1147
|
+
}
|
|
1148
|
+
// Method calls: obj.method() / self.method()
|
|
1149
|
+
for (const match of navCallQuery.matches(tree.rootNode)) {
|
|
1150
|
+
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
1151
|
+
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
1152
|
+
if (!nameCapture || !nodeCapture)
|
|
1153
|
+
continue;
|
|
1154
|
+
const calleeName = nameCapture.node.text;
|
|
1155
|
+
if (isIgnoredCallee(calleeName))
|
|
1156
|
+
continue;
|
|
1157
|
+
const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
|
|
1158
|
+
if (!caller)
|
|
1159
|
+
continue;
|
|
1160
|
+
// Extract the receiver object (first child of navigation_expression)
|
|
1161
|
+
const navExpr = nodeCapture.node.firstChild;
|
|
1162
|
+
const objText = navExpr?.firstChild?.type === 'self_expression'
|
|
1163
|
+
? 'self'
|
|
1164
|
+
: navExpr?.firstChild?.text;
|
|
1165
|
+
rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1, calleeObject: objText });
|
|
1166
|
+
}
|
|
1167
|
+
return { nodes, rawEdges };
|
|
1168
|
+
}
|
|
1169
|
+
// ============================================================================
|
|
1170
|
+
// CLASS HIERARCHY EXTRACTION
|
|
1171
|
+
// ============================================================================
|
|
1172
|
+
/**
|
|
1173
|
+
* Extract parent class / interface relationships from source files using
|
|
1174
|
+
* tree-sitter. Returns a map from `filePath::ClassName` → relationship info.
|
|
1175
|
+
* Uses safeQuery so any query that doesn't match a grammar version is silently
|
|
1176
|
+
* skipped rather than crashing.
|
|
1177
|
+
*/
|
|
1178
|
+
async function extractClassRelationships(files) {
|
|
1179
|
+
const out = new Map();
|
|
1180
|
+
// Helper to merge into map keyed by `filePath::ClassName`
|
|
1181
|
+
function merge(filePath, className, parents, ifaces) {
|
|
1182
|
+
const key = `${filePath}::${className}`;
|
|
1183
|
+
const existing = out.get(key) ?? { parentClasses: [], interfaces: [] };
|
|
1184
|
+
for (const p of parents)
|
|
1185
|
+
if (!existing.parentClasses.includes(p))
|
|
1186
|
+
existing.parentClasses.push(p);
|
|
1187
|
+
for (const i of ifaces)
|
|
1188
|
+
if (!existing.interfaces.includes(i))
|
|
1189
|
+
existing.interfaces.push(i);
|
|
1190
|
+
out.set(key, existing);
|
|
1191
|
+
}
|
|
1192
|
+
for (const file of files) {
|
|
1193
|
+
try {
|
|
1194
|
+
if (file.language === 'TypeScript' || file.language === 'JavaScript') {
|
|
1195
|
+
const { parser, lang } = await getTSParser();
|
|
1196
|
+
const tree = parser.parse(file.content);
|
|
1197
|
+
// class Foo extends Bar implements Baz, Qux
|
|
1198
|
+
const EXTENDS_Q = `
|
|
1199
|
+
(class_declaration
|
|
1200
|
+
name: (type_identifier) @cls
|
|
1201
|
+
(class_heritage (extends_clause value: (identifier) @parent)))`;
|
|
1202
|
+
const IMPLEMENTS_Q = `
|
|
1203
|
+
(class_declaration
|
|
1204
|
+
name: (type_identifier) @cls
|
|
1205
|
+
(class_heritage (implements_clause (type_identifier) @iface)))`;
|
|
1206
|
+
for (const m of safeQuery(lang, EXTENDS_Q, tree.rootNode)) {
|
|
1207
|
+
const cls = m.captures.find(c => c.name === 'cls')?.node.text;
|
|
1208
|
+
const parent = m.captures.find(c => c.name === 'parent')?.node.text;
|
|
1209
|
+
if (cls && parent)
|
|
1210
|
+
merge(file.path, cls, [parent], []);
|
|
1211
|
+
}
|
|
1212
|
+
for (const m of safeQuery(lang, IMPLEMENTS_Q, tree.rootNode)) {
|
|
1213
|
+
const cls = m.captures.find(c => c.name === 'cls')?.node.text;
|
|
1214
|
+
const iface = m.captures.find(c => c.name === 'iface')?.node.text;
|
|
1215
|
+
if (cls && iface)
|
|
1216
|
+
merge(file.path, cls, [], [iface]);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
else if (file.language === 'Python') {
|
|
1220
|
+
const { parser, lang } = await getPyParser();
|
|
1221
|
+
const tree = parser.parse(file.content);
|
|
1222
|
+
// class Foo(Bar, Baz):
|
|
1223
|
+
const Q = `
|
|
1224
|
+
(class_definition
|
|
1225
|
+
name: (identifier) @cls
|
|
1226
|
+
superclasses: (argument_list (identifier) @parent))`;
|
|
1227
|
+
for (const m of safeQuery(lang, Q, tree.rootNode)) {
|
|
1228
|
+
const cls = m.captures.find(c => c.name === 'cls')?.node.text;
|
|
1229
|
+
const parent = m.captures.find(c => c.name === 'parent')?.node.text;
|
|
1230
|
+
if (cls && parent && parent !== 'object')
|
|
1231
|
+
merge(file.path, cls, [parent], []);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
else if (file.language === 'Java') {
|
|
1235
|
+
const { parser, lang } = await getJavaParser();
|
|
1236
|
+
const tree = parser.parse(file.content);
|
|
1237
|
+
const EXTENDS_Q = `
|
|
1238
|
+
(class_declaration
|
|
1239
|
+
name: (identifier) @cls
|
|
1240
|
+
(superclass (type_identifier) @parent))`;
|
|
1241
|
+
const IMPLEMENTS_Q = `
|
|
1242
|
+
(class_declaration
|
|
1243
|
+
name: (identifier) @cls
|
|
1244
|
+
(super_interfaces (type_list (type_identifier) @iface)))`;
|
|
1245
|
+
for (const m of safeQuery(lang, EXTENDS_Q, tree.rootNode)) {
|
|
1246
|
+
const cls = m.captures.find(c => c.name === 'cls')?.node.text;
|
|
1247
|
+
const parent = m.captures.find(c => c.name === 'parent')?.node.text;
|
|
1248
|
+
if (cls && parent)
|
|
1249
|
+
merge(file.path, cls, [parent], []);
|
|
1250
|
+
}
|
|
1251
|
+
for (const m of safeQuery(lang, IMPLEMENTS_Q, tree.rootNode)) {
|
|
1252
|
+
const cls = m.captures.find(c => c.name === 'cls')?.node.text;
|
|
1253
|
+
const iface = m.captures.find(c => c.name === 'iface')?.node.text;
|
|
1254
|
+
if (cls && iface)
|
|
1255
|
+
merge(file.path, cls, [], [iface]);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
else if (file.language === 'C++') {
|
|
1259
|
+
const { parser, lang } = await getCppParser();
|
|
1260
|
+
const tree = parser.parse(file.content);
|
|
1261
|
+
// class Foo : public Bar
|
|
1262
|
+
const Q = `
|
|
1263
|
+
(class_specifier
|
|
1264
|
+
name: (type_identifier) @cls
|
|
1265
|
+
(base_class_clause (type_identifier) @parent))`;
|
|
1266
|
+
for (const m of safeQuery(lang, Q, tree.rootNode)) {
|
|
1267
|
+
const cls = m.captures.find(c => c.name === 'cls')?.node.text;
|
|
1268
|
+
const parent = m.captures.find(c => c.name === 'parent')?.node.text;
|
|
1269
|
+
if (cls && parent)
|
|
1270
|
+
merge(file.path, cls, [parent], []);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
else if (file.language === 'Ruby') {
|
|
1274
|
+
const { parser, lang } = await getRubyParser();
|
|
1275
|
+
const tree = parser.parse(file.content);
|
|
1276
|
+
// class Foo < Bar
|
|
1277
|
+
const Q = `
|
|
1278
|
+
(class
|
|
1279
|
+
name: (constant) @cls
|
|
1280
|
+
superclass: (superclass (constant) @parent))`;
|
|
1281
|
+
for (const m of safeQuery(lang, Q, tree.rootNode)) {
|
|
1282
|
+
const cls = m.captures.find(c => c.name === 'cls')?.node.text;
|
|
1283
|
+
const parent = m.captures.find(c => c.name === 'parent')?.node.text;
|
|
1284
|
+
if (cls && parent)
|
|
1285
|
+
merge(file.path, cls, [parent], []);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
else if (file.language === 'Go') {
|
|
1289
|
+
// Go has no inheritance but has struct embedding; treat as 'embeds' edges
|
|
1290
|
+
const { parser, lang } = await getGoParser();
|
|
1291
|
+
const tree = parser.parse(file.content);
|
|
1292
|
+
// Anonymous (embedded) field in a struct: type Foo struct { Bar }
|
|
1293
|
+
const Q = `
|
|
1294
|
+
(type_declaration
|
|
1295
|
+
(type_spec
|
|
1296
|
+
name: (type_identifier) @cls
|
|
1297
|
+
type: (struct_type
|
|
1298
|
+
(field_declaration_list
|
|
1299
|
+
(field_declaration
|
|
1300
|
+
type: (type_identifier) @embedded)))))`;
|
|
1301
|
+
for (const m of safeQuery(lang, Q, tree.rootNode)) {
|
|
1302
|
+
const cls = m.captures.find(c => c.name === 'cls')?.node.text;
|
|
1303
|
+
const embedded = m.captures.find(c => c.name === 'embedded')?.node.text;
|
|
1304
|
+
if (cls && embedded) {
|
|
1305
|
+
const key = `${file.path}::${cls}`;
|
|
1306
|
+
const existing = out.get(key) ?? { parentClasses: [], interfaces: [] };
|
|
1307
|
+
// Store Go embeds as parentClasses (will be tagged as 'embeds' when building edges)
|
|
1308
|
+
if (!existing.parentClasses.includes(embedded))
|
|
1309
|
+
existing.parentClasses.push(embedded);
|
|
1310
|
+
out.set(key, existing);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
// Rust: trait impls are structural but less like OOP inheritance; skip for now
|
|
1315
|
+
}
|
|
1316
|
+
catch {
|
|
1317
|
+
// Best-effort; skip unparseable files
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
return out;
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Build ClassNode[] from the set of extracted FunctionNodes (which carry
|
|
1324
|
+
* `className`), enriched with inheritance data from `extractClassRelationships`.
|
|
1325
|
+
*
|
|
1326
|
+
* Functions without a className are grouped by file into synthetic module nodes
|
|
1327
|
+
* (e.g. `[call-graph]`) so every function appears in the class graph, not just
|
|
1328
|
+
* class methods. This is essential for codebases that use mostly module-level
|
|
1329
|
+
* exports rather than OOP classes.
|
|
1330
|
+
*/
|
|
1331
|
+
function buildClassNodes(allNodes, relationships) {
|
|
1332
|
+
// Group FunctionNodes by (filePath, className).
|
|
1333
|
+
// Free functions use a synthetic "[basename]" module name keyed by filePath alone.
|
|
1334
|
+
const groups = new Map();
|
|
1335
|
+
for (const fn of allNodes.values()) {
|
|
1336
|
+
let key;
|
|
1337
|
+
let name;
|
|
1338
|
+
let isModule;
|
|
1339
|
+
if (fn.className) {
|
|
1340
|
+
key = `${fn.filePath}::${fn.className}`;
|
|
1341
|
+
name = fn.className;
|
|
1342
|
+
isModule = false;
|
|
1343
|
+
}
|
|
1344
|
+
else {
|
|
1345
|
+
// Synthetic module node — one per file
|
|
1346
|
+
key = fn.filePath;
|
|
1347
|
+
const base = fn.filePath.split('/').pop() ?? fn.filePath;
|
|
1348
|
+
name = '[' + base.replace(/\.[^.]+$/, '') + ']';
|
|
1349
|
+
isModule = true;
|
|
1350
|
+
}
|
|
1351
|
+
if (!groups.has(key)) {
|
|
1352
|
+
groups.set(key, { name, filePath: fn.filePath, language: fn.language, isModule, methods: [] });
|
|
1353
|
+
}
|
|
1354
|
+
groups.get(key).methods.push(fn);
|
|
1355
|
+
}
|
|
1356
|
+
// Build ClassNode[]
|
|
1357
|
+
const classMap = new Map();
|
|
1358
|
+
for (const [id, g] of groups) {
|
|
1359
|
+
const rel = relationships.get(id) ?? { parentClasses: [], interfaces: [] };
|
|
1360
|
+
const cls = {
|
|
1361
|
+
id,
|
|
1362
|
+
name: g.name,
|
|
1363
|
+
filePath: g.filePath,
|
|
1364
|
+
language: g.language,
|
|
1365
|
+
parentClasses: rel.parentClasses,
|
|
1366
|
+
interfaces: rel.interfaces,
|
|
1367
|
+
methodIds: g.methods.map(m => m.id),
|
|
1368
|
+
fanIn: g.methods.reduce((s, m) => s + m.fanIn, 0),
|
|
1369
|
+
fanOut: g.methods.reduce((s, m) => s + m.fanOut, 0),
|
|
1370
|
+
isModule: g.isModule,
|
|
1371
|
+
};
|
|
1372
|
+
classMap.set(id, cls);
|
|
1373
|
+
}
|
|
1374
|
+
// Build InheritanceEdge[] — only when both parent and child are in our graph
|
|
1375
|
+
// Parent lookup: match by class name across all ClassNodes (first match wins)
|
|
1376
|
+
const byName = new Map();
|
|
1377
|
+
for (const cls of classMap.values()) {
|
|
1378
|
+
if (!byName.has(cls.name))
|
|
1379
|
+
byName.set(cls.name, cls);
|
|
1380
|
+
}
|
|
1381
|
+
const inheritanceEdges = [];
|
|
1382
|
+
const seenEdges = new Set();
|
|
1383
|
+
for (const cls of classMap.values()) {
|
|
1384
|
+
for (const parentName of cls.parentClasses) {
|
|
1385
|
+
const parent = byName.get(parentName);
|
|
1386
|
+
if (!parent)
|
|
1387
|
+
continue;
|
|
1388
|
+
const edgeId = `${parent.id}->${cls.id}`;
|
|
1389
|
+
if (seenEdges.has(edgeId))
|
|
1390
|
+
continue;
|
|
1391
|
+
seenEdges.add(edgeId);
|
|
1392
|
+
// Go embedding vs OOP inheritance
|
|
1393
|
+
const kind = cls.language === 'Go' ? 'embeds' : 'extends';
|
|
1394
|
+
inheritanceEdges.push({ id: edgeId, parentId: parent.id, childId: cls.id, kind });
|
|
1395
|
+
}
|
|
1396
|
+
for (const ifaceName of cls.interfaces) {
|
|
1397
|
+
const parent = byName.get(ifaceName);
|
|
1398
|
+
if (!parent)
|
|
1399
|
+
continue;
|
|
1400
|
+
const edgeId = `${parent.id}->${cls.id}`;
|
|
1401
|
+
if (seenEdges.has(edgeId))
|
|
1402
|
+
continue;
|
|
1403
|
+
seenEdges.add(edgeId);
|
|
1404
|
+
inheritanceEdges.push({ id: edgeId, parentId: parent.id, childId: cls.id, kind: 'implements' });
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
// OVERRIDES edges: child defines method with same name as parent — language-agnostic
|
|
1408
|
+
const methodNameSet = new Map();
|
|
1409
|
+
for (const [id, cls] of classMap) {
|
|
1410
|
+
const names = new Set();
|
|
1411
|
+
for (const memberId of cls.methodIds) {
|
|
1412
|
+
const fn = allNodes.get(memberId);
|
|
1413
|
+
if (fn && !fn.isExternal)
|
|
1414
|
+
names.add(fn.name);
|
|
1415
|
+
}
|
|
1416
|
+
methodNameSet.set(id, names);
|
|
1417
|
+
}
|
|
1418
|
+
const extendsEdges = inheritanceEdges.filter(e => e.kind === 'extends');
|
|
1419
|
+
for (const edge of extendsEdges) {
|
|
1420
|
+
const childNames = methodNameSet.get(edge.childId);
|
|
1421
|
+
const parentNames = methodNameSet.get(edge.parentId);
|
|
1422
|
+
if (!childNames || !parentNames)
|
|
1423
|
+
continue;
|
|
1424
|
+
if (![...childNames].some(n => parentNames.has(n)))
|
|
1425
|
+
continue;
|
|
1426
|
+
const overrideId = `${edge.parentId}=>${edge.childId}:overrides`;
|
|
1427
|
+
if (seenEdges.has(overrideId))
|
|
1428
|
+
continue;
|
|
1429
|
+
seenEdges.add(overrideId);
|
|
1430
|
+
inheritanceEdges.push({ id: overrideId, parentId: edge.parentId, childId: edge.childId, kind: 'overrides' });
|
|
1431
|
+
}
|
|
1432
|
+
return { classes: Array.from(classMap.values()), inheritanceEdges };
|
|
1433
|
+
}
|
|
1434
|
+
// ============================================================================
|
|
1435
|
+
// EXTERNAL NODE HELPER
|
|
1436
|
+
// ============================================================================
|
|
1437
|
+
const TEST_FILE_PATTERNS = [
|
|
1438
|
+
/\.test\.[tj]sx?$/, /\.spec\.[tj]sx?$/,
|
|
1439
|
+
/_test\.py$/, /test_[^/]+\.py$/,
|
|
1440
|
+
/_spec\.rb$/, /_test\.go$/, /[A-Z][^/]*Test\.java$/,
|
|
1441
|
+
];
|
|
1442
|
+
function isTestFile(filePath) {
|
|
1443
|
+
return TEST_FILE_PATTERNS.some(p => p.test(filePath));
|
|
1444
|
+
}
|
|
1445
|
+
const EXTERNAL_HTTP_RE = /^(fetch|axios|got|superagent|node-fetch|ky|request|https?|xmlhttprequest|grpc|undici|requests|aiohttp|httpx|urllib|urllib2|urllib3|curl|curleasy|pycurl|http|httpclient|httpurlconnection|reqwest|hyper|ureq|isahc|surf|net|faraday|httparty|rest|typhoeus|excon|okhttp|retrofit|feign|resttemplate|webclient|urlsession|alamofire|moya)$/;
|
|
1446
|
+
const EXTERNAL_DB_RE = /^(pg|mysql|mysql2|sqlite|sqlite3|redis|ioredis|mongoose|mongo|mongodb|prisma|knex|sequelize|typeorm|drizzle|cassandra|dynamodb|firestore|supabase|neo4j|influxdb|clickhouse|kysely|psycopg2|psycopg|sqlalchemy|pymysql|asyncpg|motor|aiomysql|tortoise|sql|gorm|sqlx|pgx|bun|diesel|seaorm|rusqlite|activerecord|sequel|jdbc|hibernate|jpa|entitymanager|datasource|jdbctemplate|r2dbc|coredata|grdb|realm)$/;
|
|
1447
|
+
const EXTERNAL_FS_RE = /^(fs|fsp|readfile|writefile|readdir|stat|mkdir|unlink|rename|copyfile|createreadstream|createwritestream|open|fopen|fread|fwrite|fclose|remove|ifstream|ofstream|fstream|os|path|file)$/;
|
|
1448
|
+
const EXTERNAL_STDLIB_BASES = new Set([
|
|
1449
|
+
// JavaScript / Node.js
|
|
1450
|
+
'array', 'object', 'string', 'number', 'math', 'json', 'date', 'regexp',
|
|
1451
|
+
'promise', 'map', 'set', 'weakmap', 'weakset', 'symbol', 'reflect', 'proxy',
|
|
1452
|
+
'console', 'error', 'buffer', 'process', 'int8array', 'uint8array',
|
|
1453
|
+
// Python
|
|
1454
|
+
'os', 'sys', 're', 'io', 'abc', 'ast', 'csv', 'copy', 'enum', 'glob',
|
|
1455
|
+
'gzip', 'hmac', 'html', 'http', 'logging', 'operator', 'pathlib', 'pickle',
|
|
1456
|
+
'pprint', 'queue', 'random', 'shutil', 'signal', 'socket', 'ssl', 'struct',
|
|
1457
|
+
'subprocess', 'tempfile', 'threading', 'time', 'traceback', 'typing', 'uuid',
|
|
1458
|
+
'warnings', 'collections', 'functools', 'itertools', 'contextlib',
|
|
1459
|
+
'dataclasses', 'unittest', 'hashlib', 'base64', 'binascii', 'codecs',
|
|
1460
|
+
'inspect', 'importlib', 'weakref', 'gc', 'platform', 'shlex', 'textwrap',
|
|
1461
|
+
// C / C++
|
|
1462
|
+
'std', 'printf', 'fprintf', 'sprintf', 'snprintf', 'scanf', 'malloc',
|
|
1463
|
+
'calloc', 'realloc', 'free', 'memcpy', 'memmove', 'memset', 'memcmp',
|
|
1464
|
+
'strlen', 'strcpy', 'strncpy', 'strcat', 'strcmp', 'strncmp', 'strstr',
|
|
1465
|
+
'assert', 'abort', 'exit', 'atexit',
|
|
1466
|
+
// Go
|
|
1467
|
+
'fmt', 'log', 'sort', 'sync', 'atomic', 'bytes', 'errors', 'context',
|
|
1468
|
+
'reflect', 'runtime', 'bufio', 'unicode', 'strings', 'strconv', 'math',
|
|
1469
|
+
'rand', 'time', 'flag', 'testing',
|
|
1470
|
+
// Rust
|
|
1471
|
+
'vec', 'option', 'result', 'iter', 'collections', 'thread', 'env',
|
|
1472
|
+
'cell', 'rc', 'arc', 'mutex', 'rwlock', 'channel', 'mpsc',
|
|
1473
|
+
// Ruby
|
|
1474
|
+
'integer', 'float', 'numeric', 'enumerable', 'comparable', 'kernel',
|
|
1475
|
+
'module', 'class', 'basicobject', 'nilclass', 'trueclass', 'falseclass',
|
|
1476
|
+
'symbol', 'regexp', 'range', 'proc', 'method', 'encoding',
|
|
1477
|
+
// Java
|
|
1478
|
+
'system', 'integer', 'long', 'double', 'boolean', 'character',
|
|
1479
|
+
'list', 'arraylist', 'linkedlist', 'hashmap', 'treemap', 'hashset', 'treeset',
|
|
1480
|
+
'optional', 'stream', 'arrays', 'collections', 'objects', 'math',
|
|
1481
|
+
'thread', 'runnable', 'exception', 'runtimeexception', 'illegalargumentexception',
|
|
1482
|
+
'stringbuilder', 'stringbuffer', 'scanner',
|
|
1483
|
+
// Swift
|
|
1484
|
+
'int', 'double', 'bool', 'dictionary', 'swift', 'foundation',
|
|
1485
|
+
'dispatchqueue', 'notificationcenter', 'nsstring', 'nsarray', 'nsdictionary',
|
|
1486
|
+
]);
|
|
1487
|
+
const EXTERNAL_NOISE_RECEIVERS = new Set([
|
|
1488
|
+
'response', 'body', 't', 'err', 'error', 'buf', 'str', 'res', 'req', 'data', 'result',
|
|
1489
|
+
]);
|
|
1490
|
+
function classifyExternal(name) {
|
|
1491
|
+
const base = name.split('.')[0].toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
1492
|
+
if (EXTERNAL_HTTP_RE.test(base))
|
|
1493
|
+
return 'http';
|
|
1494
|
+
if (EXTERNAL_DB_RE.test(base))
|
|
1495
|
+
return 'database';
|
|
1496
|
+
if (EXTERNAL_FS_RE.test(base))
|
|
1497
|
+
return 'filesystem';
|
|
1498
|
+
if (EXTERNAL_STDLIB_BASES.has(base))
|
|
1499
|
+
return 'stdlib';
|
|
1500
|
+
if (name.includes('.') && EXTERNAL_NOISE_RECEIVERS.has(name.split('.')[0].toLowerCase()))
|
|
1501
|
+
return 'stdlib';
|
|
1502
|
+
return 'unknown';
|
|
1503
|
+
}
|
|
1504
|
+
function getOrCreateExternalNode(name, nodes) {
|
|
1505
|
+
const id = `external::${name}`;
|
|
1506
|
+
if (!nodes.has(id)) {
|
|
1507
|
+
nodes.set(id, {
|
|
1508
|
+
id, name, filePath: 'external', isExternal: true,
|
|
1509
|
+
externalKind: classifyExternal(name),
|
|
1510
|
+
isAsync: false, language: 'external',
|
|
1511
|
+
startIndex: 0, endIndex: 0, fanIn: 0, fanOut: 0,
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
return nodes.get(id);
|
|
1515
|
+
}
|
|
1516
|
+
// ============================================================================
|
|
1517
|
+
// CYCLOMATIC COMPLEXITY
|
|
1518
|
+
// ============================================================================
|
|
1519
|
+
const CC_PATTERN_PYTHON = /\bif\s|\belif\s|\bwhile\s|\bfor\s|\bexcept\b|\band\s|\bor\s/g;
|
|
1520
|
+
const CC_PATTERN_DEFAULT = /\bif\s*\(|\bwhile\s*\(|\bfor\s*[(]|\bdo\s*[{]|\bcase\s+|\bcatch\s*\(|&&|\|\|/g;
|
|
1521
|
+
/**
|
|
1522
|
+
* McCabe cyclomatic complexity via regex over function body.
|
|
1523
|
+
* CC = 1 + decision points (if, while, for, case, catch, &&, ||).
|
|
1524
|
+
* Approximate (regex, not AST), suitable for triage/ranking.
|
|
1525
|
+
*/
|
|
1526
|
+
export function computeCyclomaticComplexity(body, language) {
|
|
1527
|
+
const source = language === 'Python' ? CC_PATTERN_PYTHON.source : CC_PATTERN_DEFAULT.source;
|
|
1528
|
+
return 1 + (body.match(new RegExp(source, 'g'))?.length ?? 0);
|
|
1529
|
+
}
|
|
1530
|
+
// ============================================================================
|
|
1531
|
+
// CALL GRAPH BUILDER
|
|
1532
|
+
// ============================================================================
|
|
1533
|
+
export class CallGraphBuilder {
|
|
1534
|
+
/**
|
|
1535
|
+
* Build a call graph from a list of source files.
|
|
1536
|
+
*
|
|
1537
|
+
* @param files Source files with path, content, and language
|
|
1538
|
+
* @param layers Optional layer map { layerName: [path prefix, ...] }
|
|
1539
|
+
* @param importMap Optional per-file import map (from ImportResolverBridge)
|
|
1540
|
+
*/
|
|
1541
|
+
async build(files, layers, importMap) {
|
|
1542
|
+
const allNodes = new Map();
|
|
1543
|
+
const allRawEdges = [];
|
|
1544
|
+
// Pass 1: Extract nodes and raw edges from each file
|
|
1545
|
+
for (const file of files) {
|
|
1546
|
+
try {
|
|
1547
|
+
let result;
|
|
1548
|
+
if (file.language === 'Python') {
|
|
1549
|
+
result = await extractPyGraph(file.path, file.content);
|
|
1550
|
+
}
|
|
1551
|
+
else if (file.language === 'TypeScript' || file.language === 'JavaScript') {
|
|
1552
|
+
result = await extractTSGraph(file.path, file.content);
|
|
1553
|
+
}
|
|
1554
|
+
else if (file.language === 'Go') {
|
|
1555
|
+
result = await extractGoGraph(file.path, file.content);
|
|
1556
|
+
}
|
|
1557
|
+
else if (file.language === 'Rust') {
|
|
1558
|
+
result = await extractRustGraph(file.path, file.content);
|
|
1559
|
+
}
|
|
1560
|
+
else if (file.language === 'Ruby') {
|
|
1561
|
+
result = await extractRubyGraph(file.path, file.content);
|
|
1562
|
+
}
|
|
1563
|
+
else if (file.language === 'Java') {
|
|
1564
|
+
result = await extractJavaGraph(file.path, file.content);
|
|
1565
|
+
}
|
|
1566
|
+
else if (file.language === 'C++') {
|
|
1567
|
+
result = await extractCppGraph(file.path, file.content);
|
|
1568
|
+
}
|
|
1569
|
+
else if (file.language === 'Swift') {
|
|
1570
|
+
result = await extractSwiftGraph(file.path, file.content);
|
|
1571
|
+
}
|
|
1572
|
+
else {
|
|
1573
|
+
continue;
|
|
1574
|
+
}
|
|
1575
|
+
// Compute startLine (1-based) from byte offset — cheap, done once at build time
|
|
1576
|
+
const lineOffsets = [0];
|
|
1577
|
+
for (let i = 0; i < file.content.length; i++) {
|
|
1578
|
+
if (file.content[i] === '\n')
|
|
1579
|
+
lineOffsets.push(i + 1);
|
|
1580
|
+
}
|
|
1581
|
+
const byteToLine = (offset) => {
|
|
1582
|
+
let lo = 0, hi = lineOffsets.length - 1;
|
|
1583
|
+
while (lo < hi) {
|
|
1584
|
+
const mid = (lo + hi + 1) >> 1;
|
|
1585
|
+
if (lineOffsets[mid] <= offset)
|
|
1586
|
+
lo = mid;
|
|
1587
|
+
else
|
|
1588
|
+
hi = mid - 1;
|
|
1589
|
+
}
|
|
1590
|
+
return lo + 1;
|
|
1591
|
+
};
|
|
1592
|
+
for (const node of result.nodes) {
|
|
1593
|
+
node.startLine = byteToLine(node.startIndex);
|
|
1594
|
+
node.endLine = byteToLine(node.endIndex);
|
|
1595
|
+
allNodes.set(node.id, node);
|
|
1596
|
+
}
|
|
1597
|
+
allRawEdges.push(...result.rawEdges);
|
|
1598
|
+
}
|
|
1599
|
+
catch (error) {
|
|
1600
|
+
// Skip files that fail to parse (syntax errors, encoding issues, etc.)
|
|
1601
|
+
if (process.env.DEBUG) {
|
|
1602
|
+
console.debug(`[call-graph] Failed to parse ${file.path}: ${error.message}`);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
// Pass 2: Resolve raw edges — multi-strategy resolution
|
|
1607
|
+
const trie = new FunctionRegistryTrie();
|
|
1608
|
+
for (const node of allNodes.values())
|
|
1609
|
+
trie.insert(node);
|
|
1610
|
+
// Build per-function-body content slices for type inference (keyed by functionId)
|
|
1611
|
+
const fileContents = new Map();
|
|
1612
|
+
for (const file of files)
|
|
1613
|
+
fileContents.set(file.path, file.content);
|
|
1614
|
+
const edges = [];
|
|
1615
|
+
for (const raw of allRawEdges) {
|
|
1616
|
+
const callerNode = allNodes.get(raw.callerId);
|
|
1617
|
+
if (!callerNode)
|
|
1618
|
+
continue;
|
|
1619
|
+
let calleeNode;
|
|
1620
|
+
let confidence = 'name_only';
|
|
1621
|
+
// Strategy 1 — self/cls intra-class (Python self.*, cls.* or same-class method)
|
|
1622
|
+
if (raw.calleeObject === 'self' || raw.calleeObject === 'cls') {
|
|
1623
|
+
if (callerNode.className) {
|
|
1624
|
+
const candidates = trie.findByQualifiedName(callerNode.className, raw.calleeName);
|
|
1625
|
+
if (candidates.length > 0) {
|
|
1626
|
+
calleeNode = candidates[0];
|
|
1627
|
+
confidence = 'self_cls';
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
// Strategy 1b — Swift/C++ type-name resolution (capitalized receiver = type/class reference)
|
|
1632
|
+
// In Swift and C++, there are no intra-module imports, so cross-file calls appear as
|
|
1633
|
+
// TypeName.method() or TypeName::method(). A capitalized receiver with no same-file
|
|
1634
|
+
// class of that name is a reliable signal for a cross-file type reference.
|
|
1635
|
+
if (!calleeNode && raw.calleeObject && (callerNode.language === 'Swift' || callerNode.language === 'C++')) {
|
|
1636
|
+
const ch = raw.calleeObject.charCodeAt(0);
|
|
1637
|
+
const isCapitalized = ch >= 65 && ch <= 90; // A-Z
|
|
1638
|
+
if (isCapitalized) {
|
|
1639
|
+
const candidates = trie.findByQualifiedName(raw.calleeObject, raw.calleeName);
|
|
1640
|
+
if (candidates.length > 0) {
|
|
1641
|
+
calleeNode = candidates[0];
|
|
1642
|
+
confidence = 'type_name';
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
// Strategy 2 — type inference on receiver variable
|
|
1647
|
+
if (!calleeNode && raw.calleeObject) {
|
|
1648
|
+
const fileContent = fileContents.get(callerNode.filePath);
|
|
1649
|
+
if (fileContent) {
|
|
1650
|
+
const bodySlice = fileContent.slice(callerNode.startIndex, callerNode.endIndex);
|
|
1651
|
+
const inferredTypes = inferTypesFromSource(bodySlice, callerNode.language);
|
|
1652
|
+
const resolved = resolveViaTypeInference(raw.calleeObject, raw.calleeName, inferredTypes, trie);
|
|
1653
|
+
if (resolved) {
|
|
1654
|
+
calleeNode = resolved;
|
|
1655
|
+
confidence = 'type_inference';
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
// Strategy 3 — import resolution (TS/JS/Python/Go/Rust/Ruby/Java)
|
|
1660
|
+
if (!calleeNode && importMap) {
|
|
1661
|
+
const importedFile = importMap.get(callerNode.filePath)?.get(raw.calleeName)
|
|
1662
|
+
?? (raw.calleeObject ? importMap.get(callerNode.filePath)?.get(raw.calleeObject) : undefined);
|
|
1663
|
+
if (importedFile) {
|
|
1664
|
+
const candidates = trie.findBySimpleName(raw.calleeName).filter(n => n.filePath.startsWith(importedFile));
|
|
1665
|
+
if (candidates.length > 0) {
|
|
1666
|
+
calleeNode = candidates[0];
|
|
1667
|
+
confidence = 'import';
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
// Strategy 4 — same-file preference (only for calls without a typed receiver)
|
|
1672
|
+
// When a receiver is explicitly present but unresolvable (e.g. redis_client.get()),
|
|
1673
|
+
// skip name_only fallback to avoid false-positive edges.
|
|
1674
|
+
if (!calleeNode && !raw.calleeObject) {
|
|
1675
|
+
const candidates = trie.findBySimpleName(raw.calleeName);
|
|
1676
|
+
if (candidates.length === 0) {
|
|
1677
|
+
// Unresolved bare call — create a synthetic external leaf node
|
|
1678
|
+
calleeNode = getOrCreateExternalNode(raw.calleeName, allNodes);
|
|
1679
|
+
confidence = 'external';
|
|
1680
|
+
}
|
|
1681
|
+
else {
|
|
1682
|
+
const sameFile = candidates.find(c => c.filePath === callerNode.filePath);
|
|
1683
|
+
if (sameFile) {
|
|
1684
|
+
calleeNode = sameFile;
|
|
1685
|
+
confidence = 'same_file';
|
|
1686
|
+
}
|
|
1687
|
+
else {
|
|
1688
|
+
calleeNode = candidates[0];
|
|
1689
|
+
confidence = 'name_only';
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
if (!calleeNode) {
|
|
1694
|
+
// Unresolved receiver-based call (e.g. redis_client.get()) — synthetic external node
|
|
1695
|
+
const label = raw.calleeObject
|
|
1696
|
+
? `${raw.calleeObject}.${raw.calleeName}`
|
|
1697
|
+
: raw.calleeName;
|
|
1698
|
+
calleeNode = getOrCreateExternalNode(label, allNodes);
|
|
1699
|
+
confidence = 'external';
|
|
1700
|
+
}
|
|
1701
|
+
const callType = raw.callType
|
|
1702
|
+
?? (raw.calleeObject ? 'method' : 'direct');
|
|
1703
|
+
edges.push({
|
|
1704
|
+
callerId: raw.callerId,
|
|
1705
|
+
calleeId: calleeNode.id,
|
|
1706
|
+
calleeName: raw.calleeName,
|
|
1707
|
+
line: raw.line,
|
|
1708
|
+
confidence,
|
|
1709
|
+
kind: 'calls',
|
|
1710
|
+
callType,
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
// Pass 2b: HTTP cross-language edges (JS/TS caller → Python handler)
|
|
1714
|
+
try {
|
|
1715
|
+
const filePaths = files.map(f => f.path);
|
|
1716
|
+
const { edges: httpEdges } = await extractAllHttpEdges(filePaths);
|
|
1717
|
+
for (const he of httpEdges) {
|
|
1718
|
+
// Find callee: handler function by name in handlerFile
|
|
1719
|
+
const calleeNode = trie.findBySimpleName(he.route.handlerName)
|
|
1720
|
+
.find(n => n.filePath === he.handlerFile);
|
|
1721
|
+
if (!calleeNode)
|
|
1722
|
+
continue;
|
|
1723
|
+
// Find caller: any function in callerFile that encloses the HTTP call's line
|
|
1724
|
+
const callerContent = fileContents.get(he.callerFile);
|
|
1725
|
+
const callerNode = callerContent
|
|
1726
|
+
? (() => {
|
|
1727
|
+
let offset = 0;
|
|
1728
|
+
const lines = callerContent.split('\n');
|
|
1729
|
+
for (let i = 0; i < he.call.line - 1 && i < lines.length; i++) {
|
|
1730
|
+
offset += lines[i].length + 1;
|
|
1731
|
+
}
|
|
1732
|
+
const candidates = Array.from(allNodes.values())
|
|
1733
|
+
.filter(n => n.filePath === he.callerFile);
|
|
1734
|
+
return findEnclosingFunction(candidates, offset);
|
|
1735
|
+
})()
|
|
1736
|
+
: undefined;
|
|
1737
|
+
if (!callerNode)
|
|
1738
|
+
continue;
|
|
1739
|
+
edges.push({
|
|
1740
|
+
callerId: callerNode.id,
|
|
1741
|
+
calleeId: calleeNode.id,
|
|
1742
|
+
calleeName: he.route.handlerName,
|
|
1743
|
+
line: he.call.line,
|
|
1744
|
+
confidence: 'http_endpoint',
|
|
1745
|
+
kind: 'calls',
|
|
1746
|
+
callType: 'direct',
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
catch {
|
|
1751
|
+
// HTTP edge extraction is best-effort; don't fail the whole build
|
|
1752
|
+
}
|
|
1753
|
+
// Pass 3: Calculate fanIn / fanOut (count unique caller→callee pairs, not call sites)
|
|
1754
|
+
const seenPairs = new Set();
|
|
1755
|
+
for (const edge of edges) {
|
|
1756
|
+
const pairKey = `${edge.callerId}\0${edge.calleeId}`;
|
|
1757
|
+
if (seenPairs.has(pairKey))
|
|
1758
|
+
continue;
|
|
1759
|
+
seenPairs.add(pairKey);
|
|
1760
|
+
const caller = allNodes.get(edge.callerId);
|
|
1761
|
+
const callee = allNodes.get(edge.calleeId);
|
|
1762
|
+
if (caller)
|
|
1763
|
+
caller.fanOut++;
|
|
1764
|
+
if (callee)
|
|
1765
|
+
callee.fanIn++;
|
|
1766
|
+
}
|
|
1767
|
+
// Pass 4 (prep): Mark test-file nodes before tested_by derivation
|
|
1768
|
+
const nodes = Array.from(allNodes.values());
|
|
1769
|
+
for (const n of nodes) {
|
|
1770
|
+
if (!n.isExternal && isTestFile(n.filePath))
|
|
1771
|
+
n.isTest = true;
|
|
1772
|
+
}
|
|
1773
|
+
// Pass 3b: Derive tested_by edges — reverse edges from production fn ← test fn
|
|
1774
|
+
// Source 1: call edges where the caller is a test function
|
|
1775
|
+
const callsEdges = edges.filter(e => !e.kind || e.kind === 'calls');
|
|
1776
|
+
const testedByPairs = new Set(); // deduplicate across sources
|
|
1777
|
+
for (const edge of callsEdges) {
|
|
1778
|
+
const caller = allNodes.get(edge.callerId);
|
|
1779
|
+
if (!caller || !isTestFile(caller.filePath))
|
|
1780
|
+
continue;
|
|
1781
|
+
const callee = allNodes.get(edge.calleeId);
|
|
1782
|
+
// Only emit tested_by when the production fn is internal (not external, not a test helper)
|
|
1783
|
+
if (!callee || callee.isExternal || callee.isTest)
|
|
1784
|
+
continue;
|
|
1785
|
+
const pairKey = `${edge.calleeId}\0${caller.filePath}`;
|
|
1786
|
+
if (testedByPairs.has(pairKey))
|
|
1787
|
+
continue;
|
|
1788
|
+
testedByPairs.add(pairKey);
|
|
1789
|
+
edges.push({
|
|
1790
|
+
kind: 'tested_by',
|
|
1791
|
+
callerId: edge.calleeId,
|
|
1792
|
+
calleeId: edge.callerId,
|
|
1793
|
+
calleeName: caller.name,
|
|
1794
|
+
confidence: edge.confidence,
|
|
1795
|
+
callType: undefined,
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
// Source 2: import-based — every name imported by a test file from a production file.
|
|
1799
|
+
// Catches mocked functions that are imported but never directly called in the test.
|
|
1800
|
+
// Build a lightweight import map from file content (only test files, TS/JS).
|
|
1801
|
+
const allFilePaths = files.map(f => f.path);
|
|
1802
|
+
const NAMED_IMPORT_RE = /^\s*import\s+(?:type\s+)?\{([^}]+)\}\s+from\s+['"](\.[^'"]+)['"]/gm;
|
|
1803
|
+
const DEFAULT_IMPORT_RE = /^\s*import\s+(?:type\s+)?(\w+)\s+from\s+['"](\.[^'"]+)['"]/gm;
|
|
1804
|
+
for (const file of files) {
|
|
1805
|
+
if (!isTestFile(file.path))
|
|
1806
|
+
continue;
|
|
1807
|
+
if (file.language !== 'TypeScript' && file.language !== 'JavaScript')
|
|
1808
|
+
continue;
|
|
1809
|
+
const dir = dirname(file.path);
|
|
1810
|
+
const resolveSource = (rel) => {
|
|
1811
|
+
// Strip .js extension: TS ESM imports use './foo.js' to refer to './foo.ts'
|
|
1812
|
+
const stripped = rel.replace(/\.js$/, '');
|
|
1813
|
+
const base = resolvePath(dir, stripped);
|
|
1814
|
+
return allFilePaths.find(p => p === base || p === `${base}.ts` || p === `${base}.tsx` ||
|
|
1815
|
+
p === `${base}.js` || p === `${base}.jsx` || p === `${base}/index.ts`);
|
|
1816
|
+
};
|
|
1817
|
+
const testLabel = file.path.split('/').pop().replace(/\.[tj]sx?$/, '');
|
|
1818
|
+
const emitEdge = (name, sourceFile) => {
|
|
1819
|
+
const candidates = trie.findBySimpleName(name)
|
|
1820
|
+
.filter(n => n.filePath === sourceFile && !n.isTest && !n.isExternal);
|
|
1821
|
+
for (const callee of candidates) {
|
|
1822
|
+
const pairKey = `${callee.id}\0${file.path}`;
|
|
1823
|
+
if (testedByPairs.has(pairKey))
|
|
1824
|
+
continue;
|
|
1825
|
+
testedByPairs.add(pairKey);
|
|
1826
|
+
edges.push({
|
|
1827
|
+
kind: 'tested_by',
|
|
1828
|
+
callerId: callee.id,
|
|
1829
|
+
calleeId: `${file.path}::*`,
|
|
1830
|
+
calleeName: testLabel,
|
|
1831
|
+
confidence: 'import',
|
|
1832
|
+
callType: undefined,
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
};
|
|
1836
|
+
// Named imports: import { foo, bar as baz } from './module'
|
|
1837
|
+
for (const m of file.content.matchAll(NAMED_IMPORT_RE)) {
|
|
1838
|
+
const sourceFile = resolveSource(m[2]);
|
|
1839
|
+
if (!sourceFile)
|
|
1840
|
+
continue;
|
|
1841
|
+
for (const part of m[1].split(',')) {
|
|
1842
|
+
const name = (part.match(/\bas\s+(\w+)/) ?? part.match(/(\w+)/))?.[1]?.trim();
|
|
1843
|
+
if (name)
|
|
1844
|
+
emitEdge(name, sourceFile);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
// Default imports: import foo from './module'
|
|
1848
|
+
for (const m of file.content.matchAll(DEFAULT_IMPORT_RE)) {
|
|
1849
|
+
const sourceFile = resolveSource(m[2]);
|
|
1850
|
+
if (!sourceFile)
|
|
1851
|
+
continue;
|
|
1852
|
+
emitEdge(m[1], sourceFile);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
// Also apply caller-provided importMap if present (cross-language coverage)
|
|
1856
|
+
if (importMap) {
|
|
1857
|
+
for (const [testFilePath, imports] of importMap) {
|
|
1858
|
+
if (!isTestFile(testFilePath))
|
|
1859
|
+
continue;
|
|
1860
|
+
for (const [importedName, sourceFile] of imports) {
|
|
1861
|
+
const candidates = trie.findBySimpleName(importedName)
|
|
1862
|
+
.filter(n => n.filePath === sourceFile && !n.isTest && !n.isExternal);
|
|
1863
|
+
for (const callee of candidates) {
|
|
1864
|
+
const pairKey = `${callee.id}\0${testFilePath}`;
|
|
1865
|
+
if (testedByPairs.has(pairKey))
|
|
1866
|
+
continue;
|
|
1867
|
+
testedByPairs.add(pairKey);
|
|
1868
|
+
edges.push({
|
|
1869
|
+
kind: 'tested_by',
|
|
1870
|
+
callerId: callee.id,
|
|
1871
|
+
calleeId: `${testFilePath}::*`,
|
|
1872
|
+
calleeName: testFilePath.split('/').pop().replace(/\.[tj]sx?$/, ''),
|
|
1873
|
+
confidence: 'import',
|
|
1874
|
+
callType: undefined,
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
// Pass 4: Derive hub functions, entry points, layer violations
|
|
1881
|
+
// External and test nodes are excluded from structural stats
|
|
1882
|
+
const internalNodes = nodes.filter(n => !n.isExternal && !n.isTest);
|
|
1883
|
+
const hubFunctions = internalNodes
|
|
1884
|
+
.filter(n => n.fanIn >= HUB_THRESHOLD)
|
|
1885
|
+
.sort((a, b) => b.fanIn - a.fanIn);
|
|
1886
|
+
const calledIds = new Set(edges.map(e => e.calleeId));
|
|
1887
|
+
const entryPoints = internalNodes
|
|
1888
|
+
.filter(n => !calledIds.has(n.id))
|
|
1889
|
+
.sort((a, b) => b.fanOut - a.fanOut);
|
|
1890
|
+
const layerViolations = layers
|
|
1891
|
+
? this.detectLayerViolations(edges, allNodes, layers)
|
|
1892
|
+
: [];
|
|
1893
|
+
const totalFanIn = internalNodes.reduce((s, n) => s + n.fanIn, 0);
|
|
1894
|
+
const totalFanOut = internalNodes.reduce((s, n) => s + n.fanOut, 0);
|
|
1895
|
+
// Pass 5: Label-propagation community detection (internal non-test nodes only)
|
|
1896
|
+
// Each node starts with its own label; iteratively adopts the most common neighbor label.
|
|
1897
|
+
// Converges in ~10 passes for typical codebases. External/test nodes get no community.
|
|
1898
|
+
{
|
|
1899
|
+
const callsEdgesOnly = edges.filter(e => !e.kind || e.kind === 'calls');
|
|
1900
|
+
const label = new Map();
|
|
1901
|
+
for (const n of internalNodes)
|
|
1902
|
+
label.set(n.id, n.id);
|
|
1903
|
+
// Build adjacency for internal nodes (bidirectional — community ignores direction)
|
|
1904
|
+
const neighbors = new Map();
|
|
1905
|
+
for (const n of internalNodes)
|
|
1906
|
+
neighbors.set(n.id, []);
|
|
1907
|
+
for (const e of callsEdgesOnly) {
|
|
1908
|
+
if (label.has(e.callerId) && label.has(e.calleeId)) {
|
|
1909
|
+
neighbors.get(e.callerId).push(e.calleeId);
|
|
1910
|
+
neighbors.get(e.calleeId).push(e.callerId);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
for (let iter = 0; iter < 15; iter++) {
|
|
1914
|
+
let changed = false;
|
|
1915
|
+
// Deterministic order each iteration (sorted) avoids oscillation
|
|
1916
|
+
const order = [...internalNodes].sort((a, b) => a.id < b.id ? -1 : 1);
|
|
1917
|
+
for (const n of order) {
|
|
1918
|
+
const nbrs = neighbors.get(n.id);
|
|
1919
|
+
if (nbrs.length === 0)
|
|
1920
|
+
continue;
|
|
1921
|
+
const counts = new Map();
|
|
1922
|
+
for (const nbId of nbrs) {
|
|
1923
|
+
const l = label.get(nbId) ?? nbId;
|
|
1924
|
+
counts.set(l, (counts.get(l) ?? 0) + 1);
|
|
1925
|
+
}
|
|
1926
|
+
let best = label.get(n.id);
|
|
1927
|
+
let bestCnt = 0;
|
|
1928
|
+
for (const [l, c] of counts) {
|
|
1929
|
+
if (c > bestCnt || (c === bestCnt && l < best)) {
|
|
1930
|
+
best = l;
|
|
1931
|
+
bestCnt = c;
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
if (best !== label.get(n.id)) {
|
|
1935
|
+
label.set(n.id, best);
|
|
1936
|
+
changed = true;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
if (!changed)
|
|
1940
|
+
break;
|
|
1941
|
+
}
|
|
1942
|
+
// Name each community by its highest-fanIn member
|
|
1943
|
+
const communityMembers = new Map();
|
|
1944
|
+
for (const n of internalNodes) {
|
|
1945
|
+
const l = label.get(n.id);
|
|
1946
|
+
if (!communityMembers.has(l))
|
|
1947
|
+
communityMembers.set(l, []);
|
|
1948
|
+
communityMembers.get(l).push(n);
|
|
1949
|
+
}
|
|
1950
|
+
for (const members of communityMembers.values()) {
|
|
1951
|
+
const hub = members.slice().sort((a, b) => b.fanIn - a.fanIn)[0];
|
|
1952
|
+
const communityLabel = hub.name;
|
|
1953
|
+
for (const n of members) {
|
|
1954
|
+
n.communityId = label.get(n.id);
|
|
1955
|
+
n.communityLabel = communityLabel;
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
// Pass 6: Cyclomatic complexity — regex over body slice for each internal node
|
|
1960
|
+
for (const node of allNodes.values()) {
|
|
1961
|
+
if (node.isExternal || node.startIndex === undefined || node.endIndex === undefined)
|
|
1962
|
+
continue;
|
|
1963
|
+
const content = fileContents.get(node.filePath);
|
|
1964
|
+
if (!content)
|
|
1965
|
+
continue;
|
|
1966
|
+
node.cyclomaticComplexity = computeCyclomaticComplexity(content.slice(node.startIndex, node.endIndex), node.language);
|
|
1967
|
+
}
|
|
1968
|
+
// Pass 7: Build class hierarchy (inheritance + grouping)
|
|
1969
|
+
const relationships = await extractClassRelationships(files);
|
|
1970
|
+
const { classes, inheritanceEdges } = buildClassNodes(allNodes, relationships);
|
|
1971
|
+
return {
|
|
1972
|
+
nodes: allNodes,
|
|
1973
|
+
edges,
|
|
1974
|
+
classes,
|
|
1975
|
+
inheritanceEdges,
|
|
1976
|
+
hubFunctions,
|
|
1977
|
+
entryPoints,
|
|
1978
|
+
layerViolations,
|
|
1979
|
+
stats: {
|
|
1980
|
+
totalNodes: internalNodes.length,
|
|
1981
|
+
totalEdges: edges.filter(e => !e.kind || e.kind === 'calls').length,
|
|
1982
|
+
avgFanIn: internalNodes.length > 0 ? totalFanIn / internalNodes.length : 0,
|
|
1983
|
+
avgFanOut: internalNodes.length > 0 ? totalFanOut / internalNodes.length : 0,
|
|
1984
|
+
},
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
detectLayerViolations(edges, nodes, layers) {
|
|
1988
|
+
// Build ordered layer list (index 0 = top layer, higher index = lower layer)
|
|
1989
|
+
const layerOrder = Object.keys(layers);
|
|
1990
|
+
const getLayer = (filePath) => {
|
|
1991
|
+
for (const [layerName, prefixes] of Object.entries(layers)) {
|
|
1992
|
+
if (prefixes.some(p => filePath.includes(p)))
|
|
1993
|
+
return layerName;
|
|
1994
|
+
}
|
|
1995
|
+
return undefined;
|
|
1996
|
+
};
|
|
1997
|
+
const violations = [];
|
|
1998
|
+
for (const edge of edges) {
|
|
1999
|
+
const caller = nodes.get(edge.callerId);
|
|
2000
|
+
const callee = nodes.get(edge.calleeId);
|
|
2001
|
+
if (!caller || !callee)
|
|
2002
|
+
continue;
|
|
2003
|
+
const callerLayer = getLayer(caller.filePath);
|
|
2004
|
+
const calleeLayer = getLayer(callee.filePath);
|
|
2005
|
+
if (!callerLayer || !calleeLayer || callerLayer === calleeLayer)
|
|
2006
|
+
continue;
|
|
2007
|
+
const callerIdx = layerOrder.indexOf(callerLayer);
|
|
2008
|
+
const calleeIdx = layerOrder.indexOf(calleeLayer);
|
|
2009
|
+
if (callerIdx === -1 || calleeIdx === -1)
|
|
2010
|
+
continue;
|
|
2011
|
+
if (callerIdx > calleeIdx) {
|
|
2012
|
+
// Lower layer calling upper layer — violation
|
|
2013
|
+
violations.push({
|
|
2014
|
+
callerId: edge.callerId,
|
|
2015
|
+
calleeId: edge.calleeId,
|
|
2016
|
+
callerLayer,
|
|
2017
|
+
calleeLayer,
|
|
2018
|
+
reason: `${callerLayer} calls ${calleeLayer} (${caller.name} → ${callee.name})`,
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
return violations;
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
// ============================================================================
|
|
2026
|
+
// SERIALIZATION HELPER
|
|
2027
|
+
// ============================================================================
|
|
2028
|
+
export function serializeCallGraph(result) {
|
|
2029
|
+
return {
|
|
2030
|
+
nodes: Array.from(result.nodes.values()),
|
|
2031
|
+
edges: result.edges,
|
|
2032
|
+
classes: result.classes,
|
|
2033
|
+
inheritanceEdges: result.inheritanceEdges,
|
|
2034
|
+
hubFunctions: result.hubFunctions,
|
|
2035
|
+
entryPoints: result.entryPoints,
|
|
2036
|
+
layerViolations: result.layerViolations,
|
|
2037
|
+
stats: result.stats,
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
//# sourceMappingURL=call-graph.js.map
|