Skip to content

Commit eed9414

Browse files
author
Robert Egginton
committed
New rule to warn if proxies or interceptors explicitly requested in constructors
1 parent 2e5e855 commit eed9414

File tree

5 files changed

+427
-0
lines changed

5 files changed

+427
-0
lines changed
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
<?php
2+
/**
3+
* Copyright © Magento. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace Magento\Sniffs\Classes;
8+
9+
use PHP_CodeSniffer\Files\File;
10+
use PHP_CodeSniffer\Sniffs\Sniff;
11+
12+
/**
13+
* Detects explicit request of proxies and interceptors in constructors
14+
*
15+
* Search is in variable names and namespaces, including indirect namespaces from use statements
16+
*/
17+
class ConstructorProxyInterceptorSniff implements Sniff
18+
{
19+
const CONSTRUCT_METHOD_NAME = '__construct';
20+
21+
/**
22+
* String representation of warning.
23+
*
24+
* @var string
25+
*/
26+
protected $warningMessage = 'Proxies and interceptors MUST never be explicitly requested in constructors.';
27+
28+
/**
29+
* Warning violation code.
30+
*
31+
* @var string
32+
*/
33+
protected $warningCode = 'ConstructorProxyInterceptor';
34+
35+
/**
36+
* @var array Aliases of proxies or plugins from use statements
37+
*/
38+
private $aliases = [];
39+
40+
/**
41+
* @var array Terms to search for in variables and namespaces
42+
*/
43+
private $warnNames = [
44+
'proxy',
45+
'plugin'
46+
];
47+
48+
/**
49+
* @inheritDoc
50+
*/
51+
public function register()
52+
{
53+
return [
54+
T_USE,
55+
T_FUNCTION
56+
];
57+
}
58+
59+
/**
60+
* @inheritDoc
61+
*/
62+
public function process(File $phpcsFile, $stackPtr)
63+
{
64+
// Match use statements and constructor (latter matches T_FUNCTION to find constructors
65+
$tokens = $phpcsFile->getTokens();
66+
if ($tokens[$stackPtr]['code'] == T_USE) {
67+
$this->processUse($phpcsFile, $stackPtr, $tokens);
68+
} elseif ($tokens[$stackPtr]['code'] == T_FUNCTION) {
69+
$this->processFunction($phpcsFile, $stackPtr, $tokens);
70+
}
71+
}
72+
73+
/**
74+
* Store plugin/proxy class names for use in matching constructor
75+
*
76+
* @param File $phpcsFile
77+
* @param int $stackPtr
78+
* @param array $tokens
79+
*/
80+
private function processUse(
81+
File $phpcsFile,
82+
$stackPtr,
83+
$tokens
84+
) {
85+
// Find end of use statement and position of AS alias if exists
86+
$endPos = $phpcsFile->findNext(T_SEMICOLON, $stackPtr);
87+
$asPos = $phpcsFile->findNext(T_AS, $stackPtr, $endPos);
88+
// Find whether this use statement includes any of the warning words
89+
$includesWarnWord =
90+
$this->includesWarnWordsInSTRINGs(
91+
$phpcsFile,
92+
$stackPtr,
93+
min($asPos, $endPos),
94+
$tokens,
95+
$lastWord
96+
);
97+
if (! $includesWarnWord) {
98+
return;
99+
}
100+
// If there is an alias then store this explicit alias for matching in constructor
101+
if ($asPos) {
102+
$aliasNamePos = $asPos + 2;
103+
$this->aliases[] = strtolower($tokens[$aliasNamePos]['content']);
104+
}
105+
// Always store last word as alias for checking in constructor
106+
$this->aliases[] = $lastWord;
107+
}
108+
109+
/**
110+
* If constructor, check for proxy/plugin names and warn
111+
*
112+
* @param File $phpcsFile
113+
* @param int $stackPtr
114+
* @param array $tokens
115+
*/
116+
private function processFunction(
117+
File $phpcsFile,
118+
$stackPtr,
119+
$tokens
120+
) {
121+
// Find start and end of constructor signature based on brackets
122+
if (! $this->getConstructorPosition($phpcsFile, $stackPtr, $tokens, $openParenth, $closeParenth)) {
123+
return;
124+
}
125+
$positionInConstrSig = $openParenth;
126+
$skipTillAfterVariable = false;
127+
do {
128+
// Find next part of namespace (string) or variable name
129+
$positionInConstrSig = $phpcsFile->findNext(
130+
[T_STRING, T_VARIABLE],
131+
$positionInConstrSig + 1,
132+
$closeParenth
133+
);
134+
$currentTokenIsString = $tokens[$positionInConstrSig]['code'] == T_STRING;
135+
// If we've already found a match, continue till after variable
136+
if ($skipTillAfterVariable) {
137+
if (!$currentTokenIsString) {
138+
$skipTillAfterVariable = false;
139+
}
140+
continue;
141+
}
142+
// If match in namespace or variable then add warning
143+
$name = strtolower($tokens[$positionInConstrSig]['content']);
144+
$namesToWarn = $this->mergedNamesToWarn($currentTokenIsString);
145+
if ($this->containsWord($namesToWarn, $name)) {
146+
$phpcsFile->addWarning($this->warningMessage, $positionInConstrSig, $this->warningCode, [$name]);
147+
if ($currentTokenIsString) {
148+
$skipTillAfterVariable = true;
149+
}
150+
}
151+
} while ($positionInConstrSig !== false && $positionInConstrSig < $closeParenth);
152+
}
153+
154+
/**
155+
* Sets start and end of constructor signature or returns false
156+
*
157+
* @param File $phpcsFile
158+
* @param int $stackPtr
159+
* @param array $tokens
160+
* @param int $openParenth
161+
* @param int $closeParenth
162+
*
163+
* @return bool Whether a constructor
164+
*/
165+
private function getConstructorPosition(
166+
File $phpcsFile,
167+
$stackPtr,
168+
array $tokens,
169+
&$openParenth,
170+
&$closeParenth
171+
) {
172+
$methodNamePos = $phpcsFile->findNext(T_STRING, $stackPtr - 1);
173+
if ($methodNamePos === false) {
174+
return false;
175+
}
176+
// There is a method name
177+
if ($tokens[$methodNamePos]['content'] != self::CONSTRUCT_METHOD_NAME) {
178+
return false;
179+
}
180+
181+
// KNOWN: There is a constructor, so get position
182+
183+
$openParenth = $phpcsFile->findNext(T_OPEN_PARENTHESIS, $methodNamePos);
184+
$closeParenth = $phpcsFile->findNext(T_CLOSE_PARENTHESIS, $openParenth);
185+
if ($openParenth === false || $closeParenth === false) {
186+
return false;
187+
}
188+
189+
return true;
190+
}
191+
192+
/**
193+
* Whether $name is contained in any of $haystacks
194+
*
195+
* @param array $haystacks
196+
* @param string $name
197+
*
198+
* @return bool
199+
*/
200+
private function containsWord($haystacks, $name)
201+
{
202+
$matchWarnWord = false;
203+
foreach ($haystacks as $warn) {
204+
if (strpos($name, $warn) !== false) {
205+
$matchWarnWord = true;
206+
break;
207+
}
208+
}
209+
210+
return $matchWarnWord;
211+
}
212+
213+
/**
214+
* Whether warn words are included in STRING tokens in the given range
215+
*
216+
* Populates $lastWord in set to get usable name from namespace
217+
*
218+
* @param File $phpcsFile
219+
* @param int $startPosition
220+
* @param int $endPosition
221+
* @param array $tokens
222+
* @param string|null $lastWord
223+
*
224+
* @return bool
225+
*/
226+
private function includesWarnWordsInSTRINGs(
227+
File $phpcsFile,
228+
$startPosition,
229+
$endPosition,
230+
$tokens,
231+
&$lastWord
232+
) {
233+
$includesWarnWord = false;
234+
$currentPosition = $startPosition;
235+
do {
236+
$currentPosition = $phpcsFile->findNext(T_STRING, $currentPosition + 1, $endPosition);
237+
if ($currentPosition !== false) {
238+
$word = strtolower($tokens[$currentPosition]['content']);
239+
if ($this->containsWord($this->mergedNamesToWarn(false), $word)) {
240+
$includesWarnWord = true;
241+
}
242+
$lastWord = $word;
243+
}
244+
} while ($currentPosition !== false && $currentPosition < $endPosition);
245+
246+
return $includesWarnWord;
247+
}
248+
249+
/**
250+
* Get array of names that if matched should raise warning.
251+
*
252+
* Includes aliases if required
253+
*
254+
* @param bool $includeAliases
255+
*
256+
* @return array
257+
*/
258+
private function mergedNamesToWarn($includeAliases = false)
259+
{
260+
$namesToWarn = $this->warnNames;
261+
if ($includeAliases) {
262+
$namesToWarn = array_merge($namesToWarn, $this->aliases);
263+
}
264+
265+
return $namesToWarn;
266+
}
267+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace Vendor\Module\Plugin;
4+
5+
use \Vendor\Module\Plugin\AnyName as PlugAlias;
6+
use \Exception as SafeAlias;
7+
8+
class AnyName {}
9+
10+
/**
11+
* // Rule find: constructor use of plugin class
12+
*/
13+
class Foo
14+
{
15+
public function __construct(
16+
$first,
17+
\Vendor\Module\Plugin\AnyName $anything,
18+
$plugin,
19+
$another = []
20+
)
21+
{
22+
23+
}
24+
public function notAConstruct(
25+
\Vendor\Module\Plugin\AnyName $anything
26+
)
27+
{
28+
29+
}
30+
}
31+
32+
/**
33+
* // Rule find: constructor use of plugin class with alias
34+
*/
35+
class Foo2 {
36+
public function __construct(PlugAlias $anything, $aPlugin) {}
37+
}
38+
39+
/**
40+
* // Rule find: constructor use of plugin class with use statement
41+
*/
42+
class Foo3 {
43+
public function __construct(AnyName $anything) {}
44+
}
45+
46+
/**
47+
* // Rule find: This is fine
48+
*/
49+
class Foo4
50+
{
51+
public function __construct(SafeAlias $other) {
52+
53+
}
54+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace Vendor\Module\Proxy;
4+
5+
use \Vendor\Module\Proxy\AnyName as ProxAlias;
6+
use \Exception as SafeAlias;
7+
8+
class AnyName {}
9+
10+
/**
11+
* // Rule find: constructor use of proxy class
12+
*/
13+
class Foo
14+
{
15+
public function __construct(
16+
$first,
17+
\Vendor\Module\Proxy\AnyName $anything,
18+
$proxy,
19+
$another = []
20+
)
21+
{
22+
23+
}
24+
public function notAConstruct(
25+
\Vendor\Module\Proxy\AnyName $anything
26+
)
27+
{
28+
29+
}
30+
}
31+
32+
/**
33+
* // Rule find: constructor use of proxy class with alias
34+
*/
35+
class Foo2 {
36+
public function __construct(ProxAlias $anything, $aProxy) {}
37+
}
38+
39+
/**
40+
* // Rule find: constructor use of proxy class with use statement
41+
*/
42+
class Foo3 {
43+
public function __construct(AnyName $anything) {}
44+
}
45+
46+
/**
47+
* // Rule find: This is fine
48+
*/
49+
class Foo4
50+
{
51+
public function __construct(SafeAlias $other) {
52+
53+
}
54+
}

0 commit comments

Comments
 (0)