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]);
 | 
						|
    }
 | 
						|
}
 |