ClassesTest.php 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686
  1. <?php
  2. /**
  3. * Scan source code for references to classes and see if they indeed exist
  4. *
  5. * Copyright © Magento, Inc. All rights reserved.
  6. * See COPYING.txt for license details.
  7. */
  8. namespace Magento\Test\Integrity;
  9. use Magento\Framework\App\Utility\Classes;
  10. use Magento\Framework\Component\ComponentRegistrar;
  11. use Magento\Framework\App\Utility\Files;
  12. /**
  13. * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
  14. */
  15. class ClassesTest extends \PHPUnit\Framework\TestCase
  16. {
  17. /**
  18. * @var ComponentRegistrar
  19. */
  20. private $componentRegistrar;
  21. /**
  22. * List of already found classes to avoid checking them over and over again
  23. *
  24. * @var array
  25. */
  26. private $existingClasses = [];
  27. /**
  28. * @var array
  29. */
  30. private static $keywordsBlacklist = ["String", "Array", "Boolean", "Element"];
  31. /**
  32. * @var array|null
  33. */
  34. private $referenceBlackList = null;
  35. /**
  36. * Set Up
  37. */
  38. protected function setUp()
  39. {
  40. $this->componentRegistrar = new ComponentRegistrar();
  41. }
  42. public function testPhpFiles()
  43. {
  44. $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this);
  45. $invoker(
  46. /**
  47. * @param string $file
  48. */
  49. function ($file) {
  50. $contents = file_get_contents($file);
  51. $classes = Classes::getAllMatches(
  52. $contents,
  53. '/
  54. # ::getResourceModel ::getBlockSingleton ::getModel ::getSingleton
  55. \:\:get(?:ResourceModel | BlockSingleton | Model | Singleton)?\(\s*[\'"]([a-z\d\\\\]+)[\'"]\s*[\),]
  56. # various methods, first argument
  57. | \->(?:initReport | addBlock | createBlock
  58. | setAttributeModel | setBackendModel | setFrontendModel | setSourceModel | setModel
  59. )\(\s*\'([a-z\d\\\\]+)\'\s*[\),]
  60. # various methods, second argument
  61. | \->add(?:ProductConfigurationHelper | OptionsRenderCfg)\(.+?,\s*\'([a-z\d\\\\]+)\'\s*[\),]
  62. # \Mage::helper ->helper
  63. | (?:Mage\:\:|\->)helper\(\s*\'([a-z\d\\\\]+)\'\s*\)
  64. # misc
  65. | function\s_getCollectionClass\(\)\s+{\s+return\s+[\'"]([a-z\d\\\\]+)[\'"]
  66. | \'resource_model\'\s*=>\s*[\'"]([a-z\d\\\\]+)[\'"]
  67. | (?:_parentResourceModelName | _checkoutType | _apiType)\s*=\s*\'([a-z\d\\\\]+)\'
  68. | \'renderer\'\s*=>\s*\'([a-z\d\\\\]+)\'
  69. /ix'
  70. );
  71. // without modifier "i". Starting from capital letter is a significant characteristic of a class name
  72. Classes::getAllMatches(
  73. $contents,
  74. '/(?:\-> | parent\:\:)(?:_init | setType)\(\s*
  75. \'([A-Z][a-z\d][A-Za-z\d\\\\]+)\'(?:,\s*\'([A-Z][a-z\d][A-Za-z\d\\\\]+)\')
  76. \s*\)/x',
  77. $classes
  78. );
  79. $this->collectResourceHelpersPhp($contents, $classes);
  80. $this->assertClassesExist($classes, $file);
  81. },
  82. Files::init()->getPhpFiles(
  83. Files::INCLUDE_APP_CODE
  84. | Files::INCLUDE_PUB_CODE
  85. | Files::INCLUDE_LIBS
  86. | Files::INCLUDE_TEMPLATES
  87. | Files::AS_DATA_SET
  88. | Files::INCLUDE_NON_CLASSES
  89. )
  90. );
  91. }
  92. /**
  93. * Special case: collect resource helper references in PHP-code
  94. *
  95. * @param string $contents
  96. * @param array &$classes
  97. * @return void
  98. */
  99. private function collectResourceHelpersPhp(string $contents, array &$classes): void
  100. {
  101. $regex = '/(?:\:\:|\->)getResourceHelper\(\s*\'([a-z\d\\\\]+)\'\s*\)/ix';
  102. $matches = Classes::getAllMatches($contents, $regex);
  103. foreach ($matches as $moduleName) {
  104. $classes[] = "{$moduleName}\\Model\\ResourceModel\\Helper\\Mysql4";
  105. }
  106. }
  107. public function testConfigFiles()
  108. {
  109. $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this);
  110. $invoker(
  111. /**
  112. * @param string $path
  113. */
  114. function ($path) {
  115. $classes = Classes::collectClassesInConfig(simplexml_load_file($path));
  116. $this->assertClassesExist($classes, $path);
  117. },
  118. Files::init()->getMainConfigFiles()
  119. );
  120. }
  121. public function testLayoutFiles()
  122. {
  123. $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this);
  124. $invoker(
  125. /**
  126. * @param string $path
  127. */
  128. function ($path) {
  129. $xml = simplexml_load_file($path);
  130. $classes = Classes::getXmlNodeValues(
  131. $xml,
  132. '/layout//*[contains(text(), "\\\\Block\\\\") or contains(text(),
  133. "\\\\Model\\\\") or contains(text(), "\\\\Helper\\\\")]'
  134. );
  135. foreach (Classes::getXmlAttributeValues(
  136. $xml,
  137. '/layout//@helper',
  138. 'helper'
  139. ) as $class) {
  140. $classes[] = Classes::getCallbackClass($class);
  141. }
  142. foreach (Classes::getXmlAttributeValues(
  143. $xml,
  144. '/layout//@module',
  145. 'module'
  146. ) as $module) {
  147. $classes[] = str_replace('_', '\\', "{$module}_Helper_Data");
  148. }
  149. $classes = array_merge($classes, Classes::collectLayoutClasses($xml));
  150. $this->assertClassesExist(array_unique($classes), $path);
  151. },
  152. Files::init()->getLayoutFiles()
  153. );
  154. }
  155. /**
  156. * Check whether specified classes correspond to a file according PSR-0 standard
  157. *
  158. * Cyclomatic complexity is because of temporary marking test as incomplete
  159. * Suppressing "unused variable" because of the "catch" block
  160. *
  161. * @param array $classes
  162. * @param string $path
  163. * @return void
  164. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  165. * @SuppressWarnings(PHPMD.UnusedLocalVariable)
  166. */
  167. private function assertClassesExist(array $classes, string $path): void
  168. {
  169. if (!$classes) {
  170. return;
  171. }
  172. $badClasses = [];
  173. $badUsages = [];
  174. foreach ($classes as $class) {
  175. $class = trim($class, '\\');
  176. try {
  177. if (strrchr($class, '\\') === false && !Classes::isVirtual($class)) {
  178. $badUsages[] = $class;
  179. continue;
  180. } else {
  181. $this->assertTrue(
  182. isset(
  183. $this->existingClasses[$class]
  184. ) || Files::init()->classFileExists(
  185. $class
  186. ) || Classes::isVirtual(
  187. $class
  188. ) || Classes::isAutogenerated(
  189. $class
  190. )
  191. );
  192. }
  193. $this->existingClasses[$class] = 1;
  194. } catch (\PHPUnit\Framework\AssertionFailedError $e) {
  195. $badClasses[] = '\\' . $class;
  196. }
  197. }
  198. if ($badClasses) {
  199. $this->fail("Files not found for following usages in {$path}:\n" . implode("\n", $badClasses));
  200. }
  201. if ($badUsages) {
  202. $this->fail("Bad usages of classes in {$path}: \n" . implode("\n", $badUsages));
  203. }
  204. }
  205. public function testClassNamespaces()
  206. {
  207. $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this);
  208. $invoker(
  209. /**
  210. * Assert PHP classes have valid formal namespaces according to file locations
  211. *
  212. * @param array $file
  213. */
  214. function ($file) {
  215. $relativePath = str_replace(BP . "/", "", $file);
  216. // exceptions made for fixture files from tests
  217. if (strpos($relativePath, '/_files/') !== false) {
  218. return;
  219. }
  220. $contents = file_get_contents($file);
  221. $classPattern = '/^(abstract\s)?class\s[A-Z][^\s\/]+/m';
  222. $classNameMatch = [];
  223. $className = null;
  224. // if no class declaration found for $file, then skip this file
  225. if (preg_match($classPattern, $contents, $classNameMatch) == 0) {
  226. return;
  227. }
  228. $classParts = explode(' ', $classNameMatch[0]);
  229. $className = array_pop($classParts);
  230. $this->assertClassNamespace($file, $relativePath, $contents, $className);
  231. },
  232. Files::init()->getPhpFiles()
  233. );
  234. }
  235. /**
  236. * Assert PHP classes have valid formal namespaces according to file locations
  237. *
  238. *
  239. * @param string $file
  240. * @param string $relativePath
  241. * @param string $contents
  242. * @param string $className
  243. * @return void
  244. */
  245. private function assertClassNamespace(string $file, string $relativePath, string $contents, string $className): void
  246. {
  247. $namespacePattern = '/(Magento|Zend)\/[a-zA-Z]+[^\.]+/';
  248. $formalPattern = '/^namespace\s[a-zA-Z]+(\\\\[a-zA-Z0-9]+)*/m';
  249. $namespaceMatch = [];
  250. $formalNamespaceArray = [];
  251. $namespaceFolders = null;
  252. // if no namespace pattern found according to the path of the file, skip the file
  253. if (preg_match($namespacePattern, $relativePath, $namespaceMatch) == 0) {
  254. return;
  255. }
  256. $namespaceFolders = $namespaceMatch[0];
  257. $classParts = explode('/', $namespaceFolders);
  258. array_pop($classParts);
  259. $expectedNamespace = implode('\\', $classParts);
  260. if (preg_match($formalPattern, $contents, $formalNamespaceArray) != 0) {
  261. $foundNamespace = substr($formalNamespaceArray[0], 10);
  262. $foundNamespace = str_replace('\\', '/', $foundNamespace);
  263. $foundNamespace .= '/' . $className;
  264. if ($namespaceFolders != null && $foundNamespace != null) {
  265. $this->assertEquals(
  266. $namespaceFolders,
  267. $foundNamespace,
  268. "Location of {$file} does not match formal namespace: {$expectedNamespace}\n"
  269. );
  270. }
  271. } else {
  272. $this->fail("Missing expected namespace \"{$expectedNamespace}\" for file: {$file}");
  273. }
  274. }
  275. public function testClassReferences()
  276. {
  277. $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this);
  278. $invoker(
  279. /**
  280. * @param string $file
  281. */
  282. function ($file) {
  283. $relativePath = str_replace(BP, "", $file);
  284. // Due to the examples given with the regex patterns, we skip this test file itself
  285. if (preg_match(
  286. '/\/dev\/tests\/static\/testsuite\/Magento\/Test\/Integrity\/ClassesTest.php$/',
  287. $relativePath
  288. )) {
  289. return;
  290. }
  291. $contents = file_get_contents($file);
  292. $formalPattern = '/^namespace\s[a-zA-Z]+(\\\\[a-zA-Z0-9]+)*/m';
  293. $formalNamespaceArray = [];
  294. // Skip the file if the class is not defined using formal namespace
  295. if (preg_match($formalPattern, $contents, $formalNamespaceArray) == 0) {
  296. return;
  297. }
  298. $namespacePath = str_replace('\\', '/', substr($formalNamespaceArray[0], 10));
  299. // Instantiation of new object, for example: "return new Foo();"
  300. $newObjectPattern = '/^' .
  301. '.*new\s(?<venderClass>\\\\Magento(?:\\\\[a-zA-Z0-9_]+)+)\(.*\)' .
  302. '|.*new\s(?<badClass>[A-Z][a-zA-Z0-9]+[a-zA-Z0-9_\\\\]*)\(.*\)\;' .
  303. '|use [A-Z][a-zA-Z0-9_\\\\]+ as (?<aliasClass>[A-Z][a-zA-Z0-9]+)' .
  304. '/m';
  305. $result1 = [];
  306. preg_match_all($newObjectPattern, $contents, $result1);
  307. // Static function/variable, for example: "Foo::someStaticFunction();"
  308. $staticCallPattern = '/^' .
  309. '((?!Magento).)*(?<venderClass>\\\\Magento(?:\\\\[a-zA-Z0-9_]+)+)\:\:.*\;' .
  310. '|[^\\\\^a-z^A-Z^0-9^_^:](?<badClass>[A-Z][a-zA-Z0-9_]+)\:\:.*\;' .
  311. '|use [A-Z][a-zA-Z0-9_\\\\]+ as (?<aliasClass>[A-Z][a-zA-Z0-9]+)' .
  312. '/m';
  313. $result2 = [];
  314. preg_match_all($staticCallPattern, $contents, $result2);
  315. // Annotation, for example: "* @return \Magento\Foo\Bar" or "* @throws Exception" or "* @return Foo"
  316. $annotationPattern = '/^' .
  317. '[\s]*\*\s\@(?:return|throws)\s(?<venderClass>\\\\Magento(?:\\\\[a-zA-Z0-9_]+)+)' .
  318. '|[\s]*\*\s\@return\s(?<badClass>[A-Z][a-zA-Z0-9_\\\\]+)' .
  319. '|[\s]*\*\s\@throws\s(?<exception>[A-Z][a-zA-Z0-9_\\\\]+)' .
  320. '|use [A-Z][a-zA-Z0-9_\\\\]+ as (?<aliasClass>[A-Z][a-zA-Z0-9]+)' .
  321. '/m';
  322. $result3 = [];
  323. preg_match_all($annotationPattern, $contents, $result3);
  324. $vendorClasses = array_unique(
  325. array_merge_recursive($result1['venderClass'], $result2['venderClass'], $result3['venderClass'])
  326. );
  327. $badClasses = array_unique(
  328. array_merge_recursive($result1['badClass'], $result2['badClass'], $result3['badClass'])
  329. );
  330. $aliasClasses = array_unique(
  331. array_merge_recursive($result1['aliasClass'], $result2['aliasClass'], $result3['aliasClass'])
  332. );
  333. $vendorClasses = array_filter($vendorClasses, 'strlen');
  334. $vendorClasses = $this->referenceBlacklistFilter($vendorClasses);
  335. if (!empty($vendorClasses)) {
  336. $this->assertClassesExist($vendorClasses, $file);
  337. }
  338. if (!empty($result3['exception']) && $result3['exception'][0] != "") {
  339. $badClasses = array_merge($badClasses, array_filter($result3['exception'], 'strlen'));
  340. }
  341. $badClasses = array_filter($badClasses, 'strlen');
  342. if (empty($badClasses)) {
  343. return;
  344. }
  345. $aliasClasses = array_filter($aliasClasses, 'strlen');
  346. if (!empty($aliasClasses)) {
  347. $badClasses = $this->handleAliasClasses($aliasClasses, $badClasses);
  348. }
  349. $badClasses = $this->referenceBlacklistFilter($badClasses);
  350. $badClasses = $this->removeSpecialCases($badClasses, $file, $contents, $namespacePath);
  351. $this->assertClassReferences($badClasses, $file);
  352. },
  353. Files::init()->getPhpFiles()
  354. );
  355. }
  356. /**
  357. * Remove alias class name references that have been identified as 'bad'.
  358. *
  359. * @param array $aliasClasses
  360. * @param array $badClasses
  361. * @return array
  362. */
  363. private function handleAliasClasses(array $aliasClasses, array $badClasses): array
  364. {
  365. foreach ($aliasClasses as $aliasClass) {
  366. foreach ($badClasses as $badClass) {
  367. if (strpos($badClass, $aliasClass) === 0) {
  368. unset($badClasses[array_search($badClass, $badClasses)]);
  369. }
  370. }
  371. }
  372. return $badClasses;
  373. }
  374. /**
  375. * This function is to remove legacy code usages according to _files/blacklist/reference.txt
  376. *
  377. * @param array $classes
  378. * @return array
  379. */
  380. private function referenceBlacklistFilter(array $classes): array
  381. {
  382. // exceptions made for the files from the blacklist
  383. $classes = $this->getReferenceBlacklist();
  384. foreach ($classes as $class) {
  385. if (in_array($class, $this->referenceBlackList)) {
  386. unset($classes[array_search($class, $classes)]);
  387. }
  388. }
  389. return $classes;
  390. }
  391. /**
  392. * Returns array of class names from black list.
  393. *
  394. * @return array
  395. */
  396. private function getReferenceBlacklist(): array
  397. {
  398. if (!isset($this->referenceBlackList)) {
  399. $this->referenceBlackList = file(
  400. __DIR__ . '/_files/blacklist/reference.txt',
  401. FILE_IGNORE_NEW_LINES
  402. );
  403. }
  404. return $this->referenceBlackList;
  405. }
  406. /**
  407. * This function is to remove special cases (if any) from the list of found bad classes
  408. *
  409. * @param array $badClasses
  410. * @param string $file
  411. * @param string $contents
  412. * @param string $namespacePath
  413. * @return array
  414. */
  415. private function removeSpecialCases(array $badClasses, string $file, string $contents, string $namespacePath): array
  416. {
  417. foreach ($badClasses as $badClass) {
  418. // Remove valid usages of Magento modules from the list
  419. // for example: 'Magento_Sales::actions_edit'
  420. if (preg_match('/^[A-Z][a-z]+_[A-Z0-9][a-z0-9]+$/', $badClass)) {
  421. $moduleDir = $this->componentRegistrar->getPath(ComponentRegistrar::MODULE, $badClass);
  422. if ($moduleDir !== null) {
  423. unset($badClasses[array_search($badClass, $badClasses)]);
  424. continue;
  425. }
  426. }
  427. // Remove usage of key words such as "Array", "String", and "Boolean"
  428. if (in_array($badClass, self::$keywordsBlacklist)) {
  429. unset($badClasses[array_search($badClass, $badClasses)]);
  430. continue;
  431. }
  432. $classParts = explode('/', $file);
  433. $className = array_pop($classParts);
  434. // Remove usage of the class itself from the list
  435. if ($badClass . '.php' == $className) {
  436. unset($badClasses[array_search($badClass, $badClasses)]);
  437. continue;
  438. }
  439. if ($this->removeSpecialCasesNonFullyQualifiedClassNames($namespacePath, $badClasses, $badClass)) {
  440. continue;
  441. }
  442. $referenceFile = implode('/', $classParts) . '/' . str_replace('\\', '/', $badClass) . '.php';
  443. if (file_exists($referenceFile)) {
  444. unset($badClasses[array_search($badClass, $badClasses)]);
  445. continue;
  446. }
  447. // Remove usage of classes that have been declared as "use" or "include"
  448. // Also deals with case like: "use \Zend\Code\Scanner\FileScanner, Magento\Tools\Di\Compiler\Log\Log;"
  449. // (continued) where there is a comma separating two different classes.
  450. if (preg_match('/use\s.*[\\n]?.*' . str_replace('\\', '\\\\', $badClass) . '[\,\;]/', $contents)) {
  451. unset($badClasses[array_search($badClass, $badClasses)]);
  452. continue;
  453. }
  454. }
  455. return $badClasses;
  456. }
  457. /**
  458. * Helper class for removeSpecialCases to remove classes that do not use fully-qualified class names
  459. *
  460. * @param string $namespacePath
  461. * @param array $badClasses
  462. * @param string $badClass
  463. * @return bool
  464. * @throws \Exception
  465. */
  466. private function removeSpecialCasesNonFullyQualifiedClassNames($namespacePath, &$badClasses, $badClass)
  467. {
  468. $namespaceParts = explode('/', $namespacePath);
  469. $moduleDir = null;
  470. if (isset($namespaceParts[1])) {
  471. $moduleName = array_shift($namespaceParts) . '_' . array_shift($namespaceParts);
  472. $moduleDir = $this->componentRegistrar->getPath(ComponentRegistrar::MODULE, $moduleName);
  473. }
  474. if ($moduleDir) {
  475. $fullPath = $moduleDir . '/' . implode('/', $namespaceParts) . '/' .
  476. str_replace('\\', '/', $badClass) . '.php';
  477. if (file_exists($fullPath)) {
  478. unset($badClasses[array_search($badClass, $badClasses)]);
  479. return true;
  480. }
  481. }
  482. $fullPath = $this->getLibraryDirByPath($namespacePath, $badClass);
  483. if ($fullPath && file_exists($fullPath)) {
  484. unset($badClasses[array_search($badClass, $badClasses)]);
  485. return true;
  486. } else {
  487. return $this->removeSpecialCasesForAllOthers($namespacePath, $badClass, $badClasses);
  488. }
  489. }
  490. /**
  491. * Get path to the file in the library based on namespace path
  492. *
  493. * @param string $namespacePath
  494. * @param string $badClass
  495. * @return null|string
  496. */
  497. private function getLibraryDirByPath(string $namespacePath, string $badClass)
  498. {
  499. $libraryDir = null;
  500. $fullPath = null;
  501. $namespaceParts = explode('/', $namespacePath);
  502. if (isset($namespaceParts[1]) && $namespaceParts[1]) {
  503. $vendor = array_shift($namespaceParts);
  504. $lib = array_shift($namespaceParts);
  505. if ($lib == 'framework') {
  506. $subLib = $namespaceParts[0];
  507. $subLib = strtolower(preg_replace('/(.)([A-Z])/', "$1-$2", $subLib));
  508. $libraryName = $vendor . '/' . $lib . '-' . $subLib;
  509. $libraryDir = $this->componentRegistrar->getPath(
  510. ComponentRegistrar::LIBRARY,
  511. strtolower($libraryName)
  512. );
  513. if ($libraryDir) {
  514. array_shift($namespaceParts);
  515. } else {
  516. $libraryName = $vendor . '/' . $lib;
  517. $libraryDir = $this->componentRegistrar->getPath(
  518. ComponentRegistrar::LIBRARY,
  519. strtolower($libraryName)
  520. );
  521. }
  522. } else {
  523. $lib = strtolower(preg_replace('/(.)([A-Z])/', "$1-$2", $lib));
  524. $libraryName = $vendor . '/' . $lib;
  525. $libraryDir = $this->componentRegistrar->getPath(
  526. ComponentRegistrar::LIBRARY,
  527. strtolower($libraryName)
  528. );
  529. }
  530. }
  531. if ($libraryDir) {
  532. $fullPath = $libraryDir . '/' . implode('/', $namespaceParts) . '/' .
  533. str_replace('\\', '/', $badClass) . '.php';
  534. }
  535. return $fullPath;
  536. }
  537. /**
  538. * @param string $namespacePath
  539. * @param string $badClass
  540. * @param array $badClasses
  541. * @return bool
  542. */
  543. private function removeSpecialCasesForAllOthers(string $namespacePath, string $badClass, array &$badClasses): bool
  544. {
  545. // Remove usage of classes that do NOT using fully-qualified class names (possibly under same namespace)
  546. $directories = [
  547. BP . '/dev/tools/',
  548. BP . '/dev/tests/api-functional/framework/',
  549. BP . '/dev/tests/functional/',
  550. BP . '/dev/tests/integration/framework/',
  551. BP . '/dev/tests/integration/framework/tests/unit/testsuite/',
  552. BP . '/dev/tests/integration/testsuite/',
  553. BP . '/dev/tests/integration/testsuite/Magento/Test/Integrity/',
  554. BP . '/dev/tests/static/framework/',
  555. BP . '/dev/tests/static/testsuite/',
  556. BP . '/setup/src/',
  557. ];
  558. $libraryPaths = $this->componentRegistrar->getPaths(ComponentRegistrar::LIBRARY);
  559. $directories = array_merge($directories, $libraryPaths);
  560. // Full list of directories where there may be namespace classes
  561. foreach ($directories as $directory) {
  562. $fullPath = $directory . $namespacePath . '/' . str_replace('\\', '/', $badClass) . '.php';
  563. if (file_exists($fullPath)) {
  564. unset($badClasses[array_search($badClass, $badClasses)]);
  565. return true;
  566. }
  567. }
  568. return false;
  569. }
  570. /**
  571. * Assert any found class name resolves into a file name and corresponds to an existing file
  572. *
  573. * @param array $badClasses
  574. * @param string $file
  575. * @return void
  576. */
  577. private function assertClassReferences(array $badClasses, string $file): void
  578. {
  579. if (empty($badClasses)) {
  580. return;
  581. }
  582. $this->fail("Incorrect namespace usage(s) found in file {$file}:\n" . implode("\n", $badClasses));
  583. }
  584. public function testCoversAnnotation()
  585. {
  586. $files = Files::init();
  587. $errors = [];
  588. $filesToTest = $files->getPhpFiles(Files::INCLUDE_TESTS);
  589. if (($key = array_search(str_replace('\\', '/', __FILE__), $filesToTest)) !== false) {
  590. unset($filesToTest[$key]);
  591. }
  592. foreach ($filesToTest as $file) {
  593. $code = file_get_contents($file);
  594. if (preg_match('/@covers(DefaultClass)?\s+([\w\\\\]+)(::([\w\\\\]+))?/', $code, $matches)) {
  595. if ($this->isNonexistentEntityCovered($matches)) {
  596. $errors[] = $file . ': ' . $matches[0];
  597. }
  598. }
  599. }
  600. if ($errors) {
  601. $this->fail(
  602. 'Nonexistent classes/methods were found in @covers annotations: ' . PHP_EOL . implode(PHP_EOL, $errors)
  603. );
  604. }
  605. }
  606. /**
  607. * @param array $matches
  608. * @return bool
  609. */
  610. private function isNonexistentEntityCovered($matches)
  611. {
  612. return !empty($matches[2]) && !class_exists($matches[2])
  613. || !empty($matches[4]) && !method_exists($matches[2], $matches[4]);
  614. }
  615. }