mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-25 03:49:59 +08:00
`new a.b.Batch()` made an instantiates edge, but
`createObject("component", "a.b.Batch")`, the form older CFML uses most,
was extracted as a call to the engine function createObject, which never
resolves. A call chained onto either construction, `createObject(...).init()`
or `new a.b.Batch().init()`, was extracted as a bare `init`, which the
name matcher bound to whichever component's init came first (0.4), or to
none when there are many.
Fix:
- cfscript extraction (tag expressions included): a component
createObject, with two arguments, one, or named ones, makes an
instantiates reference under the dotted path as written, not a call.
Java and other object types, and names built at runtime, are unchanged.
- A call on a construction is extracted as `<component>().<method>`.
- The resolver reads a dotted instantiates path with
resolveCfmlComponentPath, the rules extends uses, and resolves a chained
call to the method of that component or one it inherits (matched
ignoring case; inheritance is wired before calls). A bare component name
is the one in the calling file's folder, else the only one so named.
Without both, the call gets no edge rather than a guess by name.
- resolveCfmlComponentPath also accepts a component at the project root
for a path with one folder segment, its file named like it:
`wheels.JobWorker` in Wheels' own source, where `wheels` maps onto the
root. Without it Wheels' core lost calls like
`new wheels.JobWorker().processNext()` that name matching had found. It
ranks below any folder match; extends gains the same.
Validation:
- New cfml-create-object.test.ts (5 cases) and a root-component case in
cfml-inheritance-resolution.test.ts: 5 of the 6 fail on main (the sixth
pins the variable typing that already worked), all pass.
- A/B vs main d90f6abe on FW/1, BlogCFC, ColdBox, ContentBox, Preside,
CFWheels and its vendor/wheels core indexed as its own project:
1,022 edges removed: 563 resolved again to the same target by type,
444 moved from a method guessed by name to the constructed component's
own or inherited method, and 15 dropped, all name guesses for
components outside the index, without the method, or tied. 1,342 added:
641 instantiates, 38 calls, and in Wheels' core 653 extends and 10
implements to root components. Oracles over the fix graphs: of the 983
chained-construction call edges, 965 land in the file named like the
component and 18 on a method its supertypes declare (each checked); all
608 dotted instantiates and, in Wheels' core, all 786 dotted
extends/implements edges target a file named like the path's last
segment. One site is doubtful: Wheels' core public/docs/core.cfm names
the application's app.models.Model, which that subtree lacks; one
`models` folder corroborates a test asset's Model.cfc, as it would for
extends. express and flask controls byte-identical.
- Index time, median of 4 interleaved runs, CPU with workers, main vs
fix: Preside 58.7 / 54.5 s, Wheels core 32.2 / 33.0 s. Within noise.
Fixes #2652
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
132 lines
5.8 KiB
TypeScript
132 lines
5.8 KiB
TypeScript
/**
|
|
* CFML `createObject("component", "a.b.Batch")` (#2652). `new a.b.Batch()`
|
|
* made an `instantiates` edge; createObject, the form older code uses most,
|
|
* made none — it was a call to an engine function that never resolves. And a
|
|
* call chained onto either construction, `createObject(…).init()` or `new
|
|
* a.b.Batch().init()`, was emitted as a bare `init`, which matched whichever
|
|
* component's `init` came first. A component path resolves as `extends`
|
|
* resolves one: by its folders, from the right.
|
|
*/
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import * as fs from 'node:fs';
|
|
import * as os from 'node:os';
|
|
import * as path from 'node:path';
|
|
import { CodeGraph } from '../src';
|
|
|
|
let dir = '';
|
|
let edges: string[] = [];
|
|
let unresolved: string[] = [];
|
|
|
|
const files: Record<string, string> = {
|
|
// Other components with an `init`, which a guess by name would reach first.
|
|
'contens/aaa/alpha.cfc': `component {\n\tfunction init() { return this; }\n\tfunction $init() { return this; }\n}\n`,
|
|
'contens/core/mixin.cfc': `component {\n\tfunction $init() { return this; }\n}\n`,
|
|
// A framework's own source, where its mapping (`wheels`) is the project root.
|
|
'JobWorker.cfc': `component {\n\tfunction processNext() {}\n}\n`,
|
|
'tests/specs/JobSpec.cfc': `component {
|
|
\tfunction run() {
|
|
\t\tnew wheels.JobWorker().processNext();
|
|
\t\tvar w = createObject("component", "wheels.JobWorker");
|
|
\t}
|
|
}
|
|
`,
|
|
// An app's own Controller is not the framework's `wheels.Controller`.
|
|
'app/controllers/Controller.cfc': `component {\n\tfunction init() { return this; }\n}\n`,
|
|
'app/handlers/Main.cfc': `component {\n\tfunction index() {\n\t\tcreateObject("component", "wheels.Controller").init();\n\t}\n}\n`,
|
|
'contens/aaa/beta.cfc': `component {\n\tfunction init() { return this; }\n}\n`,
|
|
'contens/core/base.cfc': `component {\n\tfunction init() { return this; }\n}\n`,
|
|
'contens/core/queue/batchcontext.cfc': `<cfcomponent>
|
|
\t<cffunction name="init"><cfreturn this></cffunction>
|
|
\t<cffunction name="run"></cffunction>
|
|
</cfcomponent>
|
|
`,
|
|
// Inherits its init.
|
|
'contens/core/queue/jobrunner.cfc': `component extends="contenscms.core.base" {\n\tfunction start() {}\n}\n`,
|
|
'contens/api/batch/collection.cfc': `<cfcomponent>
|
|
\t<cffunction name="process">
|
|
\t\t<cfset var oContext = createObject("component", "contenscms.core.queue.batchcontext").init()>
|
|
\t\t<cfset oContext.run()>
|
|
\t\t<cfset var oRunner = CreateObject('Component', 'contenscms.core.queue.jobrunner')>
|
|
\t</cffunction>
|
|
</cfcomponent>
|
|
`,
|
|
'contens/api/batch/scriptcollection.cfc': `component {
|
|
\tfunction process() {
|
|
\t\tvar oRunner = createObject("contenscms.core.queue.jobrunner").init();
|
|
\t\tvar oNew = new contenscms.core.queue.batchcontext().init();
|
|
\t\tvar oMap = createObject("java", "java.util.HashMap");
|
|
\t\tvar oBox = createObject("component", "testbox.system.TestBox").init();
|
|
\t\tvar oDyn = createObject("component", "#variables.root#.batchcontext");
|
|
\t\tvar oMix = new contenscms.core.mixin().$init();
|
|
\t}
|
|
}
|
|
`,
|
|
};
|
|
|
|
beforeAll(async () => {
|
|
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cfml-create-object-'));
|
|
for (const [rel, body] of Object.entries(files)) {
|
|
fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true });
|
|
fs.writeFileSync(path.join(dir, rel), body);
|
|
}
|
|
const cg = await CodeGraph.init(dir, { silent: true });
|
|
await cg.indexAll();
|
|
const db = (cg as any).db.db;
|
|
edges = db
|
|
.prepare(
|
|
`SELECT e.kind, e.line, s.qualified_name src, t.qualified_name tgt FROM edges e
|
|
JOIN nodes s ON s.id = e.source JOIN nodes t ON t.id = e.target
|
|
WHERE e.kind IN ('instantiates', 'calls')`
|
|
)
|
|
.all()
|
|
.map((r: any) => `${r.src} -${r.kind}-> ${r.tgt} @${r.line}`)
|
|
.sort();
|
|
unresolved = db
|
|
.prepare(`SELECT reference_kind k, reference_name n, line FROM unresolved_refs`)
|
|
.all()
|
|
.map((r: any) => `${r.k} ${r.n} @${r.line}`);
|
|
cg.close?.();
|
|
});
|
|
|
|
afterAll(() => {
|
|
if (dir) fs.rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('CFML createObject (#2652)', () => {
|
|
it('links createObject("component", "a.b.c") to the component it makes, in tags and in script', () => {
|
|
expect(edges).toEqual(expect.arrayContaining([
|
|
'collection::process -instantiates-> contens/core/queue/batchcontext.cfc::batchcontext @3',
|
|
'collection::process -instantiates-> contens/core/queue/jobrunner.cfc::jobrunner @5',
|
|
'scriptcollection::process -instantiates-> contens/core/queue/jobrunner.cfc::jobrunner @3',
|
|
]));
|
|
// Only the Java object and the component named at runtime are left as calls.
|
|
expect(unresolved.filter((r) => r.startsWith('calls createObject')).sort()).toEqual(['calls createObject @5', 'calls createObject @7']);
|
|
});
|
|
|
|
it('links a call chained onto the construction to that component\'s method, or one it inherits', () => {
|
|
expect(edges).toEqual(expect.arrayContaining([
|
|
'collection::process -calls-> batchcontext::init @3',
|
|
'scriptcollection::process -calls-> base::init @3',
|
|
'scriptcollection::process -calls-> batchcontext::init @4',
|
|
'scriptcollection::process -calls-> mixin::$init @8',
|
|
]));
|
|
expect(edges.filter((e) => /-> (alpha|beta)::\$?init/.test(e))).toEqual([]);
|
|
});
|
|
|
|
it('makes nothing of a component outside the project, another type of object or a name built at runtime', () => {
|
|
expect(edges.filter((e) => / @[567]$/.test(e) && e.startsWith('scriptcollection::'))).toEqual([]);
|
|
});
|
|
|
|
it('still types a variable assigned from createObject', () => {
|
|
expect(edges).toContain('collection::process -calls-> batchcontext::run @4');
|
|
});
|
|
|
|
it('finds a component at the project root by a one-folder path, and only there', () => {
|
|
expect(edges).toEqual(expect.arrayContaining([
|
|
'JobSpec::run -calls-> JobWorker::processNext @3',
|
|
'JobSpec::run -instantiates-> JobWorker.cfc::JobWorker @4',
|
|
]));
|
|
expect(edges.filter((e) => e.startsWith('Main::index'))).toEqual([]);
|
|
});
|
|
});
|