mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-24 19:30:10 +08:00
fix(cfml): <cfinclude template> and cfinclude() link the code to the file they include (#2838)
Only cfscript's `include "x.cfm";`, written outside any function, linked
the including code to the template. CfmlExtractor never read
`<cfinclude template="...">`, which the grammar parses as a
cf_selfclose_tag whose name is only in the text after `<cf`, so the tag
form made nothing anywhere. cfscript's include hook read only the string
form, `cfinclude(template="x.cfm")` went through the call extractor as a
call to `cfinclude`, and the function-body walker never extracts import
statements, so an `include` in a function was dropped. Resolution went
through the generic pipeline: the file-name matcher links a relative path
like `../shared/helpers.cfm` only when no other file shares its name, and
past it the symbol strategies could take another template's import node
of the same path, or the including file itself.
Fix:
- `<cfinclude template="...">` is extracted like the script form: an
import node in the scope it sits in (function, component, or the
template's file) and an imports reference from that scope.
`cfinclude(...)` and `include template="...";` make the same import and
no call, and an `include` in a function body counts, from the function.
- A path built at runtime (`#dir#/x.cfm`) is no import, in every form.
- A CFML include of a template path resolves as CFML does: relative to the
including file's directory, or from the project root for a leading `/`
(less sure, 0.85), to the file at that path, ignoring case when one file
matches. A path that names no file there goes to the file-name matcher,
as before, but only ever to a file, and never to the including file.
Validation:
- New cfml-include.test.ts: the tag form at component level (same folder,
../, upper case, unquoted), in a function body inside <cfif>, in a
template and from the web root; the script forms; runtime-built paths,
cfinclude(template=variables.x) and <cfmodule> (no import); a relative
path with a namesake elsewhere; an unmatched path that another template
also includes, and a path whose only namesake is the includer. 5 of 7
fail on main, all 7 pass.
- A/B vs main 51c4124d (CFML has no kernel path) on FW/1, BlogCFC,
ColdBox, ContentBox, Preside, CFWheels and its vendor/wheels core as a
project of its own: 127 new imports edges and 152 import nodes (36
more resolved again to the same file, more surely); 9 import nodes of
runtime-built paths gone; 1 edge gone, CFWheels' starter-app
views/wheels/wheels.cfm including itself. No edge moved to another
target. Every CFML imports edge in the fix graphs (225) targets a file
named like the include path, and every relative path that stays inside
the project reaches exactly that file but one: Wheels' Public.cfc
including views/routetesterprocess.cfm, no file beside it, matched by
name to public/views/routetesterprocess.cfm as before. express and flask
controls byte-identical.
- Index time, median of 4 interleaved runs, CPU with workers, main vs
fix: Preside 52.6 / 50.8 s, Wheels core 28.2 / 29.5 s. Within noise.
Refs #2651 (part 1; part 2 is a feature request and stays open)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
cf3e6bc525
commit
d90f6abec7
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* CFML includes (#2651). Only cfscript's `include "x.cfm";`, written outside
|
||||
* any function, linked the including code to the template: the tag form
|
||||
* `<cfinclude template="x.cfm">` — the common one in tag-based code — made
|
||||
* nothing anywhere, the function form `cfinclude(template="x.cfm")` became a
|
||||
* call to a function named `cfinclude`, and an `include` inside a function
|
||||
* was dropped. A template path is relative to the including file's directory
|
||||
* (or to the web root with a leading `/`), so `../shared/helpers.cfm` is that
|
||||
* file even when another `helpers.cfm` exists.
|
||||
*/
|
||||
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 imports: string[] = [];
|
||||
let unresolved: string[] = [];
|
||||
let importTargetKinds: string[] = [];
|
||||
|
||||
const files: Record<string, string> = {
|
||||
// Tag form at component scope: same directory, relative, upper case, unquoted.
|
||||
'app/core/baseform.cfc': `<cfcomponent output="false">
|
||||
\t<cffunction name="init"><cfreturn this></cffunction>
|
||||
\t<cfinclude template="formlistshared.cfm">
|
||||
\t<cfinclude template="../shared/helpers.cfm">
|
||||
\t<CFINCLUDE TEMPLATE="Upper.cfm">
|
||||
\t<cfinclude template=bare.cfm>
|
||||
</cfcomponent>
|
||||
`,
|
||||
// Tag form in a function body, nested in control flow; runtime-built paths and <cfmodule> are no includes.
|
||||
'app/core/report.cfc': `<cfcomponent>
|
||||
\t<cffunction name="render">
|
||||
\t\t<cfif arguments.full>
|
||||
\t\t\t<cfinclude template="formlistshared.cfm" />
|
||||
\t\t</cfif>
|
||||
\t\t<cfinclude template="#variables.view#">
|
||||
\t\t<cfinclude template="#variables.dir#/helpers.cfm">
|
||||
\t\t<cfmodule template="formlistshared.cfm">
|
||||
\t</cffunction>
|
||||
</cfcomponent>
|
||||
`,
|
||||
// Tag form in a template, and a path from the web root.
|
||||
'app/page.cfm': `<cfinclude template="core/formlistshared.cfm">
|
||||
<cfif x><cfinclude template="/app/shared/helpers.cfm"></cfif>
|
||||
`,
|
||||
// A template and a function including a path no file has (a server
|
||||
// mapping's): the function may not link to the template's import of it.
|
||||
'app/one.cfm': `<cfinclude template="/mapped/runner.cfm">\n`,
|
||||
'app/core/runtests.cfc': `component {\n\tfunction run() {\n\t\tinclude "/mapped/runner.cfm";\n\t}\n}\n`,
|
||||
// The only file of the name an include names is the includer itself.
|
||||
'app/views/wheels/wheels.cfm': `<cfinclude template="../../wheels/public/wheels.cfm">\n`,
|
||||
// Script forms: in a function body, the attribute form, the function form.
|
||||
'app/core/scriptform.cfc': `component {
|
||||
\tinclude "formlistshared.cfm";
|
||||
\tfunction render() {
|
||||
\t\tinclude "../shared/helpers.cfm";
|
||||
\t\tcfinclude(template="upper.cfm");
|
||||
\t\tcfinclude("bare.cfm");
|
||||
\t\tCFINCLUDE( TEMPLATE = 'formlistshared.cfm' );
|
||||
\t}
|
||||
\tfunction other() {
|
||||
\t\tinclude template="formlistshared.cfm";
|
||||
\t\tcfinclude(template=variables.view);
|
||||
\t\tinclude "#variables.view#";
|
||||
\t\tinclude "#variables.dir#/helpers.cfm";
|
||||
\t}
|
||||
}
|
||||
`,
|
||||
'app/core/formlistshared.cfm': `<cffunction name="checkRightsPartPrepare"></cffunction>\n`,
|
||||
'app/core/Upper.cfm': `<cfset x = 1>\n`,
|
||||
'app/core/bare.cfm': `<cfset x = 1>\n`,
|
||||
'app/shared/helpers.cfm': `<cfset x = 1>\n`,
|
||||
// A namesake the relative path does not name.
|
||||
'other/shared/helpers.cfm': `<cfset x = 1>\n`,
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cfml-include-'));
|
||||
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 s.qualified_name src, t.file_path tgt FROM edges e
|
||||
JOIN nodes s ON s.id = e.source JOIN nodes t ON t.id = e.target
|
||||
WHERE e.kind = 'imports'`
|
||||
)
|
||||
.all()
|
||||
.map((r: any) => `${r.src} -> ${r.tgt}`)
|
||||
.sort();
|
||||
imports = db.prepare(`SELECT qualified_name q FROM nodes WHERE kind = 'import'`).all().map((r: any) => r.q).sort();
|
||||
importTargetKinds = db
|
||||
.prepare(`SELECT DISTINCT t.kind k FROM edges e JOIN nodes t ON t.id = e.target WHERE e.kind = 'imports'`)
|
||||
.all()
|
||||
.map((r: any) => r.k);
|
||||
unresolved = db
|
||||
.prepare(`SELECT reference_kind k, reference_name n FROM unresolved_refs`)
|
||||
.all()
|
||||
.map((r: any) => `${r.k} ${r.n}`);
|
||||
cg.close?.();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (dir) fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('CFML includes (#2651)', () => {
|
||||
it('links <cfinclude template="…"> at component scope to the template, relative to the including file', () => {
|
||||
expect(edges).toEqual(expect.arrayContaining([
|
||||
'app/core/baseform.cfc::baseform -> app/core/formlistshared.cfm',
|
||||
'app/core/baseform.cfc::baseform -> app/shared/helpers.cfm',
|
||||
'app/core/baseform.cfc::baseform -> app/core/Upper.cfm',
|
||||
'app/core/baseform.cfc::baseform -> app/core/bare.cfm',
|
||||
]));
|
||||
expect(imports).toContain('baseform::formlistshared.cfm');
|
||||
});
|
||||
|
||||
it('links a <cfinclude> in a function body from that function', () => {
|
||||
expect(edges).toContain('report::render -> app/core/formlistshared.cfm');
|
||||
expect(imports).toContain('report::render::formlistshared.cfm');
|
||||
});
|
||||
|
||||
it('links a <cfinclude> in a template, including one from the web root', () => {
|
||||
expect(edges).toContain('app/page.cfm -> app/core/formlistshared.cfm');
|
||||
expect(edges).toContain('app/page.cfm -> app/shared/helpers.cfm');
|
||||
});
|
||||
|
||||
it('makes nothing of a template built at runtime or of <cfmodule>', () => {
|
||||
expect(edges.filter((e) => e.startsWith('report::render ->'))).toEqual(['report::render -> app/core/formlistshared.cfm']);
|
||||
expect(imports.filter((q) => q.startsWith('report::'))).toEqual(['report::render::formlistshared.cfm']);
|
||||
expect(edges.filter((e) => e.startsWith('scriptform::other ->'))).toEqual(['scriptform::other -> app/core/formlistshared.cfm']);
|
||||
expect(imports.filter((q) => q.includes('#'))).toEqual([]);
|
||||
});
|
||||
|
||||
it('links the script forms: in a function body, `include template=…` and cfinclude(…)', () => {
|
||||
expect(edges).toEqual(expect.arrayContaining([
|
||||
'scriptform::render -> app/shared/helpers.cfm',
|
||||
'scriptform::render -> app/core/Upper.cfm',
|
||||
'scriptform::render -> app/core/bare.cfm',
|
||||
'scriptform::render -> app/core/formlistshared.cfm',
|
||||
'scriptform::other -> app/core/formlistshared.cfm',
|
||||
]));
|
||||
expect(unresolved.filter((r) => /cfinclude/i.test(r))).toEqual([]);
|
||||
});
|
||||
|
||||
it('never links a relative include to a namesake the path does not name', () => {
|
||||
expect(edges.filter((e) => e.endsWith('other/shared/helpers.cfm'))).toEqual([]);
|
||||
});
|
||||
|
||||
it('links an include to a file or to nothing: never to an import statement, nor to the including file', () => {
|
||||
expect(importTargetKinds).toEqual(['file']);
|
||||
expect(edges.filter((e) => e.startsWith('app/one.cfm') || e.startsWith('runtests::'))).toEqual([]);
|
||||
expect(edges.filter((e) => e.startsWith('app/views/wheels/wheels.cfm'))).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user