From 4329a52becbef247a5641f6342d525dffd17192c Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Thu, 21 May 2026 09:28:00 -0500 Subject: [PATCH] feat: add Lua and Luau language support (#273) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Lua (.lua) and Luau (.luau) extraction — functions, methods with receivers, type aliases (Luau), require imports (incl. Roblox instance-path), and call edges. Vendors the ABI-15 Lua and ABI-14 Luau tree-sitter grammars. Addresses #232. --- .claude/skills/add-lang/SKILL.md | 219 ++++++++++++++++++++++ .claude/skills/agent-eval/corpus.json | 10 + CHANGELOG.md | 14 ++ README.md | 4 +- __tests__/extraction.test.ts | 177 +++++++++++++++++ scripts/add-lang/bench.sh | 60 ++++++ scripts/add-lang/check-grammar.mjs | 75 ++++++++ scripts/add-lang/dump-ast.mjs | 103 ++++++++++ scripts/add-lang/verify-extraction.mjs | 70 +++++++ src/extraction/grammars.ts | 14 +- src/extraction/languages/index.ts | 4 + src/extraction/languages/lua.ts | 152 +++++++++++++++ src/extraction/languages/luau.ts | 36 ++++ src/extraction/tree-sitter.ts | 28 +++ src/extraction/wasm/tree-sitter-lua.wasm | Bin 0 -> 49488 bytes src/extraction/wasm/tree-sitter-luau.wasm | Bin 0 -> 94204 bytes src/types.ts | 6 + 17 files changed, 969 insertions(+), 3 deletions(-) create mode 100644 .claude/skills/add-lang/SKILL.md create mode 100755 scripts/add-lang/bench.sh create mode 100755 scripts/add-lang/check-grammar.mjs create mode 100755 scripts/add-lang/dump-ast.mjs create mode 100755 scripts/add-lang/verify-extraction.mjs create mode 100644 src/extraction/languages/lua.ts create mode 100644 src/extraction/languages/luau.ts create mode 100644 src/extraction/wasm/tree-sitter-lua.wasm create mode 100644 src/extraction/wasm/tree-sitter-luau.wasm diff --git a/.claude/skills/add-lang/SKILL.md b/.claude/skills/add-lang/SKILL.md new file mode 100644 index 0000000..0e107a3 --- /dev/null +++ b/.claude/skills/add-lang/SKILL.md @@ -0,0 +1,219 @@ +--- +name: add-lang +description: Add tree-sitter language support to codegraph end-to-end — wire the grammar + extractor, write tests, then benchmark extraction quality and retrieval value on 3 popular real-world repos. Use when the user runs /add-lang or asks to add/support a new language (e.g. Lua, Elixir, Zig, OCaml) in codegraph. +--- + +# Add a language to CodeGraph + +Wire a new tree-sitter language into codegraph's extraction pipeline, prove it +extracts real symbols on popular repos, and prove it beats no-codegraph for an +agent. Runs **fully autonomously** — pick repos, benchmark, update docs, then +report. **Never commit, push, publish, or tag** (house rule); leave all changes +for the user to review. + +The argument is the language token used throughout the `Language` union, e.g. +`lua`, `elixir`, `zig`. If none was given, ask which language. Use the lowercase +single-token form everywhere (`csharp`, not `c#`). + +## Prerequisites +- Run from the codegraph repo root. `node`, `git`, `gh`, and a logged-in + `claude` CLI (the benchmark spawns real `claude -p` runs). +- The benchmark uses the local dev build — Step 8 builds + links it on PATH. + +## Workflow + +Copy this checklist and work through it in order: +``` +- [ ] 1. Resolve language; bail early if already supported (just benchmark) +- [ ] 2. Find a grammar + health-check it (ABI / heap corruption) +- [ ] 3. Discover the grammar's AST node types (dump-ast.mjs) +- [ ] 4. Wire the language (4 files; sometimes a 5th core touch) +- [ ] 5. Build + verify-extraction loop until PASS +- [ ] 6. Add extraction tests; make them green +- [ ] 7. Auto-pick 3 popular repos by size tier; add to corpus.json +- [ ] 8. Benchmark all 3: extraction + with/without A/B +- [ ] 9. Update README + CHANGELOG +- [ ] 10. Report; do NOT commit +``` + +### Step 1 — Resolve + short-circuit + +Check whether the language is already wired: look for the token in the +`LANGUAGES` const (`src/types.ts`) and the `EXTRACTORS` map +(`src/extraction/languages/index.ts`). If it is already supported (e.g. +`typescript`, `rust`), **skip Steps 2–6** and go straight to benchmarking +(Steps 7–8) to validate/measure it — note in the report that no code changed. + +### Step 2 — Find a grammar, then health-check it + +```bash +ls node_modules/tree-sitter-wasms/out/ | grep -i # csharp -> c_sharp +``` +- **Present** → likely off-the-shelf; `grammars.ts` resolves it from + `tree-sitter-wasms` automatically. (Many languages: elixir, zig, ocaml, + solidity, toml, yaml, …) +- **Absent** → vendor a `.wasm` into `src/extraction/wasm/` (like `pascal` / + `scala` / `lua`) and add the token to the vendored branch in Step 4. + +**Always health-check before writing an extractor — a *present* grammar can +still be unusable:** +```bash +node scripts/add-lang/check-grammar.mjs path/to/valid-sample. +``` +It prints the grammar's ABI version and parses a valid sample many times in a +multi-grammar runtime. If it **FAILs** (ERROR trees on valid code — an old ABI +corrupting the shared WASM heap, which silently drops nested calls/imports on +every file after the first; e.g. the tree-sitter-wasms **Lua** grammar is ABI 13 +and fails), do NOT use that wasm. **Vendor a newer (ABI 14/15) build instead:** +```bash +npm pack @tree-sitter-grammars/tree-sitter- # often ships a prebuilt *.wasm +# or build one: npx tree-sitter build --wasm (needs Docker/emscripten) +cp .wasm src/extraction/wasm/tree-sitter-.wasm +``` +then add the token to the vendored branch in Step 4 and re-run check-grammar on +the vendored path until it PASSes. **If you cannot obtain a healthy wasm, STOP +and tell the user.** + +### Step 3 — Discover AST node types + +Get a representative source file (write a small sample covering functions, +classes/structs, imports, enums; or `curl` a raw file from a known repo), then: +```bash +node scripts/add-lang/dump-ast.mjs path/to/sample. +# vendored grammar: pass the wasm path instead of the token +node scripts/add-lang/dump-ast.mjs src/extraction/wasm/tree-sitter-.wasm sample. +``` +The frequency table + field names (`name:`, `parameters:`, `body:`, +`return_type:`) tell you what to map. Open the existing extractor closest to the +language's paradigm as a model: `rust.ts`/`scala.ts` (functional, traits), +`java.ts`/`csharp.ts` (OO), `python.ts`/`ruby.ts` (scripting), `go.ts` +(top-level methods + receivers). + +### Step 4 — Wire the language (4 files) + +These are exact, fragile wiring — match the existing style precisely: + +1. **`src/types.ts`** — TWO edits: + - add `'',` to the `LANGUAGES` const (before `'unknown'`); + - add `'**/*.',` to `DEFAULT_CONFIG.include`. **Don't skip this** — it's + the file-scan allowlist; without the glob, `codegraph init` finds **0 + files** even though detection/extraction are wired. +2. **`src/extraction/grammars.ts`** — three maps: + - `WASM_GRAMMAR_FILES`: `: 'tree-sitter-.wasm',` + - `EXTENSION_MAP`: each file extension → `''` (e.g. `'.lua': 'lua',`) + - `getLanguageDisplayName`: `: '',` + - **vendored only**: add `` to the + `(lang === 'pascal' || lang === 'scala' || …)` wasm-path branch. +3. **`src/extraction/languages/.ts`** — new file exporting + `export const Extractor: LanguageExtractor = { … }`. Map the node types + from Step 3. Required fields: `functionTypes`, `classTypes`, `methodTypes`, + `interfaceTypes`, `structTypes`, `enumTypes`, `typeAliasTypes`, + `importTypes`, `callTypes`, `variableTypes`, `nameField`, `bodyField`, + `paramsField`. Add hooks as the grammar needs them (`getSignature`, + `getVisibility`, `isExported`, `extractImport`, `visitNode`, `getReceiverType`, + `interfaceKind`, `enumMemberTypes`, etc. — see + `src/extraction/tree-sitter-types.ts`). +4. **`src/extraction/languages/index.ts`** — `import { Extractor } from + './';` and add `: Extractor,` to `EXTRACTORS`. + +**Sometimes a 5th, core touch in `src/extraction/tree-sitter.ts`** — variable +extraction has per-language branches in `extractVariable` (the generic fallback +only finds direct `identifier`/`variable_declarator` children). If the grammar +nests declared names (e.g. Lua's `variable_declaration → variable_list`), add a +`} else if (this.language === '')` branch there, mirroring the existing +ts/python/go ones. Import forms that aren't a distinct node (Lua/Ruby `require` +is a *call*) are handled in the extractor's `visitNode` hook instead. + +### Step 5 — Build + verify loop + +```bash +npm run build # tsc + copy-assets (copies any vendored *.wasm into dist/) +``` +Index a small sample repo and check extraction: +```bash +( cd && codegraph init -i ) +node scripts/add-lang/verify-extraction.mjs +``` +`verify-extraction.mjs` fails (exit 1) if the language isn't detected or only +`file`/`import` nodes were produced — the classic symptom of wrong node-type +names. On FAIL or a thin WARN: re-run `dump-ast.mjs` on a richer file, fix the +mappings in `.ts`, `npm run build`, re-index, re-verify. **Repeat until +PASS.** + +### Step 6 — Tests + +Add to `__tests__/extraction.test.ts`, modeled on the `Rust Extraction` block: +- a `detectLanguage` assertion in `describe('Language Detection')` +- a `describe(' Extraction')` block asserting functions/classes/imports + are extracted from an inline source string. +```bash +npx vitest run __tests__/extraction.test.ts +``` +Green before continuing. + +### Step 7 — Auto-pick 3 repos + corpus + +Pick **without asking**. Find candidates, then curate 3 that are genuinely +``-dominant, one per size tier: +```bash +gh search repos --language= --sort=stars --limit 40 \ + --json fullName,stargazerCount,description +``` +Tiers (match `corpus.json`): **Small** <~150 files · **Medium** ~150–1500 · +**Large** >~1500. Skip repos that are tagged `` but mostly another +language. Write one cross-file architecture **question** per repo (the kind that +needs tracing across files). Add a `""` block to +`.claude/skills/agent-eval/corpus.json` (fields: `name`, `repo`, `size`, +`files`, `question`) so `/agent-eval` can reuse them. + +### Step 8 — Benchmark all 3 (extraction + A/B) + +Make the dev build the codegraph on PATH **once**, then loop: +```bash +npm run build && ./scripts/local-install.sh +scripts/add-lang/bench.sh "" headless # ×3 +``` +`bench.sh` clones (shared `/tmp/codegraph-corpus`), wipes + indexes, runs +`verify-extraction.mjs`, then the with/without retrieval A/B via +`scripts/agent-eval/run-all.sh` (skips the paid A/B if extraction is broken). +Read each `parse-run.mjs` summary printed by `run-all.sh`: tool calls, file +`Read`s, Grep/Bash, codegraph-tool calls, duration, and **cost** — for both the +`with` and `without` arms. After the loop, restore the dev link if needed: +`./scripts/local-install.sh`. + +### Step 9 — Docs + CHANGELOG + +- **README.md**: add `` to the "19+ Languages" feature bullet, and add a + row to the **Supported Languages** table: + `| | \`.ext\` | Full support (classes, methods, …) |`. +- **CHANGELOG.md**: add an `## [Unreleased]` section at the top (above the + latest version) with `### Added` → a user-perspective bullet, e.g. + *"CodeGraph now indexes **** (`.ext`) — functions, classes, imports, and + call edges."* If `## [Unreleased]` already exists, append under it. (`/publish` + folds this into the next versioned block at release time.) + +### Step 10 — Report (do NOT commit) + +Summarize for review: +- **Files changed**: the 4 wiring edits + new extractor + tests + README + + CHANGELOG + corpus.json (+ any vendored `.wasm`). +- **Extraction** per repo: files / nodes / edges / `verify-extraction` result. +- **A/B** per repo: `with` vs `without` (tool calls, file Reads, cost) and a + one-line verdict — did codegraph reduce effort, and did both arms reach a + correct answer? +- **Gaps / follow-ups** (node types not yet mapped, resolution edges missing, + framework routes, etc.). + +Hand the changes to the user. **Do not** run `git commit`/`push`, +`npm publish`, or `scripts/release.sh`. + +## Notes +- The A/B spawns real **paid** `claude -p` runs (opus, `--max-budget-usd`), + 2 arms × 3 repos. The corpus dir `/tmp/codegraph-corpus` is shared with + `/agent-eval`, so clones are reused across runs. +- Any new `*.wasm` must live in `src/extraction/wasm/` — `copy-assets` (run by + `npm run build`) ships it; otherwise it won't be in `dist/`. +- An index must be served by the **same** binary that built it. Step 8 builds + + links the dev build first, so this holds. +- If a grammar can't be obtained, or extraction can't reach PASS, **STOP and + report** — don't ship a half-wired language. diff --git a/.claude/skills/agent-eval/corpus.json b/.claude/skills/agent-eval/corpus.json index 6e22352..3dcc875 100644 --- a/.claude/skills/agent-eval/corpus.json +++ b/.claude/skills/agent-eval/corpus.json @@ -59,5 +59,15 @@ ], "Svelte": [ { "name": "shadcn-svelte", "repo": "https://github.com/huntabyte/shadcn-svelte", "size": "Medium", "files": "~600", "question": "How do shadcn-svelte components compose and apply their styling?" } + ], + "Lua": [ + { "name": "lualine.nvim", "repo": "https://github.com/nvim-lualine/lualine.nvim", "size": "Small", "files": "~120", "question": "How does lualine assemble and render its statusline sections and components?" }, + { "name": "telescope.nvim", "repo": "https://github.com/nvim-telescope/telescope.nvim", "size": "Medium", "files": "~80", "question": "How does Telescope wire a picker to its finder, sorter, and previewer?" }, + { "name": "kong", "repo": "https://github.com/Kong/kong", "size": "Large", "files": "~1330", "question": "How does Kong execute plugins across a request's lifecycle phases?" } + ], + "Luau": [ + { "name": "Knit", "repo": "https://github.com/Sleitnick/Knit", "size": "Small", "files": "~10", "question": "How does Knit register services and expose them to clients?" }, + { "name": "vide", "repo": "https://github.com/centau/vide", "size": "Small", "files": "~40", "question": "How does vide track reactive sources and re-run effects when state changes?" }, + { "name": "Fusion", "repo": "https://github.com/dphfox/Fusion", "size": "Medium", "files": "~115", "question": "How does Fusion build and update its reactive UI graph from state objects?" } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 321721a..9b924af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ a [GitHub Release](https://github.com/colbymchenry/codegraph/releases) tagged This project follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added +- **Lua**: CodeGraph now indexes Lua (`.lua`) — functions, methods (table `t.f` + and `t:m` definitions become methods with a `t::f` receiver-qualified name), + local variables, `require(...)` imports, and the call edges between them. + Querying a Lua project (Neovim plugins, Kong, OpenResty, game code) now + surfaces its modules, methods, and call graph. +- **Luau** ([#232](https://github.com/colbymchenry/codegraph/issues/232)): + CodeGraph now indexes Luau (`.luau`), Roblox's typed superset of Lua — + everything Lua extracts, plus `type` / `export type` aliases, typed function + signatures, generics, and Roblox instance-path `require(script.Parent.X)` + imports. + ## [0.8.0] - 2026-05-20 ### Added diff --git a/README.md b/README.md index 559e884..d4dc3bf 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ The gains scale with codebase size: on large repos the agent answers from the in | **Full-Text Search** | Find code by name instantly across your entire codebase, powered by FTS5 | | **Impact Analysis** | Trace callers, callees, and the full impact radius of any symbol before making changes | | **Always Fresh** | File watcher uses native OS events (FSEvents/inotify/ReadDirectoryChangesW) with debounced auto-sync — the graph stays current as you code, zero config | -| **19+ Languages** | TypeScript, JavaScript, Python, Go, Rust, Java, C#, PHP, Ruby, C, C++, Swift, Kotlin, Dart, Svelte, Liquid, Pascal/Delphi | +| **19+ Languages** | TypeScript, JavaScript, Python, Go, Rust, Java, C#, PHP, Ruby, C, C++, Swift, Kotlin, Dart, Lua, Luau, Svelte, Liquid, Pascal/Delphi | | **Framework-aware Routes** | Recognizes web-framework routing files and links URL patterns to their handlers across 13 frameworks | | **100% Local** | No data leaves your machine. No API keys. No external services. SQLite database only | @@ -447,6 +447,8 @@ The `.codegraph/config.json` file controls indexing: | Vue | `.vue` | Full support (script + script-setup extraction, Nuxt page/API/middleware routes) | | Liquid | `.liquid` | Full support | | Pascal / Delphi | `.pas`, `.dpr`, `.dpk`, `.lpr` | Full support (classes, records, interfaces, enums, DFM/FMX form files) | +| Lua | `.lua` | Full support (functions, methods with receivers, local variables, `require` imports, call edges) | +| Luau | `.luau` | Full support (everything in Lua, plus `type`/`export type` aliases, typed signatures, and Roblox instance-path `require`) | ## Troubleshooting diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index b08408a..1b12147 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -3722,3 +3722,180 @@ class Svc { expect(decoratedNode?.name).toBe('method'); }); }); + +// ============================================================================= +// Lua +// ============================================================================= + +describe('Lua Extraction', () => { + describe('Language detection', () => { + it('should detect Lua files', () => { + expect(detectLanguage('init.lua')).toBe('lua'); + expect(detectLanguage('src/util.lua')).toBe('lua'); + }); + + it('should report Lua as supported', () => { + expect(isLanguageSupported('lua')).toBe(true); + expect(getSupportedLanguages()).toContain('lua'); + }); + }); + + describe('Function extraction', () => { + it('should extract global and local functions', () => { + const code = ` +function configure(opts) return opts end +local function helper(x) return x * 2 end +`; + const result = extractFromSource('init.lua', code); + const funcs = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name); + expect(funcs).toContain('configure'); + expect(funcs).toContain('helper'); + const configure = result.nodes.find((n) => n.name === 'configure'); + expect(configure?.language).toBe('lua'); + expect(configure?.signature).toBe('(opts)'); + }); + + it('should split table/method functions into a receiver and method name', () => { + const code = ` +function M.connect(host, port) return host end +function M:send(data) return self end +`; + const result = extractFromSource('init.lua', code); + const methods = result.nodes.filter((n) => n.kind === 'method'); + const connect = methods.find((m) => m.name === 'connect'); + expect(connect?.qualifiedName).toBe('M::connect'); + const send = methods.find((m) => m.name === 'send'); + expect(send?.qualifiedName).toBe('M::send'); + }); + }); + + describe('Variable extraction', () => { + it('should extract local variable declarations', () => { + const code = ` +local M = {} +local count = 0 +`; + const result = extractFromSource('mod.lua', code); + const vars = result.nodes.filter((n) => n.kind === 'variable').map((n) => n.name); + expect(vars).toContain('M'); + expect(vars).toContain('count'); + }); + }); + + describe('Import extraction (require)', () => { + it('should extract require() in local declarations and bare calls', () => { + const code = ` +local socket = require("socket") +local http = require "resty.http" +require("side.effect") +`; + const result = extractFromSource('net.lua', code); + const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name); + expect(imports).toContain('socket'); + expect(imports).toContain('resty.http'); + expect(imports).toContain('side.effect'); + + const ref = result.unresolvedReferences.find( + (r) => r.referenceKind === 'imports' && r.referenceName === 'socket' + ); + expect(ref).toBeDefined(); + }); + + // Regression: the tree-sitter-wasms Lua grammar (ABI 13) corrupts the shared + // WASM heap under web-tree-sitter 0.25, dropping nested calls/imports on every + // parse after the first. We vendor the ABI-15 grammar instead — this guards it + // by extracting several sources in sequence and asserting the LAST still works. + it('should keep extracting require across many sequential parses', () => { + let last; + for (let i = 0; i < 8; i++) { + last = extractFromSource(`f${i}.lua`, `local m = require("module.${i}")\nreturn m\n`); + } + const imports = last!.nodes.filter((n) => n.kind === 'import').map((n) => n.name); + expect(imports).toContain('module.7'); + }); + }); + + describe('Call extraction', () => { + it('should record intra-file calls as resolvable references', () => { + const code = ` +local function helper(x) return x end +local function run(y) return helper(y) end +`; + const result = extractFromSource('calls.lua', code); + const call = result.unresolvedReferences.find( + (r) => r.referenceKind === 'calls' && r.referenceName === 'helper' + ); + expect(call).toBeDefined(); + }); + }); +}); + +// ============================================================================= +// Luau (typed superset of Lua — https://luau.org) +// ============================================================================= + +describe('Luau Extraction', () => { + describe('Language detection', () => { + it('should detect Luau files', () => { + expect(detectLanguage('init.luau')).toBe('luau'); + expect(detectLanguage('src/Client.luau')).toBe('luau'); + }); + + it('should report Luau as supported', () => { + expect(isLanguageSupported('luau')).toBe(true); + expect(getSupportedLanguages()).toContain('luau'); + }); + }); + + describe('Type aliases', () => { + it('should extract `type` and `export type` definitions', () => { + const code = ` +export type Vector = { x: number, y: number } +type Handler = (msg: string) -> boolean +`; + const result = extractFromSource('types.luau', code); + const aliases = result.nodes.filter((n) => n.kind === 'type_alias'); + const vector = aliases.find((a) => a.name === 'Vector'); + expect(vector).toBeDefined(); + expect(vector?.isExported).toBe(true); + const handler = aliases.find((a) => a.name === 'Handler'); + expect(handler).toBeDefined(); + expect(handler?.isExported).toBe(false); + }); + }); + + describe('Typed functions and methods', () => { + it('should capture typed signatures and split methods by receiver', () => { + const code = ` +function configure(opts: { debug: boolean }): boolean + return opts.debug +end +function Client:fetch(path: string): Response + return path +end +`; + const result = extractFromSource('client.luau', code); + const configure = result.nodes.find((n) => n.kind === 'function' && n.name === 'configure'); + expect(configure?.language).toBe('luau'); + expect(configure?.signature).toBe('(opts: { debug: boolean }): boolean'); + const fetch = result.nodes.find((n) => n.kind === 'method' && n.name === 'fetch'); + expect(fetch?.qualifiedName).toBe('Client::fetch'); + }); + }); + + describe('Imports and variables', () => { + it('should extract string and Roblox instance-path require imports', () => { + const code = ` +local http = require("http") +local Signal = require(script.Parent.Signal) +local count = 0 +`; + const result = extractFromSource('mod.luau', code); + const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name); + expect(imports).toContain('http'); // string require + expect(imports).toContain('Signal'); // Roblox instance-path require + const vars = result.nodes.filter((n) => n.kind === 'variable').map((n) => n.name); + expect(vars).toContain('count'); + }); + }); +}); diff --git a/scripts/add-lang/bench.sh b/scripts/add-lang/bench.sh new file mode 100755 index 0000000..172fe40 --- /dev/null +++ b/scripts/add-lang/bench.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Add-lang benchmark for ONE repo: +# clone -> wipe+index (with the codegraph on PATH) -> verify extraction -> +# with/without retrieval A/B (reuses scripts/agent-eval/run-all.sh). +# +# Assumes the codegraph dev build is already built + linked on PATH — the skill +# runs `npm run build && ./scripts/local-install.sh` ONCE before looping repos. +# The A/B is skipped if extraction fails its critical checks (don't burn $ on a +# broken extractor); set FORCE_AB=1 to run it anyway. +# +# Usage: bench.sh "" [headless|tmux|all] +# Env: CORPUS corpus dir (default /tmp/codegraph-corpus, shared with agent-eval) +set -uo pipefail + +LANG_TOKEN="${1:?usage: bench.sh \"\" [mode]}" +NAME="${2:?repo-name required}" +URL="${3:?repo-url required}" +Q="${4:?question required}" +MODE="${5:-headless}" + +HARNESS="$(cd "$(dirname "$0")" && pwd)" +AGENT_EVAL="$(cd "$HARNESS/../agent-eval" && pwd)" +CORPUS="${CORPUS:-/tmp/codegraph-corpus}" +REPO="$CORPUS/$NAME" + +command -v codegraph >/dev/null || { echo "no codegraph on PATH (build + ./scripts/local-install.sh first)"; exit 1; } + +echo "==================== add-lang bench: $NAME ($LANG_TOKEN) ====================" +echo "codegraph: $(command -v codegraph) -> $(codegraph --version 2>/dev/null || echo '?')" + +# 1. Ensure the repo (shallow clone, reuse if present). +mkdir -p "$CORPUS" +if [ -d "$REPO/.git" ]; then + echo "→ reusing checkout: $REPO" +else + echo "→ cloning $URL" + git clone --depth 1 "$URL" "$REPO" || { echo "git clone failed"; exit 1; } +fi + +# 2. Wipe + index with the binary under test. +echo "→ wiping .codegraph and indexing" +rm -rf "$REPO/.codegraph" +( cd "$REPO" && codegraph init -i ) || { echo "indexing failed"; exit 1; } + +# 3. Verify extraction (cheap guard before the paid A/B). +echo "→ verifying extraction" +node "$HARNESS/verify-extraction.mjs" "$REPO" "$LANG_TOKEN" +VERIFY=$? + +# 4. Retrieval A/B (skipped if extraction is broken, unless FORCE_AB=1). +if [ "$VERIFY" -ne 0 ] && [ "${FORCE_AB:-0}" != "1" ]; then + echo "→ SKIPPING A/B — extraction failed critical checks (set FORCE_AB=1 to override)" +else + echo "→ retrieval A/B (mode=$MODE)" + bash "$AGENT_EVAL/run-all.sh" "$REPO" "$Q" "$MODE" +fi + +echo "==================== bench complete: $NAME (verify exit=$VERIFY) ====================" +# Exit reflects extraction: 0 = pass/warn, 1 = critical fail, 2 = couldn't read status. +exit "$VERIFY" diff --git a/scripts/add-lang/check-grammar.mjs b/scripts/add-lang/check-grammar.mjs new file mode 100755 index 0000000..461b129 --- /dev/null +++ b/scripts/add-lang/check-grammar.mjs @@ -0,0 +1,75 @@ +#!/usr/bin/env node +// Verify a tree-sitter grammar wasm is HEALTHY under the project's web-tree-sitter +// runtime BEFORE writing an extractor. Prints the ABI version and parses a valid +// sample many times in a multi-grammar context, to catch heap-corruption bugs +// that silently drop nodes on every parse after the first. +// +// Why this exists: the tree-sitter-wasms Lua grammar is ABI 13 and corrupts the +// shared WASM heap under web-tree-sitter 0.25 — Lua extraction degraded on every +// file after the first (nested calls/imports vanished). The fix was to vendor the +// upstream ABI-15 wasm. Run this on any new grammar first; if it FAILs, vendor a +// newer build instead of using the tree-sitter-wasms one. +// +// Usage: node scripts/add-lang/check-grammar.mjs [iterations] +// Exit: 0 healthy, 1 corruption / parse errors, 2 could not run. +// NOTE: the sample must be SYNTACTICALLY VALID — a broken sample fails for the +// wrong reason. + +import { readFileSync, existsSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { Parser, Language } from 'web-tree-sitter'; + +const require = createRequire(import.meta.url); +const fail = (code, msg) => { console.error(`[check-grammar] ${msg}`); process.exit(code); }; + +const [token, sample, iterArg] = process.argv.slice(2); +if (!token || !sample) fail(2, 'usage: check-grammar.mjs [iterations]'); +if (!existsSync(sample)) fail(2, `sample not found: ${sample}`); +const iters = iterArg ? parseInt(iterArg, 10) : 20; + +const SPECIAL = { csharp: 'c_sharp', 'c#': 'c_sharp' }; +function resolveWasm(t) { + if (t.endsWith('.wasm')) return existsSync(t) ? t : fail(2, `wasm not found: ${t}`); + const base = SPECIAL[t.toLowerCase()] ?? t.toLowerCase(); + try { return require.resolve(`tree-sitter-wasms/out/tree-sitter-${base}.wasm`); } catch { /* try vendored */ } + const vendored = `src/extraction/wasm/tree-sitter-${base}.wasm`; + if (existsSync(vendored)) return vendored; + return fail(2, `no grammar for "${t}" — not in tree-sitter-wasms and not vendored`); +} + +const wasmPath = resolveWasm(token); +const source = readFileSync(sample, 'utf8'); + +try { await Parser.init(); } +catch { await Parser.init({ locateFile: () => require.resolve('web-tree-sitter/tree-sitter.wasm') }); } + +// Load a second, known-good grammar — the corruption surfaces under the +// multi-grammar runtime that real indexing uses, not a single grammar in isolation. +try { await Language.load(require.resolve('tree-sitter-wasms/out/tree-sitter-python.wasm')); } catch { /* ok */ } + +let language; +try { language = await Language.load(wasmPath); } +catch (e) { fail(2, `failed to load ${wasmPath}: ${e.message}`); } + +const parser = new Parser(); +parser.setLanguage(language); + +let ok = 0, err = 0; +for (let i = 0; i < iters; i++) { + const tree = parser.parse(source); + if (tree.rootNode.hasError) err++; else ok++; +} + +console.log(`grammar: ${wasmPath.split('/').pop()}`); +console.log(` ABI version: ${language.abiVersion}`); +console.log(` parses: ${ok} clean / ${err} with errors (of ${iters})`); +if (err > 0) { + console.log( + `RESULT: FAIL — ${err}/${iters} parses produced ERROR trees on a valid sample. ` + + `This grammar corrupts under web-tree-sitter; vendor a newer (ABI 14/15) wasm ` + + `(see SKILL.md "Find a grammar"). Confirm your sample is syntactically valid first.` + ); + process.exit(1); +} +console.log('RESULT: PASS — grammar parses cleanly and reuses safely.'); +process.exit(0); diff --git a/scripts/add-lang/dump-ast.mjs b/scripts/add-lang/dump-ast.mjs new file mode 100755 index 0000000..26406b0 --- /dev/null +++ b/scripts/add-lang/dump-ast.mjs @@ -0,0 +1,103 @@ +#!/usr/bin/env node +// Dump the tree-sitter AST for a sample file so you can write a LanguageExtractor +// mapping. Loads a grammar .wasm directly via web-tree-sitter (the same runtime +// codegraph uses) — you do NOT need to register the language first. +// +// Usage: +// node scripts/add-lang/dump-ast.mjs [--depth=N] [--full] +// Examples: +// node scripts/add-lang/dump-ast.mjs lua sample.lua +// node scripts/add-lang/dump-ast.mjs src/extraction/wasm/tree-sitter-zig.wasm a.zig --depth=4 +// +// Output: an indented AST (named nodes, with field names) followed by a +// node-type FREQUENCY table. The frequency table is the payoff — it tells you +// which node types to map to functionTypes / classTypes / importTypes / etc. + +import { readFileSync, existsSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { Parser, Language } from 'web-tree-sitter'; + +const require = createRequire(import.meta.url); +const fail = (msg) => { console.error(`[dump-ast] ${msg}`); process.exit(1); }; + +const argv = process.argv.slice(2); +const positional = argv.filter((a) => !a.startsWith('--')); +const [langOrWasm, sampleFile] = positional; +const depthFlag = argv.find((a) => a.startsWith('--depth=')); +const showAll = argv.includes('--full'); // also print anonymous (token) nodes +const maxDepth = depthFlag ? parseInt(depthFlag.split('=')[1], 10) : (showAll ? Infinity : 8); + +if (!langOrWasm || !sampleFile) { + fail('usage: dump-ast.mjs [--depth=N] [--full]'); +} +if (!existsSync(sampleFile)) fail(`sample file not found: ${sampleFile}`); + +// Language tokens whose tree-sitter-wasms filename differs from the token. +const WASM_SPECIAL = { csharp: 'c_sharp', 'c#': 'c_sharp' }; + +function resolveWasm(token) { + if (token.endsWith('.wasm')) { + if (!existsSync(token)) fail(`wasm not found: ${token}`); + return token; + } + const base = WASM_SPECIAL[token.toLowerCase()] ?? token.toLowerCase(); + try { + return require.resolve(`tree-sitter-wasms/out/tree-sitter-${base}.wasm`); + } catch { + /* not in tree-sitter-wasms — try a vendored copy */ + } + const vendored = `src/extraction/wasm/tree-sitter-${base}.wasm`; + if (existsSync(vendored)) return vendored; + fail( + `no grammar for "${token}" — not in tree-sitter-wasms and not vendored at ` + + `${vendored}. Pass an explicit .wasm path, or vendor one (see SKILL.md "Find a grammar").` + ); +} + +const wasmPath = resolveWasm(langOrWasm); +const source = readFileSync(sampleFile, 'utf8'); + +try { + await Parser.init(); +} catch { + await Parser.init({ locateFile: () => require.resolve('web-tree-sitter/tree-sitter.wasm') }); +} + +let language; +try { + language = await Language.load(wasmPath); +} catch (e) { + fail(`failed to load grammar ${wasmPath}: ${e.message}`); +} + +const parser = new Parser(); +parser.setLanguage(language); +const tree = parser.parse(source); + +const freq = new Map(); +const snippet = (node) => { + const t = node.text.replace(/\s+/g, ' ').trim(); + return t.length > 48 ? `${t.slice(0, 48)}…` : t; +}; + +function walk(node, depth, fieldName) { + if (node.isNamed) freq.set(node.type, (freq.get(node.type) || 0) + 1); + if ((node.isNamed || showAll) && depth <= maxDepth) { + const field = fieldName ? `${fieldName}: ` : ''; + const leaf = node.childCount === 0 ? ` "${snippet(node)}"` : ''; + console.log(`${' '.repeat(depth)}${field}${node.type} @${node.startPosition.row + 1}:${node.startPosition.column}${leaf}`); + } + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child) walk(child, depth + 1, node.fieldNameForChild(i)); + } +} + +console.log(`\n# AST for ${sampleFile} (grammar: ${wasmPath.split('/').pop()})\n`); +walk(tree.rootNode, 0, null); + +console.log('\n# Node-type frequency (named nodes) — map the relevant ones in your extractor:\n'); +[...freq.entries()] + .sort((a, b) => b[1] - a[1]) + .forEach(([type, n]) => console.log(` ${String(n).padStart(5)} ${type}`)); +console.log(); diff --git a/scripts/add-lang/verify-extraction.mjs b/scripts/add-lang/verify-extraction.mjs new file mode 100755 index 0000000..bdb443e --- /dev/null +++ b/scripts/add-lang/verify-extraction.mjs @@ -0,0 +1,70 @@ +#!/usr/bin/env node +// Sanity-check that codegraph extracted REAL symbols (not just file/import nodes) +// from a repo for a given language. Exits non-zero on a critical failure so it +// can drive a write-extractor -> build -> re-check loop. +// +// Usage: node scripts/add-lang/verify-extraction.mjs +// Reads `codegraph status --json` using whatever codegraph is on PATH, +// so it reflects the binary that built the index. +// +// Exit codes: 0 = pass or soft-warn, 1 = critical fail, 2 = could not run. + +import { execFileSync } from 'node:child_process'; + +const [repo, lang] = process.argv.slice(2); +if (!repo || !lang) { + console.error('usage: verify-extraction.mjs '); + process.exit(2); +} + +let status; +try { + const out = execFileSync('codegraph', ['status', repo, '--json'], { encoding: 'utf8' }); + status = JSON.parse(out); +} catch (e) { + console.error(`[verify] could not read codegraph status for ${repo}: ${e.message}`); + process.exit(2); +} + +// Kinds that prove the extractor mapped AST node types (everything except +// 'file' and 'import', which codegraph creates structurally for any language). +const SYMBOL_KINDS = new Set([ + 'module', 'class', 'struct', 'interface', 'trait', 'protocol', 'function', + 'method', 'property', 'field', 'variable', 'constant', 'enum', 'enum_member', + 'type_alias', 'namespace', 'route', 'component', +]); + +const byKind = status.nodesByKind || {}; +const langs = status.languages || []; +const files = status.fileCount || 0; +const edges = status.edgeCount || 0; +const symbolKinds = Object.keys(byKind).filter((k) => SYMBOL_KINDS.has(k)); +const symbolCount = symbolKinds.reduce((s, k) => s + byKind[k], 0); + +const checks = []; +const add = (severity, ok, label, detail) => checks.push({ severity, ok, label, detail }); + +add('critical', status.initialized === true, 'index initialized', `initialized=${status.initialized}`); +add('critical', langs.includes(lang), `language "${lang}" detected`, `languages=[${langs.join(', ')}]`); +add('critical', symbolCount > 0, 'structural symbols extracted', `${symbolCount} symbols (${symbolKinds.join(', ') || 'NONE — only file/import nodes!'})`); +add('soft', symbolCount >= files, 'symbol density >= 1/file', `${symbolCount} symbols across ${files} files`); +add('soft', edges > files, 'edges resolved', `${edges} edges across ${files} files`); + +console.log(`\n# Extraction check — ${repo} (lang=${lang}, backend=${status.backend})`); +console.log(` files=${files} nodes=${status.nodeCount} edges=${edges}`); +console.log(` nodesByKind: ${JSON.stringify(byKind)}\n`); +for (const c of checks) console.log(` ${c.ok ? '✓' : '✗'} ${c.label} — ${c.detail}`); + +const critical = checks.filter((c) => !c.ok && c.severity === 'critical'); +const soft = checks.filter((c) => !c.ok && c.severity === 'soft'); +console.log(); +if (critical.length) { + console.log(`RESULT: FAIL (${critical.length} critical) — extractor or grammar wiring is broken. Re-run dump-ast.mjs and fix the node-type mappings.`); + process.exit(1); +} +if (soft.length) { + console.log(`RESULT: WARN (${soft.length} soft) — extraction works but looks thin; inspect the counts above.`); + process.exit(0); +} +console.log('RESULT: PASS — extraction looks healthy.'); +process.exit(0); diff --git a/src/extraction/grammars.ts b/src/extraction/grammars.ts index d154042..15f224d 100644 --- a/src/extraction/grammars.ts +++ b/src/extraction/grammars.ts @@ -35,6 +35,8 @@ const WASM_GRAMMAR_FILES: Record = { dart: 'tree-sitter-dart.wasm', pascal: 'tree-sitter-pascal.wasm', scala: 'tree-sitter-scala.wasm', + lua: 'tree-sitter-lua.wasm', + luau: 'tree-sitter-luau.wasm', }; /** @@ -78,6 +80,8 @@ export const EXTENSION_MAP: Record = { '.fmx': 'pascal', '.scala': 'scala', '.sc': 'scala', + '.lua': 'lua', + '.luau': 'luau', }; /** @@ -125,8 +129,12 @@ export async function loadGrammarsForLanguages(languages: Language[]): Promise> = { typescript: typescriptExtractor, @@ -43,4 +45,6 @@ export const EXTRACTORS: Partial> = { dart: dartExtractor, pascal: pascalExtractor, scala: scalaExtractor, + lua: luaExtractor, + luau: luauExtractor, }; diff --git a/src/extraction/languages/lua.ts b/src/extraction/languages/lua.ts new file mode 100644 index 0000000..31094dc --- /dev/null +++ b/src/extraction/languages/lua.ts @@ -0,0 +1,152 @@ +import type { Node as SyntaxNode } from 'web-tree-sitter'; +import { getNodeText, getChildByField } from '../tree-sitter-helpers'; +import type { LanguageExtractor } from '../tree-sitter-types'; + +// Node names follow the vendored ABI-15 grammar (@tree-sitter-grammars/ +// tree-sitter-lua), NOT the older tree-sitter-wasms build — see grammars.ts. + +/** First descendant of a given type (breadth-first), or null. */ +function findDescendant(node: SyntaxNode, type: string): SyntaxNode | null { + const queue: SyntaxNode[] = [...node.namedChildren]; + while (queue.length) { + const n = queue.shift()!; + if (n.type === type) return n; + queue.push(...n.namedChildren); + } + return null; +} + +/** + * If `callNode` is a `require(...)` call, return the module name; otherwise null. + * Lua/Luau have no import statement — modules are loaded by calling the global + * `require`. Handles both: + * - string requires: `require("net.http")` / `require "net.http"` → "net.http" + * - Roblox/Luau path requires: `require(script.Parent.Signal)` → "Signal" + * (the dominant idiom in Roblox code, where the argument is an instance path + * rather than a string — use the trailing field as the module name). + */ +function requireModule(callNode: SyntaxNode, source: string): string | null { + // function_call > name: , arguments: arguments + const name = getChildByField(callNode, 'name'); + // A dotted/colon callee (e.g. `socket.connect`) is dot/method_index_expression, + // never a bare `require`. + if (!name || name.type !== 'identifier') return null; + if (getNodeText(name, source) !== 'require') return null; + + const args = getChildByField(callNode, 'arguments'); + if (!args) return null; + + // String require — `string > content: string_content` gives the bare name. + const content = findDescendant(args, 'string_content'); + if (content) return getNodeText(content, source).trim() || null; + const str = findDescendant(args, 'string'); + if (str) { + const mod = getNodeText(str, source) + .trim() + .replace(/^\[\[/, '') + .replace(/\]\]$/, '') + .replace(/^["']/, '') + .replace(/["']$/, ''); + if (mod) return mod; + } + + // Roblox/Luau instance-path require: `require(script.Parent.Signal)` → "Signal". + const idx = findDescendant(args, 'dot_index_expression') ?? findDescendant(args, 'method_index_expression'); + if (idx) { + const field = getChildByField(idx, 'field') ?? getChildByField(idx, 'method'); + if (field) return getNodeText(field, source).trim() || null; + } + return null; +} + +export const luaExtractor: LanguageExtractor = { + // function_declaration covers global (`function f`), table (`function t.f`), + // method (`function t:m`), and local (`local function f`) forms — the form is + // distinguished by the `name:` child (identifier / dot_index_expression / + // method_index_expression) and a `local` token, not by separate node types. + // Anonymous `function() ... end` (function_definition) has no name and is + // captured via its enclosing variable instead. + functionTypes: ['function_declaration'], + classTypes: [], // Lua has no classes/structs/interfaces/enums — tables are used for everything + methodTypes: [], + interfaceTypes: [], + structTypes: [], + enumTypes: [], + typeAliasTypes: [], + importTypes: [], // `require` is a function_call — handled in visitNode below + callTypes: ['function_call'], + variableTypes: ['variable_declaration'], // see the `lua` branch in extractVariable + nameField: 'name', + bodyField: 'body', + paramsField: 'parameters', + + getSignature: (node, source) => { + const params = getChildByField(node, 'parameters'); + return params ? getNodeText(params, source) : undefined; + }, + + // `function t.f()` / `function t:m()` are methods on table `t`: return the + // table as the receiver so they extract as methods with a `t::f` qualified + // name. Plain `function f()` / `local function f()` have no receiver and stay + // functions. (For `a.b.c`, the receiver is the nested `a.b`.) + getReceiverType: (node, source) => { + const name = getChildByField(node, 'name'); + if (name && (name.type === 'dot_index_expression' || name.type === 'method_index_expression')) { + const table = getChildByField(name, 'table'); + if (table) return getNodeText(table, source); + } + return undefined; + }, + + // Emit import nodes for `require(...)`. The local-declaration form is handled + // explicitly because the variable branch skips the initializer subtree; bare + // and global `require` calls are caught when the walker reaches the + // function_call node. + visitNode: (node, ctx) => { + const source = ctx.source; + + const emit = (callNode: SyntaxNode): void => { + const mod = requireModule(callNode, source); + if (!mod) return; + const imp = ctx.createNode('import', mod, callNode, { + signature: getNodeText(callNode, source).trim().slice(0, 100), + }); + if (imp && ctx.nodeStack.length > 0) { + const parentId = ctx.nodeStack[ctx.nodeStack.length - 1]; + if (parentId) { + ctx.addUnresolvedReference({ + fromNodeId: parentId, + referenceName: mod, + referenceKind: 'imports', + line: callNode.startPosition.row + 1, + column: callNode.startPosition.column, + }); + } + } + }; + + // Bare / global `require("x")` — claim it so it isn't double-counted as a call. + if (node.type === 'function_call') { + if (requireModule(node, source)) { + emit(node); + return true; + } + return false; + } + + // `local x = require("x")` — variable_declaration wraps an assignment_statement + // whose initializer subtree the variable branch will skip, so dig it out here. + if (node.type === 'variable_declaration') { + const assign = node.namedChildren.find((c) => c.type === 'assignment_statement'); + const exprList = assign?.namedChildren.find((c) => c.type === 'expression_list'); + if (exprList) { + for (const val of exprList.namedChildren) { + if (val.type === 'function_call') emit(val); + } + } + return false; + } + + return false; + }, +}; diff --git a/src/extraction/languages/luau.ts b/src/extraction/languages/luau.ts new file mode 100644 index 0000000..f4f51a1 --- /dev/null +++ b/src/extraction/languages/luau.ts @@ -0,0 +1,36 @@ +import { getNodeText, getChildByField } from '../tree-sitter-helpers'; +import type { LanguageExtractor } from '../tree-sitter-types'; +import { luaExtractor } from './lua'; + +// Luau (https://luau.org) is a gradually-typed superset of Lua. The +// tree-sitter-luau grammar reuses the same node names as the vendored Lua +// grammar (function_declaration, variable_declaration, function_call, +// dot/method_index_expression, …), so the Luau extractor extends the Lua one +// and adds the type-system pieces Luau introduces: +// - `type X = ...` / `export type X = ...` → type_definition (type_alias) +// - typed parameters and return types → richer signatures +// +// require detection, receiver-splitting (t.f / t:m → methods), and local +// variable extraction are inherited unchanged from luaExtractor. The shared +// `extractVariable` core branch is gated on `lua` || `luau`. +export const luauExtractor: LanguageExtractor = { + ...luaExtractor, + + // `type X = ...` and `export type X = ...` + typeAliasTypes: ['type_definition'], + + // Only Luau `export type` is exported; the keyword leads the node. + isExported: (node, source) => source.slice(node.startIndex, node.startIndex + 7) === 'export ', + + // Params + Luau return type (the named child after `parameters`, before the body). + getSignature: (node, source) => { + const params = getChildByField(node, 'parameters'); + if (!params) return undefined; + let sig = getNodeText(params, source); + const kids = node.namedChildren; + const idx = kids.findIndex((c) => c.startIndex === params.startIndex); + const ret = idx >= 0 ? kids[idx + 1] : null; + if (ret && ret.type !== 'block') sig += `: ${getNodeText(ret, source)}`; + return sig; + }, +}; diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 00830ab..5a40c75 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -50,6 +50,17 @@ function extractName(node: SyntaxNode, source: string, extractor: LanguageExtrac const innerName = getChildByField(resolved, 'declarator') || resolved.namedChild(0); return innerName ? getNodeText(innerName, source) : getNodeText(resolved, source); } + // Lua: `function t.f()` / `function t:m()` — the name node is a dot/method + // index expression; the simple name is the trailing field/method (the table + // receiver is captured separately via getReceiverType). + if (resolved.type === 'dot_index_expression') { + const field = getChildByField(resolved, 'field'); + if (field) return getNodeText(field, source); + } + if (resolved.type === 'method_index_expression') { + const method = getChildByField(resolved, 'method'); + if (method) return getNodeText(method, source); + } return getNodeText(resolved, source); } @@ -1111,6 +1122,23 @@ export class TreeSitterExtractor { } } } + } else if (this.language === 'lua' || this.language === 'luau') { + // Lua/Luau: variable_declaration → assignment_statement → variable_list + // (name: identifier...) = expression_list. `local x, y = 1, 2` + // declares multiple names; only plain identifiers are locals. + const assign = node.namedChildren.find((c) => c.type === 'assignment_statement') ?? node; + const varList = assign.namedChildren.find((c) => c.type === 'variable_list'); + const exprList = assign.namedChildren.find((c) => c.type === 'expression_list'); + const values = exprList ? exprList.namedChildren : []; + const names = varList ? varList.namedChildren.filter((c) => c.type === 'identifier') : []; + names.forEach((nameNode, i) => { + const name = getNodeText(nameNode, this.source); + if (!name) return; + const valueNode = values[i]; + const initValue = valueNode ? getNodeText(valueNode, this.source).slice(0, 100) : undefined; + const initSignature = initValue ? `= ${initValue}${initValue.length >= 100 ? '...' : ''}` : undefined; + this.createNode(kind, name, nameNode, { docstring, signature: initSignature, isExported }); + }); } else { // Generic fallback for other languages // Try to find identifier children diff --git a/src/extraction/wasm/tree-sitter-lua.wasm b/src/extraction/wasm/tree-sitter-lua.wasm new file mode 100644 index 0000000000000000000000000000000000000000..be3231dcd4c1e188c3a24e256093b412346b6f7d GIT binary patch literal 49488 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 a1Tw>Me;qWoc`M)>kVRu6UIN|${{I7XDd2bj literal 0 HcmV?d00001 diff --git a/src/extraction/wasm/tree-sitter-luau.wasm b/src/extraction/wasm/tree-sitter-luau.wasm new file mode 100644 index 0000000000000000000000000000000000000000..1ed5af18fceca766bf96f07e6cb471414cc986c0 GIT binary patch literal 94204 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 literal 0 HcmV?d00001 diff --git a/src/types.ts b/src/types.ts index 328f743..f788040 100644 --- a/src/types.ts +++ b/src/types.ts @@ -85,6 +85,8 @@ export const LANGUAGES = [ 'liquid', 'pascal', 'scala', + 'lua', + 'luau', 'unknown', ] as const; @@ -545,6 +547,10 @@ export const DEFAULT_CONFIG: CodeGraphConfig = { // Scala '**/*.scala', '**/*.sc', + // Lua + '**/*.lua', + // Luau + '**/*.luau', ], exclude: [ // Version control