419 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			PHP
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			419 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			PHP
		
	
	
		
			Executable File
		
	
	
<?php
 | 
						|
/**
 | 
						|
 * Compiler test. Check compilation of DI definitions and code generation
 | 
						|
 *
 | 
						|
 * Copyright © Magento, Inc. All rights reserved.
 | 
						|
 * See COPYING.txt for license details.
 | 
						|
 */
 | 
						|
namespace Magento\Test\Integrity\Di;
 | 
						|
 | 
						|
use Magento\Framework\Api\Code\Generator\Mapper;
 | 
						|
use Magento\Framework\Api\Code\Generator\SearchResults;
 | 
						|
use Magento\Framework\App\Filesystem\DirectoryList;
 | 
						|
use Magento\Framework\Component\ComponentRegistrar;
 | 
						|
use Magento\Framework\Interception\Code\InterfaceValidator;
 | 
						|
use Magento\Framework\ObjectManager\Code\Generator\Converter;
 | 
						|
use Magento\Framework\ObjectManager\Code\Generator\Factory;
 | 
						|
use Magento\Framework\ObjectManager\Code\Generator\Repository;
 | 
						|
use Magento\Framework\Api\Code\Generator\ExtensionAttributesInterfaceGenerator;
 | 
						|
use Magento\Framework\Api\Code\Generator\ExtensionAttributesGenerator;
 | 
						|
use Magento\Framework\App\Utility\Files;
 | 
						|
use Magento\TestFramework\Integrity\PluginValidator;
 | 
						|
 | 
						|
/**
 | 
						|
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 | 
						|
 */
 | 
						|
class CompilerTest extends \PHPUnit\Framework\TestCase
 | 
						|
{
 | 
						|
    /**
 | 
						|
     * @var string
 | 
						|
     */
 | 
						|
    protected $_command;
 | 
						|
 | 
						|
    /**
 | 
						|
     * @var \Magento\Framework\Shell
 | 
						|
     */
 | 
						|
    protected $_shell;
 | 
						|
 | 
						|
    /**
 | 
						|
     * @var string
 | 
						|
     */
 | 
						|
    protected $_generationDir;
 | 
						|
 | 
						|
    /**
 | 
						|
     * @var string
 | 
						|
     */
 | 
						|
    protected $_compilationDir;
 | 
						|
 | 
						|
    /**
 | 
						|
     * @var \Magento\Framework\ObjectManager\Config\Mapper\Dom()
 | 
						|
     */
 | 
						|
    protected $_mapper;
 | 
						|
 | 
						|
    /**
 | 
						|
     * @var \Magento\Framework\Code\Validator
 | 
						|
     */
 | 
						|
    protected $_validator;
 | 
						|
 | 
						|
    /**
 | 
						|
     * Class arguments reader
 | 
						|
     *
 | 
						|
     * @var PluginValidator
 | 
						|
     */
 | 
						|
    protected $pluginValidator;
 | 
						|
 | 
						|
    /**
 | 
						|
     * @var string[]|null
 | 
						|
     */
 | 
						|
    private $pluginBlacklist;
 | 
						|
 | 
						|
    protected function setUp(): void
 | 
						|
    {
 | 
						|
        $this->_shell = new \Magento\Framework\Shell(new \Magento\Framework\Shell\CommandRenderer());
 | 
						|
        $basePath = BP;
 | 
						|
        $basePath = str_replace('\\', '/', $basePath);
 | 
						|
 | 
						|
        $directoryList = new DirectoryList($basePath);
 | 
						|
        $this->_generationDir = $directoryList->getPath(DirectoryList::GENERATED_CODE);
 | 
						|
        $this->_compilationDir = $directoryList->getPath(DirectoryList::GENERATED_METADATA);
 | 
						|
 | 
						|
        $this->_command = 'php ' . $basePath . '/bin/magento setup:di:compile';
 | 
						|
 | 
						|
        $booleanUtils = new \Magento\Framework\Stdlib\BooleanUtils();
 | 
						|
        $constInterpreter = new \Magento\Framework\Data\Argument\Interpreter\Constant();
 | 
						|
        $argumentInterpreter = new \Magento\Framework\Data\Argument\Interpreter\Composite(
 | 
						|
            [
 | 
						|
                'boolean' => new \Magento\Framework\Data\Argument\Interpreter\Boolean($booleanUtils),
 | 
						|
                'string' => new \Magento\Framework\Data\Argument\Interpreter\BaseStringUtils($booleanUtils),
 | 
						|
                'number' => new \Magento\Framework\Data\Argument\Interpreter\Number(),
 | 
						|
                'null' => new \Magento\Framework\Data\Argument\Interpreter\NullType(),
 | 
						|
                'object' => new \Magento\Framework\Data\Argument\Interpreter\DataObject($booleanUtils),
 | 
						|
                'const' => $constInterpreter,
 | 
						|
                'init_parameter' => new \Magento\Framework\App\Arguments\ArgumentInterpreter($constInterpreter),
 | 
						|
            ],
 | 
						|
            \Magento\Framework\ObjectManager\Config\Reader\Dom::TYPE_ATTRIBUTE
 | 
						|
        );
 | 
						|
        // Add interpreters that reference the composite
 | 
						|
        $argumentInterpreter->addInterpreter(
 | 
						|
            'array',
 | 
						|
            new \Magento\Framework\Data\Argument\Interpreter\ArrayType($argumentInterpreter)
 | 
						|
        );
 | 
						|
 | 
						|
        $this->_mapper = new \Magento\Framework\ObjectManager\Config\Mapper\Dom(
 | 
						|
            $argumentInterpreter,
 | 
						|
            $booleanUtils,
 | 
						|
            new \Magento\Framework\ObjectManager\Config\Mapper\ArgumentParser()
 | 
						|
        );
 | 
						|
        $this->_validator = new \Magento\Framework\Code\Validator();
 | 
						|
        $this->_validator->add(new \Magento\Framework\Code\Validator\ConstructorIntegrity());
 | 
						|
        $this->_validator->add(new \Magento\Framework\Code\Validator\TypeDuplication());
 | 
						|
        $this->_validator->add(new \Magento\Framework\Code\Validator\ArgumentSequence());
 | 
						|
        $this->_validator->add(new \Magento\Framework\Code\Validator\ConstructorArgumentTypes());
 | 
						|
        $this->pluginValidator = new PluginValidator(new InterfaceValidator());
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Return plugin blacklist class names
 | 
						|
     *
 | 
						|
     * @return string[]
 | 
						|
     */
 | 
						|
    private function getPluginBlacklist(): array
 | 
						|
    {
 | 
						|
        if ($this->pluginBlacklist === null) {
 | 
						|
            $blacklistFiles = str_replace(
 | 
						|
                '\\',
 | 
						|
                '/',
 | 
						|
                realpath(__DIR__) . '/../_files/blacklist/compiler_plugins*.txt'
 | 
						|
            );
 | 
						|
            $blacklistItems = [];
 | 
						|
            foreach (glob($blacklistFiles) as $fileName) {
 | 
						|
                $blacklistItems[] = file($fileName, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
 | 
						|
            }
 | 
						|
            $blacklistItems = array_merge([], ...$blacklistItems);
 | 
						|
            $this->pluginBlacklist = $blacklistItems;
 | 
						|
        }
 | 
						|
        return $this->pluginBlacklist;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Validate DI config file
 | 
						|
     *
 | 
						|
     * @param string $file
 | 
						|
     */
 | 
						|
    protected function _validateFile($file)
 | 
						|
    {
 | 
						|
        $dom = new \DOMDocument();
 | 
						|
        $dom->load($file);
 | 
						|
        $data = $this->_mapper->convert($dom);
 | 
						|
 | 
						|
        foreach ($data as $instanceName => $parameters) {
 | 
						|
            if (!isset($parameters['parameters']) || empty($parameters['parameters'])) {
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
            if (\Magento\Framework\App\Utility\Classes::isVirtual($instanceName)) {
 | 
						|
                $instanceName = \Magento\Framework\App\Utility\Classes::resolveVirtualType($instanceName);
 | 
						|
            }
 | 
						|
 | 
						|
            if (!$this->_classExistsAsReal($instanceName)) {
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
 | 
						|
            $reflectionClass = new \ReflectionClass($instanceName);
 | 
						|
 | 
						|
            $constructor = $reflectionClass->getConstructor();
 | 
						|
            if (!$constructor) {
 | 
						|
                $this->fail('Class ' . $instanceName . ' does not have __constructor');
 | 
						|
            }
 | 
						|
 | 
						|
            $parameters = $parameters['parameters'];
 | 
						|
            $classParameters = $constructor->getParameters();
 | 
						|
            foreach ($classParameters as $classParameter) {
 | 
						|
                $parameterName = $classParameter->getName();
 | 
						|
                if (array_key_exists($parameterName, $parameters)) {
 | 
						|
                    unset($parameters[$parameterName]);
 | 
						|
                }
 | 
						|
            }
 | 
						|
            $message = 'Configuration of ' . $instanceName . ' contains data for non-existed parameters: ' . implode(
 | 
						|
                ', ',
 | 
						|
                array_keys($parameters)
 | 
						|
            );
 | 
						|
            $this->assertEmpty($parameters, $message);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Checks if class is a real one or generated Factory
 | 
						|
     * @param string $instanceName class name
 | 
						|
     * @throws \PHPUnit\Framework\AssertionFailedError
 | 
						|
     * @return bool
 | 
						|
     */
 | 
						|
    protected function _classExistsAsReal($instanceName)
 | 
						|
    {
 | 
						|
        if (class_exists($instanceName)) {
 | 
						|
            return true;
 | 
						|
        }
 | 
						|
        // check for generated factory
 | 
						|
        if (substr($instanceName, -7) == 'Factory' && class_exists(substr($instanceName, 0, -7))) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        $this->fail('Detected configuration of non existed class: ' . $instanceName);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Get php classes list
 | 
						|
     *
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    protected function _phpClassesDataProvider()
 | 
						|
    {
 | 
						|
        $generationPath = str_replace('/', '\\', $this->_generationDir);
 | 
						|
 | 
						|
        $files = Files::init()->getPhpFiles(Files::INCLUDE_APP_CODE | Files::INCLUDE_LIBS);
 | 
						|
 | 
						|
        $patterns = ['/' . preg_quote($generationPath) . '/',];
 | 
						|
        $replacements = [''];
 | 
						|
 | 
						|
        $componentRegistrar = new ComponentRegistrar();
 | 
						|
        foreach ($componentRegistrar->getPaths(ComponentRegistrar::MODULE) as $moduleName => $modulePath) {
 | 
						|
            $patterns[] = '/' . preg_quote(str_replace('/', '\\', $modulePath)) . '/';
 | 
						|
            $replacements[] = '\\' . str_replace('_', '\\', $moduleName);
 | 
						|
        }
 | 
						|
 | 
						|
        foreach ($componentRegistrar->getPaths(ComponentRegistrar::LIBRARY) as $libPath) {
 | 
						|
            $patterns[] = '/' . preg_quote(str_replace('/', '\\', $libPath)) . '/';
 | 
						|
            $replacements[] = '\\Magento\\Framework';
 | 
						|
        }
 | 
						|
 | 
						|
        /** Convert file names into class name format */
 | 
						|
        $classes = [];
 | 
						|
        foreach ($files as $file) {
 | 
						|
            $file = str_replace('/', '\\', $file);
 | 
						|
            $filePath = preg_replace($patterns, $replacements, $file);
 | 
						|
            $className = substr($filePath, 0, -4);
 | 
						|
            if (class_exists($className, false)) {
 | 
						|
                $file = str_replace('\\', DIRECTORY_SEPARATOR, $file);
 | 
						|
                $classes[$file] = $className;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        /** Build class inheritance hierarchy  */
 | 
						|
        $output = [];
 | 
						|
        $allowedFiles = array_keys($classes);
 | 
						|
        foreach ($classes as $class) {
 | 
						|
            if (!in_array($class, $output)) {
 | 
						|
                $output[] = $this->_buildInheritanceHierarchyTree($class, $allowedFiles);
 | 
						|
            }
 | 
						|
        }
 | 
						|
        $output = array_unique(array_merge([], ...$output));
 | 
						|
 | 
						|
        /** Convert data into data provider format */
 | 
						|
        $outputClasses = [];
 | 
						|
        foreach ($output as $className) {
 | 
						|
            $outputClasses[] = [$className];
 | 
						|
        }
 | 
						|
        return $outputClasses;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Build inheritance hierarchy tree
 | 
						|
     *
 | 
						|
     * @param string $className
 | 
						|
     * @param array $allowedFiles
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    protected function _buildInheritanceHierarchyTree($className, array $allowedFiles)
 | 
						|
    {
 | 
						|
        $output = [];
 | 
						|
        if (0 !== strpos($className, '\\')) {
 | 
						|
            $className = '\\' . $className;
 | 
						|
        }
 | 
						|
        $class = new \ReflectionClass($className);
 | 
						|
        $parent = $class->getParentClass();
 | 
						|
        $file = false;
 | 
						|
        if ($parent) {
 | 
						|
            $file = str_replace('\\', DIRECTORY_SEPARATOR, $parent->getFileName());
 | 
						|
        }
 | 
						|
        /** Prevent analysis of non Magento classes  */
 | 
						|
        if ($parent && in_array($file, $allowedFiles)) {
 | 
						|
            $output = array_merge(
 | 
						|
                $this->_buildInheritanceHierarchyTree($parent->getName(), $allowedFiles),
 | 
						|
                [$className],
 | 
						|
                $output
 | 
						|
            );
 | 
						|
        } else {
 | 
						|
            $output[] = $className;
 | 
						|
        }
 | 
						|
        return array_unique($output);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Validate class
 | 
						|
     *
 | 
						|
     * @param string $className
 | 
						|
     */
 | 
						|
    protected function _validateClass($className)
 | 
						|
    {
 | 
						|
        try {
 | 
						|
            $this->_validator->validate($className);
 | 
						|
        } catch (\Magento\Framework\Exception\ValidatorException $exceptions) {
 | 
						|
            $this->fail($exceptions->getMessage());
 | 
						|
        } catch (\ReflectionException $exceptions) {
 | 
						|
            $this->fail($exceptions->getMessage());
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Validate DI configuration
 | 
						|
     */
 | 
						|
    public function testConfigurationOfInstanceParameters()
 | 
						|
    {
 | 
						|
        $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this);
 | 
						|
        $invoker(
 | 
						|
            function ($file) {
 | 
						|
                $this->_validateFile($file);
 | 
						|
            },
 | 
						|
            Files::init()->getDiConfigs(true)
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Validate constructor integrity
 | 
						|
     */
 | 
						|
    public function testConstructorIntegrity()
 | 
						|
    {
 | 
						|
        $generatorIo = new \Magento\Framework\Code\Generator\Io(
 | 
						|
            new \Magento\Framework\Filesystem\Driver\File(),
 | 
						|
            $this->_generationDir
 | 
						|
        );
 | 
						|
        $generator = new \Magento\Framework\Code\Generator(
 | 
						|
            $generatorIo,
 | 
						|
            [
 | 
						|
                Factory::ENTITY_TYPE => \Magento\Framework\ObjectManager\Code\Generator\Factory::class,
 | 
						|
                Repository::ENTITY_TYPE => \Magento\Framework\ObjectManager\Code\Generator\Repository::class,
 | 
						|
                Converter::ENTITY_TYPE => \Magento\Framework\ObjectManager\Code\Generator\Converter::class,
 | 
						|
                Mapper::ENTITY_TYPE => \Magento\Framework\Api\Code\Generator\Mapper::class,
 | 
						|
                SearchResults::ENTITY_TYPE => \Magento\Framework\Api\Code\Generator\SearchResults::class,
 | 
						|
                ExtensionAttributesInterfaceGenerator::ENTITY_TYPE =>
 | 
						|
                    \Magento\Framework\Api\Code\Generator\ExtensionAttributesInterfaceGenerator::class,
 | 
						|
                ExtensionAttributesGenerator::ENTITY_TYPE =>
 | 
						|
                    \Magento\Framework\Api\Code\Generator\ExtensionAttributesGenerator::class
 | 
						|
            ]
 | 
						|
        );
 | 
						|
        $generationAutoloader = new \Magento\Framework\Code\Generator\Autoloader($generator);
 | 
						|
        spl_autoload_register([$generationAutoloader, 'load']);
 | 
						|
 | 
						|
        $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this);
 | 
						|
        $invoker(
 | 
						|
            function ($className) {
 | 
						|
                $this->_validateClass($className);
 | 
						|
            },
 | 
						|
            $this->_phpClassesDataProvider()
 | 
						|
        );
 | 
						|
        spl_autoload_unregister([$generationAutoloader, 'load']);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Test consistency of plugin interfaces
 | 
						|
     */
 | 
						|
    public function testPluginInterfaces()
 | 
						|
    {
 | 
						|
        $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this);
 | 
						|
        $invoker(
 | 
						|
            function ($plugin, $type) {
 | 
						|
                $this->validatePlugins($plugin, $type);
 | 
						|
            },
 | 
						|
            $this->pluginDataProvider()
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Validate plugin interface
 | 
						|
     *
 | 
						|
     * @param string $plugin
 | 
						|
     * @param string $type
 | 
						|
     */
 | 
						|
    protected function validatePlugins($plugin, $type)
 | 
						|
    {
 | 
						|
        try {
 | 
						|
            $module = \Magento\Framework\App\Utility\Classes::getClassModuleName($type);
 | 
						|
            if (Files::init()->isModuleExists($module)) {
 | 
						|
                $this->pluginValidator->validate($plugin, $type);
 | 
						|
            }
 | 
						|
        } catch (\Magento\Framework\Exception\ValidatorException $exception) {
 | 
						|
            $this->fail($exception->getMessage());
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Get application plugins
 | 
						|
     *
 | 
						|
     * @return array
 | 
						|
     * @throws \Exception
 | 
						|
     */
 | 
						|
    protected function pluginDataProvider()
 | 
						|
    {
 | 
						|
        $files = Files::init()->getDiConfigs();
 | 
						|
        $plugins = [];
 | 
						|
        foreach ($files as $file) {
 | 
						|
            $dom = new \DOMDocument();
 | 
						|
            $dom->load($file);
 | 
						|
            $xpath = new \DOMXPath($dom);
 | 
						|
            $pluginList = $xpath->query('//config/type/plugin');
 | 
						|
            foreach ($pluginList as $node) {
 | 
						|
                /** @var $node \DOMNode */
 | 
						|
                $type = $node->parentNode->attributes->getNamedItem('name')->nodeValue;
 | 
						|
                $type = \Magento\Framework\App\Utility\Classes::resolveVirtualType($type);
 | 
						|
                if ($node->attributes->getNamedItem('type')) {
 | 
						|
                    $plugin = $node->attributes->getNamedItem('type')->nodeValue;
 | 
						|
                    if (!in_array($plugin, $this->getPluginBlacklist())) {
 | 
						|
                        $plugin = \Magento\Framework\App\Utility\Classes::resolveVirtualType($plugin);
 | 
						|
                        $plugins[] = [$plugin, $type];
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return $plugins;
 | 
						|
    }
 | 
						|
}
 |