Skip to content

Commit 42aa373

Browse files
authored
Merge pull request #4 from Space48/plugin-detector
Plugin detector (https://space48.atlassian.net/browse/S48-943)
2 parents 8b99694 + 99c85b6 commit 42aa373

File tree

6 files changed

+234
-0
lines changed

6 files changed

+234
-0
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"psr-4": {
3232
"Space48\\CodeQuality\\": "src",
3333
"Space48\\CodeQuality\\RuleSets\\": "rulesets",
34+
"Space48\\CodeQuality\\Utils\\": "utils",
3435
"PHP_CodeSniffer\\": "../../squizlabs/php_codesniffer/src",
3536
"Magento\\": "../magento-coding-standard/Magento",
3637
"EcgM2\\": "../../magento-ecg/coding-standard/EcgM2",
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace Space48\CodeQuality\RuleSets\PhpMd;
4+
5+
use Space48\CodeQuality\Utils\MagentoClassType;
6+
use PHPMD\AbstractNode;
7+
use PHPMD\Node\ASTNode;
8+
use PHPMD\Node\MethodNode;
9+
10+
/**
11+
* This rule collects all formal parameters of a given function or method that
12+
* are not used in a statement of the artifact's body.
13+
*/
14+
class UnusedFormalParameter extends \PHPMD\Rule\UnusedFormalParameter
15+
{
16+
17+
/**
18+
* Ignore this rule for Plugin and Observer methods.
19+
*
20+
* @param \PHPMD\AbstractNode $node
21+
* @return void
22+
*/
23+
public function apply(AbstractNode $node)
24+
{
25+
if (MagentoClassType::isPluginMethod($node) && MagentoClassType::isPlugin($node)) {
26+
return;
27+
}
28+
29+
if (MagentoClassType::isObserverMethod($node) && MagentoClassType::isObserver($node)) {
30+
return;
31+
}
32+
33+
parent::apply($node);
34+
}
35+
}

rulesets/PhpMd/extra.xml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,26 @@ class Foo {
4545

4646
</rule>
4747

48+
<rule name="UnusedFormalParameter"
49+
since="0.2"
50+
message="Avoid unused parameters such as '{0}'."
51+
class="Space48\CodeQuality\RuleSets\PhpMd\UnusedFormalParameter"
52+
externalInfoUrl="https://phpmd.org/rules/unusedcode.html#unusedformalparameter">
53+
<description>
54+
Avoid passing parameters to methods or constructors and then not using those parameters.
55+
</description>
56+
<priority>3</priority>
57+
<example>
58+
<![CDATA[
59+
class Foo
60+
{
61+
private function bar($howdy)
62+
{
63+
// $howdy is not used
64+
}
65+
}
66+
]]>
67+
</example>
68+
</rule>
69+
4870
</ruleset>

rulesets/phpmd.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
<!-- overwritten in 'extra.xml' -->
1111
<exclude name="ExcessiveParameterList" />
12+
<exclude name="UnusedFormalParameter" />
1213
</rule>
1314

1415
<rule ref="dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml/CyclomaticComplexity">

utils/MagentoClassType.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
namespace Space48\CodeQuality\Utils;
4+
5+
use PHPMD\AbstractNode;
6+
7+
class MagentoClassType
8+
{
9+
/** @var MagentoClassTypeResolver */
10+
private static $resolver;
11+
12+
/**
13+
* @param AbstractNode $node
14+
* @return bool
15+
*/
16+
public static function isPlugin(AbstractNode $node): bool
17+
{
18+
$type = self::getResolver()->resolveType($node);
19+
20+
return $type == 'plugin';
21+
}
22+
23+
/**
24+
* @param AbstractNode $node
25+
* @return bool
26+
*/
27+
public static function isObserver(AbstractNode $node): bool
28+
{
29+
$type = self::getResolver()->resolveType($node);
30+
31+
return $type == 'observer';
32+
}
33+
34+
/**
35+
* @param AbstractNode $node
36+
* @return bool
37+
*/
38+
public static function isPluginMethod(AbstractNode $node): bool
39+
{
40+
return (bool)preg_match('/^(before|after|around)[A-Z].*/', $node->getName());
41+
}
42+
43+
/**
44+
* @param AbstractNode $node
45+
* @return bool
46+
*/
47+
public function isObserverMethod(AbstractNode $node): bool
48+
{
49+
return $node->getName() == 'execute';
50+
}
51+
52+
/**
53+
* @return MagentoClassTypeResolver
54+
*/
55+
private static function getResolver(): MagentoClassTypeResolver
56+
{
57+
if (!self::$resolver) {
58+
self::$resolver = new MagentoClassTypeResolver();
59+
}
60+
61+
return self::$resolver;
62+
}
63+
}

utils/MagentoClassTypeResolver.php

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
namespace Space48\CodeQuality\Utils;
4+
5+
use PHPMD\AbstractNode;
6+
7+
class MagentoClassTypeResolver
8+
{
9+
private $types = [];
10+
11+
private $typeConfig = [
12+
'plugin' => [
13+
'xmlNode' => 'plugin',
14+
'xmlAttribute' => 'type',
15+
'configFile' => 'di.xml'
16+
],
17+
'observer' => [
18+
'xmlNode' => 'observer',
19+
'xmlAttribute' => 'instance',
20+
'configFile' => 'events.xml'
21+
]
22+
];
23+
24+
/**
25+
* Returns type of the Magento class - 'plugin', 'observer', etc
26+
*
27+
* @param AbstractNode $node
28+
* @return false|string
29+
*/
30+
public function resolveType(AbstractNode $node)
31+
{
32+
if (!$this->types[$this->getClassName($node)]) {
33+
foreach ($this->typeConfig as $typeName => $type) {
34+
foreach (self::locateFiles($node, $type['configFile']) as $filePath) {
35+
$domDocument = new \DomDocument('1.0', 'UTF-8');
36+
$domDocument->loadXML(\file_get_contents($filePath));
37+
$domXPath = new \DOMXPath($domDocument);
38+
39+
/** @var \DOMElement $nodeMatches [] */
40+
$nodeMatches = $domXPath->query(
41+
\sprintf(
42+
'//%s[@%s="%s"]',
43+
$type['xmlNode'],
44+
$type['xmlAttribute'],
45+
$this->getClassName($node)
46+
)
47+
);
48+
49+
if ($nodeMatches->length) {
50+
$this->types[$this->getClassName($node)] = $typeName;
51+
break 2;
52+
}
53+
}
54+
}
55+
}
56+
57+
return $this->types[$this->getClassName($node)] ?? false;
58+
}
59+
60+
/**
61+
* @param AbstractNode $node
62+
* @return string
63+
*/
64+
private function getClassName(AbstractNode $node): string
65+
{
66+
return $node->getNamespaceName() . '\\' . $node->getParentName();
67+
}
68+
69+
/**
70+
* @param AbstractNode $node
71+
* @param string $filename
72+
* @return array
73+
*/
74+
private function locateFiles(AbstractNode $node, string $filename): array
75+
{
76+
$namespace = explode('\\', $node->getNamespaceName());
77+
// [2] will be the name of the folder just after Module folder in standard Magento module structure
78+
if (empty($namespace[2])) {
79+
return [];
80+
}
81+
82+
$path = strstr($node->getFileName(), $namespace[2], true) . 'etc';
83+
84+
return array_filter($this->searchFiles($path, $filename));
85+
}
86+
87+
/**
88+
* @param string $path
89+
* @param string $fileName
90+
* @return array
91+
*/
92+
private function searchFiles(string $path, string $fileName): array
93+
{
94+
$configFiles = [];
95+
96+
foreach (scandir($path) as $directory) {
97+
if ($directory == '.' || $directory == '..') {
98+
continue;
99+
}
100+
101+
$filePath = $path . DIRECTORY_SEPARATOR . $directory;
102+
if ($directory == $fileName) {
103+
$configFiles[] = $filePath;
104+
} elseif (is_dir($filePath)) {
105+
$configFiles = array_merge($configFiles, $this->searchFiles($filePath, $fileName));
106+
}
107+
}
108+
109+
return $configFiles;
110+
}
111+
112+
}

0 commit comments

Comments
 (0)