feat(luau): add Luau/Roblox language support (issue #52)#89
feat(luau): add Luau/Roblox language support (issue #52)#89CyanoTex wants to merge 4 commits intorepowise-dev:mainfrom
Conversation
Adds a `luau` tier covering `.luau` and `.lua` files: LanguageSpec,
LanguageTag, tree-sitter-luau grammar wiring, .scm captures for
function/type/require, a dedicated import resolver, and unit tests.
The resolver handles `require("rel/path")` and `script` /
`script.Parent` relative instance paths. `game.<Service>...` absolute
instance paths currently register as external nodes; full Rojo-aware
resolution via `default.project.json` is deferred to a follow-up and
pinned by an `xfail` test documenting the expected end state.
Replaces the existing git-blame-only `lua` spec at registry.py rather
than leaving two sources of truth for the same extension set.
Refs: repowise-dev#52
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
parser.py:705 normalizes the captured @import.module text with
.strip("\"'` ") before handing it to resolve_luau_import, but the
resolver's string-literal branch identified literals by checking
arg.startswith('"') — so every require("./path") landed on the
external-node fallback in production. The unit tests masked this by
passing explicitly-quoted strings, which the production parser never
emits.
Surfaced by running `repowise init --index-only` against the upstream
luau-lang/luau repo (per maintainer's request to test against live
systems): 146/146 string-literal requires — including 107 instances of
require("../bench_support") — went to external when they should have
resolved internally.
Fix:
- Drop the unreachable quote-detection branch. Detect literal paths
by elimination after the script./game. regex checks.
- Update the test class to pass *unquoted* input mirroring what the
parser actually emits. Add cases for ../ relative paths and stem
matching so the production handoff is exercised end-to-end.
- Document the parser contract in both the resolver and test module
docstrings so the next reader doesn't reintroduce the mismatch.
The @alias (.luaurc) branch is deferred alongside the existing Rojo
follow-up and records the reference as external for now.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `script.Parent:WaitForChild("Foo")` and `script.X:FindFirstChild("Foo")`
idioms are how shipping Rojo code loads modules — they guard against the
sync-order race between Rojo's filesystem stream and `require()`. On
OSRPS they account for **451 of 485** `require(...)` calls (93%);
the bare-dot-chain form that the previous resolver matched accounts for
only 13 (2.7%). Everything else fell through to the external-node
fallback, leaving the graph's Luau edges almost entirely unresolved.
Fix: normalize `:WaitForChild("Name")` / `:FindFirstChild("Name")` into
`.Name` before the `_SCRIPT_RELATIVE` regex runs. Both forms have
identical name-resolution semantics (child lookup by string on the
preceding instance), so the path-walking logic in
`_resolve_script_relative` is reused unchanged.
Design notes:
- `WaitForChild`'s optional second `timeoutSeconds` argument is
discarded by the regex — the timeout is runtime behavior and has no
bearing on the target module's name.
- External-node labels use the *original* pre-normalization text, so
the graph shows what was written at the call site rather than a
rewritten dot-chain form.
- Method chains like `...:WaitForChild("shared"):WaitForChild("Signal")`
normalize to `.shared.Signal` and resolve through the same code path.
Variable-aliased chains (e.g. `SharedFolder:WaitForChild("X")` where
`SharedFolder = ReplicatedStorage:WaitForChild("OSRPS")`) are a separate
problem — they need lightweight flow tracking to resolve the alias and
are deliberately out of scope here.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Luau's official require-by-string alias mechanism: a `.luaurc` file in
the directory hierarchy declares `{"aliases": {"dep": "./dependency"}}`,
and `require("@dep")` resolves through that map. Parallel in shape to
tsconfig/jsconfig path aliases (already handled by the TS resolver) and
to the Rojo follow-up this PR already pins.
Counts from `repowise init` against upstream luau-lang/luau: 24 of the
170 string-literal-shaped requires are `@alias` references — every one
currently lands on the external-node fallback.
Implementation needs a `.luaurc` reader layered in via
`core/ingestion/dynamic_hints/luaurc.py`, with:
- parent-directory walk to find the nearest `.luaurc`
- child `.luaurc` overriding parent aliases
- alias values resolved as relative paths from the `.luaurc`'s dir
Out of scope for this PR — recorded here as a strict xfail so the
expected end state is versioned alongside the existing Rojo xfail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Three follow-up commits pushed on top of Commits
Runtime-verified numbersRan Private Rojo project (161
~26× more resolutions. The 128 remaining externals are all known-unsupported shapes: variable-aliased chains (out of scope here), and service-based lookups ( Upstream
~113× more resolutions. The 138 remaining break down as:
SuiteBoth xfails are the strict-mode documentation-only tests pinning the two deferred follow-ups (Rojo project-tree resolution, One side-finding worth flagging separatelyWhile running -Opus 4.7 |
Summary
Draft skeleton for issue #52. Adds a
luaulanguage tier so Rojo-synced Roblox projects produce a real dependency graph instead of the reported 21 disconnected nodes.This PR wires the full language path end-to-end with the exception of Rojo
default.project.json-aware resolution forgame.<Service>...absolute instance paths, which is deliberately scoped to a follow-up.What's in scope
LanguageSpec(tag="luau", ...)in the central registry, replacing the existing git-blame-onlyluapassthrough spec. One source of truth for both.luaand.luau(Luau's grammar is a strict superset of Lua 5.1)."luau"added toLanguageTagLiteral inmodels.pyso the extension flows throughFileTraverser/ASTParserinstead of being filtered out atmodels.py:61.LANGUAGE_CONFIGS["luau"]entry inparser.pymappingfunction_declaration→ function andtype_definition→ type_alias.queries/luau.scmwith captures for symbols,require(...)arguments, and call sites.resolvers/luau.pyhandling:require("rel/path")— relative then stem fallback.script/script.Parentchains — Roblox semantics where the firstParentis identity (the importer's container directory) and each subsequentParentwalks up.game.<Service>.Path...— registers as external node; full Rojo-tree resolution is the next PR.tree-sitter-luau>=1.2,<2added topyproject.tomldependencies.docs/LANGUAGE_SUPPORT.mdupdated to document the new tier.TestLuauParserintest_parser.py,test_luau_resolver.pyfor the resolver. Absolute instance-path resolution is pinned with anxfail(strict=True)test documenting the expected end state.What's deliberately NOT in scope
default.project.jsonto map service subtrees to filesystem directories. The shape is documented in the resolver's module docstring and the failingxfailtest. Planned home:core/ingestion/dynamic_hints/rojo.py..luafiles in pure-vanilla-Lua projects (not Rojo) will now be AST-parsed where before they were git-blame-only. This is aligned with issue [Feature] Luau/Roblox support: dependency graph parsing and language detection #52's stated goal, but flagging explicitly since it's a behavior change for non-Roblox Lua repos.repowise.core.ingestion.parsersreference inpyproject.toml'spackageslist (unrelated to this PR; surfaced in PR feat: add language support for wiki generation #75 review). Left untouched per the maintainer's preference to keep that fix in its own PR.Verification
Open questions / risks
[core]extra pinstree-sitter~=0.22, while repowise requires>=0.23,<1. Installingtree-sitter-luauwithout the[core]extra (which is what the standard dep resolution does) worked locally against tree-sitter 0.23.x — verified bytest_parses_require_importsexercising the loader. Worth a CI run against each matrix slot to confirm..luaownership. Taking.luainto theluauspec is a judgment call. Pure-Lua projects will now get AST-parsed; vanilla Lua parses cleanly as a Luau subset. Open to splitting the extensions if preferred.game.<Service>.... Currently returns an external node rather than silently matching the nearest-stem file. This matches the maintainer's "no silent fallback" norm from PRs feat: add OpenRouter as LLM and embedding provider #56 / feat(zai): adopt tier framework for plan-aware rate limiting #83, but means these edges won't land in the graph until the Rojo reader ships.Test plan
uv run pytest tests/unit/ingestion/ -q— green (230 passed, 2 xfailed)uv run ruff checkon changed files — cleanuv run ruff format --checkon changed files — cleanrepowise init— done against a private Rojo project + upstream luau-lang/luau (see comment for numbers); Rojo-tree resolution remains pending the follow-up PR