Files
codegraph/__tests__/lua-local-alias-calls.test.ts
Colby Mchenry 60f1db03c8 fix(lua): follow a local alias to the function it names (#2188)
Kong (and OpenResty code generally) localizes everything it calls:
`local splitn = require("kong.tools.string").splitn`, `local check_phase =
phase_checker.check`, `local fmt = string.format`, `local type = type`. Every
bare call then resolved to the same-file alias variable - 8,000 edges on kong,
more than its calls into functions - a dead end for the real callee.

A bare Lua call whose name is a `local` alias in scope is now followed:
- `require "m"` (or a require-named loader like kong's `reload_module`) plus
  members, or a member of such a local, goes to the module's function of that
  name, or to what the module exports under it (`return { check = check_phase }`,
  `unindent = misc.unindent`, up to three hops);
- an alias of a Lua global or standard-library table is no edge, unless the
  project patches that function itself outside its tests (kong's `ngx.sleep`);
- a module outside the repo is no edge;
- anything else resolves as before.
A `local` reached this way is visible across files.

kong: -7057 +1150 (every loss is a std/outside alias or a reroute), indexing
4.3s -> 3.3s. apisix -3507 +170, lua-resty-core -967 +231, telescope -28 +17
(all reroutes or outside modules), lazy.nvim reroute only, roact unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:34:52 +00:00

116 lines
3.2 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 through a `local` alias reaches what the alias names. Kong
* localizes everything it calls — `local splitn = require("kong.tools.string").splitn`,
* `local check_phase = phase_checker.check`, `local fmt = string.format`,
* `local type = type` — so its calls stopped at a same-file variable: 8,000 of
* them, most for the standard library. A module member is followed to its
* function, including one the module hands out under another name
* (`return { check = check_phase }`) or re-exports from a module of its own
* (`unindent = misc.unindent`); the standard library and outside modules are
* no edge at all.
*/
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-alias-'));
const files: Record<string, string> = {
'kong/tools/string.lua': `local _M = {}
function _M.splitn(value)
return value
end
return _M
`,
'kong/pdk/phases.lua': `local function check_phase(accepted)
return accepted
end
return {
check = check_phase,
}
`,
'kong/handler.lua': `local type = type
local fmt = string.format
local splitn = require("kong.tools.string").splitn
local phase_checker = require "kong.pdk.phases"
local check_phase = phase_checker.check
local cjson_encode = require("cjson").encode
local function run(v)
check_phase("access")
if type(v) ~= "string" then
error(fmt("bad %s", v))
end
return splitn(v), cjson_encode(v)
end
return { run = run }
`,
'spec/internal/misc.lua': `local function unindent(s)
return s
end
return { unindent = unindent }
`,
'spec/helpers.lua': `local misc = require("spec.internal.misc")
return {
unindent = misc.unindent,
}
`,
'spec/handler_spec.lua': `local helpers = require "spec.helpers"
local unindent = helpers.unindent
describe("handler", function()
it("runs", function()
unindent("a")
end)
end)
`,
};
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 });
});
/** `file: qualified name` of every call edge out of a file. */
function callsFrom(file: string): string[] {
const ids = cg.getNodesInFile(file).map((n) => n.id);
return cg
.getOutgoingEdgesFrom(ids)
.filter((e) => e.kind === 'calls')
.map((e) => {
const t = cg.getNode(e.target)!;
return `${t.filePath}: ${t.qualifiedName}`;
})
.sort();
}
describe('bare Lua calls through a local alias', () => {
it('reach the module function the alias names, and nothing for the standard library', () => {
expect(callsFrom('kong/handler.lua')).toEqual([
'kong/pdk/phases.lua: check_phase',
'kong/tools/string.lua: _M::splitn',
]);
});
it('follow a module that re-exports another module’s member', () => {
expect(callsFrom('spec/handler_spec.lua')).toEqual(['spec/internal/misc.lua: unindent']);
});
});