@unerr-ai/unerr 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/cli.js +37236 -35793
- package/package.json +6 -1
- package/dist/behaviors/agent-llm-bridge.js +0 -166
- package/dist/behaviors/architecture-guard.js +0 -256
- package/dist/behaviors/auto-doc.js +0 -247
- package/dist/behaviors/cascade-guard.js +0 -289
- package/dist/behaviors/change-narrative.js +0 -270
- package/dist/behaviors/convention-drift.js +0 -290
- package/dist/behaviors/framework.js +0 -235
- package/dist/behaviors/guard-formatter.js +0 -44
- package/dist/behaviors/incomplete-work.js +0 -270
- package/dist/behaviors/loop-breaker.js +0 -300
- package/dist/behaviors/session-continuity.js +0 -208
- package/dist/commands/branches.js +0 -97
- package/dist/commands/check-commit.js +0 -225
- package/dist/commands/compress-output.js +0 -64
- package/dist/commands/config-verify.js +0 -243
- package/dist/commands/daemon.js +0 -905
- package/dist/commands/dashboard.js +0 -52
- package/dist/commands/debug.js +0 -200
- package/dist/commands/enrich.js +0 -184
- package/dist/commands/exec.js +0 -233
- package/dist/commands/gain.js +0 -156
- package/dist/commands/hook.js +0 -88
- package/dist/commands/index.js +0 -88
- package/dist/commands/init.js +0 -74
- package/dist/commands/install.js +0 -505
- package/dist/commands/learn.js +0 -116
- package/dist/commands/manifest.js +0 -193
- package/dist/commands/rewind.js +0 -103
- package/dist/commands/serve.js +0 -19
- package/dist/commands/setup-wizard.js +0 -414
- package/dist/commands/skills.js +0 -64
- package/dist/commands/stats.js +0 -20
- package/dist/commands/status.js +0 -654
- package/dist/commands/timeline.js +0 -139
- package/dist/commands/uninstall.js +0 -230
- package/dist/components/App.js +0 -109
- package/dist/components/Banner.js +0 -12
- package/dist/components/ConfirmPrompt.js +0 -25
- package/dist/components/DriftSummary.js +0 -23
- package/dist/components/GradeBadge.js +0 -15
- package/dist/components/HealthCard.js +0 -18
- package/dist/components/InkSpinner.js +0 -22
- package/dist/components/InputBox.js +0 -17
- package/dist/components/KeyValue.js +0 -13
- package/dist/components/MessageList.js +0 -14
- package/dist/components/ProgressBar.js +0 -26
- package/dist/components/Section.js +0 -16
- package/dist/components/SessionSummaryCard.js +0 -73
- package/dist/components/StartupDisplay.js +0 -24
- package/dist/components/StatusDashboard.js +0 -57
- package/dist/components/StatusLine.js +0 -8
- package/dist/components/StepLine.js +0 -22
- package/dist/components/Theme.js +0 -20
- package/dist/components/ToolProgress.js +0 -8
- package/dist/components/ViolationList.js +0 -21
- package/dist/components/render.js +0 -13
- package/dist/config/agent-registry.js +0 -237
- package/dist/config/claude-settings-hooks.js +0 -304
- package/dist/config/hook-installer.js +0 -65
- package/dist/config/instruction-writer.js +0 -388
- package/dist/config/mcp-config-writer.js +0 -266
- package/dist/config/settings.js +0 -174
- package/dist/config/tool-detector.js +0 -42
- package/dist/config/value-surfacing.js +0 -119
- package/dist/core/context-assembly.js +0 -108
- package/dist/core/conversation.js +0 -33
- package/dist/core/local-chat-provider.js +0 -475
- package/dist/core/provider-factory.js +0 -55
- package/dist/core/providers.js +0 -90
- package/dist/core/query-engine.js +0 -174
- package/dist/daemon/api.js +0 -312
- package/dist/daemon/autostart.js +0 -119
- package/dist/daemon/bootstrap.js +0 -39
- package/dist/daemon/client.js +0 -164
- package/dist/daemon/detect-ci.js +0 -81
- package/dist/daemon/platform-linux.js +0 -146
- package/dist/daemon/platform-macos.js +0 -134
- package/dist/daemon/platform-windows.js +0 -116
- package/dist/daemon/process-manager.js +0 -299
- package/dist/daemon/protocol.js +0 -23
- package/dist/daemon/registry.js +0 -270
- package/dist/daemon/settings-schema.js +0 -72
- package/dist/daemon/system-health.js +0 -134
- package/dist/daemon/version-checker.js +0 -262
- package/dist/daemon/warm-start.js +0 -223
- package/dist/entrypoints/cli.js +0 -1043
- package/dist/entrypoints/daemon.js +0 -380
- package/dist/entrypoints/repl.js +0 -147
- package/dist/hooks/adapters/claude-code.js +0 -90
- package/dist/hooks/adapters/cline.js +0 -100
- package/dist/hooks/adapters/cursor.js +0 -98
- package/dist/hooks/hook-dedup.js +0 -79
- package/dist/hooks/hook-runner.js +0 -113
- package/dist/hooks/navigation-hooks.js +0 -175
- package/dist/hooks/prompt-hooks.js +0 -63
- package/dist/hooks/shell-hooks.js +0 -47
- package/dist/ignore.js +0 -111
- package/dist/intelligence/approach-suggester.js +0 -61
- package/dist/intelligence/ast-extractor.js +0 -2615
- package/dist/intelligence/ast-worker.js +0 -34
- package/dist/intelligence/background-indexer.js +0 -121
- package/dist/intelligence/blast-radius.js +0 -200
- package/dist/intelligence/community-detection.js +0 -691
- package/dist/intelligence/community-detector.js +0 -184
- package/dist/intelligence/computation-scheduler.js +0 -75
- package/dist/intelligence/confidence-propagation.js +0 -47
- package/dist/intelligence/convention-detector.js +0 -242
- package/dist/intelligence/convention-learner.js +0 -205
- package/dist/intelligence/convention-matcher.js +0 -205
- package/dist/intelligence/cozo-schema.js +0 -376
- package/dist/intelligence/decision-point-detector.js +0 -90
- package/dist/intelligence/deep-dive-tools.js +0 -586
- package/dist/intelligence/durability-scorer.js +0 -84
- package/dist/intelligence/exploration-cost.js +0 -204
- package/dist/intelligence/exploration-pattern-tracker.js +0 -61
- package/dist/intelligence/fact-generator.js +0 -322
- package/dist/intelligence/facts-schema.js +0 -90
- package/dist/intelligence/file-intelligence.js +0 -59
- package/dist/intelligence/graph-holder.js +0 -220
- package/dist/intelligence/graph-temporal-joiner.js +0 -238
- package/dist/intelligence/health-grade.js +0 -423
- package/dist/intelligence/health-grader.js +0 -200
- package/dist/intelligence/health-map-data.js +0 -259
- package/dist/intelligence/import-symbols.js +0 -136
- package/dist/intelligence/incremental-indexer.js +0 -658
- package/dist/intelligence/indexer/centrality.js +0 -62
- package/dist/intelligence/indexer/cfg-context.js +0 -95
- package/dist/intelligence/indexer/confidence.js +0 -34
- package/dist/intelligence/indexer/cross-file-resolver.js +0 -104
- package/dist/intelligence/indexer/edge-repair.js +0 -89
- package/dist/intelligence/indexer/entity-key.js +0 -17
- package/dist/intelligence/indexer/export-map.js +0 -132
- package/dist/intelligence/indexer/git-cochange.js +0 -128
- package/dist/intelligence/indexer/graph-patch.js +0 -147
- package/dist/intelligence/indexer/incremental.js +0 -78
- package/dist/intelligence/indexer/ingest.js +0 -160
- package/dist/intelligence/indexer/language-detect.js +0 -226
- package/dist/intelligence/indexer/metadata.js +0 -63
- package/dist/intelligence/indexer/mutation-tracker.js +0 -79
- package/dist/intelligence/indexer/orchestrator.js +0 -155
- package/dist/intelligence/indexer/plugin-interface.js +0 -31
- package/dist/intelligence/indexer/plugins/csharp.js +0 -440
- package/dist/intelligence/indexer/plugins/go.js +0 -335
- package/dist/intelligence/indexer/plugins/java.js +0 -370
- package/dist/intelligence/indexer/plugins/python.js +0 -358
- package/dist/intelligence/indexer/plugins/regex-fallback.js +0 -82
- package/dist/intelligence/indexer/plugins/ruby.js +0 -290
- package/dist/intelligence/indexer/plugins/rust.js +0 -484
- package/dist/intelligence/indexer/plugins/tier2-generic.js +0 -310
- package/dist/intelligence/indexer/plugins/typescript.js +0 -456
- package/dist/intelligence/indexer/resource-monitor.js +0 -93
- package/dist/intelligence/indexer/scip/decoder.js +0 -253
- package/dist/intelligence/indexer/scip/detector.js +0 -232
- package/dist/intelligence/indexer/scip/downloader.js +0 -427
- package/dist/intelligence/indexer/scip/fallback.js +0 -34
- package/dist/intelligence/indexer/scip/merger.js +0 -109
- package/dist/intelligence/indexer/scip/orchestrator.js +0 -433
- package/dist/intelligence/indexer/scip/runner.js +0 -98
- package/dist/intelligence/indexer/snapshot.js +0 -66
- package/dist/intelligence/indexer/test-detector.js +0 -196
- package/dist/intelligence/indexer/watch-integration.js +0 -61
- package/dist/intelligence/indexer/worker.js +0 -85
- package/dist/intelligence/local-convention-detector.js +0 -437
- package/dist/intelligence/local-embeddings.js +0 -190
- package/dist/intelligence/local-graph.js +0 -1946
- package/dist/intelligence/local-indexer.js +0 -1575
- package/dist/intelligence/local-llm.js +0 -163
- package/dist/intelligence/local-rule-generator.js +0 -154
- package/dist/intelligence/local-snapshot.js +0 -213
- package/dist/intelligence/negative-knowledge.js +0 -103
- package/dist/intelligence/persistent-db.js +0 -85
- package/dist/intelligence/query-router.js +0 -2556
- package/dist/intelligence/risk-classifier.js +0 -116
- package/dist/intelligence/rule-evaluator.js +0 -380
- package/dist/intelligence/rule-generator.js +0 -49
- package/dist/intelligence/search-index.js +0 -173
- package/dist/intelligence/semantic/docstring-extractor.js +0 -67
- package/dist/intelligence/semantic/embedding-store.js +0 -52
- package/dist/intelligence/semantic/enrichment-orchestrator.js +0 -48
- package/dist/intelligence/semantic/git-message-miner.js +0 -114
- package/dist/intelligence/semantic/identifier-tokenizer.js +0 -51
- package/dist/intelligence/semantic/node2vec-embeddings.js +0 -71
- package/dist/intelligence/semantic/node2vec-walks.js +0 -103
- package/dist/intelligence/semantic/path-domain-inference.js +0 -112
- package/dist/intelligence/semantic/similarity-engine.js +0 -60
- package/dist/intelligence/semantic/tfidf-vectors.js +0 -88
- package/dist/intelligence/session-brief-builder.js +0 -159
- package/dist/intelligence/session-context.js +0 -221
- package/dist/intelligence/session-health-monitor.js +0 -211
- package/dist/intelligence/session-narrative.js +0 -197
- package/dist/intelligence/session-pattern-analyzer.js +0 -218
- package/dist/intelligence/signal-scorer.js +0 -390
- package/dist/intelligence/signal-show-store.js +0 -182
- package/dist/intelligence/smart-truncate.js +0 -158
- package/dist/intelligence/subgraph-cache.js +0 -88
- package/dist/intelligence/temporal-facts.js +0 -494
- package/dist/intelligence/token-estimator.js +0 -100
- package/dist/intelligence/tool-injector.js +0 -87
- package/dist/intelligence/tree-sitter-loader.js +0 -71
- package/dist/intelligence/worker-pool.js +0 -116
- package/dist/proxy/arg-validator.js +0 -79
- package/dist/proxy/auto-bootstrap.js +0 -167
- package/dist/proxy/bridge.js +0 -147
- package/dist/proxy/budget-enforcer.js +0 -70
- package/dist/proxy/compression-quality-monitor.js +0 -160
- package/dist/proxy/compression-stats.js +0 -51
- package/dist/proxy/context-rot-detector.js +0 -137
- package/dist/proxy/drift-detector.js +0 -139
- package/dist/proxy/efficiency-tracker.js +0 -79
- package/dist/proxy/fact-ranking.js +0 -154
- package/dist/proxy/format-encoder.js +0 -266
- package/dist/proxy/http-transport.js +0 -90
- package/dist/proxy/lifecycle-actor.js +0 -55
- package/dist/proxy/lifecycle-machine.js +0 -187
- package/dist/proxy/log-tailer.js +0 -265
- package/dist/proxy/model-pricing.js +0 -98
- package/dist/proxy/network-firewall.js +0 -141
- package/dist/proxy/nudge-state.js +0 -93
- package/dist/proxy/output-compressor.js +0 -185
- package/dist/proxy/pid-lock.js +0 -291
- package/dist/proxy/proxy-context.js +0 -11
- package/dist/proxy/proxy.js +0 -2633
- package/dist/proxy/response-enrichment.js +0 -32
- package/dist/proxy/response-envelope.js +0 -313
- package/dist/proxy/session-dedup.js +0 -82
- package/dist/proxy/session-legend.js +0 -30
- package/dist/proxy/session-persistence.js +0 -210
- package/dist/proxy/session-resume.js +0 -94
- package/dist/proxy/session-stats.js +0 -513
- package/dist/proxy/shell-classifier.js +0 -1346
- package/dist/proxy/shell-compression-log.js +0 -93
- package/dist/proxy/shell-compressor.js +0 -390
- package/dist/proxy/shell-graph-boost.js +0 -202
- package/dist/proxy/shell-monitor-map.js +0 -18
- package/dist/proxy/shell-stats.js +0 -54
- package/dist/proxy/shell-strategies/cloud.js +0 -215
- package/dist/proxy/shell-strategies/diff.js +0 -159
- package/dist/proxy/shell-strategies/error-diagnostic.js +0 -796
- package/dist/proxy/shell-strategies/filter-dsl.js +0 -358
- package/dist/proxy/shell-strategies/git-status.js +0 -177
- package/dist/proxy/shell-strategies/key-value.js +0 -193
- package/dist/proxy/shell-strategies/log-text.js +0 -154
- package/dist/proxy/shell-strategies/omni.js +0 -188
- package/dist/proxy/shell-strategies/progress.js +0 -55
- package/dist/proxy/shell-strategies/redact.js +0 -76
- package/dist/proxy/shell-strategies/structured.js +0 -241
- package/dist/proxy/shell-strategies/tabular.js +0 -243
- package/dist/proxy/shell-strategies/test-results-types.js +0 -13
- package/dist/proxy/shell-strategies/test-results.js +0 -784
- package/dist/proxy/shell-strategies/tree-paths.js +0 -144
- package/dist/proxy/shell-strategies/yaml.js +0 -182
- package/dist/proxy/shell-tee.js +0 -111
- package/dist/proxy/signal-dedup.js +0 -171
- package/dist/proxy/startup-renderer.js +0 -158
- package/dist/proxy/task-token-display.js +0 -38
- package/dist/proxy/token-counter.js +0 -61
- package/dist/proxy/tool-clusters.js +0 -273
- package/dist/proxy/tool-definitions.js +0 -525
- package/dist/proxy/transport-mux.js +0 -229
- package/dist/proxy/wire-cap.js +0 -268
- package/dist/rules/developer.mozilla.org.json +0 -9
- package/dist/rules/github.com.json +0 -21
- package/dist/schemas/api/skills.js +0 -19
- package/dist/schemas/common/errors.js +0 -7
- package/dist/schemas/common/headers.js +0 -5
- package/dist/schemas/entities/edge.js +0 -25
- package/dist/schemas/entities/entity.js +0 -22
- package/dist/schemas/entities/rule.js +0 -18
- package/dist/schemas/index.js +0 -14
- package/dist/server/event-bus.js +0 -59
- package/dist/server/http.js +0 -156
- package/dist/server/middleware.js +0 -70
- package/dist/server/routes/drift.js +0 -97
- package/dist/server/routes/intelligence.js +0 -1217
- package/dist/server/routes/reasoning-quality.js +0 -444
- package/dist/server/routes/session.js +0 -86
- package/dist/server/routes/stream.js +0 -120
- package/dist/server/routes/system.js +0 -73
- package/dist/server/routes/temporal.js +0 -170
- package/dist/server/routes/timeline.js +0 -232
- package/dist/server/routes/token-flow.js +0 -403
- package/dist/skills/effectiveness-tracker.js +0 -93
- package/dist/skills/local-pack.js +0 -380
- package/dist/skills/resolver.js +0 -495
- package/dist/state-detector.js +0 -83
- package/dist/timeline/intent-detector.js +0 -263
- package/dist/timeline/loop-miner.js +0 -140
- package/dist/timeline/open-threads.js +0 -49
- package/dist/timeline/signal-reinforcer.js +0 -62
- package/dist/timeline/timeline-bootstrap.js +0 -151
- package/dist/timeline/timeline-store.js +0 -618
- package/dist/tools/coding/bash.js +0 -49
- package/dist/tools/coding/file-edit.js +0 -72
- package/dist/tools/coding/file-outline.js +0 -227
- package/dist/tools/coding/file-read-protocol.js +0 -425
- package/dist/tools/coding/file-read.js +0 -35
- package/dist/tools/coding/file-write.js +0 -43
- package/dist/tools/coding/glob-tool.js +0 -109
- package/dist/tools/coding/grep.js +0 -162
- package/dist/tools/coding/index.js +0 -27
- package/dist/tools/intelligence/index.js +0 -269
- package/dist/tools/intelligence/record-fact.js +0 -48
- package/dist/tools/intelligence/timeline-markers.js +0 -130
- package/dist/tools/registry.js +0 -47
- package/dist/tools/types.js +0 -8
- package/dist/tracking/auto-snapshot-triggers.js +0 -246
- package/dist/tracking/branch-context.js +0 -115
- package/dist/tracking/branch-snapshot.js +0 -217
- package/dist/tracking/causal-bridge.js +0 -317
- package/dist/tracking/circuit-breaker.js +0 -147
- package/dist/tracking/commit-watcher.js +0 -114
- package/dist/tracking/context-ledger.js +0 -119
- package/dist/tracking/correction-detector.js +0 -324
- package/dist/tracking/drift-tracker.js +0 -874
- package/dist/tracking/durability-tracker.js +0 -94
- package/dist/tracking/entity-rewind.js +0 -200
- package/dist/tracking/file-hash-state.js +0 -114
- package/dist/tracking/git-attribution.js +0 -132
- package/dist/tracking/git-trailers.js +0 -171
- package/dist/tracking/intelligence-counter.js +0 -46
- package/dist/tracking/intent-correlator.js +0 -202
- package/dist/tracking/intent-encoder.js +0 -52
- package/dist/tracking/intent-token-tracker.js +0 -159
- package/dist/tracking/ledger-archiver.js +0 -94
- package/dist/tracking/ledger-chains.js +0 -245
- package/dist/tracking/metrics-store.js +0 -361
- package/dist/tracking/native-watcher.js +0 -131
- package/dist/tracking/offline-rewind.js +0 -295
- package/dist/tracking/pending-violations.js +0 -74
- package/dist/tracking/persistence-effectiveness.js +0 -167
- package/dist/tracking/prompt-durability.js +0 -202
- package/dist/tracking/quality-signals.js +0 -213
- package/dist/tracking/redactor.js +0 -73
- package/dist/tracking/rewind-engine.js +0 -161
- package/dist/tracking/session-history.js +0 -128
- package/dist/tracking/session-receipt.js +0 -88
- package/dist/tracking/session-summary-writer.js +0 -157
- package/dist/tracking/shadow-ledger.js +0 -321
- package/dist/tracking/stash-manager.js +0 -258
- package/dist/tracking/timeline-fork.js +0 -213
- package/dist/tracking/timeline.js +0 -69
- package/dist/tracking/token-flow.js +0 -276
- package/dist/tracking/turn-segmenter.js +0 -122
- package/dist/tracking/weekly-accumulator.js +0 -179
- package/dist/tracking/working-snapshots.js +0 -188
- package/dist/tracking/workspace-manifest.js +0 -176
- package/dist/transport/http.js +0 -102
- package/dist/utils/counterfactual.js +0 -65
- package/dist/utils/deep-link.js +0 -34
- package/dist/utils/detect.js +0 -193
- package/dist/utils/exec.js +0 -73
- package/dist/utils/file-logger.js +0 -87
- package/dist/utils/format-error.js +0 -29
- package/dist/utils/git.js +0 -181
- package/dist/utils/log.js +0 -57
- package/dist/utils/logger.js +0 -35
- package/dist/utils/mcp-content-json.js +0 -8
- package/dist/utils/session-logger.js +0 -154
- package/dist/utils/startup-log.js +0 -512
- package/dist/utils/ui.js +0 -56
|
@@ -1,2615 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lightweight AST entity extractor — regex-based, zero-dependency.
|
|
3
|
-
*
|
|
4
|
-
* Extracts function, class, method, and interface declarations from source files
|
|
5
|
-
* using language-specific regex patterns. Designed for drift detection where we
|
|
6
|
-
* need entity boundaries (name + line range) without full semantic analysis.
|
|
7
|
-
*
|
|
8
|
-
* The SCIP indexer handles the heavy lifting for the base graph.
|
|
9
|
-
* This extractor only needs to identify "what entities exist in this file" for
|
|
10
|
-
* overlay diffing against the CozoDB base entities.
|
|
11
|
-
*
|
|
12
|
-
* Supported: TypeScript/JavaScript, Python, Go, Java, Rust, C/C++
|
|
13
|
-
*/
|
|
14
|
-
import { createHash } from "node:crypto";
|
|
15
|
-
import { existsSync } from "node:fs";
|
|
16
|
-
import { join } from "node:path";
|
|
17
|
-
import { fileURLToPath } from "node:url";
|
|
18
|
-
const EXTENSION_MAP = {
|
|
19
|
-
".ts": "typescript",
|
|
20
|
-
".tsx": "typescript",
|
|
21
|
-
".js": "javascript",
|
|
22
|
-
".jsx": "javascript",
|
|
23
|
-
".mjs": "javascript",
|
|
24
|
-
".cjs": "javascript",
|
|
25
|
-
".py": "python",
|
|
26
|
-
".go": "go",
|
|
27
|
-
".java": "java",
|
|
28
|
-
".rs": "rust",
|
|
29
|
-
".c": "c",
|
|
30
|
-
".h": "c",
|
|
31
|
-
".cpp": "cpp",
|
|
32
|
-
".cc": "cpp",
|
|
33
|
-
".cxx": "cpp",
|
|
34
|
-
".hpp": "cpp",
|
|
35
|
-
".cs": "csharp",
|
|
36
|
-
".rb": "ruby",
|
|
37
|
-
".rake": "ruby",
|
|
38
|
-
".php": "php",
|
|
39
|
-
".kt": "kotlin",
|
|
40
|
-
".kts": "kotlin",
|
|
41
|
-
".swift": "swift",
|
|
42
|
-
};
|
|
43
|
-
/**
|
|
44
|
-
* Detect language from file extension. Returns null for unsupported languages.
|
|
45
|
-
*/
|
|
46
|
-
export function detectLanguage(filePath) {
|
|
47
|
-
const dot = filePath.lastIndexOf(".");
|
|
48
|
-
if (dot < 0)
|
|
49
|
-
return null;
|
|
50
|
-
const ext = filePath.slice(dot).toLowerCase();
|
|
51
|
-
return EXTENSION_MAP[ext] ?? null;
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Extract entities from source code using regex patterns.
|
|
55
|
-
* Returns empty array for unsupported languages (never throws).
|
|
56
|
-
*/
|
|
57
|
-
export function extractEntities(content, filePath) {
|
|
58
|
-
const language = detectLanguage(filePath);
|
|
59
|
-
if (!language)
|
|
60
|
-
return [];
|
|
61
|
-
const lines = content.split("\n");
|
|
62
|
-
const entities = [];
|
|
63
|
-
const patterns = getPatterns(language);
|
|
64
|
-
let currentClassName = null;
|
|
65
|
-
for (let i = 0; i < lines.length; i++) {
|
|
66
|
-
const line = lines[i] ?? "";
|
|
67
|
-
for (const pattern of patterns) {
|
|
68
|
-
const match = pattern.regex.exec(line);
|
|
69
|
-
if (match) {
|
|
70
|
-
const rawName = match[pattern.nameGroup] ?? "";
|
|
71
|
-
if (!rawName || rawName.length === 0)
|
|
72
|
-
continue;
|
|
73
|
-
// Track current class context for method naming
|
|
74
|
-
if (pattern.kind === "class") {
|
|
75
|
-
currentClassName = rawName;
|
|
76
|
-
}
|
|
77
|
-
// Prefix method names with parent class (matches tree-sitter behavior)
|
|
78
|
-
const name = pattern.kind === "method" && currentClassName
|
|
79
|
-
? `${currentClassName}.${rawName}`
|
|
80
|
-
: rawName;
|
|
81
|
-
const signature = match[pattern.sigGroup ?? 0] ?? "";
|
|
82
|
-
const lineStart = i + 1; // 1-based
|
|
83
|
-
const lineEnd = findBlockEnd(lines, i, language);
|
|
84
|
-
const bodyLines = lines.slice(i, lineEnd);
|
|
85
|
-
const body = bodyLines.join("\n");
|
|
86
|
-
const contentHash = createHash("sha256")
|
|
87
|
-
.update(body)
|
|
88
|
-
.digest("hex")
|
|
89
|
-
.slice(0, 16);
|
|
90
|
-
entities.push({
|
|
91
|
-
name,
|
|
92
|
-
kind: pattern.kind,
|
|
93
|
-
signature: signature.trim(),
|
|
94
|
-
line_start: lineStart,
|
|
95
|
-
line_end: lineEnd,
|
|
96
|
-
content_hash: contentHash,
|
|
97
|
-
parent_class: pattern.kind === "method"
|
|
98
|
-
? (currentClassName ?? undefined)
|
|
99
|
-
: undefined,
|
|
100
|
-
});
|
|
101
|
-
break; // Only match first pattern per line
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return entities;
|
|
106
|
-
}
|
|
107
|
-
function getPatterns(language) {
|
|
108
|
-
switch (language) {
|
|
109
|
-
case "typescript":
|
|
110
|
-
case "javascript":
|
|
111
|
-
return [
|
|
112
|
-
// export function name(params) / async function name(params)
|
|
113
|
-
{
|
|
114
|
-
regex: /^(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*(\([^)]*\))/,
|
|
115
|
-
kind: "function",
|
|
116
|
-
nameGroup: 1,
|
|
117
|
-
sigGroup: 2,
|
|
118
|
-
},
|
|
119
|
-
// export class Name / class Name
|
|
120
|
-
{
|
|
121
|
-
regex: /^(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/,
|
|
122
|
-
kind: "class",
|
|
123
|
-
nameGroup: 1,
|
|
124
|
-
},
|
|
125
|
-
// export interface Name
|
|
126
|
-
{
|
|
127
|
-
regex: /^(?:export\s+)?interface\s+(\w+)/,
|
|
128
|
-
kind: "interface",
|
|
129
|
-
nameGroup: 1,
|
|
130
|
-
},
|
|
131
|
-
// method(params) { — inside class (with optional access modifiers)
|
|
132
|
-
{
|
|
133
|
-
regex: /^\s+(?:(?:private|protected|public|override|abstract|readonly)\s+)*(?:async\s+)?(?:static\s+)?(?:get\s+|set\s+)?(\w+)\s*(\([^)]*\))\s*(?::\s*\S+\s*)?[{]/,
|
|
134
|
-
kind: "method",
|
|
135
|
-
nameGroup: 1,
|
|
136
|
-
sigGroup: 2,
|
|
137
|
-
},
|
|
138
|
-
// const name = function / const name = (params) =>
|
|
139
|
-
{
|
|
140
|
-
regex: /^(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:function|\([^)]*\)\s*=>)/,
|
|
141
|
-
kind: "function",
|
|
142
|
-
nameGroup: 1,
|
|
143
|
-
},
|
|
144
|
-
];
|
|
145
|
-
case "python":
|
|
146
|
-
return [
|
|
147
|
-
// def name(params):
|
|
148
|
-
{
|
|
149
|
-
regex: /^(?:\s*)(?:async\s+)?def\s+(\w+)\s*(\([^)]*\))/,
|
|
150
|
-
kind: "function",
|
|
151
|
-
nameGroup: 1,
|
|
152
|
-
sigGroup: 2,
|
|
153
|
-
},
|
|
154
|
-
// class Name:
|
|
155
|
-
{ regex: /^class\s+(\w+)/, kind: "class", nameGroup: 1 },
|
|
156
|
-
];
|
|
157
|
-
case "go":
|
|
158
|
-
return [
|
|
159
|
-
// func name(params)
|
|
160
|
-
{
|
|
161
|
-
regex: /^func\s+(\w+)\s*(\([^)]*\))/,
|
|
162
|
-
kind: "function",
|
|
163
|
-
nameGroup: 1,
|
|
164
|
-
sigGroup: 2,
|
|
165
|
-
},
|
|
166
|
-
// func (r *Type) Name(params)
|
|
167
|
-
{
|
|
168
|
-
regex: /^func\s+\([^)]+\)\s+(\w+)\s*(\([^)]*\))/,
|
|
169
|
-
kind: "method",
|
|
170
|
-
nameGroup: 1,
|
|
171
|
-
sigGroup: 2,
|
|
172
|
-
},
|
|
173
|
-
// type Name struct
|
|
174
|
-
{ regex: /^type\s+(\w+)\s+struct/, kind: "class", nameGroup: 1 },
|
|
175
|
-
// type Name interface
|
|
176
|
-
{ regex: /^type\s+(\w+)\s+interface/, kind: "interface", nameGroup: 1 },
|
|
177
|
-
];
|
|
178
|
-
case "java":
|
|
179
|
-
return [
|
|
180
|
-
// public class Name
|
|
181
|
-
{
|
|
182
|
-
regex: /^\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:abstract\s+)?class\s+(\w+)/,
|
|
183
|
-
kind: "class",
|
|
184
|
-
nameGroup: 1,
|
|
185
|
-
},
|
|
186
|
-
// public interface Name
|
|
187
|
-
{
|
|
188
|
-
regex: /^\s*(?:public\s+|private\s+|protected\s+)?interface\s+(\w+)/,
|
|
189
|
-
kind: "interface",
|
|
190
|
-
nameGroup: 1,
|
|
191
|
-
},
|
|
192
|
-
// public void name(params)
|
|
193
|
-
{
|
|
194
|
-
regex: /^\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:\w+(?:<[^>]+>)?)\s+(\w+)\s*(\([^)]*\))/,
|
|
195
|
-
kind: "method",
|
|
196
|
-
nameGroup: 1,
|
|
197
|
-
sigGroup: 2,
|
|
198
|
-
},
|
|
199
|
-
];
|
|
200
|
-
case "rust":
|
|
201
|
-
return [
|
|
202
|
-
// fn name(params)
|
|
203
|
-
{
|
|
204
|
-
regex: /^\s*(?:pub\s+)?(?:async\s+)?fn\s+(\w+)\s*(\([^)]*\))/,
|
|
205
|
-
kind: "function",
|
|
206
|
-
nameGroup: 1,
|
|
207
|
-
sigGroup: 2,
|
|
208
|
-
},
|
|
209
|
-
// struct Name
|
|
210
|
-
{ regex: /^\s*(?:pub\s+)?struct\s+(\w+)/, kind: "class", nameGroup: 1 },
|
|
211
|
-
// trait Name
|
|
212
|
-
{
|
|
213
|
-
regex: /^\s*(?:pub\s+)?trait\s+(\w+)/,
|
|
214
|
-
kind: "interface",
|
|
215
|
-
nameGroup: 1,
|
|
216
|
-
},
|
|
217
|
-
// impl Name
|
|
218
|
-
{ regex: /^\s*impl(?:<[^>]+>)?\s+(\w+)/, kind: "class", nameGroup: 1 },
|
|
219
|
-
];
|
|
220
|
-
case "c":
|
|
221
|
-
case "cpp":
|
|
222
|
-
return [
|
|
223
|
-
// returnType name(params) {
|
|
224
|
-
{
|
|
225
|
-
regex: /^(?:\w+(?:\s*\*)?)\s+(\w+)\s*(\([^)]*\))\s*\{?$/,
|
|
226
|
-
kind: "function",
|
|
227
|
-
nameGroup: 1,
|
|
228
|
-
sigGroup: 2,
|
|
229
|
-
},
|
|
230
|
-
// class Name
|
|
231
|
-
{ regex: /^\s*class\s+(\w+)/, kind: "class", nameGroup: 1 },
|
|
232
|
-
// struct Name
|
|
233
|
-
{
|
|
234
|
-
regex: /^\s*(?:typedef\s+)?struct\s+(\w+)/,
|
|
235
|
-
kind: "class",
|
|
236
|
-
nameGroup: 1,
|
|
237
|
-
},
|
|
238
|
-
];
|
|
239
|
-
case "csharp":
|
|
240
|
-
return [
|
|
241
|
-
// class Name / public class Name
|
|
242
|
-
{
|
|
243
|
-
regex: /^\s*(?:public\s+|private\s+|protected\s+|internal\s+)?(?:static\s+)?(?:abstract\s+|sealed\s+)?(?:partial\s+)?class\s+(\w+)/,
|
|
244
|
-
kind: "class",
|
|
245
|
-
nameGroup: 1,
|
|
246
|
-
},
|
|
247
|
-
// interface Name
|
|
248
|
-
{
|
|
249
|
-
regex: /^\s*(?:public\s+|private\s+|protected\s+|internal\s+)?(?:partial\s+)?interface\s+(\w+)/,
|
|
250
|
-
kind: "interface",
|
|
251
|
-
nameGroup: 1,
|
|
252
|
-
},
|
|
253
|
-
// struct Name
|
|
254
|
-
{
|
|
255
|
-
regex: /^\s*(?:public\s+|private\s+|protected\s+|internal\s+)?(?:readonly\s+)?(?:partial\s+)?struct\s+(\w+)/,
|
|
256
|
-
kind: "class",
|
|
257
|
-
nameGroup: 1,
|
|
258
|
-
},
|
|
259
|
-
// method: returnType Name(params)
|
|
260
|
-
{
|
|
261
|
-
regex: /^\s+(?:public\s+|private\s+|protected\s+|internal\s+)?(?:static\s+)?(?:async\s+)?(?:virtual\s+|override\s+|abstract\s+)?(?:\w+(?:<[^>]+>)?(?:\[\])?)\s+(\w+)\s*(\([^)]*\))/,
|
|
262
|
-
kind: "method",
|
|
263
|
-
nameGroup: 1,
|
|
264
|
-
sigGroup: 2,
|
|
265
|
-
},
|
|
266
|
-
];
|
|
267
|
-
case "ruby":
|
|
268
|
-
return [
|
|
269
|
-
// def name(params) / def self.name(params)
|
|
270
|
-
{
|
|
271
|
-
regex: /^\s*def\s+(?:self\.)?(\w+[?!=]?)\s*(\([^)]*\))?/,
|
|
272
|
-
kind: "function",
|
|
273
|
-
nameGroup: 1,
|
|
274
|
-
sigGroup: 2,
|
|
275
|
-
},
|
|
276
|
-
// class Name
|
|
277
|
-
{ regex: /^\s*class\s+(\w+)/, kind: "class", nameGroup: 1 },
|
|
278
|
-
// module Name
|
|
279
|
-
{ regex: /^\s*module\s+(\w+)/, kind: "interface", nameGroup: 1 },
|
|
280
|
-
];
|
|
281
|
-
case "php":
|
|
282
|
-
return [
|
|
283
|
-
// class Name
|
|
284
|
-
{
|
|
285
|
-
regex: /^\s*(?:abstract\s+|final\s+)?class\s+(\w+)/,
|
|
286
|
-
kind: "class",
|
|
287
|
-
nameGroup: 1,
|
|
288
|
-
},
|
|
289
|
-
// interface Name
|
|
290
|
-
{
|
|
291
|
-
regex: /^\s*interface\s+(\w+)/,
|
|
292
|
-
kind: "interface",
|
|
293
|
-
nameGroup: 1,
|
|
294
|
-
},
|
|
295
|
-
// trait Name
|
|
296
|
-
{
|
|
297
|
-
regex: /^\s*trait\s+(\w+)/,
|
|
298
|
-
kind: "interface",
|
|
299
|
-
nameGroup: 1,
|
|
300
|
-
},
|
|
301
|
-
// function name(params) / public function name(params)
|
|
302
|
-
{
|
|
303
|
-
regex: /^\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?function\s+(\w+)\s*(\([^)]*\))/,
|
|
304
|
-
kind: "function",
|
|
305
|
-
nameGroup: 1,
|
|
306
|
-
sigGroup: 2,
|
|
307
|
-
},
|
|
308
|
-
];
|
|
309
|
-
case "kotlin":
|
|
310
|
-
return [
|
|
311
|
-
// class Name / data class Name
|
|
312
|
-
{
|
|
313
|
-
regex: /^\s*(?:public\s+|private\s+|protected\s+|internal\s+)?(?:open\s+|abstract\s+|sealed\s+|data\s+|enum\s+)?class\s+(\w+)/,
|
|
314
|
-
kind: "class",
|
|
315
|
-
nameGroup: 1,
|
|
316
|
-
},
|
|
317
|
-
// interface Name
|
|
318
|
-
{
|
|
319
|
-
regex: /^\s*(?:public\s+|private\s+|protected\s+|internal\s+)?(?:sealed\s+)?interface\s+(\w+)/,
|
|
320
|
-
kind: "interface",
|
|
321
|
-
nameGroup: 1,
|
|
322
|
-
},
|
|
323
|
-
// object Name
|
|
324
|
-
{
|
|
325
|
-
regex: /^\s*(?:public\s+|private\s+|protected\s+|internal\s+)?object\s+(\w+)/,
|
|
326
|
-
kind: "class",
|
|
327
|
-
nameGroup: 1,
|
|
328
|
-
},
|
|
329
|
-
// fun name(params)
|
|
330
|
-
{
|
|
331
|
-
regex: /^\s*(?:public\s+|private\s+|protected\s+|internal\s+)?(?:open\s+|override\s+|abstract\s+)?(?:suspend\s+)?fun\s+(\w+)\s*(\([^)]*\))/,
|
|
332
|
-
kind: "function",
|
|
333
|
-
nameGroup: 1,
|
|
334
|
-
sigGroup: 2,
|
|
335
|
-
},
|
|
336
|
-
];
|
|
337
|
-
case "swift":
|
|
338
|
-
return [
|
|
339
|
-
// class Name
|
|
340
|
-
{
|
|
341
|
-
regex: /^\s*(?:public\s+|private\s+|internal\s+|open\s+|fileprivate\s+)?(?:final\s+)?class\s+(\w+)/,
|
|
342
|
-
kind: "class",
|
|
343
|
-
nameGroup: 1,
|
|
344
|
-
},
|
|
345
|
-
// struct Name
|
|
346
|
-
{
|
|
347
|
-
regex: /^\s*(?:public\s+|private\s+|internal\s+|fileprivate\s+)?struct\s+(\w+)/,
|
|
348
|
-
kind: "class",
|
|
349
|
-
nameGroup: 1,
|
|
350
|
-
},
|
|
351
|
-
// protocol Name
|
|
352
|
-
{
|
|
353
|
-
regex: /^\s*(?:public\s+|private\s+|internal\s+|fileprivate\s+)?protocol\s+(\w+)/,
|
|
354
|
-
kind: "interface",
|
|
355
|
-
nameGroup: 1,
|
|
356
|
-
},
|
|
357
|
-
// enum Name
|
|
358
|
-
{
|
|
359
|
-
regex: /^\s*(?:public\s+|private\s+|internal\s+|fileprivate\s+)?enum\s+(\w+)/,
|
|
360
|
-
kind: "class",
|
|
361
|
-
nameGroup: 1,
|
|
362
|
-
},
|
|
363
|
-
// func name(params)
|
|
364
|
-
{
|
|
365
|
-
regex: /^\s*(?:public\s+|private\s+|internal\s+|open\s+|fileprivate\s+)?(?:static\s+|class\s+)?(?:override\s+)?func\s+(\w+)\s*(\([^)]*\))/,
|
|
366
|
-
kind: "function",
|
|
367
|
-
nameGroup: 1,
|
|
368
|
-
sigGroup: 2,
|
|
369
|
-
},
|
|
370
|
-
];
|
|
371
|
-
default:
|
|
372
|
-
return [];
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
/**
|
|
376
|
-
* Find the end line of a code block starting at the given line.
|
|
377
|
-
* Uses brace matching for C-like languages, indentation for Python.
|
|
378
|
-
*/
|
|
379
|
-
function findBlockEnd(lines, startIdx, language) {
|
|
380
|
-
if (language === "python") {
|
|
381
|
-
return findPythonBlockEnd(lines, startIdx);
|
|
382
|
-
}
|
|
383
|
-
if (language === "ruby") {
|
|
384
|
-
return findRubyBlockEnd(lines, startIdx);
|
|
385
|
-
}
|
|
386
|
-
return findBraceBlockEnd(lines, startIdx);
|
|
387
|
-
}
|
|
388
|
-
function findBraceBlockEnd(lines, startIdx) {
|
|
389
|
-
let depth = 0;
|
|
390
|
-
let foundOpen = false;
|
|
391
|
-
for (let i = startIdx; i < lines.length; i++) {
|
|
392
|
-
const line = lines[i] ?? "";
|
|
393
|
-
for (const ch of line) {
|
|
394
|
-
if (ch === "{") {
|
|
395
|
-
depth++;
|
|
396
|
-
foundOpen = true;
|
|
397
|
-
}
|
|
398
|
-
else if (ch === "}") {
|
|
399
|
-
depth--;
|
|
400
|
-
if (foundOpen && depth === 0) {
|
|
401
|
-
return i + 1; // 1-based
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
// If no braces found after 200 lines, cap it
|
|
406
|
-
if (i - startIdx > 200)
|
|
407
|
-
return i + 1;
|
|
408
|
-
}
|
|
409
|
-
// If no closing brace, return a reasonable range
|
|
410
|
-
return Math.min(startIdx + 10, lines.length);
|
|
411
|
-
}
|
|
412
|
-
function findPythonBlockEnd(lines, startIdx) {
|
|
413
|
-
const startLine = lines[startIdx] ?? "";
|
|
414
|
-
const baseIndent = startLine.search(/\S/);
|
|
415
|
-
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
416
|
-
const line = lines[i] ?? "";
|
|
417
|
-
// Skip blank lines
|
|
418
|
-
if (line.trim().length === 0)
|
|
419
|
-
continue;
|
|
420
|
-
const indent = line.search(/\S/);
|
|
421
|
-
if (indent <= baseIndent) {
|
|
422
|
-
return i; // Previous line was last in block
|
|
423
|
-
}
|
|
424
|
-
if (i - startIdx > 200)
|
|
425
|
-
return i + 1;
|
|
426
|
-
}
|
|
427
|
-
return lines.length;
|
|
428
|
-
}
|
|
429
|
-
function findRubyBlockEnd(lines, startIdx) {
|
|
430
|
-
const keywords = /^\s*(?:def|class|module|if|unless|while|until|for|begin|case|do)\b/;
|
|
431
|
-
let depth = 1; // The opening keyword is on startIdx
|
|
432
|
-
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
433
|
-
const line = (lines[i] ?? "").trim();
|
|
434
|
-
if (line.length === 0 || line.startsWith("#"))
|
|
435
|
-
continue;
|
|
436
|
-
if (keywords.test(lines[i] ?? ""))
|
|
437
|
-
depth++;
|
|
438
|
-
if (/^\s*end\b/.test(lines[i] ?? "")) {
|
|
439
|
-
depth--;
|
|
440
|
-
if (depth === 0)
|
|
441
|
-
return i + 1;
|
|
442
|
-
}
|
|
443
|
-
if (i - startIdx > 200)
|
|
444
|
-
return i + 1;
|
|
445
|
-
}
|
|
446
|
-
return Math.min(startIdx + 10, lines.length);
|
|
447
|
-
}
|
|
448
|
-
/**
|
|
449
|
-
* Generate an entity key that matches the entityHash algorithm.
|
|
450
|
-
* Hash inputs: repoId + filePath + kind + name + signature
|
|
451
|
-
* Output: 16-char hex (matches lib/indexer/entity-hash.ts)
|
|
452
|
-
*/
|
|
453
|
-
export function entityKey(repoId, filePath, kind, name, signature) {
|
|
454
|
-
const input = [repoId, filePath, kind, name, signature ?? ""].join("\0");
|
|
455
|
-
return createHash("sha256").update(input).digest("hex").slice(0, 16);
|
|
456
|
-
}
|
|
457
|
-
/** Tree-sitter grammar name for each language (maps to WASM file). */
|
|
458
|
-
const TS_GRAMMAR_MAP = {
|
|
459
|
-
typescript: "typescript",
|
|
460
|
-
javascript: "javascript",
|
|
461
|
-
python: "python",
|
|
462
|
-
go: "go",
|
|
463
|
-
java: "java",
|
|
464
|
-
rust: "rust",
|
|
465
|
-
c: "c",
|
|
466
|
-
cpp: "cpp",
|
|
467
|
-
csharp: "c_sharp",
|
|
468
|
-
ruby: "ruby",
|
|
469
|
-
php: "php",
|
|
470
|
-
kotlin: "kotlin",
|
|
471
|
-
swift: "swift",
|
|
472
|
-
};
|
|
473
|
-
/** TSX file extensions need the tsx grammar, not typescript. */
|
|
474
|
-
const TSX_EXTENSIONS = new Set([".tsx", ".jsx"]);
|
|
475
|
-
/** Cached parsers keyed by grammar name. null = load attempted but failed. */
|
|
476
|
-
const tsParserCache = new Map();
|
|
477
|
-
/** Whether tree-sitter init has been called. */
|
|
478
|
-
let tsInitDone = false;
|
|
479
|
-
/**
|
|
480
|
-
* Resolve the grammar name for a file, handling TSX/JSX → tsx grammar.
|
|
481
|
-
*/
|
|
482
|
-
function resolveGrammar(filePath, language) {
|
|
483
|
-
const dot = filePath.lastIndexOf(".");
|
|
484
|
-
const ext = dot >= 0 ? filePath.slice(dot).toLowerCase() : "";
|
|
485
|
-
if (TSX_EXTENSIONS.has(ext))
|
|
486
|
-
return "tsx";
|
|
487
|
-
return TS_GRAMMAR_MAP[language];
|
|
488
|
-
}
|
|
489
|
-
/**
|
|
490
|
-
* Lazy-load a tree-sitter parser for a given grammar.
|
|
491
|
-
* Returns null if WASM not available (graceful degradation to regex).
|
|
492
|
-
*/
|
|
493
|
-
async function getTSParser(grammar) {
|
|
494
|
-
if (tsParserCache.has(grammar)) {
|
|
495
|
-
return tsParserCache.get(grammar) ?? null;
|
|
496
|
-
}
|
|
497
|
-
try {
|
|
498
|
-
const TreeSitter = (await import("web-tree-sitter")).default;
|
|
499
|
-
if (!tsInitDone) {
|
|
500
|
-
await TreeSitter.init();
|
|
501
|
-
tsInitDone = true;
|
|
502
|
-
}
|
|
503
|
-
const parser = new TreeSitter();
|
|
504
|
-
const wasmFile = `tree-sitter-${grammar}.wasm`;
|
|
505
|
-
// Check tree-sitter-wasms/out/ first, then fallback paths.
|
|
506
|
-
// Must check BOTH process.cwd() (user project) AND the unerr package dir,
|
|
507
|
-
// since unerr often runs in a different project's directory.
|
|
508
|
-
const pkgDir = import.meta.dirname ?? join(fileURLToPath(import.meta.url), "..");
|
|
509
|
-
const pkgRoot = join(pkgDir, ".."); // dist/ → package root
|
|
510
|
-
const possiblePaths = [
|
|
511
|
-
join(process.cwd(), "node_modules", "tree-sitter-wasms", "out", wasmFile),
|
|
512
|
-
join(process.cwd(), "node_modules", `tree-sitter-${grammar}`, wasmFile),
|
|
513
|
-
join(process.cwd(), "node_modules", "web-tree-sitter", wasmFile),
|
|
514
|
-
// Package-relative paths (when unerr is installed globally or in another project)
|
|
515
|
-
join(pkgRoot, "node_modules", "tree-sitter-wasms", "out", wasmFile),
|
|
516
|
-
join(pkgRoot, "node_modules", `tree-sitter-${grammar}`, wasmFile),
|
|
517
|
-
join(pkgRoot, "node_modules", "web-tree-sitter", wasmFile),
|
|
518
|
-
];
|
|
519
|
-
let wasmPath = null;
|
|
520
|
-
for (const p of possiblePaths) {
|
|
521
|
-
if (existsSync(p)) {
|
|
522
|
-
wasmPath = p;
|
|
523
|
-
break;
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
if (!wasmPath) {
|
|
527
|
-
tsParserCache.set(grammar, null);
|
|
528
|
-
return null;
|
|
529
|
-
}
|
|
530
|
-
const lang = await TreeSitter.Language.load(wasmPath);
|
|
531
|
-
parser.setLanguage(lang);
|
|
532
|
-
const tsParser = parser;
|
|
533
|
-
tsParserCache.set(grammar, tsParser);
|
|
534
|
-
return tsParser;
|
|
535
|
-
}
|
|
536
|
-
catch {
|
|
537
|
-
tsParserCache.set(grammar, null);
|
|
538
|
-
return null;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
/**
|
|
542
|
-
* Pre-load tree-sitter WASM grammars for given languages (DEFERRED startup).
|
|
543
|
-
* Called during deferred init so it doesn't block MCP startup.
|
|
544
|
-
*/
|
|
545
|
-
export async function preloadGrammars(languages = [
|
|
546
|
-
"typescript",
|
|
547
|
-
"javascript",
|
|
548
|
-
"python",
|
|
549
|
-
"go",
|
|
550
|
-
"java",
|
|
551
|
-
"rust",
|
|
552
|
-
]) {
|
|
553
|
-
const grammars = new Set();
|
|
554
|
-
for (const lang of languages) {
|
|
555
|
-
grammars.add(TS_GRAMMAR_MAP[lang]);
|
|
556
|
-
}
|
|
557
|
-
// Also preload tsx
|
|
558
|
-
grammars.add("tsx");
|
|
559
|
-
await Promise.allSettled([...grammars].map((g) => getTSParser(g)));
|
|
560
|
-
}
|
|
561
|
-
/**
|
|
562
|
-
* Extract entities from source code using tree-sitter AST parsing.
|
|
563
|
-
* Falls back to regex-based extraction if WASM is unavailable.
|
|
564
|
-
*
|
|
565
|
-
* Advantages over regex:
|
|
566
|
-
* - No false positives from comments or string literals
|
|
567
|
-
* - Methods include parent class context
|
|
568
|
-
* - Handles multi-line signatures and nested generics correctly
|
|
569
|
-
*/
|
|
570
|
-
export async function extractEntitiesAsync(content, filePath) {
|
|
571
|
-
const language = detectLanguage(filePath);
|
|
572
|
-
if (!language)
|
|
573
|
-
return [];
|
|
574
|
-
const grammar = resolveGrammar(filePath, language);
|
|
575
|
-
const parser = await getTSParser(grammar);
|
|
576
|
-
if (!parser) {
|
|
577
|
-
// Graceful fallback to regex extraction
|
|
578
|
-
return extractEntities(content, filePath);
|
|
579
|
-
}
|
|
580
|
-
try {
|
|
581
|
-
const tree = parser.parse(content);
|
|
582
|
-
const lines = content.split("\n");
|
|
583
|
-
return extractFromAST(tree.rootNode, lines, language);
|
|
584
|
-
}
|
|
585
|
-
catch {
|
|
586
|
-
// Parse failure — fall back to regex
|
|
587
|
-
return extractEntities(content, filePath);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
/**
|
|
591
|
-
* Walk tree-sitter AST and extract entities based on language-specific node types.
|
|
592
|
-
*/
|
|
593
|
-
function extractFromAST(root, lines, language) {
|
|
594
|
-
const entities = [];
|
|
595
|
-
switch (language) {
|
|
596
|
-
case "typescript":
|
|
597
|
-
case "javascript":
|
|
598
|
-
extractTSEntities(root, lines, entities);
|
|
599
|
-
break;
|
|
600
|
-
case "python":
|
|
601
|
-
extractPythonEntities(root, lines, entities);
|
|
602
|
-
break;
|
|
603
|
-
case "go":
|
|
604
|
-
extractGoEntities(root, lines, entities);
|
|
605
|
-
break;
|
|
606
|
-
case "java":
|
|
607
|
-
extractJavaEntities(root, lines, entities);
|
|
608
|
-
break;
|
|
609
|
-
case "rust":
|
|
610
|
-
extractRustEntities(root, lines, entities);
|
|
611
|
-
break;
|
|
612
|
-
case "c":
|
|
613
|
-
case "cpp":
|
|
614
|
-
extractCEntities(root, lines, entities);
|
|
615
|
-
break;
|
|
616
|
-
case "csharp":
|
|
617
|
-
extractCSharpEntities(root, lines, entities);
|
|
618
|
-
break;
|
|
619
|
-
case "ruby":
|
|
620
|
-
extractRubyEntities(root, lines, entities);
|
|
621
|
-
break;
|
|
622
|
-
case "php":
|
|
623
|
-
extractPHPEntities(root, lines, entities);
|
|
624
|
-
break;
|
|
625
|
-
case "kotlin":
|
|
626
|
-
extractKotlinEntities(root, lines, entities);
|
|
627
|
-
break;
|
|
628
|
-
case "swift":
|
|
629
|
-
extractSwiftEntities(root, lines, entities);
|
|
630
|
-
break;
|
|
631
|
-
}
|
|
632
|
-
return entities;
|
|
633
|
-
}
|
|
634
|
-
/** Compute content hash from line range. */
|
|
635
|
-
function hashLines(lines, startLine, endLine) {
|
|
636
|
-
const body = lines.slice(startLine - 1, endLine).join("\n");
|
|
637
|
-
return createHash("sha256").update(body).digest("hex").slice(0, 16);
|
|
638
|
-
}
|
|
639
|
-
/** Get signature text from a parameters/formal_parameters node. */
|
|
640
|
-
function getSignature(node, fieldName) {
|
|
641
|
-
const params = node.childForFieldName(fieldName);
|
|
642
|
-
return params ? params.text : "";
|
|
643
|
-
}
|
|
644
|
-
/** Find the enclosing class name for a method node. */
|
|
645
|
-
function findParentClassName(node) {
|
|
646
|
-
let current = node.parent;
|
|
647
|
-
while (current) {
|
|
648
|
-
if (current.type === "class_declaration" ||
|
|
649
|
-
current.type === "class_definition" ||
|
|
650
|
-
current.type === "class_body") {
|
|
651
|
-
if (current.type === "class_body" && current.parent) {
|
|
652
|
-
current = current.parent;
|
|
653
|
-
continue;
|
|
654
|
-
}
|
|
655
|
-
const nameNode = current.childForFieldName("name");
|
|
656
|
-
return nameNode ? nameNode.text : null;
|
|
657
|
-
}
|
|
658
|
-
current = current.parent;
|
|
659
|
-
}
|
|
660
|
-
return null;
|
|
661
|
-
}
|
|
662
|
-
// ── TypeScript/JavaScript extraction ────────────────────────────
|
|
663
|
-
/**
|
|
664
|
-
* Test framework primitives recognized for is_test classification. The first
|
|
665
|
-
* dotted segment is matched so `it.only`, `describe.skip`, `test.each(...)`,
|
|
666
|
-
* `bench.only` all qualify. When a call_expression's callee matches, the
|
|
667
|
-
* function-expression arg (the callback body) is extracted as an entity with
|
|
668
|
-
* is_test=true. Top-level helpers in test files (seedEntities, MockEntity)
|
|
669
|
-
* stay is_test=false and stop seeding spurious `tests` edges.
|
|
670
|
-
*/
|
|
671
|
-
const TEST_PRIMITIVES = new Set([
|
|
672
|
-
"describe",
|
|
673
|
-
"it",
|
|
674
|
-
"test",
|
|
675
|
-
"suite",
|
|
676
|
-
"bench",
|
|
677
|
-
"context",
|
|
678
|
-
"beforeAll",
|
|
679
|
-
"beforeEach",
|
|
680
|
-
"afterAll",
|
|
681
|
-
"afterEach",
|
|
682
|
-
"before",
|
|
683
|
-
"after",
|
|
684
|
-
"fdescribe",
|
|
685
|
-
"fit",
|
|
686
|
-
"xdescribe",
|
|
687
|
-
"xit",
|
|
688
|
-
"xtest",
|
|
689
|
-
]);
|
|
690
|
-
function extractTSEntities(node, lines, entities) {
|
|
691
|
-
walkNodes(node, (n) => {
|
|
692
|
-
switch (n.type) {
|
|
693
|
-
case "call_expression": {
|
|
694
|
-
// Test-primitive callbacks: it("desc", () => {...}) becomes an entity
|
|
695
|
-
// with is_test=true so resolveTestEdges can wire it to the source
|
|
696
|
-
// entities its calls reach. Pure helpers and fixtures (not inside a
|
|
697
|
-
// test primitive) are NOT marked — that's the whole point of this fix.
|
|
698
|
-
const fnNode = n.childForFieldName("function");
|
|
699
|
-
if (!fnNode)
|
|
700
|
-
break;
|
|
701
|
-
// First dotted segment — handles `it.only`, `describe.skip`, etc.
|
|
702
|
-
const primitive = fnNode.text.split(".")[0]?.trim();
|
|
703
|
-
if (!primitive || !TEST_PRIMITIVES.has(primitive))
|
|
704
|
-
break;
|
|
705
|
-
const argsNode = n.childForFieldName("arguments");
|
|
706
|
-
if (!argsNode)
|
|
707
|
-
break;
|
|
708
|
-
let description = null;
|
|
709
|
-
let callback = null;
|
|
710
|
-
for (const arg of argsNode.namedChildren) {
|
|
711
|
-
if (description === null &&
|
|
712
|
-
(arg.type === "string" || arg.type === "template_string")) {
|
|
713
|
-
description = arg.text.replace(/^["'`]|["'`]$/g, "");
|
|
714
|
-
}
|
|
715
|
-
else if (callback === null &&
|
|
716
|
-
(arg.type === "arrow_function" ||
|
|
717
|
-
arg.type === "function_expression" ||
|
|
718
|
-
arg.type === "function")) {
|
|
719
|
-
callback = arg;
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
if (!callback)
|
|
723
|
-
break;
|
|
724
|
-
const startLine = callback.startPosition.row + 1;
|
|
725
|
-
const endLine = callback.endPosition.row + 1;
|
|
726
|
-
const name = description
|
|
727
|
-
? `${primitive}: ${description}`
|
|
728
|
-
: `${primitive}@L${startLine}`;
|
|
729
|
-
entities.push({
|
|
730
|
-
name,
|
|
731
|
-
kind: "function",
|
|
732
|
-
signature: "",
|
|
733
|
-
line_start: startLine,
|
|
734
|
-
line_end: endLine,
|
|
735
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
736
|
-
is_test: true,
|
|
737
|
-
});
|
|
738
|
-
break;
|
|
739
|
-
}
|
|
740
|
-
case "function_declaration": {
|
|
741
|
-
const name = n.childForFieldName("name");
|
|
742
|
-
if (!name)
|
|
743
|
-
return;
|
|
744
|
-
const startLine = n.startPosition.row + 1;
|
|
745
|
-
const endLine = n.endPosition.row + 1;
|
|
746
|
-
entities.push({
|
|
747
|
-
name: name.text,
|
|
748
|
-
kind: "function",
|
|
749
|
-
signature: getSignature(n, "parameters"),
|
|
750
|
-
line_start: startLine,
|
|
751
|
-
line_end: endLine,
|
|
752
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
753
|
-
});
|
|
754
|
-
break;
|
|
755
|
-
}
|
|
756
|
-
case "class_declaration": {
|
|
757
|
-
const name = n.childForFieldName("name");
|
|
758
|
-
if (!name)
|
|
759
|
-
return;
|
|
760
|
-
const startLine = n.startPosition.row + 1;
|
|
761
|
-
const endLine = n.endPosition.row + 1;
|
|
762
|
-
entities.push({
|
|
763
|
-
name: name.text,
|
|
764
|
-
kind: "class",
|
|
765
|
-
signature: "",
|
|
766
|
-
line_start: startLine,
|
|
767
|
-
line_end: endLine,
|
|
768
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
769
|
-
});
|
|
770
|
-
break;
|
|
771
|
-
}
|
|
772
|
-
case "interface_declaration": {
|
|
773
|
-
const name = n.childForFieldName("name");
|
|
774
|
-
if (!name)
|
|
775
|
-
return;
|
|
776
|
-
const startLine = n.startPosition.row + 1;
|
|
777
|
-
const endLine = n.endPosition.row + 1;
|
|
778
|
-
entities.push({
|
|
779
|
-
name: name.text,
|
|
780
|
-
kind: "interface",
|
|
781
|
-
signature: "",
|
|
782
|
-
line_start: startLine,
|
|
783
|
-
line_end: endLine,
|
|
784
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
785
|
-
});
|
|
786
|
-
break;
|
|
787
|
-
}
|
|
788
|
-
case "method_definition": {
|
|
789
|
-
const name = n.childForFieldName("name");
|
|
790
|
-
if (!name)
|
|
791
|
-
return;
|
|
792
|
-
const className = findParentClassName(n);
|
|
793
|
-
const entityName = className ? `${className}.${name.text}` : name.text;
|
|
794
|
-
const startLine = n.startPosition.row + 1;
|
|
795
|
-
const endLine = n.endPosition.row + 1;
|
|
796
|
-
entities.push({
|
|
797
|
-
name: entityName,
|
|
798
|
-
kind: "method",
|
|
799
|
-
signature: getSignature(n, "parameters"),
|
|
800
|
-
line_start: startLine,
|
|
801
|
-
line_end: endLine,
|
|
802
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
803
|
-
parent_class: className ?? undefined,
|
|
804
|
-
});
|
|
805
|
-
break;
|
|
806
|
-
}
|
|
807
|
-
case "lexical_declaration": {
|
|
808
|
-
// const foo = (...) => { ... } or const foo = function(...) { ... }
|
|
809
|
-
// Also: const FOO = { ... }, const BAR = [...], const X = <expr>
|
|
810
|
-
for (const child of n.namedChildren) {
|
|
811
|
-
if (child.type === "variable_declarator") {
|
|
812
|
-
const nameNode = child.childForFieldName("name");
|
|
813
|
-
const valueNode = child.childForFieldName("value");
|
|
814
|
-
if (!nameNode || !valueNode)
|
|
815
|
-
continue;
|
|
816
|
-
const startLine = n.startPosition.row + 1;
|
|
817
|
-
const endLine = n.endPosition.row + 1;
|
|
818
|
-
if (valueNode.type === "arrow_function" ||
|
|
819
|
-
valueNode.type === "function_expression" ||
|
|
820
|
-
valueNode.type === "function") {
|
|
821
|
-
entities.push({
|
|
822
|
-
name: nameNode.text,
|
|
823
|
-
kind: "function",
|
|
824
|
-
signature: getSignature(valueNode, "parameters"),
|
|
825
|
-
line_start: startLine,
|
|
826
|
-
line_end: endLine,
|
|
827
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
828
|
-
});
|
|
829
|
-
}
|
|
830
|
-
else if (valueNode.type === "object" ||
|
|
831
|
-
valueNode.type === "array" ||
|
|
832
|
-
valueNode.type === "as_expression" ||
|
|
833
|
-
valueNode.type === "satisfies_expression" ||
|
|
834
|
-
valueNode.type === "new_expression" ||
|
|
835
|
-
valueNode.type === "call_expression" ||
|
|
836
|
-
valueNode.type === "template_string" ||
|
|
837
|
-
valueNode.type === "string" ||
|
|
838
|
-
valueNode.type === "number" ||
|
|
839
|
-
valueNode.type === "true" ||
|
|
840
|
-
valueNode.type === "false" ||
|
|
841
|
-
valueNode.type === "regex") {
|
|
842
|
-
// Only extract if top-level (not nested inside a function/class body)
|
|
843
|
-
const isTopLevel = !n.parent ||
|
|
844
|
-
n.parent.type === "program" ||
|
|
845
|
-
n.parent.type === "export_statement";
|
|
846
|
-
if (isTopLevel) {
|
|
847
|
-
entities.push({
|
|
848
|
-
name: nameNode.text,
|
|
849
|
-
kind: "variable",
|
|
850
|
-
signature: "",
|
|
851
|
-
line_start: startLine,
|
|
852
|
-
line_end: endLine,
|
|
853
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
854
|
-
});
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
break;
|
|
860
|
-
}
|
|
861
|
-
case "type_alias_declaration": {
|
|
862
|
-
const name = n.childForFieldName("name");
|
|
863
|
-
if (!name)
|
|
864
|
-
return;
|
|
865
|
-
const startLine = n.startPosition.row + 1;
|
|
866
|
-
const endLine = n.endPosition.row + 1;
|
|
867
|
-
entities.push({
|
|
868
|
-
name: name.text,
|
|
869
|
-
kind: "interface",
|
|
870
|
-
signature: "",
|
|
871
|
-
line_start: startLine,
|
|
872
|
-
line_end: endLine,
|
|
873
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
874
|
-
});
|
|
875
|
-
break;
|
|
876
|
-
}
|
|
877
|
-
case "enum_declaration": {
|
|
878
|
-
const name = n.childForFieldName("name");
|
|
879
|
-
if (!name)
|
|
880
|
-
return;
|
|
881
|
-
const startLine = n.startPosition.row + 1;
|
|
882
|
-
const endLine = n.endPosition.row + 1;
|
|
883
|
-
entities.push({
|
|
884
|
-
name: name.text,
|
|
885
|
-
kind: "class",
|
|
886
|
-
signature: "",
|
|
887
|
-
line_start: startLine,
|
|
888
|
-
line_end: endLine,
|
|
889
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
890
|
-
});
|
|
891
|
-
break;
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
});
|
|
895
|
-
}
|
|
896
|
-
// ── Python extraction ───────────────────────────────────────────
|
|
897
|
-
function extractPythonEntities(node, lines, entities) {
|
|
898
|
-
walkNodes(node, (n) => {
|
|
899
|
-
switch (n.type) {
|
|
900
|
-
case "function_definition": {
|
|
901
|
-
const name = n.childForFieldName("name");
|
|
902
|
-
if (!name)
|
|
903
|
-
return;
|
|
904
|
-
const className = findPythonParentClass(n);
|
|
905
|
-
const startLine = n.startPosition.row + 1;
|
|
906
|
-
const endLine = n.endPosition.row + 1;
|
|
907
|
-
if (className) {
|
|
908
|
-
entities.push({
|
|
909
|
-
name: `${className}.${name.text}`,
|
|
910
|
-
kind: "method",
|
|
911
|
-
signature: getSignature(n, "parameters"),
|
|
912
|
-
line_start: startLine,
|
|
913
|
-
line_end: endLine,
|
|
914
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
915
|
-
parent_class: className,
|
|
916
|
-
});
|
|
917
|
-
}
|
|
918
|
-
else {
|
|
919
|
-
entities.push({
|
|
920
|
-
name: name.text,
|
|
921
|
-
kind: "function",
|
|
922
|
-
signature: getSignature(n, "parameters"),
|
|
923
|
-
line_start: startLine,
|
|
924
|
-
line_end: endLine,
|
|
925
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
926
|
-
});
|
|
927
|
-
}
|
|
928
|
-
break;
|
|
929
|
-
}
|
|
930
|
-
case "class_definition": {
|
|
931
|
-
const name = n.childForFieldName("name");
|
|
932
|
-
if (!name)
|
|
933
|
-
return;
|
|
934
|
-
const startLine = n.startPosition.row + 1;
|
|
935
|
-
const endLine = n.endPosition.row + 1;
|
|
936
|
-
entities.push({
|
|
937
|
-
name: name.text,
|
|
938
|
-
kind: "class",
|
|
939
|
-
signature: "",
|
|
940
|
-
line_start: startLine,
|
|
941
|
-
line_end: endLine,
|
|
942
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
943
|
-
});
|
|
944
|
-
break;
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
});
|
|
948
|
-
}
|
|
949
|
-
function findPythonParentClass(node) {
|
|
950
|
-
let current = node.parent;
|
|
951
|
-
while (current) {
|
|
952
|
-
if (current.type === "class_definition") {
|
|
953
|
-
const nameNode = current.childForFieldName("name");
|
|
954
|
-
return nameNode ? nameNode.text : null;
|
|
955
|
-
}
|
|
956
|
-
current = current.parent;
|
|
957
|
-
}
|
|
958
|
-
return null;
|
|
959
|
-
}
|
|
960
|
-
// ── Go extraction ───────────────────────────────────────────────
|
|
961
|
-
function extractGoEntities(node, lines, entities) {
|
|
962
|
-
walkNodes(node, (n) => {
|
|
963
|
-
switch (n.type) {
|
|
964
|
-
case "function_declaration": {
|
|
965
|
-
const name = n.childForFieldName("name");
|
|
966
|
-
if (!name)
|
|
967
|
-
return;
|
|
968
|
-
const startLine = n.startPosition.row + 1;
|
|
969
|
-
const endLine = n.endPosition.row + 1;
|
|
970
|
-
entities.push({
|
|
971
|
-
name: name.text,
|
|
972
|
-
kind: "function",
|
|
973
|
-
signature: getSignature(n, "parameters"),
|
|
974
|
-
line_start: startLine,
|
|
975
|
-
line_end: endLine,
|
|
976
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
977
|
-
});
|
|
978
|
-
break;
|
|
979
|
-
}
|
|
980
|
-
case "method_declaration": {
|
|
981
|
-
const name = n.childForFieldName("name");
|
|
982
|
-
if (!name)
|
|
983
|
-
return;
|
|
984
|
-
// Go methods: func (r *Type) Name(params)
|
|
985
|
-
const receiver = n.childForFieldName("receiver");
|
|
986
|
-
let className = null;
|
|
987
|
-
if (receiver) {
|
|
988
|
-
// Extract type from receiver parameter list
|
|
989
|
-
for (const child of receiver.namedChildren) {
|
|
990
|
-
const typeNode = child.childForFieldName("type");
|
|
991
|
-
if (typeNode) {
|
|
992
|
-
className = typeNode.text.replace(/^\*/, "");
|
|
993
|
-
break;
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
const entityName = className ? `${className}.${name.text}` : name.text;
|
|
998
|
-
const startLine = n.startPosition.row + 1;
|
|
999
|
-
const endLine = n.endPosition.row + 1;
|
|
1000
|
-
entities.push({
|
|
1001
|
-
name: entityName,
|
|
1002
|
-
kind: "method",
|
|
1003
|
-
signature: getSignature(n, "parameters"),
|
|
1004
|
-
line_start: startLine,
|
|
1005
|
-
line_end: endLine,
|
|
1006
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1007
|
-
parent_class: className ?? undefined,
|
|
1008
|
-
});
|
|
1009
|
-
break;
|
|
1010
|
-
}
|
|
1011
|
-
case "type_declaration": {
|
|
1012
|
-
// type Name struct { ... } or type Name interface { ... }
|
|
1013
|
-
for (const spec of n.namedChildren) {
|
|
1014
|
-
if (spec.type === "type_spec") {
|
|
1015
|
-
const name = spec.childForFieldName("name");
|
|
1016
|
-
const typeNode = spec.childForFieldName("type");
|
|
1017
|
-
if (!name || !typeNode)
|
|
1018
|
-
continue;
|
|
1019
|
-
const kind = typeNode.type === "interface_type" ? "interface" : "class";
|
|
1020
|
-
const startLine = n.startPosition.row + 1;
|
|
1021
|
-
const endLine = n.endPosition.row + 1;
|
|
1022
|
-
entities.push({
|
|
1023
|
-
name: name.text,
|
|
1024
|
-
kind,
|
|
1025
|
-
signature: "",
|
|
1026
|
-
line_start: startLine,
|
|
1027
|
-
line_end: endLine,
|
|
1028
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1029
|
-
});
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
break;
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
});
|
|
1036
|
-
}
|
|
1037
|
-
// ── Java extraction ─────────────────────────────────────────────
|
|
1038
|
-
function extractJavaEntities(node, lines, entities) {
|
|
1039
|
-
walkNodes(node, (n) => {
|
|
1040
|
-
switch (n.type) {
|
|
1041
|
-
case "class_declaration": {
|
|
1042
|
-
const name = n.childForFieldName("name");
|
|
1043
|
-
if (!name)
|
|
1044
|
-
return;
|
|
1045
|
-
const startLine = n.startPosition.row + 1;
|
|
1046
|
-
const endLine = n.endPosition.row + 1;
|
|
1047
|
-
entities.push({
|
|
1048
|
-
name: name.text,
|
|
1049
|
-
kind: "class",
|
|
1050
|
-
signature: "",
|
|
1051
|
-
line_start: startLine,
|
|
1052
|
-
line_end: endLine,
|
|
1053
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1054
|
-
});
|
|
1055
|
-
break;
|
|
1056
|
-
}
|
|
1057
|
-
case "interface_declaration": {
|
|
1058
|
-
const name = n.childForFieldName("name");
|
|
1059
|
-
if (!name)
|
|
1060
|
-
return;
|
|
1061
|
-
const startLine = n.startPosition.row + 1;
|
|
1062
|
-
const endLine = n.endPosition.row + 1;
|
|
1063
|
-
entities.push({
|
|
1064
|
-
name: name.text,
|
|
1065
|
-
kind: "interface",
|
|
1066
|
-
signature: "",
|
|
1067
|
-
line_start: startLine,
|
|
1068
|
-
line_end: endLine,
|
|
1069
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1070
|
-
});
|
|
1071
|
-
break;
|
|
1072
|
-
}
|
|
1073
|
-
case "method_declaration": {
|
|
1074
|
-
const name = n.childForFieldName("name");
|
|
1075
|
-
if (!name)
|
|
1076
|
-
return;
|
|
1077
|
-
const className = findJavaParentClass(n);
|
|
1078
|
-
const entityName = className ? `${className}.${name.text}` : name.text;
|
|
1079
|
-
const startLine = n.startPosition.row + 1;
|
|
1080
|
-
const endLine = n.endPosition.row + 1;
|
|
1081
|
-
entities.push({
|
|
1082
|
-
name: entityName,
|
|
1083
|
-
kind: "method",
|
|
1084
|
-
signature: getSignature(n, "parameters"),
|
|
1085
|
-
line_start: startLine,
|
|
1086
|
-
line_end: endLine,
|
|
1087
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1088
|
-
parent_class: className ?? undefined,
|
|
1089
|
-
});
|
|
1090
|
-
break;
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
});
|
|
1094
|
-
}
|
|
1095
|
-
function findJavaParentClass(node) {
|
|
1096
|
-
let current = node.parent;
|
|
1097
|
-
while (current) {
|
|
1098
|
-
if (current.type === "class_declaration" ||
|
|
1099
|
-
current.type === "interface_declaration") {
|
|
1100
|
-
const nameNode = current.childForFieldName("name");
|
|
1101
|
-
return nameNode ? nameNode.text : null;
|
|
1102
|
-
}
|
|
1103
|
-
current = current.parent;
|
|
1104
|
-
}
|
|
1105
|
-
return null;
|
|
1106
|
-
}
|
|
1107
|
-
// ── Rust extraction ─────────────────────────────────────────────
|
|
1108
|
-
/**
|
|
1109
|
-
* Check if a node is inside a #[cfg(test)] module by walking up the tree.
|
|
1110
|
-
* Detects attribute_item siblings of mod_item ancestors containing "cfg" and "test".
|
|
1111
|
-
*/
|
|
1112
|
-
function isInsideCfgTest(node) {
|
|
1113
|
-
let current = node.parent;
|
|
1114
|
-
while (current) {
|
|
1115
|
-
if (current.type === "mod_item") {
|
|
1116
|
-
let sibling = current.previousSibling;
|
|
1117
|
-
while (sibling) {
|
|
1118
|
-
if (sibling.type === "attribute_item") {
|
|
1119
|
-
const text = sibling.text;
|
|
1120
|
-
if (text.includes("cfg") && text.includes("test"))
|
|
1121
|
-
return true;
|
|
1122
|
-
}
|
|
1123
|
-
else if (sibling.type !== "line_comment" &&
|
|
1124
|
-
sibling.type !== "block_comment") {
|
|
1125
|
-
break;
|
|
1126
|
-
}
|
|
1127
|
-
sibling = sibling.previousSibling;
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
current = current.parent;
|
|
1131
|
-
}
|
|
1132
|
-
return false;
|
|
1133
|
-
}
|
|
1134
|
-
function extractRustEntities(node, lines, entities) {
|
|
1135
|
-
walkNodes(node, (n) => {
|
|
1136
|
-
switch (n.type) {
|
|
1137
|
-
case "function_item": {
|
|
1138
|
-
const name = n.childForFieldName("name");
|
|
1139
|
-
if (!name)
|
|
1140
|
-
return;
|
|
1141
|
-
// Check if inside impl block
|
|
1142
|
-
const implName = findRustImplName(n);
|
|
1143
|
-
const entityName = implName ? `${implName}.${name.text}` : name.text;
|
|
1144
|
-
const kind = implName ? "method" : "function";
|
|
1145
|
-
const startLine = n.startPosition.row + 1;
|
|
1146
|
-
const endLine = n.endPosition.row + 1;
|
|
1147
|
-
const entity = {
|
|
1148
|
-
name: entityName,
|
|
1149
|
-
kind,
|
|
1150
|
-
signature: getSignature(n, "parameters"),
|
|
1151
|
-
line_start: startLine,
|
|
1152
|
-
line_end: endLine,
|
|
1153
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1154
|
-
parent_class: implName ?? undefined,
|
|
1155
|
-
};
|
|
1156
|
-
if (isInsideCfgTest(n))
|
|
1157
|
-
entity.is_test = true;
|
|
1158
|
-
entities.push(entity);
|
|
1159
|
-
break;
|
|
1160
|
-
}
|
|
1161
|
-
case "struct_item": {
|
|
1162
|
-
const name = n.childForFieldName("name");
|
|
1163
|
-
if (!name)
|
|
1164
|
-
return;
|
|
1165
|
-
const startLine = n.startPosition.row + 1;
|
|
1166
|
-
const endLine = n.endPosition.row + 1;
|
|
1167
|
-
const entity = {
|
|
1168
|
-
name: name.text,
|
|
1169
|
-
kind: "class",
|
|
1170
|
-
signature: "",
|
|
1171
|
-
line_start: startLine,
|
|
1172
|
-
line_end: endLine,
|
|
1173
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1174
|
-
};
|
|
1175
|
-
if (isInsideCfgTest(n))
|
|
1176
|
-
entity.is_test = true;
|
|
1177
|
-
entities.push(entity);
|
|
1178
|
-
break;
|
|
1179
|
-
}
|
|
1180
|
-
case "trait_item": {
|
|
1181
|
-
const name = n.childForFieldName("name");
|
|
1182
|
-
if (!name)
|
|
1183
|
-
return;
|
|
1184
|
-
const startLine = n.startPosition.row + 1;
|
|
1185
|
-
const endLine = n.endPosition.row + 1;
|
|
1186
|
-
const entity = {
|
|
1187
|
-
name: name.text,
|
|
1188
|
-
kind: "interface",
|
|
1189
|
-
signature: "",
|
|
1190
|
-
line_start: startLine,
|
|
1191
|
-
line_end: endLine,
|
|
1192
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1193
|
-
};
|
|
1194
|
-
if (isInsideCfgTest(n))
|
|
1195
|
-
entity.is_test = true;
|
|
1196
|
-
entities.push(entity);
|
|
1197
|
-
break;
|
|
1198
|
-
}
|
|
1199
|
-
case "impl_item": {
|
|
1200
|
-
const name = n.childForFieldName("type");
|
|
1201
|
-
if (!name)
|
|
1202
|
-
return;
|
|
1203
|
-
const startLine = n.startPosition.row + 1;
|
|
1204
|
-
const endLine = n.endPosition.row + 1;
|
|
1205
|
-
const entity = {
|
|
1206
|
-
name: name.text,
|
|
1207
|
-
kind: "class",
|
|
1208
|
-
signature: "",
|
|
1209
|
-
line_start: startLine,
|
|
1210
|
-
line_end: endLine,
|
|
1211
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1212
|
-
};
|
|
1213
|
-
if (isInsideCfgTest(n))
|
|
1214
|
-
entity.is_test = true;
|
|
1215
|
-
entities.push(entity);
|
|
1216
|
-
break;
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
});
|
|
1220
|
-
}
|
|
1221
|
-
function findRustImplName(node) {
|
|
1222
|
-
let current = node.parent;
|
|
1223
|
-
while (current) {
|
|
1224
|
-
if (current.type === "impl_item") {
|
|
1225
|
-
const typeNode = current.childForFieldName("type");
|
|
1226
|
-
return typeNode ? typeNode.text : null;
|
|
1227
|
-
}
|
|
1228
|
-
// Skip declaration_list (impl body)
|
|
1229
|
-
current = current.parent;
|
|
1230
|
-
}
|
|
1231
|
-
return null;
|
|
1232
|
-
}
|
|
1233
|
-
// ── C/C++ extraction ────────────────────────────────────────────
|
|
1234
|
-
function extractCEntities(node, lines, entities) {
|
|
1235
|
-
walkNodes(node, (n) => {
|
|
1236
|
-
switch (n.type) {
|
|
1237
|
-
case "function_definition": {
|
|
1238
|
-
const declarator = n.childForFieldName("declarator");
|
|
1239
|
-
if (!declarator)
|
|
1240
|
-
return;
|
|
1241
|
-
// function_declarator has name and parameters
|
|
1242
|
-
const name = declarator.type === "function_declarator"
|
|
1243
|
-
? declarator.childForFieldName("declarator")
|
|
1244
|
-
: null;
|
|
1245
|
-
if (!name)
|
|
1246
|
-
return;
|
|
1247
|
-
const startLine = n.startPosition.row + 1;
|
|
1248
|
-
const endLine = n.endPosition.row + 1;
|
|
1249
|
-
entities.push({
|
|
1250
|
-
name: name.text,
|
|
1251
|
-
kind: "function",
|
|
1252
|
-
signature: getSignature(declarator, "parameters"),
|
|
1253
|
-
line_start: startLine,
|
|
1254
|
-
line_end: endLine,
|
|
1255
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1256
|
-
});
|
|
1257
|
-
break;
|
|
1258
|
-
}
|
|
1259
|
-
case "struct_specifier": {
|
|
1260
|
-
const name = n.childForFieldName("name");
|
|
1261
|
-
if (!name)
|
|
1262
|
-
return;
|
|
1263
|
-
const startLine = n.startPosition.row + 1;
|
|
1264
|
-
const endLine = n.endPosition.row + 1;
|
|
1265
|
-
entities.push({
|
|
1266
|
-
name: name.text,
|
|
1267
|
-
kind: "class",
|
|
1268
|
-
signature: "",
|
|
1269
|
-
line_start: startLine,
|
|
1270
|
-
line_end: endLine,
|
|
1271
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1272
|
-
});
|
|
1273
|
-
break;
|
|
1274
|
-
}
|
|
1275
|
-
case "class_specifier": {
|
|
1276
|
-
const name = n.childForFieldName("name");
|
|
1277
|
-
if (!name)
|
|
1278
|
-
return;
|
|
1279
|
-
const startLine = n.startPosition.row + 1;
|
|
1280
|
-
const endLine = n.endPosition.row + 1;
|
|
1281
|
-
entities.push({
|
|
1282
|
-
name: name.text,
|
|
1283
|
-
kind: "class",
|
|
1284
|
-
signature: "",
|
|
1285
|
-
line_start: startLine,
|
|
1286
|
-
line_end: endLine,
|
|
1287
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1288
|
-
});
|
|
1289
|
-
break;
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
});
|
|
1293
|
-
}
|
|
1294
|
-
// ── C# extraction ──────────────────────────────────────────────
|
|
1295
|
-
function extractCSharpEntities(node, lines, entities) {
|
|
1296
|
-
walkNodes(node, (n) => {
|
|
1297
|
-
switch (n.type) {
|
|
1298
|
-
case "class_declaration": {
|
|
1299
|
-
const name = n.childForFieldName("name");
|
|
1300
|
-
if (!name)
|
|
1301
|
-
return;
|
|
1302
|
-
const startLine = n.startPosition.row + 1;
|
|
1303
|
-
const endLine = n.endPosition.row + 1;
|
|
1304
|
-
entities.push({
|
|
1305
|
-
name: name.text,
|
|
1306
|
-
kind: "class",
|
|
1307
|
-
signature: "",
|
|
1308
|
-
line_start: startLine,
|
|
1309
|
-
line_end: endLine,
|
|
1310
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1311
|
-
});
|
|
1312
|
-
break;
|
|
1313
|
-
}
|
|
1314
|
-
case "interface_declaration": {
|
|
1315
|
-
const name = n.childForFieldName("name");
|
|
1316
|
-
if (!name)
|
|
1317
|
-
return;
|
|
1318
|
-
const startLine = n.startPosition.row + 1;
|
|
1319
|
-
const endLine = n.endPosition.row + 1;
|
|
1320
|
-
entities.push({
|
|
1321
|
-
name: name.text,
|
|
1322
|
-
kind: "interface",
|
|
1323
|
-
signature: "",
|
|
1324
|
-
line_start: startLine,
|
|
1325
|
-
line_end: endLine,
|
|
1326
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1327
|
-
});
|
|
1328
|
-
break;
|
|
1329
|
-
}
|
|
1330
|
-
case "struct_declaration": {
|
|
1331
|
-
const name = n.childForFieldName("name");
|
|
1332
|
-
if (!name)
|
|
1333
|
-
return;
|
|
1334
|
-
const startLine = n.startPosition.row + 1;
|
|
1335
|
-
const endLine = n.endPosition.row + 1;
|
|
1336
|
-
entities.push({
|
|
1337
|
-
name: name.text,
|
|
1338
|
-
kind: "class",
|
|
1339
|
-
signature: "",
|
|
1340
|
-
line_start: startLine,
|
|
1341
|
-
line_end: endLine,
|
|
1342
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1343
|
-
});
|
|
1344
|
-
break;
|
|
1345
|
-
}
|
|
1346
|
-
case "method_declaration": {
|
|
1347
|
-
const name = n.childForFieldName("name");
|
|
1348
|
-
if (!name)
|
|
1349
|
-
return;
|
|
1350
|
-
const className = findCSharpParentClass(n);
|
|
1351
|
-
const entityName = className ? `${className}.${name.text}` : name.text;
|
|
1352
|
-
const startLine = n.startPosition.row + 1;
|
|
1353
|
-
const endLine = n.endPosition.row + 1;
|
|
1354
|
-
entities.push({
|
|
1355
|
-
name: entityName,
|
|
1356
|
-
kind: "method",
|
|
1357
|
-
signature: getSignature(n, "parameters"),
|
|
1358
|
-
line_start: startLine,
|
|
1359
|
-
line_end: endLine,
|
|
1360
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1361
|
-
parent_class: className ?? undefined,
|
|
1362
|
-
});
|
|
1363
|
-
break;
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
});
|
|
1367
|
-
}
|
|
1368
|
-
function findCSharpParentClass(node) {
|
|
1369
|
-
let current = node.parent;
|
|
1370
|
-
while (current) {
|
|
1371
|
-
if (current.type === "class_declaration" ||
|
|
1372
|
-
current.type === "struct_declaration" ||
|
|
1373
|
-
current.type === "interface_declaration") {
|
|
1374
|
-
const nameNode = current.childForFieldName("name");
|
|
1375
|
-
return nameNode ? nameNode.text : null;
|
|
1376
|
-
}
|
|
1377
|
-
current = current.parent;
|
|
1378
|
-
}
|
|
1379
|
-
return null;
|
|
1380
|
-
}
|
|
1381
|
-
// ── Ruby extraction ────────────────────────────────────────────
|
|
1382
|
-
function extractRubyEntities(node, lines, entities) {
|
|
1383
|
-
walkNodes(node, (n) => {
|
|
1384
|
-
switch (n.type) {
|
|
1385
|
-
case "class": {
|
|
1386
|
-
const name = n.childForFieldName("name");
|
|
1387
|
-
if (!name)
|
|
1388
|
-
return;
|
|
1389
|
-
const startLine = n.startPosition.row + 1;
|
|
1390
|
-
const endLine = n.endPosition.row + 1;
|
|
1391
|
-
entities.push({
|
|
1392
|
-
name: name.text,
|
|
1393
|
-
kind: "class",
|
|
1394
|
-
signature: "",
|
|
1395
|
-
line_start: startLine,
|
|
1396
|
-
line_end: endLine,
|
|
1397
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1398
|
-
});
|
|
1399
|
-
break;
|
|
1400
|
-
}
|
|
1401
|
-
case "module": {
|
|
1402
|
-
const name = n.childForFieldName("name");
|
|
1403
|
-
if (!name)
|
|
1404
|
-
return;
|
|
1405
|
-
const startLine = n.startPosition.row + 1;
|
|
1406
|
-
const endLine = n.endPosition.row + 1;
|
|
1407
|
-
entities.push({
|
|
1408
|
-
name: name.text,
|
|
1409
|
-
kind: "interface",
|
|
1410
|
-
signature: "",
|
|
1411
|
-
line_start: startLine,
|
|
1412
|
-
line_end: endLine,
|
|
1413
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1414
|
-
});
|
|
1415
|
-
break;
|
|
1416
|
-
}
|
|
1417
|
-
case "method":
|
|
1418
|
-
case "singleton_method": {
|
|
1419
|
-
const name = n.childForFieldName("name");
|
|
1420
|
-
if (!name)
|
|
1421
|
-
return;
|
|
1422
|
-
const className = findRubyParentClass(n);
|
|
1423
|
-
const entityName = className ? `${className}.${name.text}` : name.text;
|
|
1424
|
-
const kind = className ? "method" : "function";
|
|
1425
|
-
const startLine = n.startPosition.row + 1;
|
|
1426
|
-
const endLine = n.endPosition.row + 1;
|
|
1427
|
-
entities.push({
|
|
1428
|
-
name: entityName,
|
|
1429
|
-
kind,
|
|
1430
|
-
signature: getSignature(n, "parameters"),
|
|
1431
|
-
line_start: startLine,
|
|
1432
|
-
line_end: endLine,
|
|
1433
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1434
|
-
parent_class: className ?? undefined,
|
|
1435
|
-
});
|
|
1436
|
-
break;
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
});
|
|
1440
|
-
}
|
|
1441
|
-
function findRubyParentClass(node) {
|
|
1442
|
-
let current = node.parent;
|
|
1443
|
-
while (current) {
|
|
1444
|
-
if (current.type === "class") {
|
|
1445
|
-
const nameNode = current.childForFieldName("name");
|
|
1446
|
-
return nameNode ? nameNode.text : null;
|
|
1447
|
-
}
|
|
1448
|
-
current = current.parent;
|
|
1449
|
-
}
|
|
1450
|
-
return null;
|
|
1451
|
-
}
|
|
1452
|
-
// ── PHP extraction ─────────────────────────────────────────────
|
|
1453
|
-
function extractPHPEntities(node, lines, entities) {
|
|
1454
|
-
walkNodes(node, (n) => {
|
|
1455
|
-
switch (n.type) {
|
|
1456
|
-
case "class_declaration": {
|
|
1457
|
-
const name = n.childForFieldName("name");
|
|
1458
|
-
if (!name)
|
|
1459
|
-
return;
|
|
1460
|
-
const startLine = n.startPosition.row + 1;
|
|
1461
|
-
const endLine = n.endPosition.row + 1;
|
|
1462
|
-
entities.push({
|
|
1463
|
-
name: name.text,
|
|
1464
|
-
kind: "class",
|
|
1465
|
-
signature: "",
|
|
1466
|
-
line_start: startLine,
|
|
1467
|
-
line_end: endLine,
|
|
1468
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1469
|
-
});
|
|
1470
|
-
break;
|
|
1471
|
-
}
|
|
1472
|
-
case "interface_declaration": {
|
|
1473
|
-
const name = n.childForFieldName("name");
|
|
1474
|
-
if (!name)
|
|
1475
|
-
return;
|
|
1476
|
-
const startLine = n.startPosition.row + 1;
|
|
1477
|
-
const endLine = n.endPosition.row + 1;
|
|
1478
|
-
entities.push({
|
|
1479
|
-
name: name.text,
|
|
1480
|
-
kind: "interface",
|
|
1481
|
-
signature: "",
|
|
1482
|
-
line_start: startLine,
|
|
1483
|
-
line_end: endLine,
|
|
1484
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1485
|
-
});
|
|
1486
|
-
break;
|
|
1487
|
-
}
|
|
1488
|
-
case "trait_declaration": {
|
|
1489
|
-
const name = n.childForFieldName("name");
|
|
1490
|
-
if (!name)
|
|
1491
|
-
return;
|
|
1492
|
-
const startLine = n.startPosition.row + 1;
|
|
1493
|
-
const endLine = n.endPosition.row + 1;
|
|
1494
|
-
entities.push({
|
|
1495
|
-
name: name.text,
|
|
1496
|
-
kind: "interface",
|
|
1497
|
-
signature: "",
|
|
1498
|
-
line_start: startLine,
|
|
1499
|
-
line_end: endLine,
|
|
1500
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1501
|
-
});
|
|
1502
|
-
break;
|
|
1503
|
-
}
|
|
1504
|
-
case "function_definition": {
|
|
1505
|
-
const name = n.childForFieldName("name");
|
|
1506
|
-
if (!name)
|
|
1507
|
-
return;
|
|
1508
|
-
const startLine = n.startPosition.row + 1;
|
|
1509
|
-
const endLine = n.endPosition.row + 1;
|
|
1510
|
-
entities.push({
|
|
1511
|
-
name: name.text,
|
|
1512
|
-
kind: "function",
|
|
1513
|
-
signature: getSignature(n, "parameters"),
|
|
1514
|
-
line_start: startLine,
|
|
1515
|
-
line_end: endLine,
|
|
1516
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1517
|
-
});
|
|
1518
|
-
break;
|
|
1519
|
-
}
|
|
1520
|
-
case "method_declaration": {
|
|
1521
|
-
const name = n.childForFieldName("name");
|
|
1522
|
-
if (!name)
|
|
1523
|
-
return;
|
|
1524
|
-
const className = findPHPParentClass(n);
|
|
1525
|
-
const entityName = className ? `${className}.${name.text}` : name.text;
|
|
1526
|
-
const startLine = n.startPosition.row + 1;
|
|
1527
|
-
const endLine = n.endPosition.row + 1;
|
|
1528
|
-
entities.push({
|
|
1529
|
-
name: entityName,
|
|
1530
|
-
kind: "method",
|
|
1531
|
-
signature: getSignature(n, "parameters"),
|
|
1532
|
-
line_start: startLine,
|
|
1533
|
-
line_end: endLine,
|
|
1534
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1535
|
-
parent_class: className ?? undefined,
|
|
1536
|
-
});
|
|
1537
|
-
break;
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
});
|
|
1541
|
-
}
|
|
1542
|
-
function findPHPParentClass(node) {
|
|
1543
|
-
let current = node.parent;
|
|
1544
|
-
while (current) {
|
|
1545
|
-
if (current.type === "class_declaration" ||
|
|
1546
|
-
current.type === "trait_declaration" ||
|
|
1547
|
-
current.type === "interface_declaration") {
|
|
1548
|
-
const nameNode = current.childForFieldName("name");
|
|
1549
|
-
return nameNode ? nameNode.text : null;
|
|
1550
|
-
}
|
|
1551
|
-
current = current.parent;
|
|
1552
|
-
}
|
|
1553
|
-
return null;
|
|
1554
|
-
}
|
|
1555
|
-
// ── Kotlin extraction ──────────────────────────────────────────
|
|
1556
|
-
function extractKotlinEntities(node, lines, entities) {
|
|
1557
|
-
walkNodes(node, (n) => {
|
|
1558
|
-
switch (n.type) {
|
|
1559
|
-
case "class_declaration": {
|
|
1560
|
-
const name = findFirstIdentifier(n);
|
|
1561
|
-
if (!name)
|
|
1562
|
-
return;
|
|
1563
|
-
const startLine = n.startPosition.row + 1;
|
|
1564
|
-
const endLine = n.endPosition.row + 1;
|
|
1565
|
-
entities.push({
|
|
1566
|
-
name,
|
|
1567
|
-
kind: "class",
|
|
1568
|
-
signature: "",
|
|
1569
|
-
line_start: startLine,
|
|
1570
|
-
line_end: endLine,
|
|
1571
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1572
|
-
});
|
|
1573
|
-
break;
|
|
1574
|
-
}
|
|
1575
|
-
case "interface_declaration": {
|
|
1576
|
-
const name = findFirstIdentifier(n);
|
|
1577
|
-
if (!name)
|
|
1578
|
-
return;
|
|
1579
|
-
const startLine = n.startPosition.row + 1;
|
|
1580
|
-
const endLine = n.endPosition.row + 1;
|
|
1581
|
-
entities.push({
|
|
1582
|
-
name,
|
|
1583
|
-
kind: "interface",
|
|
1584
|
-
signature: "",
|
|
1585
|
-
line_start: startLine,
|
|
1586
|
-
line_end: endLine,
|
|
1587
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1588
|
-
});
|
|
1589
|
-
break;
|
|
1590
|
-
}
|
|
1591
|
-
case "object_declaration": {
|
|
1592
|
-
const name = findFirstIdentifier(n);
|
|
1593
|
-
if (!name)
|
|
1594
|
-
return;
|
|
1595
|
-
const startLine = n.startPosition.row + 1;
|
|
1596
|
-
const endLine = n.endPosition.row + 1;
|
|
1597
|
-
entities.push({
|
|
1598
|
-
name,
|
|
1599
|
-
kind: "class",
|
|
1600
|
-
signature: "",
|
|
1601
|
-
line_start: startLine,
|
|
1602
|
-
line_end: endLine,
|
|
1603
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1604
|
-
});
|
|
1605
|
-
break;
|
|
1606
|
-
}
|
|
1607
|
-
case "function_declaration": {
|
|
1608
|
-
const name = (n.childForFieldName("name") ?? findFirstIdentifier(n))
|
|
1609
|
-
? { text: findFirstIdentifier(n) }
|
|
1610
|
-
: null;
|
|
1611
|
-
if (!name?.text)
|
|
1612
|
-
return;
|
|
1613
|
-
const className = findKotlinParentClass(n);
|
|
1614
|
-
const entityName = className ? `${className}.${name.text}` : name.text;
|
|
1615
|
-
const kind = className ? "method" : "function";
|
|
1616
|
-
const startLine = n.startPosition.row + 1;
|
|
1617
|
-
const endLine = n.endPosition.row + 1;
|
|
1618
|
-
entities.push({
|
|
1619
|
-
name: entityName,
|
|
1620
|
-
kind,
|
|
1621
|
-
signature: getSignature(n, "value_parameters"),
|
|
1622
|
-
line_start: startLine,
|
|
1623
|
-
line_end: endLine,
|
|
1624
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1625
|
-
parent_class: className ?? undefined,
|
|
1626
|
-
});
|
|
1627
|
-
break;
|
|
1628
|
-
}
|
|
1629
|
-
}
|
|
1630
|
-
});
|
|
1631
|
-
}
|
|
1632
|
-
/** Find the first simple_identifier child of a node (Kotlin grammars use this instead of "name" field). */
|
|
1633
|
-
function findFirstIdentifier(node) {
|
|
1634
|
-
for (const child of node.namedChildren) {
|
|
1635
|
-
if (child.type === "type_identifier" ||
|
|
1636
|
-
child.type === "simple_identifier") {
|
|
1637
|
-
return child.text;
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
return null;
|
|
1641
|
-
}
|
|
1642
|
-
function findKotlinParentClass(node) {
|
|
1643
|
-
let current = node.parent;
|
|
1644
|
-
while (current) {
|
|
1645
|
-
if (current.type === "class_declaration" ||
|
|
1646
|
-
current.type === "object_declaration" ||
|
|
1647
|
-
current.type === "interface_declaration") {
|
|
1648
|
-
return findFirstIdentifier(current);
|
|
1649
|
-
}
|
|
1650
|
-
// Skip class_body
|
|
1651
|
-
current = current.parent;
|
|
1652
|
-
}
|
|
1653
|
-
return null;
|
|
1654
|
-
}
|
|
1655
|
-
// ── Swift extraction ───────────────────────────────────────────
|
|
1656
|
-
function extractSwiftEntities(node, lines, entities) {
|
|
1657
|
-
walkNodes(node, (n) => {
|
|
1658
|
-
switch (n.type) {
|
|
1659
|
-
case "class_declaration": {
|
|
1660
|
-
const name = n.childForFieldName("name");
|
|
1661
|
-
if (!name)
|
|
1662
|
-
return;
|
|
1663
|
-
const startLine = n.startPosition.row + 1;
|
|
1664
|
-
const endLine = n.endPosition.row + 1;
|
|
1665
|
-
entities.push({
|
|
1666
|
-
name: name.text,
|
|
1667
|
-
kind: "class",
|
|
1668
|
-
signature: "",
|
|
1669
|
-
line_start: startLine,
|
|
1670
|
-
line_end: endLine,
|
|
1671
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1672
|
-
});
|
|
1673
|
-
break;
|
|
1674
|
-
}
|
|
1675
|
-
case "struct_declaration": {
|
|
1676
|
-
const name = n.childForFieldName("name");
|
|
1677
|
-
if (!name)
|
|
1678
|
-
return;
|
|
1679
|
-
const startLine = n.startPosition.row + 1;
|
|
1680
|
-
const endLine = n.endPosition.row + 1;
|
|
1681
|
-
entities.push({
|
|
1682
|
-
name: name.text,
|
|
1683
|
-
kind: "class",
|
|
1684
|
-
signature: "",
|
|
1685
|
-
line_start: startLine,
|
|
1686
|
-
line_end: endLine,
|
|
1687
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1688
|
-
});
|
|
1689
|
-
break;
|
|
1690
|
-
}
|
|
1691
|
-
case "protocol_declaration": {
|
|
1692
|
-
const name = n.childForFieldName("name");
|
|
1693
|
-
if (!name)
|
|
1694
|
-
return;
|
|
1695
|
-
const startLine = n.startPosition.row + 1;
|
|
1696
|
-
const endLine = n.endPosition.row + 1;
|
|
1697
|
-
entities.push({
|
|
1698
|
-
name: name.text,
|
|
1699
|
-
kind: "interface",
|
|
1700
|
-
signature: "",
|
|
1701
|
-
line_start: startLine,
|
|
1702
|
-
line_end: endLine,
|
|
1703
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1704
|
-
});
|
|
1705
|
-
break;
|
|
1706
|
-
}
|
|
1707
|
-
case "enum_declaration": {
|
|
1708
|
-
const name = n.childForFieldName("name");
|
|
1709
|
-
if (!name)
|
|
1710
|
-
return;
|
|
1711
|
-
const startLine = n.startPosition.row + 1;
|
|
1712
|
-
const endLine = n.endPosition.row + 1;
|
|
1713
|
-
entities.push({
|
|
1714
|
-
name: name.text,
|
|
1715
|
-
kind: "class",
|
|
1716
|
-
signature: "",
|
|
1717
|
-
line_start: startLine,
|
|
1718
|
-
line_end: endLine,
|
|
1719
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1720
|
-
});
|
|
1721
|
-
break;
|
|
1722
|
-
}
|
|
1723
|
-
case "function_declaration": {
|
|
1724
|
-
const name = n.childForFieldName("name");
|
|
1725
|
-
if (!name)
|
|
1726
|
-
return;
|
|
1727
|
-
const className = findSwiftParentClass(n);
|
|
1728
|
-
const entityName = className ? `${className}.${name.text}` : name.text;
|
|
1729
|
-
const kind = className ? "method" : "function";
|
|
1730
|
-
const startLine = n.startPosition.row + 1;
|
|
1731
|
-
const endLine = n.endPosition.row + 1;
|
|
1732
|
-
entities.push({
|
|
1733
|
-
name: entityName,
|
|
1734
|
-
kind,
|
|
1735
|
-
signature: getSignature(n, "parameters"),
|
|
1736
|
-
line_start: startLine,
|
|
1737
|
-
line_end: endLine,
|
|
1738
|
-
content_hash: hashLines(lines, startLine, endLine),
|
|
1739
|
-
parent_class: className ?? undefined,
|
|
1740
|
-
});
|
|
1741
|
-
break;
|
|
1742
|
-
}
|
|
1743
|
-
}
|
|
1744
|
-
});
|
|
1745
|
-
}
|
|
1746
|
-
function findSwiftParentClass(node) {
|
|
1747
|
-
let current = node.parent;
|
|
1748
|
-
while (current) {
|
|
1749
|
-
if (current.type === "class_declaration" ||
|
|
1750
|
-
current.type === "struct_declaration" ||
|
|
1751
|
-
current.type === "enum_declaration" ||
|
|
1752
|
-
current.type === "protocol_declaration") {
|
|
1753
|
-
const nameNode = current.childForFieldName("name");
|
|
1754
|
-
return nameNode ? nameNode.text : null;
|
|
1755
|
-
}
|
|
1756
|
-
current = current.parent;
|
|
1757
|
-
}
|
|
1758
|
-
return null;
|
|
1759
|
-
}
|
|
1760
|
-
/**
|
|
1761
|
-
* Extract edges (imports, calls, extends, implements) from source code using tree-sitter AST.
|
|
1762
|
-
* Falls back to regex-based extraction if WASM is unavailable.
|
|
1763
|
-
*/
|
|
1764
|
-
export async function extractEdgesAsync(content, filePath, entities) {
|
|
1765
|
-
const language = detectLanguage(filePath);
|
|
1766
|
-
if (!language)
|
|
1767
|
-
return [];
|
|
1768
|
-
const grammar = resolveGrammar(filePath, language);
|
|
1769
|
-
const parser = await getTSParser(grammar);
|
|
1770
|
-
if (!parser) {
|
|
1771
|
-
return extractEdgesRegex(content, filePath, language);
|
|
1772
|
-
}
|
|
1773
|
-
try {
|
|
1774
|
-
const tree = parser.parse(content);
|
|
1775
|
-
return extractEdgesFromAST(tree.rootNode, language, entities);
|
|
1776
|
-
}
|
|
1777
|
-
catch {
|
|
1778
|
-
return extractEdgesRegex(content, filePath, language);
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
1781
|
-
/** Extract edges from tree-sitter AST by language. */
|
|
1782
|
-
function extractEdgesFromAST(root, language, entities) {
|
|
1783
|
-
const edges = [];
|
|
1784
|
-
switch (language) {
|
|
1785
|
-
case "typescript":
|
|
1786
|
-
case "javascript":
|
|
1787
|
-
extractTSEdges(root, edges, entities);
|
|
1788
|
-
break;
|
|
1789
|
-
case "python":
|
|
1790
|
-
extractPythonEdges(root, edges);
|
|
1791
|
-
break;
|
|
1792
|
-
case "go":
|
|
1793
|
-
extractGoEdges(root, edges);
|
|
1794
|
-
break;
|
|
1795
|
-
case "java":
|
|
1796
|
-
extractJavaEdges(root, edges);
|
|
1797
|
-
break;
|
|
1798
|
-
case "rust":
|
|
1799
|
-
extractRustEdges(root, edges);
|
|
1800
|
-
break;
|
|
1801
|
-
case "csharp":
|
|
1802
|
-
extractCSharpEdges(root, edges);
|
|
1803
|
-
break;
|
|
1804
|
-
case "ruby":
|
|
1805
|
-
extractRubyEdges(root, edges);
|
|
1806
|
-
break;
|
|
1807
|
-
case "php":
|
|
1808
|
-
extractPHPEdges(root, edges);
|
|
1809
|
-
break;
|
|
1810
|
-
case "kotlin":
|
|
1811
|
-
extractKotlinEdges(root, edges);
|
|
1812
|
-
break;
|
|
1813
|
-
case "swift":
|
|
1814
|
-
extractSwiftEdges(root, edges);
|
|
1815
|
-
break;
|
|
1816
|
-
default:
|
|
1817
|
-
break;
|
|
1818
|
-
}
|
|
1819
|
-
return edges;
|
|
1820
|
-
}
|
|
1821
|
-
/** TS/JS: extract imports, calls, extends, implements from AST. */
|
|
1822
|
-
function extractTSEdges(root, edges, entities) {
|
|
1823
|
-
const entityNames = new Set(entities.map((e) => e.name.split(".").pop() ?? e.name));
|
|
1824
|
-
// Collect imported symbol names so cross-file calls are captured.
|
|
1825
|
-
// First pass: gather import names before recording call edges.
|
|
1826
|
-
const importedNames = new Set();
|
|
1827
|
-
walkNodes(root, (n) => {
|
|
1828
|
-
if (n.type === "import_statement") {
|
|
1829
|
-
for (const child of n.namedChildren) {
|
|
1830
|
-
if (child.type === "import_clause") {
|
|
1831
|
-
for (const spec of child.namedChildren) {
|
|
1832
|
-
if (spec.type === "named_imports") {
|
|
1833
|
-
for (const s of spec.namedChildren) {
|
|
1834
|
-
if (s.type === "import_specifier") {
|
|
1835
|
-
const name = s.childForFieldName("name");
|
|
1836
|
-
if (name)
|
|
1837
|
-
importedNames.add(name.text);
|
|
1838
|
-
}
|
|
1839
|
-
}
|
|
1840
|
-
}
|
|
1841
|
-
else if (spec.type === "identifier") {
|
|
1842
|
-
importedNames.add(spec.text);
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
}
|
|
1848
|
-
});
|
|
1849
|
-
walkNodes(root, (n) => {
|
|
1850
|
-
// import { Foo, Bar } from "./module"
|
|
1851
|
-
if (n.type === "import_statement") {
|
|
1852
|
-
const source = n.childForFieldName("source");
|
|
1853
|
-
const importSource = source ? source.text.replace(/['"]/g, "") : "";
|
|
1854
|
-
// Named imports
|
|
1855
|
-
for (const child of n.namedChildren) {
|
|
1856
|
-
if (child.type === "import_clause") {
|
|
1857
|
-
for (const spec of child.namedChildren) {
|
|
1858
|
-
if (spec.type === "named_imports") {
|
|
1859
|
-
for (const s of spec.namedChildren) {
|
|
1860
|
-
if (s.type === "import_specifier") {
|
|
1861
|
-
const name = s.childForFieldName("name");
|
|
1862
|
-
if (name) {
|
|
1863
|
-
edges.push({
|
|
1864
|
-
from_name: "__file__",
|
|
1865
|
-
to_name: name.text,
|
|
1866
|
-
type: "imports",
|
|
1867
|
-
import_source: importSource,
|
|
1868
|
-
});
|
|
1869
|
-
}
|
|
1870
|
-
}
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1873
|
-
else if (spec.type === "identifier") {
|
|
1874
|
-
// Default import
|
|
1875
|
-
edges.push({
|
|
1876
|
-
from_name: "__file__",
|
|
1877
|
-
to_name: spec.text,
|
|
1878
|
-
type: "imports",
|
|
1879
|
-
import_source: importSource,
|
|
1880
|
-
});
|
|
1881
|
-
}
|
|
1882
|
-
}
|
|
1883
|
-
}
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
// class Foo extends Bar implements Baz
|
|
1887
|
-
if (n.type === "class_declaration") {
|
|
1888
|
-
const className = n.childForFieldName("name");
|
|
1889
|
-
if (!className)
|
|
1890
|
-
return;
|
|
1891
|
-
// Heritage: extends and implements
|
|
1892
|
-
for (const child of n.namedChildren) {
|
|
1893
|
-
if (child.type === "class_heritage") {
|
|
1894
|
-
for (const clause of child.namedChildren) {
|
|
1895
|
-
if (clause.type === "extends_clause") {
|
|
1896
|
-
for (const val of clause.namedChildren) {
|
|
1897
|
-
if (val.type === "identifier") {
|
|
1898
|
-
edges.push({
|
|
1899
|
-
from_name: className.text,
|
|
1900
|
-
to_name: val.text,
|
|
1901
|
-
type: "extends",
|
|
1902
|
-
});
|
|
1903
|
-
}
|
|
1904
|
-
}
|
|
1905
|
-
}
|
|
1906
|
-
if (clause.type === "implements_clause") {
|
|
1907
|
-
for (const val of clause.namedChildren) {
|
|
1908
|
-
if (val.type === "type_identifier" ||
|
|
1909
|
-
val.type === "identifier") {
|
|
1910
|
-
edges.push({
|
|
1911
|
-
from_name: className.text,
|
|
1912
|
-
to_name: val.text,
|
|
1913
|
-
type: "implements",
|
|
1914
|
-
});
|
|
1915
|
-
}
|
|
1916
|
-
}
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1919
|
-
}
|
|
1920
|
-
}
|
|
1921
|
-
}
|
|
1922
|
-
// Function calls: foo(), bar.baz(), new Foo()
|
|
1923
|
-
if (n.type === "call_expression") {
|
|
1924
|
-
const func = n.childForFieldName("function");
|
|
1925
|
-
if (!func)
|
|
1926
|
-
return;
|
|
1927
|
-
let calledName = null;
|
|
1928
|
-
if (func.type === "identifier") {
|
|
1929
|
-
calledName = func.text;
|
|
1930
|
-
}
|
|
1931
|
-
else if (func.type === "member_expression") {
|
|
1932
|
-
const prop = func.childForFieldName("property");
|
|
1933
|
-
if (prop)
|
|
1934
|
-
calledName = prop.text;
|
|
1935
|
-
}
|
|
1936
|
-
if (calledName &&
|
|
1937
|
-
(entityNames.has(calledName) || importedNames.has(calledName))) {
|
|
1938
|
-
const caller = findEnclosingEntity(n, entities);
|
|
1939
|
-
edges.push({
|
|
1940
|
-
from_name: caller ?? "__file__",
|
|
1941
|
-
to_name: calledName,
|
|
1942
|
-
type: "calls",
|
|
1943
|
-
});
|
|
1944
|
-
}
|
|
1945
|
-
}
|
|
1946
|
-
// new Foo()
|
|
1947
|
-
if (n.type === "new_expression") {
|
|
1948
|
-
const ctor = n.childForFieldName("constructor");
|
|
1949
|
-
if (ctor && ctor.type === "identifier") {
|
|
1950
|
-
const caller = findEnclosingEntity(n, entities);
|
|
1951
|
-
edges.push({
|
|
1952
|
-
from_name: caller ?? "__file__",
|
|
1953
|
-
to_name: ctor.text,
|
|
1954
|
-
type: "calls",
|
|
1955
|
-
});
|
|
1956
|
-
}
|
|
1957
|
-
}
|
|
1958
|
-
});
|
|
1959
|
-
}
|
|
1960
|
-
/** Python: extract imports, calls, class inheritance. */
|
|
1961
|
-
function extractPythonEdges(root, edges) {
|
|
1962
|
-
walkNodes(root, (n) => {
|
|
1963
|
-
// import foo / from foo import bar
|
|
1964
|
-
if (n.type === "import_statement" || n.type === "import_from_statement") {
|
|
1965
|
-
const moduleName = n.childForFieldName("module_name");
|
|
1966
|
-
const importSource = moduleName ? moduleName.text : "";
|
|
1967
|
-
for (const child of n.namedChildren) {
|
|
1968
|
-
if (child.type === "dotted_name" && child !== moduleName) {
|
|
1969
|
-
edges.push({
|
|
1970
|
-
from_name: "__file__",
|
|
1971
|
-
to_name: child.text.split(".").pop() ?? child.text,
|
|
1972
|
-
type: "imports",
|
|
1973
|
-
import_source: importSource,
|
|
1974
|
-
});
|
|
1975
|
-
}
|
|
1976
|
-
if (child.type === "aliased_import") {
|
|
1977
|
-
const name = child.childForFieldName("name");
|
|
1978
|
-
if (name) {
|
|
1979
|
-
edges.push({
|
|
1980
|
-
from_name: "__file__",
|
|
1981
|
-
to_name: name.text.split(".").pop() ?? name.text,
|
|
1982
|
-
type: "imports",
|
|
1983
|
-
import_source: importSource,
|
|
1984
|
-
});
|
|
1985
|
-
}
|
|
1986
|
-
}
|
|
1987
|
-
}
|
|
1988
|
-
}
|
|
1989
|
-
// class Foo(Bar, Baz):
|
|
1990
|
-
if (n.type === "class_definition") {
|
|
1991
|
-
const name = n.childForFieldName("name");
|
|
1992
|
-
const superclasses = n.childForFieldName("superclasses");
|
|
1993
|
-
if (name && superclasses) {
|
|
1994
|
-
for (const arg of superclasses.namedChildren) {
|
|
1995
|
-
if (arg.type === "identifier") {
|
|
1996
|
-
edges.push({
|
|
1997
|
-
from_name: name.text,
|
|
1998
|
-
to_name: arg.text,
|
|
1999
|
-
type: "extends",
|
|
2000
|
-
});
|
|
2001
|
-
}
|
|
2002
|
-
}
|
|
2003
|
-
}
|
|
2004
|
-
}
|
|
2005
|
-
});
|
|
2006
|
-
}
|
|
2007
|
-
/** Go: extract imports and struct embedding. */
|
|
2008
|
-
function extractGoEdges(root, edges) {
|
|
2009
|
-
walkNodes(root, (n) => {
|
|
2010
|
-
if (n.type === "import_declaration") {
|
|
2011
|
-
for (const child of n.namedChildren) {
|
|
2012
|
-
if (child.type === "import_spec" || child.type === "import_spec_list") {
|
|
2013
|
-
const specs = child.type === "import_spec_list" ? child.namedChildren : [child];
|
|
2014
|
-
for (const spec of specs) {
|
|
2015
|
-
if (spec.type === "import_spec") {
|
|
2016
|
-
const path = spec.childForFieldName("path");
|
|
2017
|
-
if (path) {
|
|
2018
|
-
const importPath = path.text.replace(/"/g, "");
|
|
2019
|
-
const pkgName = importPath.split("/").pop() ?? importPath;
|
|
2020
|
-
edges.push({
|
|
2021
|
-
from_name: "__file__",
|
|
2022
|
-
to_name: pkgName,
|
|
2023
|
-
type: "imports",
|
|
2024
|
-
import_source: importPath,
|
|
2025
|
-
});
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
}
|
|
2030
|
-
}
|
|
2031
|
-
}
|
|
2032
|
-
});
|
|
2033
|
-
}
|
|
2034
|
-
/** Java: extract imports and extends/implements. */
|
|
2035
|
-
function extractJavaEdges(root, edges) {
|
|
2036
|
-
walkNodes(root, (n) => {
|
|
2037
|
-
if (n.type === "import_declaration") {
|
|
2038
|
-
// import com.example.Foo;
|
|
2039
|
-
for (const child of n.namedChildren) {
|
|
2040
|
-
if (child.type === "scoped_identifier") {
|
|
2041
|
-
const name = child.text.split(".").pop() ?? child.text;
|
|
2042
|
-
edges.push({
|
|
2043
|
-
from_name: "__file__",
|
|
2044
|
-
to_name: name,
|
|
2045
|
-
type: "imports",
|
|
2046
|
-
import_source: child.text,
|
|
2047
|
-
});
|
|
2048
|
-
}
|
|
2049
|
-
}
|
|
2050
|
-
}
|
|
2051
|
-
if (n.type === "class_declaration") {
|
|
2052
|
-
const name = n.childForFieldName("name");
|
|
2053
|
-
const superclass = n.childForFieldName("superclass");
|
|
2054
|
-
const interfaces = n.childForFieldName("interfaces");
|
|
2055
|
-
if (name && superclass) {
|
|
2056
|
-
edges.push({
|
|
2057
|
-
from_name: name.text,
|
|
2058
|
-
to_name: superclass.text,
|
|
2059
|
-
type: "extends",
|
|
2060
|
-
});
|
|
2061
|
-
}
|
|
2062
|
-
if (name && interfaces) {
|
|
2063
|
-
for (const iface of interfaces.namedChildren) {
|
|
2064
|
-
if (iface.type === "type_identifier" || iface.type === "type_list") {
|
|
2065
|
-
const names = iface.type === "type_list" ? iface.namedChildren : [iface];
|
|
2066
|
-
for (const t of names) {
|
|
2067
|
-
edges.push({
|
|
2068
|
-
from_name: name.text,
|
|
2069
|
-
to_name: t.text,
|
|
2070
|
-
type: "implements",
|
|
2071
|
-
});
|
|
2072
|
-
}
|
|
2073
|
-
}
|
|
2074
|
-
}
|
|
2075
|
-
}
|
|
2076
|
-
}
|
|
2077
|
-
});
|
|
2078
|
-
}
|
|
2079
|
-
/** Rust: extract use statements and trait impls. */
|
|
2080
|
-
function extractRustEdges(root, edges) {
|
|
2081
|
-
walkNodes(root, (n) => {
|
|
2082
|
-
if (n.type === "use_declaration") {
|
|
2083
|
-
// Extract the full use path for import_source, then leaf names
|
|
2084
|
-
const fullPath = extractRustUsePath(n);
|
|
2085
|
-
extractRustUseNames(n, edges, fullPath);
|
|
2086
|
-
}
|
|
2087
|
-
// impl Trait for Type
|
|
2088
|
-
if (n.type === "impl_item") {
|
|
2089
|
-
const traitNode = n.childForFieldName("trait");
|
|
2090
|
-
const typeNode = n.childForFieldName("type");
|
|
2091
|
-
if (traitNode && typeNode) {
|
|
2092
|
-
edges.push({
|
|
2093
|
-
from_name: typeNode.text,
|
|
2094
|
-
to_name: traitNode.text,
|
|
2095
|
-
type: "implements",
|
|
2096
|
-
});
|
|
2097
|
-
}
|
|
2098
|
-
}
|
|
2099
|
-
});
|
|
2100
|
-
}
|
|
2101
|
-
// ── New language edge extraction (tree-sitter) ──────────────────
|
|
2102
|
-
function extractCSharpEdges(root, edges) {
|
|
2103
|
-
walkNodes(root, (n) => {
|
|
2104
|
-
// using Namespace.Type;
|
|
2105
|
-
if (n.type === "using_directive") {
|
|
2106
|
-
const name = n.text
|
|
2107
|
-
.replace(/^using\s+/, "")
|
|
2108
|
-
.replace(/;$/, "")
|
|
2109
|
-
.trim();
|
|
2110
|
-
const leaf = name.split(".").pop() ?? name;
|
|
2111
|
-
edges.push({
|
|
2112
|
-
from_name: "__file__",
|
|
2113
|
-
to_name: leaf,
|
|
2114
|
-
type: "imports",
|
|
2115
|
-
import_source: name,
|
|
2116
|
-
});
|
|
2117
|
-
}
|
|
2118
|
-
// class Foo : Bar, IBaz
|
|
2119
|
-
if (n.type === "base_list") {
|
|
2120
|
-
const parent = n.parent;
|
|
2121
|
-
const parentName = parent?.childForFieldName("name");
|
|
2122
|
-
if (parentName) {
|
|
2123
|
-
for (const child of n.namedChildren) {
|
|
2124
|
-
const baseName = child.type === "identifier"
|
|
2125
|
-
? child.text
|
|
2126
|
-
: child.childForFieldName("name")?.text;
|
|
2127
|
-
if (baseName) {
|
|
2128
|
-
edges.push({
|
|
2129
|
-
from_name: parentName.text,
|
|
2130
|
-
to_name: baseName,
|
|
2131
|
-
type: "extends",
|
|
2132
|
-
});
|
|
2133
|
-
}
|
|
2134
|
-
}
|
|
2135
|
-
}
|
|
2136
|
-
}
|
|
2137
|
-
});
|
|
2138
|
-
}
|
|
2139
|
-
function extractRubyEdges(root, edges) {
|
|
2140
|
-
walkNodes(root, (n) => {
|
|
2141
|
-
// require "foo" / require_relative "foo"
|
|
2142
|
-
if (n.type === "call" && n.namedChildren.length >= 1) {
|
|
2143
|
-
const methodName = n.childForFieldName("method");
|
|
2144
|
-
if (methodName &&
|
|
2145
|
-
(methodName.text === "require" ||
|
|
2146
|
-
methodName.text === "require_relative")) {
|
|
2147
|
-
const args = n.childForFieldName("arguments");
|
|
2148
|
-
if (args) {
|
|
2149
|
-
for (const arg of args.namedChildren) {
|
|
2150
|
-
if (arg.type === "string") {
|
|
2151
|
-
const val = arg.text.replace(/^['"]|['"]$/g, "");
|
|
2152
|
-
const leaf = val.split("/").pop() ?? val;
|
|
2153
|
-
edges.push({
|
|
2154
|
-
from_name: "__file__",
|
|
2155
|
-
to_name: leaf,
|
|
2156
|
-
type: "imports",
|
|
2157
|
-
import_source: val,
|
|
2158
|
-
});
|
|
2159
|
-
}
|
|
2160
|
-
}
|
|
2161
|
-
}
|
|
2162
|
-
}
|
|
2163
|
-
}
|
|
2164
|
-
// class Foo < Bar
|
|
2165
|
-
if (n.type === "class") {
|
|
2166
|
-
const name = n.childForFieldName("name");
|
|
2167
|
-
const superclass = n.childForFieldName("superclass");
|
|
2168
|
-
if (name && superclass) {
|
|
2169
|
-
edges.push({
|
|
2170
|
-
from_name: name.text,
|
|
2171
|
-
to_name: superclass.text,
|
|
2172
|
-
type: "extends",
|
|
2173
|
-
});
|
|
2174
|
-
}
|
|
2175
|
-
}
|
|
2176
|
-
});
|
|
2177
|
-
}
|
|
2178
|
-
function extractPHPEdges(root, edges) {
|
|
2179
|
-
walkNodes(root, (n) => {
|
|
2180
|
-
// use Namespace\Class;
|
|
2181
|
-
if (n.type === "namespace_use_declaration") {
|
|
2182
|
-
for (const clause of n.namedChildren) {
|
|
2183
|
-
if (clause.type === "namespace_use_clause") {
|
|
2184
|
-
const name = clause.text;
|
|
2185
|
-
const leaf = name.split("\\").pop() ?? name;
|
|
2186
|
-
edges.push({
|
|
2187
|
-
from_name: "__file__",
|
|
2188
|
-
to_name: leaf,
|
|
2189
|
-
type: "imports",
|
|
2190
|
-
import_source: name,
|
|
2191
|
-
});
|
|
2192
|
-
}
|
|
2193
|
-
}
|
|
2194
|
-
}
|
|
2195
|
-
// class Foo extends Bar implements Baz
|
|
2196
|
-
if (n.type === "class_declaration") {
|
|
2197
|
-
const name = n.childForFieldName("name");
|
|
2198
|
-
if (!name)
|
|
2199
|
-
return;
|
|
2200
|
-
const baseClause = n.childForFieldName("base_clause");
|
|
2201
|
-
if (baseClause) {
|
|
2202
|
-
for (const child of baseClause.namedChildren) {
|
|
2203
|
-
if (child.type === "name" || child.type === "qualified_name") {
|
|
2204
|
-
edges.push({
|
|
2205
|
-
from_name: name.text,
|
|
2206
|
-
to_name: child.text,
|
|
2207
|
-
type: "extends",
|
|
2208
|
-
});
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
|
-
const interfaces = n.childForFieldName("interfaces");
|
|
2213
|
-
if (interfaces) {
|
|
2214
|
-
for (const child of interfaces.namedChildren) {
|
|
2215
|
-
if (child.type === "name" || child.type === "qualified_name") {
|
|
2216
|
-
edges.push({
|
|
2217
|
-
from_name: name.text,
|
|
2218
|
-
to_name: child.text,
|
|
2219
|
-
type: "implements",
|
|
2220
|
-
});
|
|
2221
|
-
}
|
|
2222
|
-
}
|
|
2223
|
-
}
|
|
2224
|
-
}
|
|
2225
|
-
});
|
|
2226
|
-
}
|
|
2227
|
-
function extractKotlinEdges(root, edges) {
|
|
2228
|
-
walkNodes(root, (n) => {
|
|
2229
|
-
// import foo.bar.Baz
|
|
2230
|
-
if (n.type === "import_header") {
|
|
2231
|
-
const path = n.text.replace(/^import\s+/, "").trim();
|
|
2232
|
-
const leaf = path.split(".").pop() ?? path;
|
|
2233
|
-
if (leaf !== "*") {
|
|
2234
|
-
edges.push({
|
|
2235
|
-
from_name: "__file__",
|
|
2236
|
-
to_name: leaf,
|
|
2237
|
-
type: "imports",
|
|
2238
|
-
import_source: path,
|
|
2239
|
-
});
|
|
2240
|
-
}
|
|
2241
|
-
}
|
|
2242
|
-
// class Foo : Bar, Baz
|
|
2243
|
-
if (n.type === "class_declaration" || n.type === "object_declaration") {
|
|
2244
|
-
const name = findFirstIdentifier(n);
|
|
2245
|
-
if (!name)
|
|
2246
|
-
return;
|
|
2247
|
-
const delegation = n.namedChildren.find((c) => c.type === "delegation_specifier" ||
|
|
2248
|
-
c.type === "delegation_specifiers");
|
|
2249
|
-
if (delegation) {
|
|
2250
|
-
for (const child of delegation.namedChildren) {
|
|
2251
|
-
const typeName = child.text.split("(")[0]?.trim();
|
|
2252
|
-
if (typeName) {
|
|
2253
|
-
edges.push({ from_name: name, to_name: typeName, type: "extends" });
|
|
2254
|
-
}
|
|
2255
|
-
}
|
|
2256
|
-
}
|
|
2257
|
-
}
|
|
2258
|
-
});
|
|
2259
|
-
}
|
|
2260
|
-
function extractSwiftEdges(root, edges) {
|
|
2261
|
-
walkNodes(root, (n) => {
|
|
2262
|
-
// import Foundation
|
|
2263
|
-
if (n.type === "import_declaration") {
|
|
2264
|
-
const path = n.text.replace(/^import\s+/, "").trim();
|
|
2265
|
-
const leaf = path.split(".").pop() ?? path;
|
|
2266
|
-
edges.push({
|
|
2267
|
-
from_name: "__file__",
|
|
2268
|
-
to_name: leaf,
|
|
2269
|
-
type: "imports",
|
|
2270
|
-
import_source: path,
|
|
2271
|
-
});
|
|
2272
|
-
}
|
|
2273
|
-
// class Foo: Bar, Baz — inheritance_specifier
|
|
2274
|
-
if (n.type === "class_declaration" ||
|
|
2275
|
-
n.type === "struct_declaration" ||
|
|
2276
|
-
n.type === "enum_declaration") {
|
|
2277
|
-
const name = n.childForFieldName("name");
|
|
2278
|
-
if (!name)
|
|
2279
|
-
return;
|
|
2280
|
-
for (const child of n.namedChildren) {
|
|
2281
|
-
if (child.type === "type_inheritance_clause") {
|
|
2282
|
-
for (const inherited of child.namedChildren) {
|
|
2283
|
-
const typeName = inherited.text.trim();
|
|
2284
|
-
if (typeName) {
|
|
2285
|
-
edges.push({
|
|
2286
|
-
from_name: name.text,
|
|
2287
|
-
to_name: typeName,
|
|
2288
|
-
type: "extends",
|
|
2289
|
-
});
|
|
2290
|
-
}
|
|
2291
|
-
}
|
|
2292
|
-
}
|
|
2293
|
-
}
|
|
2294
|
-
}
|
|
2295
|
-
});
|
|
2296
|
-
}
|
|
2297
|
-
/** Extract the full path from a Rust use declaration (e.g., "crate::module::sub"). */
|
|
2298
|
-
function extractRustUsePath(node) {
|
|
2299
|
-
// The use_declaration's text is like "use crate::foo::bar;" — extract the path portion
|
|
2300
|
-
const text = node.text
|
|
2301
|
-
.replace(/^use\s+/, "")
|
|
2302
|
-
.replace(/;$/, "")
|
|
2303
|
-
.trim();
|
|
2304
|
-
// Remove any trailing ::{...} or ::* for the base path
|
|
2305
|
-
const braceIdx = text.indexOf("::{");
|
|
2306
|
-
if (braceIdx >= 0)
|
|
2307
|
-
return text.slice(0, braceIdx);
|
|
2308
|
-
const starIdx = text.indexOf("::*");
|
|
2309
|
-
if (starIdx >= 0)
|
|
2310
|
-
return text.slice(0, starIdx);
|
|
2311
|
-
// For "use crate::foo::Bar" → base is "crate::foo"
|
|
2312
|
-
const lastSep = text.lastIndexOf("::");
|
|
2313
|
-
if (lastSep >= 0)
|
|
2314
|
-
return text.slice(0, lastSep);
|
|
2315
|
-
return text;
|
|
2316
|
-
}
|
|
2317
|
-
function extractRustUseNames(node, edges, importSource) {
|
|
2318
|
-
walkNodes(node, (n) => {
|
|
2319
|
-
if (n.type === "identifier" && n.parent?.type === "use_as_clause") {
|
|
2320
|
-
edges.push({
|
|
2321
|
-
from_name: "__file__",
|
|
2322
|
-
to_name: n.text,
|
|
2323
|
-
type: "imports",
|
|
2324
|
-
import_source: importSource,
|
|
2325
|
-
});
|
|
2326
|
-
}
|
|
2327
|
-
else if (n.type === "identifier" &&
|
|
2328
|
-
(n.parent?.type === "use_declaration" ||
|
|
2329
|
-
n.parent?.type === "scoped_identifier" ||
|
|
2330
|
-
n.parent?.type === "use_list")) {
|
|
2331
|
-
// Only leaf identifiers (no parent scoped_identifier that has this as left side)
|
|
2332
|
-
const isLeaf = !n.children.some((c) => c.type === "scoped_identifier");
|
|
2333
|
-
if (isLeaf && n.parent?.type !== "scoped_identifier") {
|
|
2334
|
-
edges.push({
|
|
2335
|
-
from_name: "__file__",
|
|
2336
|
-
to_name: n.text,
|
|
2337
|
-
type: "imports",
|
|
2338
|
-
import_source: importSource,
|
|
2339
|
-
});
|
|
2340
|
-
}
|
|
2341
|
-
}
|
|
2342
|
-
});
|
|
2343
|
-
}
|
|
2344
|
-
/**
|
|
2345
|
-
* Find the enclosing entity (function/method/class) for a given AST node.
|
|
2346
|
-
* Returns the entity name or null if at file scope.
|
|
2347
|
-
*/
|
|
2348
|
-
function findEnclosingEntity(node, entities) {
|
|
2349
|
-
const callLine = node.startPosition.row + 1;
|
|
2350
|
-
let best = null;
|
|
2351
|
-
let bestSpan = Number.POSITIVE_INFINITY;
|
|
2352
|
-
for (const entity of entities) {
|
|
2353
|
-
if (callLine >= entity.line_start && callLine <= entity.line_end) {
|
|
2354
|
-
const span = entity.line_end - entity.line_start;
|
|
2355
|
-
// Prefer the narrowest enclosing entity (method over class)
|
|
2356
|
-
if (span < bestSpan) {
|
|
2357
|
-
bestSpan = span;
|
|
2358
|
-
best = entity;
|
|
2359
|
-
}
|
|
2360
|
-
}
|
|
2361
|
-
}
|
|
2362
|
-
return best?.name ?? null;
|
|
2363
|
-
}
|
|
2364
|
-
/** Regex-based edge extraction fallback (imports only). */
|
|
2365
|
-
function extractEdgesRegex(content, _filePath, language) {
|
|
2366
|
-
const edges = [];
|
|
2367
|
-
const lines = content.split("\n");
|
|
2368
|
-
for (const line of lines) {
|
|
2369
|
-
switch (language) {
|
|
2370
|
-
case "typescript":
|
|
2371
|
-
case "javascript": {
|
|
2372
|
-
// import { Foo, Bar } from "./module"
|
|
2373
|
-
const importMatch = line.match(/^\s*import\s+(?:(?:type\s+)?{([^}]+)}|(\w+))\s+from\s+['"]([^'"]+)['"]/);
|
|
2374
|
-
if (importMatch) {
|
|
2375
|
-
const names = importMatch[1] ?? importMatch[2] ?? "";
|
|
2376
|
-
const source = importMatch[3] ?? "";
|
|
2377
|
-
for (const name of names.split(",")) {
|
|
2378
|
-
const trimmed = name
|
|
2379
|
-
.trim()
|
|
2380
|
-
.split(/\s+as\s+/)[0]
|
|
2381
|
-
?.trim();
|
|
2382
|
-
if (trimmed) {
|
|
2383
|
-
edges.push({
|
|
2384
|
-
from_name: "__file__",
|
|
2385
|
-
to_name: trimmed,
|
|
2386
|
-
type: "imports",
|
|
2387
|
-
import_source: source,
|
|
2388
|
-
});
|
|
2389
|
-
}
|
|
2390
|
-
}
|
|
2391
|
-
}
|
|
2392
|
-
// class Foo extends Bar
|
|
2393
|
-
const extendsMatch = line.match(/class\s+(\w+)\s+extends\s+(\w+)/);
|
|
2394
|
-
if (extendsMatch?.[1] && extendsMatch[2]) {
|
|
2395
|
-
edges.push({
|
|
2396
|
-
from_name: extendsMatch[1],
|
|
2397
|
-
to_name: extendsMatch[2],
|
|
2398
|
-
type: "extends",
|
|
2399
|
-
});
|
|
2400
|
-
}
|
|
2401
|
-
break;
|
|
2402
|
-
}
|
|
2403
|
-
case "python": {
|
|
2404
|
-
const fromImport = line.match(/^\s*from\s+(\S+)\s+import\s+(.+)/);
|
|
2405
|
-
if (fromImport?.[1] && fromImport[2]) {
|
|
2406
|
-
const pySource = fromImport[1];
|
|
2407
|
-
for (const name of fromImport[2].split(",")) {
|
|
2408
|
-
const trimmed = name
|
|
2409
|
-
.trim()
|
|
2410
|
-
.split(/\s+as\s+/)[0]
|
|
2411
|
-
?.trim();
|
|
2412
|
-
if (trimmed) {
|
|
2413
|
-
edges.push({
|
|
2414
|
-
from_name: "__file__",
|
|
2415
|
-
to_name: trimmed,
|
|
2416
|
-
type: "imports",
|
|
2417
|
-
import_source: pySource,
|
|
2418
|
-
});
|
|
2419
|
-
}
|
|
2420
|
-
}
|
|
2421
|
-
}
|
|
2422
|
-
const pyImport = line.match(/^\s*import\s+(\S+)/);
|
|
2423
|
-
if (pyImport?.[1] && !line.includes(" from ")) {
|
|
2424
|
-
const mod = pyImport[1].replace(/,$/, "");
|
|
2425
|
-
edges.push({
|
|
2426
|
-
from_name: "__file__",
|
|
2427
|
-
to_name: mod.split(".").pop() ?? mod,
|
|
2428
|
-
type: "imports",
|
|
2429
|
-
import_source: mod,
|
|
2430
|
-
});
|
|
2431
|
-
}
|
|
2432
|
-
break;
|
|
2433
|
-
}
|
|
2434
|
-
case "go": {
|
|
2435
|
-
const goImport = line.match(/^\s*"([^"]+)"/);
|
|
2436
|
-
if (goImport?.[1]) {
|
|
2437
|
-
const pkg = goImport[1].split("/").pop() ?? goImport[1];
|
|
2438
|
-
edges.push({
|
|
2439
|
-
from_name: "__file__",
|
|
2440
|
-
to_name: pkg,
|
|
2441
|
-
type: "imports",
|
|
2442
|
-
import_source: goImport[1],
|
|
2443
|
-
});
|
|
2444
|
-
}
|
|
2445
|
-
break;
|
|
2446
|
-
}
|
|
2447
|
-
case "java": {
|
|
2448
|
-
const javaImport = line.match(/^\s*import\s+(?:static\s+)?([^;]+);/);
|
|
2449
|
-
if (javaImport?.[1]) {
|
|
2450
|
-
const name = javaImport[1].split(".").pop() ?? javaImport[1];
|
|
2451
|
-
edges.push({
|
|
2452
|
-
from_name: "__file__",
|
|
2453
|
-
to_name: name,
|
|
2454
|
-
type: "imports",
|
|
2455
|
-
import_source: javaImport[1],
|
|
2456
|
-
});
|
|
2457
|
-
}
|
|
2458
|
-
break;
|
|
2459
|
-
}
|
|
2460
|
-
case "rust": {
|
|
2461
|
-
const useMatch = line.match(/^\s*use\s+([^;]+);/);
|
|
2462
|
-
if (useMatch?.[1]) {
|
|
2463
|
-
const path = useMatch[1];
|
|
2464
|
-
const name = path.split("::").pop() ?? path;
|
|
2465
|
-
// Compute base path (everything before last ::)
|
|
2466
|
-
const lastSep = path.lastIndexOf("::");
|
|
2467
|
-
const rustSource = lastSep >= 0 ? path.slice(0, lastSep) : path;
|
|
2468
|
-
if (name !== "*" && name !== "self") {
|
|
2469
|
-
edges.push({
|
|
2470
|
-
from_name: "__file__",
|
|
2471
|
-
to_name: name,
|
|
2472
|
-
type: "imports",
|
|
2473
|
-
import_source: rustSource,
|
|
2474
|
-
});
|
|
2475
|
-
}
|
|
2476
|
-
}
|
|
2477
|
-
break;
|
|
2478
|
-
}
|
|
2479
|
-
case "csharp": {
|
|
2480
|
-
const usingMatch = line.match(/^\s*using\s+(?:static\s+)?([^;=]+);/);
|
|
2481
|
-
if (usingMatch?.[1]) {
|
|
2482
|
-
const ns = usingMatch[1].trim();
|
|
2483
|
-
const leaf = ns.split(".").pop() ?? ns;
|
|
2484
|
-
edges.push({
|
|
2485
|
-
from_name: "__file__",
|
|
2486
|
-
to_name: leaf,
|
|
2487
|
-
type: "imports",
|
|
2488
|
-
import_source: ns,
|
|
2489
|
-
});
|
|
2490
|
-
}
|
|
2491
|
-
const csExtends = line.match(/class\s+(\w+)\s*:\s*(\w+)/);
|
|
2492
|
-
if (csExtends?.[1] && csExtends[2]) {
|
|
2493
|
-
edges.push({
|
|
2494
|
-
from_name: csExtends[1],
|
|
2495
|
-
to_name: csExtends[2],
|
|
2496
|
-
type: "extends",
|
|
2497
|
-
});
|
|
2498
|
-
}
|
|
2499
|
-
break;
|
|
2500
|
-
}
|
|
2501
|
-
case "ruby": {
|
|
2502
|
-
const reqMatch = line.match(/^\s*require(?:_relative)?\s+['"]([^'"]+)['"]/);
|
|
2503
|
-
if (reqMatch?.[1]) {
|
|
2504
|
-
const mod = reqMatch[1];
|
|
2505
|
-
const leaf = mod.split("/").pop() ?? mod;
|
|
2506
|
-
edges.push({
|
|
2507
|
-
from_name: "__file__",
|
|
2508
|
-
to_name: leaf,
|
|
2509
|
-
type: "imports",
|
|
2510
|
-
import_source: mod,
|
|
2511
|
-
});
|
|
2512
|
-
}
|
|
2513
|
-
const rbExtends = line.match(/class\s+(\w+)\s*<\s*(\w+)/);
|
|
2514
|
-
if (rbExtends?.[1] && rbExtends[2]) {
|
|
2515
|
-
edges.push({
|
|
2516
|
-
from_name: rbExtends[1],
|
|
2517
|
-
to_name: rbExtends[2],
|
|
2518
|
-
type: "extends",
|
|
2519
|
-
});
|
|
2520
|
-
}
|
|
2521
|
-
break;
|
|
2522
|
-
}
|
|
2523
|
-
case "php": {
|
|
2524
|
-
const phpUse = line.match(/^\s*use\s+([^;]+);/);
|
|
2525
|
-
if (phpUse?.[1]) {
|
|
2526
|
-
const ns = phpUse[1].trim();
|
|
2527
|
-
const leaf = ns.split("\\").pop() ?? ns;
|
|
2528
|
-
edges.push({
|
|
2529
|
-
from_name: "__file__",
|
|
2530
|
-
to_name: leaf,
|
|
2531
|
-
type: "imports",
|
|
2532
|
-
import_source: ns,
|
|
2533
|
-
});
|
|
2534
|
-
}
|
|
2535
|
-
const phpExtends = line.match(/class\s+(\w+)\s+extends\s+(\w+)/);
|
|
2536
|
-
if (phpExtends?.[1] && phpExtends[2]) {
|
|
2537
|
-
edges.push({
|
|
2538
|
-
from_name: phpExtends[1],
|
|
2539
|
-
to_name: phpExtends[2],
|
|
2540
|
-
type: "extends",
|
|
2541
|
-
});
|
|
2542
|
-
}
|
|
2543
|
-
const phpImpl = line.match(/class\s+(\w+)\s+(?:extends\s+\w+\s+)?implements\s+(.+?)(?:\s*\{|$)/);
|
|
2544
|
-
if (phpImpl?.[1] && phpImpl[2]) {
|
|
2545
|
-
for (const iface of phpImpl[2].split(",")) {
|
|
2546
|
-
const trimmed = iface.trim();
|
|
2547
|
-
if (trimmed)
|
|
2548
|
-
edges.push({
|
|
2549
|
-
from_name: phpImpl[1],
|
|
2550
|
-
to_name: trimmed,
|
|
2551
|
-
type: "implements",
|
|
2552
|
-
});
|
|
2553
|
-
}
|
|
2554
|
-
}
|
|
2555
|
-
break;
|
|
2556
|
-
}
|
|
2557
|
-
case "kotlin": {
|
|
2558
|
-
const ktImport = line.match(/^\s*import\s+(\S+)/);
|
|
2559
|
-
if (ktImport?.[1]) {
|
|
2560
|
-
const path = ktImport[1];
|
|
2561
|
-
const leaf = path.split(".").pop() ?? path;
|
|
2562
|
-
if (leaf !== "*") {
|
|
2563
|
-
edges.push({
|
|
2564
|
-
from_name: "__file__",
|
|
2565
|
-
to_name: leaf,
|
|
2566
|
-
type: "imports",
|
|
2567
|
-
import_source: path,
|
|
2568
|
-
});
|
|
2569
|
-
}
|
|
2570
|
-
}
|
|
2571
|
-
const ktExtends = line.match(/class\s+(\w+)\s*(?:\([^)]*\))?\s*:\s*(\w+)/);
|
|
2572
|
-
if (ktExtends?.[1] && ktExtends[2]) {
|
|
2573
|
-
edges.push({
|
|
2574
|
-
from_name: ktExtends[1],
|
|
2575
|
-
to_name: ktExtends[2],
|
|
2576
|
-
type: "extends",
|
|
2577
|
-
});
|
|
2578
|
-
}
|
|
2579
|
-
break;
|
|
2580
|
-
}
|
|
2581
|
-
case "swift": {
|
|
2582
|
-
const swImport = line.match(/^\s*import\s+(\S+)/);
|
|
2583
|
-
if (swImport?.[1]) {
|
|
2584
|
-
const mod = swImport[1];
|
|
2585
|
-
edges.push({
|
|
2586
|
-
from_name: "__file__",
|
|
2587
|
-
to_name: mod,
|
|
2588
|
-
type: "imports",
|
|
2589
|
-
import_source: mod,
|
|
2590
|
-
});
|
|
2591
|
-
}
|
|
2592
|
-
const swExtends = line.match(/(?:class|struct|enum)\s+(\w+)\s*:\s*(\w+)/);
|
|
2593
|
-
if (swExtends?.[1] && swExtends[2]) {
|
|
2594
|
-
edges.push({
|
|
2595
|
-
from_name: swExtends[1],
|
|
2596
|
-
to_name: swExtends[2],
|
|
2597
|
-
type: "extends",
|
|
2598
|
-
});
|
|
2599
|
-
}
|
|
2600
|
-
break;
|
|
2601
|
-
}
|
|
2602
|
-
default:
|
|
2603
|
-
break;
|
|
2604
|
-
}
|
|
2605
|
-
}
|
|
2606
|
-
return edges;
|
|
2607
|
-
}
|
|
2608
|
-
// ── AST walker utility ──────────────────────────────────────────
|
|
2609
|
-
/** Depth-first walk of tree-sitter AST, calling visitor on each node. */
|
|
2610
|
-
function walkNodes(node, visitor) {
|
|
2611
|
-
visitor(node);
|
|
2612
|
-
for (const child of node.children) {
|
|
2613
|
-
walkNodes(child, visitor);
|
|
2614
|
-
}
|
|
2615
|
-
}
|