mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-07 02:58:02 +00:00
fix(lua): address resolver and scanner review findings
This commit is contained in:
parent
16f3641ca5
commit
f192855ba4
7 changed files with 101 additions and 20 deletions
|
|
@ -2,7 +2,7 @@
|
|||
* Lua import resolution config (legacy Phase A path).
|
||||
*
|
||||
* `require("a.b.c")` → module path `a/b/c`, resolved via suffixResolve
|
||||
* (tries .lua then /init.lua — see EXTENSIONS in utils.ts). Mirrors
|
||||
* (tries .lua then /init.lua — see LUA_EXTENSIONS in utils.ts). Mirrors
|
||||
* rubyRequireStrategy but splits on `.` (Lua's module separator) and strips
|
||||
* surrounding quotes (the @import.source capture is a string literal).
|
||||
*
|
||||
|
|
@ -15,13 +15,19 @@
|
|||
*/
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import type { ImportResolutionConfig, ImportResolverStrategy } from '../types.js';
|
||||
import { suffixResolve } from '../utils.js';
|
||||
import { LUA_EXTENSIONS, suffixResolve } from '../utils.js';
|
||||
|
||||
export const luaRequireStrategy: ImportResolverStrategy = (rawImportPath, _filePath, ctx) => {
|
||||
const cleaned = rawImportPath.replace(/^["']|["']$/g, '');
|
||||
const pathParts = cleaned.split('.').filter(Boolean);
|
||||
if (pathParts.length === 0) return null;
|
||||
const resolved = suffixResolve(pathParts, ctx.normalizedFileList, ctx.allFileList, ctx.index);
|
||||
const resolved = suffixResolve(
|
||||
pathParts,
|
||||
ctx.normalizedFileList,
|
||||
ctx.allFileList,
|
||||
ctx.index,
|
||||
LUA_EXTENSIONS,
|
||||
);
|
||||
return resolved ? { kind: 'files', files: [resolved] } : null;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -22,9 +22,6 @@ export const EXTENSIONS = [
|
|||
// Python
|
||||
'.py',
|
||||
'/__init__.py',
|
||||
// Lua — require("a.b.c") resolves to a/b/c.lua or a/b/c/init.lua
|
||||
'.lua',
|
||||
'/init.lua',
|
||||
// Java
|
||||
'.java',
|
||||
// Kotlin
|
||||
|
|
@ -57,6 +54,9 @@ export const EXTENSIONS = [
|
|||
'.rb',
|
||||
];
|
||||
|
||||
/** Lua module extensions used only by Lua import resolvers. */
|
||||
export const LUA_EXTENSIONS = ['', '.lua', '/init.lua'] as const;
|
||||
|
||||
/**
|
||||
* Try to match a path (with extensions) against the known file set.
|
||||
* Returns the matched file path or null.
|
||||
|
|
@ -64,8 +64,9 @@ export const EXTENSIONS = [
|
|||
export function tryResolveWithExtensions(
|
||||
basePath: string,
|
||||
allFiles: ReadonlySet<string>,
|
||||
extensions: readonly string[] = EXTENSIONS,
|
||||
): string | null {
|
||||
for (const ext of EXTENSIONS) {
|
||||
for (const ext of extensions) {
|
||||
const candidate = basePath + ext;
|
||||
if (allFiles.has(candidate)) return candidate;
|
||||
}
|
||||
|
|
@ -392,11 +393,12 @@ export function suffixResolve(
|
|||
normalizedFileList: readonly string[],
|
||||
allFileList: readonly string[],
|
||||
index?: SuffixIndex,
|
||||
extensions: readonly string[] = EXTENSIONS,
|
||||
): string | null {
|
||||
if (index) {
|
||||
for (let i = 0; i < pathParts.length; i++) {
|
||||
const suffix = pathParts.slice(i).join('/');
|
||||
for (const ext of EXTENSIONS) {
|
||||
for (const ext of extensions) {
|
||||
const suffixWithExt = suffix + ext;
|
||||
const result = index.get(suffixWithExt) || index.getInsensitive(suffixWithExt);
|
||||
if (result) return result;
|
||||
|
|
@ -408,7 +410,7 @@ export function suffixResolve(
|
|||
// Fallback: linear scan (for backward compatibility)
|
||||
for (let i = 0; i < pathParts.length; i++) {
|
||||
const suffix = pathParts.slice(i).join('/');
|
||||
for (const ext of EXTENSIONS) {
|
||||
for (const ext of extensions) {
|
||||
const suffixWithExt = suffix + ext;
|
||||
const suffixPattern = '/' + suffixWithExt;
|
||||
const matchIdx = normalizedFileList.findIndex(
|
||||
|
|
|
|||
|
|
@ -14,7 +14,12 @@
|
|||
import type { ParsedFile } from 'gitnexus-shared';
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import { populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js';
|
||||
import { buildSuffixIndex, suffixResolve, type SuffixIndex } from '../../import-resolvers/utils.js';
|
||||
import {
|
||||
buildSuffixIndex,
|
||||
LUA_EXTENSIONS,
|
||||
suffixResolve,
|
||||
type SuffixIndex,
|
||||
} from '../../import-resolvers/utils.js';
|
||||
import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js';
|
||||
import { luaProvider } from '../lua.js';
|
||||
import { emitLuaHeritageEdges } from './heritage.js';
|
||||
|
|
@ -43,7 +48,7 @@ const luaScopeResolver: ScopeResolver = {
|
|||
return undefined;
|
||||
},
|
||||
|
||||
// require("a.b.c") → module path a/b/c (+ EXTENSIONS: .lua / /init.lua).
|
||||
// require("a.b.c") → module path a/b/c (+ Lua extensions: .lua / /init.lua).
|
||||
// targetRaw arrives quote-stripped (interpretLuaImport); Lua's module
|
||||
// separator is `.`, so split on it before joining to a path.
|
||||
resolveImportTarget: (targetRaw, _fromFile, allFilePaths) => {
|
||||
|
|
@ -60,11 +65,11 @@ const luaScopeResolver: ScopeResolver = {
|
|||
// Once the suffix index exists, suffixResolve only consults the index;
|
||||
// passing a spread copy here would still traverse the entire workspace on
|
||||
// every require() despite the cache (#2909 contract).
|
||||
return suffixResolve(parts, [], [], _cachedIndex ?? undefined);
|
||||
return suffixResolve(parts, [], [], _cachedIndex ?? undefined, LUA_EXTENSIONS);
|
||||
},
|
||||
|
||||
// Lua: default local-first-then-imports merge (no language-specific precedence).
|
||||
mergeBindings: (existing) => [...existing],
|
||||
mergeBindings: (existing, incoming) => [...existing, ...incoming],
|
||||
|
||||
// Lua varargs (...) + optional params make static arity checks unreliable —
|
||||
// 'unknown' (no signal) is the safe minimal choice.
|
||||
|
|
|
|||
|
|
@ -19,8 +19,10 @@ import {
|
|||
runPipelineFromRepo,
|
||||
type PipelineResult,
|
||||
} from './helpers.js';
|
||||
import { SupportedLanguages, type BindingRef, type ScopeId } from 'gitnexus-shared';
|
||||
import { emitLuaScopeCaptures } from '../../../src/core/ingestion/languages/lua/index.js';
|
||||
import { collectLuaCaptureSideChannel } from '../../../src/core/ingestion/languages/lua/capture-side-channel.js';
|
||||
import { luaScopeResolver } from '../../../src/core/ingestion/languages/lua/scope-resolver.js';
|
||||
|
||||
function writeFixtureRepo(root: string, files: Record<string, string>): void {
|
||||
for (const [rel, content] of Object.entries(files)) {
|
||||
|
|
@ -30,6 +32,32 @@ function writeFixtureRepo(root: string, files: Record<string, string>): void {
|
|||
}
|
||||
}
|
||||
|
||||
describe('Lua scope resolver binding merge', () => {
|
||||
it('retains imported bindings when layering them onto existing bindings', () => {
|
||||
const local = {
|
||||
def: { nodeId: 'local', filePath: 'main.lua', type: 'Variable', qualifiedName: 'local' },
|
||||
origin: 'local',
|
||||
} satisfies BindingRef;
|
||||
const imported = {
|
||||
def: { nodeId: 'imported', filePath: 'lib.lua', type: 'Variable', qualifiedName: 'imported' },
|
||||
origin: 'import',
|
||||
} satisfies BindingRef;
|
||||
|
||||
expect(luaScopeResolver.mergeBindings([local], [imported], 'scope:main' as ScopeId)).toEqual([
|
||||
local,
|
||||
imported,
|
||||
]);
|
||||
expect(luaScopeResolver.language).toBe(SupportedLanguages.Lua);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Lua scope resolver import extensions', () => {
|
||||
it('prefers the Lua module when another language has the same module stem', () => {
|
||||
const files = new Set(['main.lua', 'foo.ts', 'foo.lua']);
|
||||
expect(luaScopeResolver.resolveImportTarget('foo', 'main.lua', files)).toBe('foo.lua');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// require("lib.util") + member call util.answer() across files
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -88,6 +88,12 @@ describe('TypeScript ESM .js extension resolution', () => {
|
|||
expect(result).toBe('src/utils.ts');
|
||||
});
|
||||
|
||||
it('does not resolve a JavaScript extensionless import to a Lua file', () => {
|
||||
const ctx = makeCtx(['src/index.js', 'src/utils.lua']);
|
||||
const result = resolve('src/index.js', './utils', SupportedLanguages.JavaScript, ctx);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('does NOT apply ESM fallback for non-TS/JS languages', () => {
|
||||
const ctx = makeCtx(['src/main.py', 'src/utils.ts']);
|
||||
const result = resolve('src/main.py', './utils.js', SupportedLanguages.Python, ctx);
|
||||
|
|
|
|||
|
|
@ -170,13 +170,27 @@ describe('parser-loader ABI load-smoke (#1922)', () => {
|
|||
}
|
||||
|
||||
it('repeatedly creates and parses Lua parsers without stale scanner state', () => {
|
||||
const grammar = getLanguageGrammar(SupportedLanguages.Lua);
|
||||
let grammar: unknown;
|
||||
try {
|
||||
grammar = getLanguageGrammar(SupportedLanguages.Lua);
|
||||
} catch (err) {
|
||||
// Lua is a user-skippable vendored grammar. Its dedicated smoke must
|
||||
// have the same unavailable-is-okay behavior as the matrix above.
|
||||
expect(err).toBeInstanceOf(Error);
|
||||
return;
|
||||
}
|
||||
|
||||
const longEquals = '='.repeat(300);
|
||||
for (let i = 0; i < 16; i += 1) {
|
||||
const parser = new Parser();
|
||||
parser.setLanguage(grammar as Parameters<Parser['setLanguage']>[0]);
|
||||
const snippet = i % 2 === 0 ? 'local x = "lua"\n' : '-- comment\nlocal x = [[lua]]\n';
|
||||
const snippet =
|
||||
i % 2 === 0
|
||||
? 'local x = "lua"\n'
|
||||
: `-- comment\nlocal x = [${longEquals}[lua]${longEquals}]\n`;
|
||||
const tree = parser.parse(snippet);
|
||||
expect(tree.rootNode.type).toBe('chunk');
|
||||
expect(tree.rootNode.hasError).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
30
gitnexus/vendor/tree-sitter-lua/src/scanner.c
vendored
30
gitnexus/vendor/tree-sitter-lua/src/scanner.c
vendored
|
|
@ -48,6 +48,12 @@ struct ScannerState
|
|||
unsigned int depth;
|
||||
};
|
||||
|
||||
// Keep the scanner state portable across parser checkpoints. The previous
|
||||
// format stored depth in one byte, which truncated long-bracket delimiters
|
||||
// with more than 255 '=' characters. Length 2 remains readable for old
|
||||
// checkpoints; new checkpoints use a full 32-bit little-endian depth.
|
||||
#define LUA_SCANNER_SERIALIZED_LENGTH 5
|
||||
|
||||
void *tree_sitter_lua_external_scanner_create()
|
||||
{
|
||||
return calloc(1, sizeof(struct ScannerState));
|
||||
|
|
@ -62,17 +68,31 @@ unsigned int tree_sitter_lua_external_scanner_serialize(void *payload, char *buf
|
|||
{
|
||||
struct ScannerState *state = payload;
|
||||
buffer[0] = state->started;
|
||||
buffer[1] = state->depth;
|
||||
return 2;
|
||||
buffer[1] = (char)(state->depth & 0xffu);
|
||||
buffer[2] = (char)((state->depth >> 8) & 0xffu);
|
||||
buffer[3] = (char)((state->depth >> 16) & 0xffu);
|
||||
buffer[4] = (char)((state->depth >> 24) & 0xffu);
|
||||
return LUA_SCANNER_SERIALIZED_LENGTH;
|
||||
}
|
||||
|
||||
void tree_sitter_lua_external_scanner_deserialize(void *payload, const char *buffer, unsigned int length)
|
||||
{
|
||||
struct ScannerState *state = payload;
|
||||
state->started = 0;
|
||||
state->depth = 0;
|
||||
|
||||
if (length == 2)
|
||||
{
|
||||
struct ScannerState *state = payload;
|
||||
state->started = buffer[0];
|
||||
state->depth = buffer[1];
|
||||
state->started = (enum StartedToken)(unsigned char)buffer[0];
|
||||
state->depth = (unsigned int)(unsigned char)buffer[1];
|
||||
}
|
||||
else if (length >= LUA_SCANNER_SERIALIZED_LENGTH)
|
||||
{
|
||||
state->started = (enum StartedToken)(unsigned char)buffer[0];
|
||||
state->depth = (unsigned int)(unsigned char)buffer[1] |
|
||||
((unsigned int)(unsigned char)buffer[2] << 8) |
|
||||
((unsigned int)(unsigned char)buffer[3] << 16) |
|
||||
((unsigned int)(unsigned char)buffer[4] << 24);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue