This commit is contained in:
2026-09-15 10:26:34 +08:00
commit ecf36afa56
10493 changed files with 1404888 additions and 0 deletions
@@ -0,0 +1,57 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\CodeMessDetector\Rule\Design;
use PDepend\Source\AST\ASTClass;
use PHPMD\AbstractNode;
use PHPMD\AbstractRule;
use PHPMD\Node\ClassNode;
use PHPMD\Rule\ClassAware;
use Magento\Framework\App\ActionInterface;
/**
* Actions must process a defined list of HTTP methods.
*/
class AllPurposeAction extends AbstractRule implements ClassAware
{
/**
* @inheritdoc
*
* @param ClassNode|ASTClass $node
*/
public function apply(AbstractNode $node)
{
// Skip validation for Abstract Controllers
if ($node->isAbstract()) {
return;
}
try {
if (!class_exists($node->getFullQualifiedName(), true)) {
return;
}
$impl = class_implements($node->getFullQualifiedName(), true);
} catch (\Throwable $exception) {
//Couldn't load a class.
return;
}
if (is_array($impl) && in_array(ActionInterface::class, $impl, true)) {
$methodsDefined = false;
foreach ($impl as $i) {
if (preg_match('/\\\Http[a-z]+ActionInterface$/i', $i)) {
$methodsDefined = true;
break;
}
}
if (!$methodsDefined) {
$this->addViolation($node, [$node->getFullQualifiedName()]);
}
}
}
}
@@ -0,0 +1,230 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\CodeMessDetector\Rule\Design;
use Magento\Framework\GetParameterClassTrait;
use Magento\Framework\Session\SessionManagerInterface;
use Magento\Framework\Stdlib\Cookie\CookieReaderInterface;
use PDepend\Source\AST\ASTClass;
use PHPMD\AbstractNode;
use PHPMD\AbstractRule;
use PHPMD\Node\ClassNode;
use PHPMD\Rule\ClassAware;
/**
* Session and Cookies must be used only in HTML Presentation layer.
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class CookieAndSessionMisuse extends AbstractRule implements ClassAware
{
use GetParameterClassTrait;
/**
* Is given class a controller?
*
* @param \ReflectionClass $class
* @return bool
*/
private function isController(\ReflectionClass $class): bool
{
return $class->isSubclassOf(\Magento\Framework\App\ActionInterface::class);
}
/**
* Is given class a block?
*
* @param \ReflectionClass $class
* @return bool
*/
private function isBlock(\ReflectionClass $class): bool
{
return $class->isSubclassOf(\Magento\Framework\View\Element\BlockInterface::class);
}
/**
* Is given class an HTML UI data provider?
*
* @param \ReflectionClass $class
* @return bool
*/
private function isUiDataProvider(\ReflectionClass $class): bool
{
return $class->isSubclassOf(
\Magento\Framework\View\Element\UiComponent\DataProvider\DataProviderInterface::class
);
}
/**
* Is given class a Layout Processor?
*
* @param \ReflectionClass $class
* @return bool
*/
private function isLayoutProcessor(\ReflectionClass $class): bool
{
return $class->isSubclassOf(
\Magento\Checkout\Block\Checkout\LayoutProcessorInterface::class
);
}
/**
* Is given class a View Model?
*
* @param \ReflectionClass $class
* @return bool
*/
private function isViewModel(\ReflectionClass $class): bool
{
return $class->isSubclassOf(
\Magento\Framework\View\Element\Block\ArgumentInterface::class
);
}
/**
* Is given class an HTML UI Document?
*
* @param \ReflectionClass $class
* @return bool
*/
private function isUiDocument(\ReflectionClass $class): bool
{
return $class->isSubclassOf(\Magento\Framework\View\Element\UiComponent\DataProvider\Document::class)
|| $class->getName() === \Magento\Framework\View\Element\UiComponent\DataProvider\Document::class;
}
/**
* Is given class a plugin for controllers?
*
* @param \ReflectionClass $class
* @return bool
*/
private function isControllerPlugin(\ReflectionClass $class): bool
{
foreach ($class->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
if (preg_match('/^(after|around|before).+/i', $method->getName())) {
try {
$parameters = $method->getParameters();
if (count($parameters) === 0) {
continue;
}
$argument = $this->getParameterClass($parameters[0]);
} catch (\Throwable $exception) {
//Non-existing class (autogenerated perhaps) or doesn't have an argument.
continue;
}
if ($argument) {
$isAction = $argument->isSubclassOf(\Magento\Framework\App\ActionInterface::class)
|| $argument->getName() === \Magento\Framework\App\ActionInterface::class;
if ($isAction) {
return true;
}
}
}
}
return false;
}
/**
* Is given class a plugin for blocks?
*
* @param \ReflectionClass $class
* @return bool
*/
private function isBlockPlugin(\ReflectionClass $class): bool
{
foreach ($class->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
if (preg_match('/^(after|around|before).+/i', $method->getName())) {
try {
$parameters = $method->getParameters();
if (count($parameters) === 0) {
continue;
}
$argument = $this->getParameterClass($parameters[0]);
} catch (\Throwable $exception) {
//Non-existing class (autogenerated perhaps) or doesn't have an argument.
continue;
}
if ($argument) {
$isBlock = $argument->isSubclassOf(\Magento\Framework\View\Element\BlockInterface::class)
|| $argument->getName() === \Magento\Framework\View\Element\BlockInterface::class;
if ($isBlock) {
return true;
}
}
}
}
return false;
}
/**
* Whether given class depends on classes to pay attention to.
*
* @param \ReflectionClass $class
* @return bool
*/
private function doesUseRestrictedClasses(\ReflectionClass $class): bool
{
$constructor = $class->getConstructor();
if ($constructor) {
foreach ($constructor->getParameters() as $argument) {
try {
$class = $this->getParameterClass($argument);
if ($class === null) {
continue;
}
if ($class->isSubclassOf(SessionManagerInterface::class)
|| $class->getName() === SessionManagerInterface::class
|| $class->isSubclassOf(CookieReaderInterface::class)
|| $class->getName() === CookieReaderInterface::class
) {
return true;
}
} catch (\ReflectionException $exception) {
//Failed to load the argument's class information
continue;
}
}
}
return false;
}
/**
* @inheritdoc
*
* @param ClassNode|ASTClass $node
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
public function apply(AbstractNode $node)
{
try {
$class = new \ReflectionClass($node->getFullQualifiedName());
} catch (\Throwable $exception) {
//Failed to load class, nothing we can do
return;
}
if ($this->doesUseRestrictedClasses($class)) {
if (!$this->isController($class)
&& !$this->isBlock($class)
&& !$this->isUiDataProvider($class)
&& !$this->isUiDocument($class)
&& !$this->isControllerPlugin($class)
&& !$this->isBlockPlugin($class)
&& !$this->isLayoutProcessor($class)
&& !$this->isViewModel($class)
) {
$this->addViolation($node, [$node->getFullQualifiedName()]);
}
}
}
}
@@ -0,0 +1,103 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\CodeMessDetector\Rule\UnusedCode;
use PHPMD\AbstractNode;
use PHPMD\Node\ClassNode;
use PHPMD\Node\MethodNode;
use PHPMD\Rule\UnusedFormalParameter as PhpmdUnusedFormalParameter;
class UnusedFormalParameter extends PhpmdUnusedFormalParameter
{
/**
* This method collects all local variables in the body of the currently
* analyzed method or function and removes those parameters that are
* referenced by one of the collected variables.
*
* @param AbstractNode $node
* @return void
*/
protected function removeUsedParameters(AbstractNode $node)
{
parent::removeUsedParameters($node);
$this->removeVariablesUsedInPlugins($node);
}
/**
* Remove required method variables used in plugins from given node
*
* @param AbstractNode $node
*/
private function removeVariablesUsedInPlugins(AbstractNode $node)
{
if (!$node instanceof MethodNode) {
return;
}
/** @var ClassNode $classNode */
$classNode = $node->getParentType();
if (!$this->isPluginClass($classNode->getNamespaceName())) {
return;
}
/**
* Around and After plugins has 2 required params $subject and $proceed or $result
* that should be ignored
*/
foreach (['around', 'after'] as $pluginMethodPrefix) {
if ($this->isFunctionNameStartingWith($node, $pluginMethodPrefix)) {
$this->removeVariablesByCount(2);
break;
}
}
/**
* Before plugins has 1 required params $subject
* that should be ignored
*/
if ($this->isFunctionNameStartingWith($node, 'before')) {
$this->removeVariablesByCount(1);
}
}
/**
* Check if the first part of function fully qualified name is equal to $name
*
* Methods getImage and getName are equal. getImage used prior to usage in phpmd source
*
* @param MethodNode $node
* @param string $name
* @return boolean
*/
private function isFunctionNameStartingWith(MethodNode $node, string $name): bool
{
return (0 === strpos($node->getImage(), $name));
}
/**
* Remove first $countOfRemovingVariables from given node
*
* @param int $countOfRemovingVariables
*/
private function removeVariablesByCount(int $countOfRemovingVariables)
{
array_splice($this->nodes, 0, $countOfRemovingVariables);
}
/**
* Check if namespace contain "Plugin". Case-sensitive ignored
*
* @param string $class
* @return bool
*/
private function isPluginClass(string $class): bool
{
return (stripos($class, 'plugin') !== false);
}
}
@@ -0,0 +1,138 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\CodeMessDetector\Test\Unit\Rule\Design;
use Magento\CodeMessDetector\Rule\Design\AllPurposeAction;
use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\App\ActionInterface;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\MockObject\MockObject as MockObject;
use PHPUnit\Framework\MockObject\Builder\InvocationMocker;
use PHPMD\Report;
use PHPMD\Node\ClassNode;
class AllPurposeActionTest extends TestCase
{
/**
* @param object $fakeAction
* @param bool $violates
*
* @dataProvider getCases
*/
public function testApply($fakeAction, bool $violates)
{
$node = $this->createNodeMock($fakeAction);
$rule = new AllPurposeAction();
$this->expectsRuleViolation($rule, $violates);
$rule->apply($node);
}
/**
* @return array
*/
public function getCases(): array
{
return [
[
new class implements ActionInterface, HttpGetActionInterface
{
/**
* @inheritDoc
*/
public function execute()
{
return null;
}
},
false
],
[
new class implements ActionInterface
{
/**
* @inheritDoc
*/
public function execute()
{
return null;
}
},
true
],
[
new class implements HttpGetActionInterface
{
/**
* @inheritDoc
*/
public function execute()
{
return null;
}
},
false
],
[
new class
{
},
false
]
];
}
/**
* @param object $dynamicObject
*
* @return ClassNode|MockObject
*/
private function createNodeMock($dynamicObject): MockObject
{
$node = $this->getMockBuilder(ClassNode::class)
->disableOriginalConstructor()
->disableProxyingToOriginalMethods()
->setMethods(
[
// disable name lookup from AST artifact
'getNamespaceName',
'getParentName',
'getName',
'getFullQualifiedName',
]
)
->getMock();
$node->expects($this->any())
->method('getFullQualifiedName')
->willReturn(get_class($dynamicObject));
return $node;
}
/**
* @param AllPurposeAction $rule
* @param bool $expects
* @return InvocationMocker
*/
private function expectsRuleViolation(AllPurposeAction $rule, bool $expects): InvocationMocker
{
/** @var Report|MockObject $report */
$report = $this->getMockBuilder(Report::class)->getMock();
if ($expects) {
$violationExpectation = $this->atLeastOnce();
} else {
$violationExpectation = $this->never();
}
$invokation = $report->expects($violationExpectation)
->method('addRuleViolation');
$rule->setReport($report);
return $invokation;
}
}
@@ -0,0 +1,178 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\CodeMessDetector\Test\Unit\Rule\UnusedCode;
use Magento\CodeMessDetector\Rule\UnusedCode\UnusedFormalParameter;
use PHPMD\Node\ASTNode;
use PHPMD\Node\MethodNode;
use PHPMD\Report;
use PHPUnit\Framework\MockObject\MockObject as MockObject;
use PHPUnit\Framework\TestCase;
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
class UnusedFormalParameterTest extends TestCase
{
private const FAKE_PLUGIN_NAMESPACE = 'Magento\CodeMessDetector\Test\UnusedCode\Plugin';
private const FAKE_NAMESPACE = 'Magento\CodeMessDetector\Test\UnusedCode';
/**
*
* @dataProvider getCases
*/
public function testApply($methodName, $methodParams, $namespace, $expectViolation)
{
$node = $this->createMethodNodeMock($methodName, $methodParams, $namespace);
$rule = new UnusedFormalParameter();
$this->expectsRuleViolation($rule, $expectViolation);
$rule->apply($node);
}
/**
* Prepare method node mock
*
* @param $methodName
* @param $methodParams
* @param $namespace
* @return MethodNode|MockObject
*/
private function createMethodNodeMock($methodName, $methodParams, $namespace)
{
$methodNode = $this->createConfiguredMock(
MethodNode::class,
[
'getName' => $methodName,
'getImage' => $methodName,
'isAbstract' => false,
'isDeclaration' => true
]
);
$variableDeclarators = [];
foreach ($methodParams as $methodParam) {
$variableDeclarator = $this->createASTNodeMock();
$variableDeclarator->method('getImage')
->willReturn($methodParam);
$variableDeclarators[] = $variableDeclarator;
}
$parametersMock = $this->createASTNodeMock();
$parametersMock->expects($this->once())
->method('findChildrenOfType')
->with('VariableDeclarator')
->willReturn($variableDeclarators);
/**
* Declare mock result for findChildrenOfType
* with Dummy for removeCompoundVariables and removeVariablesUsedByFuncGetArgs
*/
$methodNode->expects($this->atLeastOnce())
->method('findChildrenOfType')
->withConsecutive(['FormalParameters'], ['CompoundVariable'], ['FunctionPostfix'])
->willReturnOnConsecutiveCalls([$parametersMock], [], []);
// Dummy result for removeRegularVariables
$methodNode->expects($this->once())
->method('findChildrenOfTypeVariable')
->willReturn([]);
$classNode = $this->createASTNodeMock();
$classNode->expects($this->once())
->method('getNamespaceName')
->willReturn($namespace);
$methodNode->expects($this->once())
->method('getParentType')
->willReturn($classNode);
return $methodNode;
}
/**
* Create ASTNode mock
*
* @return ASTNode|MockObject
*/
private function createASTNodeMock()
{
return $this->createMock(ASTNode::class);
}
/**
* @param UnusedFormalParameter $rule
* @param bool $expects
*/
private function expectsRuleViolation(UnusedFormalParameter $rule, bool $expects)
{
/** @var Report|MockObject $reportMock */
$reportMock = $this->createMock(Report::class);
if ($expects) {
$violationExpectation = $this->atLeastOnce();
} else {
$violationExpectation = $this->never();
}
$reportMock->expects($violationExpectation)
->method('addRuleViolation');
$rule->setReport($reportMock);
}
/**
* @return array
*/
public function getCases(): array
{
return [
// Plugin methods
[
'beforePluginMethod',
[
'subject'
],
self::FAKE_PLUGIN_NAMESPACE,
false
],
[
'aroundPluginMethod',
[
'subject',
'proceed'
],
self::FAKE_PLUGIN_NAMESPACE,
false
],
[
'aroundPluginMethod',
[
'subject',
'result'
],
self::FAKE_PLUGIN_NAMESPACE,
false
],
// Plugin method that contain unused parameter
[
'someMethod',
[
'unusedParameter'
],
self::FAKE_PLUGIN_NAMESPACE,
true
],
// Non plugin method
[
'someMethod',
[
'subject',
'result'
],
self::FAKE_NAMESPACE,
true
]
];
}
}
@@ -0,0 +1,67 @@
<?xml version="1.0"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<ruleset name="Magento Specific Design Rules"
xmlns="http://pmd.sf.net/ruleset/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd"
xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd">
<rule name="AllPurposeAction"
class="Magento\CodeMessDetector\Rule\Design\AllPurposeAction"
message= "The class {0} does not restrict processed HTTP methods by implementing a Http*Method name*ActionInterface">
<description>
<![CDATA[
Controllers (classes implementing ActionInterface) have to implement marker Http<Method>ActionInterface
to restrict incoming requests by methods.
]]>
</description>
<priority>2</priority>
<properties />
<example>
<![CDATA[
class PostOrder implements ActionInterface
{
public function execute()
{
//I process GET, POST, PATCH etc. while only intended for POST
...
return $response;
}
}
]]>
</example>
</rule>
<rule name="CookieAndSessionMisuse"
class="Magento\CodeMessDetector\Rule\Design\CookieAndSessionMisuse"
message= "The class {0} uses sessions or cookies while not being a part of HTML Presentation layer">
<description>
<![CDATA[
Sessions and cookies must only be used in classes directly responsible for HTML presentation because Web APIs do not
rely on cookies and sessions. If you need to get current user use Magento\Authorization\Model\UserContextInterface
]]>
</description>
<priority>2</priority>
<properties />
<example>
<![CDATA[
class OrderProcessor
{
public function __construct(SessionManagerInterface $session) {
$this->session = $session;
}
public function place(OrderInterface $order)
{
//Will not be present if processing a WebAPI request
$currentOrder = $this->session->get('current_order');
...
}
}
]]>
</example>
</rule>
</ruleset>
@@ -0,0 +1,31 @@
<?xml version="1.0"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<ruleset name="Unused Code Rules"
xmlns="http://pmd.sf.net/ruleset/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd"
xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd">
<rule name="UnusedFormalParameter"
message="Avoid unused parameters such as '{0}'."
class="Magento\CodeMessDetector\Rule\UnusedCode\UnusedFormalParameter">
<description><![CDATA[Avoid passing parameters to methods or constructors and then not using those parameters except on plugins]]></description>
<example>
<![CDATA[
class Foo
{
private function bar($howdy)
{
// $howdy is not used
}
}
]]>
</example>
</rule>
</ruleset>
@@ -0,0 +1,157 @@
<?php
/**
* Copyright 2019 Adobe
* All Rights Reserved.
*/
declare(strict_types=1);
namespace Magento\PhpStan\Formatters;
use PHPStan\Command\AnalysisResult;
use PHPStan\Command\ErrorFormatter\TableErrorFormatter;
use PHPStan\Command\ErrorFormatter\ErrorFormatter;
use PHPStan\Command\Output;
/**
* To mute the PHPStan error message add a comment above the reported error line.
*
* Example of usage:
*
* // phpstan:ignore "Method Magento\TestModule\TestClass::testMethod() invoked with 1 parameter, 0 required."
* $this->testMethod(1);
*
* or replace some part of error message with *
*
* // phpstan:ignore "Method * invoked with 1 parameter, 0 required."
* $this->testMethod(1);
*
* or just
*
* // phpstan:ignore
* $this->testMethod(1);
*
* or
*
* $this->testMethod(1); // phpstan:ignore
*
* The error message will be suppressed.
*
* @see \Magento\PhpStan\Formatters\Fixtures\ClassWithIgnoreAnnotation
*/
class FilteredErrorFormatter implements ErrorFormatter
{
private const MUTE_ERROR_ANNOTATION = 'phpstan:ignore';
private const NO_ERRORS = 0;
/**
* @var TableErrorFormatter
*/
private $tableErrorFormatter;
/**
* @param TableErrorFormatter $tableErrorFormatter
*/
public function __construct(TableErrorFormatter $tableErrorFormatter)
{
$this->tableErrorFormatter = $tableErrorFormatter;
}
/**
* @inheritdoc
*/
public function formatErrors(AnalysisResult $analysisResult, Output $output): int
{
if (!$analysisResult->hasErrors()) {
$output->getStyle()->success('No errors');
return self::NO_ERRORS;
}
//@phpstan:ignore-line
$clearedAnalysisResult = new AnalysisResult(
$this->clearIgnoredErrors($analysisResult->getFileSpecificErrors()),
$analysisResult->getNotFileSpecificErrors(),
$analysisResult->getInternalErrors(),
$analysisResult->getWarnings(),
$analysisResult->getCollectedData(),
$analysisResult->isDefaultLevelUsed(),
$analysisResult->getProjectConfigFile(),
$analysisResult->isResultCacheSaved(),
$analysisResult->getPeakMemoryUsageBytes(),
$analysisResult->isResultCacheUsed(),
$analysisResult->getChangedProjectExtensionFilesOutsideOfAnalysedPaths()
);
return $this->tableErrorFormatter->formatErrors($clearedAnalysisResult, $output);
}
/**
* Filters error list.
*
* @param array $fileSpecificErrors
* @return array
*/
private function clearIgnoredErrors(array $fileSpecificErrors): array
{
foreach ($fileSpecificErrors as $index => $error) {
$fileName = $error->getFile();
// phpcs:ignore Magento2.Functions.DiscouragedFunction
if (!file_exists($fileName)) {
continue;
}
$line = $error->getLine() ? $this->getLineWithMuteErrorAnnotation($error->getLine(), $fileName) : null;
if ($line === null) {
continue;
}
$extractErrorPattern = '@' . self::MUTE_ERROR_ANNOTATION . '\s+"(.*?)"@';
$errorPattern = preg_match($extractErrorPattern, $line, $result) ? $this->preparePattern($result[1]) : '';
if ($errorPattern && !preg_match('@' . $errorPattern . '@i', $error->getMessage())) {
continue;
}
unset($fileSpecificErrors[$index]);
}
return $fileSpecificErrors;
}
/**
* Returns context of the line with mute error annotation.
*
* @param int $errorLine
* @param string $fileName
* @return string|null
*/
private function getLineWithMuteErrorAnnotation(int $errorLine, string $fileName): ?string
{
$file = new \SplFileObject($fileName);
$lineNumbersToCheck = [
$errorLine - 2, // the line above to the line that caused the error
$errorLine - 1, // the line that caused the error
$errorLine - 3, // the line two lines above to the line that caused the error
];
foreach ($lineNumbersToCheck as $lineNumber) {
$file->seek($lineNumber > 0 ? $lineNumber : 0);
$line = $file->current();
if (strpos($line, self::MUTE_ERROR_ANNOTATION) !== false) {
return $line;
}
}
return null;
}
/**
* Prepares error pattern.
*
* @param string $errorDescription
* @return string
*/
private function preparePattern(string $errorDescription)
{
// phpcs:ignore Magento2.Functions.DiscouragedFunction
return str_replace('*', '(?:.*?)', addcslashes(trim($errorDescription), '\()[]'));
}
}
@@ -0,0 +1,105 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\PhpStan\Reflection\Php;
use Magento\Framework\DataObject;
use Magento\Framework\Session\SessionManager;
use PHPStan\DependencyInjection\Container;
use PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\MethodsClassReflectionExtension;
/**
* Extension to add support of magic methods (get/set/uns/has) based on @see DataObject
*/
class DataObjectClassReflectionExtension implements MethodsClassReflectionExtension
{
private const MAGIC_METHODS_PREFIXES = [
'get',
'set',
'uns',
'has'
];
private const PREFIX_LENGTH = 3;
/**
* @var Container
*/
private $container;
/**
* @var AnnotationsMethodsClassReflectionExtension
*/
private $annotationsMethodsClassReflectionExtension;
/**
* @param Container $container
*/
public function __construct(Container $container)
{
$this->container = $container;
$this->annotationsMethodsClassReflectionExtension = $this->container
->getByType(AnnotationsMethodsClassReflectionExtension::class);
}
/**
* Check if class has relations with DataObject and requested method can be considered as a magic method.
*
* @param ClassReflection $classReflection
* @param string $methodName
*
* @return bool
*/
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
{
// Workaround due to annotation extension always loads last.
if ($this->annotationsMethodsClassReflectionExtension->hasMethod($classReflection, $methodName)) {
// In case when annotation already available for the method, we will not use 'magic methods' approach.
return false;
}
if ($classReflection->isSubclassOf(DataObject::class) || $classReflection->getName() == DataObject::class) {
return in_array($this->getPrefix($methodName), self::MAGIC_METHODS_PREFIXES);
}
/** SessionManager redirects all calls to `__call` to container which extends DataObject */
if ($classReflection->isSubclassOf(SessionManager::class)
|| $classReflection->getName() === SessionManager::class
) {
/** @see \Magento\Framework\Session\SessionManager::__call */
return in_array($this->getPrefix($methodName), self::MAGIC_METHODS_PREFIXES);
}
return false;
}
/**
* Get prefix from method name.
*
* @param string $methodName
*
* @return string
*/
private function getPrefix(string $methodName): string
{
return (string)substr($methodName, 0, self::PREFIX_LENGTH);
}
/**
* Get method reflection instance.
*
* @param ClassReflection $classReflection
* @param string $methodName
*
* @return MethodReflection
*/
public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection
{
return new DataObjectMethodReflection($classReflection, $methodName);
}
}
@@ -0,0 +1,269 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\PhpStan\Reflection\Php;
use PHPStan\Reflection\ClassMemberReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\FunctionVariant;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Reflection\Php\DummyParameter;
use PHPStan\TrinaryLogic;
use PHPStan\Type\BooleanType;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\IntegerType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use PHPStan\Type\VoidType;
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class DataObjectMethodReflection implements MethodReflection
{
private const PREFIX_LENGTH = 3;
/**
* @var ClassReflection
*/
private $classReflection;
/**
* @var string
*/
private $methodName;
/**
* @param ClassReflection $classReflection
* @param string $methodName
*/
public function __construct(ClassReflection $classReflection, string $methodName)
{
$this->classReflection = $classReflection;
$this->methodName = $methodName;
}
/**
* Get methods class reflection.
*
* @return ClassReflection
*/
public function getDeclaringClass(): ClassReflection
{
return $this->classReflection;
}
/**
* Get if method is static.
*
* @return bool
*/
public function isStatic(): bool
{
return false;
}
/**
* Get if method is private.
*
* @return bool
*/
public function isPrivate(): bool
{
return false;
}
/**
* Get if method is public.
*
* @return bool
*/
public function isPublic(): bool
{
return true;
}
/**
* Get Method PHP Doc comment message.
*
* @return string|null
*/
public function getDocComment(): ?string
{
return null;
}
/**
* Get Method Name.
*
* @return string
*/
public function getName(): string
{
return $this->methodName;
}
/**
* Get Prototype.
*
* @return ClassMemberReflection
*/
public function getPrototype(): ClassMemberReflection
{
return $this;
}
/**
* Get Magic Methods variant based on type (get/set/has/uns).
*
* @return ParametersAcceptor[]
*/
public function getVariants(): array
{
return [
new FunctionVariant(
TemplateTypeMap::createEmpty(),
TemplateTypeMap::createEmpty(),
$this->getMethodParameters(),
false,
$this->getReturnType()
)
];
}
/**
* Get prefix from method name.
*
* @return string
*/
private function getMethodNamePrefix(): string
{
return (string)substr($this->methodName, 0, self::PREFIX_LENGTH);
}
/**
* Get Magic Methods parameters.
*
* @return ParameterReflection[]
*/
private function getMethodParameters(): array
{
$params = [];
switch ($this->getMethodNamePrefix()) {
case 'set':
$params[] = new DummyParameter(
'value',
new MixedType(),
false,
null,
false,
null
);
break;
case 'get':
$params[] = new DummyParameter(
'index',
new UnionType([new StringType(), new IntegerType()]),
true,
null,
false,
null
);
break;
}
return $params;
}
/**
* Get Magic Methods return type.
*
* @return Type
*/
private function getReturnType(): Type
{
switch ($this->getMethodNamePrefix()) {
case 'set':
case 'uns':
$returnType = new ObjectType($this->classReflection->getName());
break;
case 'has':
$returnType = new BooleanType();
break;
default:
$returnType = new MixedType();
break;
}
return $returnType;
}
/**
* Get if method is deprecated.
*
* @return TrinaryLogic
*/
public function isDeprecated(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
/**
* Get deprecated description.
*
* @return string|null
*/
public function getDeprecatedDescription(): ?string
{
return null;
}
/**
* Get if method is final.
*
* @return TrinaryLogic
*/
public function isFinal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
/**
* Get if method is internal.
*
* @return TrinaryLogic
*/
public function isInternal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
/**
* Get method throw type.
*
* @return Type|null
*/
public function getThrowType(): ?Type
{
return new VoidType();
}
/**
* Get if method has side effect.
*
* @return TrinaryLogic
*/
public function hasSideEffects(): TrinaryLogic
{
return TrinaryLogic::createYes();
}
}
@@ -0,0 +1,34 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
// phpcs:disable
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\Code\Generator\Io;
use Magento\Framework\Filesystem\Driver\File;
use Magento\Framework\TestFramework\Unit\Autoloader\ExtensionAttributesGenerator;
use Magento\Framework\TestFramework\Unit\Autoloader\ExtensionAttributesInterfaceGenerator;
use Magento\Framework\TestFramework\Unit\Autoloader\FactoryGenerator;
use Magento\Framework\TestFramework\Unit\Autoloader\GeneratedClassesAutoloader;
if (!defined('TESTS_TEMP_DIR')) {
//phpcs:ignore Magento2.Functions.DiscouragedFunction
define('TESTS_TEMP_DIR', dirname(__DIR__) . '/../../tmp');
}
$generatorIo = new Io(
new File(),
TESTS_TEMP_DIR . '/' . DirectoryList::getDefaultConfig()[DirectoryList::GENERATED_CODE][DirectoryList::PATH]
);
$generatedCodeAutoloader = new GeneratedClassesAutoloader(
[
new ExtensionAttributesGenerator(),
new ExtensionAttributesInterfaceGenerator(),
new FactoryGenerator(),
],
$generatorIo
);
spl_autoload_register([$generatedCodeAutoloader, 'load']);
@@ -0,0 +1,18 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\CodingStandard\Tool;
interface BlacklistInterface
{
/**
* Set list of paths to be excluded from tool run
*
* @param array $blackList
* @return void
*/
public function setBlackList(array $blackList);
}
@@ -0,0 +1,73 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
/**
* PHP Code Mess v1.3.3 tool wrapper
*/
namespace Magento\TestFramework\CodingStandard\Tool;
use \Magento\TestFramework\CodingStandard\ToolInterface;
use Magento\TestFramework\CodingStandard\Tool\CodeMessOutput;
class CodeMessDetector implements ToolInterface
{
/**
* Ruleset directory
*
* @var string
*/
private $rulesetFile;
/**
* @var string
*/
private $reportFile;
/**
* @param string $rulesetFile \Directory that locates the inspection rules
* @param string $reportFile Destination file to write inspection report to
*/
public function __construct($rulesetFile, $reportFile)
{
$this->reportFile = $reportFile;
$this->rulesetFile = $rulesetFile;
}
/**
* Whether the tool can be ran on the current environment
*
* @return bool
*/
public function canRun()
{
return class_exists(\PHPMD\TextUI\Command::class);
}
/**
* @inheritdoc
*/
public function run(array $whiteList)
{
if (empty($whiteList)) {
return \PHPMD\TextUI\Command::EXIT_SUCCESS;
}
$commandLineArguments = [
'run_file_mock', //emulate script name in console arguments
implode(',', $whiteList),
'text', //report format
$this->rulesetFile,
'--reportfile',
$this->reportFile,
];
$options = new \PHPMD\TextUI\CommandLineOptions($commandLineArguments);
$command = new \PHPMD\TextUI\Command(new CodeMessOutput());
return $command->run($options, new \PHPMD\RuleSetFactory());
}
}
@@ -0,0 +1,25 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
/**
* PHP Code Mess v1.3.3 tool wrapper
*/
declare(strict_types=1);
namespace Magento\TestFramework\CodingStandard\Tool;
use PHPMD\Console\Output;
class CodeMessOutput extends Output
{
/**
* @inheritdoc
*/
protected function doWrite($message)
{
// TODO: Implement doWrite() method.
}
}
@@ -0,0 +1,112 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\CodingStandard\Tool;
use Magento\TestFramework\CodingStandard\Tool\CodeSniffer\Wrapper;
use Magento\TestFramework\CodingStandard\ToolInterface;
/**
* PHP Code Sniffer tool wrapper
*/
class CodeSniffer implements ToolInterface, ExtensionInterface
{
/**
* Ruleset directory
*
* @var string
*/
private $rulesetDir;
/**
* Report file
*
* @var string
*/
private $reportFile;
/**
* PHPCS cli tool wrapper
*
* @var Wrapper
*/
private $wrapper;
/**
* List of extensions for tool run
*
* @var array
*/
private $extensions = [
'php' => 'PHP',
'phtml' => 'PHP',
];
/**
* Constructor
*
* @param string $rulesetDir \Directory that locates the inspection rules
* @param string $reportFile Destination file to write inspection report to
* @param Wrapper $wrapper
*/
public function __construct($rulesetDir, $reportFile, Wrapper $wrapper)
{
$this->rulesetDir = $rulesetDir;
// phpcs:ignore Magento2.Functions.DiscouragedFunction
if (!file_exists($rulesetDir) && file_exists($fullPath = realpath(__DIR__ . '/../../../../' . $rulesetDir))) {
$this->rulesetDir = $fullPath;
}
$this->reportFile = $reportFile;
$this->wrapper = $wrapper;
}
/**
* @inheritdoc
*/
public function setExtensions(array $extensions)
{
$this->extensions = $extensions;
}
/**
* Whether the tool can be ran on the current environment
*
* @return bool
*/
public function canRun()
{
return class_exists('\PHP_CodeSniffer\Runner');
}
/**
* @inheritdoc
*/
public function run(array $whiteList)
{
if (empty($whiteList)) {
return 0;
}
if (!defined('PHP_CODESNIFFER_IN_TESTS')) {
define('PHP_CODESNIFFER_IN_TESTS', true);
}
$this->wrapper->checkRequirements();
$settings = [];
$settings['files'] = $whiteList;
$settings['standards'] = [$this->rulesetDir];
$settings['extensions'] = $this->extensions;
$settings['reports']['full'] = $this->reportFile;
$settings['reportWidth'] = 120;
$this->wrapper->setSettings($settings);
// phpcs:ignore Magento2.Functions.DiscouragedFunction
ob_start();
$result = $this->wrapper->runPHPCS();
ob_end_clean();
return $result;
}
}
@@ -0,0 +1,70 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
/**
* PHP Code Sniffer Cli wrapper
*/
namespace Magento\TestFramework\CodingStandard\Tool\CodeSniffer;
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Runner;
/**
* PHP Code Sniffer wrapper class
*/
class Wrapper extends Runner
{
/**
* @var array
*/
private $settings = [];
/**
* Return the current version of php code sniffer
*
* @return string
*/
public function version()
{
$version = '0.0.0';
if (defined('\PHP_CodeSniffer\Config::VERSION')) {
$version = Config::VERSION;
}
return $version;
}
/**
* Initialize PHPCS runner and modifies the configuration settings
*
* @throws \PHP_CodeSniffer\Exceptions\DeepExitException
*/
public function init()
{
$this->config->extensions = $this->settings['extensions'];
unset($this->settings['extensions']);
$settings = $this->config->getSettings();
unset($settings['files']);
$this->config->setSettings($settings);
$this->config->setSettings(array_replace_recursive(
$this->config->getSettings(),
$this->settings
));
return parent::init();
}
/**
* Sets the settings
*
* @param array $settings
*/
public function setSettings($settings)
{
$this->settings = $settings;
}
}
@@ -0,0 +1,22 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\TestFramework\CodingStandard\Tool;
/**
* Defines an interface for compatibility checks against a specific version.
*/
interface CompatibilityInterface
{
/**
* Sets the version against which to test code.
*
* @param string $version
* @return void
*/
public function setTestVersion(string $version): void;
}
@@ -0,0 +1,174 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\TestFramework\CodingStandard\Tool;
use Magento\TestFramework\CodingStandard\ToolInterface;
use SebastianBergmann\FileIterator\Facade;
use SebastianBergmann\PHPCPD\Detector\Detector;
use SebastianBergmann\PHPCPD\Detector\Strategy\DefaultStrategy;
use SebastianBergmann\PHPCPD\Log\PMD;
use SebastianBergmann\PHPCPD\Log\Text;
use Symfony\Component\Finder\Finder;
/**
* PHP Copy Paste Detector tool wrapper
*/
class CopyPasteDetector implements ToolInterface, BlacklistInterface
{
/**
* Minimum number of equal lines to identify a copy paste snippet
*/
private const MIN_LINES = 13;
/**
* Destination file to write inspection report to
*
* @var string
*/
private $reportFile;
/**
* List of paths to be excluded from tool run
*
* @var array
*/
private $blacklist;
/**
* @param string $reportFile
*/
public function __construct(string $reportFile)
{
$this->reportFile = $reportFile;
}
/**
* @inheritdoc
*/
public function setBlackList(array $blackList): void
{
$this->blacklist = $blackList;
}
/**
* Whether the tool can be run in the current environment
*
* @SuppressWarnings(PHPMD.UnusedLocalVariable)
*
* @return bool
*/
public function canRun(): bool
{
return class_exists(Detector::class)
&& class_exists(Facade::class)
&& class_exists(Finder::class);
}
/**
* Run tool for files specified
*
* @param array $whiteList Files/directories to be inspected
* @return bool
*/
public function run(array $whiteList): bool
{
$clones = (new Detector(new DefaultStrategy()))->copyPasteDetection(
(new Facade())->getFilesAsArray(
$whiteList,
'',
'',
$this->getExclude()
),
self::MIN_LINES
);
(new PMD($this->reportFile))->processClones($clones);
(new Text)->printResult($clones, false);
return count($clones) === 0;
}
/**
* Get exclude params from blacklist
*
* @return string[]
*/
private function getExclude(): array
{
$exclude = [];
$blacklistedDirs = [];
$blacklistedFileNames = [];
$blacklistedPatterns = [];
foreach ($this->blacklist as $file) {
$file = trim($file);
if (!$file) {
continue;
}
$realPath = realpath(BP . '/' . $file);
if ($realPath === false) {
$ext = pathinfo($file, PATHINFO_EXTENSION);
if ($ext != '') {
$blacklistedFileNames[] = $file;
} else {
$blacklistedPatterns[] = $file;
}
continue;
}
$exclude[] = [$realPath];
$blacklistedDirs[] = $file;
}
foreach ($blacklistedPatterns as $pattern) {
$files = $this->find($pattern, false, $blacklistedDirs);
if (empty($files)) {
continue;
}
$exclude[] = $files;
}
foreach ($blacklistedFileNames as $fileName) {
$files = $this->find($fileName, true, $blacklistedDirs);
if (empty($files)) {
continue;
}
$exclude[] = $files;
}
return array_unique(array_merge(...$exclude));
}
/**
* Find all files by pattern
*
* @param string $pattern
* @param bool $searchFiles
* @param array $excludePaths
* @return array
*/
private function find(string $pattern, bool $searchFiles, array $excludePaths): array
{
$finder = new Finder();
$finder->in(BP);
$finder->notPath($excludePaths);
if ($searchFiles) {
$finder->files();
$finder->name($pattern);
} else {
$finder->path($pattern);
}
$result = [];
foreach ($finder as $file) {
$result[] = $file->getRealPath();
}
return $result;
}
}
@@ -0,0 +1,19 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\CodingStandard\Tool;
interface ExtensionInterface
{
/**
* Set extensions for tool to run
* Example: 'php', 'xml', 'phtml', 'css'
*
* @param array $extensions
* @return void
*/
public function setExtensions(array $extensions);
}
@@ -0,0 +1,108 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\TestFramework\CodingStandard\Tool;
use Magento\TestFramework\CodingStandard\ToolInterface;
/**
* PhpStan tool wrapper.
*/
class PhpStan implements ToolInterface
{
/**
* Rule level to be used.
*
* @see https://github.com/phpstan/phpstan#rule-levels
*/
private const RULE_LEVEL = 1;
/**
* Memory limit required by PHPStan for full Magento project scan.
*/
private const MEMORY_LIMIT = '4G';
/**
* Error formatter to be used.
*
* @see https://github.com/phpstan/phpstan#existing-error-formatters-to-be-used
*/
private const ERROR_FORMAT = 'filtered';
/**
* Report file.
*
* @var string
*/
private $reportFile;
/**
* PHPStan configuration file in neon format.
*
* @var string
*/
private $confFile;
/**
* @param string $confFile
* @param string $reportFile
*/
public function __construct(string $confFile, string $reportFile)
{
$this->reportFile = $reportFile;
$this->confFile = $confFile;
}
/**
* @inheritdoc
* @SuppressWarnings(PHPMD.UnusedLocalVariable)
*/
public function canRun(): bool
{
// phpcs:disable Magento2.Security.InsecureFunction
exec($this->getCommand() . ' --version', $output, $exitCode);
return $exitCode === 0;
}
/**
* @inheritdoc
* @SuppressWarnings(PHPMD.UnusedLocalVariable)
*/
public function run(array $whiteList): int
{
if (empty($whiteList)) {
return 0;
}
$command = $this->getCommand() . ' analyse' .
' --level ' . self::RULE_LEVEL .
' --no-progress' .
' --error-format=' . self::ERROR_FORMAT .
' --memory-limit=' . self::MEMORY_LIMIT .
// phpcs:ignore Magento2.Functions.DiscouragedFunction
' --configuration ' . escapeshellarg($this->confFile) .
' ' . implode(' ', $whiteList) .
' > ' . $this->reportFile;
// phpcs:disable Magento2.Security.InsecureFunction
exec($command, $output, $exitCode);
return $exitCode;
}
/**
* Get PHPStan CLI command
*
* @return string
*/
private function getCommand(): string
{
// phpcs:ignore Magento2.Security.IncludeFile
$vendorDir = require BP . '/app/etc/vendor_path.php';
return 'php ' . BP . '/' . $vendorDir . '/bin/phpstan';
}
}
@@ -0,0 +1,28 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
/**
* Code standard tool wrapper interface
*/
namespace Magento\TestFramework\CodingStandard;
interface ToolInterface
{
/**
* Whether the tool can be ran on the current environment
*
* @return bool
*/
public function canRun();
/**
* Run tool for files specified
*
* @param array $whiteList Files/directories to be inspected
* @return int
*/
public function run(array $whiteList);
}
@@ -0,0 +1,43 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\TestFramework\Dependency;
/**
* Class provides dependency rule for analytics.xml config file.
*/
class AnalyticsConfigRule implements RuleInterface
{
/**
* @inheritdoc
*/
public function getDependencyInfo($currentModule, $fileType, $file, &$contents)
{
if ('config' != $fileType || !preg_match('#.*/analytics\.xml$#', $file)) {
return [];
}
$dependenciesInfo = [];
if (preg_match_all('#<[customProvider|reportProvider][^>]*class=[\'"]([^\'"]+)[\'"]#i', $contents, $matches)) {
$classes = array_pop($matches);
foreach ($classes as $class) {
$classParts = explode('\\', $class);
$module = implode('\\', array_slice($classParts, 0, 2));
if (strtolower($currentModule) !== strtolower($module)) {
$dependenciesInfo[] = [
'modules' => [$module],
'type' => RuleInterface::TYPE_HARD,
'source' => $file,
];
}
}
}
return $dependenciesInfo;
}
}
@@ -0,0 +1,72 @@
<?php
/**
* Rule for searching DB dependency
*
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Dependency;
/**
* Class to get DB dependencies information
*/
class DbRule implements \Magento\TestFramework\Dependency\RuleInterface
{
/**
* Map of tables and modules
*
* @var array
*/
protected $_moduleTableMap;
/**
* Constructor
*
* @param array $tables
*/
public function __construct(array $tables)
{
$this->_moduleTableMap = $tables;
}
/**
* Gets alien dependencies information for current module by analyzing file's contents
*
* @param string $currentModule
* @param string $fileType
* @param string $file
* @param string $contents
* @return array
*/
public function getDependencyInfo($currentModule, $fileType, $file, &$contents)
{
if ('php' !== $fileType || !preg_match('#.*/(Setup|Resource|Query)/.*\.php$#', $file)) {
return [];
}
$dependenciesInfo = [];
$unKnowTables = [];
if (preg_match_all('#>gettable(name)?\([\'"]([^\'"]+)[\'"]\)#i', $contents, $matches)) {
$tables = array_pop($matches);
foreach ($tables as $table) {
if (!isset($this->_moduleTableMap[$table])) {
$unKnowTables[$file][$table] = $table;
continue;
}
if (strtolower($currentModule) !== strtolower($this->_moduleTableMap[$table])) {
$dependenciesInfo[] = [
'modules' => [$this->_moduleTableMap[$table]],
'type' => \Magento\TestFramework\Dependency\RuleInterface::TYPE_HARD,
'source' => $table,
];
}
}
}
foreach ($unKnowTables as $tables) {
foreach ($tables as $table) {
$dependenciesInfo[] = ['modules' => ['Unknown'], 'source' => $table];
}
}
return $dependenciesInfo;
}
}
@@ -0,0 +1,166 @@
<?php
/**
* Rule for searching php file dependency
*
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Dependency;
use Magento\Framework\App\Utility\Classes;
use Magento\Framework\App\Utility\Files;
use Magento\TestFramework\Dependency\VirtualType\VirtualTypeMapper;
/**
* Class provide dependency rule for di.xml config files.
*/
class DiRule implements RuleInterface
{
/**
* @var VirtualTypeMapper
*/
private $mapper;
/**
* @var string
*/
private $pattern;
/**
* @param VirtualTypeMapper $mapper
*/
public function __construct(VirtualTypeMapper $mapper)
{
$this->mapper = $mapper;
}
/**
* Get class name pattern.
*
* @return string
* @throws \Exception
*/
private function getPattern()
{
if ($this->pattern === null) {
$this->pattern = '~\b(?<class>(?<module>('
. implode('[_\\\\]|', Files::init()->getNamespaces())
. '[_\\\\])[a-zA-Z0-9]+)[a-zA-Z0-9_\\\\]*)\b~';
}
return $this->pattern;
}
/**
* @var array
*/
private static $tagNameMap = [
'type' => ['name'],
'preference' => [
'type',
'for'
],
'plugin' => ['type'],
'virtualType' => ['type']
];
/**
* Gets alien dependencies information for current module by analyzing file's contents
*
* @param string $currentModule
* @param string $fileType
* @param string $file
* @param string $contents
* @return array
* @throws \Exception
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function getDependencyInfo($currentModule, $fileType, $file, &$contents)
{
//phpcs:ignore Magento2.Functions.DiscouragedFunction
if (pathinfo($file, PATHINFO_BASENAME) !== 'di.xml') {
return [];
}
$dependenciesInfo = [];
$scope = $this->mapper->getScopeFromFile($file);
foreach ($this->fetchPossibleDependencies($contents) as $type => $deps) {
foreach ($deps as $dep) {
$dep = $this->mapper->getType($dep, $scope);
if (preg_match($this->getPattern(), $dep, $matches)) {
$referenceModule = str_replace('_', '\\', $matches['module']);
if ($currentModule === $referenceModule) {
continue;
}
$dependenciesInfo[] = [
'modules' => [$referenceModule],
'type' => $type,
'source' => $matches['class'],
];
}
}
}
return $dependenciesInfo;
}
/**
* Fetch all possible dependencies.
*
* @param string $contents
* @return array
*/
private function fetchPossibleDependencies($contents)
{
$doc = new \DOMDocument();
$doc->loadXML($contents);
return [
RuleInterface::TYPE_SOFT => $this->getSoftDependencies($doc),
RuleInterface::TYPE_HARD => $this->getHardDependencies($doc)
];
}
/**
* Collect soft dependencies.
*
* @param \DOMDocument $doc
* @return array
*/
private function getSoftDependencies(\DOMDocument $doc)
{
$result = [];
foreach (self::$tagNameMap as $tagName => $attributeNames) {
$nodes = $doc->getElementsByTagName($tagName);
/** @var \DOMElement $node */
foreach ($nodes as $node) {
if ($tagName === 'virtualType' && !$node->getAttribute('type')) {
$result[] = Classes::resolveVirtualType($node->getAttribute('name'));
continue;
}
foreach ($attributeNames as $attributeName) {
$result[] = $node->getAttribute($attributeName);
}
}
}
return $result;
}
/**
* Collect hard dependencies.
*
* @param \DOMDocument $doc
* @return array
*/
private function getHardDependencies(\DOMDocument $doc)
{
$result = [];
$xpath = new \DOMXPath($doc);
$textNodes = $xpath->query('//*[@xsi:type="object"]');
/** @var \DOMElement $node */
foreach ($textNodes as $node) {
$result[] = $node->nodeValue;
}
return $result;
}
}
@@ -0,0 +1,512 @@
<?php
/**
* Rule for searching dependencies in layout files
*
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Dependency;
class LayoutRule implements \Magento\TestFramework\Dependency\RuleInterface
{
/**
* Default modules list.
*
* @var array
*/
protected $_defaultModules = [
'frontend' => 'Magento\Theme',
'adminhtml' => 'Magento\Adminhtml',
];
/**
* Namespaces to analyze
*
* Format: {Namespace}|{Namespace}|...
*
* @var string
*/
protected $_namespaces;
/**
* List of routers
*
* Format: array(
* '{Router}' => '{Module_Name}'
* )
*
* @var array
*/
protected $_mapRouters = [];
/**
* List of layout blocks
*
* Format: array(
* '{Area}' => array(
* '{Block_Name}' => array('{Module_Name}' => '{Module_Name}')
* ))
*
* @var array
*/
protected $_mapLayoutBlocks = [];
/**
* List of layout handles
*
* Format: array(
* '{Area}' => array(
* '{Handle_Name}' => array('{Module_Name}' => '{Module_Name}')
* ))
*
* @var array
*/
protected $_mapLayoutHandles = [];
/**
* Unknown layout handle
*/
const EXCEPTION_TYPE_UNKNOWN_HANDLE = 'UNKNOWN_HANDLE';
/**
* Unknown layout block
*/
const EXCEPTION_TYPE_UNKNOWN_BLOCK = 'UNKNOWN_BLOCK';
/**
* Undefined dependency
*/
const EXCEPTION_TYPE_UNDEFINED_DEPENDENCY = 'UNDEFINED_DEPENDENCY';
/**
* Constructor
*
* @param array $mapRouters
* @param array $mapLayoutBlocks
* @param array $mapLayoutHandles
*/
public function __construct(array $mapRouters, array $mapLayoutBlocks, array $mapLayoutHandles)
{
$this->_mapRouters = $mapRouters;
$this->_mapLayoutBlocks = $mapLayoutBlocks;
$this->_mapLayoutHandles = $mapLayoutHandles;
$this->_namespaces = implode('|', \Magento\Framework\App\Utility\Files::init()->getNamespaces());
}
/**
* Retrieve dependencies information for current module
*
* @param string $currentModule
* @param string $fileType
* @param string $file
* @param string $contents
* @return array
*/
public function getDependencyInfo($currentModule, $fileType, $file, &$contents)
{
if ('layout' != $fileType) {
return [];
}
$attributes = $this->_caseAttributeModule($currentModule, $contents);
$blocks = $this->_caseElementBlock($currentModule, $contents);
$actions = $this->_caseElementAction($currentModule, $contents);
$handle = $this->_caseLayoutHandle($currentModule, $file, $contents);
$handleParents = $this->_caseLayoutHandleParent($currentModule, $file, $contents);
$handleUpdates = $this->_caseLayoutHandleUpdate($currentModule, $file, $contents);
$references = $this->_caseLayoutReference($currentModule, $file, $contents);
$dependencies = array_merge(
$attributes,
$blocks,
$actions,
$handle,
$handleParents,
$handleUpdates,
$references
);
return $dependencies;
}
/**
* Check dependencies for 'module' attribute
*
* Ex.: <element module="{module}">
*
* @param string $currentModule
* @param string $contents
* @return array
*/
protected function _caseAttributeModule($currentModule, &$contents)
{
$patterns = [
'/(?<source><.+module\s*=\s*[\'"](?<namespace>' .
$this->_namespaces .
')[_\\\\]' .
'(?<module>[A-Z][a-zA-Z]+)[\'"].*>)/' => \Magento\Test\Integrity\DependencyTest::TYPE_SOFT,
];
return $this->_checkDependenciesByRegexp($currentModule, $contents, $patterns);
}
/**
* Check dependencies for <block> element
*
* Ex.: <block class="{name}">
* <block template="{path}">
*
* @param string $currentModule
* @param string $contents
* @return array
*/
protected function _caseElementBlock($currentModule, &$contents)
{
$patterns = [
'/(?<source><block.*class\s*=\s*[\'"](?<namespace>' .
$this->_namespaces .
')[_\\\\]' .
'(?<module>[A-Z][a-zA-Z]+)[_\\\\]' .
'(?:[A-Z][a-zA-Z]+[_\\\\]?){1,}[\'"].*>)/' => \Magento\Test\Integrity\DependencyTest::TYPE_HARD,
'/(?<source><block.*template\s*=\s*[\'"](?<namespace>' .
$this->_namespaces .
')[_\\\\]' .
'(?<module>[A-Z][a-zA-Z]+)::[\w\/\.]+[\'"].*>)/' => \Magento\Test\Integrity\DependencyTest::TYPE_SOFT,
];
return $this->_checkDependenciesByRegexp($currentModule, $contents, $patterns);
}
/**
* Check dependencies for <action> element
*
* Ex.: <block>{name}
* <template>{path}
* <file>{path}
* <element helper="{name}">
*
* @param string $currentModule
* @param string $contents
* @return array
*/
protected function _caseElementAction($currentModule, &$contents)
{
$patterns = [
'/(?<source><block\s*>(?<namespace>' .
$this->_namespaces .
')[_\\\\]' .
'(?<module>[A-Z][a-zA-Z]+)[_\\\\]' .
'(?:[A-Z][a-zA-Z]+[_\\\\]?){1,}<\/block\s*>)/' => \Magento\Test\Integrity\DependencyTest::TYPE_SOFT,
'/(?<source><template\s*>(?<namespace>' .
$this->_namespaces .
')[_\\\\]' .
'(?<module>[A-Z][a-zA-Z]+)::[\w\/\.]+' .
'<\/template\s*>)/' => \Magento\Test\Integrity\DependencyTest::TYPE_SOFT,
'/(?<source><file\s*>(?<namespace>' .
$this->_namespaces .
')[_\\\\]' .
'(?<module>[A-Z][a-zA-Z]+)::[\w\/\.-]+' .
'<\/file\s*>)/' => \Magento\Test\Integrity\DependencyTest::TYPE_SOFT,
'/(?<source><.*helper\s*=\s*[\'"](?<namespace>' .
$this->_namespaces .
')[_\\\\]' .
'(?<module>[A-Z][a-zA-Z]+)[_\\\\](?:[A-Z][a-z]+[_\\\\]?){1,}::[\w]+' .
'[\'"].*>)/' => \Magento\Test\Integrity\DependencyTest::TYPE_SOFT,
];
return $this->_checkDependenciesByRegexp($currentModule, $contents, $patterns);
}
/**
* Check layout handles
*
* Ex.: <layout><{name}>...</layout>
*
* @param string $currentModule
* @param string $file
* @param string $contents
* @return array
*/
protected function _caseLayoutHandle($currentModule, $file, &$contents)
{
$xml = simplexml_load_string($contents);
if (!$xml) {
return [];
}
$area = $this->_getAreaByFile($file);
$result = [];
foreach ((array)$xml->xpath('/layout/child::*') as $element) {
$check = $this->_checkDependencyLayoutHandle($currentModule, $area, $element->getName());
$modules = isset($check['modules']) ? $check['modules'] : null;
if ($modules) {
foreach ($modules as $module) {
$result[$module] = [
'type' => \Magento\Test\Integrity\DependencyTest::TYPE_SOFT,
'source' => $element->getName(),
];
}
}
}
return $this->_getUniqueDependencies($result);
}
/**
* Check layout handles parents
*
* Ex.: <layout_name parent="{name}">
*
* @param string $currentModule
* @param string $file
* @param string $contents
* @return array
*/
protected function _caseLayoutHandleParent($currentModule, $file, &$contents)
{
$xml = simplexml_load_string($contents);
if (!$xml) {
return [];
}
$area = $this->_getAreaByFile($file);
$result = [];
foreach ((array)$xml->xpath('/layout/child::*/@parent') as $element) {
$check = $this->_checkDependencyLayoutHandle($currentModule, $area, (string)$element);
$modules = isset($check['modules']) ? $check['modules'] : null;
if ($modules) {
foreach ($modules as $module) {
$result[$module] = [
'type' => \Magento\Test\Integrity\DependencyTest::TYPE_HARD,
'source' => (string)$element,
];
}
}
}
return $this->_getUniqueDependencies($result);
}
/**
* Check layout handles updates
*
* Ex.: <update handle="{name}" />
*
* @param string $currentModule
* @param string $file
* @param string $contents
* @return array
*/
protected function _caseLayoutHandleUpdate($currentModule, $file, &$contents)
{
$xml = simplexml_load_string($contents);
if (!$xml) {
return [];
}
$area = $this->_getAreaByFile($file);
$result = [];
foreach ((array)$xml->xpath('//update/@handle') as $element) {
$check = $this->_checkDependencyLayoutHandle($currentModule, $area, (string)$element);
$modules = isset($check['modules']) ? $check['modules'] : null;
if ($modules) {
foreach ($modules as $module) {
$result[$module] = [
'type' => \Magento\Test\Integrity\DependencyTest::TYPE_SOFT,
'source' => (string)$element,
];
}
}
}
return $this->_getUniqueDependencies($result);
}
/**
* Check layout references
*
* Ex.: <reference name="{name}">
*
* @param string $currentModule
* @param string $file
* @param string $contents
* @return array
*/
protected function _caseLayoutReference($currentModule, $file, &$contents)
{
$xml = simplexml_load_string($contents);
if (!$xml) {
return [];
}
$area = $this->_getAreaByFile($file);
$result = [];
foreach ((array)$xml->xpath('//reference/@name') as $element) {
$check = $this->_checkDependencyLayoutBlock($currentModule, $area, (string)$element);
$modules = isset($check['modules']) ? $check['modules'] : null;
if ($modules) {
foreach ($modules as $module) {
$result[$module] = [
'type' => \Magento\Test\Integrity\DependencyTest::TYPE_SOFT,
'source' => (string)$element,
];
}
}
}
return $this->_getUniqueDependencies($result);
}
/**
* Search dependencies by defined regexp patterns
*
* @param string $currentModule
* @param string $contents
* @param array $patterns
* @return array
*/
protected function _checkDependenciesByRegexp($currentModule, &$contents, $patterns = [])
{
$result = [];
foreach ($patterns as $pattern => $type) {
if (preg_match_all($pattern, $contents, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
$module = $match['namespace'] . '\\' . $match['module'];
if ($currentModule != $module) {
$result[$module] = ['type' => $type, 'source' => $match['source']];
}
}
}
}
return $this->_getUniqueDependencies($result);
}
/**
* Check layout handle dependency
*
* Return: array(
* 'modules' // dependent modules
* 'source' // source text
* )
*
* @param admin $currentModule
* @param string $area
* @param string $handle
* @return string[]
*/
protected function _checkDependencyLayoutHandle($currentModule, $area, $handle)
{
$chunks = explode('_', $handle);
if (count($chunks) > 1) {
// Remove 'action' part from handle name
array_pop($chunks);
}
$router = implode('_', $chunks);
if (isset($this->_mapRouters[$router])) {
// CASE 1: Single dependency
$modules = $this->_mapRouters[$router];
if (!in_array($currentModule, $modules)) {
return ['modules' => $modules];
}
}
if (isset($this->_mapLayoutHandles[$area][$handle])) {
// CASE 2: No dependencies
$modules = $this->_mapLayoutHandles[$area][$handle];
if (isset($modules[$currentModule])) {
return ['modules' => []];
}
// CASE 3: Single dependency
if (1 == count($modules)) {
return ['modules' => $modules];
}
// CASE 4: Default module dependency
$defaultModule = $this->_getDefaultModuleName($area);
if (isset($modules[$defaultModule])) {
return ['modules' => [$defaultModule]];
}
}
return [];
}
/**
* Check layout block dependency
*
* Return: array(
* 'modules' // dependent modules
* 'source' // source text
* )
*
* @param string $currentModule
* @param string $area
* @param string $block
* @return string[]
*/
protected function _checkDependencyLayoutBlock($currentModule, $area, $block)
{
if (isset($this->_mapLayoutBlocks[$area][$block])) {
// CASE 1: No dependencies
$modules = $this->_mapLayoutBlocks[$area][$block];
if (isset($modules[$currentModule])) {
return ['modules' => []];
}
// CASE 2: Single dependency
if (1 == count($modules)) {
return ['modules' => $modules];
}
// CASE 3: Default module dependency
$defaultModule = $this->_getDefaultModuleName($area);
if (isset($modules[$defaultModule])) {
return ['modules' => [$defaultModule]];
}
}
return [];
}
/**
* Get area from file path
*
* @param string $file
* @return string
*/
protected function _getAreaByFile($file)
{
$area = 'default';
if (preg_match('/\/(?<area>adminhtml|frontend)\//', $file, $matches)) {
$area = $matches['area'];
}
return $area;
}
/**
* Retrieve unique dependencies
*
* @param array $dependencies
* @return array
*/
protected function _getUniqueDependencies($dependencies = [])
{
$result = [];
foreach ($dependencies as $module => $value) {
$result[] = ['modules' => [$module], 'type' => $value['type'], 'source' => $value['source']];
}
return $result;
}
/**
* Retrieve default module name (by area)
*
* @param string $area
* @return string|null
*/
protected function _getDefaultModuleName($area = 'default')
{
if (isset($this->_defaultModules[$area])) {
return $this->_defaultModules[$area];
}
return null;
}
}
@@ -0,0 +1,614 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\TestFramework\Dependency;
use Magento\Framework\App\Utility\Files;
use Magento\Framework\Config\Reader\Filesystem as ConfigReader;
use Magento\Framework\Exception\ConfigurationMismatchException;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\UrlInterface;
use Magento\TestFramework\Dependency\Reader\ClassScanner;
use Magento\TestFramework\Dependency\Route\RouteMapper;
use Magento\TestFramework\Exception\NoSuchActionException;
use Magento\TestFramework\Inspection\Exception;
/**
* Rule to check the dependencies between modules based on references, getUrl and layout blocks
*/
class PhpRule implements RuleInterface
{
/**
* List of filepaths for DI files
*
* @var array
*/
private $diFiles;
/**
* Map from plugin classes to the subjects they modify
*
* @var array
*/
private $pluginMap;
/**
* List of routers
*
* Format: array(
* '{Router}' => '{Module_Name}'
* )
*
* @var array
*/
private $_mapRouters = [];
/**
* List of layout blocks
*
* Format: array(
* '{Area}' => array(
* '{Block_Name}' => array('{Module_Name}' => '{Module_Name}')
* ))
*
* @var array
*/
protected $_mapLayoutBlocks = [];
/**
* Used to retrieve information from WebApi urls
* @var ConfigReader
*/
protected $configReader;
/**
* Default modules list.
*
* @var array
*/
protected $_defaultModules = [
'frontend' => 'Magento\Theme',
'adminhtml' => 'Magento\Adminhtml',
];
/**
* @var RouteMapper
*/
private $routeMapper;
/**
* Whitelists for dependency check
*
* @var array
*/
private $whitelists;
/**
* @var ClassScanner
*/
private $classScanner;
/**
* @var array
*/
private $serviceMethods;
/**
* @param array $mapRouters
* @param array $mapLayoutBlocks
* @param ConfigReader $configReader
* @param array $pluginMap
* @param array $whitelists
* @param ClassScanner|null $classScanner
* @param RouteMapper|null $routeMapper
*/
public function __construct(
array $mapRouters,
array $mapLayoutBlocks,
ConfigReader $configReader,
array $pluginMap = [],
array $whitelists = [],
ClassScanner $classScanner = null,
RouteMapper $routeMapper = null
) {
$this->_mapRouters = $mapRouters;
$this->_mapLayoutBlocks = $mapLayoutBlocks;
$this->configReader = $configReader;
$this->pluginMap = $pluginMap ?: null;
$this->whitelists = $whitelists;
$this->classScanner = $classScanner ?? new ClassScanner();
$this->routeMapper = $routeMapper ?? new RouteMapper();
}
/**
* Gets alien dependencies information for current module by analyzing file's contents
*
* @param string $currentModule
* @param string $fileType
* @param string $file
* @param string $contents
* @return array
* @throws \Exception
*/
public function getDependencyInfo($currentModule, $fileType, $file, &$contents)
{
if (!in_array($fileType, ['php', 'template', 'fixture'])) {
return [];
}
$dependenciesInfo = [];
$dependenciesInfo = $this->considerCaseDependencies(
$dependenciesInfo,
$this->caseClassesAndIdentifiers($currentModule, $file, $contents)
);
$dependenciesInfo = $this->considerCaseDependencies(
$dependenciesInfo,
$this->_caseGetUrl($currentModule, $contents, $file)
);
$dependenciesInfo = $this->considerCaseDependencies(
$dependenciesInfo,
$this->_caseLayoutBlock($currentModule, $fileType, $file, $contents)
);
return $dependenciesInfo;
}
/**
* Get routes whitelist
*
* @return array
*/
private function getRoutesWhitelist(): array
{
return $this->whitelists['routes'] ?? [];
}
/**
* Check references to classes and identifiers defined in other modules
*
* @param string $currentModule
* @param string $file
* @param string $contents
* @return array
* @throws \Exception
*/
private function caseClassesAndIdentifiers($currentModule, $file, &$contents)
{
$pattern = '~\b(?<class>(?<module>('
. implode(
'[_\\\\]|',
Files::init()->getNamespaces()
)
. '(?<delimiter>[_\\\\]))[a-zA-Z0-9]{2,})'
. '(?<class_inside_module>\\4[a-zA-Z0-9_\\\\]{2,})?)\b'
. '(?:::(?<module_scoped_key>[A-Za-z0-9_/.]+)[\'"])?~';
if (!preg_match_all($pattern, $contents, $matches)) {
return [];
}
$dependenciesInfo = [];
$matches['module'] = array_unique($matches['module']);
foreach ($matches['module'] as $i => $referenceModule) {
$referenceModule = str_replace('_', '\\', $referenceModule);
if ($currentModule == $referenceModule) {
continue;
}
$dependencyClass = trim($matches['class'][$i]);
if (empty($matches['class_inside_module'][$i]) && !empty($matches['module_scoped_key'][$i])) {
$dependencyType = RuleInterface::TYPE_SOFT;
} else {
$currentClass = $this->getClassFromFilepath($file);
$dependencyType = $this->isPluginDependency($currentClass, $dependencyClass)
? RuleInterface::TYPE_SOFT
: RuleInterface::TYPE_HARD;
}
$dependenciesInfo[] = [
'modules' => [$referenceModule],
'type' => $dependencyType,
'source' => $dependencyClass,
];
}
return $dependenciesInfo;
}
/**
* Get class name from filename based on class/file naming conventions
*
* @param string $filepath
* @return string
*/
private function getClassFromFilepath(string $filepath): string
{
return $this->classScanner->getClassName($filepath);
}
/**
* Load DI configuration files
*
* @return array
* @throws \Exception
*/
private function loadDiFiles()
{
if (!$this->diFiles) {
$this->diFiles = Files::init()->getDiConfigs();
}
return $this->diFiles;
}
/**
* Generate an array of plugin info
*
* @return array
* @throws \Exception
*/
private function loadPluginMap()
{
if (!$this->pluginMap) {
foreach ($this->loadDiFiles() as $filepath) {
$dom = new \DOMDocument();
// phpcs:ignore Magento2.Functions.DiscouragedFunction
$dom->loadXML(file_get_contents($filepath));
$typeNodes = $dom->getElementsByTagName('type');
/** @var \DOMElement $type */
foreach ($typeNodes as $type) {
/** @var \DOMElement $plugin */
foreach ($type->getElementsByTagName('plugin') as $plugin) {
$subject = $type->getAttribute('name');
$pluginType = $plugin->getAttribute('type');
$this->pluginMap[$pluginType] = $subject;
}
}
}
}
return $this->pluginMap;
}
/**
* Determine whether a the dependency relation is because of a plugin
*
* True IFF the dependent is a plugin for some class in the same module as the dependency.
*
* @param string $dependent
* @param string $dependency
* @return bool
* @throws \Exception
*/
private function isPluginDependency($dependent, $dependency)
{
$pluginMap = $this->loadPluginMap();
$subject = isset($pluginMap[$dependent])
? $pluginMap[$dependent]
: null;
if ($subject === $dependency) {
return true;
} elseif ($subject) {
$moduleNameLength = strpos($subject, '\\', strpos($subject, '\\') + 1);
$subjectModule = substr($subject, 0, $moduleNameLength);
return strpos($dependency, $subjectModule) === 0;
} else {
return false;
}
}
/**
* Check get URL method
*
* Ex.: getUrl('{path}')
*
* @param string $currentModule
* @param string $contents
* @param string $file
* @return array
* @throws LocalizedException
*/
protected function _caseGetUrl(string $currentModule, string &$contents, string $file): array
{
$dependencies = [];
$pattern = '#(\->|:)(?<source>getUrl\(([\'"])(?<path>[a-zA-Z0-9\-_*/]+)\3)\s*[,)]#';
if (!preg_match_all($pattern, $contents, $matches, PREG_SET_ORDER)) {
return $dependencies;
}
try {
foreach ($matches as $item) {
$path = $item['path'];
$modules = [];
if (strpos($path, '*') !== false) {
$modules = $this->processWildcardUrl($path, $file);
} elseif (preg_match('#rest(?<service>/V1/.+)#i', $path, $apiMatch)) {
$modules = $this->processApiUrl($apiMatch['service']);
} else {
$modules = $this->processStandardUrl($path);
}
if ($modules && !in_array($currentModule, $modules)) {
$dependencies[] = [
'modules' => $modules,
'type' => RuleInterface::TYPE_HARD,
'source' => $item['source'],
];
}
}
} catch (NoSuchActionException $e) {
if (array_search($e->getMessage(), $this->getRoutesWhitelist()) === false) {
throw new LocalizedException(__('Invalid URL path: %1', $e->getMessage()), $e);
}
}
return $dependencies;
}
/**
* Helper method to get module dependencies used by a wildcard Url
*
* @param string $urlPath
* @param string $filePath
* @return string[]
* @throws NoSuchActionException
*/
private function processWildcardUrl(string $urlPath, string $filePath)
{
$filePath = strtolower($filePath);
$urlRoutePieces = explode('/', $urlPath);
$routeId = array_shift($urlRoutePieces);
//Skip route wildcard processing as this requires using the routeMapper
if ('*' === $routeId) {
return [];
}
/**
* Only handle Controllers. ie: Ignore Blocks, Templates, and Models due to complexity in static resolution
* of route
*/
if (!preg_match(
'#controller/(adminhtml/)?(?<controller_name>.+)/(?<action_name>\w+).php$#',
$filePath,
$fileParts
)) {
return [];
}
$controllerName = array_shift($urlRoutePieces);
if ('*' === $controllerName) {
$controllerName = str_replace('/', '_', $fileParts['controller_name']);
}
if (empty($urlRoutePieces) || !$urlRoutePieces[0]) {
$actionName = UrlInterface::DEFAULT_ACTION_NAME;
} else {
$actionName = array_shift($urlRoutePieces);
if ('*' === $actionName) {
$actionName = $fileParts['action_name'];
}
}
return $this->routeMapper->getDependencyByRoutePath(
strtolower($routeId),
strtolower($controllerName),
strtolower($actionName)
);
}
/**
* Helper method to get module dependencies used by a standard URL
*
* @param string $path
* @return string[]
* @throws NoSuchActionException
*/
private function processStandardUrl(string $path)
{
$pattern = '#(?<route_id>[a-z0-9\-_]{3,})'
. '(/(?<controller_name>[a-z0-9\-_]+))?(/(?<action_name>[a-z0-9\-_]+))?#i';
if (!preg_match($pattern, $path, $match)) {
throw new NoSuchActionException('Failed to parse standard url path: ' . $path);
}
$routeId = $match['route_id'];
$controllerName = $match['controller_name'] ?? UrlInterface::DEFAULT_CONTROLLER_NAME;
$actionName = $match['action_name'] ?? UrlInterface::DEFAULT_ACTION_NAME;
return $this->routeMapper->getDependencyByRoutePath(
$routeId,
$controllerName,
$actionName
);
}
/**
* Create regex patterns from service url paths
*
* @return array
*/
private function getServiceMethodRegexps(): array
{
if (!$this->serviceMethods) {
$this->serviceMethods = [];
$serviceRoutes = $this->configReader->read()['routes'];
foreach ($serviceRoutes as $serviceRouteUrl => $methods) {
$pattern = '#:\w+#';
$replace = '\w+';
$serviceRouteUrlRegex = preg_replace($pattern, $replace, $serviceRouteUrl);
$serviceRouteUrlRegex = '#^' . $serviceRouteUrlRegex . '$#';
$this->serviceMethods[$serviceRouteUrlRegex] = $methods;
}
}
return $this->serviceMethods;
}
/**
* Helper method to get module dependencies used by an API URL
*
* @param string $path
* @return string[]
*
* @throws NoSuchActionException
* @throws Exception
*/
private function processApiUrl(string $path): array
{
foreach ($this->getServiceMethodRegexps() as $serviceRouteUrlRegex => $methods) {
/**
* Since we expect that every service method should be within the same module, we can use the class from
* any method
*/
if (preg_match($serviceRouteUrlRegex, $path)) {
$method = reset($methods);
$className = $method['service']['class'];
//get module from className
if (preg_match('#^(?<module>\w+[\\\]\w+)#', $className, $match)) {
return [$match['module']];
}
throw new Exception('Failed to parse class from className: ' . $className);
}
}
throw new NoSuchActionException('Failed to match service with url path: ' . $path);
}
/**
* Check layout blocks
*
* @param string $currentModule
* @param string $fileType
* @param string $file
* @param string $contents
* @return array
*/
protected function _caseLayoutBlock(string $currentModule, string $fileType, string $file, string &$contents): array
{
$pattern = '/[\->:]+(?<source>(?:getBlock|getBlockHtml)\([\'"](?<block>[\w\.\-]+)[\'"]\))/';
$area = $this->_getAreaByFile($file, $fileType);
$result = [];
if (!preg_match_all($pattern, $contents, $matches, PREG_SET_ORDER)) {
return $result;
}
foreach ($matches as $match) {
if (in_array($match['block'], ['root', 'content'])) {
continue;
}
$check = $this->_checkDependencyLayoutBlock($currentModule, $area, $match['block']);
$modules = isset($check['modules']) ? $check['modules'] : null;
if ($modules) {
foreach ($modules as $module) {
$result[$module] = [
'type' => RuleInterface::TYPE_HARD,
'source' => $match['source'],
];
}
}
}
return $this->_getUniqueDependencies($result);
}
/**
* Get area from file path
*
* @param string $file
* @param string $fileType
* @return string|null
*/
protected function _getAreaByFile(string $file, string $fileType): ?string
{
if ($fileType == 'php') {
return null;
}
$area = 'default';
if (preg_match('/\/(?<area>adminhtml|frontend)\//', $file, $matches)) {
$area = $matches['area'];
}
return $area;
}
/**
* Check layout block dependency
*
* Return: array(
* 'modules' // dependent modules
* 'source' // source text
* )
*
* @param string $currentModule
* @param string|null $area
* @param string $block
* @return array
*/
protected function _checkDependencyLayoutBlock(string $currentModule, ?string $area, string $block): array
{
if (isset($this->_mapLayoutBlocks[$area][$block]) || $area === null) {
// CASE 1: No dependencies
$modules = [];
if ($area === null) {
foreach ($this->_mapLayoutBlocks as $blocks) {
if (array_key_exists($block, $blocks)) {
$modules += $blocks[$block];
}
}
} else {
$modules = $this->_mapLayoutBlocks[$area][$block];
}
if (isset($modules[$currentModule])) {
return ['modules' => []];
}
// CASE 2: Single dependency
if (1 == count($modules)) {
return ['modules' => $modules];
}
// CASE 3: Default module dependency
$defaultModule = $this->_getDefaultModuleName($area);
if (isset($modules[$defaultModule])) {
return ['modules' => [$defaultModule]];
}
}
// CASE 4: \Exception - Undefined block
return [];
}
/**
* Retrieve default module name (by area)
*
* @param string $area
* @return null
*/
protected function _getDefaultModuleName($area = 'default')
{
if (isset($this->_defaultModules[$area])) {
return $this->_defaultModules[$area];
}
return null;
}
/**
* Retrieve unique dependencies
*
* @param array $dependencies
* @return array
*/
protected function _getUniqueDependencies($dependencies = [])
{
$result = [];
foreach ($dependencies as $module => $value) {
$result[] = ['modules' => [$module], 'type' => $value['type'], 'source' => $value['source']];
}
return $result;
}
/**
* Merge dependencies
*
* @param array $known
* @param array $new
* @return array
*/
private function considerCaseDependencies($known, $new)
{
return array_merge($known, $new);
}
}
@@ -0,0 +1,50 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\TestFramework\Dependency\Reader;
use Magento\Setup\Module\Di\Code\Reader\FileClassScanner;
/**
* Search classes in file by path.
*/
class ClassScanner
{
/**
* @var string[]
*/
private $classNames = [];
/**
* Get class name by file name.
*
* @param string $filePath
*
* @return string
*/
public function getClassName(string $filePath): string
{
if (!isset($this->classNames[$filePath])) {
$this->classNames[$filePath] = $this->loadClassName($filePath);
}
return $this->classNames[$filePath];
}
/**
* Load class name from file.
*
* @param string $filePath
*
* @return string
*/
private function loadClassName(string $filePath): string
{
$scanner = new FileClassScanner($filePath);
return $scanner->getClassName();
}
}
@@ -0,0 +1,56 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Dependency;
/**
* Class provides dependency rule for reports.xml config file.
*/
class ReportsConfigRule implements RuleInterface
{
/**
* Map of tables and modules
*
* @var array
*/
private $moduleTableMap;
/**
* @param array $tables
*/
public function __construct(array $tables)
{
$this->moduleTableMap = $tables;
}
/**
* @inheritdoc
*/
public function getDependencyInfo($currentModule, $fileType, $file, &$contents)
{
if ('config' != $fileType || !preg_match('#.*/reports\.xml$#', $file)) {
return [];
}
$dependenciesInfo = [];
if (preg_match_all('#<source[^>]*name=[\'"]([^\'"]+)[\'"]#i', $contents, $matches)) {
$tables = array_pop($matches);
foreach ($tables as $table) {
if (!isset($this->moduleTableMap[$table])) {
continue;
}
if (strtolower($currentModule) !== strtolower($this->moduleTableMap[$table])) {
$dependenciesInfo[] = [
'modules' => [$this->moduleTableMap[$table]],
'type' => RuleInterface::TYPE_HARD,
'source' => $table,
];
}
}
}
return $dependenciesInfo;
}
}
@@ -0,0 +1,337 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\TestFramework\Dependency\Route;
use Exception;
use Magento\Framework\App\Area;
use Magento\Framework\App\Utility\Files;
use Magento\Framework\Component\ComponentFile;
use Magento\TestFramework\Exception\NoSuchActionException;
/**
* Route mapper based on routes.xml declarations
*/
class RouteMapper
{
/**
* List of routers
*
* Format: array(
* '{Router_Id}' => '{Route_Id}' => ['{Module_Name}']
* )
*
* @var array
*/
private $routers = [];
/**
* List of routes.xml files by modules
*
* Format: array(
* '{Module_Name}' => ['{Filename}']
* )
*
* @var array
*/
private $routeConfigFiles = [];
/**
* List of controllers actions
*
* Format: array(
* '{Router_Id}' => '{Route_Id}' => '{Controller_Name}' => '{Action_Name}' => [{'Module_Name'}]
* )
*
* @var array
*/
private $actions = [];
/**
* List of reserved words
*
* @see \Magento\Framework\App\Router\ActionList
* @var array
*/
private $reservedWords = [
'abstract' => 1,
'and' => 1,
'array' => 1,
'as' => 1,
'break' => 1,
'callable' => 1,
'case' => 1,
'catch' => 1,
'class' => 1,
'clone' => 1,
'const' => 1,
'continue' => 1,
'declare' => 1,
'default' => 1,
'die' => 1,
'do' => 1,
'echo' => 1,
'else' => 1,
'elseif' => 1,
'empty' => 1,
'enddeclare' => 1,
'endfor' => 1,
'endforeach' => 1,
'endif' => 1,
'endswitch' => 1,
'endwhile' => 1,
'eval' => 1,
'exit' => 1,
'extends' => 1,
'final' => 1,
'finally' => 1,
'fn' => 1,
'for' => 1,
'foreach' => 1,
'function' => 1,
'global' => 1,
'goto' => 1,
'if' => 1,
'implements' => 1,
'include' => 1,
'instanceof' => 1,
'insteadof' => 1,
'interface' => 1,
'isset' => 1,
'list' => 1,
'match' => 1,
'namespace' => 1,
'new' => 1,
'or' => 1,
'print' => 1,
'private' => 1,
'protected' => 1,
'public' => 1,
'require' => 1,
'return' => 1,
'static' => 1,
'switch' => 1,
'throw' => 1,
'trait' => 1,
'try' => 1,
'unset' => 1,
'use' => 1,
'var' => 1,
'void' => 1,
'while' => 1,
'xor' => 1,
'yield' => 1
];
/**
* Provide routing declaration by router_id
*
* @param string $routerId
* @return array
* @throws Exception
*/
public function getRoutes(string $routerId = ''): array
{
$routes = [];
if (!$routerId) {
foreach ($this->getRoutersMap() as $routesByRouterType) {
$routes = array_merge_recursive($routes, $routesByRouterType);
}
array_walk(
$routes,
function (&$modules) {
$modules = array_unique($modules);
}
);
} else {
$routes = $this->getRoutersMap()[$routerId] ?? [];
}
return $routes;
}
/**
* Provide dependencies for a specific URL path
*
* @param string $routeId
* @param string $controllerName
* @param string $actionName
* @return string[]
* @throws NoSuchActionException
* @throws Exception
*/
public function getDependencyByRoutePath(
string $routeId,
string $controllerName,
string $actionName
): array {
$routeId = strtolower($routeId);
$controllerName = strtolower($controllerName);
$actionName = strtolower($actionName);
if (isset($this->reservedWords[$actionName])) {
$actionName .= 'action';
}
$dependencies = [];
foreach ($this->getRouterTypes() as $routerId) {
if (isset($this->getActionsMap()[$routerId][$routeId][$controllerName][$actionName])) {
$dependencies[] = $this->getActionsMap()[$routerId][$routeId][$controllerName][$actionName];
}
}
$dependencies = array_merge([], ...$dependencies);
if (empty($dependencies)) {
throw new NoSuchActionException(implode('/', [$routeId, $controllerName, $actionName]));
} else {
$dependencies = array_unique($dependencies);
}
return $dependencies;
}
/**
* Provide a list of router type
*
* @return array
* @throws Exception
*/
private function getRouterTypes()
{
return array_keys($this->getActionsMap());
}
/**
* Provide routing declaration
*
* @return array
* @throws Exception
*/
private function getRoutersMap()
{
if (empty($this->routers)) {
foreach ($this->getListRoutesXml() as $module => $configFiles) {
foreach ($configFiles as $configFile) {
$this->processConfigFile($module, $configFile);
}
}
}
return $this->routers;
}
/**
* Update routers map for the module basing on the routing config file
*
* @param string $module
* @param string $configFile
*
* @return void
*/
private function processConfigFile(string $module, string $configFile)
{
// Read module's routes.xml file
$config = simplexml_load_file($configFile);
$routers = $config->xpath("/config/router");
foreach ($routers as $router) {
$routerId = (string)$router['id'];
foreach ($router->xpath('route') as $route) {
$routeId = (string)$route['id'];
if (!isset($this->routers[$routerId][$routeId])) {
$this->routers[$routerId][$routeId] = [];
}
if (!in_array($module, $this->routers[$routerId][$routeId])) {
$this->routers[$routerId][$routeId][] = $module;
}
}
}
}
/**
* Prepare the list of routes.xml files (by modules)
*
* @throws Exception
*/
private function getListRoutesXml()
{
if (empty($this->routeConfigFiles)) {
$files = Files::init()->getConfigFiles('*/routes.xml', [], false, true);
/** @var ComponentFile $componentFile */
foreach ($files as $componentFile) {
$module = str_replace('_', '\\', $componentFile->getComponentName());
$this->routeConfigFiles[$module][] = $componentFile->getFullPath();
}
}
return $this->routeConfigFiles;
}
/**
* Provide a list of available actions
*
* @return array
* @throws Exception
*/
private function getActionsMap(): array
{
if (empty($this->actions)) {
$files = Files::init()->getPhpFiles(Files::INCLUDE_APP_CODE);
$actionsMap = [];
foreach ($this->getRoutersMap() as $routerId => $routes) {
$actionsMapPerArea = [];
foreach ($routes as $routeId => $dependencies) {
$actionsMapPerArea[$routeId] = [];
foreach ($dependencies as $module) {
$moduleActions = $this->getModuleActionsMapping($module, $routerId, $files);
$actionsMapPerArea[$routeId] =
array_merge_recursive($actionsMapPerArea[$routeId], $moduleActions);
}
}
$actionsMap[$routerId] = $actionsMapPerArea;
}
$this->actions = $actionsMap;
}
return $this->actions;
}
/**
* Provide a list of available module actions by router_id
*
* @param string $module
* @param string $routerId
* @param array $files
* @return array
*/
private function getModuleActionsMapping(string $module, string $routerId, array $files): array
{
$subdirectoryPattern = str_replace('\\', DIRECTORY_SEPARATOR, $module);
$subdirectoryPattern .= DIRECTORY_SEPARATOR . 'Controller/';
if (array_search($routerId, [Area::AREA_ADMINHTML, Area::AREA_ADMIN], true) !== false) {
$subdirectoryPattern .= ucfirst(Area::AREA_ADMINHTML) . DIRECTORY_SEPARATOR;
} else {
$subdirectoryPattern .= '(?!' . ucfirst(Area::AREA_ADMINHTML) . ')';
}
$actions = preg_grep('~' . $subdirectoryPattern . '~', $files);
$actionsPattern = '#' . $subdirectoryPattern . '(?<controller>\S+)/'
. '(?<action_name>\w+)\.php$#';
$moduleActionsMapping = [];
foreach ($actions as $action) {
if (preg_match($actionsPattern, $action, $matches)) {
$controllerName = strtolower(str_replace('/', '_', $matches['controller']));
$actionName = strtolower($matches['action_name']);
if (!isset($moduleActionsMapping[$controllerName][$actionName])) {
$moduleActionsMapping[$controllerName][$actionName] = [];
}
$moduleActionsMapping[$controllerName][$actionName][] = $module;
}
}
return $moduleActionsMapping;
}
}
@@ -0,0 +1,29 @@
<?php
/**
* Rule for searching dependencies in layout files
*
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Dependency;
interface RuleInterface
{
/**
* Types of dependencies between modules
*/
const TYPE_SOFT = 'soft';
const TYPE_HARD = 'hard';
/**
* Gets alien dependencies information for current module by analyzing file's contents
*
* @param string $currentModule
* @param string $fileType
* @param string $file
* @param string $contents
* @return array
*/
public function getDependencyInfo($currentModule, $fileType, $file, &$contents);
}
@@ -0,0 +1,107 @@
<?php
/**
* Rule for searching php file dependency
*
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Dependency\VirtualType;
use DOMDocument;
use Magento\Framework\App\Utility\Files;
class VirtualTypeMapper
{
/**
* @var array
*/
private $map = [];
/**
* @var string
*/
private static $mainScope = 'global';
/**
* VirtualTypeMapper constructor.
* @param array $map
*/
public function __construct(array $map = [])
{
$this->map = $map;
}
/**
* @param string $name
* @param string $scope
* @return string|null
*/
public function getType($name, $scope = null)
{
if (empty($this->map)) {
$this->loadMap();
}
$scopes = [];
if ($scope !== null && $scope !== self::$mainScope) {
array_unshift($scopes, $scope);
}
$scopes[] = self::$mainScope;
foreach ($scopes as $scp) {
if (isset($this->map[$scp][$name])) {
return $this->map[$scp][$name];
}
}
return $name;
}
/**
* @param string $file
* @return string
*/
public function getScopeFromFile($file)
{
$basename = basename(pathinfo($file, PATHINFO_DIRNAME));
return $basename === 'etc' ? 'global' : $basename;
}
/**
* @return array
* @throws \Exception
*/
private function loadDiConfigs()
{
return Files::init()->getDiConfigs();
}
/**
* @param array $diFiles
* @return array
* @throws \Exception
*/
public function loadMap(array $diFiles = [])
{
if (empty($diFiles)) {
$diFiles = $this->loadDiConfigs();
}
foreach ($diFiles as $file) {
$scope = $this->getScopeFromFile($file);
$doc = new DOMDocument();
$doc->loadXML(file_get_contents($file));
$nodes = $doc->getElementsByTagName('virtualType');
/** @var \DOMElement $node */
foreach ($nodes as $node) {
$name = $node->getAttribute('name');
$type = $node->getAttribute('type');
if (empty($type)) {
continue;
}
$this->map[$scope][$name] = $type;
}
}
return $this->map;
}
}
@@ -0,0 +1,16 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\TestFramework\Exception;
/**
* An exception is thrown if such a controller action does not exist
*/
class NoSuchActionException extends \Exception
{
}
@@ -0,0 +1,160 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
/**
* Abstract shell command for the static code inspection
*/
namespace Magento\TestFramework\Inspection;
/**
* Abstract class for commands
*/
abstract class AbstractCommand
{
/**
* @var string
*/
protected $_reportFile;
/**
* @var int
*/
protected $_lastExitCode;
/**
* @var string
*/
protected $_lastOutput;
/**
* @var string
*/
protected $_lastRunMessage;
/**
* Constructor
*
* @param string $reportFile Destination file to write inspection report to
*/
public function __construct($reportFile)
{
$this->_reportFile = $reportFile;
}
/**
* Build and execute the shell command
*
* @param array $whiteList Files/directories to be inspected
* @param array $blackList Files/directories to be excluded from the inspection
* @return bool
* phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod
*/
public function run(array $whiteList, array $blackList = [])
{
if (file_exists($this->_reportFile)) {
unlink($this->_reportFile);
}
$shellCmd = $this->_buildShellCmd($whiteList, $blackList);
$result = $this->_execShellCmd($shellCmd);
$this->_generateLastRunMessage();
return $result !== false;
}
/**
* Whether the command can be ran on the current environment
*
* @return bool
*/
public function canRun()
{
return $this->_execShellCmd($this->_buildVersionShellCmd()) !== false;
}
/**
* Retrieve the shell command version
*
* @return string|null
*/
public function getVersion()
{
$versionOutput = $this->_execShellCmd($this->_buildVersionShellCmd());
if (!$versionOutput) {
return null;
}
return preg_match('/[^\d]*([^\s]+)/', $versionOutput, $matches) ? $matches[1] : $versionOutput;
}
/**
* Get path to the report file
*
* @return string
*/
public function getReportFile()
{
return $this->_reportFile;
}
/**
* Build the shell command that outputs the version
*
* @return string
*/
abstract protected function _buildVersionShellCmd();
/**
* Build the valid shell command
*
* @param array $whiteList
* @param array $blackList
* @return string
*/
abstract protected function _buildShellCmd($whiteList, $blackList);
/**
* Execute a shell command on the current environment and return its output or FALSE on failure
*
* @param string $shellCmd
* @return string|false
*/
protected function _execShellCmd($shellCmd)
{
$output = [];
//phpcs:disable
exec($shellCmd . ' 2>&1', $output, $this->_lastExitCode);
//phpcs:enable
$this->_lastOutput = implode(PHP_EOL, $output);
return $this->_lastExitCode === 0 ? $this->_lastOutput : false;
}
/**
* Generate message about last execution result, prepared for output to a user
*
* @return \Magento\TestFramework\Inspection\AbstractCommand
*/
protected function _generateLastRunMessage()
{
if ($this->_lastExitCode === null) {
$this->_lastRunMessage = "Nothing was executed.";
} elseif (!$this->_lastExitCode) {
$this->_lastRunMessage = 'Success reported.';
} elseif (file_exists($this->_reportFile)) {
$this->_lastRunMessage = "See detailed report in '{$this->_reportFile}'.";
} else {
$this->_lastRunMessage = 'Command-line tool reports: ' . $this->_lastOutput;
}
return $this;
}
/**
* Return message from the last run of the command
*
* @return string
*/
public function getLastRunMessage()
{
return $this->_lastRunMessage;
}
}
@@ -0,0 +1,14 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
/**
* \Exceptional situation during the static inspection
*/
namespace Magento\TestFramework\Inspection;
class Exception extends \Exception
{
}
@@ -0,0 +1,380 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
/**
* Finder for a list of preconfigured words
*/
namespace Magento\TestFramework\Inspection;
class WordsFinder
{
/**
* List of file extensions, that indicate a binary file
*
* @var array
*/
protected $_binaryExtensions = [
'jpg', 'jpeg', 'png', 'gif', 'swf', 'mp3', 'avi', 'mov', 'flv', 'jar', 'zip',
'eot', 'ttf', 'woff', 'woff2', 'ico', 'svg',
];
/**
* Copyright string which must be present in every non-binary file
*
* @var string
*/
protected $copyrightString = 'Copyright © Magento, Inc. All rights reserved.';
/**
* Copying string which must be present in every non-binary file right after copyright string
*
* @var string
*/
protected $copyingString = 'See COPYING.txt for license details.';
/**
* List of extensions for which copyright check must be skipped
*
* @var array
*/
protected $copyrightSkipExtensions = ['csv', 'json', 'lock', 'md', 'txt'];
/**
* List of paths where copyright check must be skipped
*
* @var array
*/
protected $copyrightSkipList = [
'lib/web/legacy-build.min.js'
];
/**
* Whether copyright presence should be checked or not
*
* @var bool
*/
protected $isCopyrightChecked;
/**
* Words to search for
*
* @var array
*/
protected $_words = [];
/**
* Map of whitelisted paths to whitelisted words
*
* @var array
*/
protected $_whitelist = [];
/**
* Path to base dir, used to calculate relative paths
*
* @var string
*/
protected $_baseDir;
/**
* Component Registrar
*
* @var \Magento\Framework\Component\ComponentRegistrar
*/
protected $componentRegistrar;
/**
* Map of phrase to exclude from the file content
*
* @var array
*/
private $exclude = [];
/**
* @param string|array $configFiles
* @param string $baseDir
* @param \Magento\Framework\Component\ComponentRegistrar $componentRegistrar
* @param bool $isCopyrightChecked
* @throws \Magento\TestFramework\Inspection\Exception
*/
public function __construct($configFiles, $baseDir, $componentRegistrar, $isCopyrightChecked = false)
{
if (!is_dir($baseDir)) {
throw new \Magento\TestFramework\Inspection\Exception("Base directory {$baseDir} does not exist");
}
$this->_baseDir = str_replace('\\', '/', realpath($baseDir));
$this->componentRegistrar = $componentRegistrar;
// Load config files
if (!is_array($configFiles)) {
$configFiles = [$configFiles];
}
foreach ($configFiles as $configFile) {
$this->_loadConfig($configFile);
}
// Add config files to whitelist, as they surely contain banned words
foreach ($configFiles as $configFile) {
$configFile = str_replace('\\', '/', realpath($configFile));
$this->_whitelist[$configFile] = [];
}
$this->_normalizeWhitelistPaths();
// Final verifications
if (!$this->_words) {
throw new \Magento\TestFramework\Inspection\Exception('No words to check');
}
$this->isCopyrightChecked = $isCopyrightChecked;
}
/**
* Load configuration from file, adding words and whitelisted entries to main config
*
* @param string $configFile
* @throws \Magento\TestFramework\Inspection\Exception
*/
protected function _loadConfig($configFile)
{
if (!file_exists($configFile)) {
throw new \Magento\TestFramework\Inspection\Exception("Configuration file {$configFile} does not exist");
}
try {
$xml = new \SimpleXMLElement(file_get_contents($configFile));
} catch (\Exception $e) {
throw new \Magento\TestFramework\Inspection\Exception($e->getMessage(), $e->getCode(), $e);
}
$this->_extractWords($xml)->_extractWhitelist($xml);
}
/**
* Extract words from configuration xml
*
* @param \SimpleXMLElement $configXml
* @return \Magento\TestFramework\Inspection\WordsFinder
* @throws \Magento\TestFramework\Inspection\Exception
*/
protected function _extractWords(\SimpleXMLElement $configXml)
{
$words = [];
$nodes = $configXml->xpath('//config/words/word');
foreach ($nodes as $node) {
$words[] = (string)$node;
}
$words = array_filter($words);
$words = array_merge($this->_words, $words);
$this->_words = array_unique($words);
return $this;
}
/**
* Extract whitelisted entries and words from configuration xml
*
* @param \SimpleXMLElement $configXml
* @return \Magento\TestFramework\Inspection\WordsFinder
* @throws \Magento\TestFramework\Inspection\Exception
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
protected function _extractWhitelist(\SimpleXMLElement $configXml)
{
// Load whitelist entries
$whitelist = [];
$exclude = [];
$nodes = $configXml->xpath('//config/whitelist/item');
foreach ($nodes as $node) {
$path = $node->xpath('path');
if (!$path) {
throw new \Magento\TestFramework\Inspection\Exception(
'A "path" must be defined for the whitelisted item'
);
}
$component = $node->xpath('component');
if ($component) {
$componentType = $component[0]->xpath('@type')[0];
$componentName = $component[0]->xpath('@name')[0];
$path = $this->componentRegistrar->getPath((string)$componentType, (string)$componentName)
. '/' . (string)$path[0];
} else {
$path = $this->_baseDir . '/' . (string)$path[0];
}
// Words
$words = [];
$wordNodes = $node->xpath('word');
if ($wordNodes) {
foreach ($wordNodes as $wordNode) {
$words[] = (string)$wordNode;
}
}
$whitelist[$path] = $words;
$excludeNodes = $node->xpath('exclude');
$excludes = [];
if ($excludeNodes) {
foreach ($excludeNodes as $extractNode) {
$excludes[] = (string)$extractNode;
}
}
if (isset($exclude[$path])) {
$exclude[$path] = array_merge($excludes, $exclude[$path]);
} else {
$exclude[$path] = $excludes;
}
}
// Merge with already present whitelist
foreach ($whitelist as $newPath => $newWords) {
if (isset($this->_whitelist[$newPath])) {
$newWords = array_merge($this->_whitelist[$newPath], $newWords);
}
$this->_whitelist[$newPath] = array_unique($newWords);
}
foreach ($exclude as $newPath => $newWords) {
if (isset($this->exclude[$newPath])) {
$newWords = array_merge($this->exclude[$newPath], $newWords);
}
$this->exclude[$newPath] = array_unique($newWords);
}
return $this;
}
/**
* Normalize whitelist paths, so that they containt only native directory separators
*/
protected function _normalizeWhitelistPaths()
{
$whitelist = $this->_whitelist;
$this->_whitelist = [];
foreach ($whitelist as $whitelistFile => $whitelistWords) {
$whitelistFile = str_replace('\\', '/', $whitelistFile);
$this->_whitelist[$whitelistFile] = $whitelistWords;
}
}
/**
* Checks the file content and name against the list of words. Do not check content of binary files.
* Exclude whitelisted entries.
*
* @param string $file
* @return array Words, found
*/
public function findWords($file)
{
$foundWords = $this->_findWords($file);
if (!$foundWords) {
return [];
}
return self::_removeWhitelistedWords($file, $foundWords);
}
/**
* Checks the file content and name against the list of words. Do not check content of binary files.
*
* @param string $file
* @return array
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
protected function _findWords($file)
{
$checkContents = !$this->_isBinaryFile($file);
$path = $this->getSearchablePath($file);
$contents = $checkContents ? file_get_contents($file) : '';
if (isset($this->exclude[$file]) && !empty($this->exclude[$file])) {
foreach ($this->exclude[$file] as $stringToEliminate) {
$contents = str_replace($stringToEliminate, "", $contents);
}
}
$foundWords = [];
foreach ($this->_words as $word) {
if (stripos($path, $word) !== false || stripos($contents, $word) !== false) {
$foundWords[] = $word;
}
}
if ($contents && $this->isCopyrightChecked && !$this->isCopyrightCheckSkipped($file)
&& (($copyrightStringPosition = mb_strpos($contents, $this->copyrightString)) === false
|| ($copyingStringPosition = strpos($contents, $this->copyingString)) === false
|| $copyingStringPosition - $copyrightStringPosition - mb_strlen($this->copyrightString) > 10)
) {
$foundWords[] = 'Copyright string is missing';
}
return $foundWords;
}
/**
* @param string $path
* @return bool
*/
protected function isCopyrightCheckSkipped($path)
{
if (in_array(pathinfo($path, PATHINFO_EXTENSION), $this->copyrightSkipExtensions)) {
return true;
}
foreach ($this->copyrightSkipList as $dir) {
if (strpos($path, $dir) !== false) {
return true;
}
}
return false;
}
/**
* Check, whether file is a binary one
*
* @param string $file
* @return bool
*/
protected function _isBinaryFile($file)
{
return in_array(pathinfo($file, PATHINFO_EXTENSION), $this->_binaryExtensions);
}
/**
* Removes whitelisted words from array of found words
*
* @param string $path
* @param array $foundWords
* @return array
*/
protected function _removeWhitelistedWords($path, $foundWords)
{
$path = str_replace('\\', '/', $path);
foreach ($this->_whitelist as $whitelistPath => $whitelistWords) {
if (strncmp($whitelistPath, $path, strlen($whitelistPath)) != 0) {
continue;
}
if (!$whitelistWords) {
// All words are permitted there
return [];
}
$foundWords = array_diff($foundWords, $whitelistWords);
}
return $foundWords;
}
/**
* Return the path for words search
*
* @param string $file
* @return string
*/
protected function getSearchablePath($file)
{
if (strpos($file, $this->_baseDir) === false) {
return $file;
}
return substr($file, strlen($this->_baseDir) + 1);
}
}
@@ -0,0 +1,213 @@
<?php
/**
* An abstract test class for XML/XSD validation
*
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Integrity;
abstract class AbstractConfig extends \PHPUnit\Framework\TestCase
{
public function testXmlFiles()
{
if (null === $this->_getXmlName()) {
$this->markTestSkipped('No XML validation of files requested');
}
$invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this);
$invoker(
/**
* @param string $configFile
*/
function ($configFile) {
$this->_validateFileExpectSuccess($configFile, $this->_getXsd(), $this->_getFileXsd());
},
\Magento\Framework\App\Utility\Files::init()->getConfigFiles($this->_getXmlName())
);
}
public function testSchemaUsingValidXml()
{
$xmlFile = $this->_getKnownValidXml();
$schema = $this->_getXsd();
$this->_validateFileExpectSuccess($xmlFile, $schema);
}
public function testSchemaUsingInvalidXml($expectedErrors = null)
{
if (!function_exists('libxml_set_external_entity_loader')) {
$this->markTestSkipped('Skipped due to MAGETWO-45033');
}
$xmlFile = $this->_getKnownInvalidXml();
$schema = $this->_getXsd();
$this->_validateFileExpectFailure($xmlFile, $schema, $expectedErrors);
}
public function testFileSchemaUsingPartialXml()
{
if (!function_exists('libxml_set_external_entity_loader')) {
$this->markTestSkipped('Skipped due to MAGETWO-45033');
}
$xmlFile = $this->_getKnownValidPartialXml();
if ($xmlFile === null) {
$this->markTestSkipped('No Partial File');
return;
}
$schema = $this->_getFileXsd();
$this->_validateFileExpectSuccess($xmlFile, $schema);
}
public function testFileSchemaUsingInvalidXml($expectedErrors = null)
{
if (!function_exists('libxml_set_external_entity_loader')) {
$this->markTestSkipped('Skipped due to MAGETWO-45033');
}
$xmlFile = $this->_getKnownInvalidPartialXml();
if ($xmlFile === null) {
$this->markTestSkipped('No Partial File');
return;
}
$schema = $this->_getFileXsd();
$this->_validateFileExpectFailure($xmlFile, $schema, $expectedErrors);
}
public function testSchemaUsingPartialXml($expectedErrors = null)
{
if (!function_exists('libxml_set_external_entity_loader')) {
$this->markTestSkipped('Skipped due to MAGETWO-45033');
}
$xmlFile = $this->_getKnownValidPartialXml();
if ($xmlFile === null) {
$this->markTestSkipped('No Partial File');
return;
}
$schema = $this->_getXsd();
$this->_validateFileExpectFailure($xmlFile, $schema, $expectedErrors);
}
/**
* Run schema validation against a known bad xml file with a provided schema.
*
* This helper expects the validation to fail and will fail a test if no errors are found.
*
* @param $xmlFile string a known bad xml file.
* @param $schemaFile string schema that should find errors in the known bad xml file.
* @param $fileSchemaFile string schema that should find errors in the known bad xml file
*/
protected function _validateFileExpectSuccess($xmlFile, $schemaFile, $fileSchemaFile = null)
{
$dom = new \DOMDocument();
$dom->loadXML(file_get_contents($xmlFile));
$errors = \Magento\Framework\Config\Dom::validateDomDocument($dom, $schemaFile);
if ($errors) {
if ($fileSchemaFile !== null) {
$moreErrors = \Magento\Framework\Config\Dom::validateDomDocument($dom, $fileSchemaFile);
if (empty($moreErrors)) {
return;
} else {
$errors = array_merge($errors, $moreErrors);
}
}
$this->fail(
'There is a problem with the schema. A known good XML file failed validation: ' . PHP_EOL . implode(
PHP_EOL . PHP_EOL,
$errors
)
);
}
}
/**
* Run schema validation against an xml file with a provided schema.
*
* This helper expects the validation to pass and will fail a test if any errors are found.
*
* @param $xmlFile string a known good xml file.
* @param $schemaFile string schema that should find no errors in the known good xml file.
* @param $expectedErrors null|array that may contain a list of expected errors. Each element can be a substring
* of an error, but all errors must be listed.
*/
protected function _validateFileExpectFailure($xmlFile, $schemaFile, $expectedErrors = null)
{
$dom = new \DOMDocument();
$dom->loadXML(file_get_contents($xmlFile));
$actualErrors = \Magento\Framework\Config\Dom::validateDomDocument($dom, $schemaFile);
if (isset($expectedErrors)) {
$this->assertNotEmpty(
$actualErrors,
'No schema validation errors found, expected errors: ' . PHP_EOL . implode(PHP_EOL, $expectedErrors)
);
foreach ($expectedErrors as $expectedError) {
$found = false;
foreach ($actualErrors as $errorKey => $actualError) {
if (!(strpos($actualError, $expectedError) === false)) {
// found expected string
$found = true;
// remove found error from list of actual errors
unset($actualErrors[$errorKey]);
break;
}
}
$this->assertTrue(
$found,
'Failed asserting that ' . $expectedError . " is in: \n" . implode(PHP_EOL, $actualErrors)
);
}
// list of actual errors should now be empty
$this->assertEmpty($actualErrors, "There were unexpected errors: \n" . implode(PHP_EOL, $actualErrors));
} elseif (!$actualErrors) {
$this->fail('There is a problem with the schema. A known bad XML file passed validation');
}
}
/**
* Returns the name of the XSD file to be used to validate the XML
*
* @return string
*/
abstract protected function _getXsd();
/**
* The location of a single valid complete xml file
*
* @return string
*/
abstract protected function _getKnownValidXml();
/**
* The location of a single known invalid complete xml file
*
* @return string
*/
abstract protected function _getKnownInvalidXml();
/**
* The location of a single known valid partial xml file
*
* @return string
*/
abstract protected function _getKnownValidPartialXml();
/**
* Returns the name of the XSD file to be used to validate partial XML
*
* @return string
*/
abstract protected function _getFileXsd();
/**
* The location of a single known invalid partial xml file
*
* @return string
*/
abstract protected function _getKnownInvalidPartialXml();
/**
* Returns the name of the xml files to validate
*
* @return string
*/
abstract protected function _getXmlName();
}
@@ -0,0 +1,57 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Integrity\Library;
use Laminas\Code\Reflection\ClassReflection;
use Magento\Framework\GetParameterClassTrait;
use ReflectionException;
/**
* Provide dependencies for the file
*/
class Injectable
{
use GetParameterClassTrait;
/**
* @var string[]
*/
protected $dependencies = [];
/**
* Get dependencies
*
* @param ClassReflection $class
*
* @return string[]
* @throws ReflectionException
*/
public function getDependencies(ClassReflection $class): array
{
foreach ($class->getMethods() as $method) {
if ($method->getDeclaringClass()->getName() !== $class->getName()) {
continue;
}
foreach ($method->getParameters() as $parameter) {
try {
$dependency = $this->getParameterClass($parameter);
if ($dependency !== null) {
$this->dependencies[] = $dependency->getName();
}
} catch (ReflectionException $e) {
if (preg_match('#^Class ([A-Za-z0-9_\"\\\\]+) does not exist$#', $e->getMessage(), $result)) {
$this->dependencies[] = trim($result[1], '"');
} else {
throw $e;
}
}
}
}
return $this->dependencies;
}
}
@@ -0,0 +1,21 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Integrity\Library\PhpParser;
/**
* Collect dependencies
*
*/
interface DependenciesCollectorInterface
{
/**
* Return list of dependencies
*
* @param Uses $uses
* @return string[]
*/
public function getDependencies(Uses $uses);
}
@@ -0,0 +1,86 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Integrity\Library\PhpParser;
/**
* Class know how create any parser
*
*/
class ParserFactory
{
/**
* @var ParserInterface[]
*/
protected $parsers = [];
/**
* @var Uses
*/
protected $uses;
/**
* @var StaticCalls
*/
protected $staticCalls;
/**
* @var Throws
*/
protected $throws;
/**
* @var Tokens
*/
protected $tokens;
/**
* Return all parsers
*
* @param Tokens $tokens
* @return ParserInterface[]
*/
public function createParsers(Tokens $tokens)
{
if (empty($this->parsers)) {
$this->parsers = [
$this->uses = new Uses(),
$this->staticCalls = new StaticCalls($tokens),
$this->throws = new Throws($tokens),
];
}
return $this->parsers;
}
/**
* Get uses
*
* @return Uses
*/
public function getUses()
{
return $this->uses;
}
/**
* Get static calls
*
* @return StaticCalls
*/
public function getStaticCalls()
{
return $this->staticCalls;
}
/**
* Get throws
*
* @return Throws
*/
public function getThrows()
{
return $this->throws;
}
}
@@ -0,0 +1,21 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Integrity\Library\PhpParser;
/**
* Parser for each token type
*
*/
interface ParserInterface
{
/**
* Parse specific token
*
* @param array|string $value
* @param int $key
*/
public function parse($value, $key);
}
@@ -0,0 +1,119 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Integrity\Library\PhpParser;
/**
* Parse static calls and collect dependencies for it
*
*/
class StaticCalls implements ParserInterface, DependenciesCollectorInterface
{
/**
* @var Tokens
*/
protected $tokens;
/**
* Save static calls token key
*
* @var array
*/
protected $staticCalls = [];
/**
* Collect dependencies
*
* @var array
*/
protected $dependencies = [];
/**
* @param Tokens $tokens
*/
public function __construct(Tokens $tokens)
{
$this->tokens = $tokens;
}
/**
* Check if it's foreign dependency
*
* @param array $token
* @return bool
*/
protected function isTokenClass($token)
{
return is_array(
$token
) && !(in_array(
$token[1],
['static', 'self', 'parent']
) || preg_match(
'#^\$#',
$token[1]
));
}
/**
* @inheritdoc
*/
public function parse($token, $key)
{
if (is_array(
$token
) && $token[0] == T_PAAMAYIM_NEKUDOTAYIM && $this->isTokenClass(
$this->tokens->getPreviousToken($key)
)
) {
$this->staticCalls[] = $key;
}
}
/**
* Return class name from token
*
* @param int $staticCall
* @return string
*/
protected function getClassByStaticCall($staticCall)
{
$step = 1;
$staticClassParts = [];
$token = $this->tokens->getTokenCodeByKey($staticCall - $step);
if ($token === T_NAME_FULLY_QUALIFIED || $token === T_NAME_QUALIFIED) {
return $this->tokens->getTokenValueByKey($staticCall - $step);
}
// PHP 7 compatibility
while ($this->tokens->getTokenCodeByKey(
$staticCall - $step
) == T_STRING || $this->tokens->getTokenCodeByKey(
$staticCall - $step
) == T_NS_SEPARATOR) {
$staticClassParts[] = $this->tokens->getTokenValueByKey($staticCall - $step);
$step++;
}
return implode(array_reverse($staticClassParts));
}
/**
* @inheritdoc
*/
public function getDependencies(Uses $uses)
{
foreach ($this->staticCalls as $staticCall) {
$class = $this->getClassByStaticCall($staticCall);
if ($uses->hasUses()) {
$class = $uses->getClassNameWithNamespace($class);
}
$this->dependencies[] = $class;
}
return $this->dependencies;
}
}
@@ -0,0 +1,82 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Integrity\Library\PhpParser;
/**
* Parse throws and collect dependencies for it
*
*/
class Throws implements ParserInterface, DependenciesCollectorInterface
{
/**
* @var Tokens
*/
protected $tokens;
/**
* Collect dependencies
*
* @var array
*/
protected $dependencies = [];
/**
* Save throw token key
*
* @var array
*/
protected $throws = [];
/**
* @param Tokens $tokens
*/
public function __construct(Tokens $tokens)
{
$this->tokens = $tokens;
}
/**
* @inheritdoc
*/
public function parse($token, $key)
{
if (is_array($token) && $token[0] == T_THROW) {
$this->throws[] = $key;
}
}
/**
* @inheritdoc
*/
public function getDependencies(Uses $uses)
{
foreach ($this->throws as $throw) {
$class = '';
if ($this->tokens->getTokenCodeByKey($throw + 2) == T_NEW) {
$step = 4;
$token = $this->tokens->getTokenCodeByKey($throw + $step);
if ($token === T_NAME_FULLY_QUALIFIED || $token === T_NAME_QUALIFIED) {
$class = $this->tokens->getTokenValueByKey($throw + $step);
} else {
// PHP 7 compatibility
while ($this->tokens->getTokenCodeByKey($throw + $step) === T_STRING
|| $this->tokens->getTokenCodeByKey($throw + $step) === T_NS_SEPARATOR) {
$class .= trim($this->tokens->getTokenValueByKey($throw + $step));
$step++;
}
}
if ($uses->hasUses()) {
$class = $uses->getClassNameWithNamespace($class);
}
$this->dependencies[] = $class;
}
}
return $this->dependencies;
}
}
@@ -0,0 +1,120 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Integrity\Library\PhpParser;
/**
* Parse php code and found dependencies
*
*/
class Tokens
{
/**
* Collect all tokens
*
* @var array
*/
protected $tokens = [];
/**
* Collect dependencies
*
* @var array
*/
protected $dependencies = [];
/**
* Collect all parsers
*
* @var ParserInterface[]
*/
protected $parsers = [];
/**
* Parser factory for creating parsers
*
* @var ParserFactory
*/
protected $parserFactory;
/**
* @param string $content
* @param ParserFactory $parserFactory
*/
public function __construct($content, ParserFactory $parserFactory)
{
$this->tokens = token_get_all($content);
$this->parserFactory = $parserFactory;
}
/**
* Parse content
*/
public function parseContent()
{
foreach ($this->tokens as $k => $token) {
foreach ($this->getParsers() as $parser) {
$parser->parse($token, $k);
}
}
}
/**
* Get all parsers
*
* @return ParserInterface[]
*/
protected function getParsers()
{
return $this->parserFactory->createParsers($this);
}
/**
* Get parsed dependencies
*
* @return array
*/
public function getDependencies()
{
return array_merge(
$this->parserFactory->getStaticCalls()->getDependencies($this->parserFactory->getUses()),
$this->parserFactory->getThrows()->getDependencies($this->parserFactory->getUses())
);
}
/**
* Return previous token
*
* @param int $key
* @param int $step
* @return array
*/
public function getPreviousToken($key, $step = 1)
{
return $this->tokens[$key - $step];
}
/**
* Return token code by key
*
* @param $key
* @return null|int
*/
public function getTokenCodeByKey($key)
{
return is_array($this->tokens[$key]) ? $this->tokens[$key][0] : null;
}
/**
* Return token value by key
*
* @param $key
* @return string
*/
public function getTokenValueByKey($key)
{
return is_array($this->tokens[$key]) ? $this->tokens[$key][1] : $this->tokens[$key];
}
}
@@ -0,0 +1,135 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Integrity\Library\PhpParser;
/**
* Parse uses block
*
*/
class Uses implements ParserInterface
{
/**
* Flag for parse use block
*
* @var bool
*/
protected $parseUse = false;
/**
* Collect all uses
*
* @var array
*/
protected $uses = [];
/**
* Check if uses present in content
*
* @return bool
*/
public function hasUses()
{
return !empty($this->uses);
}
/**
* Create empty uses in collection
*/
protected function createEmptyItem()
{
$this->uses[] = '';
}
/**
* Return class name with namespace
*
* @param string $class
* @return string
*/
public function getClassNameWithNamespace($class)
{
if (preg_match('#^\\\\#', $class)) {
return $class;
}
preg_match('#^([A-Za-z0-9_]+)(.*)$#', $class, $match);
foreach ($this->uses as $use) {
if (preg_match('#^([A-Za-z0-9_\\\\]+)\s+as\s+(.*)$#', $use, $useMatch) && $useMatch[2] == $match[1]) {
$class = $useMatch[1] . $match[2];
break;
}
$packages = explode('\\', $use);
end($packages);
$lastPackageKey = key($packages);
if ($packages[$lastPackageKey] == $match[1]) {
$class = $use . $match[2];
}
}
return $class;
}
/**
* Append part of uses into last item
*
* @param string $value
*/
protected function appendToLast($value)
{
end($this->uses);
$this->uses[key($this->uses)] = ltrim($this->uses[key($this->uses)] . $value);
}
/**
* Check flag parse
*
* @return bool
*/
protected function isParseInProgress()
{
return $this->parseUse;
}
/**
* Start parse
*/
protected function stopParse()
{
$this->parseUse = false;
}
/**
* Stop parse
*/
protected function startParse()
{
$this->parseUse = true;
}
/**
* @inheritdoc
*/
public function parse($token, $key)
{
if (is_array($token)) {
if ($this->isParseInProgress()) {
$this->appendToLast($token[1]);
}
if (T_USE == $token[0]) {
$this->startParse();
$this->createEmptyItem();
}
} else {
if ($this->isParseInProgress()) {
if ($token == ';') {
$this->stopParse();
}
if ($token == ',') {
$this->createEmptyItem();
}
}
}
}
}
@@ -0,0 +1,58 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Integrity;
use Magento\Framework\Exception\ValidatorException;
use Magento\Framework\Interception\Code\InterfaceValidator;
use Magento\Framework\Phrase;
class PluginValidator
{
/** @var InterfaceValidator */
private $frameworkValidator;
/**
* @param InterfaceValidator $frameworkValidator
*/
public function __construct(InterfaceValidator $frameworkValidator)
{
$this->frameworkValidator = $frameworkValidator;
}
/**
* Validate plugin and intercepted class
*
* @param string $pluginClass
* @param string $interceptedType
* @throws ValidatorException
*/
public function validate($pluginClass, $interceptedType)
{
$this->frameworkValidator->validate($pluginClass, $interceptedType);
$this->validateClassNameMatchesCase($pluginClass);
$this->validateClassNameMatchesCase($interceptedType);
}
/**
* Verify that the class name has same capitalization as the class declaration
*
* @param string $className
* @throws ValidatorException
*/
private function validateClassNameMatchesCase($className)
{
$declarationName = (new \ReflectionClass($className))->getName();
if (ltrim($className, '\\') != ltrim($declarationName)) {
throw new ValidatorException(
new Phrase(
"Capitalization of class name '%1' does not match expected '%2'",
[$className, $declarationName]
)
);
}
}
}
@@ -0,0 +1,35 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\TestFramework\Utility;
/**
* Helper class to add list of added new files.
*/
class AddedFiles
{
/**
* Provide list of new files.
*
* @param string $changedFilesBaseDir
*
* @return string[]
*/
public static function getAddedFilesList(string $changedFilesBaseDir): array
{
return FilesSearch::getFilesFromListFile(
$changedFilesBaseDir,
'changed_files*.added.*',
function () {
// if no list files, probably, this is the dev environment
// phpcs:ignore Generic.PHP.NoSilencedErrors,Magento2.Security.InsecureFunction
@exec('git diff --cached --name-only --diff-filter=A', $addedFiles);
return $addedFiles;
}
);
}
}
@@ -0,0 +1,149 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\TestFramework\Utility;
use ReflectionNamedType;
/**
* Find classes created by object manager that are not requested in constructor. All autogenerated classes should be
* requested in constructor, otherwise compiler will not be able to find and generate these classes
*/
class AutogeneratedClassNotInConstructorFinder
{
/**
* @var ClassNameExtractor
*/
private $classNameExtractor;
/**
* Constructor
*
* @param ClassNameExtractor $classNameExtractor
*/
public function __construct(
ClassNameExtractor $classNameExtractor
) {
$this->classNameExtractor = $classNameExtractor;
}
/**
* Find classes created by object manager that are not requested in constructor
*
* @param string $fileContent
* @return array
* @throws \ReflectionException
*/
public function find($fileContent)
{
$classNames = [];
preg_match_all(
'/(get|create)\(\s*([a-z0-9\\\\]+)::class\s*\)/im',
$fileContent,
$shortNameMatches
);
if (isset($shortNameMatches[2])) {
foreach ($shortNameMatches[2] as $shortName) {
if (substr($shortName, 0, 1) !== '\\') {
$class = $this->matchPartialNamespace($fileContent, $shortName);
} else {
$class = $shortName;
}
$class = ltrim($class, '\\');
if (\Magento\Framework\App\Utility\Classes::isVirtual($class)) {
continue;
}
$className = $this->classNameExtractor->getNameWithNamespace($fileContent);
if ($className) {
$arguments = $this->getConstructorArguments($className);
if (in_array($class, $arguments)) {
continue;
}
}
$classNames[] = $class;
}
}
return $classNames;
}
/**
* Get constructor arguments
*
* @param string $className
* @return string[]
* @throws \ReflectionException
*/
private function getConstructorArguments($className)
{
$arguments = [];
$reflectionClass = new \ReflectionClass($className);
$constructor = $reflectionClass->getConstructor();
if ($constructor) {
$classParameters = $constructor->getParameters();
foreach ($classParameters as $classParameter) {
if ($classParameter->getType()) {
/** @var ReflectionNamedType $parameterType */
$parameterType = $classParameter->getType();
$arguments[] = ltrim($parameterType->getName(), '\\');
}
}
}
return $arguments;
}
/**
* Match partial namespace
*
* @param string $fileContent
* @param string $shortName
* @return string
*/
private function matchPartialNamespace(string $fileContent, string $shortName): string
{
preg_match_all(
'/^use\s([a-z0-9\\\\]+' . str_replace('\\', '\\\\', $shortName) . ');$/im',
$fileContent,
$fullNameMatches
);
if (isset($fullNameMatches[1][0])) {
$class = $fullNameMatches[1][0];
} else {
preg_match_all(
'/^use\s([a-z0-9\\\\]+)\sas\s' . str_replace('\\', '\\\\', $shortName) . ';$/im',
$fileContent,
$fullNameAliasMatches
);
if (isset($fullNameAliasMatches[1][0])) {
$class = $fullNameAliasMatches[1][0];
} else {
$forwardSlashPos = strpos($shortName, '\\');
if (is_int($forwardSlashPos)) {
$partialNamespace = substr($shortName, 0, $forwardSlashPos);
} else {
$partialNamespace = $shortName;
}
preg_match_all(
'/^use\s([a-z0-9\\\\]+)' . $partialNamespace . ';$/im',
$fileContent,
$partialNamespaceMatches
);
if ($forwardSlashPos && isset($partialNamespaceMatches[1][0])) {
$class = $partialNamespaceMatches[1][0] . $shortName;
} else {
$class = $this->classNameExtractor->getNamespace($fileContent) . '\\' . $shortName;
}
}
}
return $class;
}
}
@@ -0,0 +1,86 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Utility;
use Magento\Framework\App\Utility\Files;
/**
* A helper to gather various changed files
* if INCREMENTAL_BUILD env variable is set by CI build infrastructure, only files changed in the
* branch are gathered, otherwise all files
*/
class ChangedFiles
{
/**
* File path with changed files content.
*/
const CHANGED_FILES_CONTENT_FILE = '/dev/tests/static/testsuite/Magento/Test/_files/changed_%s_files_content.json';
/**
* Returns array of PHP-files, that use or declare Magento application classes and Magento libs
*
* @param string $changedFilesList
* @param int $fileTypes
* @return array
*/
public static function getPhpFiles($changedFilesList, $fileTypes = 0)
{
$fileUtilities = Files::init();
if (isset($_ENV['INCREMENTAL_BUILD'])) {
$phpFiles = [];
foreach (glob($changedFilesList, GLOB_NOSORT) as $listFile) {
$phpFiles = array_merge($phpFiles, file($listFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES));
}
array_walk(
$phpFiles,
function (&$file) {
$file = BP . '/' . $file;
}
);
if (!empty($phpFiles)) {
$phpFiles = Files::composeDataSets($phpFiles);
$phpFiles = array_intersect_key($phpFiles, $fileUtilities->getPhpFiles($fileTypes));
}
} else {
$phpFiles = $fileUtilities->getPhpFiles($fileTypes);
}
return $phpFiles;
}
/**
* Get changed content.
*
* @param string $fileName
* @return string
*/
public static function getChangedContent($fileName)
{
$data = [];
$extension = self::getFileExtension($fileName);
$fileName = ltrim(str_replace(BP, '', $fileName), DIRECTORY_SEPARATOR);
$changedFilesContentFile = BP . sprintf(self::CHANGED_FILES_CONTENT_FILE, $extension);
if (file_exists($changedFilesContentFile)) {
$changedContent = file_get_contents($changedFilesContentFile);
$data = json_decode($changedContent, true);
}
return isset($data[$fileName]) ? $data[$fileName] : '';
}
/**
* Get file extension.
*
* @param string $fileName
* @return string
*/
public static function getFileExtension($fileName)
{
$fileInfo = pathinfo($fileName);
return isset($fileInfo['extension']) ? $fileInfo['extension'] : 'unknown';
}
}
@@ -0,0 +1,56 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\TestFramework\Utility;
/**
* Search for children classes in list of files.
*/
class ChildrenClassesSearch
{
/**
* @var ClassNameExtractor
*/
private $classNameExtractor;
/**
* ChildrenClassesSearch constructor.
*/
public function __construct()
{
$this->classNameExtractor = new ClassNameExtractor();
}
/**
* Get list of classes name which are subclasses of mentioned class.
*
* @param array $fileList
* @param string $parent
* @param bool $asDataSet
*
* @return array
* @throws \ReflectionException
*/
public function getClassesWhichAreChildrenOf(array $fileList, string $parent, bool $asDataSet = true): array
{
$found = [];
foreach ($fileList as $file) {
$name = $asDataSet ? $file[0] : $file;
$class = $this->classNameExtractor->getNameWithNamespace(file_get_contents($name));
if ($class) {
$classReflection = new \ReflectionClass($class);
if ($classReflection->isSubclassOf($parent)) {
$found[] = $class;
}
}
}
return $found;
}
}
@@ -0,0 +1,66 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Utility;
class ClassNameExtractor
{
/**
* Get class name with namespace
*
* @param string $fileContent
* @return bool|string
*/
public function getNameWithNamespace($fileContent)
{
$namespace = $this->getNamespace($fileContent);
$name = $this->getName($fileContent);
if ($name) {
return $namespace ? $namespace . '\\' . $name : $name;
}
return false;
}
/**
* Get class name
*
* @param string $fileContent
* @return string|bool
*/
public function getName($fileContent)
{
$namespace = $this->getNamespace($fileContent);
if (isset($namespace)) {
preg_match_all(
'/^\s*(class|abstract\sclass|interface)\s+([a-z0-9]+)(\s+(extends|implements)|\s*$)/im',
$fileContent,
$classMatches
);
if (isset($classMatches[2][0])) {
return $classMatches[2][0];
}
}
return false;
}
/**
* Get class namespace
*
* @param string $fileContent
* @return string|bool
*/
public function getNamespace($fileContent)
{
preg_match_all(
'/namespace\s([a-z0-9\\\\]+);/im',
$fileContent,
$namespaceMatches
);
if (isset($namespaceMatches[1][0])) {
return $namespaceMatches[1][0];
}
return false;
}
}
@@ -0,0 +1,150 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Utility;
/**
* Searches for usage of classes, namespaces, functions, etc in PHP files
*/
class CodeCheck
{
/**
* Check if the class is used in the content
*
* @param string $className
* @param string $content
* @return bool
*/
public static function isClassUsed($className, $content)
{
/* avoid matching namespace instead of class */
$content = preg_replace('/namespace[^;]+;/', '', $content);
return self::_isRegexpMatched('/[^a-z\d_\$]' . preg_quote($className, '/') . '[^a-z\d_\\\\]/iS', $content);
}
/**
* Check if the namespace is used in the content
*
* @param string $namespace
* @param string $content
* @return bool
*/
public static function isNamespaceUsed($namespace, $content)
{
return self::_isRegexpMatched('/namespace\s+' . preg_quote($namespace, '/') . ';/S', $content)
|| self::_isRegexpMatched('/[^a-zA-Z\d_]' . preg_quote($namespace . '\\', '/') . '/S', $content);
}
/**
* Check if the specified function is called in the content.
* Note that declarations are not considered.
*
* If class context is not specified, invocation of all functions or methods (of any class)
* will be matched across the board
*
* If some class is specified, only its methods will be matched as follows:
* - usage of class::method
* - usage of $this, self and static within the class and its descendants
*
* @param string $method
* @param string $content
* @param string $class
* @return bool
*/
public static function isFunctionCalled($method, $content, $class = null)
{
$quotedMethod = preg_quote($method, '/');
if (!$class) {
return self::_isRegexpMatched(
'/(?<![a-z\d_:]|->|function\s)' . $quotedMethod . '\s*\(/iS',
$content
);
}
// without opening parentheses to match static callbacks notation
if (self::_isRegexpMatched(
'/' . preg_quote($class, '/') . '::\s*' . $quotedMethod . '[^a-z\d_]/iS',
$content
)
) {
return true;
}
if (self::isClassOrInterface($content, $class) || self::isDirectDescendant($content, $class)) {
return self::_isRegexpMatched('/this->' . $quotedMethod . '\s*\(/iS', $content)
|| self::_isRegexpMatched(
'/(self|static|parent)::\s*' . $quotedMethod . '\s*\(/iS',
$content
);
}
}
/**
* Check if methods or functions are used in the content
*
* If class context is specified, only the class and its descendants will be checked.
*
* @param string $property
* @param string $content
* @param string $class
* @return bool
*/
public static function isPropertyUsed($property, $content, $class = null)
{
if ($class) {
if (!self::isClassOrInterface($content, $class) && !self::isDirectDescendant($content, $class)) {
return false;
}
}
return self::_isRegexpMatched(
'/[^a-z\d_]' . preg_quote($property, '/') . '[^a-z\d_]/iS',
$content
);
}
/**
* Analyze content to determine whether it is a declaration of the specified class/interface
*
* @param string $content
* @param string $name
* @return bool
*/
public static function isClassOrInterface($content, $name)
{
return self::_isRegexpMatched('/\b(?:class|interface)\s+' . preg_quote($name, '/') . '\b[^{]*\{/iS', $content);
}
/**
* Analyze content to determine whether this is a direct descendant of the specified class/interface
*
* @param string $content
* @param string $name
* @return bool
*/
public static function isDirectDescendant($content, $name)
{
$name = preg_quote($name, '/');
return self::_isRegexpMatched(
'/\s+extends\s+\\\\?' . $name . '\b|\s+implements\s+[^{]*\b' . $name . '\b[^{^\\\\]*\{/iS',
$content
);
}
/**
* Check if content matches the regexp
*
* @param string $regexp
* @param string $content
* @throws \Exception
* @return bool True if the content matches the regexp
*/
protected static function _isRegexpMatched($regexp, $content)
{
$result = preg_match($regexp, $content);
if ($result === false) {
throw new \Exception('An error occurred when matching regexp "' . $regexp . '""');
}
return 1 === $result;
}
}
@@ -0,0 +1,26 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Utility\File;
/**
* Factory for \RegexIterator
*/
class RegexIteratorFactory
{
/**
* Create instance of \RegexIterator
*
* @param string $directoryPath
* @param string $regexp
* @return \RegexIterator
*/
public function create($directoryPath, $regexp)
{
$directory = new \RecursiveDirectoryIterator($directoryPath);
$recursiveIterator = new \RecursiveIteratorIterator($directory);
return new \RegexIterator($recursiveIterator, $regexp, \RegexIterator::GET_MATCH);
}
}
@@ -0,0 +1,48 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\TestFramework\Utility;
/**
* Helper class to search files by provided directory and file pattern.
*/
class FilesSearch
{
/**
* Read files from generated lists.
*
* @param string $listsBaseDir
* @param string $listFilePattern
* @param callable $noListCallback
* @return string[]
*/
public static function getFilesFromListFile(
string $listsBaseDir,
string $listFilePattern,
callable $noListCallback
): array {
$filesDefinedInList = [];
$listFiles = glob($listsBaseDir . '/_files/' . $listFilePattern);
if (!empty($listFiles)) {
foreach ($listFiles as $listFile) {
$filesDefinedInList[] = file($listFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
}
$filesDefinedInList = array_merge([], ...$filesDefinedInList);
} else {
$filesDefinedInList = call_user_func($noListCallback);
}
array_walk(
$filesDefinedInList,
function (&$file) {
$file = BP . '/' . $file;
}
);
$filesDefinedInList = array_values(array_unique($filesDefinedInList));
return $filesDefinedInList;
}
}
@@ -0,0 +1,113 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Utility;
/**
* Check if one or more functions are used in the file
*/
class FunctionDetector
{
/**
* Detect functions in given file
*
* return result in this format:
* [
* line_number => [
* function_name_1,
* function_name_2,
* function_name_3,
* ],
* line_number => [
* function_name_1,
* function_name_2,
* function_name_3,
* ],
* ]
*
* @param string $filePath
* @param string[] $functions
* @return array
*/
public function detect($filePath, $functions)
{
$result = [];
$regexp = $this->composeRegexp($functions);
if (!$regexp) {
return $result;
}
$fileContent = \Magento\TestFramework\Utility\ChangedFiles::getChangedContent($filePath);
$file = file($filePath);
return $fileContent
? $this->grepChangedContent($file, $regexp, $functions, $fileContent)
: $this->grepFile($file, $regexp);
}
/**
* Grep only changed content.
*
* @param array $file
* @param string $regexp
* @param string[] $functions
* @param string $fileContent
* @return array
*/
public function grepChangedContent(array $file, $regexp, $functions, $fileContent)
{
$result = [];
$matches = preg_grep($regexp, explode("\n", $fileContent));
if (!empty($matches)) {
foreach ($matches as $line) {
$actualFunctions = [];
foreach ($functions as $function) {
if (false !== strpos($line, $function)) {
$actualFunctions[] = $function;
}
}
$result[array_search($line . "\n", $file) + 1] = $actualFunctions;
}
}
return $result;
}
/**
* Grep File.
*
* @param array $file
* @param string $regexp
* @return array
*/
public function grepFile(array $file, $regexp)
{
$result = [];
array_unshift($file, '');
$lines = preg_grep($regexp, $file);
foreach ($lines as $lineNumber => $line) {
if (preg_match_all($regexp, $line, $matches)) {
$result[$lineNumber] = $matches[1];
}
}
return $result;
}
/**
* Compose regular expression
*
* @param array $functions
* @return string
*/
private function composeRegexp(array $functions)
{
if (empty($functions)) {
return '';
}
return '/(?<!function |->|::)\b(' . join('|', $functions) . ')\s*\(/i';
}
}
@@ -0,0 +1,14 @@
<?xml version="1.0"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<ruleset name="Magento">
<description>Custom Magento coding standard.</description>
<rule ref="./../../../../../vendor/magento/magento-coding-standard/Magento2"/>
<rule ref="./../../../../../vendor/magento/magento-coding-standard/Magento2Framework"/>
<exclude-pattern>*\.min.js$</exclude-pattern>
<exclude-pattern>lib\/web\/(?!mage)</exclude-pattern>
</ruleset>
+39
View File
@@ -0,0 +1,39 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
use \Magento\Framework\App\Filesystem\DirectoryList;
//phpcs:ignore Magento2.Functions.DiscouragedFunction
$baseDir = realpath(__DIR__ . '/../../../../');
// phpcs:ignore Magento2.Security.IncludeFile.FoundIncludeFile
require $baseDir . '/app/autoload.php';
// phpcs:ignore Magento2.Security.IncludeFile.FoundIncludeFile
require $baseDir . '/vendor/squizlabs/php_codesniffer/autoload.php';
$testsBaseDir = $baseDir . '/dev/tests/static';
$autoloadWrapper = \Magento\Framework\Autoload\AutoloaderRegistry::getAutoloader();
$autoloadWrapper->addPsr4(
'Magento\\',
[
$testsBaseDir . '/testsuite/Magento/',
$testsBaseDir . '/framework/Magento/',
$testsBaseDir . '/framework/tests/unit/testsuite/Magento',
]
);
$autoloadWrapper->addPsr4(
'Magento\\TestFramework\\',
[
$testsBaseDir . '/framework/Magento/TestFramework/',
$testsBaseDir . '/../integration/framework/Magento/TestFramework/',
$testsBaseDir . '/../api-functional/framework/Magento/TestFramework/',
]
);
$autoloadWrapper->addPsr4('Magento\\CodeMessDetector\\', $testsBaseDir . '/framework/Magento/CodeMessDetector');
$generatedCode = DirectoryList::getDefaultConfig()[DirectoryList::GENERATED_CODE][DirectoryList::PATH];
$autoloadWrapper->addPsr4('Magento\\', $baseDir . '/' . $generatedCode . '/Magento/');
$setup = DirectoryList::getDefaultConfig()[DirectoryList::SETUP][DirectoryList::PATH];
$autoloadWrapper->addPsr4('Magento\\Setup\\', $baseDir . '/' . $setup . '/Magento/Setup/');
+81
View File
@@ -0,0 +1,81 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
// phpcs:disable
use Magento\Framework\App\Utility\Files;
use Magento\Framework\Component\ComponentRegistrar;
use Magento\Framework\Component\DirSearch;
use Magento\Framework\Filesystem\Directory\ReadFactory;
use Magento\Framework\Filesystem\DriverPool;
use Magento\Framework\View\Design\Theme\ThemePackageList;
use Magento\Framework\View\Design\Theme\ThemePackageFactory;
require __DIR__ . '/autoload.php';
error_reporting(E_ALL);
if (!defined('TESTS_TEMP_DIR')) {
define('TESTS_TEMP_DIR', dirname(__DIR__) . '/tmp');
}
// PHP 8 compatibility. Define constants that are not present in PHP < 8.0
if (!defined('PHP_VERSION_ID') || PHP_VERSION_ID < 80000) {
if (!defined('T_NAME_QUALIFIED')) {
define('T_NAME_QUALIFIED', 24001);
}
if (!defined('T_NAME_FULLY_QUALIFIED')) {
define('T_NAME_FULLY_QUALIFIED', 24002);
}
}
setCustomErrorHandler();
$componentRegistrar = new ComponentRegistrar();
$dirSearch = new DirSearch($componentRegistrar, new ReadFactory(new DriverPool()));
$themePackageList = new ThemePackageList($componentRegistrar, new ThemePackageFactory());
$serializer = new \Magento\Framework\Serialize\Serializer\Json();
$regexIteratorFactory = new Magento\Framework\App\Utility\RegexIteratorFactory();
\Magento\Framework\App\Utility\Files::setInstance(
new Files($componentRegistrar, $dirSearch, $themePackageList, $serializer, $regexIteratorFactory)
);
/**
* Set custom error handler
*/
function setCustomErrorHandler()
{
set_error_handler(
function ($errNo, $errStr, $errFile, $errLine) {
$errLevel = error_reporting();
if (($errLevel & $errNo) !== 0) {
$errorNames = [
E_ERROR => 'Error',
E_WARNING => 'Warning',
E_PARSE => 'Parse',
E_NOTICE => 'Notice',
E_CORE_ERROR => 'Core Error',
E_CORE_WARNING => 'Core Warning',
E_COMPILE_ERROR => 'Compile Error',
E_COMPILE_WARNING => 'Compile Warning',
E_USER_ERROR => 'User Error',
E_USER_WARNING => 'User Warning',
E_USER_NOTICE => 'User Notice',
E_STRICT => 'Strict',
E_RECOVERABLE_ERROR => 'Recoverable Error',
E_DEPRECATED => 'Deprecated',
E_USER_DEPRECATED => 'User Deprecated',
];
$errName = isset($errorNames[$errNo]) ? $errorNames[$errNo] : "";
throw new \PHPUnit\Framework\Exception(
sprintf("%s: %s in %s:%s.", $errName, $errStr, $errFile, $errLine),
$errNo
);
}
}
);
}
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
colors="true"
columns="max"
beStrictAboutTestsThatDoNotTestAnything="false"
bootstrap="../../bootstrap.php">
<testsuites>
<testsuite name="Magento Unit Tests for Static Code Analysis Framework">
<directory>testsuite/Magento/TestFramework</directory>
</testsuite>
</testsuites>
<php>
<ini name="date.timezone" value="America/Los_Angeles"/>
</php>
<extensions>
<extension class="Qameta\Allure\PHPUnit\AllureExtension">
<!-- Optional arguments block; omit it if you want to use default values -->
<arguments>
<!-- Path to config file (default is config/allure.config.php) -->
<string>../../../allure/allure.config.php</string>
</arguments>
</extension>
</extensions>
</phpunit>
@@ -0,0 +1,144 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types = 1);
namespace Magento\PhpStan\Formatters;
use PHPStan\Analyser\Error;
use PHPStan\Command\AnalysisResult;
use PHPStan\File\FuzzyRelativePathHelper;
use PHPStan\ShouldNotHappenException;
use PHPStan\Testing\ErrorFormatterTestCase;
/**
* Tests filter error formatter.
*/
class FilteredErrorFormatterTest extends ErrorFormatterTestCase
{
protected const DIRECTORY_PATH = __DIR__ . '/Fixtures';
/**
* Tests errors filtering.
*
* @param string $message
* @param int $exitCode
* @param array $fileErrors
* @param string $expected
* @throws ShouldNotHappenException
*
* @dataProvider dataFormatterOutputProvider
*/
public function testFormatErrors(
string $message,
int $exitCode,
array $fileErrors,
string $expected
): void {
$formatter = new FilteredErrorFormatter(
new FuzzyRelativePathHelper(self::DIRECTORY_PATH, [], '/'),
false,
false,
false,
true
);
$analysisResult = new AnalysisResult(
$fileErrors,
[],
[],
false,
false,
null
);
$this->assertSame(
$exitCode,
$formatter->formatErrors(
$analysisResult,
$this->getOutput()
),
sprintf('%s: response code do not match', $message)
);
$this->assertEquals(
$expected,
$this->getOutputContent(),
sprintf('%s: output do not match', $message)
);
}
/**
* @return array
*/
public function dataFormatterOutputProvider(): array
{
// phpcs:disable Generic.Files.LineLength.TooLong
$errorMessage = 'Method Magento\PhpStan\Formatters\Fixtures\ClassWithIgnoreAnnotation::testMethod() invoked with 2 parameters, 1 required.';
// phpcs:enable Generic.Files.LineLength.TooLong
return [
[
'No errors',
0,
[],
"\n [OK] No errors\n\n",
],
[
'All errors are suppressed by ignore annotations',
0,
[
new Error(
'Method level error',
self::DIRECTORY_PATH . '/ClassWithIgnoreAnnotation.php',
22
),
new Error(
$errorMessage,
self::DIRECTORY_PATH . '/ClassWithIgnoreAnnotation.php',
25
),
new Error(
$errorMessage,
self::DIRECTORY_PATH . '/ClassWithIgnoreAnnotation.php',
28
),
new Error(
$errorMessage,
self::DIRECTORY_PATH . '/ClassWithIgnoreAnnotation.php',
31
),
new Error(
$errorMessage,
self::DIRECTORY_PATH . '/ClassWithIgnoreAnnotation.php',
33
),
],
"\n [OK] No errors\n\n",
],
[
'Errors aren\'t suppressed by ignore annotations',
1,
[
new Error(
$errorMessage,
self::DIRECTORY_PATH . '/ClassWithoutIgnoreAnnotation.php',
21
),
],
// phpcs:disable Generic.Files.LineLength.TooLong
' ------ ---------------------------------------------------------------------------------------------------------------------------
Line ClassWithoutIgnoreAnnotation.php
------ ---------------------------------------------------------------------------------------------------------------------------
21 Method Magento\PhpStan\Formatters\Fixtures\ClassWithIgnoreAnnotation::testMethod() invoked with 2 parameters, 1 required.
------ ---------------------------------------------------------------------------------------------------------------------------
[ERROR] Found 1 error
',
// phpcs:enable Generic.Files.LineLength.TooLong
]
];
}
}
@@ -0,0 +1,44 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\PhpStan\Formatters\Fixtures;
/**
* Class ClassWithIgnoreAnnotation
*
* phpcs:ignoreFile
*/
class ClassWithIgnoreAnnotation
{
/**
* Test method.
* phpstan:ignore "Method level error"
*/
public function getProductList()
{
// phpstan:ignore "Method Magento\PhpStan\Formatters\Fixtures\ClassWithIgnoreAnnotation::testMethod() invoked with 2 parameters, 1 required."
$this->testMethod('test1', 'test2');
// phpstan:ignore "Method * invoked with 2 parameters, 1 required."
$this->testMethod('test1', 'test2');
// phpstan:ignore
$this->testMethod('test1', 'test2');
$this->testMethod('test1', 'test2'); // phpstan:ignore
}
/**
* @param string $arg1
* @return string
*/
private function testMethod(string $arg1)
{
return $arg1;
}
}
@@ -0,0 +1,33 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\PhpStan\Formatters\Fixtures;
/**
* Class ClassWithoutIgnoreAnnotation
* phpcs:ignoreFile
*/
class ClassWithoutIgnoreAnnotation
{
/**
* Test method.
*/
public function getProductList()
{
$this->testMethod('test1', 'test2');
}
/**
* @param string $arg1
* @return string
*/
private function testMethod(string $arg1)
{
return $arg1;
}
}
@@ -0,0 +1,44 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\PhpStan\Reflection\DataObject\Fixtures;
use Magento\Framework\DataObject;
class ClassWithCorrectUsageOfDataObject
{
/**
* @var DataObject
*/
private $container;
/**
* ClassWithCorrectUsageOfDataObject constructor.
*/
public function __construct()
{
$this->container = new DataObject();
}
/**
* Process with amazing stuff.
*
* @return void
*/
public function doStuff(): void
{
$a = $this->container->getSomething('1');
if ($this->container->hasSomething()) {
$this->container->setSomething2($a);
} else {
$this->container->unsetSomething();
$this->container->setSometing2($this->container->getStuff());
}
}
}
@@ -0,0 +1,49 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\PhpStan\Reflection\DataObject\Fixtures;
use Magento\Framework\DataObject;
class ClassWithIncorrectUsageOfDataObject
{
/**
* @var DataObject
*/
private $container;
/**
* ClassWithIncorrectUsageOfDataObject constructor.
*/
public function __construct()
{
$this->container = new DataObject();
}
/**
* Do Magic Stuff.
*
* 'get' - args: $index[optional] - string|int, return: mixed;
* 'set' - args: $value - mixed, return: self;
* 'uns' - args: -, return: self;
* 'has' - args: -, return: bool;
*/
public function doStuff(): void
{
$this->container->getBaz(
$this->container->unsFoo(
$this->container->setBaz()
)
);
$this->container->hasFoo(
$this->container->setStuff()
);
$this->container->getSomething($this->container->hasFoo());
}
}
@@ -0,0 +1,69 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\PhpStan\Reflection\DataObject\Fixtures;
use Magento\Framework\Session\SessionManager;
/**
* @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
*/
class ClassWithSessionManagerUsage
{
/**
* @var SessionManager
*/
private $container;
/**
* @param SessionManager $sessionManager
*/
public function __construct(SessionManager $sessionManager)
{
$this->container = $sessionManager;
}
/**
* Do Magic Stuff.
*
* 'get' - args: $index[optional] - string|int, return: mixed;
* 'set' - args: $value - mixed, return: self;
* 'uns' - args: -, return: self;
* 'has' - args: -, return: bool;
*/
public function doStuff(): void
{
$this->container->getBaz(
$this->container->unsFoo(
$this->container->setBaz()
)
);
$this->container->hasFoo(
$this->container->setStuff()
);
$this->container->getSomething($this->container->hasFoo());
}
/**
* Correct usage.
*
* @return void
*/
public function doCorrectStuff(): void
{
$a = $this->container->getSomething('1');
if ($this->container->hasSomething()) {
$this->container->setSomething2($a);
} else {
$this->container->unsetSomething();
$this->container->setSometing2($this->container->getStuff());
}
}
}
@@ -0,0 +1,4 @@
services:
-
class: Magento\PhpStan\Reflection\Php\DataObjectClassReflectionExtension
tags: {phpstan.broker.methodsClassReflectionExtension: {priority: 100}}
@@ -0,0 +1,109 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\PhpStan\Reflection\DataObject;
use PHPStan\Rules\Methods\CallMethodsRule;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
class MagicMethodsTest extends RuleTestCase
{
/**
* @inheritdoc
*/
protected function getRule(): Rule
{
return $this->getContainer()->getByType(CallMethodsRule::class);
}
/**
* Return directory path of fixture directory.
*
* @return string
*/
private static function getFixturesDir(): string
{
return __DIR__ . DIRECTORY_SEPARATOR . 'Fixtures' . DIRECTORY_SEPARATOR;
}
/**
* Add config files.
*
* @return string[]
*/
public static function getAdditionalConfigFiles(): array
{
return [self::getFixturesDir() . 'config.neon'];
}
/**
* Test Data Object Reflection extension.
*/
public function testExtension()
{
$this->analyse(
[
self::getFixturesDir() . 'ClassWithCorrectUsageOfDataObject.php',
self::getFixturesDir() . 'ClassWithIncorrectUsageOfDataObject.php',
self::getFixturesDir() . 'ClassWithSessionManagerUsage.php'
],
[
// phpcs:disable Generic.Files.LineLength.TooLong
[
'Parameter #1 $index of method Magento\Framework\DataObject::getBaz() expects int|string, Magento\Framework\DataObject given.',
38
],
[
'Method Magento\Framework\DataObject::unsFoo() invoked with 1 parameter, 0 required.',
39
],
[
'Method Magento\Framework\DataObject::setBaz() invoked with 0 parameters, 1 required.',
40
],
[
'Method Magento\Framework\DataObject::hasFoo() invoked with 1 parameter, 0 required.',
43
],
[
'Method Magento\Framework\DataObject::setStuff() invoked with 0 parameters, 1 required.',
44
],
[
'Parameter #1 $index of method Magento\Framework\DataObject::getSomething() expects int|string, bool given.',
47
],
[
'Parameter #1 $index of method Magento\Framework\Session\SessionManager::getBaz() expects int|string, Magento\Framework\Session\SessionManager given.',
41
],
[
'Method Magento\Framework\Session\SessionManager::unsFoo() invoked with 1 parameter, 0 required.',
42
],
[
'Method Magento\Framework\Session\SessionManager::setBaz() invoked with 0 parameters, 1 required.',
43
],
[
'Method Magento\Framework\Session\SessionManager::hasFoo() invoked with 1 parameter, 0 required.',
46
],
[
'Method Magento\Framework\Session\SessionManager::setStuff() invoked with 0 parameters, 1 required.',
47
],
[
'Parameter #1 $index of method Magento\Framework\Session\SessionManager::getSomething() expects int|string, bool given.',
50
]
// phpcs:enable Generic.Files.LineLength.TooLong
]
);
}
}
@@ -0,0 +1,22 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\CodingStandard\Tool;
class CodeMessDetectorTest extends \PHPUnit\Framework\TestCase
{
public function testCanRun()
{
$messDetector = new \Magento\TestFramework\CodingStandard\Tool\CodeMessDetector(
'some/ruleset/file.xml',
'some/report/file.xml'
);
$this->assertEquals(
class_exists(\PHPMD\TextUI\Command::class),
$messDetector->canRun()
);
}
}
@@ -0,0 +1,19 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\CodingStandard\Tool\CodeSniffer;
class WrapperTest extends \PHPUnit\Framework\TestCase
{
public function testSetValues()
{
if (!class_exists('\PHP_CodeSniffer\Runner')) {
$this->markTestSkipped('Code Sniffer is not installed');
}
$wrapper = new Wrapper();
$expected = ['some_key' => 'some_value'];
$wrapper->setSettings($expected);
}
}
@@ -0,0 +1,69 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\CodingStandard\Tool;
use PHP_CodeSniffer\Runner;
/**
* Unit test to check CodeSniffer tool.
*/
class CodeSnifferTest extends \PHPUnit\Framework\TestCase
{
/**
* @var \Magento\TestFramework\CodingStandard\Tool\CodeSniffer
*/
protected $_tool;
/**
* @var Runner
*/
protected $_wrapper;
/**
* Rule set directory
*/
const RULE_SET = 'some/ruleset/directory';
/**
* Report file path
*/
const REPORT_FILE = 'some/report/file.xml';
protected function setUp(): void
{
$this->_wrapper = $this->createMock(\Magento\TestFramework\CodingStandard\Tool\CodeSniffer\Wrapper::class);
$this->_tool = new \Magento\TestFramework\CodingStandard\Tool\CodeSniffer(
self::RULE_SET,
self::REPORT_FILE,
$this->_wrapper
);
}
public function testRun()
{
$whiteList = ['test' . rand(), 'test' . rand()];
$extensions = ['test' . rand(), 'test' . rand()];
$expectedCliEmulation = [
'files' => $whiteList,
'standards' => [self::RULE_SET],
'extensions' => $extensions,
'reports' => ['full' => self::REPORT_FILE],
'reportWidth' => 120
];
$this->_tool->setExtensions($extensions);
$this->_wrapper->expects($this->once())
->method('setSettings')
->with($this->equalTo($expectedCliEmulation));
$this->_wrapper->expects($this->once())
->method('runPHPCS');
$this->_tool->run($whiteList);
}
}
@@ -0,0 +1,62 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Dependency;
class DbRuleTest extends \PHPUnit\Framework\TestCase
{
/**
* @var DbRule
*/
protected $model;
protected function setUp(): void
{
$this->model = new DbRule(['some_table' => 'SomeModule']);
}
/**
* @param string $module
* @param string $file
* @param string $contents
* @param array $expected
* @dataProvider getDependencyInfoDataProvider
*/
public function testGetDependencyInfo($module, $file, $contents, array $expected)
{
$this->assertEquals($expected, $this->model->getDependencyInfo($module, 'php', $file, $contents));
}
public function getDependencyInfoDataProvider()
{
return [
['any', 'non-resource-file-path.php', 'any', []],
[
'any',
'/app/some/path/Setup/some-file.php',
'$install->getTableName("unknown_table")',
[['modules' => ['Unknown'], 'source' => 'unknown_table']]
],
[
'SomeModule',
'/app/some/path/Resource/Setup.php',
'$install->getTableName("some_table")',
[]
],
[
'any',
'/app/some/path/Resource/Setup.php',
'$install->getTableName("some_table")',
[
[
'modules' => ['SomeModule'],
'type' => \Magento\TestFramework\Dependency\RuleInterface::TYPE_HARD,
'source' => 'some_table',
]
]
]
];
}
}
@@ -0,0 +1,111 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Dependency;
use Magento\TestFramework\Dependency\VirtualType\VirtualTypeMapper;
class DiRuleTest extends \PHPUnit\Framework\TestCase
{
/**
* @param string $module
* @param string $contents
* @param array $expected
* @dataProvider getDependencyInfoDataProvider
*/
public function testGetDependencyInfo($module, $contents, array $expected)
{
$diRule = new DiRule(new VirtualTypeMapper([
'scope' => [
'someVirtualType1' => 'Magento\AnotherModule\Some\Class1',
'someVirtualType2' => 'Magento\AnotherModule\Some\Class2'
]
]));
$file = '/some/path/scope/di.xml';
static::assertEquals($expected, $diRule->getDependencyInfo($module, null, $file, $contents));
}
/**
* @return array
*/
public function getDependencyInfoDataProvider()
{
return [
'Di without dependencies' => [
'Magento\SomeModule',
$this->getFileContent('di_no_dependency.xml'),
[]
],
'Di only in module dependencies' => [
'Magento\SomeModule',
$this->getFileContent('di_in_module_dependency.xml'),
[]
],
'Di external dependencies' => [
'Magento\SomeModule',
$this->getFileContent('di_external_dependency.xml'),
[
[
'modules' => ['Magento\ExternalModule3'],
'type' => RuleInterface::TYPE_SOFT,
'source' => 'Magento\ExternalModule3\Some\Another\Class'
],
[
'modules' => ['Magento\ExternalModule5'],
'type' => RuleInterface::TYPE_SOFT,
'source' => 'Magento\ExternalModule5\Some\Another\Class'
],
[
'modules' => ['Magento\ExternalModule6'],
'type' => RuleInterface::TYPE_SOFT,
'source' => 'Magento\ExternalModule6\Some\Plugin\Class'
],
[
'modules' => ['Magento\ExternalModule1'],
'type' => RuleInterface::TYPE_HARD,
'source' => 'Magento\ExternalModule1\Some\Argument1'
],
[
'modules' => ['Magento\ExternalModule2'],
'type' => RuleInterface::TYPE_HARD,
'source' => 'Magento\ExternalModule2\Some\Argument2'
],
[
'modules' => ['Magento\ExternalModule4'],
'type' => RuleInterface::TYPE_HARD,
'source' => 'Magento\ExternalModule4\Some\Argument3'
]
]
],
'Di virtual dependencies' => [
'Magento\SomeModule',
$this->getFileContent('di_virtual_dependency.xml'),
[
[
'modules' => ['Magento\AnotherModule'],
'type' => RuleInterface::TYPE_HARD,
'source' => 'Magento\AnotherModule\Some\Class1',
],
[
'modules' => ['Magento\AnotherModule'],
'type' => RuleInterface::TYPE_HARD,
'source' => 'Magento\AnotherModule\Some\Class2',
]
]
]
];
}
/**
* Get content of di file
*
* @param string $fileName
* @return string
*/
private function getFileContent($fileName)
{
return file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . $fileName);
}
}
@@ -0,0 +1,223 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Dependency;
class LayoutRuleTest extends \PHPUnit\Framework\TestCase
{
public function testNonLayoutGetDependencyInfo()
{
$model = new LayoutRule([], [], []);
$content = 'any content';
$this->assertEmpty($model->getDependencyInfo('any', 'not layout', 'any', $content));
}
/**
* @param string $contents
* @param array $expected
* @dataProvider getDependencyInfoDataProvider
*/
public function testGetDependencyInfo($contents, array $expected)
{
$model = new LayoutRule([], [], []);
$this->assertEquals($expected, $model->getDependencyInfo('Magento\SomeModule', 'layout', 'any', $contents));
}
public function getDependencyInfoDataProvider()
{
return [
[
'<element module="Magento\AnotherModule"/>',
[
[
'modules' => ['Magento\AnotherModule'],
'type' => \Magento\Test\Integrity\DependencyTest::TYPE_SOFT,
'source' => '<element module="Magento\AnotherModule"/>',
]
],
],
['<element module="Magento\SomeModule"/>', []],
[
'<block class="Magento\AnotherModule\Several\Chunks"/>',
[
[
'modules' => ['Magento\AnotherModule'],
'type' => \Magento\Test\Integrity\DependencyTest::TYPE_HARD,
'source' => '<block class="Magento\AnotherModule\Several\Chunks"/>',
]
]
],
['<block class="Magento\SomeModule\Several\Chunks"/>', []],
[
'<any>
<extra></extra><block template="Magento_AnotherModule::template/path.phtml"/>
</any>',
[
[
'modules' => ['Magento\AnotherModule'],
'type' => \Magento\Test\Integrity\DependencyTest::TYPE_SOFT,
'source' => '<block template="Magento_AnotherModule::template/path.phtml"/>',
]
]
],
['<block template="Magento_SomeModule::template/path.phtml"/>', []],
[
'<block>Magento\AnotherModule\Several\Chunks</block>',
[
[
'modules' => ['Magento\AnotherModule'],
'type' => \Magento\Test\Integrity\DependencyTest::TYPE_SOFT,
'source' => '<block>Magento\AnotherModule\Several\Chunks</block>',
]
]
],
['<block>Magento\SomeModule\Several\Chunks</block>', []],
[
'<template>Magento_AnotherModule::template/path.phtml</template>',
[
[
'modules' => ['Magento\AnotherModule'],
'type' => \Magento\Test\Integrity\DependencyTest::TYPE_SOFT,
'source' => '<template>Magento_AnotherModule::template/path.phtml</template>',
]
]
],
['<template>Magento_SomeModule::template/path.phtml</template>', []],
[
'<file>Magento_AnotherModule::file/path.txt</file>',
[
[
'modules' => ['Magento\AnotherModule'],
'type' => \Magento\Test\Integrity\DependencyTest::TYPE_SOFT,
'source' => '<file>Magento_AnotherModule::file/path.txt</file>',
]
]
],
['<file>Magento_SomeModule::file/path.txt</file>', []],
[
'<any helper="Magento\AnotherModule\Several\Chunks::text"/>',
[
[
'modules' => ['Magento\AnotherModule'],
'type' => \Magento\Test\Integrity\DependencyTest::TYPE_SOFT,
'source' => '<any helper="Magento\AnotherModule\Several\Chunks::text"/>',
]
]
],
['<any helper="Magento\SomeModule\Several\Chunks::text"/>', []]
];
}
/**
* @param string $contents
* @param string $type
* @dataProvider layoutGetDependencyInfoDataProvider
*/
public function testUpdatesRouterGetDependencyInfo($contents, $type)
{
$model = new LayoutRule(['router_name' => ['Magento\RouterModule']], [], []);
$this->assertEquals([], $model->getDependencyInfo('Magento\RouterModule', 'layout', 'any', $contents));
$this->assertEquals(
[['modules' => ['Magento\RouterModule'], 'type' => $type, 'source' => 'router_name_action']],
$model->getDependencyInfo('Magento\AnotherModule', 'layout', 'any', $contents)
);
}
/**
* @param string $contents
* @param string $type
* @param bool $isHandle
* @dataProvider layoutGetDependencyInfoWithReferenceDataProvider
*/
public function testLayoutGetDependencyInfo($contents, $type, $isHandle)
{
// test one module
$data = [
'frontend' => ['any_handle_name' => ['Magento\AnyHandleModule' => 'Magento\AnyHandleModule']],
'default' => ['singlechunk' => ['Magento\DefaultHandleModule' => 'Magento\DefaultHandleModule']],
];
$model = $isHandle ? new LayoutRule([], [], $data) : new LayoutRule([], $data, []);
$this->assertEquals(
[],
$model->getDependencyInfo('Magento\AnyHandleModule', 'layout', 'path/frontend/file.txt', $contents)
);
$this->assertEquals(
[],
$model->getDependencyInfo('Magento\DefaultHandleModule', 'layout', 'any', $contents)
);
$this->assertEquals(
[['modules' => ['Magento\DefaultHandleModule'], 'type' => $type, 'source' => 'singlechunk']],
$model->getDependencyInfo('any', 'layout', 'any', $contents)
);
$this->assertEquals(
[['modules' => ['Magento\AnyHandleModule'], 'type' => $type, 'source' => 'any_handle_name']],
$model->getDependencyInfo('any', 'layout', 'path/frontend/file.txt', $contents)
);
// test several modules
$data = [
'frontend' => [
'any_handle_name' => [
'Magento\Theme' => 'Magento\Theme',
'Magento\HandleModule' => 'Magento\HandleModule',
],
],
];
$model = $isHandle ? new LayoutRule([], [], $data) : new LayoutRule([], $data, []);
$this->assertEquals(
[['modules' => ['Magento\Theme'], 'type' => $type, 'source' => 'any_handle_name']],
$model->getDependencyInfo('any', 'layout', 'path/frontend/file.txt', $contents)
);
$this->assertEquals(
[],
$model->getDependencyInfo('Magento\HandleModule', 'layout', 'path/frontend/file.txt', $contents)
);
}
public function layoutGetDependencyInfoDataProvider()
{
return [
[
$this->_getLayoutFileContent('layout_handle.xml'),
\Magento\Test\Integrity\DependencyTest::TYPE_SOFT,
true,
],
[
$this->_getLayoutFileContent('layout_handle_parent.xml'),
\Magento\Test\Integrity\DependencyTest::TYPE_HARD,
true
],
[
$this->_getLayoutFileContent('layout_handle_update.xml'),
\Magento\Test\Integrity\DependencyTest::TYPE_SOFT,
true
]
];
}
public function layoutGetDependencyInfoWithReferenceDataProvider()
{
return array_merge(
$this->layoutGetDependencyInfoDataProvider(),
[
[
$this->_getLayoutFileContent('layout_reference.xml'),
\Magento\Test\Integrity\DependencyTest::TYPE_SOFT,
false,
]
]
);
}
/**
* Get content of layout file
*
* @param string $fileName
* @return string
*/
protected function _getLayoutFileContent($fileName)
{
return file_get_contents(str_replace('\\', '/', realpath(__DIR__)) . '/_files/' . $fileName);
}
}
@@ -0,0 +1,545 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Dependency;
use Magento\Framework\Config\Reader\Filesystem;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper;
use Magento\TestFramework\Dependency\Reader\ClassScanner;
use Magento\TestFramework\Dependency\Route\RouteMapper;
/**
* Test for PhpRule dependency check
*/
class PhpRuleTest extends \PHPUnit\Framework\TestCase
{
/**
* @var PhpRule
*/
protected $model;
/**
* @var ObjectManagerHelper
*/
private $objectManagerHelper;
/**
* @var ClassScanner
*/
private $classScanner;
/**
* @var \PHPUnit\Framework\MockObject\MockObject | Filesystem
*/
private $webApiConfigReader;
private $pluginMap;
private $mapRoutes;
private $mapLayoutBlocks;
private $whitelist;
/**
* @inheritDoc
* @throws \Exception
*/
protected function setUp(): void
{
$this->mapRoutes = ['someModule' => ['Magento\SomeModule'], 'anotherModule' => ['Magento\OneModule']];
$this->mapLayoutBlocks = ['area' => ['block.name' => ['Magento\SomeModule' => 'Magento\SomeModule']]];
$this->pluginMap = [
'Magento\Module1\Plugin1' => 'Magento\Module1\Subject',
'Magento\Module1\Plugin2' => 'Magento\Module2\Subject',
];
$this->whitelist = [];
$this->objectManagerHelper = new ObjectManagerHelper($this);
$this->classScanner = $this->createMock(ClassScanner::class);
$this->webApiConfigReader = $this->makeWebApiConfigReaderMock();
$this->model = new PhpRule(
$this->mapRoutes,
$this->mapLayoutBlocks,
$this->webApiConfigReader,
$this->pluginMap,
$this->whitelist,
$this->classScanner
);
}
/**
* @throws \Exception
*/
public function testNonPhpGetDependencyInfo()
{
$content = 'any content';
$this->assertEmpty($this->model->getDependencyInfo('any', 'not php', 'any', $content));
}
/**
* @param string $class
* @param string $content
* @param int $expectedScans
* @param array $expected
*
* @throws \Exception
* @dataProvider getDependencyInfoDataProvider
*/
public function testGetDependencyInfo(string $class, string $content, int $expectedScans, array $expected): void
{
$file = $this->makeMockFilepath($class);
$module = $this->getModuleFromClass($class);
$this->classScanner->expects($this->exactly($expectedScans))
->method('getClassName')
->with($file)
->willReturn($class);
$this->assertEquals($expected, $this->model->getDependencyInfo($module, 'php', $file, $content));
}
/**
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
public function getDependencyInfoDataProvider()
{
return [
'Extend class in same module' => [
'Magento\SomeModule\SomeClass',
'something extends \Magento\SomeModule\Any\ClassName {',
0,
[]
],
'Extend class in different module' => [
'Magento\AnotherModule\SomeClass',
'something extends \Magento\SomeModule\Any\ClassName {',
1,
[
[
'modules' => ['Magento\SomeModule'],
'type' => RuleInterface::TYPE_HARD,
'source' => 'Magento\SomeModule\Any\ClassName',
]
]
],
'getViewFileUrl in same module' => [
'Magento\SomeModule\SomeClass',
'$this->getViewFileUrl("Magento_SomeModule::js/order-by-sku-failure.js")',
0,
[]
],
'getViewFileUrl in different module' => [
'Magento\AnotherModule\SomeClass',
'$this->getViewFileUrl("Magento_SomeModule::js/order-by-sku-failure.js")',
1,
[
[
'modules' => ['Magento\SomeModule'],
'type' => RuleInterface::TYPE_HARD,
'source' => 'Magento_SomeModule',
]
]
],
'Helper class from same module' => [
'Magento\SomeModule\SomeClass',
'$this->helper("Magento\SomeModule\Any\ClassName")',
0,
[]
],
'Helper class from another module' => [
'Magento\AnotherModule\SomeClass',
'$this->helper("Magento\SomeModule\Any\ClassName")',
1,
[
[
'modules' => ['Magento\SomeModule'],
'type' => RuleInterface::TYPE_HARD,
'source' => 'Magento\SomeModule\Any\ClassName',
]
]
],
'getBlock from same module' => [
'Magento\SomeModule\SomeClass',
'$this->getLayout()->getBlock(\'block.name\');',
0,
[]
],
'getBlock from another module' => [
'Magento\AnotherModule\SomeClass',
'$this->getLayout()->getBlock(\'block.name\');',
0,
[
[
'modules' => ['Magento\SomeModule'],
'type' => RuleInterface::TYPE_HARD,
'source' => 'getBlock(\'block.name\')',
]
]
],
'Plugin on class in same module' => [
'Magento\Module1\Plugin1',
', \Magento\Module1\Subject $variable',
0,
[]
],
'Plugin depends on arbitrary class in same module' => [
'Magento\Module1\Plugin1',
', \Magento\Module1\NotSubject $variable',
0,
[]
],
'Plugin on class in different module' => [
'Magento\Module1\Plugin2',
'Magento\Module2\Subject',
1,
[
[
'modules' => ['Magento\Module2'],
'type' => RuleInterface::TYPE_SOFT,
'source' => 'Magento\Module2\Subject',
]
],
],
'Plugin depends on arbitrary class in same module as subject' => [
'Magento\Module1\Plugin2',
'Magento\Module2\NotSubject',
1,
[
[
'modules' => ['Magento\Module2'],
'type' => RuleInterface::TYPE_SOFT,
'source' => 'Magento\Module2\NotSubject',
]
]
],
'Plugin depends on arbitrary class in arbitrary module' => [
'Magento\Module1\Plugin2',
'Magento\OtherModule\NotSubject',
1,
[
[
'modules' => ['Magento\OtherModule'],
'type' => RuleInterface::TYPE_HARD,
'source' => 'Magento\OtherModule\NotSubject',
]
]
],
];
}
/**
* @param string $class
* @param string $content
* @param array $expected
* @throws \Exception
* @dataProvider getDependencyInfoDataCaseGetUrlDataProvider
*/
public function testGetDependencyInfoCaseGetUrl(
string $class,
string $content,
array $expected
) {
$file = $this->makeMockFilepath($class);
$module = $this->getModuleFromClass($class);
$this->assertEquals($expected, $this->model->getDependencyInfo($module, 'php', $file, $content));
}
/**
* @return array
*/
public function getDependencyInfoDataCaseGetUrlDataProvider()
{
return [
'getUrl from same module' => [
'Magento\Cms\SomeClass',
'$this->getUrl("cms/index/index")',
[]
],
'getUrl from another module' => [
'Magento\SomeModule\SomeClass',
'$this->getUrl("cms/index/index")',
[
[
'modules' => ['Magento\Cms'],
'type' => RuleInterface::TYPE_HARD,
'source' => 'getUrl("cms/index/index"',
]
]
],
'getUrl from API of same module with parameter' => [
'Magento\Catalog\SomeClass',
'$this->getUrl("rest/V1/products/3")',
[]
],
'getUrl from API of same module without parameter' => [
'Magento\Catalog\SomeClass',
'$this->getUrl("rest/V1/products")',
[]
],
'getUrl from API of different module with parameter' => [
'Magento\Backend\SomeClass',
'$this->getUrl("rest/V1/products/43/options")',
[
[
'modules' => ['Magento\Catalog'],
'type' => RuleInterface::TYPE_HARD,
'source' => 'getUrl("rest/V1/products/43/options"'
]
],
],
'getUrl from routeid wildcard' => [
'Magento\Catalog\Controller\ControllerName\SomeClass',
'$this->getUrl("*/Invalid/*")',
[]
],
'getUrl from wildcard url within ignored Block class' => [
'Magento\Cms\Block\SomeClass',
'$this->getUrl("Catalog/*/View")',
[]
],
'getUrl from wildcard url within ignored Model file' => [
'Magento\Cms\Model\SomeClass',
'$this->getUrl("Catalog/*/View")',
[]
],
'getUrl with in admin controller for controllerName wildcard' => [
'Magento\Backend\Controller\Adminhtml\System\Store\DeleteStore',
'$this->getUrl("adminhtml/*/deleteStorePost")',
[]
],
];
}
/**
* @param string $template
* @param string $content
* @param array $expected
* @throws \Exception
* @dataProvider getDependencyInfoDataCaseGetTemplateUrlDataProvider
*/
public function testGetDependencyInfoCaseTemplateGetUrl(
string $template,
string $content,
array $expected
) {
$module = $this->getModuleFromClass($template);
$this->assertEquals($expected, $this->model->getDependencyInfo($module, 'php', $template, $content));
}
/**
* @return array[]
*/
public function getDependencyInfoDataCaseGetTemplateUrlDataProvider()
{
return [ 'getUrl from ignore template' => [
'app/code/Magento/Backend/view/adminhtml/templates/dashboard/totalbar/script.phtml',
'$getUrl("adminhtml/*/ajaxBlock")',
[]]];
}
/**
* @param string $class
* @param string $content
* @param array $expected
* @dataProvider processWildcardUrlDataProvider
*/
public function testProcessWildcardUrl(
string $class,
string $content,
array $expected
) {
$routeMapper = $this->createMock(RouteMapper::class);
$routeMapper->expects($this->once())
->method('getDependencyByRoutePath')
->with(
$this->equalTo($expected['route_id']),
$this->equalTo($expected['controller_name']),
$this->equalTo($expected['action_name'])
);
$phpRule = new PhpRule(
$this->mapRoutes,
$this->mapLayoutBlocks,
$this->webApiConfigReader,
$this->pluginMap,
$this->whitelist,
$this->classScanner,
$routeMapper
);
$file = $this->makeMockFilepath($class);
$module = $this->getModuleFromClass($class);
$phpRule->getDependencyInfo($module, 'php', $file, $content);
}
/**
* @return array[]
*/
public function processWildcardUrlDataProvider()
{
return [
'wildcard controller route' => [
'Magento\SomeModule\Controller\ControllerName\SomeClass',
'$this->getUrl("cms/*/index")',
[
'route_id' => 'cms',
'controller_name' => 'controllername',
'action_name' => 'index'
]
],
'adminhtml wildcard controller route' => [
'Magento\Backend\Controller\Adminhtml\System\Store\DeleteStore',
'$this->getUrl("adminhtml/*/deleteStorePost")',
[
'route_id' => 'adminhtml',
'controller_name' => 'system_store',
'action_name' => 'deletestorepost'
]
],
'index wildcard' => [
'Magento\Backend\Controller\System\Store\DeleteStore',
'$this->getUrl("routeid/controllername/*")',
[
'route_id' => 'routeid',
'controller_name' => 'controllername',
'action_name' => 'deletestore'
]
]
];
}
/**
* @param string $class
* @param string $content
* @param \Exception $expected
* @throws \Exception
* @dataProvider getDependencyInfoDataCaseGetUrlExceptionDataProvider
*/
public function testGetDependencyInfoCaseGetUrlException(
string $class,
string $content,
\Exception $expected
) {
$file = $this->makeMockFilepath($class);
$module = $this->getModuleFromClass($class);
$this->expectExceptionObject($expected);
$this->model->getDependencyInfo($module, 'php', $file, $content);
}
/**
* @return array
*/
public function getDependencyInfoDataCaseGetUrlExceptionDataProvider()
{
return [
'getUrl from same module' => [
'Magento\SomeModule\SomeClass',
'$this->getUrl("someModule")',
new LocalizedException(__('Invalid URL path: %1', 'somemodule/index/index')),
],
'getUrl from unknown wildcard path' => [
'Magento\Catalog\Controller\Product\View',
'$this->getUrl("Catalog/*/INVALID")',
new LocalizedException(__('Invalid URL path: %1', 'catalog/product/invalid')),
],
];
}
/**
* @param string $module
* @param string $content
* @param array $expected
* @dataProvider getDefaultModelDependencyDataProvider
* @throws \Exception
*/
public function testGetDefaultModelDependency($module, $content, array $expected)
{
$mapLayoutBlocks = [
'default' => [
'block.name' => [
'Magento\SomeModule' => 'Magento\SomeModule',
],
],
];
$this->model = new PhpRule([], $mapLayoutBlocks, $this->webApiConfigReader);
$this->assertEquals($expected, $this->model->getDependencyInfo($module, 'template', 'any', $content));
}
/**
* @return array
*/
public function getDefaultModelDependencyDataProvider()
{
return [
[
'Magento\AnotherModule',
'$this->getLayout()->getBlock(\'block.name\');',
[
[
'modules' => ['Magento\SomeModule'],
'type' => RuleInterface::TYPE_HARD,
'source' => 'getBlock(\'block.name\')',
]
],
]
];
}
/**
* Make some fake filepath to correspond to the class name
*
* @param string $class
* @return string
*/
private function makeMockFilepath($class)
{
return 'ClassRoot' . DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $class) . '.php';
}
/**
* Get the module name like Magento\Module out of a classname, assuming for test purpose that
* all modules are from "Magento" vendor
*
* @param string $class
* @return string
*/
private function getModuleFromClass(string $class): string
{
$moduleNameLength = strpos($class, '\\', strpos($class, '\\') + 1);
return substr($class, 0, $moduleNameLength);
}
/**
* Returns an example list of services that would be parsed via the configReader
*
* @return \PHPUnit\Framework\MockObject\MockObject | Filesystem
*/
private function makeWebApiConfigReaderMock()
{
$services = [ 'routes' => [
'/V1/products/:sku' => [
'GET' => ['service' => [
'class' => 'Magento\Catalog\Api\ProductRepositoryInterface',
'method' => 'get'
] ],
'PUT' => ['service' => [
'class' => 'Magento\Catalog\Api\ProductRepositoryInterface',
'method' => 'save'
] ],
],
'/V1/products/:sku/options' => ['GET' => ['service' => [
'class' => 'Magento\Catalog\Api\ProductCustomOptionRepositoryInterface',
'method' => 'getList'
] ] ],
'/V1/products' => ['GET' => ['service' => [
'class' => 'Magento\Catalog\Api\ProductCustomOptionRepositoryInterface',
'method' => 'getList'
] ] ]
] ];
return $this->createConfiguredMock(Filesystem::class, [ 'read' => $services ]);
}
}
@@ -0,0 +1,185 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Dependency\VirtualType;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
class VirtualTypeMapperTest extends \PHPUnit\Framework\TestCase
{
/**
* @var VirtualTypeMapper
*/
private $mapper;
protected function setUp(): void
{
$managerHelper = new ObjectManager($this);
$this->mapper = $managerHelper->getObject(VirtualTypeMapper::class, [
'map' => [
'global' => [
'virtualType1' => 'Magento\SomeModule\Some\Class1',
'virtualType2' => 'Magento\SomeModule\Some\Class2',
'virtualType3' => 'Magento\SomeModule\Some\Class3',
],
'adminhtml' => [
'virtualType1' => 'Magento\SomeModule\Some\Class4',
'virtualType4' => 'Magento\SomeModule\Some\Class5',
]
]
]);
}
public function testGetScopeFromFile()
{
$file = '/path/to/file/scope/filename.ext';
static::assertEquals('scope', $this->mapper->getScopeFromFile($file));
}
/**
* @param string $scope
* @param string $type
* @param string $expected
* @dataProvider getTypeDataProvider
*/
public function testGetType($scope, $type, $expected)
{
static::assertEquals($expected, $this->mapper->getType($type, $scope));
}
/**
* @return array
*/
public function getTypeDataProvider()
{
return [
[
'global',
'virtualType1',
'Magento\SomeModule\Some\Class1'
],
[
'global',
'virtualType2',
'Magento\SomeModule\Some\Class2'
],
[
'adminhtml',
'virtualType3',
'Magento\SomeModule\Some\Class3'
],
[
'adminhtml',
'virtualType1',
'Magento\SomeModule\Some\Class4'
],
[
'adminhtml',
'virtualType4',
'Magento\SomeModule\Some\Class5'
]
];
}
/**
* @param array $diFilesPath
* @param array $expectedVirtualTypesDependencies
* @dataProvider loadConfigurationDataProvider
*/
public function testLoad(array $diFilesPath, array $expectedVirtualTypesDependencies)
{
$mapper = new VirtualTypeMapper();
self::assertArrayEqualsRecursive(
$expectedVirtualTypesDependencies,
$mapper->loadMap($diFilesPath)
);
}
/**
* @param array $diFilesPath
* @param array $expectedVirtualTypesDependencies
* @dataProvider loadConfigurationDataProvider
*/
public function testGetTypeComplex(array $diFilesPath, array $expectedVirtualTypesDependencies)
{
$mapper = new VirtualTypeMapper();
$mapper->loadMap($diFilesPath);
// getType will return input value in case there no virtualType with the same name was found
$expectedVirtualTypesDependencies['global']['ConcreteClass'] = 'ConcreteClass';
foreach ($expectedVirtualTypesDependencies as $scope => $deps) {
foreach ($deps as $virtualType => $baseType) {
self::assertEquals($baseType, $mapper->getType($virtualType, $scope));
}
}
}
/**
* @return array
*/
public function loadConfigurationDataProvider()
{
return [
// collects two virtual types, defined in module configuration on global area level
[
'diFilesPath' => [$this->getFilePath('etc/di.xml')],
'expectedVirtualTypesDependencies' => [
'global' => [
'Magento\Internal\Some\Class' => 'ExternalVirtualType',
'MyVirtualType' => 'Magento\Internal\Some\Class'
]
]
],
/**
* expectation is the same for global area,
* but extended for adminhtml as it contains own virtual types definitions
*/
[
'diFilesPath' => [$this->getFilePath('etc/di.xml'), $this->getFilePath('etc/adminhtml/di.xml')],
'expectedVirtualTypesDependencies' => [
'global' => [
'Magento\Internal\Some\Class' => 'ExternalVirtualType',
'MyVirtualType' => 'Magento\Internal\Some\Class'
],
'adminhtml' => [
'MyVirtualType2' => 'ExternalVirtualType'
]
]
]
];
}
/**
* @param array $expectation
* @param array $input
*/
private static function assertArrayEqualsRecursive(
array $expectation,
array $input
) {
static::assertEquals(count($expectation), count($input));
foreach ($expectation as $ek => $ev) {
self::assertArrayHasKey($ek, $input);
if (is_array($ev)) {
self::assertArrayEqualsRecursive($ev, $input[$ek]);
} else {
self::assertEquals($ev, $input[$ek]);
}
}
}
/**
* @param string $fileName
* @return string
*/
private function getFilePath($fileName)
{
return __DIR__
. DIRECTORY_SEPARATOR
. '_files' . DIRECTORY_SEPARATOR . $fileName;
}
}
@@ -0,0 +1,29 @@
<?xml version="1.0"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="ExternalVirtualType">
<arguments>
<argument name="argument2" xsi:type="object">Magento\External\Virtual</argument>
</arguments>
</type>
<type name="Magento\Internal\Some\Class">
<arguments>
<argument name="argument1" xsi:type="object">Magento\ExternalModule4\Some\Argument4</argument>
</arguments>
</type>
<type name="MyVirtualType">
<arguments>
<argument name="argument1" xsi:type="object">Magento\External2\Class2</argument>
</arguments>
</type>
<virtualType name="MyVirtualType2" type="ExternalVirtualType">
<arguments>
<argument name="argument1" xsi:type="object">Magento\External2\Class2</argument>
</arguments>
</virtualType>
</config>
@@ -0,0 +1,27 @@
<?xml version="1.0"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<virtualType name="Magento\Internal\Some\Class" type="ExternalVirtualType">
<arguments>
<argument name="argument1" xsi:type="object">Magento\External\Class</argument>
<argument name="argument2" xsi:type="object">Magento\External\Virtual</argument>
</arguments>
</virtualType>
<type name="Magento\External\Class">
<arguments>
<argument name="argument1" xsi:type="object">Magento\ExternalModule4\Some\Argument3</argument>
<argument name="argument2" xsi:type="object">ExternalVirtualType</argument>
</arguments>
</type>
<virtualType name="MyVirtualType" type="Magento\Internal\Some\Class">
<arguments>
<argument name="argument1" xsi:type="object">Magento\External2\Class</argument>
<argument name="argument2" xsi:type="object">Magento\External2\Virtual</argument>
</arguments>
</virtualType>
</config>
@@ -0,0 +1,23 @@
<?xml version="1.0"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\SomeModule\Some\Class">
<arguments>
<argument name="argument1" xsi:type="object">Magento\ExternalModule1\Some\Argument1</argument>
<argument name="argument2" xsi:type="object">Magento\ExternalModule2\Some\Argument2</argument>
</arguments>
</type>
<type name="Magento\ExternalModule3\Some\Another\Class">
<arguments>
<argument name="argument1" xsi:type="object">Magento\ExternalModule4\Some\Argument3</argument>
</arguments>
</type>
<type name="Magento\ExternalModule5\Some\Another\Class">
<plugin name="plugin" type="Magento\ExternalModule6\Some\Plugin\Class" />
</type>
</config>
@@ -0,0 +1,20 @@
<?xml version="1.0"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\SomeModule\Some\Class">
<arguments>
<argument name="argument1" xsi:type="string">Magento\SomeModule\Some\Argument1</argument>
<argument name="argument2" xsi:type="string">Magento\SomeModule\Some\Argument2</argument>
</arguments>
</type>
<type name="Magento\SomeModule\Some\Another\Class">
<arguments>
<argument name="argument1" xsi:type="string">Magento\SomeModule\Some\Argument3</argument>
</arguments>
</type>
</config>
@@ -0,0 +1,9 @@
<?xml version="1.0"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
</config>
@@ -0,0 +1,16 @@
<?xml version="1.0"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\SomeModule\Some\Class">
<arguments>
<argument name="argument1" xsi:type="object">someVirtualType1</argument>
<argument name="argument2" xsi:type="object">someVirtualType2</argument>
<argument name="argument3" xsi:type="object">someNotExistsType</argument>
</arguments>
</type>
</config>
@@ -0,0 +1,12 @@
<?xml version="1.0"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_layout.xsd">
<any_handle_name />
<singlechunk />
<router_name_action />
</layout>
@@ -0,0 +1,12 @@
<?xml version="1.0"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_layout.xsd">
<any_node1 parent="any_handle_name" />
<any_node2 parent="singlechunk" />
<any_node3 parent="router_name_action" />
</layout>
@@ -0,0 +1,15 @@
<?xml version="1.0"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_layout.xsd">
<update handle="any_handle_name" />
<any_node>
<update handle="singlechunk">
<update handle="router_name_action" />
</update>
</any_node>
</layout>
@@ -0,0 +1,13 @@
<?xml version="1.0"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_layout.xsd">
<reference name="any_handle_name" />
<any_node>
<reference name="singlechunk" />
</any_node>
</layout>
@@ -0,0 +1,92 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Inspection;
use Magento\Framework\Component\ComponentRegistrar;
class WordsFinderTest extends \PHPUnit\Framework\TestCase
{
/**
* @param string $configFile
* @param string $baseDir
* @dataProvider constructorExceptionDataProvider
*/
public function testConstructorException($configFile, $baseDir)
{
$this->expectException(\Magento\TestFramework\Inspection\Exception::class);
new \Magento\TestFramework\Inspection\WordsFinder($configFile, $baseDir, new ComponentRegistrar());
}
public function constructorExceptionDataProvider()
{
$fixturePath = __DIR__ . '/_files/';
return [
'non-existing config file' => [$fixturePath . 'non-existing.xml', $fixturePath],
'non-existing base dir' => [$fixturePath . 'config.xml', $fixturePath . 'non-existing-dir'],
'broken config' => [$fixturePath . 'broken_config.xml', $fixturePath],
'empty words config' => [$fixturePath . 'empty_words_config.xml', $fixturePath],
'empty whitelisted path' => [$fixturePath . 'empty_whitelisted_path.xml', $fixturePath]
];
}
/**
* @param string|array $configFiles
* @param string $file
* @param array $expected
* @dataProvider findWordsDataProvider
*/
public function testFindWords($configFiles, $file, $expected)
{
$wordsFinder = new \Magento\TestFramework\Inspection\WordsFinder(
$configFiles,
__DIR__ . '/_files/words_finder',
new ComponentRegistrar()
);
$actual = $wordsFinder->findWords($file);
$this->assertEquals($expected, $actual);
}
/**
* @return array
*/
public function findWordsDataProvider()
{
$mainConfig = __DIR__ . '/_files/config.xml';
$additionalConfig = __DIR__ . '/_files/config_additional.xml';
$basePath = __DIR__ . '/_files/words_finder/';
return [
'usual file' => [$mainConfig, $basePath . 'buffy.php', ['demon', 'vampire']],
'whitelisted file' => [$mainConfig, $basePath . 'twilight/eclipse.php', []],
'partially whitelisted file' => [$mainConfig, $basePath . 'twilight/newmoon.php', ['demon']],
'filename with bad word' => [
$mainConfig,
$basePath . 'interview_with_the_vampire.php',
['vampire'],
],
'binary file, having name with bad word' => [
$mainConfig,
$basePath . 'interview_with_the_vampire.zip',
['vampire'],
],
'words in multiple configs' => [
[$mainConfig, $additionalConfig],
$basePath . 'buffy.php',
['demon', 'vampire', 'darkness'],
],
'whitelisted paths in multiple configs' => [
[$mainConfig, $additionalConfig],
$basePath . 'twilight/newmoon.php',
['demon'],
],
'config must be whitelisted automatically' => [
$basePath . 'self_tested_config.xml',
$basePath . 'self_tested_config.xml',
[],
]
];
}
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<config>
<words>
<word>alien
</words>
</config>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<config>
<words>
<word>demon</word>
<word>vampire</word>
</words>
<whitelist>
<item>
<path>twilight/eclipse.php</path>
</item>
<item>
<word>vampire</word>
<path>twilight/newmoon.php</path>
</item>
</whitelist>
</config>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<config>
<words>
<word>darkness</word>
<word>paranormal</word>
</words>
<whitelist>
<item>
<word>paranormal</word>
<path>twilight/newmoon.php</path>
</item>
</whitelist>
</config>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<config>
<words>
<word>any</word>
</words>
<whitelist>
<item></item>
</whitelist>
</config>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<config></config>
@@ -0,0 +1,7 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
?>
<?= 'At the young age of 15, Buffy Summers was chosen to hunt vampires, demons, and the forces of darkness.';
@@ -0,0 +1 @@
At the young age of 15, Buffy Summers was chosen to hunt vampires, demons, and the forces of darkness.
@@ -0,0 +1,7 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
?>
<?= 'A man tells his epic life story: love, betrayal, loneliness, and hunger.';
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<config>
<words>
<word>test</word>
</words>
</config>
@@ -0,0 +1,8 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
?>
<?= 'As a string of mysterious killings grips Seattle, Bella, whose high school graduation is fast approaching,
is forced to choose between her love for vampire Edward and her friendship with demon Jacob.';
@@ -0,0 +1,8 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
?>
<?= 'Vampire Edward leaves Bella after an attack that nearly claimed her life, and in her depression she falls into
yet another paranormal relationship - this time with demon Jacob Black.';
@@ -0,0 +1,41 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\TestFramework\Integrity\Library;
use Laminas\Code\Reflection\ClassReflection;
use PHPUnit\Framework\TestCase;
use ReflectionException;
require_once __DIR__ . '/_files/DummyInjectableClass.php';
/**
* Test for Magento\TestFramework\Integrity\Library\Injectable
*/
class InjectableTest extends TestCase
{
/**
* Covered getDependencies
*
* @return void
* @throws ReflectionException
*/
public function testGetDependencies(): void
{
$injectable = new Injectable();
$classReflection = new ClassReflection(DummyInjectableClass::class);
$actualResult = $injectable->getDependencies($classReflection);
$expectedResult = [
'Magento\Framework\DataObject',
'TestNamespace\Some\SomeTestClass',
'TestNamespace\Other\Test',
];
$this->assertEquals($expectedResult, $actualResult);
}
}
@@ -0,0 +1,49 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Integrity\Library\PhpParser;
/**
*/
class ParserFactoryTest extends \PHPUnit\Framework\TestCase
{
/**
* @var \Magento\TestFramework\Integrity\Library\PhpParser\Tokens
*/
protected $tokens;
/**
* @inheritdoc
*/
protected function setUp(): void
{
$this->tokens = $this->getMockBuilder(
\Magento\TestFramework\Integrity\Library\PhpParser\Tokens::class
)->disableOriginalConstructor()->getMock();
}
/**
* Covered createParsers method
*
* @test
*/
public function testCreateParsers()
{
$parseFactory = new ParserFactory();
$parseFactory->createParsers($this->tokens);
$this->assertInstanceOf(
\Magento\TestFramework\Integrity\Library\PhpParser\Uses::class,
$parseFactory->getUses()
);
$this->assertInstanceOf(
\Magento\TestFramework\Integrity\Library\PhpParser\StaticCalls::class,
$parseFactory->getStaticCalls()
);
$this->assertInstanceOf(
\Magento\TestFramework\Integrity\Library\PhpParser\Throws::class,
$parseFactory->getThrows()
);
}
}
@@ -0,0 +1,100 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\TestFramework\Integrity\Library\PhpParser;
use PHPUnit\Framework\TestCase;
/**
* Check static calls parsing
*/
class StaticCallsTest extends TestCase
{
/**
* @var StaticCalls
*/
protected $staticCalls;
/**
* @var \Magento\TestFramework\Integrity\Library\PhpParser\Tokens|\PHPUnit\Framework\MockObject\MockObject
*/
protected $tokens;
/**
* @inheritdoc
*/
protected function setUp(): void
{
$this->tokens = $this->getMockBuilder(
\Magento\TestFramework\Integrity\Library\PhpParser\Tokens::class
)->disableOriginalConstructor()->getMock();
}
/**
* Test get static call dependencies
*
* @dataProvider tokensDataProvider
*/
public function testGetDependencies(array $tokens)
{
$this->tokens->method('getPreviousToken')
->willReturnCallback(
function ($k) use ($tokens) {
return $tokens[$k - 1];
}
);
$this->tokens->method('getTokenCodeByKey')
->willReturnCallback(
function ($k) use ($tokens) {
return $tokens[$k][0];
}
);
$this->tokens->method('getTokenValueByKey')
->willReturnCallback(
function ($k) use ($tokens) {
return $tokens[$k][1];
}
);
$staticCalls = new StaticCalls($this->tokens);
foreach ($tokens as $k => $token) {
$staticCalls->parse($token, $k);
}
$uses = $this->getMockBuilder(
\Magento\TestFramework\Integrity\Library\PhpParser\Uses::class
)->disableOriginalConstructor()->getMock();
$this->assertEquals(['\Object'], $staticCalls->getDependencies($uses));
}
/**
* different tokens data for php 7 and php 8
*
* @return array
*/
public function tokensDataProvider(): array
{
return [
'PHP 7' => [
[
0 => [T_WHITESPACE, ' '],
1 => [T_NS_SEPARATOR, '\\'],
2 => [T_STRING, 'Object'],
3 => [T_PAAMAYIM_NEKUDOTAYIM, '::'],
]
],
'PHP 8' => [
[
0 => [T_WHITESPACE, ' '],
1 => [T_NAME_FULLY_QUALIFIED, '\\Object'],
2 => [T_PAAMAYIM_NEKUDOTAYIM, '::'],
]
]
];
}
}

Some files were not shown because too many files have changed in this diff Show More