feat: add Lua language support

This commit is contained in:
Rotem 2026-04-27 17:33:44 +03:00
parent 77844acb6a
commit bf9337a5e7
13 changed files with 466 additions and 0 deletions

View file

@ -43,6 +43,7 @@ const EXTENSION_MAP: Record<SupportedLanguages, readonly string[]> = {
[SupportedLanguages.Dart]: ['.dart'],
[SupportedLanguages.Vue]: ['.vue'],
[SupportedLanguages.Cobol]: ['.cbl', '.cob', '.cpy', '.cobol'],
[SupportedLanguages.Lua]: ['.lua'],
} satisfies Record<SupportedLanguages, readonly string[]>; // Ensure exhaustiveness
/** Pre-built reverse lookup: extension → language (built once at module load). */
@ -101,6 +102,7 @@ const SYNTAX_MAP: Record<SupportedLanguages, string> = {
[SupportedLanguages.Dart]: 'dart',
[SupportedLanguages.Vue]: 'typescript',
[SupportedLanguages.Cobol]: 'cobol',
[SupportedLanguages.Lua]: 'lua',
} satisfies Record<SupportedLanguages, string>; // Ensure exhaustiveness
/** Non-code file extensions → Prism-compatible syntax identifiers */

View file

@ -22,4 +22,5 @@ export enum SupportedLanguages {
Vue = 'vue',
/** Standalone regex processor — no tree-sitter, no LanguageProvider. */
Cobol = 'cobol',
Lua = 'lua',
}

View file

@ -80,6 +80,7 @@
"tree-sitter-javascript": "^0.23.0",
"tree-sitter-php": "^0.23.0",
"tree-sitter-python": "0.23.4",
"@tree-sitter-grammars/tree-sitter-lua": "^0.4.1",
"tree-sitter-ruby": "^0.23.1",
"tree-sitter-rust": "0.23.1",
"tree-sitter-typescript": "^0.23.2",

View file

@ -246,3 +246,6 @@ export const rubyExportChecker: ExportChecker = (_node, _name) => true;
/** Dart: public if no leading underscore (convention, same as Python). */
export const dartExportChecker: ExportChecker = (_node, name) => !name.startsWith('_');
/** Lua: no explicit visibility system — all top-level definitions are public. */
export const luaExportChecker: ExportChecker = (_node, _name) => true;

View file

@ -0,0 +1,27 @@
/**
* Lua import resolution config.
*
* Lua's `require("foo.bar")` converts dots to path separators:
* require("scripts.combat.player") → scripts/combat/player.lua
*
* We apply a simple suffix-match after converting dot-separators.
*/
import { SupportedLanguages } from 'gitnexus-shared';
import type { ImportResolutionConfig, ImportResolverStrategy } from '../types.js';
import { suffixResolve } from '../utils.js';
/** Lua require() resolution strategy. Dots in the module name map to path separators. */
export const luaRequireStrategy: ImportResolverStrategy = (rawImportPath, _filePath, ctx) => {
// Strip surrounding quotes if present (e.g. '"scripts.player"' → 'scripts.player')
const stripped = rawImportPath.replace(/^['"]|['"]$/g, '');
// Convert Lua dot-separators to path separators, then append .lua if missing.
const pathParts = stripped.split('.').filter(Boolean);
const resolved = suffixResolve(pathParts, ctx.normalizedFileList, ctx.allFileList, ctx.index);
return resolved ? { kind: 'files', files: [resolved] } : null;
};
export const luaImportConfig: ImportResolutionConfig = {
language: SupportedLanguages.Lua,
strategies: [luaRequireStrategy],
};

View file

@ -25,6 +25,7 @@ import { swiftProvider } from './swift.js';
import { dartProvider } from './dart.js';
import { vueProvider } from './vue.js';
import { cobolProvider } from './cobol.js';
import { luaProvider } from './lua.js';
export const providers = {
[SupportedLanguages.JavaScript]: javascriptProvider,
@ -43,6 +44,7 @@ export const providers = {
[SupportedLanguages.Dart]: dartProvider,
[SupportedLanguages.Vue]: vueProvider,
[SupportedLanguages.Cobol]: cobolProvider,
[SupportedLanguages.Lua]: luaProvider,
} satisfies Record<SupportedLanguages, LanguageProvider>;
/** Get provider by language enum (always succeeds for SupportedLanguages). */

View file

@ -0,0 +1,78 @@
/**
* Lua Language Provider
*
* Assembles all Lua-specific ingestion capabilities into a single
* LanguageProvider, following the Strategy pattern used by the pipeline.
*
* Key Lua traits:
* - importSemantics: 'wildcard-leaf' (require() returns the full module table)
* - No explicit visibility system — everything is public
* - No formal class/type system — tables used as namespace/class idiom
* - emitScopeCaptures: implemented (emits @scope.module for every file)
*/
import { SupportedLanguages } from 'gitnexus-shared';
import { defineLanguage } from '../language-provider.js';
import { typeConfig as luaConfig } from '../type-extractors/lua.js';
import { luaExportChecker } from '../export-detection.js';
import { createImportResolver } from '../import-resolvers/resolver-factory.js';
import { luaImportConfig } from '../import-resolvers/configs/lua.js';
import { LUA_QUERIES } from '../tree-sitter-queries.js';
import { emitLuaScopeCaptures } from './lua/index.js';
const BUILT_INS: ReadonlySet<string> = new Set([
// Standard library globals
'print',
'tostring',
'tonumber',
'type',
'pairs',
'ipairs',
'next',
'select',
'unpack',
'table',
'string',
'math',
'io',
'os',
'coroutine',
'package',
'debug',
'load',
'loadfile',
'dofile',
'require',
'rawget',
'rawset',
'rawequal',
'rawlen',
'setmetatable',
'getmetatable',
'pcall',
'xpcall',
'error',
'assert',
'collectgarbage',
'gcinfo',
'newproxy',
'_G',
'_VERSION',
'arg',
]);
export const luaProvider = defineLanguage({
id: SupportedLanguages.Lua,
extensions: ['.lua'],
treeSitterQueries: LUA_QUERIES,
typeConfig: luaConfig,
exportChecker: luaExportChecker,
importResolver: createImportResolver(luaImportConfig),
importSemantics: 'wildcard-leaf',
builtInNames: BUILT_INS,
// ── RFC #909 Ring 3: scope-based resolution hooks (RFC §5) ──────────
// Lua emits at minimum @scope.module (the `(chunk)` capture) so the
// pipeline can register each .lua file in the scope-based registry.
emitScopeCaptures: emitLuaScopeCaptures,
});

View file

@ -0,0 +1,46 @@
/**
* `emitScopeCaptures` for Lua.
*
* Drives the Lua scope query against `@tree-sitter-grammars/tree-sitter-lua`
* and groups raw matches into `CaptureMatch[]` for the central extractor.
*
* Lua has no formal module system. The top-level chunk (file) is the module
* scope. Tables assigned to names are the idiomatic namespace mechanism.
*
* Pure given the input source text. No I/O, no globals consulted.
*/
import type { Capture, CaptureMatch } from 'gitnexus-shared';
import { nodeToCapture } from '../../utils/ast-helpers.js';
import { getLuaParser, getLuaScopeQuery } from './query.js';
import { getTreeSitterBufferSize } from '../../constants.js';
export function emitLuaScopeCaptures(
sourceText: string,
_filePath: string,
cachedTree?: unknown,
): readonly CaptureMatch[] {
let tree = cachedTree as ReturnType<ReturnType<typeof getLuaParser>['parse']> | undefined;
if (tree === undefined) {
tree = getLuaParser().parse(sourceText, undefined, {
bufferSize: getTreeSitterBufferSize(sourceText),
});
}
const rawMatches = getLuaScopeQuery().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;
// Skip internal predicate captures (e.g. @_req, @_req2, @_sm, @_idx)
if (tag.startsWith('@_')) continue;
grouped[tag] = nodeToCapture(tag, c.node);
}
if (Object.keys(grouped).length === 0) continue;
out.push(grouped);
}
return out;
}

View file

@ -0,0 +1,11 @@
/**
* Lua scope-resolution hooks (RFC #909 Ring 3, RFC §5).
*
* Public API barrel for the Lua language provider.
*
* Module layout:
* - `query.ts` — LUA_SCOPE_QUERY string + lazy parser/query singletons
* - `captures.ts` — `emitLuaScopeCaptures` (top-level orchestrator)
*/
export { emitLuaScopeCaptures } from './captures.js';

View file

@ -0,0 +1,128 @@
/**
* Tree-sitter scope-query for Lua (RFC §5.1).
*
* Exposes lazy `Parser` and `Query` singletons so callers don't
* pay tree-sitter init cost per file.
*
* Capture vocabulary
* ──────────────────
* @scope.module — top-level chunk (file == module)
* @scope.function — function body
*
* @declaration.name — the identifier being declared
* @declaration.function — function definition_statement / local_function
* @declaration.method — method via dot_index / method_index
* @declaration.variable — local or global variable assignment
* @declaration.module — table literal assigned to a name (Lua namespace)
*
* @import.statement — require() call (local or bare)
*
* @reference.name — callee of any function/method call
* @reference.call.free — free call (foo())
* @reference.call.member — method call (obj:method())
*/
import Parser from 'tree-sitter';
import Lua from '@tree-sitter-grammars/tree-sitter-lua';
const LUA_SCOPE_QUERY = `
;; ── Scopes ──────────────────────────────────────────────────────────────────
;; The entire file is one module scope.
(chunk) @scope.module
;; Each function body introduces its own function scope.
(function_definition) @scope.function
;; ── Declarations: global functions ──────────────────────────────────────────
;; function foo(...) ... end
(function_definition_statement
name: (identifier) @declaration.name) @declaration.function
;; ── Declarations: method-style (Foo.bar / Foo:bar) ──────────────────────────
;; function Foo.bar(...) ... end
(function_definition_statement
name: (dot_index_expression
field: (identifier) @declaration.name)) @declaration.method
;; function Foo:bar(...) ... end
(function_definition_statement
name: (method_index_expression
method: (identifier) @declaration.name)) @declaration.method
;; ── Declarations: local functions ────────────────────────────────────────────
;; local function bar(...) ... end
(local_function_definition_statement
name: (identifier) @declaration.name) @declaration.function
;; ── Declarations: tables used as namespaces/modules ──────────────────────────
;; Foo = {}
(assignment_statement
(variable_list
name: (identifier) @declaration.name)
(expression_list
(table_constructor))) @declaration.module
;; local Foo = {}
(local_variable_declaration
(attribute_list
name: (identifier) @declaration.name)
(expression_list
(table_constructor))) @declaration.module
;; ── Declarations: variables ──────────────────────────────────────────────────
;; local x = ...
(local_variable_declaration
(attribute_list
name: (identifier) @declaration.name)) @declaration.variable
;; x = ... (global assignment)
(assignment_statement
(variable_list
name: (identifier) @declaration.name)) @declaration.variable
;; ── Imports ──────────────────────────────────────────────────────────────────
;; local M = require("foo.bar")
(local_variable_declaration
(expression_list
(function_call_expression
name: (identifier) @_req
(argument_list
(string) @import.source))
(#eq? @_req "require"))) @import.statement
;; require("foo.bar") (bare call)
(function_call_expression
name: (identifier) @_req2
(argument_list
(string) @import.source)
(#eq? @_req2 "require")) @import.statement
;; ── References: free calls ───────────────────────────────────────────────────
(function_call_expression
name: (identifier) @reference.name) @reference.call.free
;; ── References: method calls ─────────────────────────────────────────────────
(method_call_expression
method: (identifier) @reference.name) @reference.call.member
`;
let _parser: Parser | null = null;
let _query: Parser.Query | null = null;
export function getLuaParser(): Parser {
if (_parser === null) {
_parser = new Parser();
_parser.setLanguage(Lua as Parameters<Parser['setLanguage']>[0]);
}
return _parser;
}
export function getLuaScopeQuery(): Parser.Query {
if (_query === null) {
_query = new Parser.Query(
Lua as Parameters<Parser['setLanguage']>[0],
LUA_SCOPE_QUERY,
);
}
return _query;
}

View file

@ -1334,6 +1334,106 @@ export const DART_QUERIES = `
import { SupportedLanguages } from 'gitnexus-shared';
// Lua queries - works with @tree-sitter-grammars/tree-sitter-lua
// Lua has no formal module system; a chunk (file) is a module.
// Tables assigned to globals/locals are the idiomatic way to namespace code.
export const LUA_QUERIES = `
; ── Scopes ────────────────────────────────────────────────────────────────────
; The top-level chunk maps 1-to-1 to a module scope.
(chunk) @scope.module
; Each function body introduces a new scope.
(function_definition) @scope.function
; ── Functions: top-level / global ────────────────────────────────────────────
; function foo(...) ... end
(function_definition_statement
name: (identifier) @name) @definition.function
; ── Functions: method-style (Foo:bar, Foo.bar) ───────────────────────────────
; function Foo.bar(...) ... end
(function_definition_statement
name: (dot_index_expression
field: (identifier) @name)) @definition.method
; function Foo:bar(...) ... end
(function_definition_statement
name: (method_index_expression
method: (identifier) @name)) @definition.method
; ── Local functions ───────────────────────────────────────────────────────────
; local function bar(...) ... end
(local_function_definition_statement
name: (identifier) @name) @definition.function
; ── Tables as modules / namespaces ───────────────────────────────────────────
; Foo = {}
(assignment_statement
(variable_list
name: (identifier) @name)
(expression_list
(table_constructor))) @definition.module
; local Foo = {}
(local_variable_declaration
(attribute_list
name: (identifier) @name)
(expression_list
(table_constructor))) @definition.module
; ── Variables (local and global) ─────────────────────────────────────────────
; local x = ...
(local_variable_declaration
(attribute_list
name: (identifier) @name)) @definition.variable
; x = ... (bare assignment, top-level)
(assignment_statement
(variable_list
name: (identifier) @name)) @definition.variable
; ── Imports (require / dofile / loadfile) ────────────────────────────────────
; local M = require("foo.bar")
(local_variable_declaration
(expression_list
(function_call_expression
name: (identifier) @_req
(argument_list
(string) @import.source))
(#eq? @_req "require"))) @import
; require("foo.bar") (bare call)
(function_call_expression
name: (identifier) @_req
(argument_list
(string) @import.source)
(#eq? @_req "require")) @import
; ── Function calls ────────────────────────────────────────────────────────────
; foo()
(function_call_expression
name: (identifier) @call.name) @call
; obj:method() / obj.method()
(method_call_expression
method: (identifier) @call.name) @call
; ── Heritage (table extension patterns) ─────────────────────────────────────
; setmetatable(Foo, {__index = Bar})
(function_call_expression
name: (identifier) @_sm
(argument_list
(identifier) @heritage.class
(table_constructor
(field
name: (identifier) @_idx
value: (identifier) @heritage.extends)))
(#eq? @_sm "setmetatable")
(#eq? @_idx "__index")) @heritage
`;
export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
[SupportedLanguages.TypeScript]: TYPESCRIPT_QUERIES,
[SupportedLanguages.JavaScript]: JAVASCRIPT_QUERIES,
@ -1351,4 +1451,5 @@ export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
[SupportedLanguages.Dart]: DART_QUERIES,
[SupportedLanguages.Vue]: TYPESCRIPT_QUERIES, // Vue <script> blocks are parsed as TypeScript
[SupportedLanguages.Cobol]: '', // Standalone regex processor — no tree-sitter queries
[SupportedLanguages.Lua]: LUA_QUERIES,
};

View file

@ -0,0 +1,64 @@
/**
* Lua type extractor configuration.
*
* Lua is dynamically typed — there are no explicit type annotations.
* This extractor provides a minimal implementation that records
* constructor-call bindings (`local x = Foo.new(...)`) so the pipeline
* can still emit call edges, but does not attempt type inference.
*/
import type { SyntaxNode } from '../utils/ast-helpers.js';
import type { LanguageTypeConfig } from '../type-extractors/types.js';
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
'local_variable_declaration',
'assignment_statement',
]);
/**
* Lua: local x = Foo.new() / local x = Foo()
* Extracts a constructor-call binding so the pipeline can infer types on
* the left-hand-side variable. Lua has no explicit type system, so we
* only record the callee name as a heuristic type hint.
*/
const extractDeclaration = (node: SyntaxNode, env: Map<string, string>): void => {
// local x = Foo() / local x = Foo.new()
if (node.type !== 'local_variable_declaration' && node.type !== 'assignment_statement') return;
// Find the variable name(s) and the rhs expression(s).
// Tree-sitter-lua: local_variable_declaration has attribute_list + expression_list children.
const attrList =
node.type === 'local_variable_declaration'
? (node.children.find((c) => c.type === 'attribute_list') ?? null)
: (node.children.find((c) => c.type === 'variable_list') ?? null);
const exprList = node.children.find((c) => c.type === 'expression_list') ?? null;
if (!attrList || !exprList) return;
// Only handle single-name = single-expression for simplicity.
const nameNode =
node.type === 'local_variable_declaration'
? attrList.children.find((c) => c.type === 'identifier') ?? null
: attrList.children.find((c) => c.type === 'identifier') ?? null;
const exprNode = exprList.children.find((c) => c.isNamed) ?? null;
if (!nameNode || !exprNode) return;
// Heuristic: if the RHS is a function call to something ending in .new or a PascalCase name
if (exprNode.type === 'function_call_expression') {
const nameField = exprNode.children.find((c) => c.isNamed);
if (nameField) {
env.set(nameNode.text, nameField.text);
}
}
};
const extractParameter = (_node: SyntaxNode, _env: Map<string, string>): void => {
// Lua has no type annotations on parameters; no-op.
};
export const typeConfig: LanguageTypeConfig = {
declarationNodeTypes: DECLARATION_NODE_TYPES,
extractDeclaration,
extractParameter,
};

View file

@ -15,6 +15,7 @@ import Go from 'tree-sitter-go';
import Rust from 'tree-sitter-rust';
import PHP from 'tree-sitter-php';
import Ruby from 'tree-sitter-ruby';
import Lua from '@tree-sitter-grammars/tree-sitter-lua';
import { createRequire } from 'node:module';
import { SupportedLanguages } from 'gitnexus-shared';
@ -51,6 +52,7 @@ const languageMap: Record<string, any> = {
...(Kotlin ? { [SupportedLanguages.Kotlin]: Kotlin } : {}),
[SupportedLanguages.PHP]: PHP.php_only,
[SupportedLanguages.Ruby]: Ruby,
[SupportedLanguages.Lua]: Lua,
[SupportedLanguages.Vue]: TypeScript.typescript,
...(Dart ? { [SupportedLanguages.Dart]: Dart } : {}),
...(Swift ? { [SupportedLanguages.Swift]: Swift } : {}),