-
Notifications
You must be signed in to change notification settings - Fork 159
AC-662: Create phpcs static check for AutogeneratedClassNotInConstructorTest #273
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 all commits
80e07de
a6d2530
f7b3bcc
c06cbc2
1d499c2
504a50b
7d317ba
65eda98
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,226 @@ | ||
<?php | ||
/** | ||
* Copyright © Magento, Inc. All rights reserved. | ||
* See COPYING.txt for license details. | ||
*/ | ||
declare(strict_types = 1); | ||
|
||
namespace Magento2\Sniffs\PHP; | ||
|
||
use PHP_CodeSniffer\Files\File; | ||
use PHP_CodeSniffer\Sniffs\Sniff; | ||
|
||
/** | ||
* Detects bad usages of ObjectManager in constructor. | ||
*/ | ||
class AutogeneratedClassNotInConstructorSniff implements Sniff | ||
{ | ||
private const ERROR_CODE = 'AUTOGENERATED_CLASS_NOT_IN_CONSTRUCTOR'; | ||
|
||
/** | ||
* @var array | ||
*/ | ||
private $constructorParameters = []; | ||
|
||
/** | ||
* @var array | ||
*/ | ||
private $uses = []; | ||
|
||
/** | ||
* @inheritdoc | ||
*/ | ||
public function register() | ||
{ | ||
return [T_FUNCTION, T_DOUBLE_COLON, T_USE]; | ||
} | ||
|
||
/** | ||
* @inheritdoc | ||
*/ | ||
public function process(File $phpcsFile, $stackPtr) | ||
{ | ||
if ($phpcsFile->getTokens()[$stackPtr]['type'] === 'T_USE') { | ||
$this->registerUse($phpcsFile, $stackPtr); | ||
} | ||
if ($phpcsFile->getTokens()[$stackPtr]['type'] === 'T_FUNCTION') { | ||
$this->registerConstructorParameters($phpcsFile, $stackPtr); | ||
} | ||
if ($phpcsFile->getTokens()[$stackPtr]['type'] === 'T_DOUBLE_COLON') { | ||
if (!$this->isObjectManagerGetInstance($phpcsFile, $stackPtr)) { | ||
return; | ||
} | ||
|
||
$statementStart = $phpcsFile->findStartOfStatement($stackPtr); | ||
$statementEnd = $phpcsFile->findEndOfStatement($stackPtr); | ||
$equalsPtr = $phpcsFile->findNext(T_EQUAL, $statementStart, $statementEnd); | ||
|
||
if (!$equalsPtr) { | ||
return; | ||
} | ||
|
||
if (!$this->isVariableInConstructorParameters($phpcsFile, $equalsPtr, $statementEnd)) { | ||
$className = $this->obtainClassToGetOrCreate($phpcsFile, $stackPtr, $statementEnd); | ||
|
||
$phpcsFile->addError( | ||
sprintf("Class %s needs to be requested in constructor, " . | ||
"otherwise compiler will not be able to find and generate these classes", $className), | ||
$stackPtr, | ||
self::ERROR_CODE | ||
); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Check if it is a ObjectManager::getInstance | ||
* | ||
* @param File $phpcsFile | ||
* @param int $stackPtr | ||
* @return bool | ||
*/ | ||
private function isObjectManagerGetInstance(File $phpcsFile, int $stackPtr): bool | ||
{ | ||
$prev = $phpcsFile->findPrevious(T_STRING, $stackPtr - 1); | ||
$next = $phpcsFile->findNext(T_STRING, $stackPtr + 1); | ||
return $prev && | ||
$next && | ||
$phpcsFile->getTokens()[$prev]['content'] === 'ObjectManager' && | ||
$phpcsFile->getTokens()[$next]['content'] === 'getInstance'; | ||
} | ||
|
||
/** | ||
* Get the complete class namespace from the use's | ||
* | ||
* @param string $className | ||
* @return string | ||
*/ | ||
private function getClassNamespace(string $className): string | ||
{ | ||
foreach ($this->uses as $key => $use) { | ||
if ($key === $className) { | ||
return $use; | ||
} | ||
} | ||
return $className; | ||
} | ||
|
||
/** | ||
* Register php uses | ||
* | ||
* @param File $phpcsFile | ||
* @param int $stackPtr | ||
*/ | ||
private function registerUse(File $phpcsFile, int $stackPtr): void | ||
{ | ||
$useEnd = $phpcsFile->findEndOfStatement($stackPtr); | ||
$use = []; | ||
$usePosition = $stackPtr; | ||
while ($usePosition = $phpcsFile->findNext(T_STRING, $usePosition + 1, $useEnd)) { | ||
$use[] = $phpcsFile->getTokens()[$usePosition]['content']; | ||
} | ||
|
||
$key = end($use); | ||
if ($phpcsFile->findNext(T_AS, $stackPtr, $useEnd)) { | ||
$this->uses[$key] = implode("\\", array_slice($use, 0, count($use) - 1)); | ||
} else { | ||
$this->uses[$key] = implode("\\", $use); | ||
} | ||
} | ||
|
||
/** | ||
* Register php constructor parameters | ||
* | ||
* @param File $phpcsFile | ||
* @param int $stackPtr | ||
*/ | ||
private function registerConstructorParameters(File $phpcsFile, int $stackPtr): void | ||
{ | ||
$functionName = $phpcsFile->getDeclarationName($stackPtr); | ||
if ($functionName == '__construct') { | ||
$this->constructorParameters = $phpcsFile->getMethodParameters($stackPtr); | ||
} | ||
} | ||
|
||
/** | ||
* Get next token | ||
* | ||
* @param File $phpcsFile | ||
* @param int $from | ||
* @param int $to | ||
* @param int|string|array $types | ||
* @return mixed | ||
*/ | ||
private function getNext(File $phpcsFile, int $from, int $to, $types) | ||
{ | ||
return $phpcsFile->getTokens()[$phpcsFile->findNext($types, $from + 1, $to)]; | ||
} | ||
|
||
/** | ||
* Get previous token | ||
* | ||
* @param File $phpcsFile | ||
* @param int $from | ||
* @param int|string|array $types | ||
* @return mixed | ||
*/ | ||
private function getPrevious(File $phpcsFile, int $from, $types) | ||
{ | ||
return $phpcsFile->getTokens()[$phpcsFile->findPrevious($types, $from - 1)]; | ||
} | ||
|
||
/** | ||
* Get name of the variable without $ | ||
* | ||
* @param string $parameterName | ||
* @return string | ||
*/ | ||
protected function variableName(string $parameterName): string | ||
{ | ||
return str_replace('$', '', $parameterName); | ||
} | ||
|
||
/** | ||
* Checks if a variable is present in the constructor parameters | ||
* | ||
* @param File $phpcsFile | ||
* @param int $equalsPtr | ||
* @param int $statementEnd | ||
* @return bool | ||
*/ | ||
private function isVariableInConstructorParameters(File $phpcsFile, int $equalsPtr, int $statementEnd): bool | ||
{ | ||
if ($variable = $phpcsFile->findNext(T_VARIABLE, $equalsPtr, $statementEnd)) { | ||
$variableName = $phpcsFile->getTokens()[$variable]['content']; | ||
if ($variableName === '$this') { | ||
$variableName = $this->getNext($phpcsFile, $variable, $statementEnd, T_STRING)['content']; | ||
} | ||
foreach ($this->constructorParameters as $parameter) { | ||
$parameterName = $parameter['name']; | ||
if ($this->variableName($parameterName) === $this->variableName($variableName)) { | ||
return true; | ||
} | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
/** | ||
* Obtain the class inside ObjectManager::getInstance()->get|create() | ||
* | ||
* @param File $phpcsFile | ||
* @param int $next | ||
* @param int $statementEnd | ||
* @return string | ||
*/ | ||
private function obtainClassToGetOrCreate(File $phpcsFile, int $next, int $statementEnd): string | ||
{ | ||
while ($next = $phpcsFile->findNext(T_DOUBLE_COLON, $next + 1, $statementEnd)) { | ||
if ($this->getNext($phpcsFile, $next, $statementEnd, T_STRING)['content'] === 'class') { | ||
$className = $this->getPrevious($phpcsFile, $next, T_STRING)['content']; | ||
} | ||
} | ||
|
||
return $this->getClassNamespace($className); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,38 @@ | ||||||||||||||||||||||||||
<?php | ||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||
* Copyright © Magento, Inc. All rights reserved. | ||||||||||||||||||||||||||
* See COPYING.txt for license details. | ||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||
declare(strict_types = 1); | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
namespace Magento2\Tests\PHP; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
use Magento2\OneModel as Model; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
class Good | ||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||
public function __construct( | ||||||||||||||||||||||||||
Model $model = null | ||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||
$this->model = $model ?? ObjectManager::getInstance()->get(Model::class); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||
* @return Model | ||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||
public function otherMethodThatCallsGetInstanceBad(): void | ||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||
$model = ObjectManager::getInstance()->get(Model::class); | ||||||||||||||||||||||||||
$model->something(); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||
* @return Model | ||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||
public function otherMethodThatCallsGetInstanceGood(): void | ||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||
$model = $this->model ?? ObjectManager::getInstance()->get(Model::class); | ||||||||||||||||||||||||||
$model->something(); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
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. There is another case missing here, what about assigning an object manager instance to a variable?
Suggested change
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. This is not on the ticket. And I can see in the original sniff is not covered I guess. |
||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?php | ||
/** | ||
* Copyright © Magento, Inc. All rights reserved. | ||
* See COPYING.txt for license details. | ||
*/ | ||
declare(strict_types = 1); | ||
|
||
namespace Magento2\Tests\PHP; | ||
|
||
class Bad | ||
{ | ||
public function __construct() | ||
{ | ||
$this->model = ObjectManager::getInstance()->get(Model::class); | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
<?php | ||
/** | ||
* Copyright © Magento, Inc. All rights reserved. | ||
* See COPYING.txt for license details. | ||
*/ | ||
declare(strict_types = 1); | ||
|
||
namespace Magento2\Tests\PHP; | ||
|
||
use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; | ||
|
||
class AutogeneratedClassNotInConstructorUnitTest extends AbstractSniffUnitTest | ||
{ | ||
/** | ||
* @inheritdoc | ||
*/ | ||
public function getErrorList($filename = '') | ||
{ | ||
if ($filename === 'AutogeneratedClassNotInConstructorUnitTest.1.php.inc') { | ||
return [ | ||
26 => 1, | ||
]; | ||
} | ||
if ($filename === 'AutogeneratedClassNotInConstructorUnitTest.2.php.inc') { | ||
return [ | ||
14 => 1, | ||
]; | ||
} | ||
return []; | ||
} | ||
|
||
/** | ||
* @inheritdoc | ||
*/ | ||
public function getWarningList($filename = '') | ||
{ | ||
return []; | ||
} | ||
} |
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.
Is this function handling alias like
use Magento\Module\Class as MagentoModuleClass;
?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.
Not included.