Skip to content

Commit 49f0b0b

Browse files
MC-19366: Fixes that agruments with directives would result in erroneous output and that arguments could be treated as fields
1 parent d99c922 commit 49f0b0b

File tree

4 files changed

+156
-67
lines changed

4 files changed

+156
-67
lines changed

Magento2/Sniffs/GraphQL/ValidArgumentNameSniff.php

Lines changed: 122 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,40 @@
66

77
namespace Magento2\Sniffs\GraphQL;
88

9+
use GraphQL\Error\SyntaxError;
10+
use GraphQL\Language\AST\DocumentNode;
911
use PHP_CodeSniffer\Files\File;
1012

1113
/**
1214
* Detects argument names that are not specified in <kbd>cameCase</kbd>.
1315
*/
1416
class ValidArgumentNameSniff extends AbstractGraphQLSniff
1517
{
18+
1619
/**
1720
* @inheritDoc
1821
*/
1922
public function register()
2023
{
21-
return [T_OPEN_PARENTHESIS];
24+
return [T_VARIABLE];
2225
}
2326

2427
/**
2528
* @inheritDoc
2629
*/
2730
public function process(File $phpcsFile, $stackPtr)
2831
{
29-
$tokens = $phpcsFile->getTokens();
30-
$closeParenthesisPointer = $this->getCloseParenthesisPointer($stackPtr, $tokens);
32+
$tokens = $phpcsFile->getTokens();
33+
34+
//get the pointer to the argument list opener or bail out if none was found since then the field does not have arguments
35+
$openArgumentListPointer = $this->getArgumentListOpenPointer($stackPtr, $tokens);
36+
if ($openArgumentListPointer === false) {
37+
return;
38+
}
3139

32-
//if we could not find the closing parenthesis pointer, we add a warning and terminate
33-
if ($closeParenthesisPointer === false) {
40+
//get the pointer to the argument list closer or add a warning and terminate as we have an unbalanced file
41+
$closeArgumentListPointer = $this->getArgumentListClosePointer($openArgumentListPointer, $tokens);
42+
if ($closeArgumentListPointer === false) {
3443
$error = 'Possible parse error: Missing closing parenthesis for argument list in line %d';
3544
$data = [
3645
$tokens[$stackPtr]['line'],
@@ -39,18 +48,15 @@ public function process(File $phpcsFile, $stackPtr)
3948
return;
4049
}
4150

42-
$arguments = $this->getArguments($stackPtr, $closeParenthesisPointer, $tokens);
51+
$arguments = $this->getArguments($openArgumentListPointer, $closeArgumentListPointer, $tokens);
4352

44-
foreach ($arguments as $argument) {
45-
$pointer = $argument[0];
46-
$name = $argument[1];
47-
48-
if (!$this->isCamelCase($name)) {
53+
foreach ($arguments as $pointer => $argument) {
54+
if (!$this->isCamelCase($argument)) {
4955
$type = 'Argument';
5056
$error = '%s name "%s" is not in CamelCase format';
5157
$data = [
5258
$type,
53-
$name,
59+
$argument,
5460
];
5561

5662
$phpcsFile->addError($error, $pointer, 'NotCamelCase', $data);
@@ -61,56 +67,131 @@ public function process(File $phpcsFile, $stackPtr)
6167
}
6268

6369
//return stack pointer of closing parenthesis
64-
return $closeParenthesisPointer;
70+
return $closeArgumentListPointer;
6571
}
6672

6773
/**
68-
* Finds all argument names contained in <var>$tokens</var> in range <var>$startPointer</var> to
69-
* <var>$endPointer</var>.
74+
* Seeks the last token of an argument definition and returns its pointer.
7075
*
71-
* @param int $startPointer
72-
* @param int $endPointer
76+
* Arguments are defined as follows:
77+
* <noformat>
78+
* {ArgumentName}: {ArgumentType}[ = {DefaultValue}][{Directive}]*
79+
* </noformat>
80+
*
81+
* @param int $argumentDefinitionStartPointer
7382
* @param array $tokens
74-
* @return array[]
83+
* @return int
7584
*/
76-
private function getArguments($startPointer, $endPointer, array $tokens)
85+
private function getArgumentDefinitionEndPointer($argumentDefinitionStartPointer, array $tokens)
7786
{
78-
$argumentTokenPointer = null;
79-
$argument = '';
80-
$names = [];
81-
$skipTypes = [T_COMMENT, T_WHITESPACE];
87+
$colonPointer = $this->seekToken(T_COLON, $tokens, $argumentDefinitionStartPointer);
8288

83-
for ($i = $startPointer + 1; $i < $endPointer; ++$i) {
84-
//skip some tokens
85-
if (in_array($tokens[$i]['code'], $skipTypes)) {
86-
continue;
87-
}
88-
$argument .= $tokens[$i]['content'];
89+
//the colon is always followed by a type so we can consume the token after the colon
90+
$endPointer = $colonPointer + 1;
8991

90-
if ($argumentTokenPointer === null) {
91-
$argumentTokenPointer = $i;
92-
}
92+
//if argument has a default value, we advance to the default definition end
93+
if ($tokens[$endPointer + 1]['code'] === T_EQUAL) {
94+
$endPointer += 2;
95+
}
96+
97+
//while next token starts a directive, we advance to the end of the directive
98+
while ($tokens[$endPointer + 1]['code'] === T_DOC_COMMENT_TAG) {
99+
//consume next two tokens
100+
$endPointer += 2;
93101

94-
if (preg_match('/^.+:.+$/', $argument)) {
95-
list($name, $type) = explode(':', $argument);
96-
$names[] = [$argumentTokenPointer, $name];
97-
$argument = '';
98-
$argumentTokenPointer = null;
102+
//if next token is an opening parenthesis, we consume everything up to the closing parenthesis
103+
if ($tokens[$endPointer + 1]['code'] === T_OPEN_PARENTHESIS) {
104+
$endPointer = $tokens[$endPointer + 1]['parenthesis_closer'];
99105
}
100106
}
101107

102-
return $names;
108+
return $endPointer;
109+
}
110+
111+
/**
112+
* Returns the closing parenthesis for the token found at <var>$openParenthesisPointer</var> in <var>$tokens</var>.
113+
*
114+
* @param int $openParenthesisPointer
115+
* @param array $tokens
116+
* @return bool|int
117+
*/
118+
private function getArgumentListClosePointer($openParenthesisPointer, array $tokens)
119+
{
120+
$openParenthesisToken = $tokens[$openParenthesisPointer];
121+
return $openParenthesisToken['parenthesis_closer'];
103122
}
104123

105124
/**
106-
* Seeks the next available token of type {@link T_CLOSE_PARENTHESIS} in <var>$tokens</var> and returns its pointer.
125+
* Seeks the next available {@link T_OPEN_PARENTHESIS} token that comes directly after <var>$stackPointer</var>.
126+
* token.
107127
*
108128
* @param int $stackPointer
109129
* @param array $tokens
110130
* @return bool|int
111131
*/
112-
private function getCloseParenthesisPointer($stackPointer, array $tokens)
132+
private function getArgumentListOpenPointer($stackPointer, array $tokens)
133+
{
134+
//get next open parenthesis pointer or bail out if none was found
135+
$openParenthesisPointer = $this->seekToken(T_OPEN_PARENTHESIS, $tokens, $stackPointer);
136+
if ($openParenthesisPointer === false) {
137+
return false;
138+
}
139+
140+
//bail out if open parenthesis does not directly come after current stack pointer
141+
if ($openParenthesisPointer !== $stackPointer + 1) {
142+
return false;
143+
}
144+
145+
//we have found the appropriate opening parenthesis
146+
return $openParenthesisPointer;
147+
}
148+
149+
/**
150+
* Finds all argument names contained in <var>$tokens</var> in range <var>$startPointer</var> to
151+
* <var>$endPointer</var>.
152+
*
153+
* The returned array uses token pointers as keys and argument names as values.
154+
*
155+
* @param int $startPointer
156+
* @param int $endPointer
157+
* @param array $tokens
158+
* @return array<int, string>
159+
*/
160+
private function getArguments($startPointer, $endPointer, array $tokens)
113161
{
114-
return $this->seekToken(T_CLOSE_PARENTHESIS, $tokens, $stackPointer);
162+
$argumentTokenPointer = null;
163+
$argument = '';
164+
$names = [];
165+
$skipTypes = [T_COMMENT, T_WHITESPACE];
166+
167+
for ($i = $startPointer + 1; $i < $endPointer; ++$i) {
168+
$tokenCode = $tokens[$i]['code'];
169+
170+
switch (true) {
171+
case in_array($tokenCode, $skipTypes):
172+
//NOP This is a toke that we have to skip
173+
break;
174+
case $tokenCode === T_COLON:
175+
//we have reached the end of the argument name, thus we store its pointer and value
176+
$names[$argumentTokenPointer] = $argument;
177+
178+
//advance to end of argument definition
179+
$i = $this->getArgumentDefinitionEndPointer($argumentTokenPointer, $tokens);
180+
181+
//and reset temporary variables
182+
$argument = '';
183+
$argumentTokenPointer = null;
184+
break;
185+
default:
186+
//this seems to be part of the argument name
187+
$argument .= $tokens[$i]['content'];
188+
189+
if ($argumentTokenPointer === null) {
190+
$argumentTokenPointer = $i;
191+
}
192+
}
193+
}
194+
195+
return $names;
115196
}
116197
}

Magento2/Tests/GraphQL/ValidArgumentNameUnitTest.graphqls

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,21 @@ interface Foo {
2929
invalid_argument_name_snake_case: Int
3030
5invalidArgumentNameStatingWithNumber: Int
3131
): String
32-
}
32+
}
33+
34+
#
35+
# Make sure that directives on arguments do not lead to false positives
36+
#
37+
type Foo @doc(descripton: "Foo Bar Baz") {
38+
myfield (
39+
# Valid arguments
40+
validArgument: String = "My fair lady" @doc(
41+
# A comment
42+
description: "This is a valid argument, spanned over multiple lines."
43+
) @foo
44+
varlidArgumentWithDefaultValue: Int = 20 @doc(description: "This is another valid argument with a default value.")
45+
# Invalid argument
46+
invalid_argument: String @doc(description: "This is an invalid argument."),
47+
invalid_argument_with_default_value: Int = 20 @doc(description: "This is another invalid argument with a default value")
48+
): String @doc(description: "Foo Bar")
49+
}

Magento2/Tests/GraphQL/ValidArgumentNameUnitTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ protected function getErrorList()
2424
28 => 1,
2525
29 => 1,
2626
30 => 1,
27+
46 => 1,
28+
47 => 1,
2729
];
2830
}
2931

PHP_CodeSniffer/Tokenizers/GRAPHQL.php

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
namespace PHP_CodeSniffer\Tokenizers;
88

99
use GraphQL\Language\Lexer;
10+
use GraphQL\Language\Parser;
1011
use GraphQL\Language\Source;
1112
use GraphQL\Language\Token;
1213

@@ -81,11 +82,10 @@ protected function tokenize($string)
8182
{
8283
$this->logVerbose('*** START GRAPHQL TOKENIZING ***');
8384

84-
$string = str_replace($this->eolChar, "\n", $string);
85-
$tokens = [];
86-
$lexer = new Lexer(
87-
new Source($string)
88-
);
85+
$string = str_replace($this->eolChar, "\n", $string);
86+
$tokens = [];
87+
$source = new Source($string);
88+
$lexer = new Lexer($source);
8989

9090
do {
9191
$kind = $lexer->token->kind;
@@ -178,19 +178,17 @@ private function getNewLineTokens($lineStart, $lineEnd)
178178
*/
179179
private function isFieldToken($stackPointer)
180180
{
181+
//bail out if current token is nested in a parenthesis, since fields cannot be contained in parenthesises
182+
if (isset($this->tokens[$stackPointer]['nested_parenthesis'])) {
183+
return false;
184+
}
185+
181186
$nextToken = $this->tokens[$stackPointer + 1];
182187

183-
//if next token is an opening parenthesis, we seek for the closing parenthesis
188+
//if next token is an opening parenthesis, we advance to the token after the closing parenthesis
184189
if ($nextToken['code'] === T_OPEN_PARENTHESIS) {
185-
$nextPointer = $stackPointer + 1;
186-
$numTokens = count($this->tokens);
187-
188-
for ($i = $nextPointer; $i < $numTokens; ++$i) {
189-
if ($this->tokens[$i]['code'] === T_CLOSE_PARENTHESIS) {
190-
$nextToken = $this->tokens[$i + 1];
191-
break;
192-
}
193-
}
190+
$nextPointer = $nextToken['parenthesis_closer'] + 1;
191+
$nextToken = $this->tokens[$nextPointer];
194192
}
195193

196194
//return whether current token is a string and next token is a colon
@@ -215,7 +213,6 @@ private function logVerbose($message, $level = 1)
215213
*/
216214
private function processFields()
217215
{
218-
$processingArgumentList = false;
219216
$processingEntity = false;
220217
$numTokens = count($this->tokens);
221218
$entityTypes = [T_CLASS, T_INTERFACE];
@@ -237,15 +234,7 @@ private function processFields()
237234
//we have hit the end of an entity declaration
238235
$processingEntity = false;
239236
break;
240-
case $tokenCode === T_OPEN_PARENTHESIS:
241-
//we have hit an argument list
242-
$processingArgumentList = true;
243-
break;
244-
case $tokenCode === T_CLOSE_PARENTHESIS:
245-
//we have hit an argument list end
246-
$processingArgumentList = false;
247-
break;
248-
case $processingEntity && !$processingArgumentList && $this->isFieldToken($i):
237+
case $processingEntity && $this->isFieldToken($i):
249238
//we have hit a field
250239
$this->tokens[$i]['code'] = T_VARIABLE;
251240
$this->tokens[$i]['type'] = 'T_VARIABLE';

0 commit comments

Comments
 (0)