diff --git a/gitnexus/src/core/ingestion/call-extractors/configs/lua.ts b/gitnexus/src/core/ingestion/call-extractors/configs/lua.ts new file mode 100644 index 000000000..afba6fa6e --- /dev/null +++ b/gitnexus/src/core/ingestion/call-extractors/configs/lua.ts @@ -0,0 +1,6 @@ +import { SupportedLanguages } from 'gitnexus-shared'; +import type { CallExtractionConfig } from '../../call-types.js'; + +export const luaCallConfig: CallExtractionConfig = { + language: SupportedLanguages.Lua, +}; diff --git a/gitnexus/src/core/ingestion/class-extractors/configs/lua.ts b/gitnexus/src/core/ingestion/class-extractors/configs/lua.ts new file mode 100644 index 000000000..915e1a8a9 --- /dev/null +++ b/gitnexus/src/core/ingestion/class-extractors/configs/lua.ts @@ -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'], +}; diff --git a/gitnexus/src/core/ingestion/export-detection.ts b/gitnexus/src/core/ingestion/export-detection.ts index 3eb1b4f07..27e9d9764 100644 --- a/gitnexus/src/core/ingestion/export-detection.ts +++ b/gitnexus/src/core/ingestion/export-detection.ts @@ -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; diff --git a/gitnexus/src/core/ingestion/field-extractors/configs/lua.ts b/gitnexus/src/core/ingestion/field-extractors/configs/lua.ts new file mode 100644 index 000000000..620d22379 --- /dev/null +++ b/gitnexus/src/core/ingestion/field-extractors/configs/lua.ts @@ -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; + }, +}; diff --git a/gitnexus/src/core/ingestion/heritage-extractors/configs/lua.ts b/gitnexus/src/core/ingestion/heritage-extractors/configs/lua.ts new file mode 100644 index 000000000..eb479b267 --- /dev/null +++ b/gitnexus/src/core/ingestion/heritage-extractors/configs/lua.ts @@ -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, +}; diff --git a/gitnexus/src/core/ingestion/import-resolvers/configs/lua.ts b/gitnexus/src/core/ingestion/import-resolvers/configs/lua.ts new file mode 100644 index 000000000..18a520ed7 --- /dev/null +++ b/gitnexus/src/core/ingestion/import-resolvers/configs/lua.ts @@ -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], +}; diff --git a/gitnexus/src/core/ingestion/import-resolvers/utils.ts b/gitnexus/src/core/ingestion/import-resolvers/utils.ts index 8d915eb7f..e64bf81a2 100644 --- a/gitnexus/src/core/ingestion/import-resolvers/utils.ts +++ b/gitnexus/src/core/ingestion/import-resolvers/utils.ts @@ -47,6 +47,9 @@ export const EXTENSIONS = [ '.swift', // Ruby '.rb', + // Lua + '.lua', + '/init.lua', ]; /** diff --git a/gitnexus/src/core/ingestion/languages/index.ts b/gitnexus/src/core/ingestion/languages/index.ts index 041af775d..f1c9b219a 100644 --- a/gitnexus/src/core/ingestion/languages/index.ts +++ b/gitnexus/src/core/ingestion/languages/index.ts @@ -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; diff --git a/gitnexus/src/core/ingestion/languages/lua.ts b/gitnexus/src/core/ingestion/languages/lua.ts new file mode 100644 index 000000000..2d25fffe9 --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/lua.ts @@ -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 = 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, +}); diff --git a/gitnexus/src/core/ingestion/method-extractors/configs/lua.ts b/gitnexus/src/core/ingestion/method-extractors/configs/lua.ts new file mode 100644 index 000000000..a196d444f --- /dev/null +++ b/gitnexus/src/core/ingestion/method-extractors/configs/lua.ts @@ -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; + }, +}; diff --git a/gitnexus/src/core/ingestion/tree-sitter-queries.ts b/gitnexus/src/core/ingestion/tree-sitter-queries.ts index 61030ff66..89753a0d3 100644 --- a/gitnexus/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus/src/core/ingestion/tree-sitter-queries.ts @@ -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 = { @@ -1390,6 +1448,7 @@ export const LANGUAGE_QUERIES: Record = { [SupportedLanguages.Ruby]: RUBY_QUERIES, [SupportedLanguages.Swift]: SWIFT_QUERIES, [SupportedLanguages.Dart]: DART_QUERIES, + [SupportedLanguages.Lua]: LUA_QUERIES, [SupportedLanguages.Vue]: TYPESCRIPT_QUERIES, // Vue