Skip to content

Commit 80e07de

Browse files
committed
AC-662: Create phpcs static check for AutogeneratedClassNotInConstructorTest
1 parent ec62c5c commit 80e07de

4 files changed

+287
-0
lines changed
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
<?php
2+
/**
3+
* Copyright © Magento. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace Magento2\Sniffs\PHP;
8+
9+
use PHP_CodeSniffer\Files\File;
10+
use PHP_CodeSniffer\Sniffs\Sniff;
11+
12+
/**
13+
* Detects possible usage of 'var' language construction.
14+
*/
15+
class AutogeneratedClassNotInConstructorSniff implements Sniff
16+
{
17+
const ERROR_CODE = 'AUTOGENERATED_CLASS_NOT_IN_CONSTRUCTOR';
18+
/**
19+
* @var mixed
20+
*/
21+
private $constructorScopeOpener;
22+
/**
23+
* @var mixed
24+
*/
25+
private $constructorScopeCloser;
26+
27+
/**
28+
* @var array
29+
*/
30+
private $constructorParameters = [];
31+
32+
/**
33+
* @var array
34+
*/
35+
private $uses = [];
36+
37+
/**
38+
* @inheritdoc
39+
*/
40+
public function register()
41+
{
42+
return [T_FUNCTION, T_DOUBLE_COLON, T_USE];
43+
}
44+
45+
/**
46+
* @inheritdoc
47+
*/
48+
public function process(File $phpcsFile, $stackPtr)
49+
{
50+
if ($phpcsFile->getTokens()[$stackPtr]['type'] === 'T_USE') {
51+
$this->registerUses($phpcsFile, $stackPtr);
52+
}
53+
if ($phpcsFile->getTokens()[$stackPtr]['type'] === 'T_FUNCTION') {
54+
$this->registerConstructorParameters($phpcsFile, $stackPtr);
55+
}
56+
if ($phpcsFile->getTokens()[$stackPtr]['type'] === 'T_DOUBLE_COLON') {
57+
if (!$this->isInsideConstruct($stackPtr)) {
58+
return;
59+
}
60+
if (!$this->isObjectManagerGetInstance($phpcsFile, $stackPtr)) {
61+
return;
62+
}
63+
64+
$statementStart = $phpcsFile->findStartOfStatement($stackPtr);
65+
$statementEnd = $phpcsFile->findEndOfStatement($stackPtr);
66+
$equalsPtr = $phpcsFile->findNext(T_EQUAL, $statementStart, $statementEnd);
67+
68+
if (!$equalsPtr) {
69+
return;
70+
}
71+
72+
$variableInParameters = false;
73+
if ($variable = $phpcsFile->findNext(T_VARIABLE, $equalsPtr, $statementEnd)) {
74+
$variableName = $phpcsFile->getTokens()[$variable]['content'];
75+
foreach ($this->constructorParameters as $parameter) {
76+
$parameterName = $parameter['name'];
77+
if ($parameterName === $variableName) {
78+
$variableInParameters = true;
79+
}
80+
}
81+
}
82+
83+
if (!$variableInParameters) {
84+
$next = $stackPtr;
85+
while ($next = $phpcsFile->findNext(T_DOUBLE_COLON, $next + 1, $statementEnd)) {
86+
if ($this->getNext($phpcsFile, $next, $statementEnd, T_STRING)['content'] === 'class') {
87+
$className = $this->getPrevious($phpcsFile, $next, T_STRING)['content'];
88+
}
89+
}
90+
91+
$className = $this->getClassNamespace($className);
92+
93+
$phpcsFile->addError(
94+
sprintf("Class %s needs to be requested in constructor, " .
95+
"otherwise compiler will not be able to find and generate these classes", $className),
96+
$stackPtr,
97+
self::ERROR_CODE
98+
);
99+
}
100+
}
101+
}
102+
103+
/**
104+
* Check if it is a ObjectManager::getInstance
105+
*
106+
* @param File $phpcsFile
107+
* @param int $stackPtr
108+
* @return bool
109+
*/
110+
private function isObjectManagerGetInstance(File $phpcsFile, int $stackPtr): bool
111+
{
112+
$prev = $phpcsFile->findPrevious(T_STRING, $stackPtr - 1);
113+
$next = $phpcsFile->findNext(T_STRING, $stackPtr + 1);
114+
if ($prev &&
115+
$next &&
116+
$phpcsFile->getTokens()[$prev]['content'] === 'ObjectManager' &&
117+
$phpcsFile->getTokens()[$next]['content'] === 'getInstance') {
118+
return true;
119+
}
120+
return false;
121+
}
122+
123+
/**
124+
* Checks if the code is inside __construct
125+
*
126+
* @param int $stackPtr
127+
* @return bool
128+
*/
129+
private function isInsideConstruct(int $stackPtr): bool
130+
{
131+
return $stackPtr > $this->constructorScopeOpener && $stackPtr < $this->constructorScopeCloser;
132+
}
133+
134+
/**
135+
* Get the complete class namespace from the use's
136+
*
137+
* @param string $className
138+
* @return string
139+
*/
140+
private function getClassNamespace(string $className): string
141+
{
142+
foreach ($this->uses as $use) {
143+
if (end($use) === $className) {
144+
$className = implode('/', $use);
145+
}
146+
}
147+
return $className;
148+
}
149+
150+
/**
151+
* Register php uses
152+
*
153+
* @param File $phpcsFile
154+
* @param int $stackPtr
155+
*/
156+
private function registerUses(File $phpcsFile, int $stackPtr): void
157+
{
158+
$useEnd = $phpcsFile->findEndOfStatement($stackPtr);
159+
$use = [];
160+
$usePosition = $stackPtr;
161+
while ($usePosition = $phpcsFile->findNext(T_STRING, $usePosition + 1, $useEnd)) {
162+
$use[] = $phpcsFile->getTokens()[$usePosition]['content'];
163+
}
164+
$this->uses [] = $use;
165+
}
166+
167+
/**
168+
* Register php constructor parameters
169+
*
170+
* @param File $phpcsFile
171+
* @param int $stackPtr
172+
*/
173+
private function registerConstructorParameters(File $phpcsFile, int $stackPtr): void
174+
{
175+
$functionName = $phpcsFile->getDeclarationName($stackPtr);
176+
if ($functionName == '__construct') {
177+
$this->constructorParameters = $phpcsFile->getMethodParameters($stackPtr);
178+
$this->constructorScopeOpener = $phpcsFile->getTokens()[$stackPtr]['scope_opener'];
179+
$this->constructorScopeCloser = $phpcsFile->getTokens()[$stackPtr]['scope_closer'];
180+
181+
}
182+
}
183+
184+
/**
185+
* Get next token
186+
*
187+
* @param File $phpcsFile
188+
* @param int $nextDoubleColumn
189+
* @param int $statementEnd
190+
* @param int|string|array $types
191+
* @return mixed
192+
*/
193+
private function getNext(File $phpcsFile, int $nextDoubleColumn, int $statementEnd, $types)
194+
{
195+
return $phpcsFile->getTokens()[$phpcsFile->findNext($types, $nextDoubleColumn + 1, $statementEnd)];
196+
}
197+
198+
/**
199+
* Get previous token
200+
*
201+
* @param File $phpcsFile
202+
* @param int $nextDoubleColumn
203+
* @param int|string|array $types
204+
* @return mixed
205+
*/
206+
private function getPrevious(File $phpcsFile, int $nextDoubleColumn, $types)
207+
{
208+
return $phpcsFile->getTokens()[$phpcsFile->findPrevious($types, $nextDoubleColumn - 1)];
209+
}
210+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
/**
3+
* Copyright © Magento. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace Magento2\Tests\PHP;
8+
9+
use Magento2\Model;
10+
11+
class Good
12+
{
13+
public function __construct(
14+
Model $model = null
15+
)
16+
{
17+
$this->model = $model ?? ObjectManager::getInstance()->get(Model::class);
18+
}
19+
20+
/**
21+
* @return Model
22+
*/
23+
public function otherMethodThatCallsGetInstance(): void
24+
{
25+
$model = ObjectManager::getInstance()->get(Model::class);
26+
$model->something();
27+
}
28+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
/**
3+
* Copyright © Magento. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace Magento2\Tests\PHP;
8+
9+
class Bad
10+
{
11+
public function __construct()
12+
{
13+
$this->model = ObjectManager::getInstance()->get(Model::class);
14+
}
15+
}
16+
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
/**
3+
* Copyright © Magento. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace Magento2\Tests\PHP;
8+
9+
use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest;
10+
11+
class AutogeneratedClassNotInConstructorUnitTest extends AbstractSniffUnitTest
12+
{
13+
/**
14+
* @inheritdoc
15+
*/
16+
public function getErrorList($filename = '')
17+
{
18+
if ($filename === 'AutogeneratedClassNotInConstructorUnitTest.2.php.inc') {
19+
return [
20+
13 => 1
21+
];
22+
}
23+
return [];
24+
}
25+
26+
/**
27+
* @inheritdoc
28+
*/
29+
public function getWarningList()
30+
{
31+
return [];
32+
}
33+
}

0 commit comments

Comments
 (0)