feat(lua): add Lua language provider and all extractors

Full tree-sitter-based Lua support:
- LUA_QUERIES: functions, methods (dot+colon), require() imports, calls,
  variables, table field assignments
- Type extractor (no-op, dynamically typed), call/field/method/variable/
  heritage/class/import extractors following Go provider pattern
- Export checker (all public, like Ruby)
- Import resolver for dot-separated require() paths
- Registered in parser-loader SOURCES, LANGUAGE_QUERIES, providers table,
  entry-point-scoring, framework-detection exhaustive records
- FUNCTION_NODE_TYPES extended with Lua node types
- Import resolver EXTENSIONS extended with .lua and /init.lua
This commit is contained in:
Christian C. Berclaz 2026-05-01 22:38:36 +02:00
parent bfe05d54d6
commit 3e637364a2
No known key found for this signature in database
15 changed files with 486 additions and 0 deletions

View file

@ -0,0 +1,6 @@
import { SupportedLanguages } from 'gitnexus-shared';
import type { CallExtractionConfig } from '../../call-types.js';
export const luaCallConfig: CallExtractionConfig = {
language: SupportedLanguages.Lua,
};

View file

@ -0,0 +1,16 @@
import { SupportedLanguages } from 'gitnexus-shared';
import type { ClassExtractionConfig } from '../../class-types.js';
/**
* Lua class extraction config.
*
* Lua has no native class syntax. Class-like structures are detected via
* local_variable_declaration with table constructors (local M = {}), which
* is the standard module/class pattern. Method definitions on the table
* (function M:method()) are handled by the method extractor, not here.
*/
export const luaClassConfig: ClassExtractionConfig = {
language: SupportedLanguages.Lua,
typeDeclarationNodes: [],
fileScopeNodeTypes: ['chunk'],
};

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: all definitions are public (no visibility modifiers). Local scope is file-scoped. */
export const luaExportChecker: ExportChecker = (_node, _name) => true;

View file

@ -0,0 +1,40 @@
import { SupportedLanguages } from 'gitnexus-shared';
import type { FieldExtractionConfig } from '../generic.js';
/**
* Lua field extraction config.
*
* Lua tables are the only compound data type. Table fields are defined in
* constructors ({ key = value }) or via assignment (tbl.key = value).
* Tree-sitter-lua represents constructor fields as `field` nodes inside `table`.
*
* Lua has no visibility modifiers — all fields are public.
*/
export const luaFieldConfig: FieldExtractionConfig = {
language: SupportedLanguages.Lua,
typeDeclarationNodes: ['local_variable_declaration', 'variable_assignment'],
fieldNodeTypes: ['field'],
bodyNodeTypes: ['field_list'],
defaultVisibility: 'public',
extractName(node) {
const key = node.childForFieldName('key');
return key?.type === 'identifier' ? key.text : undefined;
},
extractType(_node) {
return undefined; // Lua is dynamically typed
},
extractVisibility(_node) {
return 'public';
},
isStatic(_node) {
return false;
},
isReadonly(_node) {
return false;
},
};

View file

@ -0,0 +1,14 @@
import { SupportedLanguages } from 'gitnexus-shared';
import type { HeritageExtractionConfig } from '../../heritage-types.js';
/**
* Lua heritage extraction config.
*
* Lua uses metatables for inheritance (setmetatable + __index). Since this is
* runtime behavior rather than syntactic inheritance, tree-sitter queries
* cannot reliably capture it. The heritage extractor is configured but relies
* on future call-routing hooks to detect setmetatable patterns.
*/
export const luaHeritageConfig: HeritageExtractionConfig = {
language: SupportedLanguages.Lua,
};

View file

@ -0,0 +1,20 @@
/**
* Lua import resolution config.
* Lua require() uses dot-separated module paths: require('foo.bar') → foo/bar.lua or foo/bar/init.lua
*/
import { SupportedLanguages } from 'gitnexus-shared';
import type { ImportResolutionConfig, ImportResolverStrategy } from '../types.js';
import { suffixResolve } from '../utils.js';
export const luaRequireStrategy: ImportResolverStrategy = (rawImportPath, _filePath, ctx) => {
const dotPath = rawImportPath.replace(/^['"]|['"]$/g, '');
const pathParts = dotPath.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

@ -47,6 +47,9 @@ export const EXTENSIONS = [
'.swift',
// Ruby
'.rb',
// Lua
'.lua',
'/init.lua',
];
/**

View file

@ -24,6 +24,7 @@ import { rubyProvider } from './ruby.js';
import { swiftProvider } from './swift.js';
import { dartProvider } from './dart.js';
import { vueProvider } from './vue.js';
import { luaProvider } from './lua.js';
import { cobolProvider } from './cobol.js';
export const providers = {
@ -41,6 +42,7 @@ export const providers = {
[SupportedLanguages.Ruby]: rubyProvider,
[SupportedLanguages.Swift]: swiftProvider,
[SupportedLanguages.Dart]: dartProvider,
[SupportedLanguages.Lua]: luaProvider,
[SupportedLanguages.Vue]: vueProvider,
[SupportedLanguages.Cobol]: cobolProvider,
} satisfies Record<SupportedLanguages, LanguageProvider>;

View file

@ -0,0 +1,107 @@
/**
* 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() brings the module table into scope)
* - No static typing — type extractors are minimal
* - Class-like structures via table + metatable convention
* - Methods via function M:method() (colon syntax) and function M.func() (dot syntax)
*/
import { SupportedLanguages } from 'gitnexus-shared';
import { createClassExtractor } from '../class-extractors/generic.js';
import { luaClassConfig } from '../class-extractors/configs/lua.js';
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 { createFieldExtractor } from '../field-extractors/generic.js';
import { luaFieldConfig } from '../field-extractors/configs/lua.js';
import { createMethodExtractor } from '../method-extractors/generic.js';
import { luaMethodConfig } from '../method-extractors/configs/lua.js';
import { createVariableExtractor } from '../variable-extractors/generic.js';
import { luaVariableConfig } from '../variable-extractors/configs/lua.js';
import { createCallExtractor } from '../call-extractors/generic.js';
import { luaCallConfig } from '../call-extractors/configs/lua.js';
import { createHeritageExtractor } from '../heritage-extractors/generic.js';
import { luaHeritageConfig } from '../heritage-extractors/configs/lua.js';
const BUILT_INS: ReadonlySet<string> = new Set([
'print',
'type',
'tostring',
'tonumber',
'error',
'assert',
'pcall',
'xpcall',
'require',
'dofile',
'loadfile',
'load',
'rawget',
'rawset',
'rawequal',
'rawlen',
'setmetatable',
'getmetatable',
'select',
'next',
'pairs',
'ipairs',
'unpack',
'table.insert',
'table.remove',
'table.concat',
'table.sort',
'string.format',
'string.find',
'string.match',
'string.gsub',
'string.sub',
'string.len',
'string.byte',
'string.char',
'string.rep',
'string.reverse',
'string.upper',
'string.lower',
'math.floor',
'math.ceil',
'math.abs',
'math.max',
'math.min',
'math.sqrt',
'math.random',
'io.open',
'io.read',
'io.write',
'io.close',
'os.time',
'os.clock',
'os.date',
'os.execute',
'collectgarbage',
]);
export const luaProvider = defineLanguage({
id: SupportedLanguages.Lua,
extensions: ['.lua'],
treeSitterQueries: LUA_QUERIES,
typeConfig: luaConfig,
exportChecker: luaExportChecker,
importResolver: createImportResolver(luaImportConfig),
importSemantics: 'wildcard-leaf',
callExtractor: createCallExtractor(luaCallConfig),
fieldExtractor: createFieldExtractor(luaFieldConfig),
methodExtractor: createMethodExtractor(luaMethodConfig),
variableExtractor: createVariableExtractor(luaVariableConfig),
classExtractor: createClassExtractor(luaClassConfig),
heritageExtractor: createHeritageExtractor(luaHeritageConfig),
builtInNames: BUILT_INS,
});

View file

@ -0,0 +1,90 @@
import { SupportedLanguages } from 'gitnexus-shared';
import type { MethodExtractionConfig, ParameterInfo } from '../../method-types.js';
import type { SyntaxNode } from '../../utils/ast-helpers.js';
function extractLuaName(node: SyntaxNode): string | undefined {
const nameField = node.childForFieldName('name');
if (!nameField) return undefined;
if (nameField.type === 'identifier') return nameField.text;
if (nameField.type === 'variable') {
const method = nameField.childForFieldName('method');
if (method) return method.text;
const field = nameField.childForFieldName('field');
if (field) return field.text;
}
return undefined;
}
function extractLuaParameters(node: SyntaxNode): ParameterInfo[] {
const paramList = node.childForFieldName('parameters');
if (!paramList) return [];
const params: ParameterInfo[] = [];
for (let i = 0; i < paramList.namedChildCount; i++) {
const child = paramList.namedChild(i);
if (!child) continue;
if (child.type === 'identifier') {
params.push({
name: child.text,
type: null,
rawType: null,
isOptional: false,
isVariadic: false,
});
} else if (child.type === 'vararg_expression') {
params.push({
name: '...',
type: null,
rawType: null,
isOptional: false,
isVariadic: true,
});
}
}
return params;
}
function extractLuaOwnerName(node: SyntaxNode): string | undefined {
const nameField = node.childForFieldName('name');
if (nameField?.type !== 'variable') return undefined;
const table = nameField.childForFieldName('table');
return table?.type === 'identifier' ? table.text : undefined;
}
export const luaMethodConfig: MethodExtractionConfig = {
language: SupportedLanguages.Lua,
typeDeclarationNodes: ['function_definition_statement', 'local_function_definition_statement'],
methodNodeTypes: ['function_definition_statement', 'local_function_definition_statement'],
bodyNodeTypes: [],
extractName: extractLuaName,
extractReturnType(_node) {
return undefined; // Lua is dynamically typed
},
extractParameters: extractLuaParameters,
extractVisibility(_node) {
return 'public';
},
extractReceiverType(node) {
return extractLuaOwnerName(node);
},
extractOwnerName: extractLuaOwnerName,
isStatic(node) {
const nameField = node.childForFieldName('name');
if (nameField?.type !== 'variable') return true;
return nameField.childForFieldName('method') == null;
},
isAbstract(_node) {
return false;
},
isFinal(_node) {
return false;
},
};

View file

@ -1373,6 +1373,64 @@ export const DART_QUERIES = `
(type_identifier) @heritage.trait))) @heritage
`;
export const LUA_QUERIES = `
; Functions (global)
(function_definition_statement name: (identifier) @name) @definition.function
; Methods — function M.foo() end (dot-access method on table)
(function_definition_statement
name: (variable
table: (identifier) @definition.class.hint
field: (identifier) @name)) @definition.method
; Methods — function M:bar() end (colon-access method on table)
(function_definition_statement
name: (variable
table: (identifier) @definition.class.hint
method: (identifier) @name)) @definition.method
; Local functions
(local_function_definition_statement name: (identifier) @name) @definition.function
; Imports — require('module') with expression_list wrapper
(call
function: (variable name: (identifier) @_fn)
arguments: (argument_list (expression_list (string) @import.source))
(#eq? @_fn "require")) @import
; Imports — require "module" without parens (string directly in argument_list)
(call
function: (variable name: (identifier) @_fn)
arguments: (argument_list (string) @import.source)
(#eq? @_fn "require")) @import
; Calls — free function calls: foo(...)
(call function: (variable name: (identifier) @call.name)) @call
; Calls — method/field calls: obj.method(...) or obj:method(...)
(call
function: (variable
field: (identifier) @call.name)) @call
(call
function: (variable
method: (identifier) @call.name)) @call
; Local variable declarations: local x = ...
(local_variable_declaration
(variable_list (variable name: (identifier) @name))) @definition.variable
; Global variable assignments: x = ... (top-level)
(variable_assignment
(variable_list (variable name: (identifier) @name))) @definition.variable
; Table field assignments: M.field = value (property definition)
(variable_assignment
(variable_list
(variable
table: (identifier) @assignment.receiver
field: (identifier) @assignment.property))) @assignment
`;
import { SupportedLanguages } from 'gitnexus-shared';
export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
@ -1390,6 +1448,7 @@ export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
[SupportedLanguages.Ruby]: RUBY_QUERIES,
[SupportedLanguages.Swift]: SWIFT_QUERIES,
[SupportedLanguages.Dart]: DART_QUERIES,
[SupportedLanguages.Lua]: LUA_QUERIES,
[SupportedLanguages.Vue]: TYPESCRIPT_QUERIES, // Vue <script> blocks are parsed as TypeScript
[SupportedLanguages.Cobol]: '', // Standalone regex processor — no tree-sitter queries
};

View file

@ -0,0 +1,72 @@
import type { SyntaxNode } from '../utils/ast-helpers.js';
import type {
ConstructorBindingScanner,
LanguageTypeConfig,
ParameterExtractor,
TypeBindingExtractor,
} from './types.js';
import { extractVarName } from './shared.js';
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
'local_variable_declaration',
'variable_assignment',
]);
/**
* Lua: local x = Foo.new(...) or local x = Foo(...)
* Extract variable name from local_variable_declaration or variable_assignment.
*/
const extractDeclaration: TypeBindingExtractor = (
_node: SyntaxNode,
_env: Map<string, string>,
): void => {
// Lua is dynamically typed — no type annotations to extract
};
const extractParameter: ParameterExtractor = (
_node: SyntaxNode,
_env: Map<string, string>,
): void => {
// Lua is dynamically typed — no parameter type annotations
};
const scanConstructorBinding: ConstructorBindingScanner = (node) => {
if (node.type !== 'local_variable_declaration') return undefined;
let varList: SyntaxNode | null = null;
let exprList: SyntaxNode | null = null;
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child?.type === 'variable_list') varList = child;
if (child?.type === 'expression_list') exprList = child;
}
if (!varList || !exprList) return undefined;
const firstVar = varList.firstNamedChild;
const firstExpr = exprList.firstNamedChild;
if (!firstVar || !firstExpr) return undefined;
const varName = extractVarName(firstVar.childForFieldName?.('name') ?? firstVar);
if (!varName) return undefined;
// local obj = ClassName.new(...)
if (firstExpr.type === 'call') {
const funcNode = firstExpr.childForFieldName('function');
if (funcNode?.type === 'variable') {
const table = funcNode.childForFieldName('table');
const field = funcNode.childForFieldName('field');
if (table?.type === 'identifier' && field?.text === 'new') {
return { varName, calleeName: `${table.text}.new` };
}
}
}
return undefined;
};
export const typeConfig: LanguageTypeConfig = {
declarationNodeTypes: DECLARATION_NODE_TYPES,
forLoopNodeTypes: new Set(['for_generic_statement']),
extractDeclaration,
extractParameter,
scanConstructorBinding,
};

View file

@ -95,6 +95,9 @@ export const FUNCTION_NODE_TYPES = new Set([
// Dart
'function_signature',
'method_signature',
// Lua
'function_definition_statement',
'local_function_definition_statement',
]);
/**

View file

@ -0,0 +1,46 @@
import { SupportedLanguages } from 'gitnexus-shared';
import type { VariableExtractionConfig, VariableVisibility } from '../../variable-types.js';
import type { SyntaxNode } from '../../utils/ast-helpers.js';
function extractLuaVarName(node: SyntaxNode): string | undefined {
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child?.type === 'variable_list') {
const firstVar = child.namedChildren.find((c: SyntaxNode) => c.type === 'variable');
if (firstVar) {
const name = firstVar.childForFieldName('name');
return name?.type === 'identifier' ? name.text : undefined;
}
}
}
return undefined;
}
export const luaVariableConfig: VariableExtractionConfig = {
language: SupportedLanguages.Lua,
constNodeTypes: [],
staticNodeTypes: [],
variableNodeTypes: ['local_variable_declaration', 'variable_assignment'],
extractName: extractLuaVarName,
extractType(_node) {
return undefined; // Lua is dynamically typed
},
extractVisibility(node): VariableVisibility {
return node.type === 'local_variable_declaration' ? 'private' : 'public';
},
isConst(_node) {
return false;
},
isStatic(_node) {
return false;
},
isMutable(_node) {
return true;
},
};

View file

@ -160,6 +160,11 @@ const SOURCES: Record<string, GrammarSource> = {
'Kotlin parsing disabled: `tree-sitter-kotlin` is an optionalDependency ' +
'and is not installed (or its native binding failed to build).',
},
[SupportedLanguages.Lua]: {
load: () => _require('tree-sitter-lua'),
unavailableNote:
'Lua parsing requires `tree-sitter-lua`. Check the install and native binding.',
},
};
type LoadResult =