1
0

php-import-alias-static-resolution.test.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  2. import * as fs from 'node:fs';
  3. import * as path from 'node:path';
  4. import * as os from 'node:os';
  5. import { CodeGraph } from '../src';
  6. const fixtureDir = path.join(__dirname, 'fixtures', 'php-import-alias-static');
  7. describe('PHP static calls through import aliases (#1545)', () => {
  8. let dir: string;
  9. let cg: CodeGraph | undefined;
  10. beforeEach(() => {
  11. dir = fs.mkdtempSync(path.join(os.tmpdir(), 'php-static-alias-'));
  12. fs.cpSync(fixtureDir, dir, { recursive: true });
  13. });
  14. afterEach(() => {
  15. cg?.close();
  16. cg = undefined;
  17. fs.rmSync(dir, { recursive: true, force: true });
  18. });
  19. it('attributes callers, callees and impact to SettleService instead of SettleRepository', async () => {
  20. cg = await CodeGraph.init(dir, { silent: true });
  21. await cg.indexAll();
  22. const method = (qualifiedName: string) => {
  23. const node = cg!.searchNodes(qualifiedName.split('::').pop()!)
  24. .map((result) => result.node)
  25. .find((n) => n.qualifiedName === qualifiedName);
  26. expect(node, qualifiedName).toBeDefined();
  27. return node!;
  28. };
  29. const excel = method('App\\Http\\Controllers\\Backend::SettleController::excel');
  30. const service = method('App\\Services::SettleService::getSettlesToExcel');
  31. const repository = method('App\\Repositories::SettleRepository::getSettlesToExcel');
  32. expect(cg.getCallees(excel.id).map(({ node }) => node.id)).toContain(service.id);
  33. expect(cg.getCallees(excel.id).map(({ node }) => node.id)).not.toContain(repository.id);
  34. expect(cg.getCallers(service.id).map(({ node }) => node.id)).toContain(excel.id);
  35. expect(cg.getCallers(repository.id).map(({ node }) => node.id)).not.toContain(excel.id);
  36. expect([...cg.getImpactRadius(service.id).nodes.keys()]).toContain(excel.id);
  37. expect([...cg.getImpactRadius(repository.id).nodes.keys()]).not.toContain(excel.id);
  38. });
  39. const write = (file: string, source: string) => {
  40. const target = path.join(dir, file);
  41. fs.mkdirSync(path.dirname(target), { recursive: true });
  42. fs.writeFileSync(target, source);
  43. };
  44. const controllerPath = 'app/Http/Controllers/Backend/SettleController.php';
  45. const servicePath = 'app/Services/SettleService.php';
  46. const controllerSource = fs.readFileSync(path.join(fixtureDir, controllerPath), 'utf8');
  47. const serviceSource = fs.readFileSync(path.join(fixtureDir, servicePath), 'utf8');
  48. const serviceMethod = 'App\\Services::SettleService::getSettlesToExcel';
  49. const callees = async () => {
  50. cg = await CodeGraph.init(dir, { silent: true });
  51. await cg.indexAll();
  52. const excel = cg.searchNodes('excel').map(({ node }) => node)
  53. .find((n) => n.kind === 'method' && n.filePath === controllerPath)!;
  54. expect(excel).toBeDefined();
  55. return cg.getCallees(excel.id).map(({ node }) => node.qualifiedName).sort();
  56. };
  57. it('uses the imported namespace even when another namespace declares SettleService', async () => {
  58. write('app/Repositories/SettleService.php', serviceSource.replace('App\\Services', 'App\\Repositories'));
  59. expect(await callees()).toEqual([serviceMethod]);
  60. });
  61. it('uses the import instead of a class whose actual name is the alias', async () => {
  62. write('app/Http/Controllers/Backend/Settle.php', serviceSource
  63. .replace('App\\Services', 'App\\Http\\Controllers\\Backend')
  64. .replace('class SettleService', 'class Settle'));
  65. expect(await callees()).toEqual([serviceMethod]);
  66. });
  67. it('constrains the method to its owner when another class shares the imported file', async () => {
  68. write(servicePath, serviceSource.replace(
  69. 'class SettleService',
  70. 'class SettleServiceDecoy { public static function getSettlesToExcel() {} }\nclass SettleService',
  71. ));
  72. expect(await callees()).toEqual([serviceMethod]);
  73. });
  74. it('resolves by namespace even when the file is not named after the imported class', async () => {
  75. fs.renameSync(path.join(dir, servicePath), path.join(dir, 'app/Services/exports.php'));
  76. expect(await callees()).toEqual([serviceMethod]);
  77. });
  78. it('handles an import with a leading namespace separator', async () => {
  79. write(controllerPath, controllerSource.replace('use App\\', 'use \\App\\'));
  80. expect(await callees()).toEqual([serviceMethod]);
  81. });
  82. it('keeps an unaliased class import on its declared namespace', async () => {
  83. write(controllerPath, controllerSource.replace(' as Settle;', ';').replace('Settle::', 'SettleService::'));
  84. write('app/Repositories/SettleService.php', serviceSource.replace('App\\Services', 'App\\Repositories'));
  85. expect(await callees()).toEqual([serviceMethod]);
  86. });
  87. it('supports an alias for a class in the global namespace', async () => {
  88. write(controllerPath, controllerSource.replace('App\\Services\\SettleService', 'SettleService'));
  89. write(servicePath, serviceSource.replace('namespace App\\Services;', ''));
  90. expect(await callees()).toEqual(['SettleService::getSettlesToExcel']);
  91. });
  92. it.each(['method missing', 'class outside the index'])('leaves the call unresolved when the imported %s', async (scenario) => {
  93. if (scenario === 'method missing') {
  94. write(servicePath, serviceSource.replace('getSettlesToExcel', 'otherMethod'));
  95. } else {
  96. fs.rmSync(path.join(dir, servicePath));
  97. // Even the right short name in the wrong namespace cannot donate a method.
  98. write('app/Repositories/SettleService.php', serviceSource.replace('App\\Services', 'App\\Repositories'));
  99. }
  100. expect(await callees()).toEqual([]);
  101. });
  102. it('keeps a variable and a static receiver with the same spelling in separate namespaces', async () => {
  103. write('app/Services/OtherService.php', String.raw`<?php
  104. namespace App\Services;
  105. class OtherService {
  106. public function getSettlesToExcel() {}
  107. }
  108. `);
  109. write(controllerPath, controllerSource.replace(
  110. 'return Settle::',
  111. '$Settle = new OtherService();\n $Settle->getSettlesToExcel();\n return Settle::',
  112. ));
  113. expect((await callees()).filter((name) => name.endsWith('::getSettlesToExcel'))).toEqual([
  114. 'App\\Services::OtherService::getSettlesToExcel',
  115. serviceMethod,
  116. ]);
  117. });
  118. });