cfml-inheritance-resolution.test.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. /**
  2. * CFML dotted / relative component-path inheritance resolution (#1152).
  3. *
  4. * CFML names a supertype by its component path, not a bare class name:
  5. * `extends="coldbox.system.web.Controller"` (dots = directories from the
  6. * webroot or a CFML mapping) or `extends="../base"` (FW/1's relative style).
  7. * The graph indexes the class under its final segment only, so before #1152
  8. * these references never resolved — measured on ColdBox core, 49 of 52
  9. * extends declarations were dotted and only 3 inheritance edges existed.
  10. *
  11. * These tests pin the matcher's precision rules: the mapping-root prefix may
  12. * be absent from the repo (`coldbox.` IS the repo root in the coldbox repo),
  13. * directory comparison is case-insensitive, a candidate needs at least one
  14. * corroborating parent directory (an uncorroborated same-named class is
  15. * almost always an out-of-repo library supertype — mxunit/testbox), a
  16. * corroboration tie yields no edge, and dotted `calls` refs (member-access
  17. * chains) are never treated as component paths.
  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 component-path inheritance resolution (#1152)', () => {
  25. let dir: string;
  26. beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cfml-inh-')); });
  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 edges: { src: string; srcFile: string; tgt: string; tgtFile: string; kind: string }[] = db
  38. .prepare(
  39. `SELECT s.name src, s.file_path srcFile, t.name tgt, t.file_path tgtFile, e.kind kind
  40. FROM edges e JOIN nodes s ON s.id = e.source JOIN nodes t ON t.id = e.target
  41. WHERE e.kind IN ('extends', 'implements')`
  42. )
  43. .all();
  44. cg.close?.();
  45. return edges;
  46. };
  47. const has = (edges: any[], src: string, tgt: string, tgtFile: string, kind = 'extends') =>
  48. edges.some((e) => e.src === src && e.tgt === tgt && e.tgtFile === tgtFile && e.kind === kind);
  49. it('resolves a dotted path whose mapping root is absent from the repo (the ColdBox shape)', async () => {
  50. write('system/web/Controller.cfc', `component {\n function handle() { return 1; }\n}\n`);
  51. write('handlers/Main.cfc', `component extends="coldbox.system.web.Controller" {\n function index() { return 1; }\n}\n`);
  52. const edges = await load();
  53. expect(has(edges, 'Main', 'Controller', 'system/web/Controller.cfc')).toBe(true);
  54. });
  55. it('disambiguates same-named classes by directory corroboration', async () => {
  56. write('system/web/Controller.cfc', `component {}\n`);
  57. write('other/Controller.cfc', `component {}\n`);
  58. write('handlers/Main.cfc', `component extends="coldbox.system.web.Controller" {}\n`);
  59. const edges = await load();
  60. expect(has(edges, 'Main', 'Controller', 'system/web/Controller.cfc')).toBe(true);
  61. expect(has(edges, 'Main', 'Controller', 'other/Controller.cfc')).toBe(false);
  62. });
  63. it('compares directories case-insensitively (CFML path resolution is)', async () => {
  64. write('system/web/Controller.cfc', `component {}\n`);
  65. write('handlers/Main.cfc', `component extends="COLDBOX.System.Web.Controller" {}\n`);
  66. const edges = await load();
  67. expect(has(edges, 'Main', 'Controller', 'system/web/Controller.cfc')).toBe(true);
  68. });
  69. it('creates no edge when the only same-named class has no corroborating directory (out-of-repo supertype)', async () => {
  70. // `mxunit.framework.TestCase` is an external library; the repo's own
  71. // unrelated TestCase must NOT be claimed as the supertype.
  72. write('lib/TestCase.cfc', `component {}\n`);
  73. write('tests/MyTest.cfc', `component extends="mxunit.framework.TestCase" {}\n`);
  74. const edges = await load();
  75. expect(edges.filter((e) => e.src === 'MyTest')).toHaveLength(0);
  76. });
  77. it('creates no edge on a corroboration tie', async () => {
  78. write('a/models/User.cfc', `component {}\n`);
  79. write('b/models/User.cfc', `component {}\n`);
  80. write('handlers/Main.cfc', `component extends="models.User" {}\n`);
  81. const edges = await load();
  82. expect(edges.filter((e) => e.src === 'Main')).toHaveLength(0);
  83. });
  84. it('resolves a relative path against the referencing file (the FW/1 shape)', async () => {
  85. write('examples/base.cfc', `component {\n function shared() { return 1; }\n}\n`);
  86. write('examples/sub/app.cfc', `component extends="../base" {}\n`);
  87. write('examples/sub/sibling.cfc', `component extends="./app" {}\n`);
  88. const edges = await load();
  89. expect(has(edges, 'app', 'base', 'examples/base.cfc')).toBe(true);
  90. expect(has(edges, 'sibling', 'app', 'examples/sub/app.cfc')).toBe(true);
  91. });
  92. it('resolves dotted implements to an interface as an implements edge', async () => {
  93. write('app/interfaces/IService.cfc', `interface {\n public string function getName();\n}\n`);
  94. write('app/services/Greeter.cfc', `component implements="app.interfaces.IService" {\n public string function getName() { return "hi"; }\n}\n`);
  95. const edges = await load();
  96. expect(has(edges, 'Greeter', 'IService', 'app/interfaces/IService.cfc', 'implements')).toBe(true);
  97. });
  98. it('resolves the tag-based extends attribute the same way', async () => {
  99. write('system/Base.cfc', `component {}\n`);
  100. write('legacy/Old.cfc', `<cfcomponent extends="app.system.Base">\n<cffunction name="run"><cfreturn 1></cffunction>\n</cfcomponent>\n`);
  101. const edges = await load();
  102. expect(has(edges, 'Old', 'Base', 'system/Base.cfc')).toBe(true);
  103. });
  104. it('lowercase dotted paths still resolve when the file name case matches (framework.one)', async () => {
  105. write('framework/one.cfc', `component {\n function onRequest() { return 1; }\n}\n`);
  106. write('Application.cfc', `component extends="framework.one" {}\n`);
  107. const edges = await load();
  108. expect(has(edges, 'Application', 'one', 'framework/one.cfc')).toBe(true);
  109. });
  110. it('never treats a dotted calls reference as a component path', async () => {
  111. // `variables.dsn.getName()` is a member-access chain; the matcher is
  112. // gated to extends/implements so this must not mint a bogus edge to
  113. // a class that happens to share a trailing name.
  114. write('util/getName.cfc', `component {}\n`);
  115. write('svc/Caller.cfc', `component {\n function go() { return variables.dsn.getName(); }\n}\n`);
  116. const edges = await load();
  117. expect(edges.filter((e) => e.src === 'Caller' || e.src === 'go')).toHaveLength(0);
  118. });
  119. });