mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-25 12:00:02 +08:00
fix(cfml): createObject("component", path) links its component, and .init() on it (#2841)
`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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d90f6abec7
commit
f406fe0064
@@ -43,6 +43,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
- In VB.NET, a field or property initializer like `= Compute()` or `As New List(Of Order)` now links what it calls and creates, and so do a `Custom Event`'s `AddHandler`, `RemoveHandler` and `RaiseEvent` blocks. Re-index VB.NET and C# projects after upgrading.
|
- In VB.NET, a field or property initializer like `= Compute()` or `As New List(Of Order)` now links what it calls and creates, and so do a `Custom Event`'s `AddHandler`, `RemoveHandler` and `RaiseEvent` blocks. Re-index VB.NET and C# projects after upgrading.
|
||||||
- In C#, a field or property initializer like `private readonly ILogger _log = LogManager.GetLogger(typeof(X));` or `public List<Foo> Items { get; } = new();` now links what it calls, creates and reads, including inside a lambda, and a target-typed `new()` there counts as creating the declared type. Before, initializers were skipped, so a method or class used only from one — like a converter created in a static list — looked unused, and a method passed as a value there was credited to the whole class instead of the field or property.
|
- In C#, a field or property initializer like `private readonly ILogger _log = LogManager.GetLogger(typeof(X));` or `public List<Foo> Items { get; } = new();` now links what it calls, creates and reads, including inside a lambda, and a target-typed `new()` there counts as creating the declared type. Before, initializers were skipped, so a method or class used only from one — like a converter created in a static list — looked unused, and a method passed as a value there was credited to the whole class instead of the field or property.
|
||||||
- In C# and VB.NET, a constant no longer stands in for a type with the same name: `new Station { … }` links to class `Station` even when another class declares `private const string Station`, `new Version(…)` no longer links to a `const string Version`, and a field like `static readonly Meter Meter` no longer points at itself. Thanks @EvanYu1980 for the report and @drakeo338. (#2337)
|
- In C# and VB.NET, a constant no longer stands in for a type with the same name: `new Station { … }` links to class `Station` even when another class declares `private const string Station`, `new Version(…)` no longer links to a `const string Version`, and a field like `static readonly Meter Meter` no longer points at itself. Thanks @EvanYu1980 for the report and @drakeo338. (#2337)
|
||||||
|
- In CFML, `createObject("component", "contenscms.core.queue.batchcontext")`, and the one-argument `createObject("contenscms.core.queue.batchcontext")`, now link to the component they create, found by its path the way `extends` finds one, as `new` already did. A call chained onto the new object, like `createObject(…).init()` or `new contenscms.core.queue.batchcontext().init()`, now links to that component's `init`, or to the one it inherits. In a framework's own source, a path like `wheels.JobWorker`, whose first part is the framework itself, now also finds the component at the project root, for `extends` as well. Before, `createObject` showed up as a call to a function that doesn't exist, and the chained `init` was linked to whichever component's `init` came first, or to none. Thanks @HarryMuc for the report. (#2652)
|
||||||
- In VB.NET, a call on a variable, parameter, field or property now reaches a method of the type it is declared with — one that type inherits, or an extension method written for it — and nothing when that type comes from outside your project, instead of any project method that merely shares the name.
|
- In VB.NET, a call on a variable, parameter, field or property now reaches a method of the type it is declared with — one that type inherits, or an extension method written for it — and nothing when that type comes from outside your project, instead of any project method that merely shares the name.
|
||||||
- In VB.NET, a call to one of .NET's own methods such as `Add`, `Contains`, `Clear` or `Dispose` on a value whose type isn't known is no longer linked to a project method of that name, unless the value is named after that method's class.
|
- In VB.NET, a call to one of .NET's own methods such as `Add`, `Contains`, `Clear` or `Dispose` on a value whose type isn't known is no longer linked to a project method of that name, unless the value is named after that method's class.
|
||||||
- In VB.NET, a type name is looked up the way VB.NET does it — through the namespaces around it, the file's and project's `Imports` (aliases included), and the caller's own project — so a class declared in several namespaces or projects no longer draws every call to whichever copy was indexed first, and two candidates nothing tells apart get no link at all.
|
- In VB.NET, a type name is looked up the way VB.NET does it — through the namespaces around it, the file's and project's `Imports` (aliases included), and the caller's own project — so a class declared in several namespaces or projects no longer draws every call to whichever copy was indexed first, and two candidates nothing tells apart get no link at all.
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
/**
|
||||||
|
* 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([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -163,4 +163,20 @@ describe('CFML component-path inheritance resolution (#1152)', () => {
|
|||||||
const edges = await load();
|
const edges = await load();
|
||||||
expect(edges.filter((e) => e.src === 'Caller' || e.src === 'go')).toHaveLength(0);
|
expect(edges.filter((e) => e.src === 'Caller' || e.src === 'go')).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('resolves a one-folder path to a component at the project root (a framework\'s own repo, #2652)', async () => {
|
||||||
|
// Wheels' own source: `wheels` is the mapping onto the root, so
|
||||||
|
// `wheels.Model` is Model.cfc there, with no folder to corroborate.
|
||||||
|
write('Model.cfc', `component {}\n`);
|
||||||
|
write('models/User.cfc', `component extends="wheels.Model" {}\n`);
|
||||||
|
// A longer path names folders the root component isn't in.
|
||||||
|
write('Util.cfc', `component {}\n`);
|
||||||
|
write('models/Helper.cfc', `component extends="coldbox.system.Util" {}\n`);
|
||||||
|
// An app's own Controller sits in folders the path doesn't name.
|
||||||
|
write('app/controllers/Controller.cfc', `component {}\n`);
|
||||||
|
write('app/controllers/Main.cfc', `component extends="wheels.Controller" {}\n`);
|
||||||
|
const edges = await load();
|
||||||
|
expect(has(edges, 'User', 'Model', 'Model.cfc')).toBe(true);
|
||||||
|
expect(edges.filter((e) => e.src === 'Helper' || e.src === 'Main')).toHaveLength(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ function cfmlVisibility(node: SyntaxNode): 'public' | 'private' | 'protected' |
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* A string literal's text when it is written out in full, or null when a
|
* A string literal's text when it is written out in full, or null when a
|
||||||
* `#…#` expression builds it at runtime (`"#dir#/x.cfm"`): such a path names
|
* `#…#` expression builds it at runtime (`"#dir#/x.cfm"`, `"#dir#.Batch"`):
|
||||||
* no file the index can link.
|
* such a path names no file, and such a name no component, the index can
|
||||||
|
* link.
|
||||||
*/
|
*/
|
||||||
function literalString(node: SyntaxNode | null | undefined, source: string): string | null {
|
function literalString(node: SyntaxNode | null | undefined, source: string): string | null {
|
||||||
if (node?.type !== 'string' || node.namedChildren.some((c: SyntaxNode) => c.type !== 'string_fragment')) return null;
|
if (node?.type !== 'string' || node.namedChildren.some((c: SyntaxNode) => c.type !== 'string_fragment')) return null;
|
||||||
@@ -55,6 +56,56 @@ function cfIncludeCallTemplate(node: SyntaxNode, source: string): string | null
|
|||||||
return literalString(args.namedChildren[0], source);
|
return literalString(args.namedChildren[0], source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A component name or dotted component path (`Batch`, `contenscms.core.queue.Batch`). */
|
||||||
|
const COMPONENT_PATH = /^[A-Za-z_][\w$-]*(?:\.[A-Za-z_][\w$-]*)*$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The component `createObject("component", "a.b.Batch")` makes — or the
|
||||||
|
* one-argument `createObject("a.b.Batch")` (Lucee, ACF), or with named
|
||||||
|
* arguments, `createObject(type="component", component="a.b.Batch")` — as
|
||||||
|
* written (#2652). Null for any other call, for another type of object
|
||||||
|
* (`createObject("java", …)`), and for a name built at runtime.
|
||||||
|
*/
|
||||||
|
export function cfmlCreatedComponent(call: SyntaxNode, source: string): string | null {
|
||||||
|
if (call.type !== 'call_expression') return null;
|
||||||
|
const fn = getChildByField(call, 'function');
|
||||||
|
if (fn?.type !== 'identifier' || getNodeText(fn, source).toLowerCase() !== 'createobject') return null;
|
||||||
|
const args = getChildByField(call, 'arguments')?.namedChildren ?? [];
|
||||||
|
let type: string | null | undefined;
|
||||||
|
let name: string | null | undefined;
|
||||||
|
if (args.length > 0 && args.every((a: SyntaxNode) => a.type === 'assignment_expression')) {
|
||||||
|
for (const arg of args) {
|
||||||
|
const key = getNodeText(getChildByField(arg, 'left') ?? arg, source).toLowerCase();
|
||||||
|
if (key === 'type') type = literalString(getChildByField(arg, 'right'), source);
|
||||||
|
else if (key === 'component') name = literalString(getChildByField(arg, 'right'), source);
|
||||||
|
}
|
||||||
|
} else if (args.length === 1) {
|
||||||
|
name = literalString(args[0], source);
|
||||||
|
} else if (args.length >= 2) {
|
||||||
|
type = literalString(args[0], source);
|
||||||
|
name = literalString(args[1], source);
|
||||||
|
}
|
||||||
|
if (type !== undefined && type?.toLowerCase() !== 'component') return null;
|
||||||
|
return name && COMPONENT_PATH.test(name) ? name : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The component an expression constructs, when the expression is a
|
||||||
|
* construction a call is chained onto: `new a.b.Batch()` or a component
|
||||||
|
* `createObject(…)`, parenthesized or not (#2652).
|
||||||
|
*/
|
||||||
|
export function cfmlConstructedComponent(node: SyntaxNode, source: string): string | null {
|
||||||
|
let expr: SyntaxNode | null = node;
|
||||||
|
while (expr?.type === 'parenthesized_expression') expr = expr.namedChild(0);
|
||||||
|
if (!expr) return null;
|
||||||
|
if (expr.type === 'new_expression') {
|
||||||
|
const ctor = getChildByField(expr, 'constructor');
|
||||||
|
const name = ctor ? getNodeText(ctor, source).replace(/\s+/g, '') : '';
|
||||||
|
return COMPONENT_PATH.test(name) ? name : null;
|
||||||
|
}
|
||||||
|
return cfmlCreatedComponent(expr, source);
|
||||||
|
}
|
||||||
|
|
||||||
export const cfscriptExtractor: LanguageExtractor = {
|
export const cfscriptExtractor: LanguageExtractor = {
|
||||||
functionTypes: ['function_declaration', 'function_expression', 'arrow_function'],
|
functionTypes: ['function_declaration', 'function_expression', 'arrow_function'],
|
||||||
classTypes: ['component'],
|
classTypes: ['component'],
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { appendCppMacroMethods } from './languages/cpp-macro-methods';
|
|||||||
import { rustImplItemOwner, rustImplTypeName } from './languages/rust';
|
import { rustImplItemOwner, rustImplTypeName } from './languages/rust';
|
||||||
import { goAliasTypeNames, goEmbeddedTypeName, goInterfaceMethodTypeNames, goStructFieldTypeNames } from './languages/go';
|
import { goAliasTypeNames, goEmbeddedTypeName, goInterfaceMethodTypeNames, goStructFieldTypeNames } from './languages/go';
|
||||||
import { dartMisparsedGenericCall, dartReceiverOf, isDartTypeName, pushDartTypeAliasRefs, pushDartTypeRefs } from './languages/dart';
|
import { dartMisparsedGenericCall, dartReceiverOf, isDartTypeName, pushDartTypeAliasRefs, pushDartTypeRefs } from './languages/dart';
|
||||||
import { isCfIncludeCall } from './languages/cfscript';
|
import { cfmlConstructedComponent, cfmlCreatedComponent, isCfIncludeCall } from './languages/cfscript';
|
||||||
import { LiquidExtractor } from './liquid-extractor';
|
import { LiquidExtractor } from './liquid-extractor';
|
||||||
import { RazorExtractor } from './razor-extractor';
|
import { RazorExtractor } from './razor-extractor';
|
||||||
import { SvelteExtractor } from './svelte-extractor';
|
import { SvelteExtractor } from './svelte-extractor';
|
||||||
@@ -5174,6 +5174,24 @@ export class TreeSitterExtractor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CFML `createObject("component", "a.b.Batch")` makes an instance of the
|
||||||
|
// component, as `new a.b.Batch()` does (#2652): an instantiation of it,
|
||||||
|
// under the path as written, which the resolver reads as `extends`'
|
||||||
|
// component paths. Not a call: createObject is the engine's own function.
|
||||||
|
if (this.language === 'cfscript') {
|
||||||
|
const created = cfmlCreatedComponent(node, this.source);
|
||||||
|
if (created) {
|
||||||
|
this.unresolvedReferences.push({
|
||||||
|
fromNodeId: callerId,
|
||||||
|
referenceName: created,
|
||||||
|
referenceKind: 'instantiates',
|
||||||
|
line: node.startPosition.row + 1,
|
||||||
|
column: node.startPosition.column,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// VB.NET: `foo(args)` is syntactically ambiguous between a call and an
|
// VB.NET: `foo(args)` is syntactically ambiguous between a call and an
|
||||||
// index read, so the grammar parses non-empty parens as
|
// index read, so the grammar parses non-empty parens as
|
||||||
// array_access_expression (field `array`, not `function`) — even Roslyn
|
// array_access_expression (field `array`, not `function`) — even Roslyn
|
||||||
@@ -5956,6 +5974,7 @@ export class TreeSitterExtractor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const SKIP_RECEIVERS = new Set(['self', 'this', 'cls', 'super']);
|
const SKIP_RECEIVERS = new Set(['self', 'this', 'cls', 'super']);
|
||||||
|
let constructed: string | null = null;
|
||||||
if (receiver && (receiver.type === 'identifier' || receiver.type === 'simple_identifier' || receiver.type === 'field_identifier')) {
|
if (receiver && (receiver.type === 'identifier' || receiver.type === 'simple_identifier' || receiver.type === 'field_identifier')) {
|
||||||
const receiverName = getNodeText(receiver, this.source);
|
const receiverName = getNodeText(receiver, this.source);
|
||||||
if (!SKIP_RECEIVERS.has(receiverName)) {
|
if (!SKIP_RECEIVERS.has(receiverName)) {
|
||||||
@@ -6079,6 +6098,14 @@ export class TreeSitterExtractor {
|
|||||||
// (`!Settings.Defaults.isOn()`). Mirrored in the kernel's
|
// (`!Settings.Defaults.isOn()`). Mirrored in the kernel's
|
||||||
// extract_call (swift.rs).
|
// extract_call (swift.rs).
|
||||||
calleeName = `${swiftUnwrappedText(receiver, this.source).replace(/\s+/g, '')}.${methodName}`;
|
calleeName = `${swiftUnwrappedText(receiver, this.source).replace(/\s+/g, '')}.${methodName}`;
|
||||||
|
} else if (this.language === 'cfscript' && receiver && (constructed = cfmlConstructedComponent(receiver, this.source))) {
|
||||||
|
// CFML call on the component the receiver constructs —
|
||||||
|
// `createObject("component", "a.b.Batch").init()`, `new
|
||||||
|
// a.b.Batch().init()` (#2652). Encode `<component>().<method>`:
|
||||||
|
// the resolver finds the component as `new`/`extends` do and the
|
||||||
|
// method on it, or nothing. The bare `init` this used to emit
|
||||||
|
// matched whichever component's `init` came first.
|
||||||
|
calleeName = `${constructed}().${methodName}`;
|
||||||
} else if (
|
} else if (
|
||||||
this.language === 'cfscript' &&
|
this.language === 'cfscript' &&
|
||||||
receiver &&
|
receiver &&
|
||||||
|
|||||||
+75
-1
@@ -97,6 +97,9 @@ const CHAIN_SHAPE = /^(.+)\(\)\.(\w+)$/;
|
|||||||
/** PHP `$this->prop->method()` encoded as `this->prop.method` — no `()`, so CHAIN_SHAPE misses it. */
|
/** PHP `$this->prop->method()` encoded as `this->prop.method` — no `()`, so CHAIN_SHAPE misses it. */
|
||||||
const PHP_PROP_SHAPE = /^this->\w+\.\w+$/;
|
const PHP_PROP_SHAPE = /^this->\w+\.\w+$/;
|
||||||
|
|
||||||
|
/** A CFML call on a construction, `a.b.Batch().init` (#2652); CFML names may hold `$` (`$init`). */
|
||||||
|
const CFML_CONSTRUCTED_CALL = /^([A-Za-z_$][\w$.-]*)\(\)\.([A-Za-z_$][\w$]*)$/;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How many files that import a changed module by default a sync reads the
|
* How many files that import a changed module by default a sync reads the
|
||||||
* edges of, when the module forwards its default from another file, to see
|
* edges of, when the module forwards its default from another file, to see
|
||||||
@@ -1567,6 +1570,15 @@ export class ReferenceResolver {
|
|||||||
) {
|
) {
|
||||||
return this.resolveCfmlComponentPath(ref);
|
return this.resolveCfmlComponentPath(ref);
|
||||||
}
|
}
|
||||||
|
// The component a CFML `createObject("component", "a.b.Batch")` makes,
|
||||||
|
// named by its component path as `extends` names one (#2652), and a call
|
||||||
|
// chained onto a construction — `createObject(…).init()`, `new
|
||||||
|
// a.b.Batch().init()`, encoded `a.b.Batch().init` — which is a method of
|
||||||
|
// that component or of nothing: no name match may guess past it.
|
||||||
|
if (ref.language === 'cfml' || ref.language === 'cfscript') {
|
||||||
|
if (ref.referenceKind === 'instantiates' && ref.referenceName.includes('.')) return this.resolveCfmlComponentPath(ref);
|
||||||
|
if (ref.referenceKind === 'calls' && CFML_CONSTRUCTED_CALL.test(ref.referenceName)) return this.resolveCfmlConstructedCall(ref);
|
||||||
|
}
|
||||||
|
|
||||||
// A CFML include names a template by its path from the including file's
|
// A CFML include names a template by its path from the including file's
|
||||||
// directory, or from the web root (#2651): `<cfinclude template="../shared/
|
// directory, or from the web root (#2651): `<cfinclude template="../shared/
|
||||||
@@ -3519,6 +3531,12 @@ export class ReferenceResolver {
|
|||||||
* TestBox spec mirroring the component's path (`tests/specs/unit/core/
|
* TestBox spec mirroring the component's path (`tests/specs/unit/core/
|
||||||
* basecore.cfc` beside `core/basecore.cfc`) corroborates the same segments,
|
* basecore.cfc` beside `core/basecore.cfc`) corroborates the same segments,
|
||||||
* and a mapping root like `contenscms` (onto `contens/`) matches neither.
|
* and a mapping root like `contenscms` (onto `contens/`) matches neither.
|
||||||
|
*
|
||||||
|
* The one exception to corroboration is a path with a single folder segment
|
||||||
|
* and a component at the project root (#2652): `wheels.JobWorker` in Wheels'
|
||||||
|
* own source, where `wheels` is the mapping onto the root, has no folder of
|
||||||
|
* the file's to corroborate. It ranks below any candidate a folder
|
||||||
|
* corroborates, and the file must be named like the component.
|
||||||
*/
|
*/
|
||||||
private resolveCfmlComponentPath(ref: UnresolvedRef): ResolvedRef | null {
|
private resolveCfmlComponentPath(ref: UnresolvedRef): ResolvedRef | null {
|
||||||
const cfmlCandidates = (name: string): Node[] =>
|
const cfmlCandidates = (name: string): Node[] =>
|
||||||
@@ -3582,6 +3600,12 @@ export class ReferenceResolver {
|
|||||||
) {
|
) {
|
||||||
score++;
|
score++;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
score === 0 && dirs.length === 0 && dirSegments.length === 1 &&
|
||||||
|
cand.filePath.replace(/\.cfc$/i, '').toLowerCase() === className.toLowerCase()
|
||||||
|
) {
|
||||||
|
score = 0.5; // a root component, named by the mapping onto the root
|
||||||
|
}
|
||||||
if (score > bestScore) {
|
if (score > bestScore) {
|
||||||
bestScore = score;
|
bestScore = score;
|
||||||
tied = [cand];
|
tied = [cand];
|
||||||
@@ -3593,7 +3617,57 @@ export class ReferenceResolver {
|
|||||||
// path names the component, as it does from production code.
|
// path names the component, as it does from production code.
|
||||||
if (tied.length > 1) tied = tied.filter((c) => !isTestSuitePath(c.filePath));
|
if (tied.length > 1) tied = tied.filter((c) => !isTestSuitePath(c.filePath));
|
||||||
if (bestScore === 0 || tied.length !== 1) return null;
|
if (bestScore === 0 || tied.length !== 1) return null;
|
||||||
return { original: ref, targetNodeId: tied[0]!.id, confidence: 0.9, resolvedBy: 'qualified-name' };
|
return { original: ref, targetNodeId: tied[0]!.id, confidence: bestScore >= 1 ? 0.9 : 0.8, resolvedBy: 'qualified-name' };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A CFML call chained onto a construction (#2652): `createObject("component",
|
||||||
|
* "a.b.Batch").init()` or `new a.b.Batch().init()`, encoded
|
||||||
|
* `a.b.Batch().init`. The component is the one its path names, by
|
||||||
|
* resolveCfmlComponentPath's rules, or for a bare name the one beside the
|
||||||
|
* calling file (where CFML looks first), else the only one of that name. The
|
||||||
|
* method is the component's own or one it inherits — inheritance is wired
|
||||||
|
* before calls are resolved. Null when either is missing: the bare `init`
|
||||||
|
* such a call used to emit matched whichever component's `init` came first.
|
||||||
|
*/
|
||||||
|
private resolveCfmlConstructedCall(ref: UnresolvedRef): ResolvedRef | null {
|
||||||
|
const match = CFML_CONSTRUCTED_CALL.exec(ref.referenceName);
|
||||||
|
if (!match) return null;
|
||||||
|
const component = match[1]!;
|
||||||
|
const cls = component.includes('.')
|
||||||
|
? this.nodeById(this.resolveCfmlComponentPath({ ...ref, referenceName: component, referenceKind: 'instantiates' })?.targetNodeId ?? '')
|
||||||
|
: this.cfmlComponentByName(component, ref.filePath);
|
||||||
|
const method = cls ? this.cfmlMethodOf(cls, match[2]!.toLowerCase(), new Set()) : undefined;
|
||||||
|
return method ? { original: ref, targetNodeId: method.id, confidence: 0.9, resolvedBy: 'instance-method' } : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The CFML component a bare name reaches from `fromFile`: the one in its directory, else the only one so named. */
|
||||||
|
private cfmlComponentByName(name: string, fromFile: string): Node | undefined {
|
||||||
|
const components = this.context
|
||||||
|
.getNodesByLowerName(name.toLowerCase())
|
||||||
|
.filter((n) => (n.kind === 'class' || n.kind === 'interface') && (n.language === 'cfml' || n.language === 'cfscript'));
|
||||||
|
const dirOf = (p: string): string => p.slice(0, p.lastIndexOf('/') + 1);
|
||||||
|
const beside = components.filter((n) => dirOf(n.filePath) === dirOf(fromFile));
|
||||||
|
if (beside.length === 1) return beside[0];
|
||||||
|
return components.length === 1 ? components[0] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A CFML component's method by its lowercased name (CFML names ignore case): its own, else one a supertype has. */
|
||||||
|
private cfmlMethodOf(cls: Node, lowerName: string, seen: Set<string>): Node | undefined {
|
||||||
|
if (seen.has(cls.id) || seen.size >= 8) return undefined;
|
||||||
|
seen.add(cls.id);
|
||||||
|
const own = this.context.getNodesInFile(cls.filePath).find((n) =>
|
||||||
|
n.kind === 'method' &&
|
||||||
|
n.name.toLowerCase() === lowerName &&
|
||||||
|
n.qualifiedName.toLowerCase() === `${cls.name}::${n.name}`.toLowerCase() &&
|
||||||
|
n.startLine >= cls.startLine && n.endLine <= cls.endLine
|
||||||
|
);
|
||||||
|
if (own) return own;
|
||||||
|
for (const supertype of this.context.getSupertypeNodes?.(cls.id) ?? []) {
|
||||||
|
const inherited = this.cfmlMethodOf(supertype, lowerName, seen);
|
||||||
|
if (inherited) return inherited;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user