Skip to content

Commit 7813049

Browse files
authored
Merge pull request #141 from magento/MC-19366-GraphQL-Code-Style-Test
MC-19366: Adds GraphQL sniffs
2 parents 24607be + e252b19 commit 7813049

21 files changed

+1393
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
/**
3+
* Copyright © Magento. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
namespace Magento2\Sniffs\GraphQL;
7+
8+
use PHP_CodeSniffer\Sniffs\Sniff;
9+
10+
/**
11+
* Defines an abstract base class for GraphQL sniffs.
12+
*/
13+
abstract class AbstractGraphQLSniff implements Sniff
14+
{
15+
/**
16+
* Defines the tokenizers that this sniff is using.
17+
*
18+
* @var array
19+
*/
20+
public $supportedTokenizers = ['GRAPHQL'];
21+
22+
/**
23+
* Returns whether <var>$name</var> starts with a lower case character and is written in camel case.
24+
*
25+
* @param string $name
26+
* @return bool
27+
*/
28+
protected function isCamelCase($name)
29+
{
30+
return (preg_match('/^[a-z][a-zA-Z0-9]+$/', $name) !== 0);
31+
}
32+
33+
/**
34+
* Returns whether <var>$name</var> is specified in snake case (either all lower case or all upper case).
35+
*
36+
* @param string $name
37+
* @param bool $upperCase If set to <kbd>true</kbd> checks for all upper case, otherwise all lower case
38+
* @return bool
39+
*/
40+
protected function isSnakeCase($name, $upperCase = false)
41+
{
42+
$pattern = $upperCase ? '/^[A-Z][A-Z0-9_]*$/' : '/^[a-z][a-z0-9_]*$/';
43+
return preg_match($pattern, $name);
44+
}
45+
46+
/**
47+
* Returns the pointer to the last token of a directive if the token at <var>$startPointer</var> starts a directive.
48+
*
49+
* @param array $tokens
50+
* @param int $startPointer
51+
* @return int The end of the directive if one is found, the start pointer otherwise
52+
*/
53+
protected function seekEndOfDirective(array $tokens, $startPointer)
54+
{
55+
$endPointer = $startPointer;
56+
57+
if ($tokens[$startPointer]['code'] === T_DOC_COMMENT_TAG) {
58+
//advance to next token
59+
++$endPointer;
60+
61+
//if next token is an opening parenthesis, we consume everything up to the closing parenthesis
62+
if ($tokens[$endPointer + 1]['code'] === T_OPEN_PARENTHESIS) {
63+
$endPointer = $tokens[$endPointer + 1]['parenthesis_closer'];
64+
}
65+
}
66+
67+
return $endPointer;
68+
}
69+
70+
/**
71+
* Searches for the first token that has <var>$tokenCode</var> in <var>$tokens</var> from position
72+
* <var>$startPointer</var> (excluded).
73+
*
74+
* @param mixed $tokenCode
75+
* @param array $tokens
76+
* @param int $startPointer
77+
* @return bool|int If token was found, returns its pointer, <kbd>false</kbd> otherwise
78+
*/
79+
protected function seekToken($tokenCode, array $tokens, $startPointer = 0)
80+
{
81+
$numTokens = count($tokens);
82+
83+
for ($i = $startPointer + 1; $i < $numTokens; ++$i) {
84+
if ($tokens[$i]['code'] === $tokenCode) {
85+
return $i;
86+
}
87+
}
88+
89+
//if we came here we could not find the requested token
90+
return false;
91+
}
92+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
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 GraphQL\Error\SyntaxError;
10+
use GraphQL\Language\AST\DocumentNode;
11+
use PHP_CodeSniffer\Files\File;
12+
13+
/**
14+
* Detects argument names that are not specified in <kbd>cameCase</kbd>.
15+
*/
16+
class ValidArgumentNameSniff extends AbstractGraphQLSniff
17+
{
18+
19+
/**
20+
* @inheritDoc
21+
*/
22+
public function register()
23+
{
24+
return [T_VARIABLE];
25+
}
26+
27+
/**
28+
* @inheritDoc
29+
*/
30+
public function process(File $phpcsFile, $stackPtr)
31+
{
32+
$tokens = $phpcsFile->getTokens();
33+
34+
//get the pointer to the argument list opener or bail out if none was found
35+
//since then the field does not have arguments
36+
$openArgumentListPointer = $this->getArgumentListOpenPointer($stackPtr, $tokens);
37+
if ($openArgumentListPointer === false) {
38+
return;
39+
}
40+
41+
//get the pointer to the argument list closer or add a warning and terminate as we have an unbalanced file
42+
$closeArgumentListPointer = $this->getArgumentListClosePointer($openArgumentListPointer, $tokens);
43+
if ($closeArgumentListPointer === false) {
44+
$error = 'Possible parse error: Missing closing parenthesis for argument list in line %d';
45+
$data = [
46+
$tokens[$stackPtr]['line'],
47+
];
48+
$phpcsFile->addWarning($error, $stackPtr, 'UnclosedArgumentList', $data);
49+
return;
50+
}
51+
52+
$arguments = $this->getArguments($openArgumentListPointer, $closeArgumentListPointer, $tokens);
53+
54+
foreach ($arguments as $pointer => $argument) {
55+
if (!$this->isCamelCase($argument)) {
56+
$type = 'Argument';
57+
$error = '%s name "%s" is not in CamelCase format';
58+
$data = [
59+
$type,
60+
$argument,
61+
];
62+
63+
$phpcsFile->addError($error, $pointer, 'NotCamelCase', $data);
64+
$phpcsFile->recordMetric($pointer, 'CamelCase argument name', 'no');
65+
} else {
66+
$phpcsFile->recordMetric($pointer, 'CamelCase argument name', 'yes');
67+
}
68+
}
69+
70+
//return stack pointer of closing parenthesis
71+
return $closeArgumentListPointer;
72+
}
73+
74+
/**
75+
* Seeks the last token of an argument definition and returns its pointer.
76+
*
77+
* Arguments are defined as follows:
78+
* <pre>
79+
* {ArgumentName}: {ArgumentType}[ = {DefaultValue}][{Directive}]*
80+
* </pre>
81+
*
82+
* @param int $argumentDefinitionStartPointer
83+
* @param array $tokens
84+
* @return int
85+
*/
86+
private function getArgumentDefinitionEndPointer($argumentDefinitionStartPointer, array $tokens)
87+
{
88+
$endPointer = $this->seekToken(T_COLON, $tokens, $argumentDefinitionStartPointer);
89+
90+
//the colon is always followed by the type, which we can consume. it could be a list type though, thus we check
91+
if ($tokens[$endPointer + 1]['code'] === T_OPEN_SQUARE_BRACKET) {
92+
//consume everything up to closing bracket
93+
$endPointer = $tokens[$endPointer + 1]['bracket_closer'];
94+
} else {
95+
//consume everything up to type
96+
++$endPointer;
97+
}
98+
99+
//the type may be non null, meaning that it is followed by an exclamation mark, which we consume
100+
if ($tokens[$endPointer + 1]['code'] === T_BOOLEAN_NOT) {
101+
++$endPointer;
102+
}
103+
104+
//if argument has a default value, we advance to the default definition end
105+
if ($tokens[$endPointer + 1]['code'] === T_EQUAL) {
106+
$endPointer += 2;
107+
}
108+
109+
//while next token starts a directive, we advance to the end of the directive
110+
while ($tokens[$endPointer + 1]['code'] === T_DOC_COMMENT_TAG) {
111+
$endPointer = $this->seekEndOfDirective($tokens, $endPointer + 1);
112+
}
113+
114+
return $endPointer;
115+
}
116+
117+
/**
118+
* Returns the closing parenthesis for the token found at <var>$openParenthesisPointer</var> in <var>$tokens</var>.
119+
*
120+
* @param int $openParenthesisPointer
121+
* @param array $tokens
122+
* @return bool|int
123+
*/
124+
private function getArgumentListClosePointer($openParenthesisPointer, array $tokens)
125+
{
126+
$openParenthesisToken = $tokens[$openParenthesisPointer];
127+
return $openParenthesisToken['parenthesis_closer'];
128+
}
129+
130+
/**
131+
* Seeks the next available {@link T_OPEN_PARENTHESIS} token that comes directly after <var>$stackPointer</var>.
132+
* token.
133+
*
134+
* @param int $stackPointer
135+
* @param array $tokens
136+
* @return bool|int
137+
*/
138+
private function getArgumentListOpenPointer($stackPointer, array $tokens)
139+
{
140+
//get next open parenthesis pointer or bail out if none was found
141+
$openParenthesisPointer = $this->seekToken(T_OPEN_PARENTHESIS, $tokens, $stackPointer);
142+
if ($openParenthesisPointer === false) {
143+
return false;
144+
}
145+
146+
//bail out if open parenthesis does not directly come after current stack pointer
147+
if ($openParenthesisPointer !== $stackPointer + 1) {
148+
return false;
149+
}
150+
151+
//we have found the appropriate opening parenthesis
152+
return $openParenthesisPointer;
153+
}
154+
155+
/**
156+
* Finds all argument names contained in <var>$tokens</var> in range <var>$startPointer</var> to
157+
* <var>$endPointer</var>.
158+
*
159+
* The returned array uses token pointers as keys and argument names as values.
160+
*
161+
* @param int $startPointer
162+
* @param int $endPointer
163+
* @param array $tokens
164+
* @return array<int, string>
165+
*/
166+
private function getArguments($startPointer, $endPointer, array $tokens)
167+
{
168+
$argumentTokenPointer = null;
169+
$argument = '';
170+
$names = [];
171+
$skipTypes = [T_COMMENT, T_WHITESPACE];
172+
173+
for ($i = $startPointer + 1; $i < $endPointer; ++$i) {
174+
$tokenCode = $tokens[$i]['code'];
175+
176+
switch (true) {
177+
case in_array($tokenCode, $skipTypes):
178+
//NOP This is a token that we have to skip
179+
break;
180+
case $tokenCode === T_COLON:
181+
//we have reached the end of the argument name, thus we store its pointer and value
182+
$names[$argumentTokenPointer] = $argument;
183+
184+
//advance to end of argument definition
185+
$i = $this->getArgumentDefinitionEndPointer($argumentTokenPointer, $tokens);
186+
187+
//and reset temporary variables
188+
$argument = '';
189+
$argumentTokenPointer = null;
190+
break;
191+
default:
192+
//this seems to be part of the argument name
193+
$argument .= $tokens[$i]['content'];
194+
195+
if ($argumentTokenPointer === null) {
196+
$argumentTokenPointer = $i;
197+
}
198+
}
199+
}
200+
201+
return $names;
202+
}
203+
}

0 commit comments

Comments
 (0)