Files
codegraph/__tests__/cfml-call-scope.test.ts
Colby Mchenry bd9741432f fix(cfml): a component method reached by name alone must be in reach (#2148)
- A bare call in a .cfc reaches only the component's own methods and those
  of the components it extends (extends="a.b.C" read from source, matched
  to indexed .cfc files by longest path suffix). Calls in .cfm templates and
  multi-line chain links (name not on the ref's line) are not judged.
- The untyped-receiver rule from the Ruby fix now covers CFML too: the one
  project method named `m` needs a receiver named after its owner. The word
  match also accepts the receiver's whole name (`cbsecurity` → CBSecurity)
  and ignores trailing digits (`executor1` → Executor); Ruby is
  byte-identical to main.

A/B (edges removed / added): coldbox -2429/+295, TestBox -255/+31,
fw1 -103/+4, cbsecurity -8/+5, cfdocs 0. Deliberate losses: framework-repo
calls through conventionally named, untyped receivers (ColdBox's `event.*`
→ RequestContext, TestBox's `variables.assert.*` → Assertion).

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

83 lines
2.4 KiB
TypeScript

/**
* CFML types no receiver, so a component method reached by name alone must
* be in reach:
*
* - a bare call inside a component (`.cfc`) is the component's own method or
* one of a component it `extends` — else a built-in: coldbox's `now()` went
* to a date helper's `now` 228 times;
* - `receiver.m()` through an untyped receiver takes the one method named
* `m` only when the receiver is named after its component:
* `server.keyExists(…)` is the struct member function, not an interceptor
* buffer's `keyExists` (168 times).
*/
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-cfml-scope-'));
const files: Record<string, string> = {
'system/core/DateTimeHelper.cfc': `component {
function now() {
return createObject( "java", "java.time.LocalDateTime" ).now();
}
}
`,
'system/core/InterceptorBuffer.cfc': `component {
function keyExists( required key ) {
return false;
}
}
`,
'system/Base.cfc': `component {
function getSetting( required name ) {
return "";
}
}
`,
'system/models/UserService.cfc': `component {
function list() {
return [];
}
}
`,
'system/Handler.cfc': `component extends="app.system.Base" {
function index( event, rc, prc, userService ) {
var stamp = now();
var lucee = server.keyExists( "lucee" );
var name = getSetting( "appName" );
var users = userService.list();
return stamp;
}
}
`,
};
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 });
});
describe('CFML calls reached by name alone', () => {
it('keep the inherited method and the receiver named after its component; drop the built-ins', () => {
const ids = cg.getNodesInFile('system/Handler.cfc').map((n) => n.id);
const targets = cg
.getOutgoingEdgesFrom(ids)
.filter((e) => e.kind === 'calls')
.map((e) => cg.getNode(e.target)!.qualifiedName)
.sort();
expect(targets).toEqual(['Base::getSetting', 'UserService::list']);
});
});