-
Notifications
You must be signed in to change notification settings - Fork 160
Proxies and interceptors #59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
eed9414
0dcfef0
c8d8e1d
e0bfc48
ee0bbd5
44c3a70
2e09390
33e1873
39debac
f1766a3
1d4c5d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,267 @@ | ||
<?php | ||
/** | ||
* Copyright © Magento. All rights reserved. | ||
* See COPYING.txt for license details. | ||
*/ | ||
|
||
namespace Magento\Sniffs\Classes; | ||
|
||
use PHP_CodeSniffer\Files\File; | ||
use PHP_CodeSniffer\Sniffs\Sniff; | ||
|
||
/** | ||
* Detects explicit request of proxies and interceptors in constructors | ||
* | ||
* Search is in variable names and namespaces, including indirect namespaces from use statements | ||
*/ | ||
class ConstructorProxyInterceptorSniff implements Sniff | ||
{ | ||
const CONSTRUCT_METHOD_NAME = '__construct'; | ||
|
||
/** | ||
* String representation of warning. | ||
* | ||
* @var string | ||
*/ | ||
protected $warningMessage = 'Proxies and interceptors MUST never be explicitly requested in constructors.'; | ||
|
||
/** | ||
* Warning violation code. | ||
* | ||
* @var string | ||
*/ | ||
protected $warningCode = 'ConstructorProxyInterceptor'; | ||
|
||
/** | ||
* @var array Aliases of proxies or plugins from use statements | ||
*/ | ||
private $aliases = []; | ||
|
||
/** | ||
* @var array Terms to search for in variables and namespaces | ||
*/ | ||
private $warnNames = [ | ||
'proxy', | ||
'plugin' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably the task was not clear enough, the expectation was to prevent There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense. I'll add interceptors first and then look at plugins as you suggest. |
||
]; | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
public function register() | ||
{ | ||
return [ | ||
T_USE, | ||
T_FUNCTION | ||
]; | ||
} | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
public function process(File $phpcsFile, $stackPtr) | ||
{ | ||
// Match use statements and constructor (latter matches T_FUNCTION to find constructors | ||
$tokens = $phpcsFile->getTokens(); | ||
if ($tokens[$stackPtr]['code'] == T_USE) { | ||
$this->processUse($phpcsFile, $stackPtr, $tokens); | ||
} elseif ($tokens[$stackPtr]['code'] == T_FUNCTION) { | ||
$this->processFunction($phpcsFile, $stackPtr, $tokens); | ||
} | ||
} | ||
|
||
/** | ||
* Store plugin/proxy class names for use in matching constructor | ||
* | ||
* @param File $phpcsFile | ||
* @param int $stackPtr | ||
* @param array $tokens | ||
*/ | ||
private function processUse( | ||
File $phpcsFile, | ||
$stackPtr, | ||
$tokens | ||
) { | ||
// Find end of use statement and position of AS alias if exists | ||
$endPos = $phpcsFile->findNext(T_SEMICOLON, $stackPtr); | ||
$asPos = $phpcsFile->findNext(T_AS, $stackPtr, $endPos); | ||
// Find whether this use statement includes any of the warning words | ||
$includesWarnWord = | ||
$this->includesWarnWordsInSTRINGs( | ||
$phpcsFile, | ||
$stackPtr, | ||
min($asPos, $endPos), | ||
$tokens, | ||
$lastWord | ||
); | ||
if (! $includesWarnWord) { | ||
return; | ||
} | ||
// If there is an alias then store this explicit alias for matching in constructor | ||
if ($asPos) { | ||
$aliasNamePos = $asPos + 2; | ||
$this->aliases[] = strtolower($tokens[$aliasNamePos]['content']); | ||
} | ||
// Always store last word as alias for checking in constructor | ||
$this->aliases[] = $lastWord; | ||
} | ||
|
||
/** | ||
* If constructor, check for proxy/plugin names and warn | ||
* | ||
* @param File $phpcsFile | ||
* @param int $stackPtr | ||
* @param array $tokens | ||
*/ | ||
private function processFunction( | ||
File $phpcsFile, | ||
$stackPtr, | ||
$tokens | ||
) { | ||
// Find start and end of constructor signature based on brackets | ||
if (! $this->getConstructorPosition($phpcsFile, $stackPtr, $tokens, $openParenth, $closeParenth)) { | ||
return; | ||
} | ||
$positionInConstrSig = $openParenth; | ||
$skipTillAfterVariable = false; | ||
do { | ||
// Find next part of namespace (string) or variable name | ||
$positionInConstrSig = $phpcsFile->findNext( | ||
[T_STRING, T_VARIABLE], | ||
$positionInConstrSig + 1, | ||
$closeParenth | ||
); | ||
$currentTokenIsString = $tokens[$positionInConstrSig]['code'] == T_STRING; | ||
// If we've already found a match, continue till after variable | ||
if ($skipTillAfterVariable) { | ||
if (!$currentTokenIsString) { | ||
$skipTillAfterVariable = false; | ||
} | ||
continue; | ||
} | ||
// If match in namespace or variable then add warning | ||
$name = strtolower($tokens[$positionInConstrSig]['content']); | ||
$namesToWarn = $this->mergedNamesToWarn($currentTokenIsString); | ||
if ($this->containsWord($namesToWarn, $name)) { | ||
$phpcsFile->addWarning($this->warningMessage, $positionInConstrSig, $this->warningCode, [$name]); | ||
if ($currentTokenIsString) { | ||
$skipTillAfterVariable = true; | ||
} | ||
} | ||
} while ($positionInConstrSig !== false && $positionInConstrSig < $closeParenth); | ||
} | ||
|
||
/** | ||
* Sets start and end of constructor signature or returns false | ||
* | ||
* @param File $phpcsFile | ||
* @param int $stackPtr | ||
* @param array $tokens | ||
* @param int $openParenth | ||
* @param int $closeParenth | ||
* | ||
* @return bool Whether a constructor | ||
*/ | ||
private function getConstructorPosition( | ||
File $phpcsFile, | ||
$stackPtr, | ||
array $tokens, | ||
&$openParenth, | ||
&$closeParenth | ||
) { | ||
$methodNamePos = $phpcsFile->findNext(T_STRING, $stackPtr - 1); | ||
if ($methodNamePos === false) { | ||
return false; | ||
} | ||
// There is a method name | ||
if ($tokens[$methodNamePos]['content'] != self::CONSTRUCT_METHOD_NAME) { | ||
return false; | ||
} | ||
|
||
// KNOWN: There is a constructor, so get position | ||
|
||
$openParenth = $phpcsFile->findNext(T_OPEN_PARENTHESIS, $methodNamePos); | ||
$closeParenth = $phpcsFile->findNext(T_CLOSE_PARENTHESIS, $openParenth); | ||
if ($openParenth === false || $closeParenth === false) { | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
/** | ||
* Whether $name is contained in any of $haystacks | ||
* | ||
* @param array $haystacks | ||
* @param string $name | ||
* | ||
* @return bool | ||
*/ | ||
private function containsWord($haystacks, $name) | ||
{ | ||
$matchWarnWord = false; | ||
foreach ($haystacks as $warn) { | ||
if (strpos($name, $warn) !== false) { | ||
$matchWarnWord = true; | ||
break; | ||
} | ||
} | ||
|
||
return $matchWarnWord; | ||
} | ||
|
||
/** | ||
* Whether warn words are included in STRING tokens in the given range | ||
* | ||
* Populates $lastWord in set to get usable name from namespace | ||
* | ||
* @param File $phpcsFile | ||
* @param int $startPosition | ||
* @param int $endPosition | ||
* @param array $tokens | ||
* @param string|null $lastWord | ||
* | ||
* @return bool | ||
*/ | ||
private function includesWarnWordsInSTRINGs( | ||
File $phpcsFile, | ||
$startPosition, | ||
$endPosition, | ||
$tokens, | ||
&$lastWord | ||
) { | ||
$includesWarnWord = false; | ||
$currentPosition = $startPosition; | ||
do { | ||
$currentPosition = $phpcsFile->findNext(T_STRING, $currentPosition + 1, $endPosition); | ||
if ($currentPosition !== false) { | ||
$word = strtolower($tokens[$currentPosition]['content']); | ||
if ($this->containsWord($this->mergedNamesToWarn(false), $word)) { | ||
$includesWarnWord = true; | ||
} | ||
$lastWord = $word; | ||
} | ||
} while ($currentPosition !== false && $currentPosition < $endPosition); | ||
|
||
return $includesWarnWord; | ||
} | ||
|
||
/** | ||
* Get array of names that if matched should raise warning. | ||
* | ||
* Includes aliases if required | ||
* | ||
* @param bool $includeAliases | ||
* | ||
* @return array | ||
*/ | ||
private function mergedNamesToWarn($includeAliases = false) | ||
{ | ||
$namesToWarn = $this->warnNames; | ||
if ($includeAliases) { | ||
$namesToWarn = array_merge($namesToWarn, $this->aliases); | ||
} | ||
|
||
return $namesToWarn; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
<?php | ||
|
||
namespace Vendor\Module\Plugin; | ||
|
||
use \Vendor\Module\Plugin\AnyName as PlugAlias; | ||
use \Exception as SafeAlias; | ||
|
||
class AnyName {} | ||
|
||
/** | ||
* // Rule find: constructor use of plugin class | ||
*/ | ||
class Foo | ||
{ | ||
public function __construct( | ||
$first, | ||
\Vendor\Module\Plugin\AnyName $anything, | ||
$plugin, | ||
$another = [] | ||
) | ||
{ | ||
|
||
} | ||
public function notAConstruct( | ||
\Vendor\Module\Plugin\AnyName $anything | ||
) | ||
{ | ||
|
||
} | ||
} | ||
|
||
/** | ||
* // Rule find: constructor use of plugin class with alias | ||
*/ | ||
class Foo2 { | ||
public function __construct(PlugAlias $anything, $aPlugin) {} | ||
} | ||
|
||
/** | ||
* // Rule find: constructor use of plugin class with use statement | ||
*/ | ||
class Foo3 { | ||
public function __construct(AnyName $anything) {} | ||
} | ||
|
||
/** | ||
* // Rule find: This is fine | ||
*/ | ||
class Foo4 | ||
{ | ||
public function __construct(SafeAlias $other) { | ||
|
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
<?php | ||
|
||
namespace Vendor\Module\Proxy; | ||
|
||
use \Vendor\Module\Proxy\AnyName as ProxAlias; | ||
use \Exception as SafeAlias; | ||
|
||
class AnyName {} | ||
|
||
/** | ||
* // Rule find: constructor use of proxy class | ||
*/ | ||
class Foo | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Proxy classes are the one which have Plugin classes have arbitrary names and thus are difficult to catch. These rules may have accuracy issues and can raise warnings. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As this is based on names of constructor types and variables there will be a false-positive issue. In discussion with @lenaorobei it was felt that keeping these as warnings was of utility despite this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Even is it's bad naming it's worth pointing developers that class name with |
||
{ | ||
public function __construct( | ||
$first, | ||
\Vendor\Module\Proxy\AnyName $anything, | ||
$proxy, | ||
$another = [] | ||
) | ||
{ | ||
|
||
} | ||
public function notAConstruct( | ||
\Vendor\Module\Proxy\AnyName $anything | ||
) | ||
{ | ||
|
||
} | ||
} | ||
|
||
/** | ||
* // Rule find: constructor use of proxy class with alias | ||
*/ | ||
class Foo2 { | ||
public function __construct(ProxAlias $anything, $aProxy) {} | ||
} | ||
|
||
/** | ||
* // Rule find: constructor use of proxy class with use statement | ||
*/ | ||
class Foo3 { | ||
public function __construct(AnyName $anything) {} | ||
} | ||
|
||
/** | ||
* // Rule find: This is fine | ||
*/ | ||
class Foo4 | ||
{ | ||
public function __construct(SafeAlias $other) { | ||
|
||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be better to split this sniff into two sniffs to comply with single responsibility principle.
Or make it more generic and extensible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have concerns regarding your comment:
DiscouragedDependenciesSniff
(or something like this) and$warnNames
property can be public and will be replaceable viaruleset.xml
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do like the sound of the latter - I didn't realise that this could be done. Nice :)