mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-02 09:45:39 +08:00
fix(lua): a bare call is never a table method; Lua's globals are Lua's (#2202)
- A bare Lua call cannot reach a table's method (`function M.x`, `function M:x`) without its table: kong's 118 `error(...)` calls went to its response PDK's `_RESPONSE.error`, 111 to a mock's `error`, busted's `setup`/`teardown` to perf helpers' and factories'. - A bare call to a Lua global (`ipairs`, `error`, `require`, busted's `describe`/`it`/`setup`...) is Lua's unless the file defines its own function of that name: telescope's 120 `for _, v in ipairs(...)` went to a linked list's `ipairs` method, apisix's 36 `require(...)` to a wasm module's `_M.require`. - A function's own locals are not nodes, so the alias reader now reads `local x = ...` from source above the call within its function, and a member of a host global table (`kong.request.get_query`) resolves to the one method of a table named after its holder (`_REQUEST`). kong's `local clear_header = kong.service.request.clear_header` had resolved to the RESPONSE module's - now nothing, correctly. telescope -129, kong -516 +58 (indexing 4.7s -> 3.5s), apisix -57 +18, lua-resty-core -4, roact -3 +1. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
116feb462d
commit
b55836daa3
@@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
### Fixes
|
||||
|
||||
- A bare Lua call like `ipairs(t)`, `error("…")` or busted's `setup()` now means Lua's own function unless the file defines one. It no longer lands on a table method of the same name: telescope's `ipairs` calls had gone to a linked list's `ipairs`, and kong's `error` calls to its response module. A function's own alias of a host table member, like `local get_query = kong.request.get_query`, is now followed as well.
|
||||
- A C++ call into a macro-opened namespace, like `fmt::format("{}", x)`, now picks the overload its arguments fit. Before, the first overload indexed won: fmt's 1,400 `fmt::format` calls all reached the `text_style` overload in `color.h`. A narrow string literal now fits a narrow format string, a wide `L"…"` fits the wide overload, and the argument count has to fit too.
|
||||
- Calls inside an inline route handler now keep their receivers, so they resolve like any other method call. Before, Hono's `app.get('/', (c) => c.text('Hello'))` linked the route to the one other `text` method in the project, its client's `ClientResponse.text`; that happened in over 400 routes. A call like `userService.lookup(id)` reaches the service it names.
|
||||
- A Scala chained call like `xs.map { … }` no longer lands on an object that happens to be named `map`, as with cats' `alleycats.std.map` and its `syntax`/`instances` objects. A type or object in a chain is only matched when the receiver names what holds it, as in `sttp.client4.Response(…)`.
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* A bare Lua call cannot reach a table's method — `function M.error()` is
|
||||
* `M.error(…)`, never `error(…)` — and a Lua global (`ipairs`, `error`,
|
||||
* busted's `setup`) is Lua's unless the file defines its own. Telescope's
|
||||
* 120 `for _, v in ipairs(…)` went to a linked list's `ipairs` method,
|
||||
* kong's 118 `error(…)` to its response PDK's `error`. A function's own
|
||||
* `local get_query = kong.request.get_query` alias still reaches the method
|
||||
* of the table named after its holder.
|
||||
*/
|
||||
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { CodeGraph } from '../src';
|
||||
|
||||
let root = '';
|
||||
let cg: CodeGraph;
|
||||
|
||||
beforeAll(async () => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-lua-builtins-'));
|
||||
const files: Record<string, string> = {
|
||||
'lua/app/linked_list.lua': `local LinkedList = {}
|
||||
|
||||
function LinkedList:ipairs()
|
||||
return function() end
|
||||
end
|
||||
|
||||
return LinkedList
|
||||
`,
|
||||
'lua/app/response.lua': `local _RESPONSE = {}
|
||||
|
||||
function _RESPONSE.error(status)
|
||||
return status
|
||||
end
|
||||
|
||||
return _RESPONSE
|
||||
`,
|
||||
'lua/app/request.lua': `local _REQUEST = {}
|
||||
|
||||
function _REQUEST.get_query()
|
||||
return {}
|
||||
end
|
||||
|
||||
return _REQUEST
|
||||
`,
|
||||
'lua/app/handler.lua': `local M = {}
|
||||
|
||||
function M.run(items)
|
||||
for _, v in ipairs(items) do
|
||||
if not v then
|
||||
error("missing")
|
||||
end
|
||||
end
|
||||
local get_query = kong.request.get_query
|
||||
return get_query()
|
||||
end
|
||||
|
||||
return M
|
||||
`,
|
||||
};
|
||||
for (const [rel, content] of Object.entries(files)) {
|
||||
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, rel), content);
|
||||
}
|
||||
cg = await CodeGraph.init(root, { index: true });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
cg?.close();
|
||||
if (root) fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const callsFrom = (file: string) => {
|
||||
const ids = cg.getNodesInFile(file).map((n) => n.id);
|
||||
return cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)!.qualifiedName).sort();
|
||||
};
|
||||
|
||||
describe('bare Lua calls', () => {
|
||||
it('are Lua’s globals, never a table’s method of that name', () => {
|
||||
const calls = callsFrom('lua/app/handler.lua');
|
||||
expect(calls).not.toContain('LinkedList::ipairs');
|
||||
expect(calls).not.toContain('_RESPONSE::error');
|
||||
});
|
||||
|
||||
it('still follow a function’s own alias of a host table’s member', () => {
|
||||
expect(callsFrom('lua/app/handler.lua')).toContain('_REQUEST::get_query');
|
||||
});
|
||||
});
|
||||
@@ -1012,6 +1012,22 @@ function luaLocalDecl(name: string, file: string, line: number, context: Resolut
|
||||
if (!isLuaLocal(n, context)) continue;
|
||||
if (!best || n.startLine > best.startLine) best = n;
|
||||
}
|
||||
// A function's own locals are not nodes: kong's `local clear_header =
|
||||
// kong.response.clear_header` inside an access handler. Read the nearest
|
||||
// one above the call within the innermost function around it.
|
||||
const fn = fns.filter((f) => f.startLine <= line && line <= f.endLine && f.startLine < line)
|
||||
.sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
|
||||
if (fn && (!best || best.startLine < fn.startLine)) {
|
||||
const lines = context.getFileLines?.(file) ?? context.readFile(file)?.split(/\r?\n/) ?? [];
|
||||
const decl = new RegExp(`^\\s*local\\s+${name}\\s*=\\s*(.+?)\\s*$`);
|
||||
for (let at = line - 1; at > fn.startLine; at--) {
|
||||
const m = decl.exec(lines[at - 1] ?? '');
|
||||
if (!m) continue;
|
||||
const fileNode = nodes.find((n) => n.kind === 'file');
|
||||
if (!fileNode) break;
|
||||
return { ...fileNode, name, kind: 'variable', signature: `= ${m[1]}`, startLine: at, endLine: at };
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
@@ -1044,7 +1060,17 @@ function luaAliasExpr(rhs: string, site: LuaSite, context: ResolutionContext, de
|
||||
return undefined;
|
||||
}
|
||||
if (path.length === 0) return LUA_GLOBAL_FUNCTIONS.has(root) ? null : undefined;
|
||||
if (!LUA_LIBRARY_TABLES.has(root) || path.length !== 1) return undefined;
|
||||
if (!LUA_LIBRARY_TABLES.has(root)) {
|
||||
// A member of a global table the host provides — kong's `local clear_header =
|
||||
// kong.response.clear_header` — is the one method of a table named after its holder.
|
||||
const member = path[path.length - 1]!;
|
||||
const holder = path.length > 1 ? path[path.length - 2]! : root;
|
||||
const owned = context.getNodesByName(member).filter((n) =>
|
||||
(n.language === 'lua' || n.language === 'luau') && n.kind === 'method' && sharesReceiverWord(holder, n) &&
|
||||
!(isTestPath(n.filePath) && !isTestPath(site.file)));
|
||||
return owned.length === 1 ? owned[0]! : undefined;
|
||||
}
|
||||
if (path.length !== 1) return undefined;
|
||||
// A library function the project patches itself (kong's `ngx.sleep`) is the project's —
|
||||
// a test's stand-in (`function ngx.get_phase()` in a spec) only for that test.
|
||||
const patched = context.getNodesByName(path[0]!).filter((n) =>
|
||||
@@ -3822,6 +3848,7 @@ export function matchByExactName(
|
||||
const bareNoMembers = isBareGoCall(ref, context) || isBareRCall(ref, context);
|
||||
const solidityBare = isReceiverLessSolidityCall(ref, context);
|
||||
const barePhp = isBarePhpCall(ref, context);
|
||||
const luaBareCall = (ref.language === 'lua' || ref.language === 'luau') && ref.referenceKind === 'calls' && /^[A-Za-z_]\w*$/.test(ref.referenceName);
|
||||
// A type, a value or an import the file binds from a package outside the
|
||||
// repository names nothing in it, whatever kind of reference it is.
|
||||
if (!bareJs && JS_FAMILY.has(ref.language) && ref.referenceKind !== 'calls' &&
|
||||
@@ -3913,6 +3940,8 @@ export function matchByExactName(
|
||||
!((bareJs || bareNoMembers) && TYPE_MEMBER_KINDS.has(n.kind)) &&
|
||||
// A bare PHP call is a function call: nothing else is callable without a receiver.
|
||||
!(barePhp && n.kind !== 'function') &&
|
||||
// Nor is a table's method (`function M.x`, `function M:x`) in Lua, without its table.
|
||||
!(luaBareCall && n.kind === 'method') &&
|
||||
// A Vue component's own method is `this.m()` inside that component — not
|
||||
// `this.$refs['input'].click()` on an element another component renders.
|
||||
!(ref.referenceKind === 'calls' && JS_FAMILY.has(ref.language) && isVueComponentMethod(n) && !isThisCallInOwnFile(n, ref, context)) &&
|
||||
@@ -7806,6 +7835,10 @@ function matchReferenceInner(
|
||||
if ((ref.language === 'lua' || ref.language === 'luau') && ref.referenceKind === 'calls' && /^[A-Za-z_]\w*$/.test(ref.referenceName)) {
|
||||
const aliased = luaAliasTarget(ref, context);
|
||||
if (aliased !== undefined) return aliased;
|
||||
// `ipairs(t)` is Lua's, unless the file defines its own: telescope's 122
|
||||
// `for _, v in ipairs(…)` went to a linked list's `ipairs` method.
|
||||
if (LUA_GLOBAL_FUNCTIONS.has(ref.referenceName) &&
|
||||
!context.getNodesInFile(ref.filePath).some((n) => n.name === ref.referenceName && n.kind === 'function')) return null;
|
||||
}
|
||||
|
||||
// Erlang `-behaviour(m)` refs target a MODULE. Letting them fall through to
|
||||
|
||||
Reference in New Issue
Block a user