diff --git a/gitnexus-shared/src/language-detection.ts b/gitnexus-shared/src/language-detection.ts index d31f9d58d..00547cdaa 100644 --- a/gitnexus-shared/src/language-detection.ts +++ b/gitnexus-shared/src/language-detection.ts @@ -43,6 +43,7 @@ const EXTENSION_MAP: Record = { [SupportedLanguages.Dart]: ['.dart'], [SupportedLanguages.Vue]: ['.vue'], [SupportedLanguages.Cobol]: ['.cbl', '.cob', '.cpy', '.cobol'], + [SupportedLanguages.Lua]: ['.lua'], } satisfies Record; // Ensure exhaustiveness /** Pre-built reverse lookup: extension → language (built once at module load). */ @@ -101,6 +102,7 @@ const SYNTAX_MAP: Record = { [SupportedLanguages.Dart]: 'dart', [SupportedLanguages.Vue]: 'typescript', [SupportedLanguages.Cobol]: 'cobol', + [SupportedLanguages.Lua]: 'lua', } satisfies Record; // Ensure exhaustiveness /** Non-code file extensions → Prism-compatible syntax identifiers */ diff --git a/gitnexus-shared/src/languages.ts b/gitnexus-shared/src/languages.ts index 29edb8d1e..3826dc915 100644 --- a/gitnexus-shared/src/languages.ts +++ b/gitnexus-shared/src/languages.ts @@ -22,4 +22,5 @@ export enum SupportedLanguages { Vue = 'vue', /** Standalone regex processor — no tree-sitter, no LanguageProvider. */ Cobol = 'cobol', + Lua = 'lua', } diff --git a/gitnexus/package.json b/gitnexus/package.json index 9045662aa..f1c2c5cc3 100644 --- a/gitnexus/package.json +++ b/gitnexus/package.json @@ -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", diff --git a/gitnexus/src/core/ingestion/export-detection.ts b/gitnexus/src/core/ingestion/export-detection.ts index 3eb1b4f07..98b91fa83 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: no explicit visibility system — all top-level definitions are public. */ +export const luaExportChecker: ExportChecker = (_node, _name) => true; 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..1657dfbcb --- /dev/null +++ b/gitnexus/src/core/ingestion/import-resolvers/configs/lua.ts @@ -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], +}; diff --git a/gitnexus/src/core/ingestion/languages/index.ts b/gitnexus/src/core/ingestion/languages/index.ts index 041af775d..02f907536 100644 --- a/gitnexus/src/core/ingestion/languages/index.ts +++ b/gitnexus/src/core/ingestion/languages/index.ts @@ -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; /** Get provider by language enum (always succeeds for SupportedLanguages). */ diff --git a/gitnexus/src/core/ingestion/languages/lua.ts b/gitnexus/src/core/ingestion/languages/lua.ts new file mode 100644 index 000000000..7cf81fcb1 --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/lua.ts @@ -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 = 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, +}); diff --git a/gitnexus/src/core/ingestion/languages/lua/captures.ts b/gitnexus/src/core/ingestion/languages/lua/captures.ts new file mode 100644 index 000000000..9f8d7d2fb --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/lua/captures.ts @@ -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['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 = {}; + 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; +} diff --git a/gitnexus/src/core/ingestion/languages/lua/index.ts b/gitnexus/src/core/ingestion/languages/lua/index.ts new file mode 100644 index 000000000..a2d80fa6c --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/lua/index.ts @@ -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'; diff --git a/gitnexus/src/core/ingestion/languages/lua/query.ts b/gitnexus/src/core/ingestion/languages/lua/query.ts new file mode 100644 index 000000000..2b79bbc7a --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/lua/query.ts @@ -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[0]); + } + return _parser; +} + +export function getLuaScopeQuery(): Parser.Query { + if (_query === null) { + _query = new Parser.Query( + Lua as Parameters[0], + LUA_SCOPE_QUERY, + ); + } + return _query; +} diff --git a/gitnexus/src/core/ingestion/tree-sitter-queries.ts b/gitnexus/src/core/ingestion/tree-sitter-queries.ts index 18b163777..2958ac3d3 100644 --- a/gitnexus/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus/src/core/ingestion/tree-sitter-queries.ts @@ -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.TypeScript]: TYPESCRIPT_QUERIES, [SupportedLanguages.JavaScript]: JAVASCRIPT_QUERIES, @@ -1351,4 +1451,5 @@ export const LANGUAGE_QUERIES: Record = { [SupportedLanguages.Dart]: DART_QUERIES, [SupportedLanguages.Vue]: TYPESCRIPT_QUERIES, // Vue