mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-14 04:08:05 +00:00
fix(lua): preserve positional capture bindings
This commit is contained in:
parent
151bb7865f
commit
2649949ad4
2 changed files with 131 additions and 49 deletions
|
|
@ -126,24 +126,22 @@ function collectLuaImportCaptures(root: Parser.SyntaxNode): readonly CaptureMatc
|
|||
.filter((name): name is Parser.SyntaxNode => name?.type === 'identifier');
|
||||
|
||||
// Pair by source position. Never reuse an RHS for multiple LHS names.
|
||||
const pairedRequires = new Set<Parser.SyntaxNode>();
|
||||
for (let i = 0; i < Math.min(names.length, expressions.length); i++) {
|
||||
const expression = expressions[i];
|
||||
if (!isRequireCall(expression)) continue;
|
||||
const source = requireStringNode(expression);
|
||||
if (source !== undefined) out.push(importCapture(node, source, names[i]));
|
||||
if (source !== undefined) {
|
||||
pairedRequires.add(expression);
|
||||
out.push(importCapture(node, source, names[i]));
|
||||
}
|
||||
}
|
||||
|
||||
// Keep walking all initializer descendants. Direct require calls are
|
||||
// suppressed because they already have their positional local binding;
|
||||
// nested calls (function bodies, wrappers, and other expressions) still
|
||||
// need their own IMPORTS edge.
|
||||
const directRequires = new Set<Parser.SyntaxNode>();
|
||||
for (const expression of expressions) {
|
||||
if (isRequireCall(expression) && requireStringNode(expression) !== undefined) {
|
||||
directRequires.add(expression);
|
||||
}
|
||||
}
|
||||
for (const child of node.namedChildren) visit(child, directRequires);
|
||||
for (const child of node.namedChildren) visit(child, pairedRequires);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -254,18 +252,23 @@ function collectLuaAssignmentMethodCaptures(root: Parser.SyntaxNode): readonly C
|
|||
const out: CaptureMatch[] = [];
|
||||
const visit = (node: Parser.SyntaxNode): void => {
|
||||
if (node.type === 'variable_assignment') {
|
||||
const variable = node.namedChildren.find((child) => child.type === 'variable_list')
|
||||
?.namedChildren[0];
|
||||
const value = node.namedChildren.find((child) => child.type === 'expression_list')
|
||||
?.namedChildren[0];
|
||||
const owner = variable?.childForFieldName('table');
|
||||
const method = variable?.childForFieldName('field');
|
||||
if (
|
||||
variable?.type === 'variable' &&
|
||||
owner?.type === 'identifier' &&
|
||||
(method?.type === 'identifier' || method?.type === 'string') &&
|
||||
value?.type === 'function_definition'
|
||||
) {
|
||||
const variables =
|
||||
node.namedChildren.find((child) => child.type === 'variable_list')?.namedChildren ?? [];
|
||||
const values =
|
||||
node.namedChildren.find((child) => child.type === 'expression_list')?.namedChildren ?? [];
|
||||
for (let index = 0; index < Math.min(variables.length, values.length); index++) {
|
||||
const variable = variables[index];
|
||||
const value = values[index];
|
||||
const owner = variable?.childForFieldName('table');
|
||||
const method = variable?.childForFieldName('field');
|
||||
if (
|
||||
variable?.type !== 'variable' ||
|
||||
owner?.type !== 'identifier' ||
|
||||
(method?.type !== 'identifier' && method?.type !== 'string') ||
|
||||
value?.type !== 'function_definition'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let enclosing = node.parent;
|
||||
let nestedInFunction = false;
|
||||
while (enclosing !== null) {
|
||||
|
|
@ -280,8 +283,7 @@ function collectLuaAssignmentMethodCaptures(root: Parser.SyntaxNode): readonly C
|
|||
enclosing = enclosing.parent;
|
||||
}
|
||||
if (nestedInFunction) {
|
||||
for (const child of node.namedChildren) visit(child);
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
const match: Record<string, Capture> = {
|
||||
'@scope.function': nodeToCapture('@scope.function', value),
|
||||
|
|
@ -313,33 +315,33 @@ function collectLuaCallableAliases(root: Parser.SyntaxNode): readonly LuaCallabl
|
|||
!nextInFunction &&
|
||||
(node.type === 'local_variable_declaration' || node.type === 'variable_assignment')
|
||||
) {
|
||||
const destination = node.namedChildren.find((child) => child.type === 'variable_list')
|
||||
?.namedChildren[0];
|
||||
const source = node.namedChildren.find((child) => child.type === 'expression_list')
|
||||
?.namedChildren[0];
|
||||
const destinationName =
|
||||
destination?.type === 'variable' &&
|
||||
destination.childForFieldName('name')?.type === 'identifier' &&
|
||||
destination.childForFieldName('table') === null &&
|
||||
destination.childForFieldName('field') === null
|
||||
? destination.childForFieldName('name')?.text
|
||||
: undefined;
|
||||
const sourceIsStaticMember =
|
||||
source?.type === 'variable' &&
|
||||
source.childForFieldName('table')?.type === 'identifier' &&
|
||||
(source.childForFieldName('field')?.type === 'identifier' ||
|
||||
source.childForFieldName('method')?.type === 'identifier');
|
||||
const sourceIsSimple =
|
||||
source?.type === 'variable' &&
|
||||
source.childForFieldName('name')?.type === 'identifier' &&
|
||||
source.childForFieldName('table') === null &&
|
||||
source.childForFieldName('field') === null;
|
||||
if (
|
||||
destinationName !== undefined &&
|
||||
source !== undefined &&
|
||||
(sourceIsStaticMember || sourceIsSimple)
|
||||
) {
|
||||
aliases.push({ destination: destinationName, source: source.text });
|
||||
const destinations =
|
||||
node.namedChildren.find((child) => child.type === 'variable_list')?.namedChildren ?? [];
|
||||
const sources =
|
||||
node.namedChildren.find((child) => child.type === 'expression_list')?.namedChildren ?? [];
|
||||
for (let index = 0; index < Math.min(destinations.length, sources.length); index++) {
|
||||
const destination = destinations[index];
|
||||
const source = sources[index];
|
||||
const destinationName =
|
||||
destination?.type === 'variable' &&
|
||||
destination.childForFieldName('name')?.type === 'identifier' &&
|
||||
destination.childForFieldName('table') === null &&
|
||||
destination.childForFieldName('field') === null
|
||||
? destination.childForFieldName('name')?.text
|
||||
: undefined;
|
||||
const sourceIsStaticMember =
|
||||
source?.type === 'variable' &&
|
||||
source.childForFieldName('table')?.type === 'identifier' &&
|
||||
(source.childForFieldName('field')?.type === 'identifier' ||
|
||||
source.childForFieldName('method')?.type === 'identifier');
|
||||
const sourceIsSimple =
|
||||
source?.type === 'variable' &&
|
||||
source.childForFieldName('name')?.type === 'identifier' &&
|
||||
source.childForFieldName('table') === null &&
|
||||
source.childForFieldName('field') === null;
|
||||
if (destinationName !== undefined && (sourceIsStaticMember || sourceIsSimple)) {
|
||||
aliases.push({ destination: destinationName, source: source.text });
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const child of node.namedChildren) visit(child, nextInFunction);
|
||||
|
|
|
|||
|
|
@ -113,6 +113,32 @@ describe('Lua scope resolver require syntax and positional bindings', () => {
|
|||
}
|
||||
}, 60000);
|
||||
|
||||
it('keeps unpaired RHS requires as wildcard imports', async () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lua-scope-extra-require-'));
|
||||
try {
|
||||
writeFixtureRepo(tmpDir, {
|
||||
'x.lua': 'return {}\n',
|
||||
'y.lua': 'return {}\n',
|
||||
'main.lua': 'local x = require("x"), require("y")\n',
|
||||
});
|
||||
const captures = emitLuaScopeCaptures(
|
||||
'local x = require("x"), require("y")\n',
|
||||
'main.lua',
|
||||
).filter((capture) => capture['@import.source'] !== undefined);
|
||||
expect(captures.map((capture) => capture['@import.source']?.text).sort()).toEqual([
|
||||
'"x"',
|
||||
'"y"',
|
||||
]);
|
||||
expect(
|
||||
captures.find((capture) => capture['@import.source']?.text === '"y"')?.[
|
||||
'@import.localName'
|
||||
],
|
||||
).toBeUndefined();
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
it('recognizes parenthesis-free short and long-bracket requires', async () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lua-scope-require-forms-'));
|
||||
try {
|
||||
|
|
@ -560,6 +586,32 @@ return Dog
|
|||
}
|
||||
}, 60000);
|
||||
|
||||
it('pairs assignment-form methods by position', async () => {
|
||||
const tmpDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'lua-scope-assignment-method-positional-'),
|
||||
);
|
||||
try {
|
||||
writeFixtureRepo(tmpDir, {
|
||||
'dog.lua': `local Dog = class("Dog")
|
||||
local ignored
|
||||
ignored, Dog.run = nil, function(self)
|
||||
return "run"
|
||||
end
|
||||
return Dog
|
||||
`,
|
||||
});
|
||||
|
||||
const result = await runPipelineFromRepo(tmpDir, () => {});
|
||||
expect(
|
||||
getRelationships(result, 'HAS_METHOD').some(
|
||||
(edge) => edge.source === 'Dog' && edge.target === 'run',
|
||||
),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
it('captures static string-key assignment-form methods', async () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lua-scope-string-method-'));
|
||||
try {
|
||||
|
|
@ -822,6 +874,34 @@ second()
|
|||
}
|
||||
}, 60000);
|
||||
|
||||
it('pairs callable aliases by position in multi-assignment', async () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lua-scope-callable-alias-positional-'));
|
||||
try {
|
||||
writeFixtureRepo(tmpDir, {
|
||||
'util.lua': `function answer()
|
||||
return 42
|
||||
end
|
||||
return { answer = answer }
|
||||
`,
|
||||
'main.lua': `local util = require("util")
|
||||
local ignored, answer = nil, util.answer
|
||||
answer()
|
||||
`,
|
||||
});
|
||||
const result = await runPipelineFromRepo(tmpDir, () => {});
|
||||
expect(
|
||||
getRelationships(result, 'CALLS').some(
|
||||
(edge) =>
|
||||
edge.sourceFilePath?.endsWith('main.lua') &&
|
||||
edge.target === 'answer' &&
|
||||
edge.targetFilePath?.endsWith('util.lua'),
|
||||
),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
it('fails closed for callable alias cycles, dynamic keys, and factory results', async () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lua-scope-callable-alias-negative-'));
|
||||
try {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue