mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-04 02:31:36 +00:00
Merge branch 'main' into copilot/fix-analyze-hangs-on-root
This commit is contained in:
commit
614f3803d3
21 changed files with 1646 additions and 175 deletions
|
|
@ -17,11 +17,11 @@ npx gitnexus analyze
|
|||
|
||||
Run from the project root. This parses all source files, builds the knowledge graph, writes it to `.gitnexus/`, and generates CLAUDE.md / AGENTS.md context files.
|
||||
|
||||
| Flag | Effect |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------------- |
|
||||
| `--force` | Force full re-index even if up to date |
|
||||
| `--embeddings` | Enable embedding generation for semantic search (off by default) |
|
||||
| `--drop-embeddings` | Drop existing embeddings on rebuild. By default, an `analyze` without `--embeddings` preserves them. |
|
||||
| Flag | Effect |
|
||||
| -------------- | ---------------------------------------------------------------- |
|
||||
| `--force` | Force full re-index even if up to date |
|
||||
| `--embeddings` | Enable embedding generation for semantic search (off by default) |
|
||||
| `--drop-embeddings` | Drop existing embeddings on rebuild. By default, an `analyze` without `--embeddings` preserves them. |
|
||||
|
||||
**When to run:** First time in a project, after major code changes, or when `gitnexus://repo/{name}/context` reports the index is stale. In Claude Code, a PostToolUse hook detects staleness after `git commit` and `git merge` and notifies the agent to run `analyze` — the hook does not run analyze itself, to avoid blocking the agent for up to 120s and risking KuzuDB corruption on timeout.
|
||||
|
||||
|
|
|
|||
149
AGENTS.md
149
AGENTS.md
|
|
@ -62,131 +62,64 @@ Commands and gotchas live under **Repo reference** below and in **[CONTRIBUTING.
|
|||
<!-- gitnexus:start -->
|
||||
# GitNexus — Code Intelligence
|
||||
|
||||
Indexed as **GitNexus** (4325 symbols, 10556 relationships, 300 execution flows). Use MCP tools to understand code, assess impact, and navigate safely.
|
||||
This project is indexed by GitNexus as **GitNexus** (26675 symbols, 35395 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
|
||||
> If any tool warns the index is stale, run `npx gitnexus analyze` first.
|
||||
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
|
||||
|
||||
## Always Do
|
||||
|
||||
- **MUST run impact analysis before editing any symbol.** `gitnexus_impact({target: "symbolName", direction: "upstream"})` — report blast radius to the user.
|
||||
- **MUST run `gitnexus_detect_changes()` before committing** — verify only expected symbols and flows are affected.
|
||||
- **MUST warn the user** if impact returns HIGH or CRITICAL risk.
|
||||
- Explore unfamiliar code with `gitnexus_query({query: "concept"})` (process-grouped, ranked) instead of grepping.
|
||||
- Full context on a symbol: `gitnexus_context({name: "symbolName"})`.
|
||||
|
||||
## When Debugging
|
||||
|
||||
1. `gitnexus_query({query: "<error or symptom>"})` — find related execution flows
|
||||
2. `gitnexus_context({name: "<suspect function>"})` — callers, callees, process participation
|
||||
3. `READ gitnexus://repo/GitNexus/process/{processName}` — trace flow step by step
|
||||
4. Regressions: `gitnexus_detect_changes({scope: "compare", base_ref: "main"})`
|
||||
|
||||
## When Refactoring
|
||||
|
||||
- **Rename:** `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` first. Graph edits are safe; text_search edits need manual review.
|
||||
- **Extract/Split:** `gitnexus_context` (incoming/outgoing refs) then `gitnexus_impact` (upstream callers) before moving code.
|
||||
- **After any refactor:** `gitnexus_detect_changes({scope: "all"})` to verify scope.
|
||||
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
|
||||
- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows.
|
||||
- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
|
||||
- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
|
||||
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`.
|
||||
|
||||
## Never Do
|
||||
|
||||
- Edit a symbol without running `gitnexus_impact` first.
|
||||
- Ignore HIGH/CRITICAL risk warnings.
|
||||
- Rename with find-and-replace — use `gitnexus_rename`.
|
||||
- Commit without `gitnexus_detect_changes()`.
|
||||
- Add language-specific behavior to shared ingestion code (`gitnexus/src/core/ingestion/`) — use a `LanguageProvider` hook. Seeing `provider.mroStrategy === 'xxx'` or an import from `languages/xxx.ts` in shared code means stop and add a hook.
|
||||
|
||||
## Tools Quick Reference
|
||||
|
||||
| Tool | When to use | Example |
|
||||
|------|-------------|---------|
|
||||
| `list_repos` | Discover indexed repos | `gitnexus_list_repos({})` |
|
||||
| `query` | Find code by concept | `gitnexus_query({query: "auth validation"})` |
|
||||
| `context` | 360-degree view of one symbol | `gitnexus_context({name: "validateUser"})` |
|
||||
| `impact` | Blast radius before editing | `gitnexus_impact({target: "X", direction: "upstream"})` |
|
||||
| `detect_changes` | Pre-commit scope check | `gitnexus_detect_changes({scope: "staged"})` |
|
||||
| `rename` | Safe multi-file rename | `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` |
|
||||
| `cypher` | Custom graph queries | `gitnexus_cypher({query: "MATCH ..."})` |
|
||||
| `api_impact` | Pre-change API route impact | `gitnexus_api_impact({route: "/api/users", method: "GET"})` |
|
||||
| `route_map` | Route → handler → consumer map | `gitnexus_route_map({})` |
|
||||
| `tool_map` | MCP/RPC tool definitions | `gitnexus_tool_map({})` |
|
||||
| `shape_check` | Response shape vs consumer access | `gitnexus_shape_check({route: "/api/users"})` |
|
||||
| `group_list` | List repo groups | `gitnexus_group_list({})` |
|
||||
| `group_sync` | Rebuild group Contract Registry | `gitnexus_group_sync({name: "myGroup"})` |
|
||||
| `query` (group mode) | Cross-repo search in a group (RRF-merged) | `gitnexus_query({repo: "@myGroup", query: "auth"})` |
|
||||
| `context` (group mode) | 360° view across all member repos | `gitnexus_context({repo: "@myGroup", name: "validateUser"})` |
|
||||
| `impact` (group mode) | Cross-repo blast radius via Contract Bridge | `gitnexus_impact({repo: "@myGroup", target: "X", direction: "upstream"})` |
|
||||
|
||||
> Group mode: pass `repo: "@<groupName>"` to fan out across all member repos, or `repo: "@<groupName>/<memberPath>"` to target a single member (path keys from `group.yaml`). Optional `service: "<monorepo/path>"` filters by service root. Group-level state (contracts, staleness) lives in the resources table below — there are **no** `group_query` / `group_context` / `group_impact` / `group_contracts` / `group_status` MCP tools.
|
||||
>
|
||||
> For a full walkthrough of setting up a group across multiple repos that communicate over gRPC, see [docs/guides/microservices-grpc.md](docs/guides/microservices-grpc.md).
|
||||
|
||||
## Impact Risk Levels
|
||||
|
||||
| Depth | Meaning | Action |
|
||||
|-------|---------|--------|
|
||||
| d=1 | WILL BREAK — direct callers/importers | MUST update |
|
||||
| d=2 | LIKELY AFFECTED — indirect deps | Should test |
|
||||
| d=3 | MAY NEED TESTING — transitive | Test if critical path |
|
||||
- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
|
||||
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
|
||||
- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph.
|
||||
- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope.
|
||||
|
||||
## Resources
|
||||
|
||||
| Resource | Use for |
|
||||
|----------|---------|
|
||||
| `gitnexus://repo/GitNexus/context` | Codebase overview, index freshness |
|
||||
| `gitnexus://repo/GitNexus/context` | Codebase overview, check index freshness |
|
||||
| `gitnexus://repo/GitNexus/clusters` | All functional areas |
|
||||
| `gitnexus://repo/GitNexus/processes` | All execution flows |
|
||||
| `gitnexus://repo/GitNexus/process/{name}` | Step-by-step execution trace |
|
||||
| `gitnexus://group/{name}/contracts` | Group Contract Registry (provider/consumer rows + cross-links) |
|
||||
| `gitnexus://group/{name}/status` | Per-member index + Contract Registry staleness report |
|
||||
|
||||
## Self-Check Before Finishing
|
||||
## CLI
|
||||
|
||||
1. `gitnexus_impact` was run for all modified symbols
|
||||
2. No HIGH/CRITICAL warnings were ignored
|
||||
3. `gitnexus_detect_changes()` confirms expected scope
|
||||
4. All d=1 dependents were updated
|
||||
|
||||
## Keeping the Index Fresh
|
||||
|
||||
```bash
|
||||
npx gitnexus analyze # incremental by default; preserves embeddings
|
||||
npx gitnexus analyze --force # full rebuild from scratch (opt out of incremental)
|
||||
npx gitnexus analyze --embeddings # also generate embeddings for new/changed nodes
|
||||
npx gitnexus analyze --drop-embeddings # explicit opt-in to wipe existing embeddings
|
||||
```
|
||||
|
||||
`analyze` runs **incrementally by default**. The pipeline still parses every file every run (cross-file resolution requires it), but tree-sitter parsing is **served from a content-addressed cache** under `.gitnexus/parse-cache/` (per-chunk JSON shards plus `index.json`) for chunks whose file contents haven't changed since the last run. Older installs may still have a legacy single file `.gitnexus/parse-cache.json`, which is read for backward compatibility but no longer written. Only changed-file rows (and their importers) are rewritten in LadybugDB; unchanged-file rows are preserved. Output is byte-equivalent to a full rebuild. Pass `--force` to wipe and re-index from scratch (e.g., to recover from a corrupt index, or after upgrading GitNexus).
|
||||
|
||||
The parse cache key is **content-addressed and version-tagged**: it survives `--force` runs, and is automatically invalidated by a `gitnexus` package upgrade (so a new tree-sitter grammar doesn't silently replay stale parse output). Safe to delete the whole `.gitnexus/parse-cache/` directory (and remove any legacy `.gitnexus/parse-cache.json` if present) at any time — it'll be rebuilt on the next analyze.
|
||||
|
||||
Check `.gitnexus/meta.json` `stats.embeddings` (0 = none). A plain `analyze` no longer drops existing vectors — pass `--drop-embeddings` to wipe.
|
||||
|
||||
> Claude Code: PostToolUse hook detects a stale index after `git commit` and `git merge` and prompts the agent to run `analyze`. The hook does not invoke `analyze` itself.
|
||||
|
||||
## CLI Skills
|
||||
|
||||
| Task | Skill file |
|
||||
|------|-----------|
|
||||
| Architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` |
|
||||
| Blast radius / "What breaks?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` |
|
||||
| Debugging / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` |
|
||||
| Refactoring | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
|
||||
| Tools/resources/schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
|
||||
| CLI commands (index, status, clean, wiki) | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
|
||||
|
||||
## Hook env knobs
|
||||
|
||||
The Claude Code hook (`gitnexus/hooks/claude/gitnexus-hook.cjs` and the mirrored plugin copy under `gitnexus-claude-plugin/hooks/`) honours these env vars. Defaults work for normal installations; set them only to override resolution. All path overrides ignore values that do not exist on disk and fall through to the standard resolution chain.
|
||||
|
||||
| Env var | Type | Default | Purpose |
|
||||
|---------|------|---------|---------|
|
||||
| `GITNEXUS_HOOK_CLI_PATH` | path | resolved via package layout / `require.resolve` | Override path to the `gitnexus` CLI entry the hook spawns for `augment`. |
|
||||
| `GITNEXUS_HOOK_LSOF_PATH` | path | `lsof` on `PATH` (with `/usr/bin/lsof`, `/usr/sbin/lsof`, `/sbin/lsof` fallbacks) | Override POSIX `lsof` location for the DB-lock probe. |
|
||||
| `GITNEXUS_HOOK_PS_PATH` | path | `ps` on `PATH` (with `/bin/ps`, `/usr/bin/ps` fallbacks) | Override POSIX `ps` location. |
|
||||
| `GITNEXUS_HOOK_POWERSHELL_PATH` | path | `%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe` (then `SysWOW64`, then `powershell.exe` on `PATH`) | Override Windows PowerShell location used by the Restart-Manager probe. |
|
||||
| `GITNEXUS_HOOK_LINUX_PROC_BUDGET_MS` | integer ms | `1200` | Max wall-clock for the Linux `/proc` fd scan before bailing out to the `lsof` fallback. |
|
||||
| `GITNEXUS_HOOK_RM_TARGET` | path | derived | Restart-Manager target file (the LadybugDB path under `.gitnexus/`). Set internally by the hook; rarely overridden manually. |
|
||||
| `GITNEXUS_DEBUG` | boolean (`1`/`true`) | unset | Verbose stderr from the hook: prints discarded augment-stderr prefixes and one-shot `.ps1` load-failure warnings. |
|
||||
| Task | Read this skill file |
|
||||
|------|---------------------|
|
||||
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` |
|
||||
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` |
|
||||
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` |
|
||||
| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
|
||||
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
|
||||
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
|
||||
| Work in the Ingestion area (239 symbols) | `.claude/skills/generated/ingestion/SKILL.md` |
|
||||
| Work in the Extractors area (135 symbols) | `.claude/skills/generated/extractors/SKILL.md` |
|
||||
| Work in the Components area (112 symbols) | `.claude/skills/generated/components/SKILL.md` |
|
||||
| Work in the Lbug area (96 symbols) | `.claude/skills/generated/lbug/SKILL.md` |
|
||||
| Work in the Group area (94 symbols) | `.claude/skills/generated/group/SKILL.md` |
|
||||
| Work in the Cli area (92 symbols) | `.claude/skills/generated/cli/SKILL.md` |
|
||||
| Work in the Configs area (92 symbols) | `.claude/skills/generated/configs/SKILL.md` |
|
||||
| Work in the Type-extractors area (90 symbols) | `.claude/skills/generated/type-extractors/SKILL.md` |
|
||||
| Work in the Hooks area (88 symbols) | `.claude/skills/generated/hooks/SKILL.md` |
|
||||
| Work in the Unit area (80 symbols) | `.claude/skills/generated/unit/SKILL.md` |
|
||||
| Work in the Cpp area (73 symbols) | `.claude/skills/generated/cpp/SKILL.md` |
|
||||
| Work in the Scope-resolution area (72 symbols) | `.claude/skills/generated/scope-resolution/SKILL.md` |
|
||||
| Work in the Server area (66 symbols) | `.claude/skills/generated/server/SKILL.md` |
|
||||
| Work in the Local area (61 symbols) | `.claude/skills/generated/local/SKILL.md` |
|
||||
| Work in the Wiki area (60 symbols) | `.claude/skills/generated/wiki/SKILL.md` |
|
||||
| Work in the Workers area (57 symbols) | `.claude/skills/generated/workers/SKILL.md` |
|
||||
| Work in the Embeddings area (56 symbols) | `.claude/skills/generated/embeddings/SKILL.md` |
|
||||
| Work in the Typescript area (53 symbols) | `.claude/skills/generated/typescript/SKILL.md` |
|
||||
| Work in the Storage area (51 symbols) | `.claude/skills/generated/storage/SKILL.md` |
|
||||
| Work in the Php area (48 symbols) | `.claude/skills/generated/php/SKILL.md` |
|
||||
|
||||
<!-- gitnexus:end -->
|
||||
|
||||
|
|
|
|||
64
CLAUDE.md
64
CLAUDE.md
|
|
@ -52,3 +52,67 @@ If always-on instructions grow, load deep conventions via conditional reads (e.g
|
|||
## GitNexus rules
|
||||
|
||||
See the `<!-- gitnexus:start --> … <!-- gitnexus:end -->` block in **[AGENTS.md](AGENTS.md)** for the canonical MCP tools, impact analysis rules, and index instructions.
|
||||
|
||||
<!-- gitnexus:start -->
|
||||
# GitNexus — Code Intelligence
|
||||
|
||||
This project is indexed by GitNexus as **GitNexus** (26675 symbols, 35395 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
|
||||
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
|
||||
|
||||
## Always Do
|
||||
|
||||
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
|
||||
- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows.
|
||||
- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
|
||||
- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
|
||||
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`.
|
||||
|
||||
## Never Do
|
||||
|
||||
- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
|
||||
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
|
||||
- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph.
|
||||
- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope.
|
||||
|
||||
## Resources
|
||||
|
||||
| Resource | Use for |
|
||||
|----------|---------|
|
||||
| `gitnexus://repo/GitNexus/context` | Codebase overview, check index freshness |
|
||||
| `gitnexus://repo/GitNexus/clusters` | All functional areas |
|
||||
| `gitnexus://repo/GitNexus/processes` | All execution flows |
|
||||
| `gitnexus://repo/GitNexus/process/{name}` | Step-by-step execution trace |
|
||||
|
||||
## CLI
|
||||
|
||||
| Task | Read this skill file |
|
||||
|------|---------------------|
|
||||
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` |
|
||||
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` |
|
||||
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` |
|
||||
| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
|
||||
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
|
||||
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
|
||||
| Work in the Ingestion area (239 symbols) | `.claude/skills/generated/ingestion/SKILL.md` |
|
||||
| Work in the Extractors area (135 symbols) | `.claude/skills/generated/extractors/SKILL.md` |
|
||||
| Work in the Components area (112 symbols) | `.claude/skills/generated/components/SKILL.md` |
|
||||
| Work in the Lbug area (96 symbols) | `.claude/skills/generated/lbug/SKILL.md` |
|
||||
| Work in the Group area (94 symbols) | `.claude/skills/generated/group/SKILL.md` |
|
||||
| Work in the Cli area (92 symbols) | `.claude/skills/generated/cli/SKILL.md` |
|
||||
| Work in the Configs area (92 symbols) | `.claude/skills/generated/configs/SKILL.md` |
|
||||
| Work in the Type-extractors area (90 symbols) | `.claude/skills/generated/type-extractors/SKILL.md` |
|
||||
| Work in the Hooks area (88 symbols) | `.claude/skills/generated/hooks/SKILL.md` |
|
||||
| Work in the Unit area (80 symbols) | `.claude/skills/generated/unit/SKILL.md` |
|
||||
| Work in the Cpp area (73 symbols) | `.claude/skills/generated/cpp/SKILL.md` |
|
||||
| Work in the Scope-resolution area (72 symbols) | `.claude/skills/generated/scope-resolution/SKILL.md` |
|
||||
| Work in the Server area (66 symbols) | `.claude/skills/generated/server/SKILL.md` |
|
||||
| Work in the Local area (61 symbols) | `.claude/skills/generated/local/SKILL.md` |
|
||||
| Work in the Wiki area (60 symbols) | `.claude/skills/generated/wiki/SKILL.md` |
|
||||
| Work in the Workers area (57 symbols) | `.claude/skills/generated/workers/SKILL.md` |
|
||||
| Work in the Embeddings area (56 symbols) | `.claude/skills/generated/embeddings/SKILL.md` |
|
||||
| Work in the Typescript area (53 symbols) | `.claude/skills/generated/typescript/SKILL.md` |
|
||||
| Work in the Storage area (51 symbols) | `.claude/skills/generated/storage/SKILL.md` |
|
||||
| Work in the Php area (48 symbols) | `.claude/skills/generated/php/SKILL.md` |
|
||||
|
||||
<!-- gitnexus:end -->
|
||||
|
|
|
|||
30
gitnexus-web/package-lock.json
generated
30
gitnexus-web/package-lock.json
generated
|
|
@ -41,7 +41,7 @@
|
|||
"sigma": "^3.0.2",
|
||||
"tailwindcss": "^4.2.4",
|
||||
"uuid": "^14.0.0",
|
||||
"zod": "^3.25.76"
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.29.0",
|
||||
|
|
@ -5599,13 +5599,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/langsmith": {
|
||||
"version": "0.5.23",
|
||||
"resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.5.23.tgz",
|
||||
"integrity": "sha512-dE/M/2Gg2S2R8ygDdkWGJVO3JstijvsNvPXsy9V8WGbpb88Zn8xF/aTjPx4mIy5gIoo02T6FssOgYyLf51Dv1Q==",
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.6.3.tgz",
|
||||
"integrity": "sha512-pXrQ4/4myQvjFFOAUmt5pWRrLEZR20gzIJD7MNdUH+5/S5nLI4ZRBo/SYKC6coaYj9pYTfQdBIzcs+3kfJ5uDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-queue": "6.6.2",
|
||||
"uuid": "10.0.0"
|
||||
"p-queue": "6.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "*",
|
||||
|
|
@ -5632,19 +5631,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/langsmith/node_modules/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/layout-base": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz",
|
||||
|
|
@ -8903,9 +8889,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@
|
|||
"sigma": "^3.0.2",
|
||||
"tailwindcss": "^4.2.4",
|
||||
"uuid": "^14.0.0",
|
||||
"zod": "^3.25.76"
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.29.0",
|
||||
|
|
|
|||
52
gitnexus/package-lock.json
generated
52
gitnexus/package-lock.json
generated
|
|
@ -2065,12 +2065,12 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz",
|
||||
"integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==",
|
||||
"version": "25.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz",
|
||||
"integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.19.0"
|
||||
"undici-types": "~7.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
|
|
@ -3059,9 +3059,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.5.1",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.1.tgz",
|
||||
"integrity": "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==",
|
||||
"version": "8.5.2",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz",
|
||||
"integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "^10.2.0"
|
||||
|
|
@ -3284,19 +3284,6 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.13.7",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz",
|
||||
"integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/gitnexus-shared": {
|
||||
"resolved": "../gitnexus-shared",
|
||||
"link": true
|
||||
|
|
@ -4692,16 +4679,6 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz",
|
||||
|
|
@ -5492,14 +5469,13 @@
|
|||
"optional": true
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||
"version": "4.21.1",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.1.tgz",
|
||||
"integrity": "sha512-5QE2Q04cN1u0993w0LT5rPw3faZqZU1fFn1mGE0pV53N1Dn7c+QFFxQu1mBeSgeOXwFyTicZw02wVgp3Tb5cAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.27.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
"esbuild": "~0.27.0"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
|
|
@ -5551,9 +5527,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.19.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
|
||||
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
|
||||
"version": "7.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz",
|
||||
"integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
|
|
|
|||
12
gitnexus/src/core/ingestion/languages/javascript/arity.ts
Normal file
12
gitnexus/src/core/ingestion/languages/javascript/arity.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* Arity compatibility for JavaScript.
|
||||
*
|
||||
* Delegates to `typescriptArityCompatibility` unchanged — JavaScript
|
||||
* supports the same arity constructs (rest parameters `...args`, default
|
||||
* parameters `p = v`) and the metadata shape (`parameterCount`,
|
||||
* `requiredParameterCount`, `parameterTypes`) is synthesized by the same
|
||||
* `computeTsArityMetadata` function (which understands both TS and JS
|
||||
* parameter node types via `extractTsJsParameters`).
|
||||
*/
|
||||
|
||||
export { typescriptArityCompatibility as jsArityCompatibility } from '../typescript/arity.js';
|
||||
722
gitnexus/src/core/ingestion/languages/javascript/captures.ts
Normal file
722
gitnexus/src/core/ingestion/languages/javascript/captures.ts
Normal file
|
|
@ -0,0 +1,722 @@
|
|||
/**
|
||||
* `emitScopeCaptures` for JavaScript.
|
||||
*
|
||||
* Adapts `emitTsScopeCaptures` for the JavaScript grammar:
|
||||
*
|
||||
* 1. **JS grammar** — uses `tree-sitter-javascript` instead of
|
||||
* `tree-sitter-typescript`. The JS scope query is a subset of the
|
||||
* TypeScript one (TypeScript-only node types dropped).
|
||||
*
|
||||
* 2. **CJS `require()` decomposition** — `const { X } = require('./m')`
|
||||
* and `const X = require('./m')` are walked in a post-query pass and
|
||||
* synthesized as `@import.kind/name/alias/source` markers so that
|
||||
* `interpretJsImport` can recover a `ParsedImport` using the same
|
||||
* shape as the TypeScript ESM decomposer.
|
||||
*
|
||||
* 3. **JSDoc type bindings** — JavaScript has no static type annotations
|
||||
* so `@type-binding.parameter` / `@type-binding.return` must be
|
||||
* inferred from leading JSDoc comments. A lightweight regex scanner
|
||||
* (`parseJsDocParams` / `parseJsDocReturn`) extracts `@param {T} n`
|
||||
* and `@returns {T}` tags and emits synthetic captures positioned on
|
||||
* the annotated function node.
|
||||
*
|
||||
* 4. **Shared synthesis passes** — destructuring, for-of map-tuple, and
|
||||
* instanceof narrowing passes are duplicated from `typescript/captures.ts`
|
||||
* (they are pure AST operations with no grammar-specific logic).
|
||||
*
|
||||
* Pure given the input source text. No I/O, no globals consulted.
|
||||
*/
|
||||
|
||||
import type { Capture, CaptureMatch } from 'gitnexus-shared';
|
||||
import {
|
||||
findNodeAtRange,
|
||||
nodeToCapture,
|
||||
syntheticCapture,
|
||||
type SyntaxNode,
|
||||
} from '../../utils/ast-helpers.js';
|
||||
import { splitImportStatement } from '../typescript/import-decomposer.js';
|
||||
import { getJsParser, getJsScopeQuery, jsCachedTreeMatchesGrammar } from './query.js';
|
||||
import { computeTsArityMetadata } from '../typescript/arity-metadata.js';
|
||||
import { synthesizeTsReceiverBinding } from '../typescript/receiver-binding.js';
|
||||
import { getTreeSitterBufferSize } from '../../constants.js';
|
||||
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
||||
|
||||
/** JS function-like node types that may carry a synthesized `this` binding.
|
||||
* Kept in sync with the `@scope.function` patterns in `query.ts`. */
|
||||
const FUNCTION_NODE_TYPES = [
|
||||
'method_definition',
|
||||
'arrow_function',
|
||||
'function_expression',
|
||||
'function_declaration',
|
||||
'generator_function_declaration',
|
||||
] as const;
|
||||
|
||||
/** Declaration anchors that carry function-like arity metadata. */
|
||||
const FUNCTION_DECL_TAGS = ['@declaration.method', '@declaration.function'] as const;
|
||||
|
||||
/** Callsite anchors that should carry `@reference.arity` + param types. */
|
||||
const CALL_TAGS = [
|
||||
'@reference.call.free',
|
||||
'@reference.call.member',
|
||||
'@reference.call.constructor',
|
||||
] as const;
|
||||
|
||||
function pickFirstDefined(grouped: CaptureMatch, tags: readonly string[]): Capture | undefined {
|
||||
for (const tag of tags) {
|
||||
const cap = grouped[tag];
|
||||
if (cap !== undefined) return cap;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Filter `@reference.read.member` in non-read contexts (same logic as TS). */
|
||||
function shouldEmitReadMember(memberNode: SyntaxNode): boolean {
|
||||
const parent = memberNode.parent;
|
||||
if (parent === null) return true;
|
||||
switch (parent.type) {
|
||||
case 'call_expression':
|
||||
return parent.childForFieldName('function')?.id !== memberNode.id;
|
||||
case 'new_expression':
|
||||
return parent.childForFieldName('constructor')?.id !== memberNode.id;
|
||||
case 'assignment_expression':
|
||||
case 'augmented_assignment_expression':
|
||||
return parent.childForFieldName('left')?.id !== memberNode.id;
|
||||
case 'jsx_self_closing_element':
|
||||
case 'jsx_opening_element':
|
||||
return parent.childForFieldName('name')?.id !== memberNode.id;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/** Find the first JS function-like node at the given range. */
|
||||
function findFunctionNode(rootNode: SyntaxNode, range: Capture['range']): SyntaxNode | null {
|
||||
for (const nodeType of FUNCTION_NODE_TYPES) {
|
||||
const n = findNodeAtRange(rootNode, range, nodeType);
|
||||
if (n !== null) return n;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Infer a callsite argument's static type from literal shapes. */
|
||||
function inferArgType(argNode: SyntaxNode): string {
|
||||
switch (argNode.type) {
|
||||
case 'number':
|
||||
return 'number';
|
||||
case 'string':
|
||||
case 'template_string':
|
||||
return 'string';
|
||||
case 'true':
|
||||
case 'false':
|
||||
return 'boolean';
|
||||
case 'null':
|
||||
return 'null';
|
||||
case 'undefined':
|
||||
return 'undefined';
|
||||
case 'array':
|
||||
return 'Array';
|
||||
case 'object':
|
||||
return 'object';
|
||||
case 'regex':
|
||||
return 'RegExp';
|
||||
case 'new_expression': {
|
||||
const ctor = argNode.childForFieldName('constructor');
|
||||
return ctor?.text ?? '';
|
||||
}
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// ─── CJS require() decomposition ─────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Walk the AST and synthesize `@import.*` captures for CJS `require()` calls:
|
||||
*
|
||||
* - `const { X, Y } = require('./m')` → one match per destructured name,
|
||||
* `@import.kind = 'named'`, `@import.name = X / Y`.
|
||||
* - `const X = require('./m')` → `@import.kind = 'namespace'`,
|
||||
* `@import.alias = X` (the whole module is bound to X).
|
||||
* - `require('./m')` as a bare expression-statement → side-effect.
|
||||
*
|
||||
* CJS named-alias form (`const { X: alias } = require('./m')`) emits
|
||||
* `@import.kind = 'named-alias'` with `@import.name = X` and
|
||||
* `@import.alias = alias`.
|
||||
*
|
||||
* The synthesized markers are identical to those produced by
|
||||
* `splitImportStatement` for ESM, so `interpretJsImport` can delegate
|
||||
* unchanged to `interpretTsImport` for all cases.
|
||||
*/
|
||||
function synthesizeCjsImports(root: SyntaxNode, out: CaptureMatch[]): void {
|
||||
const stack: SyntaxNode[] = [root];
|
||||
for (;;) {
|
||||
const node = stack.pop();
|
||||
if (node === undefined) break;
|
||||
for (const child of node.namedChildren) {
|
||||
if (child !== null) stack.push(child);
|
||||
}
|
||||
|
||||
if (node.type !== 'call_expression') continue;
|
||||
|
||||
// Require call: function must be bare identifier "require".
|
||||
const fn = node.childForFieldName('function');
|
||||
if (fn === null || fn.type !== 'identifier' || fn.text !== 'require') continue;
|
||||
|
||||
const argsNode = node.childForFieldName('arguments');
|
||||
if (argsNode === null) continue;
|
||||
|
||||
// Source must be a string literal.
|
||||
const firstArg = argsNode.namedChild(0);
|
||||
if (firstArg === null || firstArg.type !== 'string') continue;
|
||||
const rawSource = firstArg.text; // includes surrounding quotes
|
||||
const source = firstArg.namedChild(0)?.text ?? rawSource.slice(1, -1);
|
||||
|
||||
const parent = node.parent;
|
||||
|
||||
// Case 1: const { X } = require('./m') OR const X = require('./m')
|
||||
if (parent?.type === 'variable_declarator') {
|
||||
const nameNode = parent.childForFieldName('name');
|
||||
if (nameNode === null) continue;
|
||||
|
||||
if (nameNode.type === 'object_pattern') {
|
||||
// Destructured: emit one match per specifier.
|
||||
for (const field of nameNode.namedChildren) {
|
||||
if (field === null) continue;
|
||||
if (field.type === 'shorthand_property_identifier_pattern') {
|
||||
const name = field.text;
|
||||
out.push({
|
||||
'@import.statement': syntheticCapture('@import.statement', node, rawSource),
|
||||
'@import.kind': syntheticCapture('@import.kind', node, 'named'),
|
||||
'@import.name': syntheticCapture('@import.name', field, name),
|
||||
'@import.source': syntheticCapture('@import.source', firstArg, source),
|
||||
});
|
||||
} else if (field.type === 'pair_pattern') {
|
||||
const key = field.childForFieldName('key');
|
||||
const value = field.childForFieldName('value');
|
||||
if (key === null || value === null || value.type !== 'identifier') continue;
|
||||
out.push({
|
||||
'@import.statement': syntheticCapture('@import.statement', node, rawSource),
|
||||
'@import.kind': syntheticCapture('@import.kind', node, 'named-alias'),
|
||||
'@import.name': syntheticCapture('@import.name', key, key.text),
|
||||
'@import.alias': syntheticCapture('@import.alias', value, value.text),
|
||||
'@import.source': syntheticCapture('@import.source', firstArg, source),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (nameNode.type === 'identifier') {
|
||||
// Namespace-style: const X = require('./m') → bind whole module to X.
|
||||
out.push({
|
||||
'@import.statement': syntheticCapture('@import.statement', node, rawSource),
|
||||
'@import.kind': syntheticCapture('@import.kind', node, 'namespace'),
|
||||
'@import.alias': syntheticCapture('@import.alias', nameNode, nameNode.text),
|
||||
'@import.source': syntheticCapture('@import.source', firstArg, source),
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Case 2: bare require('./m') — side-effect import.
|
||||
if (parent?.type === 'expression_statement') {
|
||||
out.push({
|
||||
'@import.statement': syntheticCapture('@import.statement', node, rawSource),
|
||||
'@import.kind': syntheticCapture('@import.kind', node, 'side-effect'),
|
||||
'@import.source': syntheticCapture('@import.source', firstArg, source),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── JSDoc type binding synthesis ────────────────────────────────────────
|
||||
|
||||
interface JsDocParam {
|
||||
readonly name: string;
|
||||
readonly type: string;
|
||||
}
|
||||
|
||||
/** Extract `@param {Type} name` entries from a JSDoc comment block. */
|
||||
function parseJsDocParams(text: string): readonly JsDocParam[] {
|
||||
const results: JsDocParam[] = [];
|
||||
// Match @param {Type} name or @param {Type} [name] (optional)
|
||||
const re = /@param\s+\{([^}]+)\}\s+\[?(\w+)\]?/g;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = re.exec(text)) !== null) {
|
||||
results.push({ type: m[1].trim(), name: m[2].trim() });
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/** Extract `@returns {Type}` or `@return {Type}` from a JSDoc comment. */
|
||||
function parseJsDocReturn(text: string): string | null {
|
||||
const m = /@returns?\s+\{([^}]+)\}/.exec(text);
|
||||
return m ? m[1].trim() : null;
|
||||
}
|
||||
|
||||
/** Extract `@type {Type}` from a JSDoc comment (variable-level annotation). */
|
||||
function parseJsDocType(text: string): string | null {
|
||||
const m = /@type\s+\{([^}]+)\}/.exec(text);
|
||||
return m ? m[1].trim() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk the AST and synthesize `@type-binding.*` captures from JSDoc
|
||||
* comments immediately preceding function declarations / expressions.
|
||||
*
|
||||
* Only `/** … */` block comments are scanned. Line comments (`//`) are
|
||||
* intentionally excluded — JSDoc lives in block comments.
|
||||
*
|
||||
* Emits:
|
||||
* - `@type-binding.parameter` for each `@param {T} n` tag.
|
||||
* - `@type-binding.return` for `@returns {T}` / `@return {T}`.
|
||||
* - `@type-binding.annotation` for `@type {T}` on `let`/`const`/`var`
|
||||
* declarations — covers the common `/** @type {User} */ const u = …`
|
||||
* pattern (ECMA-262 §14.3.1/§14.3.2 variable declarations).
|
||||
*
|
||||
* The binding is anchored on the function node so `tsBindingScopeFor`
|
||||
* can hoist method return-type bindings to Module scope (matching the
|
||||
* TypeScript path where `hoistTypeBindingsToModule: true`).
|
||||
*/
|
||||
function synthesizeJsDocBindings(root: SyntaxNode, out: CaptureMatch[]): void {
|
||||
const stack: SyntaxNode[] = [root];
|
||||
for (;;) {
|
||||
const node = stack.pop();
|
||||
if (node === undefined) break;
|
||||
for (const child of node.namedChildren) {
|
||||
if (child !== null) stack.push(child);
|
||||
}
|
||||
|
||||
const isFnDecl =
|
||||
node.type === 'function_declaration' || node.type === 'generator_function_declaration';
|
||||
const isMethodDef = node.type === 'method_definition';
|
||||
// Also check lexical_declaration containing an arrow/fn-expression
|
||||
const isLexDecl = node.type === 'lexical_declaration' || node.type === 'variable_declaration';
|
||||
|
||||
if (!isFnDecl && !isMethodDef && !isLexDecl) continue;
|
||||
|
||||
// For `export function foo() { ... }`, the JSDoc comment precedes the
|
||||
// wrapping export_statement, not the inner function_declaration.
|
||||
// Walk up to the export_statement so the preceding-sibling search finds it.
|
||||
const lookupNode =
|
||||
(isFnDecl || isLexDecl) && node.parent?.type === 'export_statement' ? node.parent : node;
|
||||
|
||||
// Find the preceding sibling comment.
|
||||
let sibling = lookupNode.previousNamedSibling;
|
||||
while (sibling !== null && sibling.type === 'comment') {
|
||||
const text = sibling.text;
|
||||
if (text.startsWith('/**')) {
|
||||
// Found a JSDoc block.
|
||||
const params = parseJsDocParams(text);
|
||||
const retType = parseJsDocReturn(text);
|
||||
const varType = isLexDecl ? parseJsDocType(text) : null;
|
||||
|
||||
// Determine the anchor node (the function-like node, for hoisting).
|
||||
const anchor = node;
|
||||
|
||||
for (const p of params) {
|
||||
out.push({
|
||||
'@type-binding.name': syntheticCapture('@type-binding.name', anchor, p.name),
|
||||
'@type-binding.type': syntheticCapture('@type-binding.type', anchor, p.type),
|
||||
'@type-binding.parameter': syntheticCapture('@type-binding.parameter', anchor, '1'),
|
||||
});
|
||||
}
|
||||
|
||||
if (retType !== null) {
|
||||
// For named functions, use the function name as the binding name so
|
||||
// `hoistTypeBindingsToModule` knows which function's return type this is.
|
||||
let fnName: string | null = null;
|
||||
if (isFnDecl) {
|
||||
fnName = node.childForFieldName('name')?.text ?? null;
|
||||
} else if (isMethodDef) {
|
||||
// method_definition uses `name:` field for the method name
|
||||
const nameNode = node.childForFieldName('name');
|
||||
if (nameNode?.type === 'property_identifier') fnName = nameNode.text;
|
||||
} else if (isLexDecl) {
|
||||
const declarator = node.namedChild(0);
|
||||
const nameNode = declarator?.childForFieldName('name');
|
||||
if (nameNode?.type === 'identifier') fnName = nameNode.text;
|
||||
}
|
||||
if (fnName !== null) {
|
||||
out.push({
|
||||
'@type-binding.name': syntheticCapture('@type-binding.name', anchor, fnName),
|
||||
'@type-binding.type': syntheticCapture('@type-binding.type', anchor, retType),
|
||||
'@type-binding.return': syntheticCapture('@type-binding.return', anchor, '1'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// @type {T} on let/const/var: `/** @type {User} */ const u = getUser()`.
|
||||
// Emits annotation-strength binding (source = 'annotation') so it
|
||||
// overrides any weaker constructor/alias inference on the same name.
|
||||
if (varType !== null) {
|
||||
for (const declarator of node.namedChildren) {
|
||||
if (declarator === null || declarator.type !== 'variable_declarator') continue;
|
||||
const nameNode = declarator.childForFieldName('name');
|
||||
if (nameNode === null || nameNode.type !== 'identifier') continue;
|
||||
out.push({
|
||||
'@type-binding.name': syntheticCapture('@type-binding.name', nameNode, nameNode.text),
|
||||
'@type-binding.type': syntheticCapture('@type-binding.type', nameNode, varType),
|
||||
'@type-binding.annotation': syntheticCapture(
|
||||
'@type-binding.annotation',
|
||||
nameNode,
|
||||
'1',
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
sibling = sibling.previousNamedSibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Destructuring / for-of / instanceof (shared with TS captures) ───────
|
||||
|
||||
function synthesizeDestructuringBindings(root: SyntaxNode, out: CaptureMatch[]): void {
|
||||
const stack: SyntaxNode[] = [root];
|
||||
for (;;) {
|
||||
const node = stack.pop();
|
||||
if (node === undefined) break;
|
||||
for (const child of node.namedChildren) {
|
||||
if (child !== null) stack.push(child);
|
||||
}
|
||||
if (node.type !== 'variable_declarator') continue;
|
||||
const nameNode = node.childForFieldName('name');
|
||||
const valueNode = node.childForFieldName('value');
|
||||
if (nameNode === null || valueNode === null) continue;
|
||||
if (nameNode.type !== 'object_pattern') continue;
|
||||
if (valueNode.type !== 'identifier') continue;
|
||||
const rhsName = valueNode.text;
|
||||
for (const fieldNode of nameNode.namedChildren) {
|
||||
if (fieldNode === null) continue;
|
||||
if (fieldNode.type === 'shorthand_property_identifier_pattern') {
|
||||
const localName = fieldNode.text;
|
||||
out.push({
|
||||
'@type-binding.name': syntheticCapture('@type-binding.name', fieldNode, localName),
|
||||
'@type-binding.type': syntheticCapture(
|
||||
'@type-binding.type',
|
||||
fieldNode,
|
||||
`${rhsName}.${localName}`,
|
||||
),
|
||||
'@type-binding.destructured': syntheticCapture(
|
||||
'@type-binding.destructured',
|
||||
fieldNode,
|
||||
fieldNode.text,
|
||||
),
|
||||
});
|
||||
} else if (fieldNode.type === 'pair_pattern') {
|
||||
const key = fieldNode.childForFieldName('key');
|
||||
const value = fieldNode.childForFieldName('value');
|
||||
if (key === null || value === null || value.type !== 'identifier') continue;
|
||||
const fieldName = key.text;
|
||||
const localName = value.text;
|
||||
out.push({
|
||||
'@type-binding.name': syntheticCapture('@type-binding.name', value, localName),
|
||||
'@type-binding.type': syntheticCapture(
|
||||
'@type-binding.type',
|
||||
fieldNode,
|
||||
`${rhsName}.${fieldName}`,
|
||||
),
|
||||
'@type-binding.destructured': syntheticCapture(
|
||||
'@type-binding.destructured',
|
||||
fieldNode,
|
||||
fieldNode.text,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function synthesizeForOfMapTupleBindings(root: SyntaxNode, out: CaptureMatch[]): void {
|
||||
const stack: SyntaxNode[] = [root];
|
||||
for (;;) {
|
||||
const node = stack.pop();
|
||||
if (node === undefined) break;
|
||||
for (const child of node.namedChildren) {
|
||||
if (child !== null) stack.push(child);
|
||||
}
|
||||
if (node.type !== 'for_in_statement') continue;
|
||||
const left = node.childForFieldName('left');
|
||||
const right = node.childForFieldName('right');
|
||||
if (left === null || right === null) continue;
|
||||
if (left.type !== 'array_pattern' || right.type !== 'identifier') continue;
|
||||
const rhs = right.text;
|
||||
let slot = 0;
|
||||
for (const child of left.namedChildren) {
|
||||
if (child === null || child.type !== 'identifier') continue;
|
||||
const localName = child.text;
|
||||
out.push({
|
||||
'@type-binding.name': syntheticCapture('@type-binding.name', child, localName),
|
||||
'@type-binding.type': syntheticCapture(
|
||||
'@type-binding.type',
|
||||
child,
|
||||
`__MAP_TUPLE_${slot}__:${rhs}`,
|
||||
),
|
||||
'@type-binding.map-tuple-entry': syntheticCapture(
|
||||
'@type-binding.map-tuple-entry',
|
||||
child,
|
||||
String(slot),
|
||||
),
|
||||
});
|
||||
slot++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function synthesizeInstanceofNarrowings(root: SyntaxNode, out: CaptureMatch[]): void {
|
||||
const stack: SyntaxNode[] = [root];
|
||||
for (;;) {
|
||||
const node = stack.pop();
|
||||
if (node === undefined) break;
|
||||
for (const child of node.namedChildren) {
|
||||
if (child !== null) stack.push(child);
|
||||
}
|
||||
if (node.type !== 'if_statement') continue;
|
||||
const cond = node.childForFieldName('condition');
|
||||
if (cond === null) continue;
|
||||
const inner = cond.type === 'parenthesized_expression' ? cond.namedChildren[0] : cond;
|
||||
if (inner === null || inner.type !== 'binary_expression') continue;
|
||||
const op = inner.childForFieldName('operator');
|
||||
const left = inner.childForFieldName('left');
|
||||
const right = inner.childForFieldName('right');
|
||||
if (op === null || left === null || right === null) continue;
|
||||
if (op.type !== 'instanceof') continue;
|
||||
if (left.type !== 'identifier') continue;
|
||||
if (right.type !== 'identifier') continue;
|
||||
const varName = left.text;
|
||||
const typeName = right.text;
|
||||
const cons = node.childForFieldName('consequence');
|
||||
if (cons === null) continue;
|
||||
out.push({
|
||||
'@type-binding.name': syntheticCapture('@type-binding.name', cons, varName),
|
||||
'@type-binding.type': syntheticCapture('@type-binding.type', right, typeName),
|
||||
'@type-binding.instanceof-narrow': syntheticCapture(
|
||||
'@type-binding.instanceof-narrow',
|
||||
cons,
|
||||
'1',
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Constructor field type bindings ─────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Synthesize class-scope type bindings from `this.X = new Y()` assignments
|
||||
* inside constructor method bodies. Covers the traditional ES5+ OOP pattern:
|
||||
*
|
||||
* class User {
|
||||
* constructor() {
|
||||
* /** @type {Address} *\/
|
||||
* this.address = new Address();
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* The emitted `@type-binding.class-field` is hoisted to the Class scope by
|
||||
* `tsBindingScopeFor` so that compound-receiver resolution can look up
|
||||
* `User.address → Address` when resolving `user.address.save()`.
|
||||
*
|
||||
* Type source priority:
|
||||
* 1. JSDoc `@type {T}` comment immediately preceding the statement
|
||||
* 2. `new Y()` constructor inference
|
||||
*/
|
||||
function synthesizeConstructorFieldBindings(root: SyntaxNode, out: CaptureMatch[]): void {
|
||||
const stack: SyntaxNode[] = [root];
|
||||
for (;;) {
|
||||
const node = stack.pop();
|
||||
if (node === undefined) break;
|
||||
for (const child of node.namedChildren) {
|
||||
if (child !== null) stack.push(child);
|
||||
}
|
||||
// Only process constructor method definitions
|
||||
if (node.type !== 'method_definition') continue;
|
||||
const nameNode = node.childForFieldName('name');
|
||||
if (nameNode?.text !== 'constructor') continue;
|
||||
|
||||
const body = node.childForFieldName('body');
|
||||
if (body === null) continue;
|
||||
|
||||
for (const stmt of body.namedChildren) {
|
||||
if (stmt === null || stmt.type !== 'expression_statement') continue;
|
||||
const expr = stmt.namedChild(0);
|
||||
if (expr === null || expr.type !== 'assignment_expression') continue;
|
||||
|
||||
const left = expr.childForFieldName('left');
|
||||
const right = expr.childForFieldName('right');
|
||||
if (left === null || right === null) continue;
|
||||
if (left.type !== 'member_expression') continue;
|
||||
|
||||
const obj = left.childForFieldName('object');
|
||||
const prop = left.childForFieldName('property');
|
||||
if (obj === null || prop === null) continue;
|
||||
if (obj.text !== 'this' || prop.type !== 'property_identifier') continue;
|
||||
|
||||
const fieldName = prop.text;
|
||||
|
||||
// Prefer JSDoc @type annotation on the preceding sibling comment.
|
||||
let typeName: string | null = null;
|
||||
const prevSib: SyntaxNode | null = stmt.previousNamedSibling;
|
||||
if (prevSib !== null && prevSib.type === 'comment') {
|
||||
const m = /@type\s*\{([^}]+)\}/.exec(prevSib.text);
|
||||
if (m?.[1]) typeName = m[1].trim();
|
||||
}
|
||||
// Fall back to constructor inference from `new Y()`.
|
||||
if (typeName === null && right.type === 'new_expression') {
|
||||
const ctor = right.childForFieldName('constructor');
|
||||
if (ctor !== null && ctor.type === 'identifier') typeName = ctor.text;
|
||||
}
|
||||
if (typeName === null) continue;
|
||||
|
||||
out.push({
|
||||
'@type-binding.name': syntheticCapture('@type-binding.name', prop, fieldName),
|
||||
'@type-binding.type': syntheticCapture('@type-binding.type', prop, typeName),
|
||||
// Anchor: positioned inside the constructor body so tsBindingScopeFor
|
||||
// can walk up from the Function (constructor) scope to the Class scope.
|
||||
'@type-binding.class-field': syntheticCapture('@type-binding.class-field', stmt, '1'),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Main emitter ──────────────────────────────────────────────────────────
|
||||
|
||||
export function emitJsScopeCaptures(
|
||||
sourceText: string,
|
||||
filePath: string,
|
||||
cachedTree?: unknown,
|
||||
): readonly CaptureMatch[] {
|
||||
let tree = cachedTree as ReturnType<ReturnType<typeof getJsParser>['parse']> | undefined;
|
||||
if (tree !== undefined && !jsCachedTreeMatchesGrammar(tree)) {
|
||||
tree = undefined;
|
||||
}
|
||||
if (tree === undefined) {
|
||||
tree = parseSourceSafe(getJsParser(filePath), sourceText, undefined, {
|
||||
bufferSize: getTreeSitterBufferSize(sourceText),
|
||||
});
|
||||
}
|
||||
|
||||
const rawMatches = getJsScopeQuery(filePath).matches(tree.rootNode);
|
||||
const out: CaptureMatch[] = [];
|
||||
|
||||
for (const m of rawMatches) {
|
||||
const grouped: Record<string, Capture> = {};
|
||||
for (const c of m.captures) {
|
||||
const tag = '@' + c.name;
|
||||
grouped[tag] = nodeToCapture(tag, c.node);
|
||||
}
|
||||
if (Object.keys(grouped).length === 0) continue;
|
||||
|
||||
// Decompose ESM import_statement / re-export export_statement.
|
||||
if (grouped['@import.statement'] !== undefined) {
|
||||
const stmtCapture = grouped['@import.statement'];
|
||||
const stmtNode =
|
||||
findNodeAtRange(tree.rootNode, stmtCapture.range, 'import_statement') ??
|
||||
findNodeAtRange(tree.rootNode, stmtCapture.range, 'export_statement');
|
||||
if (stmtNode !== null) {
|
||||
const decomposed = splitImportStatement(stmtNode);
|
||||
for (const d of decomposed) out.push(d);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Decompose dynamic import() calls.
|
||||
if (grouped['@import.dynamic'] !== undefined) {
|
||||
const dynCapture = grouped['@import.dynamic'];
|
||||
const callNode = findNodeAtRange(tree.rootNode, dynCapture.range, 'call_expression');
|
||||
if (callNode !== null) {
|
||||
const decomposed = splitImportStatement(callNode);
|
||||
for (const d of decomposed) out.push(d);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Filter @reference.read.member false-positives.
|
||||
if (grouped['@reference.read.member'] !== undefined) {
|
||||
const anchor = grouped['@reference.read.member'];
|
||||
const memberNode = findNodeAtRange(tree.rootNode, anchor.range, 'member_expression');
|
||||
if (memberNode === null || !shouldEmitReadMember(memberNode)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Synthesize arity metadata on function-like declarations.
|
||||
const declAnchor = pickFirstDefined(grouped, FUNCTION_DECL_TAGS);
|
||||
if (declAnchor !== undefined) {
|
||||
const fnNode = findFunctionNode(tree.rootNode, declAnchor.range);
|
||||
if (fnNode !== null) {
|
||||
const arity = computeTsArityMetadata(fnNode);
|
||||
if (arity.parameterCount !== undefined) {
|
||||
grouped['@declaration.parameter-count'] = syntheticCapture(
|
||||
'@declaration.parameter-count',
|
||||
fnNode,
|
||||
String(arity.parameterCount),
|
||||
);
|
||||
}
|
||||
if (arity.requiredParameterCount !== undefined) {
|
||||
grouped['@declaration.required-parameter-count'] = syntheticCapture(
|
||||
'@declaration.required-parameter-count',
|
||||
fnNode,
|
||||
String(arity.requiredParameterCount),
|
||||
);
|
||||
}
|
||||
if (arity.parameterTypes !== undefined) {
|
||||
grouped['@declaration.parameter-types'] = syntheticCapture(
|
||||
'@declaration.parameter-types',
|
||||
fnNode,
|
||||
JSON.stringify(arity.parameterTypes),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Synthesize @reference.arity on callsites.
|
||||
const callAnchor = pickFirstDefined(grouped, CALL_TAGS);
|
||||
if (callAnchor !== undefined && grouped['@reference.arity'] === undefined) {
|
||||
const callNode =
|
||||
findNodeAtRange(tree.rootNode, callAnchor.range, 'call_expression') ??
|
||||
findNodeAtRange(tree.rootNode, callAnchor.range, 'new_expression');
|
||||
if (callNode !== null) {
|
||||
const argList = callNode.childForFieldName('arguments');
|
||||
const args: SyntaxNode[] =
|
||||
argList === null
|
||||
? []
|
||||
: argList.namedChildren.filter(
|
||||
(c): c is SyntaxNode => c !== null && c.type !== 'comment',
|
||||
);
|
||||
grouped['@reference.arity'] = syntheticCapture(
|
||||
'@reference.arity',
|
||||
callNode,
|
||||
String(args.length),
|
||||
);
|
||||
grouped['@reference.parameter-types'] = syntheticCapture(
|
||||
'@reference.parameter-types',
|
||||
callNode,
|
||||
JSON.stringify(args.map(inferArgType)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
out.push(grouped);
|
||||
|
||||
// Synthesize `this` receiver type-bindings on class member functions.
|
||||
const scopeFnAnchor = grouped['@scope.function'];
|
||||
if (scopeFnAnchor !== undefined) {
|
||||
const fnNode = findFunctionNode(tree.rootNode, scopeFnAnchor.range);
|
||||
if (fnNode !== null) {
|
||||
const synth = synthesizeTsReceiverBinding(fnNode);
|
||||
if (synth !== null) out.push(synth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post-query synthesis passes.
|
||||
synthesizeCjsImports(tree.rootNode, out);
|
||||
synthesizeJsDocBindings(tree.rootNode, out);
|
||||
synthesizeConstructorFieldBindings(tree.rootNode, out);
|
||||
synthesizeDestructuringBindings(tree.rootNode, out);
|
||||
synthesizeForOfMapTupleBindings(tree.rootNode, out);
|
||||
synthesizeInstanceofNarrowings(tree.rootNode, out);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
/**
|
||||
* Import-target resolver for JavaScript.
|
||||
*
|
||||
* Delegates to the TypeScript `resolveTsTarget` standard-strategy resolver
|
||||
* with `language: SupportedLanguages.JavaScript` so the resolver tries
|
||||
* `.js` / `.jsx` extensions in addition to (or instead of) `.ts` / `.tsx`.
|
||||
*
|
||||
* The `TsResolveContext.language` flag already exists in `import-target.ts`
|
||||
* and the resolver (`resolveImportPath`) already branches on it — this
|
||||
* adapter just wires the right value in.
|
||||
*
|
||||
* CJS `require()` calls reference the same module-path strings as ESM
|
||||
* `import` statements, so the resolver handles them uniformly without any
|
||||
* CJS-specific logic here.
|
||||
*
|
||||
* No `tsconfig.json` path-alias support (JavaScript projects don't use
|
||||
* `tsconfig.json` compilerOptions.paths in general). Projects that DO use
|
||||
* tsconfig-based aliases alongside JavaScript can still resolve via the
|
||||
* standard extension-suffix fallback; the alias branch is a no-op when
|
||||
* `tsconfigPaths` is null.
|
||||
*/
|
||||
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import { resolveTsTarget, type TsResolveContext } from '../typescript/import-target.js';
|
||||
|
||||
export type JsResolveContext = TsResolveContext;
|
||||
|
||||
type PassCache = {
|
||||
readonly key: ReadonlySet<string>;
|
||||
readonly allFilePaths: Set<string>;
|
||||
readonly allFileList: readonly string[];
|
||||
readonly normalizedFileList: readonly string[];
|
||||
readonly resolveCache: Map<string, string | null>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a memoized `resolveImportTarget` adapter for JavaScript.
|
||||
* Caches the derived arrays and per-pass resolve cache across
|
||||
* `resolveImportTarget` calls within a single workspace pass.
|
||||
*/
|
||||
export function makeJsResolveImportTarget(): (
|
||||
targetRaw: string,
|
||||
fromFile: string,
|
||||
allFilePaths: ReadonlySet<string>,
|
||||
resolutionConfig?: unknown,
|
||||
) => string | readonly string[] | null {
|
||||
let cached: PassCache | null = null;
|
||||
|
||||
return (targetRaw, fromFile, allFilePaths) => {
|
||||
if (cached === null || cached.key !== allFilePaths) {
|
||||
const allFileList = Array.from(allFilePaths);
|
||||
cached = {
|
||||
key: allFilePaths,
|
||||
allFilePaths: new Set(allFilePaths),
|
||||
allFileList,
|
||||
normalizedFileList: allFileList.map((f) => f.toLowerCase()),
|
||||
resolveCache: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
const ws: JsResolveContext = {
|
||||
fromFile,
|
||||
language: SupportedLanguages.JavaScript,
|
||||
allFilePaths: cached.allFilePaths,
|
||||
allFileList: cached.allFileList,
|
||||
normalizedFileList: cached.normalizedFileList,
|
||||
resolveCache: cached.resolveCache,
|
||||
tsconfigPaths: null,
|
||||
};
|
||||
return resolveTsTarget(targetRaw, ws);
|
||||
};
|
||||
}
|
||||
49
gitnexus/src/core/ingestion/languages/javascript/index.ts
Normal file
49
gitnexus/src/core/ingestion/languages/javascript/index.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* JavaScript scope-resolution hooks (RFC #909 Ring 3, issue #928).
|
||||
*
|
||||
* Public API barrel. Consumers should import from this file rather
|
||||
* than the individual modules.
|
||||
*
|
||||
* Module layout (each file is a single concern):
|
||||
*
|
||||
* - `query.ts` — JS scope query string + lazy parser/query
|
||||
* singletons (`getJsParser`, `getJsScopeQuery`)
|
||||
* - `captures.ts` — `emitJsScopeCaptures` — runs the JS scope query,
|
||||
* synthesizes CJS require() imports and JSDoc-
|
||||
* derived type bindings, delegates arity synthesis
|
||||
* and destructuring/instanceof passes to shared
|
||||
* or TypeScript utilities
|
||||
* - `interpret.ts` — `interpretJsImport` / `interpretJsTypeBinding`
|
||||
* (delegate to TypeScript interpreters — same
|
||||
* capture-marker vocabulary)
|
||||
* - `simple-hooks.ts` — `jsBindingScopeFor` (var hoisting),
|
||||
* `jsImportOwningScope`, `jsReceiverBinding`
|
||||
* (all delegate to TypeScript counterparts)
|
||||
* - `merge-bindings.ts` — `jsMergeBindings` (LEGB via typescriptMergeBindings)
|
||||
* - `arity.ts` — `jsArityCompatibility` (delegates to TS function)
|
||||
* - `import-target.ts` — `makeJsResolveImportTarget` (memoized adapter)
|
||||
* - `scope-resolver.ts` — `javascriptScopeResolver` wiring object
|
||||
*
|
||||
* ## Known limitations
|
||||
*
|
||||
* 1. **JSDoc coverage** — `@param {T} name`, `@returns {T}` / `@return {T}`,
|
||||
* and `@type {T}` on variable declarations are synthesized. `@typedef`
|
||||
* is not yet synthesized (tracked in #1646).
|
||||
* 2. **CJS chained destructuring** — `const { X: { Y } } = require(...)`
|
||||
* (nested destructuring) emits only the outer `X` binding; `Y` is not
|
||||
* resolved.
|
||||
* 3. **Dynamic require** — `require(computedPath)` is skipped (non-literal
|
||||
* argument — cannot statically resolve the target).
|
||||
* 4. **`module.exports` / `exports.X`** — CJS export forms are not yet
|
||||
* modeled as re-exports. The finalize algorithm treats the exporting
|
||||
* module as a namespace; importers that do `const X = require('./m')`
|
||||
* bind the module namespace, and member-call resolution walks the
|
||||
* class graph from there.
|
||||
*/
|
||||
|
||||
export { emitJsScopeCaptures } from './captures.js';
|
||||
export { interpretJsImport, interpretJsTypeBinding } from './interpret.js';
|
||||
export { jsMergeBindings } from './merge-bindings.js';
|
||||
export { jsArityCompatibility } from './arity.js';
|
||||
export { makeJsResolveImportTarget } from './import-target.js';
|
||||
export { jsBindingScopeFor, jsImportOwningScope, jsReceiverBinding } from './simple-hooks.js';
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* Capture-match → semantic-shape interpreters for JavaScript.
|
||||
*
|
||||
* `interpretJsImport` delegates to `interpretTsImport` for all cases
|
||||
* because `emitJsScopeCaptures` synthesizes the same
|
||||
* `@import.kind/name/alias/source` markers for both ESM and CJS imports.
|
||||
*
|
||||
* The `@import.kind` values emitted for CJS by `captures.ts`:
|
||||
*
|
||||
* - `'named'` : `const { X } = require('./m')` → named import
|
||||
* - `'named-alias'` : `const { X: Y } = require('./m')` → aliased import
|
||||
* - `'namespace'` : `const X = require('./m')` → namespace import
|
||||
* - `'side-effect'` : `require('./m')` bare expression → side-effect
|
||||
*
|
||||
* These match the kinds `interpretTsImport` already handles for ESM
|
||||
* (`import { X }`, `import { X as Y }`, `import * as X`, `import './m'`),
|
||||
* so no new branch is needed here.
|
||||
*
|
||||
* `interpretJsTypeBinding` handles the JS-only `@type-binding.class-field`
|
||||
* tag before delegating to `interpretTsTypeBinding`. The class-field tag
|
||||
* is emitted by `synthesizeConstructorFieldBindings` and should produce
|
||||
* `source = 'annotation'` — the same strength as an explicit type
|
||||
* annotation. Remapping it to `@type-binding.annotation` achieves this
|
||||
* without adding a JS-specific branch to the shared TS interpreter
|
||||
* (DoD.md §2.2).
|
||||
*/
|
||||
|
||||
import type { CaptureMatch, ParsedImport, ParsedTypeBinding } from 'gitnexus-shared';
|
||||
import { interpretTsImport, interpretTsTypeBinding } from '../typescript/interpret.js';
|
||||
|
||||
export function interpretJsImport(captures: CaptureMatch): ParsedImport | null {
|
||||
return interpretTsImport(captures);
|
||||
}
|
||||
|
||||
export function interpretJsTypeBinding(captures: CaptureMatch): ParsedTypeBinding | null {
|
||||
// @type-binding.class-field is a JS-only tag emitted by
|
||||
// synthesizeConstructorFieldBindings. Remap it to the standard
|
||||
// @type-binding.annotation tag so interpretTsTypeBinding assigns
|
||||
// source = 'annotation' without a JS-specific branch in shared code.
|
||||
if (captures['@type-binding.class-field'] !== undefined) {
|
||||
const { '@type-binding.class-field': classField, ...rest } = captures;
|
||||
return interpretTsTypeBinding({ ...rest, '@type-binding.annotation': classField });
|
||||
}
|
||||
return interpretTsTypeBinding(captures);
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* Binding-merge precedence for JavaScript.
|
||||
*
|
||||
* JavaScript has no TypeScript declaration-merging (no `interface + class`
|
||||
* coexisting in the same scope, no `namespace + class` dual-space declarations).
|
||||
* However, `typescriptMergeBindings` handles these by falling back to
|
||||
* `['value']` for any `NodeLabel` not explicitly mapped to multiple spaces —
|
||||
* which is what every JavaScript declaration produces. The result is pure
|
||||
* LEGB precedence without any cross-space logic, which is exactly what
|
||||
* JavaScript needs.
|
||||
*
|
||||
* Reuse rather than reimplementing to keep the single source of truth for
|
||||
* the tier (local 0 / import-namespace-reexport 1 / wildcard 2) ordering.
|
||||
*/
|
||||
|
||||
import type { BindingRef } from 'gitnexus-shared';
|
||||
import { typescriptMergeBindings } from '../typescript/merge-bindings.js';
|
||||
|
||||
export function jsMergeBindings(bindings: readonly BindingRef[]): readonly BindingRef[] {
|
||||
return typescriptMergeBindings(bindings);
|
||||
}
|
||||
421
gitnexus/src/core/ingestion/languages/javascript/query.ts
Normal file
421
gitnexus/src/core/ingestion/languages/javascript/query.ts
Normal file
|
|
@ -0,0 +1,421 @@
|
|||
/**
|
||||
* Tree-sitter query for JavaScript scope captures (RFC §5.1, Ring 3).
|
||||
*
|
||||
* Subset of the TypeScript scope query (`languages/typescript/query.ts`)
|
||||
* compiled against `tree-sitter-javascript`. TypeScript-only node types
|
||||
* (`interface_declaration`, `type_alias_declaration`, `enum_declaration`,
|
||||
* `internal_module`, `abstract_class_declaration`, `function_signature`,
|
||||
* `method_signature`, `abstract_method_signature`, `type_annotation`,
|
||||
* `public_field_definition`) are dropped because:
|
||||
*
|
||||
* 1. The JS grammar doesn't define them — the query compiler would
|
||||
* throw `InvalidNodeType` if they were included.
|
||||
* 2. JavaScript has no static type annotations, so the `@type-binding.*`
|
||||
* patterns derived from TS annotation nodes don't apply.
|
||||
*
|
||||
* What IS shared with the TypeScript query:
|
||||
*
|
||||
* - Scope patterns: `program`, `class_declaration`, `(class)` (the JS
|
||||
* grammar node for class expressions — NOT `class_expression`, which
|
||||
* does not exist in `tree-sitter-javascript`), `function_declaration`,
|
||||
* `generator_function_declaration`, `function_expression`,
|
||||
* `arrow_function`, `method_definition`.
|
||||
* - Declaration patterns for functions, classes, const/let/var,
|
||||
* object-property arrows (Zustand, TanStack, etc.), and HOC-wrapped
|
||||
* variable declarations (forwardRef / memo / useCallback / useMemo).
|
||||
* - Import patterns: `import_statement`, `export_statement` re-exports,
|
||||
* and dynamic `import()` (represented as `call_expression(import)` in
|
||||
* both grammars — the `import` leaf node exists in tree-sitter-javascript
|
||||
* as well as tree-sitter-typescript).
|
||||
* - Type-binding patterns that work without static annotations:
|
||||
* constructor inference (`new User()`), call-result alias
|
||||
* (`const u = getUser()`), member-access alias (`const a = u.addr`),
|
||||
* identifier alias, assignment rebind, and for-of element bindings.
|
||||
* JSDoc-derived type bindings (`@param {User} u`, `@returns {User}`)
|
||||
* are handled separately in `captures.ts` via comment-node scanning.
|
||||
* - Reference patterns: free calls, member calls, constructor calls,
|
||||
* write-access, read-access, and dynamic import.
|
||||
*
|
||||
* CJS `require()` is NOT captured here; it is handled in `captures.ts`
|
||||
* by scanning parent context (destructured vs. namespace) of `call_expression`
|
||||
* nodes whose callee is the identifier `require`.
|
||||
*
|
||||
* Grammar version: `tree-sitter-javascript` pinned in gitnexus/package.json.
|
||||
*
|
||||
* Exposes lazy `Parser` and `Query` singletons so callers don't pay
|
||||
* tree-sitter init cost per file.
|
||||
*/
|
||||
|
||||
import Parser from 'tree-sitter';
|
||||
import JS from 'tree-sitter-javascript';
|
||||
|
||||
const JS_GRAMMAR = JS as Parameters<Parser['setLanguage']>[0];
|
||||
|
||||
/** True when the file should be parsed with the JSX-extended query. */
|
||||
function isJsxFile(filePath: string): boolean {
|
||||
return filePath.endsWith('.jsx');
|
||||
}
|
||||
|
||||
const JAVASCRIPT_SCOPE_QUERY = `
|
||||
;; Scopes — module / class-likes / function-likes
|
||||
(program) @scope.module
|
||||
|
||||
(class_declaration) @scope.class
|
||||
(class) @scope.class
|
||||
|
||||
(function_declaration) @scope.function
|
||||
(generator_function_declaration) @scope.function
|
||||
(function_expression) @scope.function
|
||||
(arrow_function) @scope.function
|
||||
(method_definition) @scope.function
|
||||
|
||||
;; Declarations — classes
|
||||
(class_declaration
|
||||
name: (identifier) @declaration.name) @declaration.class
|
||||
|
||||
;; Declarations — methods (inside class bodies)
|
||||
(method_definition
|
||||
name: (property_identifier) @declaration.name) @declaration.method
|
||||
|
||||
;; Declarations — class fields (JS uses field_definition, not public_field_definition)
|
||||
(field_definition
|
||||
property: (property_identifier) @declaration.name) @declaration.property
|
||||
|
||||
;; Declarations — free functions
|
||||
(function_declaration
|
||||
name: (identifier) @declaration.name) @declaration.function
|
||||
|
||||
(generator_function_declaration
|
||||
name: (identifier) @declaration.name) @declaration.function
|
||||
|
||||
;; Arrow / function-expression assigned to a const/let/var.
|
||||
;; Anchor discipline: @declaration.function sits on the INNER arrow or
|
||||
;; function_expression, NOT on the lexical_declaration wrapper. This
|
||||
;; aligns anchor.range with the @scope.function range so
|
||||
;; pass2AttachDeclarations resolves the innermost scope correctly and
|
||||
;; resolveCallerGraphId walks up to the right caller anchor.
|
||||
(lexical_declaration
|
||||
(variable_declarator
|
||||
name: (identifier) @declaration.name
|
||||
value: (arrow_function) @declaration.function))
|
||||
|
||||
(lexical_declaration
|
||||
(variable_declarator
|
||||
name: (identifier) @declaration.name
|
||||
value: (function_expression) @declaration.function))
|
||||
|
||||
(export_statement
|
||||
declaration: (lexical_declaration
|
||||
(variable_declarator
|
||||
name: (identifier) @declaration.name
|
||||
value: (arrow_function) @declaration.function)))
|
||||
|
||||
(export_statement
|
||||
declaration: (lexical_declaration
|
||||
(variable_declarator
|
||||
name: (identifier) @declaration.name
|
||||
value: (function_expression) @declaration.function)))
|
||||
|
||||
(variable_declaration
|
||||
(variable_declarator
|
||||
name: (identifier) @declaration.name
|
||||
value: (arrow_function) @declaration.function))
|
||||
|
||||
(variable_declaration
|
||||
(variable_declarator
|
||||
name: (identifier) @declaration.name
|
||||
value: (function_expression) @declaration.function))
|
||||
|
||||
;; Object-property arrows / function expressions named by their pair key.
|
||||
;; Same anchor discipline as the lexical_declaration block above: the
|
||||
;; @declaration.function capture must sit on the INNER arrow/fn-expression.
|
||||
(pair
|
||||
key: (property_identifier) @declaration.name
|
||||
value: (arrow_function) @declaration.function)
|
||||
|
||||
(pair
|
||||
key: (property_identifier) @declaration.name
|
||||
value: (function_expression) @declaration.function)
|
||||
|
||||
(pair
|
||||
key: (string (string_fragment) @declaration.name)
|
||||
value: (arrow_function) @declaration.function)
|
||||
|
||||
(pair
|
||||
key: (string (string_fragment) @declaration.name)
|
||||
value: (function_expression) @declaration.function)
|
||||
|
||||
;; HOC-wrapped variable declarations: const X = HOC((args) => { ... }).
|
||||
;; Covers React.forwardRef, memo, useCallback, useMemo, observer,
|
||||
;; debounce, and any user-defined HOC factory.
|
||||
(lexical_declaration
|
||||
(variable_declarator
|
||||
name: (identifier) @declaration.name
|
||||
value: (call_expression
|
||||
arguments: (arguments
|
||||
(arrow_function) @declaration.function))))
|
||||
|
||||
(lexical_declaration
|
||||
(variable_declarator
|
||||
name: (identifier) @declaration.name
|
||||
value: (call_expression
|
||||
arguments: (arguments
|
||||
(function_expression) @declaration.function))))
|
||||
|
||||
(export_statement
|
||||
declaration: (lexical_declaration
|
||||
(variable_declarator
|
||||
name: (identifier) @declaration.name
|
||||
value: (call_expression
|
||||
arguments: (arguments
|
||||
(arrow_function) @declaration.function)))))
|
||||
|
||||
(export_statement
|
||||
declaration: (lexical_declaration
|
||||
(variable_declarator
|
||||
name: (identifier) @declaration.name
|
||||
value: (call_expression
|
||||
arguments: (arguments
|
||||
(function_expression) @declaration.function)))))
|
||||
|
||||
(variable_declaration
|
||||
(variable_declarator
|
||||
name: (identifier) @declaration.name
|
||||
value: (call_expression
|
||||
arguments: (arguments
|
||||
(arrow_function) @declaration.function))))
|
||||
|
||||
(variable_declaration
|
||||
(variable_declarator
|
||||
name: (identifier) @declaration.name
|
||||
value: (call_expression
|
||||
arguments: (arguments
|
||||
(function_expression) @declaration.function))))
|
||||
|
||||
;; Variable / constant declarations (non-function values).
|
||||
(lexical_declaration
|
||||
(variable_declarator
|
||||
name: (identifier) @declaration.name)) @declaration.const
|
||||
|
||||
(export_statement
|
||||
declaration: (lexical_declaration
|
||||
(variable_declarator
|
||||
name: (identifier) @declaration.name))) @declaration.const
|
||||
|
||||
(variable_declaration
|
||||
(variable_declarator
|
||||
name: (identifier) @declaration.name)) @declaration.variable
|
||||
|
||||
;; Imports (ESM) — single anchor per statement; decomposer emits per-specifier markers.
|
||||
(import_statement) @import.statement
|
||||
|
||||
;; Re-exports with a source clause.
|
||||
(export_statement
|
||||
source: (string)) @import.statement
|
||||
|
||||
;; Dynamic imports: import('./m') — tree-sitter-javascript represents this
|
||||
;; as call_expression with a named import leaf as the function field,
|
||||
;; identical to tree-sitter-typescript.
|
||||
(call_expression
|
||||
function: (import)) @import.dynamic
|
||||
|
||||
;; ── Type bindings (no static annotations in JS; inferred from AST shape) ──
|
||||
|
||||
;; Constructor-inferred: const u = new User()
|
||||
(variable_declarator
|
||||
name: (identifier) @type-binding.name
|
||||
value: (new_expression
|
||||
constructor: (identifier) @type-binding.type)) @type-binding.constructor
|
||||
|
||||
;; Qualified constructor: const u = new models.User()
|
||||
(variable_declarator
|
||||
name: (identifier) @type-binding.name
|
||||
value: (new_expression
|
||||
constructor: (member_expression) @type-binding.type)) @type-binding.constructor
|
||||
|
||||
;; Call-result alias: const u = getUser()
|
||||
(variable_declarator
|
||||
name: (identifier) @type-binding.name
|
||||
value: (call_expression
|
||||
function: (identifier) @type-binding.type)) @type-binding.alias
|
||||
|
||||
;; Member-call alias: const u = svc.getUser()
|
||||
(variable_declarator
|
||||
name: (identifier) @type-binding.name
|
||||
value: (call_expression
|
||||
function: (member_expression) @type-binding.type)) @type-binding.alias
|
||||
|
||||
;; Await chain: const u = await getUser() / await svc.getUser()
|
||||
(variable_declarator
|
||||
name: (identifier) @type-binding.name
|
||||
value: (await_expression
|
||||
(call_expression
|
||||
function: (identifier) @type-binding.type))) @type-binding.alias
|
||||
|
||||
(variable_declarator
|
||||
name: (identifier) @type-binding.name
|
||||
value: (await_expression
|
||||
(call_expression
|
||||
function: (member_expression) @type-binding.type))) @type-binding.alias
|
||||
|
||||
;; Member-access alias: const addr = user.address
|
||||
(variable_declarator
|
||||
name: (identifier) @type-binding.name
|
||||
value: (member_expression) @type-binding.type) @type-binding.member-alias
|
||||
|
||||
;; Identifier alias: const alias = user
|
||||
(variable_declarator
|
||||
name: (identifier) @type-binding.name
|
||||
value: (identifier) @type-binding.type) @type-binding.alias
|
||||
|
||||
;; Assignment rebind: u = new User() / u = getUser()
|
||||
(assignment_expression
|
||||
left: (identifier) @type-binding.name
|
||||
right: (new_expression
|
||||
constructor: (identifier) @type-binding.type)) @type-binding.constructor
|
||||
|
||||
(assignment_expression
|
||||
left: (identifier) @type-binding.name
|
||||
right: (call_expression
|
||||
function: (identifier) @type-binding.type)) @type-binding.alias
|
||||
|
||||
(assignment_expression
|
||||
left: (identifier) @type-binding.name
|
||||
right: (identifier) @type-binding.type) @type-binding.alias
|
||||
|
||||
;; For-of element: for (const u of users) / for (const u of getUsers())
|
||||
(for_in_statement
|
||||
left: (identifier) @type-binding.name
|
||||
right: (identifier) @type-binding.type) @type-binding.alias
|
||||
|
||||
(for_in_statement
|
||||
left: (identifier) @type-binding.name
|
||||
right: (call_expression
|
||||
function: (identifier) @type-binding.type)) @type-binding.alias
|
||||
|
||||
(for_in_statement
|
||||
left: (identifier) @type-binding.name
|
||||
right: (call_expression
|
||||
function: (member_expression) @type-binding.type)) @type-binding.alias
|
||||
|
||||
(for_in_statement
|
||||
left: (identifier) @type-binding.name
|
||||
right: (member_expression
|
||||
property: (property_identifier) @type-binding.type)) @type-binding.alias
|
||||
|
||||
;; ── References ────────────────────────────────────────────────────────────
|
||||
|
||||
;; Free calls: fn(args). The dynamic-import filter runs in captures.ts.
|
||||
(call_expression
|
||||
function: (identifier) @reference.name) @reference.call.free
|
||||
|
||||
;; Awaited free call: await fn<T>(...) re-associated by tree-sitter.
|
||||
(call_expression
|
||||
function: (await_expression
|
||||
(identifier) @reference.name)) @reference.call.free
|
||||
|
||||
;; Member calls: obj.method() (includes optional chain).
|
||||
(call_expression
|
||||
function: (member_expression
|
||||
object: (_) @reference.receiver
|
||||
property: (property_identifier) @reference.name)) @reference.call.member
|
||||
|
||||
;; Awaited member call: await svc.m<T>(...)
|
||||
(call_expression
|
||||
function: (await_expression
|
||||
(member_expression
|
||||
object: (_) @reference.receiver
|
||||
property: (property_identifier) @reference.name))) @reference.call.member
|
||||
|
||||
;; Constructor calls: new User() / new ns.User()
|
||||
(new_expression
|
||||
constructor: (identifier) @reference.name) @reference.call.constructor
|
||||
|
||||
(new_expression
|
||||
constructor: (member_expression) @reference.call.constructor.qualified) @reference.call.constructor
|
||||
|
||||
;; Write access: obj.field = value
|
||||
(assignment_expression
|
||||
left: (member_expression
|
||||
object: (_) @reference.receiver
|
||||
property: (property_identifier) @reference.name)) @reference.write.member
|
||||
|
||||
(augmented_assignment_expression
|
||||
left: (member_expression
|
||||
object: (_) @reference.receiver
|
||||
property: (property_identifier) @reference.name)) @reference.write.member
|
||||
|
||||
;; Read access: obj.field (in read context; captures.ts filters non-reads).
|
||||
(member_expression
|
||||
object: (_) @reference.receiver
|
||||
property: (property_identifier) @reference.name) @reference.read.member
|
||||
`;
|
||||
|
||||
/** JSX-only suffix — appended when compiling against the JSX grammar for .jsx files. */
|
||||
const JSX_QUERY_SUFFIX = `
|
||||
;; <Foo />
|
||||
((jsx_self_closing_element
|
||||
name: (identifier) @reference.name) @reference.call.free
|
||||
(#match? @reference.name "^[A-Z]"))
|
||||
|
||||
;; <Foo> ... </Foo>
|
||||
((jsx_opening_element
|
||||
name: (identifier) @reference.name) @reference.call.free
|
||||
(#match? @reference.name "^[A-Z]"))
|
||||
|
||||
;; <Foo.Bar />
|
||||
(jsx_self_closing_element
|
||||
name: (member_expression
|
||||
object: (_) @reference.receiver
|
||||
property: (property_identifier) @reference.name)) @reference.call.member
|
||||
|
||||
(jsx_opening_element
|
||||
name: (member_expression
|
||||
object: (_) @reference.receiver
|
||||
property: (property_identifier) @reference.name)) @reference.call.member
|
||||
`;
|
||||
|
||||
let _jsParser: Parser | null = null;
|
||||
let _jsQuery: Parser.Query | null = null;
|
||||
let _jsxParser: Parser | null = null;
|
||||
let _jsxQuery: Parser.Query | null = null;
|
||||
|
||||
export function getJsParser(filePath?: string): Parser {
|
||||
// JSX files use the same JavaScript grammar in tree-sitter-javascript;
|
||||
// both .js and .jsx parse with the same grammar object. We keep separate
|
||||
// singletons only to mirror the TypeScript pattern and in case a future
|
||||
// version of the grammar diverges.
|
||||
if (filePath !== undefined && isJsxFile(filePath)) {
|
||||
if (_jsxParser === null) {
|
||||
_jsxParser = new Parser();
|
||||
_jsxParser.setLanguage(JS_GRAMMAR);
|
||||
}
|
||||
return _jsxParser;
|
||||
}
|
||||
if (_jsParser === null) {
|
||||
_jsParser = new Parser();
|
||||
_jsParser.setLanguage(JS_GRAMMAR);
|
||||
}
|
||||
return _jsParser;
|
||||
}
|
||||
|
||||
export function getJsScopeQuery(filePath?: string): Parser.Query {
|
||||
if (filePath !== undefined && isJsxFile(filePath)) {
|
||||
if (_jsxQuery === null) {
|
||||
_jsxQuery = new Parser.Query(JS_GRAMMAR, JAVASCRIPT_SCOPE_QUERY + JSX_QUERY_SUFFIX);
|
||||
}
|
||||
return _jsxQuery;
|
||||
}
|
||||
if (_jsQuery === null) {
|
||||
_jsQuery = new Parser.Query(JS_GRAMMAR, JAVASCRIPT_SCOPE_QUERY);
|
||||
}
|
||||
return _jsQuery;
|
||||
}
|
||||
|
||||
/** Validate that a cached Tree was produced by the JS grammar. */
|
||||
export function jsCachedTreeMatchesGrammar(tree: unknown): boolean {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const lang = (tree as any)?.getLanguage?.();
|
||||
if (lang === undefined || lang === null) return true;
|
||||
return lang === JS_GRAMMAR;
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
/**
|
||||
* JavaScript `ScopeResolver` registered in `SCOPE_RESOLVERS` and
|
||||
* consumed by the generic `runScopeResolution` orchestrator
|
||||
* (RFC #909 Ring 3, issue #928).
|
||||
*
|
||||
* Follows the same minimal wiring-only pattern as TypeScript (the third
|
||||
* migration). Per-hook logic lives in sibling modules:
|
||||
*
|
||||
* - `query.ts` — JS scope query + parser/query singletons
|
||||
* - `captures.ts` — `emitJsScopeCaptures` (JS grammar, CJS, JSDoc)
|
||||
* - `interpret.ts` — `interpretJsImport` (delegates to TS interpreter)
|
||||
* - `simple-hooks.ts` — `jsBindingScopeFor`, `jsImportOwningScope`,
|
||||
* `jsReceiverBinding` (all delegate to TS hooks)
|
||||
* - `merge-bindings.ts` — `jsMergeBindings` (delegates to TS function)
|
||||
* - `arity.ts` — `jsArityCompatibility` (delegates to TS function)
|
||||
* - `import-target.ts` — `makeJsResolveImportTarget` (TS resolver, JS extensions)
|
||||
*
|
||||
* See `./index.ts` for the full per-module rationale.
|
||||
*
|
||||
* ## Key differences from TypeScript resolver
|
||||
*
|
||||
* - `fieldFallbackOnMethodLookup: true` — JavaScript is dynamically typed;
|
||||
* the field-fallback heuristic is ENABLED (unlike TypeScript, which
|
||||
* disables it because the type-binding layer is precise).
|
||||
* - `allowGlobalFreeCallFallback: true` — CJS `require` patterns and
|
||||
* global helpers (e.g. `process`, `console`) benefit from workspace-
|
||||
* wide unique-name fallback. TypeScript uses explicit imports.
|
||||
* - `loadResolutionConfig` is omitted — JavaScript projects don't use
|
||||
* `tsconfig.json` path aliases in general. `tsconfigPaths: null` is
|
||||
* threaded through the resolver adapter.
|
||||
* - `hoistTypeBindingsToModule: true` — JSDoc `@returns {T}` bindings are
|
||||
* synthesized on the function scope and hoisted, matching TypeScript's
|
||||
* method return-type hoisting strategy for cross-file chain resolution.
|
||||
*/
|
||||
|
||||
import type { ParsedFile } from 'gitnexus-shared';
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js';
|
||||
import { populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js';
|
||||
import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js';
|
||||
import { javascriptProvider } from '../typescript.js';
|
||||
import { jsMergeBindings } from './merge-bindings.js';
|
||||
import { jsArityCompatibility } from './arity.js';
|
||||
import { makeJsResolveImportTarget } from './import-target.js';
|
||||
|
||||
const javascriptScopeResolver: ScopeResolver = {
|
||||
language: SupportedLanguages.JavaScript,
|
||||
languageProvider: javascriptProvider,
|
||||
importEdgeReason: 'javascript-scope: import',
|
||||
|
||||
resolveImportTarget: makeJsResolveImportTarget(),
|
||||
|
||||
// JavaScript LEGB — same tier ordering as TypeScript; no declaration-
|
||||
// merging across type/value/namespace spaces.
|
||||
mergeBindings: (existing, incoming) => [...jsMergeBindings([...existing, ...incoming])],
|
||||
|
||||
// Adapter: jsArityCompatibility uses (def, callsite); contract is (callsite, def).
|
||||
arityCompatibility: (callsite, def) => jsArityCompatibility(def, callsite),
|
||||
|
||||
buildMro: (graph, parsedFiles, nodeLookup) =>
|
||||
buildMro(graph, parsedFiles, nodeLookup, defaultLinearize),
|
||||
|
||||
populateOwners: (parsed: ParsedFile) => populateClassOwnedMembers(parsed),
|
||||
|
||||
// JavaScript `super` keyword: same pattern as TypeScript.
|
||||
isSuperReceiver: (text) => /^super(\s*\(|\s*\.|\s*\[|\s*$)/.test(text.trim()),
|
||||
|
||||
// JavaScript is dynamically typed — enable the field-fallback heuristic
|
||||
// so member-call receivers without type annotations can still resolve
|
||||
// through declared class fields (e.g. JSDoc-typed fields).
|
||||
fieldFallbackOnMethodLookup: true,
|
||||
|
||||
// Return-type propagation (across ESM imports) mirrors TypeScript's
|
||||
// default behavior. JSDoc @returns bindings are hoisted to Module scope
|
||||
// and propagated to importers via the standard mechanism.
|
||||
propagatesReturnTypesAcrossImports: true,
|
||||
|
||||
// JSDoc @returns bindings are synthesized on the function/method node
|
||||
// and hoisted to Module scope by `jsBindingScopeFor` (identical to the
|
||||
// TypeScript `tsBindingScopeFor` `@type-binding.return` branch).
|
||||
hoistTypeBindingsToModule: true,
|
||||
|
||||
// CJS-heavy codebases often have utility functions exported without
|
||||
// explicit imports at the call site. Workspace-wide unique-name fallback
|
||||
// recovers these edges.
|
||||
allowGlobalFreeCallFallback: true,
|
||||
};
|
||||
|
||||
export { javascriptScopeResolver };
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* Simple hooks for the JavaScript scope-resolution provider.
|
||||
*
|
||||
* `jsBindingScopeFor` wraps `tsBindingScopeFor` and adds the JS-only
|
||||
* `@type-binding.class-field` hoisting rule. The other two hooks
|
||||
* (`jsImportOwningScope`, `jsReceiverBinding`) are identical to their
|
||||
* TypeScript counterparts and are re-exported directly.
|
||||
*
|
||||
* ## Why class-field hoisting lives here (not in `tsBindingScopeFor`)
|
||||
*
|
||||
* `@type-binding.class-field` is emitted exclusively by
|
||||
* `synthesizeConstructorFieldBindings` in `captures.ts`, which is a
|
||||
* JavaScript-only synthesis pass. TypeScript uses
|
||||
* `@type-binding.parameter-property` for constructor parameter
|
||||
* properties instead. Keeping the JS-only rule in the JS hook file
|
||||
* prevents language-specific logic from leaking into shared TypeScript
|
||||
* infrastructure (DoD.md §2.2).
|
||||
*/
|
||||
|
||||
import type { CaptureMatch, Scope, ScopeId, ScopeTree } from 'gitnexus-shared';
|
||||
import { tsBindingScopeFor, walkToScope } from '../typescript/simple-hooks.js';
|
||||
|
||||
export {
|
||||
tsImportOwningScope as jsImportOwningScope,
|
||||
tsReceiverBinding as jsReceiverBinding,
|
||||
} from '../typescript/simple-hooks.js';
|
||||
|
||||
/**
|
||||
* Like `tsBindingScopeFor` but additionally hoists
|
||||
* `@type-binding.class-field` captures to the enclosing Class scope.
|
||||
*
|
||||
* `@type-binding.class-field` is anchored inside the constructor body
|
||||
* (by `synthesizeConstructorFieldBindings`) so that `walkToScope` can
|
||||
* walk up from the Function (constructor) scope to the Class scope.
|
||||
* This puts `User.address → Address` in the class's typeBindings so
|
||||
* compound-receiver resolution finds it when resolving
|
||||
* `user.address.save()`.
|
||||
*/
|
||||
export function jsBindingScopeFor(
|
||||
decl: CaptureMatch,
|
||||
innermost: Scope,
|
||||
tree: ScopeTree,
|
||||
): ScopeId | null {
|
||||
if (decl['@type-binding.class-field'] !== undefined) {
|
||||
return walkToScope(innermost, tree, 'Class');
|
||||
}
|
||||
return tsBindingScopeFor(decl, innermost, tree);
|
||||
}
|
||||
|
|
@ -56,6 +56,16 @@ import {
|
|||
typescriptArityCompatibility,
|
||||
resolveTsImportTarget,
|
||||
} from './typescript/index.js';
|
||||
import {
|
||||
emitJsScopeCaptures,
|
||||
interpretJsImport,
|
||||
interpretJsTypeBinding,
|
||||
jsBindingScopeFor,
|
||||
jsImportOwningScope,
|
||||
jsReceiverBinding,
|
||||
jsMergeBindings,
|
||||
jsArityCompatibility,
|
||||
} from './javascript/index.js';
|
||||
|
||||
/**
|
||||
* TypeScript/JavaScript: arrow_function and function_expression are
|
||||
|
|
@ -359,4 +369,19 @@ export const javascriptProvider = defineLanguage({
|
|||
classExtractor: createClassExtractor(javascriptClassConfig),
|
||||
heritageExtractor: createHeritageExtractor(SupportedLanguages.JavaScript),
|
||||
builtInNames: BUILT_INS,
|
||||
|
||||
// ── RFC #909 Ring 3: scope-based resolution hooks (RFC §5) ──────────
|
||||
// JavaScript is the fourth migration after Python, C#, and TypeScript.
|
||||
// Hooks are thin wrappers over the TypeScript implementations where
|
||||
// semantics are identical; JS-specific additions (CJS require(),
|
||||
// JSDoc type bindings) live in ./javascript/captures.ts.
|
||||
// See ./javascript/index.ts for the full per-module rationale.
|
||||
emitScopeCaptures: emitJsScopeCaptures,
|
||||
interpretImport: interpretJsImport,
|
||||
interpretTypeBinding: interpretJsTypeBinding,
|
||||
bindingScopeFor: jsBindingScopeFor,
|
||||
importOwningScope: jsImportOwningScope,
|
||||
mergeBindings: (_scope, bindings) => jsMergeBindings(bindings),
|
||||
receiverBinding: jsReceiverBinding,
|
||||
arityCompatibility: jsArityCompatibility,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -75,8 +75,11 @@ export function tsBindingScopeFor(
|
|||
* any of `kinds`. Returns the matching scope's id or `null` when no
|
||||
* ancestor matches (e.g., a return type binding emitted outside any
|
||||
* Module scope — shouldn't happen in well-formed input).
|
||||
*
|
||||
* Exported so language-specific hook wrappers (e.g. `jsBindingScopeFor`)
|
||||
* can reuse it without duplicating the traversal logic.
|
||||
*/
|
||||
function walkToScope(
|
||||
export function walkToScope(
|
||||
from: Scope,
|
||||
tree: ScopeTree,
|
||||
...kinds: readonly Scope['kind'][]
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ export const MIGRATED_LANGUAGES: ReadonlySet<SupportedLanguages> = new Set<Suppo
|
|||
SupportedLanguages.C,
|
||||
SupportedLanguages.CPlusPlus,
|
||||
SupportedLanguages.PHP,
|
||||
SupportedLanguages.JavaScript,
|
||||
]);
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { javaScopeResolver } from '../../languages/java/scope-resolver.js';
|
|||
import { cScopeResolver } from '../../languages/c/scope-resolver.js';
|
||||
import { cppScopeResolver } from '../../languages/cpp/scope-resolver.js';
|
||||
import { phpScopeResolver } from '../../languages/php/scope-resolver.js';
|
||||
import { javascriptScopeResolver } from '../../languages/javascript/scope-resolver.js';
|
||||
|
||||
/** Map of `SupportedLanguages` → `ScopeResolver`. The phase iterates
|
||||
* this map intersected with `MIGRATED_LANGUAGES` (the per-language
|
||||
|
|
@ -36,4 +37,5 @@ export const SCOPE_RESOLVERS: ReadonlyMap<SupportedLanguages, ScopeResolver> = n
|
|||
[SupportedLanguages.C, cScopeResolver],
|
||||
[SupportedLanguages.CPlusPlus, cppScopeResolver],
|
||||
[SupportedLanguages.PHP, phpScopeResolver],
|
||||
[SupportedLanguages.JavaScript, javascriptScopeResolver],
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ export class User {
|
|||
save() {}
|
||||
}
|
||||
|
||||
/** @returns {User} */
|
||||
export function getUser() {
|
||||
return new User();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ export class User {
|
|||
getName() { return ''; }
|
||||
}
|
||||
|
||||
/** @returns {User} */
|
||||
export function getUser() {
|
||||
return new User();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue