1
0

cfml-receiver-inference.test.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. /**
  2. * CFML local-variable / component-field receiver-type inference (#1108 family).
  3. *
  4. * `var svc = new UserService(); svc.save()` — the call's receiver type is
  5. * recoverable from its declaration, and resolveMethodOnType validates the
  6. * inferred type actually declares the method, so a mis-inference produces no
  7. * edge. CFML brings four declaration idioms the shared inferrer must know:
  8. * `new` (dotted component paths included), `createObject("component", "...")`,
  9. * typed arguments (cfscript params and `<cfargument>` tags), and component
  10. * properties — including WireBox DI (`property name="svc" inject="..."`),
  11. * whose receivers are `variables.`-scoped fields declared OUTSIDE the calling
  12. * function (so the scan must widen to the whole file, in both directions).
  13. *
  14. * These tests also pin the extraction prerequisite: CFML method
  15. * qualifiedNames carry the component scope (`UserService::save`) in all three
  16. * extraction paths (bare-script, `<cffunction>`, component-level `<cfscript>`
  17. * blocks) — without that, type-validated resolution can never match.
  18. */
  19. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  20. import * as fs from 'node:fs';
  21. import * as path from 'node:path';
  22. import * as os from 'node:os';
  23. import { CodeGraph } from '../src';
  24. describe('CFML receiver-type inference', () => {
  25. let dir: string;
  26. beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cfml-recv-')); });
  27. afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); });
  28. const write = (rel: string, body: string) => {
  29. const p = path.join(dir, rel);
  30. fs.mkdirSync(path.dirname(p), { recursive: true });
  31. fs.writeFileSync(p, body);
  32. };
  33. const load = async () => {
  34. const cg = await CodeGraph.init(dir, { silent: true });
  35. await cg.indexAll();
  36. const db = (cg as any).db.db;
  37. const calls: { src: string; tgt: string; tgtQn: string }[] = db
  38. .prepare(
  39. `SELECT s.name src, t.name tgt, t.qualified_name tgtQn
  40. FROM edges e JOIN nodes s ON s.id = e.source JOIN nodes t ON t.id = e.target
  41. WHERE e.kind = 'calls' AND t.kind = 'method'`
  42. )
  43. .all();
  44. const methods: { name: string; qn: string }[] = db
  45. .prepare(`SELECT name, qualified_name qn FROM nodes WHERE kind = 'method'`)
  46. .all();
  47. cg.close?.();
  48. return { calls, methods };
  49. };
  50. const hasCall = (calls: any[], src: string, tgtQn: string) =>
  51. calls.some((e) => e.src === src && e.tgtQn === tgtQn);
  52. // Two same-named methods so resolution MUST disambiguate by receiver type —
  53. // plain name-matching alone can't pick one.
  54. const userService = `component {\n function save(any u) { return u; }\n}\n`;
  55. const orderService = `component {\n function save(any o) { return o; }\n}\n`;
  56. it('scopes method qualifiedNames under the component in all three extraction paths', async () => {
  57. write('svc/UserService.cfc', userService);
  58. write('tag/TagService.cfc', `<cfcomponent>\n<cffunction name="save"><cfreturn 1></cffunction>\n</cfcomponent>\n`);
  59. write('mod/ModuleConfig.cfc', `<cfcomponent>\n<cfscript>\nfunction configure() { return 1; }\n</cfscript>\n</cfcomponent>\n`);
  60. const { methods } = await load();
  61. expect(methods.find((m) => m.name === 'save' && m.qn === 'UserService::save')).toBeDefined();
  62. expect(methods.find((m) => m.name === 'save' && m.qn === 'TagService::save')).toBeDefined();
  63. expect(methods.find((m) => m.name === 'configure' && m.qn === 'ModuleConfig::configure')).toBeDefined();
  64. });
  65. it('infers a local declared with new, including a dotted component path', async () => {
  66. write('svc/UserService.cfc', userService);
  67. write('svc/OrderService.cfc', orderService);
  68. write('handlers/Main.cfc', `component {
  69. function bare() {
  70. var svc = new UserService();
  71. return svc.save(1);
  72. }
  73. function dotted() {
  74. var svc2 = new svc.UserService();
  75. return svc2.save(2);
  76. }
  77. }
  78. `);
  79. const { calls } = await load();
  80. expect(hasCall(calls, 'bare', 'UserService::save')).toBe(true);
  81. expect(hasCall(calls, 'dotted', 'UserService::save')).toBe(true);
  82. expect(hasCall(calls, 'bare', 'OrderService::save')).toBe(false);
  83. });
  84. it('infers a local declared with createObject (two-arg and single-arg forms)', async () => {
  85. write('svc/UserService.cfc', userService);
  86. write('svc/OrderService.cfc', orderService);
  87. write('handlers/Legacy.cfc', `component {
  88. function classic() {
  89. var svc = createObject("component", "svc.UserService");
  90. return svc.save(1);
  91. }
  92. function modern() {
  93. var svc2 = CreateObject("svc.OrderService");
  94. return svc2.save(2);
  95. }
  96. }
  97. `);
  98. const { calls } = await load();
  99. expect(hasCall(calls, 'classic', 'UserService::save')).toBe(true);
  100. expect(hasCall(calls, 'modern', 'OrderService::save')).toBe(true);
  101. });
  102. it('infers a typed cfscript parameter', async () => {
  103. write('svc/UserService.cfc', userService);
  104. write('svc/OrderService.cfc', orderService);
  105. write('handlers/Typed.cfc', `component {
  106. function process(required UserService svc) {
  107. return svc.save(1);
  108. }
  109. }
  110. `);
  111. const { calls } = await load();
  112. expect(hasCall(calls, 'process', 'UserService::save')).toBe(true);
  113. expect(hasCall(calls, 'process', 'OrderService::save')).toBe(false);
  114. });
  115. it('infers a <cfargument> typed argument used inside a <cfscript> body', async () => {
  116. write('svc/UserService.cfc', userService);
  117. write('svc/OrderService.cfc', orderService);
  118. write('handlers/TagTyped.cfc', `<cfcomponent>
  119. <cffunction name="process">
  120. <cfargument name="svc" type="svc.UserService">
  121. <cfscript>
  122. return svc.save(1);
  123. </cfscript>
  124. </cffunction>
  125. </cfcomponent>
  126. `);
  127. const { calls } = await load();
  128. expect(hasCall(calls, 'process', 'UserService::save')).toBe(true);
  129. });
  130. it('infers a variables-scoped field from its pseudoconstructor assignment, even when init sits below the call', async () => {
  131. write('svc/UserService.cfc', userService);
  132. write('svc/OrderService.cfc', orderService);
  133. write('handlers/Fielded.cfc', `component {
  134. function handle() {
  135. return variables.svc.save(1);
  136. }
  137. function init() {
  138. variables.svc = new UserService();
  139. return this;
  140. }
  141. }
  142. `);
  143. const { calls } = await load();
  144. expect(hasCall(calls, 'handle', 'UserService::save')).toBe(true);
  145. expect(hasCall(calls, 'handle', 'OrderService::save')).toBe(false);
  146. });
  147. it('infers a WireBox-injected property (the ColdBox DI shape)', async () => {
  148. write('svc/UserService.cfc', userService);
  149. write('svc/OrderService.cfc', orderService);
  150. write('handlers/Injected.cfc', `component {
  151. property name="svc" inject="UserService";
  152. function handle() {
  153. return variables.svc.save(1);
  154. }
  155. }
  156. `);
  157. const { calls } = await load();
  158. expect(hasCall(calls, 'handle', 'UserService::save')).toBe(true);
  159. });
  160. it('creates no method edge when the inferred type does not declare the method', async () => {
  161. write('svc/UserService.cfc', userService);
  162. write('handlers/Wrong.cfc', `component {
  163. function go() {
  164. var svc = new UserService();
  165. return svc.destroyEverything();
  166. }
  167. }
  168. `);
  169. const { calls } = await load();
  170. expect(calls.filter((e) => e.src === 'go')).toHaveLength(0);
  171. });
  172. });