Add Luau language plugin; fix import-graph and tool-output path handling - #734
Add Luau language plugin; fix import-graph and tool-output path handling#734noahrepublic wants to merge 2 commits into
Conversation
… handling Scanning a Roblox/Luau project produced a score with no evidence behind it: .luau matched no language, so every mechanical detector ran over zero files and reported a clean 100/100. Adding the extension to the existing `lua` plugin is not enough — the Lua grammar emits ERROR nodes for type annotations and `export type`, which silently degrades AST-backed detection on any ordinarily-typed Luau codebase. Adds a `luau` plugin on the dedicated tree-sitter luau grammar, with a resolver for require-by-string (`./x`, `../x`, `@self/x`, `.luaurc` aliases, and host aliases such as Roblox's `@game` matched against a module index, longest tail first, accepting only an unambiguous or nearest match). selene is wired as the linter; stylua is not, because `--check` emits a diff rather than diagnostics. Three framework bugs surfaced while validating it, each affecting languages beyond Luau: * `ts_build_dep_graph` normalised resolved imports to absolute paths and then compared them against a file list that is often cwd-relative, so every edge was dropped. Edges are now matched on a normalised key that accepts either form, and a self-import no longer makes a file its own importer. On the test project this took the graph from 0 to ~1200 edges, which in turn un-broke orphan, single-use and coupling detection. * External tool findings were scored without passing through the scan's exclusion rules. Linters read their own config, not desloppify's, so running one at the project root reports on vendored trees the user excluded — 3154 of 4548 findings on the test project came from a Wally package directory. * `run_tool_result` decoded tool output with the locale codec. A non-ASCII byte raised UnicodeDecodeError inside the subprocess reader thread and killed it mid-scan (cp1252 on Windows). Output is now decoded as UTF-8. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The unused-import detector took each import's name from its module path
basename. That holds where the statement dictates the binding (Python's
`import foo`, Go's import block) but not where an import is an ordinary
expression assigned to an ordinary variable, which is the norm in Lua and
Luau:
local TeamClass = require("Tools/Team") -- binds TeamClass, not Team
local MODULES = { require("Entities/awp") } -- binds nothing at all
Both were reported unused: the first because "Team" appears nowhere, the
second because "awp" appears nowhere. On the test project all 31 findings
were false, 22 of them from a single table of inline requires.
JavaScript already had a binding-aware path for exactly this reason. This
generalises the idea as an optional `import_binding` spec hook returning the
name an import introduces plus the statement that declares it, and wires it
up for Luau. The statement matters: the captured import node covers only the
call, so excluding it alone leaves `local X = ` in the searched text, where
the name matches itself and nothing ever looks unused.
A binding whose name starts with an underscore is skipped — the Lua
convention for a name kept deliberately and knowingly unreferenced, which
selene's unused_variable rule reads the same way.
Verified against a matrix of five cases: a genuinely unused import is still
reported, a used alias is not, an unused alias is reported under its alias,
an inline require is skipped, and an underscore binding is skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Pushed a fourth fix found while validating the plugin against a real project. Unused-import names came from the module path, not the binding. That holds where the statement dictates the binding ( local TeamClass = require("Tools/Team") -- binds TeamClass, not Team
local MODULES = { require("Entities/awp") } -- binds nothing at allBoth were reported unused. On the test project all 31 unused-import findings were false, 22 from one table of inline requires. JavaScript already had a binding-aware path for this reason; this generalises it as an optional Underscore-prefixed bindings are skipped, matching the Lua convention selene's Suite still at 35 pre-existing failures; passes now 5776 → 5799 (23 new tests). |
Problem
Scanning a Roblox/Luau project reports a clean bill of health that nothing backs up.
.luauis not registered against any language, so auto-detection falls through tobash, every mechanical detector runs over zero files, and the objective score comes back 100/100:The project has 857
.luaufiles.--lang luadoes not help — it reportsNo lua source files found.Adding
.luauto the existingluaplugin's extensions is not sufficient: the Lua grammar produces ERROR nodes for type annotations, generics andexport type, so AST-backed detection would degrade silently on any ordinarily-typed Luau codebase.tree-sitter-language-packalready ships a dedicatedluaugrammar.Fix
New
luauplugin (desloppify/languages/luau/) on theluaugrammar, with:resolve_luau_importfor require-by-string:./x,../x,@self/x,.luaurcaliases, and host aliases such as Roblox's@game/.... Host aliases address a runtime instance tree via a build manifest (Rojo) rather than the filesystem, so their tails are matched against a module index — longest tail first, accepting only an unambiguous match or, when a tree is duplicated, the copy nearest the requiring file. Ambiguity yields no edge rather than a guessed one.initfile, since runtimes disagree on whether the base is the file or the directory it initialises. Only an existing file is accepted, so the ambiguity costs a stat rather than a wrong edge..spec/.storyfiles, and entry patterns for.client.luau/.server.luau(started by the runtime, never required) andControllers//Services/(bulk-required at boot — the Roblox counterpart of the/autoload/entry in the GDScript plugin).seleneas the linter.styluais deliberately not wired:--checkemits a diff, not diagnostics.Three framework bugs found while validating it, each affecting more than Luau:
ts_build_dep_graphdropped every edge when the file list was relative. It normalised each resolved import to an absolute path, then tested membership against a file list that language file finders often return relative to the process cwd, so the check never matched. Edges are now matched on a normalised key that accepts either form, recorded under the original file-list spelling, and a self-import no longer makes a file its own importer. On the test project this went from 0 to ~1200 edges, which in turn un-broke orphan, single-use and coupling detection.External tool findings bypassed the scan's exclusion rules. Linters read their own config, not desloppify's, so running one at the project root reports on vendored trees the user excluded. On the test project 3154 of 4548 findings came from a Wally
Packages/_Index/directory thatdesloppify excludehad already ruled out.make_tool_phasenow filters entries through the same exclusion patterns.run_tool_resultdecoded tool output with the locale codec. A non-ASCII byte raisedUnicodeDecodeErrorinside the subprocess reader thread and killed it mid-scan:Output is now decoded as UTF-8 with
errors="replace".Also adds
.luauto the extension list the unused-import basename helper strips.Result on the test project
Tests
18 new tests (
test_luau_plugin.py, plus dep-graph and tool-exclusion regressions). Full suite run before and after on the same machine: 35 failures both times — all pre-existing and Windows-specific, none introduced — with passes going 5776 → 5794.Three test doubles in
test_next_lint.pyhad signatures pinned to the exact kwargs ofsubprocess.runand were widened with**_kwargs.🤖 Generated with Claude Code