Files
codegraph/__tests__/lua-bare-builtins.test.ts
Colby MchenryandClaude Opus 5.5 b55836daa3 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>
2026-09-30 20:46:25 +00:00

89 lines
2.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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');
});
});