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>