mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-14 19:41:47 +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
@@ -1568,6 +1568,21 @@ export class ReferenceResolver {
|
||||
return this.resolveCfmlComponentPath(ref);
|
||||
}
|
||||
|
||||
// A CFML include names a template by its path from the including file's
|
||||
// directory, or from the web root (#2651): `<cfinclude template="../shared/
|
||||
// helpers.cfm">` is that file, not whichever `helpers.cfm` file-name
|
||||
// matching would settle on. A path that names no file here (one through a
|
||||
// server mapping, `/contenscms/…`) is matched by its file name, as before,
|
||||
// but only to a file: the symbol strategies below took a path no file
|
||||
// matched for another template's import statement of the same path. Nor
|
||||
// is it the including file, which a file-name match can land on (Wheels'
|
||||
// `views/wheels/wheels.cfm` including `../../wheels/public/wheels.cfm`).
|
||||
if ((ref.language === 'cfml' || ref.language === 'cfscript') && ref.referenceKind === 'imports' && isCfmlTemplatePath(ref.referenceName)) {
|
||||
const included = this.resolveCfmlIncludePath(ref) ?? matchByFilePath(ref, this.context);
|
||||
if (included && this.nodeById(included.targetNodeId)?.filePath === ref.filePath) return null;
|
||||
return this.gateLanguage(included, ref);
|
||||
}
|
||||
|
||||
// A PHP class written with a namespace in it — `new Alias\X()` through a
|
||||
// `use Ns as Alias;` namespace alias, `extends Sub\Base`, `\Ns\X::make()`
|
||||
// (#2256). The pre-filter below would drop most of these before any import
|
||||
@@ -3581,6 +3596,41 @@ export class ReferenceResolver {
|
||||
return { original: ref, targetNodeId: tied[0]!.id, confidence: 0.9, resolvedBy: 'qualified-name' };
|
||||
}
|
||||
|
||||
/**
|
||||
* The file a CFML include names (#2651) — `include "x.cfm";`,
|
||||
* `<cfinclude template="x.cfm">`, `cfinclude(template="x.cfm")`. A template
|
||||
* path is relative to the directory of the file that includes it
|
||||
* (`formlistshared.cfm`, `../shared/helpers.cfm`), or to the web root when
|
||||
* it starts with `/`: the project root, when the project is the web root —
|
||||
* an assumption, so such a match is less sure. Null when no indexed file is
|
||||
* there. CFML code written on a case-blind file system often spells a path
|
||||
* in another case, so a single file that matches ignoring case counts too.
|
||||
*/
|
||||
private resolveCfmlIncludePath(ref: UnresolvedRef): ResolvedRef | null {
|
||||
const written = ref.referenceName.trim().replace(/\\/g, '/');
|
||||
const fromRoot = written.startsWith('/');
|
||||
const parts = fromRoot ? [] : ref.filePath.replace(/\\/g, '/').split('/').slice(0, -1);
|
||||
for (const seg of written.split('/')) {
|
||||
if (seg === '' || seg === '.') continue;
|
||||
if (seg === '..') {
|
||||
if (parts.length === 0) return null; // above the project root
|
||||
parts.pop();
|
||||
} else {
|
||||
parts.push(seg);
|
||||
}
|
||||
}
|
||||
const fileName = parts[parts.length - 1];
|
||||
if (!fileName) return null;
|
||||
const wanted = parts.join('/');
|
||||
const files = this.context.getNodesByLowerName(fileName.toLowerCase()).filter((n) => n.kind === 'file');
|
||||
const exact = files.find((n) => n.filePath === wanted);
|
||||
if (exact) return { original: ref, targetNodeId: exact.id, confidence: fromRoot ? 0.85 : 0.95, resolvedBy: 'file-path' };
|
||||
const folded = files.filter((n) => n.filePath.toLowerCase() === wanted.toLowerCase());
|
||||
return folded.length === 1
|
||||
? { original: ref, targetNodeId: folded[0]!.id, confidence: fromRoot ? 0.8 : 0.9, resolvedBy: 'file-path' }
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a `this.<member>` function-as-value reference (#756/#808) to the
|
||||
* ENCLOSING CLASS's own member — never a same-named symbol elsewhere. The
|
||||
@@ -4267,6 +4317,15 @@ export class ReferenceResolver {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a CFML `imports` name is the path of a template an include names
|
||||
* (`formlistshared.cfm`, `../shared/helpers.cfm`, `/app/x.cfm`), not a
|
||||
* cfscript `import com.foo.Bar;` or `import "java:…";` (#2651).
|
||||
*/
|
||||
function isCfmlTemplatePath(name: string): boolean {
|
||||
return !name.includes(':') && (name.includes('/') || name.includes('\\') || /\.(?:cfml?|cfc|cfs)$/i.test(name.trim()));
|
||||
}
|
||||
|
||||
/** The project-relative directory holding `p` ('' for the project root). */
|
||||
function goDirOf(p: string): string {
|
||||
const dir = path.posix.dirname(p.replace(/\\/g, '/'));
|
||||
|
||||
Reference in New Issue
Block a user