688 lines
25 KiB
PHP
Executable File
688 lines
25 KiB
PHP
Executable File
<?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]);
|
|
}
|
|
}
|