1342 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			PHP
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			1342 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			PHP
		
	
	
		
			Executable File
		
	
	
| <?php
 | |
| /**
 | |
|  * Copyright © Magento, Inc. All rights reserved.
 | |
|  * See COPYING.txt for license details.
 | |
|  */
 | |
| namespace Magento\Test\Integrity;
 | |
| 
 | |
| use Magento\Framework\App\Bootstrap;
 | |
| use Magento\Framework\App\Utility\Files;
 | |
| use Magento\Framework\Component\ComponentRegistrar;
 | |
| use Magento\Framework\Config\Reader\Filesystem as Reader;
 | |
| use Magento\Framework\Config\ValidationState\Configurable;
 | |
| use Magento\Framework\Exception\LocalizedException;
 | |
| use Magento\Test\Integrity\Dependency\Converter;
 | |
| use Magento\Test\Integrity\Dependency\DeclarativeSchemaDependencyProvider;
 | |
| use Magento\Test\Integrity\Dependency\GraphQlSchemaDependencyProvider;
 | |
| use Magento\Test\Integrity\Dependency\SchemaLocator;
 | |
| use Magento\Test\Integrity\Dependency\WebapiFileResolver;
 | |
| use Magento\TestFramework\Dependency\AnalyticsConfigRule;
 | |
| use Magento\TestFramework\Dependency\DbRule;
 | |
| use Magento\TestFramework\Dependency\DiRule;
 | |
| use Magento\TestFramework\Dependency\LayoutRule;
 | |
| use Magento\TestFramework\Dependency\PhpRule;
 | |
| use Magento\TestFramework\Dependency\ReportsConfigRule;
 | |
| use Magento\TestFramework\Dependency\Route\RouteMapper;
 | |
| use Magento\TestFramework\Dependency\VirtualType\VirtualTypeMapper;
 | |
| 
 | |
| /**
 | |
|  * Scan source code for incorrect or undeclared modules dependencies
 | |
|  *
 | |
|  * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
 | |
|  * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 | |
|  * @SuppressWarnings(PHPMD.TooManyFields)
 | |
|  */
 | |
| class DependencyTest extends \PHPUnit\Framework\TestCase
 | |
| {
 | |
|     /**
 | |
|      * Soft dependency between modules
 | |
|      */
 | |
|     public const TYPE_SOFT = 'soft';
 | |
| 
 | |
|     /**
 | |
|      * Hard dependency between modules
 | |
|      */
 | |
|     public const TYPE_HARD = 'hard';
 | |
| 
 | |
|     /**
 | |
|      * The identifier of dependency for mapping.
 | |
|      */
 | |
|     public const MAP_TYPE_DECLARED = 'declared';
 | |
| 
 | |
|     /**
 | |
|      * The identifier of dependency for mapping.
 | |
|      */
 | |
|     public const MAP_TYPE_FOUND = 'found';
 | |
| 
 | |
|     /**
 | |
|      * The identifier of dependency for mapping.
 | |
|      */
 | |
|     public const MAP_TYPE_REDUNDANT = 'redundant';
 | |
| 
 | |
|     /**
 | |
|      * Count of directories in path
 | |
|      */
 | |
|     public const DIR_PATH_COUNT = 4;
 | |
| 
 | |
|     /**
 | |
|      * List of config.xml files by modules
 | |
|      *
 | |
|      * Format: array(
 | |
|      *  '{Module_Name}' => '{Filename}'
 | |
|      * )
 | |
|      *
 | |
|      * @var array
 | |
|      */
 | |
|     protected static $_listConfigXml = [];
 | |
| 
 | |
|     /**
 | |
|      * List of analytics.xml
 | |
|      *
 | |
|      * Format: array(
 | |
|      *  '{Module_Name}' => '{Filename}'
 | |
|      * )
 | |
|      *
 | |
|      * @var array
 | |
|      */
 | |
|     protected static $_listAnalyticsXml = [];
 | |
| 
 | |
|     /**
 | |
|      * List of layout blocks
 | |
|      *
 | |
|      * Format: array(
 | |
|      *  '{Area}' => array(
 | |
|      *   '{Block_Name}' => array('{Module_Name}' => '{Module_Name}')
 | |
|      * ))
 | |
|      *
 | |
|      * @var array
 | |
|      */
 | |
|     protected static $_mapLayoutBlocks = [];
 | |
| 
 | |
|     /**
 | |
|      * List of layout handles
 | |
|      *
 | |
|      * Format: array(
 | |
|      *  '{Area}' => array(
 | |
|      *   '{Handle_Name}' => array('{Module_Name}' => '{Module_Name}')
 | |
|      * ))
 | |
|      *
 | |
|      * @var array
 | |
|      */
 | |
|     protected static $_mapLayoutHandles = [];
 | |
| 
 | |
|     /**
 | |
|      * List of dependencies
 | |
|      *
 | |
|      * Format: array(
 | |
|      *  '{Module_Name}' => array(
 | |
|      *   '{Type}' => array(
 | |
|      *    'declared'  = array('{Dependency}', ...)
 | |
|      *    'found'     = array('{Dependency}', ...)
 | |
|      *    'redundant' = array('{Dependency}', ...)
 | |
|      * )))
 | |
|      * @var array
 | |
|      */
 | |
|     protected static $mapDependencies = [];
 | |
| 
 | |
|     /**
 | |
|      * Regex pattern for validation file path of theme
 | |
|      *
 | |
|      * @var string
 | |
|      */
 | |
|     protected static $_defaultThemes = '';
 | |
| 
 | |
|     /**
 | |
|      * Namespaces to analyze
 | |
|      *
 | |
|      * Format: {Namespace}|{Namespace}|...
 | |
|      *
 | |
|      * @var string
 | |
|      */
 | |
|     protected static $_namespaces;
 | |
| 
 | |
|     /**
 | |
|      * Rule instances
 | |
|      *
 | |
|      * @var array
 | |
|      */
 | |
|     protected static $_rulesInstances = [];
 | |
| 
 | |
|     /**
 | |
|      * White list for libraries
 | |
|      *
 | |
|      * @var array
 | |
|      */
 | |
|     private static $whiteList = [];
 | |
| 
 | |
|     /**
 | |
|      * @var array|null
 | |
|      */
 | |
|     private static $routesWhitelist = null;
 | |
| 
 | |
|     /**
 | |
|      * @var array|null
 | |
|      */
 | |
|     private static $redundantDependenciesWhitelist = null;
 | |
| 
 | |
|     /**
 | |
|      * @var RouteMapper
 | |
|      */
 | |
|     private static $routeMapper = null;
 | |
| 
 | |
|     /**
 | |
|      * @var ComponentRegistrar
 | |
|      */
 | |
|     private static $componentRegistrar = null;
 | |
| 
 | |
|     /**
 | |
|      * @var array
 | |
|      */
 | |
|     private $externalDependencyBlacklist;
 | |
| 
 | |
|     /**
 | |
|      * @var array
 | |
|      */
 | |
|     private $undeclaredDependencyBlacklist;
 | |
| 
 | |
|     /**
 | |
|      * @var array|null
 | |
|      */
 | |
|     private static $extensionConflicts = null;
 | |
| 
 | |
|     /**
 | |
|      * @var array|null
 | |
|      */
 | |
|     private static $allowedDependencies = null;
 | |
| 
 | |
|     /**
 | |
|      * Sets up data
 | |
|      *
 | |
|      * @throws \Exception
 | |
|      */
 | |
|     public static function setUpBeforeClass(): void
 | |
|     {
 | |
|         $root = BP;
 | |
|         $rootJson = json_decode(file_get_contents($root . '/composer.json'), true);
 | |
|         if (preg_match('/magento\/project-*/', $rootJson['name']) == 1) {
 | |
|             // The Dependency test is skipped for vendor/magento build
 | |
|             self::markTestSkipped(
 | |
|                 'MAGETWO-43654: The build is running from vendor/magento. DependencyTest is skipped.'
 | |
|             );
 | |
|         }
 | |
| 
 | |
|         self::$routeMapper = new RouteMapper();
 | |
|         self::$_namespaces = implode('|', Files::init()->getNamespaces());
 | |
| 
 | |
|         self::_prepareListConfigXml();
 | |
|         self::_prepareListAnalyticsXml();
 | |
| 
 | |
|         self::_prepareMapLayoutBlocks();
 | |
|         self::_prepareMapLayoutHandles();
 | |
| 
 | |
|         self::getLibraryWhiteLists();
 | |
|         self::getRedundantDependenciesWhiteLists();
 | |
| 
 | |
|         self::_initDependencies();
 | |
|         self::_initThemes();
 | |
|         self::_initRules();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Initialize library white list
 | |
|      */
 | |
|     private static function getLibraryWhiteLists()
 | |
|     {
 | |
|         $componentRegistrar = new ComponentRegistrar();
 | |
|         foreach ($componentRegistrar->getPaths(ComponentRegistrar::LIBRARY) as $library) {
 | |
|             $library = str_replace('\\', '/', $library);
 | |
|             if (strpos($library, 'Framework/') !== false) {
 | |
|                 $partOfLibraryPath = explode('/', $library);
 | |
|                 self::$whiteList[] = implode('\\', array_slice($partOfLibraryPath, -3));
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Initialize redundant dependencies whitelist
 | |
|      *
 | |
|      * @return array
 | |
|      */
 | |
|     private static function getRedundantDependenciesWhiteLists(): array
 | |
|     {
 | |
|         if (is_null(self::$redundantDependenciesWhitelist)) {
 | |
|             $redundantDependenciesWhitelistFilePattern =
 | |
|                 realpath(__DIR__) . '/_files/dependency_test/whitelist/redundant_dependencies_*.php';
 | |
|             $redundantDependenciesWhitelist = [];
 | |
|             foreach (glob($redundantDependenciesWhitelistFilePattern) as $fileName) {
 | |
|                 $redundantDependenciesWhitelist[] = include $fileName;
 | |
|             }
 | |
|             self::$redundantDependenciesWhitelist = array_merge([], ...$redundantDependenciesWhitelist);
 | |
|         }
 | |
|         return self::$redundantDependenciesWhitelist;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Initialize default themes
 | |
|      */
 | |
|     protected static function _initThemes()
 | |
|     {
 | |
|         $defaultThemes = [];
 | |
|         foreach (self::$_listConfigXml as $file) {
 | |
|             $config = simplexml_load_file($file);
 | |
|             //phpcs:ignore Generic.PHP.NoSilencedErrors
 | |
|             $nodes = @($config->xpath("/config/*/design/theme/full_name") ?: []);
 | |
|             foreach ($nodes as $node) {
 | |
|                 $defaultThemes[] = (string)$node;
 | |
|             }
 | |
|         }
 | |
|         self::$_defaultThemes = sprintf('#app/design.*/(%s)/.*#', implode('|', array_unique($defaultThemes)));
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Create rules objects
 | |
|      *
 | |
|      * @throws \Exception
 | |
|      */
 | |
|     protected static function _initRules()
 | |
|     {
 | |
|         $tableToPrimaryModuleMap= self::getTableToPrimaryModuleMap();
 | |
|         $tableToAnyModuleMap = self::getTableToAnyModuleMap();
 | |
|         // In case primary module declaring the table cannot be identified, use any module referencing this table
 | |
|         $tableToModuleMap = array_merge($tableToAnyModuleMap, $tableToPrimaryModuleMap);
 | |
| 
 | |
|         $webApiConfigReader = new Reader(
 | |
|             new WebapiFileResolver(self::getComponentRegistrar()),
 | |
|             new Converter(),
 | |
|             new SchemaLocator(self::getComponentRegistrar()),
 | |
|             new Configurable(false),
 | |
|             'webapi.xml',
 | |
|             [
 | |
|                 '/routes/route' => ['url', 'method'],
 | |
|                 '/routes/route/resources/resource' => 'ref',
 | |
|                 '/routes/route/data/parameter' => 'name',
 | |
|             ],
 | |
|         );
 | |
| 
 | |
|         self::$_rulesInstances = [
 | |
|             new PhpRule(
 | |
|                 self::$routeMapper->getRoutes(),
 | |
|                 self::$_mapLayoutBlocks,
 | |
|                 $webApiConfigReader,
 | |
|                 [],
 | |
|                 ['routes' => self::getRoutesWhitelist()]
 | |
|             ),
 | |
|             new DbRule($tableToModuleMap),
 | |
|             new LayoutRule(
 | |
|                 self::$routeMapper->getRoutes(),
 | |
|                 self::$_mapLayoutBlocks,
 | |
|                 self::$_mapLayoutHandles
 | |
|             ),
 | |
|             new DiRule(new VirtualTypeMapper()),
 | |
|             new ReportsConfigRule($tableToModuleMap),
 | |
|             new AnalyticsConfigRule(),
 | |
|         ];
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Initialize routes whitelist
 | |
|      *
 | |
|      * @return array
 | |
|      */
 | |
|     private static function getRoutesWhitelist(): array
 | |
|     {
 | |
|         if (is_null(self::$routesWhitelist)) {
 | |
|             $routesWhitelistFilePattern = realpath(__DIR__) . '/_files/dependency_test/whitelist/routes_*.php';
 | |
|             $routesWhitelist = [];
 | |
|             foreach (glob($routesWhitelistFilePattern) as $fileName) {
 | |
|                 $routesWhitelist[] = include $fileName;
 | |
|             }
 | |
|             self::$routesWhitelist = array_merge([], ...$routesWhitelist);
 | |
|         }
 | |
|         return self::$routesWhitelist;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @return ComponentRegistrar
 | |
|      */
 | |
|     private static function getComponentRegistrar()
 | |
|     {
 | |
|         if (!isset(self::$componentRegistrar)) {
 | |
|             self::$componentRegistrar = new ComponentRegistrar();
 | |
|         }
 | |
|         return self::$componentRegistrar;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Get full path to app/code directory, assuming these tests are run from the dev/tests directory.
 | |
|      *
 | |
|      * @return string
 | |
|      * @throws \LogicException
 | |
|      */
 | |
|     private static function getAppCodeDir()
 | |
|     {
 | |
|         $appCode = BP . '/app/code';
 | |
|         if (!$appCode) {
 | |
|             throw new \LogicException('app/code directory cannot be located');
 | |
|         }
 | |
|         return $appCode;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Get a map of tables to primary modules.
 | |
|      *
 | |
|      * Primary module is the one which initially defines the table (versus the module extending its declaration).
 | |
|      *
 | |
|      * @see getTableToAnyModuleMap
 | |
|      *
 | |
|      * @return array
 | |
|      */
 | |
|     private static function getTableToPrimaryModuleMap(): array
 | |
|     {
 | |
|         $appCode = self::getAppCodeDir();
 | |
|         $tableToPrimaryModuleMap = [];
 | |
|         foreach (glob($appCode . '/*/*/etc/db_schema_whitelist.json') as $file) {
 | |
|             $dbSchemaWhitelist = (array)json_decode(file_get_contents($file));
 | |
|             preg_match('|.*/(.*)/(.*)/etc/db_schema_whitelist.json|', $file, $matches);
 | |
|             $moduleName = $matches[1] . '\\' . $matches[2];
 | |
|             $isStagingModule = (substr_compare($moduleName, 'Staging', -strlen('Staging')) === 0);
 | |
|             if ($isStagingModule) {
 | |
|                 // even though staging modules modify the constraints, they almost never declare new tables
 | |
|                 continue;
 | |
|             }
 | |
|             foreach ($dbSchemaWhitelist as $tableName => $tableMetadata) {
 | |
|                 if (isset($tableMetadata->constraint)) {
 | |
|                     $tableToPrimaryModuleMap[$tableName] = $moduleName;
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         return $tableToPrimaryModuleMap;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Get a map of tables matching to module names.
 | |
|      *
 | |
|      * Every table will have a module associated with it,
 | |
|      * even if the primary module cannot be defined based on declared constraints.
 | |
|      *
 | |
|      * @see getTableToPrimaryModuleMap
 | |
|      *
 | |
|      * @return array
 | |
|      */
 | |
|     private static function getTableToAnyModuleMap(): array
 | |
|     {
 | |
|         $appCode = self::getAppCodeDir();
 | |
|         $tableToAnyModuleMap = [];
 | |
|         foreach (glob($appCode . '/*/*/etc/db_schema_whitelist.json') as $file) {
 | |
|             $dbSchemaWhitelist = (array)json_decode(file_get_contents($file));
 | |
|             $tables = array_keys($dbSchemaWhitelist);
 | |
|             preg_match('|.*/(.*)/(.*)/etc/db_schema_whitelist.json|', $file, $matches);
 | |
|             $moduleName = $matches[1] . '\\' . $matches[2];
 | |
|             foreach ($tables as $table) {
 | |
|                 $tableToAnyModuleMap[$table] = $moduleName;
 | |
|             }
 | |
|         }
 | |
|         return $tableToAnyModuleMap;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Return cleaned file contents
 | |
|      *
 | |
|      * @param string $fileType
 | |
|      * @param string $file
 | |
|      * @return string
 | |
|      */
 | |
|     protected function _getCleanedFileContents($fileType, $file)
 | |
|     {
 | |
|         $contents = null;
 | |
|         switch ($fileType) {
 | |
|             case 'fixture':
 | |
|             case 'php':
 | |
|                 $contents = php_strip_whitespace($file);
 | |
|                 break;
 | |
|             case 'layout':
 | |
|             case 'config':
 | |
|                 //Removing xml comments
 | |
|                 $contents = preg_replace(
 | |
|                     '~\<!\-\-/.*?\-\-\>~s',
 | |
|                     '',
 | |
|                     file_get_contents($file)
 | |
|                 );
 | |
|                 break;
 | |
|             case 'template':
 | |
|                 $contents = php_strip_whitespace($file);
 | |
|                 //Removing html
 | |
|                 $contentsWithoutHtml = '';
 | |
|                 preg_replace_callback(
 | |
|                     '~(<\?(php|=)\s+.*\?>)~sU',
 | |
|                     function ($matches) use ($contents, &$contentsWithoutHtml) {
 | |
|                         $contentsWithoutHtml .= $matches[1];
 | |
|                         return $contents;
 | |
|                     },
 | |
|                     $contents
 | |
|                 );
 | |
|                 $contents = $contentsWithoutHtml;
 | |
|                 break;
 | |
|             default:
 | |
|                 $contents = file_get_contents($file);
 | |
|         }
 | |
|         return $contents;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @inheritdoc
 | |
|      * @throws \Exception
 | |
|      */
 | |
|     public function testUndeclared()
 | |
|     {
 | |
|         $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this);
 | |
|         $blackList = $this->getUndeclaredDependencyBlacklist();
 | |
|         $invoker(
 | |
|             /**
 | |
|              * Check undeclared modules dependencies for specified file
 | |
|              *
 | |
|              * @param string $fileType
 | |
|              * @param string $file
 | |
|              */
 | |
|             function ($fileType, $file) use ($blackList) {
 | |
|                 $module = $this->getModuleNameForRelevantFile($file);
 | |
|                 if (!$module) {
 | |
|                     return;
 | |
|                 }
 | |
| 
 | |
|                 $contents = $this->_getCleanedFileContents($fileType, $file);
 | |
| 
 | |
|                 $dependencies = $this->getDependenciesFromFiles($module, $fileType, $file, $contents);
 | |
| 
 | |
|                 // Collect dependencies
 | |
|                 $undeclaredDependency = $this->_collectDependencies($module, $dependencies);
 | |
| 
 | |
|                 // Prepare output message
 | |
|                 $result = [];
 | |
| 
 | |
|                 foreach ($undeclaredDependency as $type => $modules) {
 | |
|                     $modules = $this->filterOutBlacklistedDependencies($file, $fileType, $modules, $blackList);
 | |
|                     $modules = array_unique($modules);
 | |
|                     if (empty($modules)) {
 | |
|                         continue;
 | |
|                     }
 | |
|                     $result[] = sprintf("%s [%s]", $type, implode(', ', $modules));
 | |
|                 }
 | |
|                 if (!empty($result)) {
 | |
|                     $this->fail('Module ' . $module . ' has undeclared dependencies: ' . implode(', ', $result));
 | |
|                 }
 | |
|             },
 | |
|             $this->getAllFiles()
 | |
|         );
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Filter out list of module dependencies based on the provided blacklist.
 | |
|      *
 | |
|      * Additionally, exclude:
 | |
|      *   - dependency on Setup for all modules as it is part of base Magento package.
 | |
|      *   - dependency on Magento\TestFramework for in fixture classes
 | |
|      *
 | |
|      * @param string $filePath
 | |
|      * @param string $fileType
 | |
|      * @param string[] $modules
 | |
|      * @param array $blackList
 | |
|      * @return string[]
 | |
|      */
 | |
|     private function filterOutBlacklistedDependencies($filePath, $fileType, $modules, $blackList): array
 | |
|     {
 | |
|         $relativeFilePath = substr_replace($filePath, '', 0, strlen(BP . '/'));
 | |
|         foreach ($modules as $moduleKey => $module) {
 | |
|             if ($module === 'Magento\Setup') {
 | |
|                 unset($modules[$moduleKey]);
 | |
|             }
 | |
|             if ($fileType === 'fixture' && $module === 'Magento\TestFramework') {
 | |
|                 unset($modules[$moduleKey]);
 | |
|             }
 | |
|             if (isset($blackList[$relativeFilePath])
 | |
|                 && in_array($module, $blackList[$relativeFilePath])
 | |
|             ) {
 | |
|                 unset($modules[$moduleKey]);
 | |
|             }
 | |
|         }
 | |
|         return $modules;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Identify dependencies on the components which are not part of the current project.
 | |
|      *
 | |
|      * For example, such test allows to prevent invalid dependencies from the storefront application to the monolith.
 | |
|      *
 | |
|      * @throws \Exception
 | |
|      */
 | |
|     public function testExternalDependencies()
 | |
|     {
 | |
|         $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this);
 | |
|         $blackList = $this->getExternalDependencyBlacklist();
 | |
|         $invoker(
 | |
|             /**
 | |
|              * Check external modules dependencies for specified file
 | |
|              *
 | |
|              * @param string $fileType
 | |
|              * @param string $file
 | |
|              */
 | |
|             function ($fileType, $file) use ($blackList) {
 | |
|                 $module = $this->getModuleNameForRelevantFile($file);
 | |
|                 if (!$module) {
 | |
|                     return;
 | |
|                 }
 | |
|                 $externalDependencies = $this->collectExternalDependencies($file, $fileType, $module);
 | |
|                 // Prepare output message
 | |
|                 $result = [];
 | |
|                 foreach ($externalDependencies as $type => $modules) {
 | |
|                     $modules = $this->filterOutBlacklistedDependencies($file, $fileType, $modules, $blackList);
 | |
|                     $modules = array_unique($modules);
 | |
|                     if (empty($modules)) {
 | |
|                         continue;
 | |
|                     }
 | |
|                     $result[] = sprintf("%s [%s]", $type, implode(', ', $modules));
 | |
|                 }
 | |
|                 if (!empty($result)) {
 | |
|                     $this->fail('Module ' . $module . ' has external dependencies: ' . implode(', ', $result));
 | |
|                 }
 | |
|             },
 | |
|             $this->getAllFiles()
 | |
|         );
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Return module name for the file being tested if it should be tested. Return empty string otherwise.
 | |
|      *
 | |
|      * @param string $file
 | |
|      * @return string
 | |
|      */
 | |
|     private function getModuleNameForRelevantFile($file)
 | |
|     {
 | |
|         $componentRegistrar = self::getComponentRegistrar();
 | |
|         // Validates file when it belongs to default themes
 | |
|         foreach ($componentRegistrar->getPaths(ComponentRegistrar::THEME) as $themeDir) {
 | |
|             if (strpos($file, $themeDir . '/') !== false) {
 | |
|                 return '';
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         $foundModuleName = '';
 | |
|         foreach ($componentRegistrar->getPaths(ComponentRegistrar::MODULE) as $moduleName => $moduleDir) {
 | |
|             if (strpos($file, $moduleDir . '/') !== false) {
 | |
|                 $foundModuleName = str_replace('_', '\\', $moduleName);
 | |
|                 break;
 | |
|             }
 | |
|         }
 | |
|         if (empty($foundModuleName)) {
 | |
|             return '';
 | |
|         }
 | |
| 
 | |
|         return $foundModuleName;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Collect a list of external dependencies of the specified file.
 | |
|      *
 | |
|      * Dependency is considered external if it cannot be traced withing current codebase.
 | |
|      *
 | |
|      * @param string $file
 | |
|      * @param string $fileType
 | |
|      * @param string $module
 | |
|      * @return array
 | |
|      * @throws LocalizedException
 | |
|      */
 | |
|     private function collectExternalDependencies($file, $fileType, $module)
 | |
|     {
 | |
|         $contents = $this->_getCleanedFileContents($fileType, $file);
 | |
| 
 | |
|         $dependencies = $this->getDependenciesFromFiles($module, $fileType, $file, $contents);
 | |
|         $externalDependencies = [];
 | |
|         foreach ($dependencies as $dependency) {
 | |
|             $dependencyModules = $dependency['modules'];
 | |
|             foreach ($dependencyModules as $dependencyModule) {
 | |
|                 if ($dependency['type'] !== 'soft'
 | |
|                     && !isset(self::$mapDependencies[$dependencyModule])
 | |
|                     && (strpos($dependencyModule, 'Magento\Framework') !== 0)
 | |
|                 ) {
 | |
|                     $dependencySummary = ($dependencyModule !== 'Unknown')
 | |
|                         ? $dependencyModule
 | |
|                         : $dependency['source'];
 | |
|                     $externalDependencies[$dependency['type']][] = $dependencySummary;
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         return $externalDependencies;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Return a list of blacklisted external dependencies.
 | |
|      *
 | |
|      * @return array
 | |
|      */
 | |
|     private function getExternalDependencyBlacklist(): array
 | |
|     {
 | |
|         if (!isset($this->externalDependencyBlacklist)) {
 | |
|             $this->externalDependencyBlacklist = [];
 | |
|             foreach (glob(__DIR__ . '/_files/blacklist/external_dependency/*.php') as $filename) {
 | |
|                 $this->externalDependencyBlacklist = array_merge_recursive(
 | |
|                     $this->externalDependencyBlacklist,
 | |
|                     include $filename
 | |
|                 );
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return $this->externalDependencyBlacklist;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Return a list of blacklisted undeclared dependencies.
 | |
|      *
 | |
|      * @return array
 | |
|      */
 | |
|     private function getUndeclaredDependencyBlacklist(): array
 | |
|     {
 | |
|         if (!isset($this->undeclaredDependencyBlacklist)) {
 | |
|             $this->undeclaredDependencyBlacklist = [];
 | |
|             foreach (glob(__DIR__ . '/_files/blacklist/undeclared_dependency/*.php') as $filename) {
 | |
|                 $this->undeclaredDependencyBlacklist = array_merge_recursive(
 | |
|                     $this->undeclaredDependencyBlacklist,
 | |
|                     include $filename
 | |
|                 );
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return $this->undeclaredDependencyBlacklist;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Retrieve dependencies from files
 | |
|      *
 | |
|      * @param string $module
 | |
|      * @param string $fileType
 | |
|      * @param string $file
 | |
|      * @param string $contents
 | |
|      * @return array [
 | |
|      *   [
 | |
|      *     'modules' => string[],
 | |
|      *     'type' => string
 | |
|      *     'source' => string
 | |
|      *   ],
 | |
|      *   ...
 | |
|      * ]
 | |
|      * @throws LocalizedException
 | |
|      */
 | |
|     protected function getDependenciesFromFiles($module, $fileType, $file, $contents)
 | |
|     {
 | |
|         // Apply rules
 | |
|         $dependencies = [];
 | |
|         foreach (self::$_rulesInstances as $rule) {
 | |
|             /** @var \Magento\TestFramework\Dependency\RuleInterface $rule */
 | |
|             $newDependencies = $rule->getDependencyInfo($module, $fileType, $file, $contents);
 | |
|             $dependencies[] = $newDependencies;
 | |
|         }
 | |
|         $dependencies = array_merge([], ...$dependencies);
 | |
| 
 | |
|         foreach ($dependencies as $dependencyKey => $dependency) {
 | |
|             foreach (self::$whiteList as $namespace) {
 | |
|                 if (strpos($dependency['source'], $namespace) !== false) {
 | |
|                     $dependency['modules'] = [$namespace];
 | |
|                     $dependencies[$dependencyKey] = $dependency;
 | |
|                 }
 | |
|             }
 | |
|             $dependency['type'] = $dependency['type'] ?? 'type is unknown';
 | |
|             if (empty($dependency['modules'])) {
 | |
|                 unset($dependencies[$dependencyKey]);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return $dependencies;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Collect dependencies
 | |
|      *
 | |
|      * @param string $currentModuleName
 | |
|      * @param array $dependencies
 | |
|      * @return array
 | |
|      */
 | |
|     protected function _collectDependencies($currentModuleName, $dependencies = [])
 | |
|     {
 | |
|         if (empty($dependencies)) {
 | |
|             return [];
 | |
|         }
 | |
|         $undeclared = [];
 | |
|         foreach ($dependencies as $dependency) {
 | |
|             $this->collectDependency($dependency, $currentModuleName, $undeclared);
 | |
|         }
 | |
|         return $undeclared;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Collect a dependency
 | |
|      *
 | |
|      * @param string $currentModule
 | |
|      * @param array $dependency
 | |
|      * @param array $undeclared
 | |
|      */
 | |
|     private function collectDependency($dependency, $currentModule, &$undeclared)
 | |
|     {
 | |
|         $type = isset($dependency['type']) ? $dependency['type'] : self::TYPE_HARD;
 | |
| 
 | |
|         $soft = $this->_getDependencies($currentModule, self::TYPE_SOFT, self::MAP_TYPE_DECLARED);
 | |
|         $hard = $this->_getDependencies($currentModule, self::TYPE_HARD, self::MAP_TYPE_DECLARED);
 | |
| 
 | |
|         $declared = $type == self::TYPE_SOFT ? array_merge($soft, $hard) : $hard;
 | |
| 
 | |
|         $modules = $dependency['modules'];
 | |
|         $this->collectConditionalDependencies($modules, $type, $currentModule, $declared, $undeclared);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Collect non-strict dependencies when the module depends on one of modules
 | |
|      *
 | |
|      * @param array $conditionalDependencies
 | |
|      * @param string $type
 | |
|      * @param string $currentModule
 | |
|      * @param array $declared
 | |
|      * @param array $undeclared
 | |
|      */
 | |
|     private function collectConditionalDependencies(
 | |
|         array $conditionalDependencies,
 | |
|         string $type,
 | |
|         string $currentModule,
 | |
|         array $declared,
 | |
|         array &$undeclared
 | |
|     ) {
 | |
|         array_walk(
 | |
|             $conditionalDependencies,
 | |
|             function (&$moduleName) {
 | |
|                 $moduleName = str_replace('_', '\\', $moduleName);
 | |
|             }
 | |
|         );
 | |
|         $declaredDependencies = array_intersect($conditionalDependencies, $declared);
 | |
| 
 | |
|         foreach ($declaredDependencies as $moduleName) {
 | |
|             if ($this->_isFake($moduleName)) {
 | |
|                 $this->_setDependencies($currentModule, $type, self::MAP_TYPE_REDUNDANT, $moduleName);
 | |
|             }
 | |
| 
 | |
|             self::addDependency($currentModule, $type, self::MAP_TYPE_FOUND, $moduleName);
 | |
|         }
 | |
| 
 | |
|         if (empty($declaredDependencies)) {
 | |
|             $undeclared[$type][] = implode(" || ", $conditionalDependencies);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Collect redundant dependencies
 | |
|      *
 | |
|      * @SuppressWarnings(PHPMD.NPathComplexity)
 | |
|      * @test
 | |
|      * @depends testUndeclared
 | |
|      * @throws \Exception
 | |
|      */
 | |
|     public function collectRedundant()
 | |
|     {
 | |
|         $objectManager = Bootstrap::create(BP, $_SERVER)->getObjectManager();
 | |
|         $schemaDependencyProvider = $objectManager->create(DeclarativeSchemaDependencyProvider::class);
 | |
|         $graphQlSchemaDependencyProvider = $objectManager->create(GraphQlSchemaDependencyProvider::class);
 | |
| 
 | |
|         foreach (array_keys(self::$mapDependencies) as $module) {
 | |
|             $declared = $this->_getDependencies($module, self::TYPE_HARD, self::MAP_TYPE_DECLARED);
 | |
|             //phpcs:ignore Magento2.Performance.ForeachArrayMerge
 | |
|             $found = array_merge(
 | |
|                 $this->_getDependencies($module, self::TYPE_HARD, self::MAP_TYPE_FOUND),
 | |
|                 $this->_getDependencies($module, self::TYPE_SOFT, self::MAP_TYPE_FOUND),
 | |
|                 $schemaDependencyProvider->getDeclaredExistingModuleDependencies($module),
 | |
|                 $graphQlSchemaDependencyProvider->getDeclaredExistingModuleDependencies($module)
 | |
|             );
 | |
|             $found['Magento\Framework'] = 'Magento\Framework';
 | |
|             $this->_setDependencies($module, self::TYPE_HARD, self::MAP_TYPE_REDUNDANT, array_diff($declared, $found));
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Check redundant dependencies
 | |
|      *
 | |
|      * @depends collectRedundant
 | |
|      */
 | |
|     public function testRedundant()
 | |
|     {
 | |
|         $output = [];
 | |
|         foreach (array_keys(self::$mapDependencies) as $module) {
 | |
|             $result = [];
 | |
|             $redundant = $this->_getDependencies($module, self::TYPE_HARD, self::MAP_TYPE_REDUNDANT);
 | |
|             if (isset(self::$redundantDependenciesWhitelist[$module])) {
 | |
|                 $redundant = array_diff($redundant, self::$redundantDependenciesWhitelist[$module]);
 | |
|             }
 | |
|             if (!empty($redundant)) {
 | |
|                 $result[] = sprintf(
 | |
|                     "\r\nModule %s: %s [%s]",
 | |
|                     $module,
 | |
|                     self::TYPE_HARD,
 | |
|                     implode(', ', array_values($redundant))
 | |
|                 );
 | |
|             }
 | |
| 
 | |
|             if (!empty($result)) {
 | |
|                 $output[] = implode(', ', $result);
 | |
|             }
 | |
|         }
 | |
|         if (!empty($output)) {
 | |
|             $this->fail("Redundant dependencies found!\r\n" . implode(' ', $output));
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Convert file list to data provider structure
 | |
|      *
 | |
|      * @param string $fileType
 | |
|      * @param array $files
 | |
|      * @param bool|null $skip
 | |
|      * @return array
 | |
|      */
 | |
|     protected function _prepareFiles($fileType, $files, $skip = null)
 | |
|     {
 | |
|         $result = [];
 | |
|         foreach ($files as $relativePath => $file) {
 | |
|             $absolutePath = $file[0];
 | |
|             if (!$skip && substr_count($relativePath, '/') < self::DIR_PATH_COUNT) {
 | |
|                 continue;
 | |
|             }
 | |
|             $result[$relativePath] = [$fileType, $absolutePath];
 | |
|         }
 | |
|         return $result;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Return all files
 | |
|      *
 | |
|      * @return array
 | |
|      * @throws \Exception
 | |
|      */
 | |
|     public function getAllFiles()
 | |
|     {
 | |
|         return array_merge(
 | |
|             $this->_prepareFiles(
 | |
|                 'php',
 | |
|                 Files::init()->getPhpFiles(Files::INCLUDE_APP_CODE | Files::AS_DATA_SET | Files::INCLUDE_NON_CLASSES),
 | |
|                 true
 | |
|             ),
 | |
|             $this->_prepareFiles('config', Files::init()->getConfigFiles()),
 | |
|             $this->_prepareFiles('layout', Files::init()->getLayoutFiles()),
 | |
|             $this->_prepareFiles('template', Files::init()->getPhtmlFiles()),
 | |
|             $this->_prepareFiles('fixture', Files::composeDataSets($this->getFixtureFiles()), true)
 | |
|         );
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Prepare list of config.xml files (by modules).
 | |
|      *
 | |
|      * @throws \Exception
 | |
|      */
 | |
|     protected static function _prepareListConfigXml()
 | |
|     {
 | |
|         $files = Files::init()->getConfigFiles('config.xml', [], false);
 | |
|         foreach ($files as $file) {
 | |
|             if (preg_match('/(?<namespace>[A-Z][a-z]+)[_\/\\\\](?<module>[A-Z][a-zA-Z]+)/', $file, $matches)) {
 | |
|                 $module = $matches['namespace'] . '\\' . $matches['module'];
 | |
|                 self::$_listConfigXml[$module] = $file;
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Prepare list of analytics.xml files
 | |
|      *
 | |
|      * @throws \Exception
 | |
|      */
 | |
|     protected static function _prepareListAnalyticsXml()
 | |
|     {
 | |
|         $files = Files::init()->getDbSchemaFiles('analytics.xml', [], false);
 | |
|         foreach ($files as $file) {
 | |
|             if (preg_match('/(?<namespace>[A-Z][a-z]+)[_\/\\\\](?<module>[A-Z][a-zA-Z]+)/', $file, $matches)) {
 | |
|                 $module = $matches['namespace'] . '\\' . $matches['module'];
 | |
|                 self::$_listAnalyticsXml[$module] = $file;
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Prepare map of layout blocks
 | |
|      *
 | |
|      * @throws \Exception
 | |
|      */
 | |
|     protected static function _prepareMapLayoutBlocks()
 | |
|     {
 | |
|         $files = Files::init()->getLayoutFiles([], false);
 | |
|         foreach ($files as $file) {
 | |
|             $area = 'default';
 | |
|             if (preg_match('/[\/](?<area>adminhtml|frontend)[\/]/', $file, $matches)) {
 | |
|                 $area = $matches['area'];
 | |
|                 self::$_mapLayoutBlocks[$area] = self::$_mapLayoutBlocks[$area] ?? [];
 | |
|             }
 | |
|             if (preg_match('/(?<namespace>[A-Z][a-z]+)[_\/\\\\](?<module>[A-Z][a-zA-Z]+)/', $file, $matches)) {
 | |
|                 $module = $matches['namespace'] . '\\' . $matches['module'];
 | |
|                 $xml = simplexml_load_file($file);
 | |
|                 foreach ((array)$xml->xpath('//container | //block') as $element) {
 | |
|                     /** @var \SimpleXMLElement $element */
 | |
|                     $attributes = $element->attributes();
 | |
|                     $block = (string)$attributes->name;
 | |
|                     if (!empty($block)) {
 | |
|                         self::$_mapLayoutBlocks[$area][$block] = self::$_mapLayoutBlocks[$area][$block] ?? [];
 | |
|                         self::$_mapLayoutBlocks[$area][$block][$module] = $module;
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Prepare map of layout handles
 | |
|      *
 | |
|      * @throws \Exception
 | |
|      */
 | |
|     protected static function _prepareMapLayoutHandles()
 | |
|     {
 | |
|         $files = Files::init()->getLayoutFiles([], false);
 | |
|         foreach ($files as $file) {
 | |
|             $area = 'default';
 | |
|             if (preg_match('/\/(?<area>adminhtml|frontend)\//', $file, $matches)) {
 | |
|                 $area = $matches['area'];
 | |
|                 self::$_mapLayoutHandles[$area] = self::$_mapLayoutHandles[$area] ?? [];
 | |
|             }
 | |
|             if (preg_match('/app\/code\/(?<namespace>[A-Z][a-z]+)[_\/\\\\](?<module>[A-Z][a-zA-Z]+)/', $file, $matches)
 | |
|             ) {
 | |
|                 $module = $matches['namespace'] . '\\' . $matches['module'];
 | |
|                 $xml = simplexml_load_file($file);
 | |
|                 foreach ((array)$xml->xpath('/layout/child::*') as $element) {
 | |
|                     /** @var \SimpleXMLElement $element */
 | |
|                     $handle = $element->getName();
 | |
|                     self::$_mapLayoutHandles[$area][$handle] = self::$_mapLayoutHandles[$area][$handle] ?? [];
 | |
|                     self::$_mapLayoutHandles[$area][$handle][$module] = $module;
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Retrieve dependency types array
 | |
|      *
 | |
|      * @return array
 | |
|      */
 | |
|     protected static function _getTypes()
 | |
|     {
 | |
|         return [self::TYPE_HARD, self::TYPE_SOFT];
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Converts a composer json component name into the Magento Module form
 | |
|      *
 | |
|      * @param string $jsonName The name of a composer json component or dependency e.g. 'magento/module-theme'
 | |
|      * @param array $packageModuleMap Mapping package name with module namespace.
 | |
|      * @return string The corresponding Magento Module e.g. 'Magento\Theme'
 | |
|      */
 | |
|     protected static function convertModuleName(string $jsonName, array $packageModuleMap): string
 | |
|     {
 | |
|         if (isset($packageModuleMap[$jsonName])) {
 | |
|             return $packageModuleMap[$jsonName];
 | |
|         }
 | |
| 
 | |
|         if (strpos($jsonName, 'magento/magento') !== false || strpos($jsonName, 'magento/framework') !== false) {
 | |
|             $moduleName = str_replace('/', "\t", $jsonName);
 | |
|             $moduleName = str_replace('framework-', "Framework\t", $moduleName);
 | |
|             $moduleName = str_replace('-', ' ', $moduleName);
 | |
|             $moduleName = ucwords($moduleName);
 | |
|             $moduleName = str_replace("\t", '\\', $moduleName);
 | |
|             $moduleName = str_replace(' ', '', $moduleName);
 | |
| 
 | |
|             return $moduleName;
 | |
|         }
 | |
| 
 | |
|         // convert names of the modules not registered in any composer.json
 | |
|         preg_match('|magento/module-(.*)|', $jsonName, $matches);
 | |
|         if (isset($matches[1])) {
 | |
|             $moduleNameHyphenated = $matches[1];
 | |
|             $moduleNameUpperCamelCase = 'Magento\\' . str_replace('-', '', ucwords($moduleNameHyphenated, '-'));
 | |
|             return $moduleNameUpperCamelCase;
 | |
|         }
 | |
| 
 | |
|         return $jsonName;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Initialise map of dependencies.
 | |
|      *
 | |
|      * @return void
 | |
|      * @SuppressWarnings(PHPMD.CyclomaticComplexity)
 | |
|      * @SuppressWarnings(PHPMD.NPathComplexity)
 | |
|      * @throws \Exception
 | |
|      */
 | |
|     protected static function _initDependencies()
 | |
|     {
 | |
|         $packageModuleMap = self::getPackageModuleMapping();
 | |
|         $jsonFiles = Files::init()->getComposerFiles(ComponentRegistrar::MODULE, false);
 | |
|         foreach ($jsonFiles as $file) {
 | |
|             $contents = file_get_contents($file);
 | |
|             $decodedJson = json_decode($contents);
 | |
|             if (null == $decodedJson) {
 | |
|                 //phpcs:ignore Magento2.Exceptions.DirectThrow
 | |
|                 throw new \Exception("Invalid Json: $file");
 | |
|             }
 | |
|             $json = new \Magento\Framework\Config\Composer\Package(json_decode($contents));
 | |
|             $moduleName = self::convertModuleName($json->get('name'), $packageModuleMap);
 | |
|             if (!isset(self::$mapDependencies[$moduleName])) {
 | |
|                 self::$mapDependencies[$moduleName] = [];
 | |
|             }
 | |
|             foreach (self::_getTypes() as $type) {
 | |
|                 if (!isset(self::$mapDependencies[$moduleName][$type])) {
 | |
|                     self::$mapDependencies[$moduleName][$type] = [
 | |
|                         self::MAP_TYPE_DECLARED  => [],
 | |
|                         self::MAP_TYPE_FOUND     => [],
 | |
|                         self::MAP_TYPE_REDUNDANT => [],
 | |
|                     ];
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             $require = array_keys((array)$json->get('require'));
 | |
|             self::addDependencies($moduleName, $require, self::TYPE_HARD, $packageModuleMap);
 | |
| 
 | |
|             $suggest = array_keys((array)$json->get('suggest'));
 | |
|             self::addDependencies($moduleName, $suggest, self::TYPE_SOFT, $packageModuleMap);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Add dependencies to dependency list.
 | |
|      *
 | |
|      * @param string $moduleName
 | |
|      * @param array $packageNames
 | |
|      * @param string $type
 | |
|      * @param array $packageModuleMap
 | |
|      *
 | |
|      * @return void
 | |
|      */
 | |
|     private static function addDependencies(
 | |
|         string $moduleName,
 | |
|         array $packageNames,
 | |
|         string $type,
 | |
|         array $packageModuleMap
 | |
|     ): void {
 | |
|         $packageNames = array_filter(
 | |
|             $packageNames,
 | |
|             function ($packageName) use ($packageModuleMap) {
 | |
|                 return isset($packageModuleMap[$packageName]) ||
 | |
|                     0 === strpos($packageName, 'magento/')
 | |
|                     && 'magento/magento-composer-installer' != $packageName;
 | |
|             }
 | |
|         );
 | |
| 
 | |
|         foreach ($packageNames as $packageName) {
 | |
|             self::addDependency(
 | |
|                 $moduleName,
 | |
|                 $type,
 | |
|                 self::MAP_TYPE_DECLARED,
 | |
|                 self::convertModuleName($packageName, $packageModuleMap)
 | |
|             );
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Add dependency map items.
 | |
|      *
 | |
|      * @param string $module
 | |
|      * @param string $type
 | |
|      * @param string $mapType
 | |
|      * @param string $dependency
 | |
|      *
 | |
|      * @return void
 | |
|      */
 | |
|     private static function addDependency(string $module, string $type, string $mapType, string $dependency): void
 | |
|     {
 | |
|         if (isset(self::$mapDependencies[$module][$type][$mapType])) {
 | |
|             self::$mapDependencies[$module][$type][$mapType][$dependency] = $dependency;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Returns package name on module name mapping.
 | |
|      *
 | |
|      * @return array
 | |
|      * @throws \Exception
 | |
|      */
 | |
|     private static function getPackageModuleMapping(): array
 | |
|     {
 | |
|         $jsonFiles = Files::init()->getComposerFiles(ComponentRegistrar::MODULE, false);
 | |
| 
 | |
|         $packageModuleMapping = [];
 | |
|         foreach ($jsonFiles as $file) {
 | |
|             $contents = file_get_contents($file);
 | |
|             $composerJson = json_decode($contents);
 | |
|             if (null == $composerJson) {
 | |
|                 //phpcs:ignore Magento2.Exceptions.DirectThrow
 | |
|                 throw new \Exception("Invalid Json: $file");
 | |
|             }
 | |
|             $moduleXml = simplexml_load_file(dirname($file) . '/etc/module.xml');
 | |
|             $moduleName = str_replace('_', '\\', (string)$moduleXml->module->attributes()->name);
 | |
|             $packageName = $composerJson->name;
 | |
|             $packageModuleMapping[$packageName] = $moduleName;
 | |
|         }
 | |
| 
 | |
|         return $packageModuleMapping;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Retrieve array of dependency items
 | |
|      *
 | |
|      * @param $module
 | |
|      * @param $type
 | |
|      * @param $mapType
 | |
|      * @return array
 | |
|      */
 | |
|     protected function _getDependencies($module, $type, $mapType)
 | |
|     {
 | |
|         if (isset(self::$mapDependencies[$module][$type][$mapType])) {
 | |
|             return self::$mapDependencies[$module][$type][$mapType];
 | |
|         }
 | |
| 
 | |
|         return [];
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Set dependency map items
 | |
|      *
 | |
|      * @param $module
 | |
|      * @param $type
 | |
|      * @param $mapType
 | |
|      * @param $dependencies
 | |
|      */
 | |
|     protected function _setDependencies($module, $type, $mapType, $dependencies)
 | |
|     {
 | |
|         if (!is_array($dependencies)) {
 | |
|             $dependencies = [$dependencies];
 | |
|         }
 | |
|         if (isset(self::$mapDependencies[$module][$type][$mapType])) {
 | |
|             self::$mapDependencies[$module][$type][$mapType] = $dependencies;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Check if module is fake
 | |
|      *
 | |
|      * @param $module
 | |
|      * @return bool
 | |
|      */
 | |
|     protected function _isFake($module)
 | |
|     {
 | |
|         return isset(self::$mapDependencies[$module]) ? false : true;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Test modules don't have direct dependencies on modules that might be disabled by 3rd-party Magento extensions.
 | |
|      *
 | |
|      * @inheritdoc
 | |
|      * @throws \Exception
 | |
|      * @return void
 | |
|      */
 | |
|     public function testDirectExtensionDependencies()
 | |
|     {
 | |
|         $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this);
 | |
| 
 | |
|         $extensionConflictList = self::getExtensionConflicts();
 | |
|         $allowedDependencies = self::getAllowedDependencies();
 | |
| 
 | |
|         $invoker(
 | |
|         /**
 | |
|          * Check modules dependencies for specified file
 | |
|          *
 | |
|          * @param string $fileType
 | |
|          * @param string $file
 | |
|          */
 | |
|             function ($fileType, $file) use ($extensionConflictList, $allowedDependencies) {
 | |
|                 $module = $this->getModuleNameForRelevantFile($file);
 | |
|                 if (!$module) {
 | |
|                     return;
 | |
|                 }
 | |
| 
 | |
|                 $contents = $this->_getCleanedFileContents($fileType, $file);
 | |
| 
 | |
|                 $dependencies = $this->getDependenciesFromFiles($module, $fileType, $file, $contents);
 | |
| 
 | |
|                 $modules = [];
 | |
|                 foreach ($dependencies as $dependency) {
 | |
|                     $modules[] = $dependency['modules'];
 | |
|                 }
 | |
| 
 | |
|                 $modulesDependencies = array_merge(...$modules);
 | |
| 
 | |
|                 foreach ($extensionConflictList as $extension => $disabledModules) {
 | |
|                     $modulesThatMustBeDisabled = \array_unique(array_intersect($modulesDependencies, $disabledModules));
 | |
|                     if (!empty($modulesThatMustBeDisabled)) {
 | |
| 
 | |
|                         foreach ($modulesThatMustBeDisabled as $foundedModule) {
 | |
|                             if (!empty($allowedDependencies[$foundedModule])
 | |
|                                 && \in_array($module, $allowedDependencies[$foundedModule])
 | |
|                             ) {
 | |
|                                 // skip, this dependency is allowed
 | |
|                                 continue;
 | |
|                             }
 | |
| 
 | |
|                             $this->fail(
 | |
|                                 \sprintf(
 | |
|                                     'Module "%s" has dependency on: "%s".' .
 | |
|                                     ' No direct dependencies must be added on "%s",' .
 | |
|                                     ' because it must be disabled when "%s" extension is used.' .
 | |
|                                     ' See AC-2516 for more details',
 | |
|                                     $module,
 | |
|                                     \implode(', ', $modulesThatMustBeDisabled),
 | |
|                                     $module,
 | |
|                                     $extension
 | |
|                                 )
 | |
|                             );
 | |
|                         }
 | |
|                     }
 | |
|                 }
 | |
|             },
 | |
|             $this->getAllFiles()
 | |
|         );
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Initialize extension conflicts list.
 | |
|      *
 | |
|      * @return array
 | |
|      */
 | |
|     private static function getExtensionConflicts(): array
 | |
|     {
 | |
|         if (null === self::$extensionConflicts) {
 | |
|             $extensionConflictsFilePattern =
 | |
|                 realpath(__DIR__) . '/_files/extension_dependencies_test/extension_conflicts/*.php';
 | |
|             $extensionConflicts = [];
 | |
|             foreach (glob($extensionConflictsFilePattern) as $fileName) {
 | |
|                 $extensionConflicts[] = include $fileName;
 | |
|             }
 | |
|             self::$extensionConflicts = \array_merge_recursive([], ...$extensionConflicts);
 | |
|         }
 | |
|         return self::$extensionConflicts;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Initialize allowed dependencies.
 | |
|      *
 | |
|      * @return array
 | |
|      */
 | |
|     private static function getAllowedDependencies(): array
 | |
|     {
 | |
|         if (null === self::$allowedDependencies) {
 | |
|             $allowedDependenciesFilePattern =
 | |
|                 realpath(__DIR__) . '/_files/extension_dependencies_test/allowed_dependencies/*.php';
 | |
|             $allowedDependencies = [];
 | |
|             foreach (glob($allowedDependenciesFilePattern) as $fileName) {
 | |
|                 $allowedDependencies[] = include $fileName;
 | |
|             }
 | |
|             self::$allowedDependencies = \array_merge_recursive([], ...$allowedDependencies);
 | |
|         }
 | |
|         return self::$allowedDependencies;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Returns fixture files located in <module-directory>/Test/Fixture directory
 | |
|      *
 | |
|      * @return array
 | |
|      */
 | |
|     private function getFixtureFiles(): array
 | |
|     {
 | |
|         $fixtureDirs = [];
 | |
|         foreach (self::getComponentRegistrar()->getPaths(ComponentRegistrar::MODULE) as $moduleDir) {
 | |
|             $fixtureDirs[] = $moduleDir . '/Test/Fixture';
 | |
|         }
 | |
|         return Files::getFiles($fixtureDirs, '*.php');
 | |
|     }
 | |
| }
 |