Skip to content

Commit 0dba0bd

Browse files
authored
Merge branch 'develop' into MC-5894
2 parents 575acb6 + 37b5963 commit 0dba0bd

21 files changed

+717
-304
lines changed

Magento2/Sniffs/Classes/AbstractApiSniff.php

+3
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ public function process(File $phpcsFile, $stackPtr)
4848
}
4949

5050
$commentStartPtr = $phpcsFile->findPrevious(T_DOC_COMMENT_OPEN_TAG, $stackPtr - 1, 0);
51+
if ($commentStartPtr === false) {
52+
return;
53+
}
5154
$commentCloserPtr = $tokens[$commentStartPtr]['comment_closer'];
5255

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

Magento2/Sniffs/PHP/LiteralNamespacesSniff.php

+1-17
Original file line numberDiff line numberDiff line change
@@ -52,28 +52,12 @@ public function process(File $sourceFile, $stackPtr)
5252
$content = preg_replace('|\\\{2,}|', '\\', $content);
5353
}
5454

55-
if (preg_match($this->literalNamespacePattern, $content) === 1 && $this->classExists($content)) {
55+
if (preg_match($this->literalNamespacePattern, $content) === 1) {
5656
$sourceFile->addWarning(
5757
"Use ::class notation instead.",
5858
$stackPtr,
5959
'LiteralClassUsage'
6060
);
6161
}
6262
}
63-
64-
/**
65-
* Checks if class or interface exists.
66-
*
67-
* ToDo: get rig of this check https://github.com/magento/magento-coding-standard/issues/9
68-
*
69-
* @param string $className
70-
* @return bool
71-
*/
72-
private function classExists($className)
73-
{
74-
if (!isset($this->classNames[$className])) {
75-
$this->classNames[$className] = class_exists($className) || interface_exists($className);
76-
}
77-
return $this->classNames[$className];
78-
}
7963
}

0 commit comments

Comments
 (0)