![]() Server : Apache System : Linux server2.corals.io 4.18.0-348.2.1.el8_5.x86_64 #1 SMP Mon Nov 15 09:17:08 EST 2021 x86_64 User : corals ( 1002) PHP Version : 7.4.33 Disable Function : exec,passthru,shell_exec,system Directory : /home/corals/old/dev/tests/static/testsuite/Magento/Test/Integrity/ |
<?php /** * Scan source code for references to classes and see if they indeed exist * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Test\Integrity; use Magento\Framework\App\Utility\Classes; use Magento\Framework\Component\ComponentRegistrar; use Magento\Framework\App\Utility\Files; /** * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ class ClassesTest extends \PHPUnit\Framework\TestCase { /** * @var ComponentRegistrar */ private $componentRegistrar; /** * List of already found classes to avoid checking them over and over again * * @var array */ private $existingClasses = []; /** * @var array */ private static $excludeKeywords = ["String", "Array", "Boolean", "Element"]; /** * @var array|null */ private $excludeReference = null; /** * Set Up */ protected function setUp(): void { $this->componentRegistrar = new ComponentRegistrar(); } public function testPhpFiles() { $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this); $invoker( /** * @param string $file */ function ($file) { $contents = file_get_contents($file); $classes = Classes::getAllMatches( $contents, '/ # ::getResourceModel ::getBlockSingleton ::getModel ::getSingleton \:\:get(?:ResourceModel | BlockSingleton | Model | Singleton)?\(\s*[\'"]([a-z\d\\\\]+)[\'"]\s*[\),] # various methods, first argument | \->(?:initReport | addBlock | createBlock | setAttributeModel | setBackendModel | setFrontendModel | setSourceModel | setModel )\(\s*\'([a-z\d\\\\]+)\'\s*[\),] # various methods, second argument | \->add(?:ProductConfigurationHelper | OptionsRenderCfg)\(.+?,\s*\'([a-z\d\\\\]+)\'\s*[\),] # \Mage::helper ->helper | (?:Mage\:\:|\->)helper\(\s*\'([a-z\d\\\\]+)\'\s*\) # misc | function\s_getCollectionClass\(\)\s+{\s+return\s+[\'"]([a-z\d\\\\]+)[\'"] | \'resource_model\'\s*=>\s*[\'"]([a-z\d\\\\]+)[\'"] | (?:_parentResourceModelName | _checkoutType | _apiType)\s*=\s*\'([a-z\d\\\\]+)\' | \'renderer\'\s*=>\s*\'([a-z\d\\\\]+)\' /ix' ); // without modifier "i". Starting from capital letter is a significant characteristic of a class name Classes::getAllMatches( $contents, '/(?:\-> | parent\:\:)(?:_init | setType)\(\s* \'([A-Z][a-z\d][A-Za-z\d\\\\]+)\'(?:,\s*\'([A-Z][a-z\d][A-Za-z\d\\\\]+)\') \s*\)/x', $classes ); $this->collectResourceHelpersPhp($contents, $classes); $this->assertClassesExist($classes, $file); }, Files::init()->getPhpFiles( Files::INCLUDE_APP_CODE | Files::INCLUDE_PUB_CODE | Files::INCLUDE_LIBS | Files::INCLUDE_TEMPLATES | Files::AS_DATA_SET | Files::INCLUDE_NON_CLASSES ) ); } /** * Special case: collect resource helper references in PHP-code * * @param string $contents * @param array &$classes * @return void */ private function collectResourceHelpersPhp(string $contents, array &$classes): void { $regex = '/(?:\:\:|\->)getResourceHelper\(\s*\'([a-z\d\\\\]+)\'\s*\)/ix'; $matches = Classes::getAllMatches($contents, $regex); foreach ($matches as $moduleName) { $classes[] = "{$moduleName}\\Model\\ResourceModel\\Helper\\Mysql4"; } } public function testConfigFiles() { $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this); $invoker( /** * @param string $path */ function ($path) { $classes = Classes::collectClassesInConfig(simplexml_load_file($path)); $this->assertClassesExist($classes, $path); }, Files::init()->getMainConfigFiles() ); } public function testLayoutFiles() { $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this); $invoker( /** * @param string $path */ function ($path) { $xml = simplexml_load_file($path); $classes = Classes::getXmlNodeValues( $xml, '/layout//*[contains(text(), "\\\\Block\\\\") or contains(text(), "\\\\Model\\\\") or contains(text(), "\\\\Helper\\\\")]' ); foreach (Classes::getXmlAttributeValues( $xml, '/layout//@helper', 'helper' ) as $class) { $classes[] = Classes::getCallbackClass($class); } foreach (Classes::getXmlAttributeValues( $xml, '/layout//@module', 'module' ) as $module) { $classes[] = str_replace('_', '\\', "{$module}_Helper_Data"); } $classes = array_merge($classes, Classes::collectLayoutClasses($xml)); $this->assertClassesExist(array_unique($classes), $path); }, Files::init()->getLayoutFiles() ); } /** * Check whether specified classes correspond to a file according PSR-0 standard * * Cyclomatic complexity is because of temporary marking test as incomplete * Suppressing "unused variable" because of the "catch" block * * @param array $classes * @param string $path * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ private function assertClassesExist(array $classes, string $path): void { if (!$classes) { return; } $badClasses = []; $badUsages = []; foreach ($classes as $class) { $class = trim($class, '\\'); try { if (strrchr($class, '\\') === false && !Classes::isVirtual($class)) { $badUsages[] = $class; continue; } else { $this->assertTrue( isset( $this->existingClasses[$class] ) || Files::init()->classFileExists( $class ) || Classes::isVirtual( $class ) || Classes::isAutogenerated( $class ) ); } $this->existingClasses[$class] = 1; } catch (\PHPUnit\Framework\AssertionFailedError $e) { $badClasses[] = '\\' . $class; } } if ($badClasses) { $this->fail("Files not found for following usages in {$path}:\n" . implode("\n", $badClasses)); } if ($badUsages) { $this->fail("Bad usages of classes in {$path}: \n" . implode("\n", $badUsages)); } } public function testClassNamespaces() { $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this); $invoker( /** * Assert PHP classes have valid formal namespaces according to file locations * * @param array $file */ function ($file) { $relativePath = str_replace(BP . "/", "", $file); // exceptions made for fixture files from tests if (strpos($relativePath, '/_files/') !== false) { return; } $contents = file_get_contents($file); $classPattern = '/^(abstract\s)?class\s[A-Z][^\s\/]+/m'; $classNameMatch = []; $className = null; // if no class declaration found for $file, then skip this file if (preg_match($classPattern, $contents, $classNameMatch) == 0) { return; } $classParts = explode(' ', $classNameMatch[0]); $className = array_pop($classParts); $this->assertClassNamespace($file, $relativePath, $contents, $className); }, Files::init()->getPhpFiles() ); } /** * Assert PHP classes have valid formal namespaces according to file locations * * * @param string $file * @param string $relativePath * @param string $contents * @param string $className * @return void */ private function assertClassNamespace(string $file, string $relativePath, string $contents, string $className): void { $namespacePattern = '/(Magento|Zend)\/[a-zA-Z]+[^\.]+/'; $formalPattern = '/^namespace\s[a-zA-Z]+(\\\\[a-zA-Z0-9]+)*/m'; $namespaceMatch = []; $formalNamespaceArray = []; $namespaceFolders = null; // if no namespace pattern found according to the path of the file, skip the file if (preg_match($namespacePattern, $relativePath, $namespaceMatch) == 0) { return; } $namespaceFolders = $namespaceMatch[0]; $classParts = explode('/', $namespaceFolders); array_pop($classParts); $expectedNamespace = implode('\\', $classParts); if (preg_match($formalPattern, $contents, $formalNamespaceArray) != 0) { $foundNamespace = substr($formalNamespaceArray[0], 10); $foundNamespace = str_replace('\\', '/', $foundNamespace); $foundNamespace .= '/' . $className; if ($namespaceFolders != null && $foundNamespace != null) { $this->assertEquals( $namespaceFolders, $foundNamespace, "Location of {$file} does not match formal namespace: {$expectedNamespace}\n" ); } } else { $this->fail("Missing expected namespace \"{$expectedNamespace}\" for file: {$file}"); } } public function testClassReferences() { $this->markTestSkipped("To be fixed in MC-33329. The test is not working properly " . "after excluded logic was fixed. Previously it was ignoring all files."); $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this); $invoker( /** * @param string $file */ function ($file) { $relativePath = str_replace(BP, "", $file); // Due to the examples given with the regex patterns, we skip this test file itself if (preg_match( '/\/dev\/tests\/static\/testsuite\/Magento\/Test\/Integrity\/ClassesTest.php$/', $relativePath )) { return; } $contents = file_get_contents($file); $formalPattern = '/^namespace\s[a-zA-Z]+(\\\\[a-zA-Z0-9]+)*/m'; $formalNamespaceArray = []; // Skip the file if the class is not defined using formal namespace if (preg_match($formalPattern, $contents, $formalNamespaceArray) == 0) { return; } $namespacePath = str_replace('\\', '/', substr($formalNamespaceArray[0], 10)); // Instantiation of new object, for example: "return new Foo();" $newObjectPattern = '/^' . '.*new\s(?<venderClass>\\\\Magento(?:\\\\[a-zA-Z0-9_]+)+)\(.*\)' . '|.*new\s(?<badClass>[A-Z][a-zA-Z0-9]+[a-zA-Z0-9_\\\\]*)\(.*\)\;' . '|use [A-Z][a-zA-Z0-9_\\\\]+ as (?<aliasClass>[A-Z][a-zA-Z0-9]+)' . '/m'; $result1 = []; preg_match_all($newObjectPattern, $contents, $result1); // Static function/variable, for example: "Foo::someStaticFunction();" $staticCallPattern = '/^' . '((?!Magento).)*(?<venderClass>\\\\Magento(?:\\\\[a-zA-Z0-9_]+)+)\:\:.*\;' . '|[^\\\\^a-z^A-Z^0-9^_^:](?<badClass>[A-Z][a-zA-Z0-9_]+)\:\:.*\;' . '|use [A-Z][a-zA-Z0-9_\\\\]+ as (?<aliasClass>[A-Z][a-zA-Z0-9]+)' . '/m'; $result2 = []; preg_match_all($staticCallPattern, $contents, $result2); // Annotation, for example: "* @return \Magento\Foo\Bar" or "* @throws Exception" or "* @return Foo" $annotationPattern = '/^' . '[\s]*\*\s\@(?:return|throws)\s(?<venderClass>\\\\Magento(?:\\\\[a-zA-Z0-9_]+)+)' . '|[\s]*\*\s\@return\s(?<badClass>[A-Z][a-zA-Z0-9_\\\\]+)' . '|[\s]*\*\s\@throws\s(?<exception>[A-Z][a-zA-Z0-9_\\\\]+)' . '|use [A-Z][a-zA-Z0-9_\\\\]+ as (?<aliasClass>[A-Z][a-zA-Z0-9]+)' . '/m'; $result3 = []; preg_match_all($annotationPattern, $contents, $result3); $vendorClasses = array_unique( array_merge_recursive($result1['venderClass'], $result2['venderClass'], $result3['venderClass']) ); $badClasses = array_unique( array_merge_recursive($result1['badClass'], $result2['badClass'], $result3['badClass']) ); $aliasClasses = array_unique( array_merge_recursive($result1['aliasClass'], $result2['aliasClass'], $result3['aliasClass']) ); $vendorClasses = array_filter($vendorClasses, 'strlen'); $vendorClasses = $this->excludedReferenceFilter($vendorClasses); if (!empty($vendorClasses)) { $this->assertClassesExist($vendorClasses, $file); } if (!empty($result3['exception']) && $result3['exception'][0] != "") { $badClasses = array_merge($badClasses, array_filter($result3['exception'], 'strlen')); } $badClasses = array_filter($badClasses, 'strlen'); if (empty($badClasses)) { return; } $aliasClasses = array_filter($aliasClasses, 'strlen'); if (!empty($aliasClasses)) { $badClasses = $this->handleAliasClasses($aliasClasses, $badClasses); } $badClasses = $this->excludedReferenceFilter($badClasses); $badClasses = $this->removeSpecialCases($badClasses, $file, $contents, $namespacePath); $this->assertClassReferences($badClasses, $file); }, Files::init()->getPhpFiles() ); } /** * Remove alias class name references that have been identified as 'bad'. * * @param array $aliasClasses * @param array $badClasses * @return array */ private function handleAliasClasses(array $aliasClasses, array $badClasses): array { foreach ($aliasClasses as $aliasClass) { foreach ($badClasses as $badClass) { if (strpos($badClass, $aliasClass) === 0) { unset($badClasses[array_search($badClass, $badClasses)]); } } } return $badClasses; } /** * This function is to remove legacy code usages according to _files/blacklist/reference.txt * * @param array $classes * @return array */ private function excludedReferenceFilter(array $classes): array { // exceptions made for the files from the exclusion $excludeClasses = $this->getExcludedReferences(); foreach ($classes as $class) { if (in_array($class, $excludeClasses)) { unset($classes[array_search($class, $classes)]); } } return $classes; } /** * Returns array of class names from black list. * * @return array */ private function getExcludedReferences(): array { if (!isset($this->excludeReference)) { $this->excludeReference = file( __DIR__ . '/_files/blacklist/reference.txt', FILE_IGNORE_NEW_LINES ); } return $this->excludeReference; } /** * This function is to remove special cases (if any) from the list of found bad classes * * @param array $badClasses * @param string $file * @param string $contents * @param string $namespacePath * @return array */ private function removeSpecialCases(array $badClasses, string $file, string $contents, string $namespacePath): array { foreach ($badClasses as $badClass) { // Remove valid usages of Magento modules from the list // for example: 'Magento_Sales::actions_edit' if (preg_match('/^[A-Z][a-z]+_[A-Z0-9][a-z0-9]+$/', $badClass)) { $moduleDir = $this->componentRegistrar->getPath(ComponentRegistrar::MODULE, $badClass); if ($moduleDir !== null) { unset($badClasses[array_search($badClass, $badClasses)]); continue; } } // Remove usage of key words such as "Array", "String", and "Boolean" if (in_array($badClass, self::$excludeKeywords)) { unset($badClasses[array_search($badClass, $badClasses)]); continue; } $classParts = explode('/', $file); $className = array_pop($classParts); // Remove usage of the class itself from the list if ($badClass . '.php' == $className) { unset($badClasses[array_search($badClass, $badClasses)]); continue; } if ($this->removeSpecialCasesNonFullyQualifiedClassNames($namespacePath, $badClasses, $badClass)) { continue; } $referenceFile = implode('/', $classParts) . '/' . str_replace('\\', '/', $badClass) . '.php'; if (file_exists($referenceFile)) { unset($badClasses[array_search($badClass, $badClasses)]); continue; } // Remove usage of classes that have been declared as "use" or "include" // Also deals with case like: "use \Laminas\Code\Scanner\FileScanner, Magento\Tools\Di\Compiler\Log\Log;" // (continued) where there is a comma separating two different classes. if (preg_match('/use\s.*[\\n]?.*' . str_replace('\\', '\\\\', $badClass) . '[\,\;]/', $contents)) { unset($badClasses[array_search($badClass, $badClasses)]); continue; } } return $badClasses; } /** * Helper class for removeSpecialCases to remove classes that do not use fully-qualified class names * * @param string $namespacePath * @param array $badClasses * @param string $badClass * @return bool * @throws \Exception */ private function removeSpecialCasesNonFullyQualifiedClassNames($namespacePath, &$badClasses, $badClass) { $namespaceParts = explode('/', $namespacePath); $moduleDir = null; if (isset($namespaceParts[1])) { $moduleName = array_shift($namespaceParts) . '_' . array_shift($namespaceParts); $moduleDir = $this->componentRegistrar->getPath(ComponentRegistrar::MODULE, $moduleName); } if ($moduleDir) { $fullPath = $moduleDir . '/' . implode('/', $namespaceParts) . '/' . str_replace('\\', '/', $badClass) . '.php'; if (file_exists($fullPath)) { unset($badClasses[array_search($badClass, $badClasses)]); return true; } } $fullPath = $this->getLibraryDirByPath($namespacePath, $badClass); if ($fullPath && file_exists($fullPath)) { unset($badClasses[array_search($badClass, $badClasses)]); return true; } else { return $this->removeSpecialCasesForAllOthers($namespacePath, $badClass, $badClasses); } } /** * Get path to the file in the library based on namespace path * * @param string $namespacePath * @param string $badClass * @return null|string */ private function getLibraryDirByPath(string $namespacePath, string $badClass) { $libraryDir = null; $fullPath = null; $namespaceParts = explode('/', $namespacePath); if (isset($namespaceParts[1]) && $namespaceParts[1]) { $vendor = array_shift($namespaceParts); $lib = array_shift($namespaceParts); if ($lib == 'framework') { $subLib = $namespaceParts[0]; $subLib = strtolower(preg_replace('/(.)([A-Z])/', "$1-$2", $subLib)); $libraryName = $vendor . '/' . $lib . '-' . $subLib; $libraryDir = $this->componentRegistrar->getPath( ComponentRegistrar::LIBRARY, strtolower($libraryName) ); if ($libraryDir) { array_shift($namespaceParts); } else { $libraryName = $vendor . '/' . $lib; $libraryDir = $this->componentRegistrar->getPath( ComponentRegistrar::LIBRARY, strtolower($libraryName) ); } } else { $lib = strtolower(preg_replace('/(.)([A-Z])/', "$1-$2", $lib)); $libraryName = $vendor . '/' . $lib; $libraryDir = $this->componentRegistrar->getPath( ComponentRegistrar::LIBRARY, strtolower($libraryName) ); } } if ($libraryDir) { $fullPath = $libraryDir . '/' . implode('/', $namespaceParts) . '/' . str_replace('\\', '/', $badClass) . '.php'; } return $fullPath; } /** * @param string $namespacePath * @param string $badClass * @param array $badClasses * @return bool */ private function removeSpecialCasesForAllOthers(string $namespacePath, string $badClass, array &$badClasses): bool { // Remove usage of classes that do NOT using fully-qualified class names (possibly under same namespace) $directories = [ BP . '/dev/tools/', BP . '/dev/tests/api-functional/framework/', BP . '/dev/tests/integration/framework/', BP . '/dev/tests/integration/framework/tests/unit/testsuite/', BP . '/dev/tests/integration/testsuite/', BP . '/dev/tests/integration/testsuite/Magento/Test/Integrity/', BP . '/dev/tests/static/framework/', BP . '/dev/tests/static/testsuite/', BP . '/setup/src/', ]; $libraryPaths = $this->componentRegistrar->getPaths(ComponentRegistrar::LIBRARY); $directories = array_merge($directories, $libraryPaths); // Full list of directories where there may be namespace classes foreach ($directories as $directory) { $fullPath = $directory . $namespacePath . '/' . str_replace('\\', '/', $badClass) . '.php'; if (file_exists($fullPath)) { unset($badClasses[array_search($badClass, $badClasses)]); return true; } } return false; } /** * Assert any found class name resolves into a file name and corresponds to an existing file * * @param array $badClasses * @param string $file * @return void */ private function assertClassReferences(array $badClasses, string $file): void { if (empty($badClasses)) { return; } $this->fail("Incorrect namespace usage(s) found in file {$file}:\n" . implode("\n", $badClasses)); } public function testCoversAnnotation() { $files = Files::init(); $errors = []; $filesToTest = $files->getPhpFiles(Files::INCLUDE_TESTS); if (($key = array_search(str_replace('\\', '/', __FILE__), $filesToTest)) !== false) { unset($filesToTest[$key]); } foreach ($filesToTest as $file) { $code = file_get_contents($file); if (preg_match('/@covers(DefaultClass)?\s+([\w\\\\]+)(::([\w\\\\]+))?/', $code, $matches)) { if ($this->isNonexistentEntityCovered($matches)) { $errors[] = $file . ': ' . $matches[0]; } } } if ($errors) { $this->fail( 'Nonexistent classes/methods were found in @covers annotations: ' . PHP_EOL . implode(PHP_EOL, $errors) ); } } /** * @param array $matches * @return bool */ private function isNonexistentEntityCovered($matches) { return !empty($matches[2]) && !class_exists($matches[2]) || !empty($matches[4]) && !method_exists($matches[2], $matches[4]); } }