Skip to content

Commit 27e99de

Browse files
MC-19366: Adds sniff for argument names
1 parent 586244a commit 27e99de

File tree

6 files changed

+270
-35
lines changed

6 files changed

+270
-35
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<?php
2+
/**
3+
* Copyright © Magento. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace Magento2\Sniffs\GraphQL;
8+
9+
use PHP_CodeSniffer\Files\File;
10+
use PHP_CodeSniffer\Sniffs\Sniff;
11+
12+
/**
13+
* Detects argument names that are not specified in <kbd>cameCase</kbd>.
14+
*/
15+
class ValidArgumentNameSniff implements Sniff
16+
{
17+
18+
/**
19+
* Defines the tokenizers that this sniff is using.
20+
*
21+
* @var array
22+
*/
23+
public $supportedTokenizers = ['GraphQL'];
24+
25+
/**
26+
* @inheritDoc
27+
*/
28+
public function register()
29+
{
30+
return [T_OPEN_PARENTHESIS];
31+
}
32+
33+
/**
34+
* @inheritDoc
35+
*/
36+
public function process(File $phpcsFile, $stackPtr)
37+
{
38+
$tokens = $phpcsFile->getTokens();
39+
$closeParenthesisPointer = $this->getCloseParenthesisPointer($stackPtr, $tokens);
40+
41+
//if we could not find the closing parenthesis pointer, we add a warning and terminate
42+
if ($closeParenthesisPointer === false) {
43+
$error = 'Possible parse error: Missing closing parenthesis for argument list in line %d';
44+
$data = [
45+
$tokens[$stackPtr]['line'],
46+
];
47+
$phpcsFile->addWarning($error, $stackPtr, 'UnclosedArgumentList', $data);
48+
return;
49+
}
50+
51+
$arguments = $this->getArguments($stackPtr, $closeParenthesisPointer, $tokens);
52+
53+
foreach ($arguments as $argument) {
54+
$pointer = $argument[0];
55+
$name = $argument[1];
56+
57+
if (!$this->isCamelCase($name)) {
58+
$type = 'Argument';
59+
$error = '%s name "%s" is not in CamelCase format';
60+
$data = [
61+
$type,
62+
$name,
63+
];
64+
65+
$phpcsFile->addError($error, $pointer, 'NotCamelCase', $data);
66+
$phpcsFile->recordMetric($pointer, 'CamelCase argument name', 'no');
67+
} else {
68+
$phpcsFile->recordMetric($pointer, 'CamelCase argument name', 'yes');
69+
}
70+
}
71+
72+
//return stack pointer of closing parenthesis
73+
return $closeParenthesisPointer;
74+
}
75+
76+
/**
77+
* Finds all argument names contained in <var>$tokens</var> range <var>$startPointer</var> and
78+
* <var>$endPointer</var>.
79+
*
80+
* @param int $startPointer
81+
* @param int $endPointer
82+
* @param array $tokens
83+
* @return array[]
84+
*/
85+
private function getArguments($startPointer, $endPointer, array $tokens)
86+
{
87+
$argumentTokenPointer = null;
88+
$argument = '';
89+
$names = [];
90+
$skipTypes = [T_COMMENT, T_WHITESPACE];
91+
92+
for ($i = $startPointer + 1; $i < $endPointer; ++$i) {
93+
//skip comment tokens
94+
if (in_array($tokens[$i]['code'], $skipTypes)) {
95+
continue;
96+
}
97+
$argument .= $tokens[$i]['content'];
98+
99+
if ($argumentTokenPointer === null) {
100+
$argumentTokenPointer = $i;
101+
}
102+
103+
if (preg_match('/^.+:.+$/', $argument)) {
104+
list($name, $type) = explode(':', $argument);
105+
$names[] = [$argumentTokenPointer, $name];
106+
$argument = '';
107+
$argumentTokenPointer = null;
108+
}
109+
}
110+
111+
return $names;
112+
}
113+
114+
/**
115+
* Seeks the next available token of type {@link T_CLOSE_PARENTHESIS} in <var>$tokens</var> and returns its pointer.
116+
*
117+
* @param int $stackPointer
118+
* @param array $tokens
119+
* @return bool|int
120+
*/
121+
private function getCloseParenthesisPointer($stackPointer, array $tokens)
122+
{
123+
$numTokens = count($tokens);
124+
125+
for ($i = $stackPointer + 1; $i < $numTokens; ++$i) {
126+
if ($tokens[$i]['code'] === T_CLOSE_PARENTHESIS) {
127+
return $i;
128+
}
129+
}
130+
131+
//if we came here we could not find the closing parenthesis
132+
return false;
133+
}
134+
135+
/**
136+
* Returns whether <var>$name</var> starts with a lower case character and is written in camel case.
137+
*
138+
* @param string $argumentName
139+
* @return bool
140+
*/
141+
private function isCamelCase($argumentName)
142+
{
143+
return (preg_match('/^[a-z][a-zA-Z0-9]+$/', $argumentName) !== 0);
144+
}
145+
}

Magento2/Sniffs/GraphQL/ValidFieldNameSniff.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
use PHP_CodeSniffer\Files\File;
99
use PHP_CodeSniffer\Sniffs\Sniff;
10-
use PHP_CodeSniffer\Util\Common;
1110

1211
/**
1312
* Detects field names the are not specified in <kbd>snake_case</kbd>.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
type Foo {
2+
bar(
3+
# Valid argument names
4+
validArgumentName: Int
5+
validArgumentNameWith1Number: Int
6+
validArgumentNameWith12345Numbers: Int
7+
validArgumentNameEndingWithNumber5: Int
8+
validArgumentNameWithUPPERCASE: Int
9+
validargumentwithlowercase: Int
10+
# Invalid argument names
11+
InvalidArgumentNameUpperCamelCase: Int
12+
INVALIDARGUMENTNAMEUPPERCASE: Int
13+
invalid_argument_name_snake_case: Int
14+
5invalidArgumentNameStatingWithNumber: Int
15+
): String
16+
}
17+
interface Foo {
18+
bar(
19+
# Valid argument names
20+
validArgumentName: Int
21+
validArgumentNameWith1Number: Int
22+
validArgumentNameWith12345Numbers: Int
23+
validArgumentNameEndingWithNumber5: Int
24+
validArgumentNameWithUPPERCASE: Int
25+
validargumentwithlowercase: Int
26+
# Invalid argument names
27+
InvalidArgumentNameUpperCamelCase: Int
28+
INVALIDARGUMENTNAMEUPPERCASE: Int
29+
invalid_argument_name_snake_case: Int
30+
5invalidArgumentNameStatingWithNumber: Int
31+
): String
32+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
/**
3+
* Copyright © Magento. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
namespace Magento2\Tests\GraphQL;
7+
8+
/**
9+
* Covers {@link \Magento2\Sniffs\GraphQL\ValidArgumentNameSniff}.
10+
*/
11+
class ValidArgumentNameUnitTest extends AbstractGraphQLSniffUnitTestCase
12+
{
13+
14+
/**
15+
* @inheritDoc
16+
*/
17+
protected function getErrorList()
18+
{
19+
return [
20+
11 => 1,
21+
12 => 1,
22+
13 => 1,
23+
14 => 1,
24+
27 => 1,
25+
28 => 1,
26+
29 => 1,
27+
30 => 1,
28+
];
29+
}
30+
31+
/**
32+
* @inheritDoc
33+
*/
34+
protected function getWarningList()
35+
{
36+
return [];
37+
}
38+
}
39+

Magento2/Tests/GraphQL/ValidFieldNameUnitTest.php

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,13 @@
66
namespace Magento2\Tests\GraphQL;
77

88
/**
9-
* Covers {@link \Magento2\Sniffs\GraphQL\ValidFieldNameUnitTest}.
9+
* Covers {@link \Magento2\Sniffs\GraphQL\ValidFieldNameSniff}.
1010
*/
1111
class ValidFieldNameUnitTest extends AbstractGraphQLSniffUnitTestCase
1212
{
1313

1414
/**
15-
* Returns the lines where errors should occur.
16-
*
17-
* The key of the array should represent the line number and the value
18-
* should represent the number of errors that should occur on that line.
19-
*
20-
* @return array<int, int>
15+
* @inheritDoc
2116
*/
2217
protected function getErrorList()
2318
{

PHP_CodeSniffer/Tokenizers/GraphQL.php

Lines changed: 52 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Copyright © Magento. All rights reserved.
44
* See COPYING.txt for license details.
55
*/
6+
67
namespace PHP_CodeSniffer\Tokenizers;
78

89
use GraphQL\Language\Lexer;
@@ -67,29 +68,7 @@ public function processAdditional()
6768
{
6869
$this->logVerbose('*** START ADDITIONAL GRAPHQL PROCESSING ***');
6970

70-
$processingEntity = false;
71-
$numTokens = count($this->tokens);
72-
$entityTypes = [T_CLASS, T_INTERFACE];
73-
74-
for ($i = 0; $i < $numTokens; ++$i) {
75-
$tokenCode = $this->tokens[$i]['code'];
76-
77-
//have we found a new entity or its end?
78-
if (in_array($tokenCode, $entityTypes) && $this->tokens[$i]['content'] !== 'enum') {
79-
$processingEntity = true;
80-
continue;
81-
} elseif ($tokenCode === T_CLOSE_CURLY_BRACKET) {
82-
$processingEntity = false;
83-
continue;
84-
}
85-
86-
//if we are processing an entity, are we currently seeing a field?
87-
if ($processingEntity && $this->isFieldToken($i)) {
88-
$this->tokens[$i]['code'] = T_VARIABLE;
89-
$this->tokens[$i]['type'] = 'T_VARIABLE';
90-
continue;
91-
}
92-
}
71+
$this->processFields();
9372

9473
$this->logVerbose('*** END ADDITIONAL GRAPHQL PROCESSING ***');
9574
}
@@ -203,18 +182,18 @@ private function isFieldToken($stackPointer)
203182
//if next token is an opening parenthesis, we seek for the closing parenthesis
204183
if ($nextToken['code'] === T_OPEN_PARENTHESIS) {
205184
$nextPointer = $stackPointer + 1;
206-
$numTokens = count($this->tokens);
185+
$numTokens = count($this->tokens);
207186

208-
for ($i=$nextPointer; $i<$numTokens; ++$i) {
187+
for ($i = $nextPointer; $i < $numTokens; ++$i) {
209188
if ($this->tokens[$i]['code'] === T_CLOSE_PARENTHESIS) {
210189
$nextToken = $this->tokens[$i + 1];
211190
break;
212191
}
213192
}
214193
}
215194

216-
//return whether next token is a colon
217-
return $nextToken['code'] === T_COLON;
195+
//return whether current token is a string and next token is a colon
196+
return $this->tokens[$stackPointer]['code'] === T_STRING && $nextToken['code'] === T_COLON;
218197
}
219198

220199
/**
@@ -229,4 +208,50 @@ private function logVerbose($message, $level = 1)
229208
printf("\t%s" . PHP_EOL, $message);
230209
}
231210
}
211+
212+
/**
213+
* Processes field tokens, setting their type to {@link T_VARIABLE}.
214+
*/
215+
private function processFields()
216+
{
217+
$processingArgumentList = false;
218+
$processingEntity = false;
219+
$numTokens = count($this->tokens);
220+
$entityTypes = [T_CLASS, T_INTERFACE];
221+
$skipTypes = [T_COMMENT, T_WHITESPACE];
222+
223+
for ($i = 0; $i < $numTokens; ++$i) {
224+
$tokenCode = $this->tokens[$i]['code'];
225+
226+
//process current token
227+
switch (true) {
228+
case in_array($tokenCode, $skipTypes):
229+
//we have hit a token that needs to be skipped -> NOP
230+
break;
231+
case in_array($tokenCode, $entityTypes) && $this->tokens[$i]['content'] !== 'enum':
232+
//we have hit an entity declaration
233+
$processingEntity = true;
234+
break;
235+
case $tokenCode === T_CLOSE_CURLY_BRACKET:
236+
//we have hit the end of an entity declaration
237+
$processingEntity = false;
238+
break;
239+
case $tokenCode === T_OPEN_PARENTHESIS:
240+
//we have hit an argument list
241+
$processingArgumentList = true;
242+
break;
243+
case $tokenCode === T_CLOSE_PARENTHESIS:
244+
//we have hit an argument list end
245+
$processingArgumentList = false;
246+
break;
247+
case $processingEntity && !$processingArgumentList && $this->isFieldToken($i):
248+
//we have hit a field
249+
$this->tokens[$i]['code'] = T_VARIABLE;
250+
$this->tokens[$i]['type'] = 'T_VARIABLE';
251+
break;
252+
default:
253+
//NOP All operations have already been executed
254+
}
255+
}
256+
}
232257
}

0 commit comments

Comments
 (0)