![]() Server : Apache System : Linux server2.corals.io 4.18.0-348.2.1.el8_5.x86_64 #1 SMP Mon Nov 15 09:17:08 EST 2021 x86_64 User : corals ( 1002) PHP Version : 7.4.33 Disable Function : exec,passthru,shell_exec,system Directory : /home/corals/old/dev/tests/static/testsuite/Magento/Test/Integrity/ |
<?php /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Test\Integrity; use Exception; use Magento\Framework\App\Utility\Files; use Magento\Setup\Module\Di\Code\Reader\FileClassScanner; use PHPUnit\Framework\TestCase; use ReflectionClass; use ReflectionMethod; /** * Tests @api annotated code integrity */ class PublicCodeTest extends TestCase { /** * List of simple return types that are used in docblocks. * Used to check if type declared in a docblock of a method is a class or interface * * @var array */ private $simpleReturnTypes = [ '$this', 'void', 'string', 'int', 'bool', 'boolean', 'integer', 'null' ]; /** * @var string[]|null */ private $blockWhitelist; /** * Return whitelist class names * * @return string[] */ private function getWhitelist(): array { if ($this->blockWhitelist === null) { $whiteListFiles = str_replace( '\\', '/', realpath(__DIR__) . '/_files/whitelist/public_code*.txt' ); $whiteListItems = []; foreach (glob($whiteListFiles) as $fileName) { $whiteListItems[] = file($fileName, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); } $this->blockWhitelist = array_merge([], ...$whiteListItems); } return $this->blockWhitelist; } /** * Since blocks can be referenced from templates, they should be stable not to break theme customizations. * So all blocks should be @api annotated. This test checks that all blocks declared in layout files are public * * @param $layoutFile * @throws \ReflectionException * @dataProvider layoutFilesDataProvider */ public function testAllBlocksReferencedInLayoutArePublic($layoutFile) { $nonPublishedBlocks = []; $xml = simplexml_load_file($layoutFile); $elements = $xml->xpath('//block | //referenceBlock') ?: []; /** @var $node \SimpleXMLElement */ foreach ($elements as $node) { $class = (string) $node['class']; if ($class && \class_exists($class) && !in_array($class, $this->getWhitelist())) { $reflection = (new ReflectionClass($class)); if (strpos($reflection->getDocComment(), '@api') === false) { $nonPublishedBlocks[] = $class; } } } if (count($nonPublishedBlocks)) { $this->fail( "Layout file '$layoutFile' uses following blocks that are not marked with @api annotation:\n" . implode(",\n", array_unique($nonPublishedBlocks)) ); } } /** * Find all layout update files in magento modules and themes. * * @return array * @throws Exception */ public function layoutFilesDataProvider() { return Files::init()->getLayoutFiles([], true); } /** * We want to avoid situation when a type is marked public (@api annotated) but one of its methods * returns or accepts the value of non-public type. * This test walks through all public PHP types and makes sure that all their method arguments * and return values are public types. * * @param string $class * @throws \ReflectionException * @dataProvider publicPHPTypesDataProvider */ public function testAllPHPClassesReferencedFromPublicClassesArePublic($class) { $nonPublishedClasses = []; $reflection = new ReflectionClass($class); $filter = ReflectionMethod::IS_PUBLIC; if ($reflection->isAbstract()) { $filter = $filter | ReflectionMethod::IS_PROTECTED; } $methods = $reflection->getMethods($filter); foreach ($methods as $method) { if ($method->isConstructor()) { continue; } $nonPublishedClasses = $this->checkParameters($class, $method, $nonPublishedClasses); /* Taking into account docblock return types since this code is written on early php 7 when return types are not actively used */ $returnTypes = []; if ($method->hasReturnType()) { $methodReturnType = $method->getReturnType(); // For PHP 8.0 - ReflectionUnionType doesn't have isBuiltin method. if (method_exists($methodReturnType, 'isBuiltin') && !$methodReturnType->isBuiltin()) { $returnTypes = [trim($methodReturnType->getName(), '?[]')]; } } else { $returnTypes = $this->getReturnTypesFromDocComment($method->getDocComment()); } $nonPublishedClasses = $this->checkReturnValues($class, $returnTypes, $nonPublishedClasses); } if (count($nonPublishedClasses)) { $this->fail( "Public type '" . $class . "' references following non-public types:\n" . implode("\n", array_unique($nonPublishedClasses)) ); } } /** * Retrieve list of all interfaces and classes in Magento codebase that are marked with @api annotation. * * @return array * @throws Exception */ public function publicPHPTypesDataProvider(): array { $files = Files::init()->getPhpFiles(Files::INCLUDE_LIBS | Files::INCLUDE_APP_CODE); $result = []; foreach ($files as $file) { $fileContents = \file_get_contents($file); if (strpos($fileContents, '@api') !== false) { $fileClassScanner = new FileClassScanner($file); $className = $fileClassScanner->getClassName(); if (!in_array($className, $this->getWhitelist()) && (class_exists($className) || interface_exists($className)) ) { $result[$className] = [$className]; } } } return $result; } /** * Check if a class is @api annotated * * @param ReflectionClass $class * * @return bool */ private function isPublished(ReflectionClass $class) { return strpos($class->getDocComment(), '@api') !== false; } /** * Simplified check of class relation. * * @param string $classNameA * @param string $classNameB * @return bool */ private function areClassesFromSameVendor($classNameA, $classNameB) { $classNameA = ltrim($classNameA, '\\'); $classNameB = ltrim($classNameB, '\\'); $aVendor = substr($classNameA, 0, strpos($classNameA, '\\')); $bVendor = substr($classNameB, 0, strpos($classNameB, '\\')); return $aVendor === $bVendor; } /** * Check if the class belongs to the list of classes generated by Magento on demand. * * We don't need to check @api annotation coverage for generated classes * * @param string $className * @return bool */ private function isGenerated($className) { return substr($className, -18) === 'ExtensionInterface' || substr($className, -7) === 'Factory'; } /** * Retrieves list of method return types from method doc comment * * Introduced this method to abstract complexity of coping with types in "return" annotation * * @param string $docComment * @return array */ private function getReturnTypesFromDocComment($docComment) { // TODO: add docblock namespace resolving using third-party library if (preg_match('/@return (\S*)/', $docComment, $matches)) { return array_map( 'trim', explode('|', $matches[1]) ); } else { return []; } } /** * Check method return values * * TODO: improve return type filtration * * @param string $class * @param array $returnTypes * @param array $nonPublishedClasses * @return mixed */ private function checkReturnValues($class, array $returnTypes, array $nonPublishedClasses) { foreach ($returnTypes as $returnType) { if (!in_array($returnType, $this->simpleReturnTypes) && !$this->isGenerated($returnType) && \class_exists($returnType) ) { $returnTypeReflection = new ReflectionClass($returnType); if (!$returnTypeReflection->isInternal() && $this->areClassesFromSameVendor($returnType, $class) && !$this->isPublished($returnTypeReflection) ) { $nonPublishedClasses[$returnType] = $returnType; } } } return $nonPublishedClasses; } /** * Check if all method parameters are public * * @param string $class * @param ReflectionMethod $method * @param array $nonPublishedClasses * * @return array * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ private function checkParameters($class, ReflectionMethod $method, array $nonPublishedClasses) { /* Ignoring docblocks for argument types */ foreach ($method->getParameters() as $parameter) { $parameterType = $parameter->getType(); if ($parameterType && method_exists($parameterType, 'isBuiltin') && !$parameterType->isBuiltin() && !$this->isGenerated($parameterType->getName()) ) { $parameterClass = new ReflectionClass($parameterType->getName()); /* * We don't want to check integrity of @api coverage of classes * that belong to different vendors, because it is too complicated. * Example: * If Magento class references non-@api annotated class from Zend, * we don't want to fail test, because Zend is considered public by default, * and we don't care if Zend classes are @api-annotated */ if ($parameterClass && !$parameterClass->isInternal() && $this->areClassesFromSameVendor($parameterClass->getName(), $class) && !$this->isPublished($parameterClass) ) { $nonPublishedClasses[$parameterClass->getName()] = $parameterClass->getName(); } } } return $nonPublishedClasses; } }