Skip to content

feat(resolution): CFML receiver-type inference for locals, typed args, and component properties#1155

Merged
colbymchenry merged 1 commit into
mainfrom
cfml-receiver-inference
Jul 3, 2026
Merged

feat(resolution): CFML receiver-type inference for locals, typed args, and component properties#1155
colbymchenry merged 1 commit into
mainfrom
cfml-receiver-inference

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Completes CFML's parity with the #1108 receiver-type inference family — the last CFML gap from the #1118/#1153/#1154 arc. var svc = new UserService(); svc.save() now links to UserService.save instead of being dropped or guessed among same-named methods.

Two coupled halves

Extraction (prerequisite): CFML method qualifiedNames now carry the component scope (UserService::save — the same Class::member shape every other language produces) in all three extraction paths: bare-script components (the <anonymous> rename now rewrites member scope chains), <cffunction> tags, and component-level <cfscript> blocks. Previously they were <anonymous>::save / file.cfc::save / bare configure — shapes type-validated resolution could never match. Scope-prefixed member calls (variables.svc.save()) also keep their receiver chain instead of collapsing to a bare method name — gated to CFML's file-local scope keywords (variables/this/local/arguments), whose calls previously emitted an ambiguous bare name, so no recall risk.

Resolution: cfml/cfscript declaration patterns for the shared inferrer, covering CFML's actual idioms:

  • var svc = new path.to.UserService() (dotted paths reduce to the class segment)
  • createObject("component", "path.UserService") — two-arg and single-arg, any casing
  • typed cfscript parameters (function save(UserService svc)) and <cfargument name= type=> (either attribute order)
  • property name="svc" type=/inject= — the WireBox DI shape — and <cfproperty>
  • variables./this.-scoped receivers strip their prefix and scan the whole file in both directions (a field's init() pseudoconstructor assignment or property declaration is position-independent); local./arguments. keep the enclosing-function bound

resolveMethodOnType validates every inference (the method must exist on the inferred component), so a mis-inference produces no edge — and its conformance walk rides the #1152 extends edges, so inherited methods resolve too (cmds.env_file_content() on an AppCommands that inherits it from Base links to Base's definition — verified in the wild).

Validation

Repo Typed-inference call edges Net edge growth
FW/1 364 +24
ColdBox 384 +1,166
CFWheels 901 +606

Precision: an independent audit re-derived each receiver's declared type from source for all 1,649 typed-inference edges — 1,649/1,649 consistent (3 initial flags were inherited-method resolutions, hand-verified correct). Node counts unchanged. The pre-existing fuzzy strategies (confidence 0.65–0.8) are untouched; inference outranks them only when a declaration proves the type.

8 new integration tests pin all four declaration idioms, the below-the-call field scan, WireBox injection, component-scoped QNs in all three extraction paths, and the no-edge-on-unvalidated-inference rule. Full suite: 2,006 passed / 0 failed.

🤖 Generated with Claude Code

…, and component properties

Extends the #1108 receiver-type inference family to CFML. Two coupled
halves:

Extraction — CFML method qualifiedNames now carry the component scope
(UserService::save, the generic Class::member shape) in all three
paths: bare-script components (the <anonymous> rename now rewrites
member scope chains too), <cffunction> tags, and component-level
<cfscript> blocks. Without this, type-validated method resolution
could never match a CFML method. Scope-prefixed member calls
(variables.svc.save()) also keep their receiver chain instead of
collapsing to a bare method name — gated to CFML's file-local scope
keywords (variables/this/local/arguments), whose calls previously
emitted an ambiguous bare name.

Resolution — cfml/cfscript declaration patterns for the shared
inferrer: new (dotted component paths included),
createObject("component", ...) two-arg and single-arg, typed cfscript
params, <cfargument> tags (either attribute order), and
(cf)property type=/inject= (WireBox DI). variables./this.-scoped
receivers strip their prefix and scan the whole file in both
directions — a component field's declaration (init pseudoconstructor,
property) is position-independent — while local./arguments. keep the
enclosing-function bound.

Validated on fw1/ColdBox/CFWheels: 1,649 typed-inference call edges
(364/384/901), audit-verified 1,649/1,649 — including methods
inherited through the #1152 extends edges via the conformance walk.
Node counts unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@colbymchenry colbymchenry merged commit 7d624ec into main Jul 3, 2026
1 check passed
@colbymchenry colbymchenry deleted the cfml-receiver-inference branch July 3, 2026 00:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant