123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686 |
- <?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 $keywordsBlacklist = ["String", "Array", "Boolean", "Element"];
- /**
- * @var array|null
- */
- private $referenceBlackList = null;
- /**
- * Set Up
- */
- protected function setUp()
- {
- $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()
- {
- $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->referenceBlacklistFilter($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->referenceBlacklistFilter($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 referenceBlacklistFilter(array $classes): array
- {
- // exceptions made for the files from the blacklist
- $classes = $this->getReferenceBlacklist();
- foreach ($classes as $class) {
- if (in_array($class, $this->referenceBlackList)) {
- unset($classes[array_search($class, $classes)]);
- }
- }
- return $classes;
- }
- /**
- * Returns array of class names from black list.
- *
- * @return array
- */
- private function getReferenceBlacklist(): array
- {
- if (!isset($this->referenceBlackList)) {
- $this->referenceBlackList = file(
- __DIR__ . '/_files/blacklist/reference.txt',
- FILE_IGNORE_NEW_LINES
- );
- }
- return $this->referenceBlackList;
- }
- /**
- * 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::$keywordsBlacklist)) {
- 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 \Zend\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/functional/',
- 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]);
- }
- }
|