diff --git a/Magento2/Sniffs/GraphQL/AbstractGraphQLSniff.php b/Magento2/Sniffs/GraphQL/AbstractGraphQLSniff.php new file mode 100644 index 00000000..2c0448f0 --- /dev/null +++ b/Magento2/Sniffs/GraphQL/AbstractGraphQLSniff.php @@ -0,0 +1,92 @@ +$name starts with a lower case character and is written in camel case. + * + * @param string $name + * @return bool + */ + protected function isCamelCase($name) + { + return (preg_match('/^[a-z][a-zA-Z0-9]+$/', $name) !== 0); + } + + /** + * Returns whether $name is specified in snake case (either all lower case or all upper case). + * + * @param string $name + * @param bool $upperCase If set to true checks for all upper case, otherwise all lower case + * @return bool + */ + protected function isSnakeCase($name, $upperCase = false) + { + $pattern = $upperCase ? '/^[A-Z][A-Z0-9_]*$/' : '/^[a-z][a-z0-9_]*$/'; + return preg_match($pattern, $name); + } + + /** + * Returns the pointer to the last token of a directive if the token at $startPointer starts a directive. + * + * @param array $tokens + * @param int $startPointer + * @return int The end of the directive if one is found, the start pointer otherwise + */ + protected function seekEndOfDirective(array $tokens, $startPointer) + { + $endPointer = $startPointer; + + if ($tokens[$startPointer]['code'] === T_DOC_COMMENT_TAG) { + //advance to next token + ++$endPointer; + + //if next token is an opening parenthesis, we consume everything up to the closing parenthesis + if ($tokens[$endPointer + 1]['code'] === T_OPEN_PARENTHESIS) { + $endPointer = $tokens[$endPointer + 1]['parenthesis_closer']; + } + } + + return $endPointer; + } + + /** + * Searches for the first token that has $tokenCode in $tokens from position + * $startPointer (excluded). + * + * @param mixed $tokenCode + * @param array $tokens + * @param int $startPointer + * @return bool|int If token was found, returns its pointer, false otherwise + */ + protected function seekToken($tokenCode, array $tokens, $startPointer = 0) + { + $numTokens = count($tokens); + + for ($i = $startPointer + 1; $i < $numTokens; ++$i) { + if ($tokens[$i]['code'] === $tokenCode) { + return $i; + } + } + + //if we came here we could not find the requested token + return false; + } +} diff --git a/Magento2/Sniffs/GraphQL/ValidArgumentNameSniff.php b/Magento2/Sniffs/GraphQL/ValidArgumentNameSniff.php new file mode 100644 index 00000000..d7a19621 --- /dev/null +++ b/Magento2/Sniffs/GraphQL/ValidArgumentNameSniff.php @@ -0,0 +1,203 @@ +cameCase. + */ +class ValidArgumentNameSniff extends AbstractGraphQLSniff +{ + + /** + * @inheritDoc + */ + public function register() + { + return [T_VARIABLE]; + } + + /** + * @inheritDoc + */ + public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + //get the pointer to the argument list opener or bail out if none was found + //since then the field does not have arguments + $openArgumentListPointer = $this->getArgumentListOpenPointer($stackPtr, $tokens); + if ($openArgumentListPointer === false) { + return; + } + + //get the pointer to the argument list closer or add a warning and terminate as we have an unbalanced file + $closeArgumentListPointer = $this->getArgumentListClosePointer($openArgumentListPointer, $tokens); + if ($closeArgumentListPointer === false) { + $error = 'Possible parse error: Missing closing parenthesis for argument list in line %d'; + $data = [ + $tokens[$stackPtr]['line'], + ]; + $phpcsFile->addWarning($error, $stackPtr, 'UnclosedArgumentList', $data); + return; + } + + $arguments = $this->getArguments($openArgumentListPointer, $closeArgumentListPointer, $tokens); + + foreach ($arguments as $pointer => $argument) { + if (!$this->isCamelCase($argument)) { + $type = 'Argument'; + $error = '%s name "%s" is not in CamelCase format'; + $data = [ + $type, + $argument, + ]; + + $phpcsFile->addError($error, $pointer, 'NotCamelCase', $data); + $phpcsFile->recordMetric($pointer, 'CamelCase argument name', 'no'); + } else { + $phpcsFile->recordMetric($pointer, 'CamelCase argument name', 'yes'); + } + } + + //return stack pointer of closing parenthesis + return $closeArgumentListPointer; + } + + /** + * Seeks the last token of an argument definition and returns its pointer. + * + * Arguments are defined as follows: + *
+     *   {ArgumentName}: {ArgumentType}[ = {DefaultValue}][{Directive}]*
+     * 
+ * + * @param int $argumentDefinitionStartPointer + * @param array $tokens + * @return int + */ + private function getArgumentDefinitionEndPointer($argumentDefinitionStartPointer, array $tokens) + { + $endPointer = $this->seekToken(T_COLON, $tokens, $argumentDefinitionStartPointer); + + //the colon is always followed by the type, which we can consume. it could be a list type though, thus we check + if ($tokens[$endPointer + 1]['code'] === T_OPEN_SQUARE_BRACKET) { + //consume everything up to closing bracket + $endPointer = $tokens[$endPointer + 1]['bracket_closer']; + } else { + //consume everything up to type + ++$endPointer; + } + + //the type may be non null, meaning that it is followed by an exclamation mark, which we consume + if ($tokens[$endPointer + 1]['code'] === T_BOOLEAN_NOT) { + ++$endPointer; + } + + //if argument has a default value, we advance to the default definition end + if ($tokens[$endPointer + 1]['code'] === T_EQUAL) { + $endPointer += 2; + } + + //while next token starts a directive, we advance to the end of the directive + while ($tokens[$endPointer + 1]['code'] === T_DOC_COMMENT_TAG) { + $endPointer = $this->seekEndOfDirective($tokens, $endPointer + 1); + } + + return $endPointer; + } + + /** + * Returns the closing parenthesis for the token found at $openParenthesisPointer in $tokens. + * + * @param int $openParenthesisPointer + * @param array $tokens + * @return bool|int + */ + private function getArgumentListClosePointer($openParenthesisPointer, array $tokens) + { + $openParenthesisToken = $tokens[$openParenthesisPointer]; + return $openParenthesisToken['parenthesis_closer']; + } + + /** + * Seeks the next available {@link T_OPEN_PARENTHESIS} token that comes directly after $stackPointer. + * token. + * + * @param int $stackPointer + * @param array $tokens + * @return bool|int + */ + private function getArgumentListOpenPointer($stackPointer, array $tokens) + { + //get next open parenthesis pointer or bail out if none was found + $openParenthesisPointer = $this->seekToken(T_OPEN_PARENTHESIS, $tokens, $stackPointer); + if ($openParenthesisPointer === false) { + return false; + } + + //bail out if open parenthesis does not directly come after current stack pointer + if ($openParenthesisPointer !== $stackPointer + 1) { + return false; + } + + //we have found the appropriate opening parenthesis + return $openParenthesisPointer; + } + + /** + * Finds all argument names contained in $tokens in range $startPointer to + * $endPointer. + * + * The returned array uses token pointers as keys and argument names as values. + * + * @param int $startPointer + * @param int $endPointer + * @param array $tokens + * @return array + */ + private function getArguments($startPointer, $endPointer, array $tokens) + { + $argumentTokenPointer = null; + $argument = ''; + $names = []; + $skipTypes = [T_COMMENT, T_WHITESPACE]; + + for ($i = $startPointer + 1; $i < $endPointer; ++$i) { + $tokenCode = $tokens[$i]['code']; + + switch (true) { + case in_array($tokenCode, $skipTypes): + //NOP This is a token that we have to skip + break; + case $tokenCode === T_COLON: + //we have reached the end of the argument name, thus we store its pointer and value + $names[$argumentTokenPointer] = $argument; + + //advance to end of argument definition + $i = $this->getArgumentDefinitionEndPointer($argumentTokenPointer, $tokens); + + //and reset temporary variables + $argument = ''; + $argumentTokenPointer = null; + break; + default: + //this seems to be part of the argument name + $argument .= $tokens[$i]['content']; + + if ($argumentTokenPointer === null) { + $argumentTokenPointer = $i; + } + } + } + + return $names; + } +} diff --git a/Magento2/Sniffs/GraphQL/ValidEnumValueSniff.php b/Magento2/Sniffs/GraphQL/ValidEnumValueSniff.php new file mode 100644 index 00000000..a9c5f245 --- /dev/null +++ b/Magento2/Sniffs/GraphQL/ValidEnumValueSniff.php @@ -0,0 +1,146 @@ +SCREAMING_SNAKE_CASE. + */ +class ValidEnumValueSniff extends AbstractGraphQLSniff +{ + + /** + * @inheritDoc + */ + public function register() + { + return [T_CLASS]; + } + + /** + * @inheritDoc + */ + public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + //bail out if we're not inspecting an enum + if ($tokens[$stackPtr]['content'] !== 'enum') { + return; + } + + $openingCurlyPointer = $this->getOpeningCurlyBracketPointer($stackPtr, $tokens); + $closingCurlyPointer = $this->getClosingCurlyBracketPointer($stackPtr, $tokens); + + //if we could not find the closing curly bracket pointer, we add a warning and terminate + if ($openingCurlyPointer === false || $closingCurlyPointer === false) { + $error = 'Possible parse error: %s missing opening or closing brace'; + $data = [$tokens[$stackPtr]['content']]; + $phpcsFile->addWarning($error, $stackPtr, 'MissingBrace', $data); + return; + } + + $values = $this->getValues($openingCurlyPointer, $closingCurlyPointer, $tokens, $phpcsFile->eolChar); + + foreach ($values as $pointer => $value) { + + if (!$this->isSnakeCase($value, true)) { + $type = 'Enum value'; + $error = '%s "%s" is not in SCREAMING_SNAKE_CASE format'; + $data = [ + $type, + $value, + ]; + + $phpcsFile->addError($error, $pointer, 'NotScreamingSnakeCase', $data); + $phpcsFile->recordMetric($pointer, 'SCREAMING_SNAKE_CASE enum value', 'no'); + } else { + $phpcsFile->recordMetric($pointer, 'SCREAMING_SNAKE_CASE enum value', 'yes'); + } + } + + return $closingCurlyPointer; + } + + /** + * Seeks the next available token of type {@link T_CLOSE_CURLY_BRACKET} in $tokens and returns its + * pointer. + * + * @param int $startPointer + * @param array $tokens + * @return bool|int + */ + private function getClosingCurlyBracketPointer($startPointer, array $tokens) + { + return $this->seekToken(T_CLOSE_CURLY_BRACKET, $tokens, $startPointer); + } + + /** + * Seeks the next available token of type {@link T_OPEN_CURLY_BRACKET} in $tokens and returns its + * pointer. + * + * @param $startPointer + * @param array $tokens + * @return bool|int + */ + private function getOpeningCurlyBracketPointer($startPointer, array $tokens) + { + return $this->seekToken(T_OPEN_CURLY_BRACKET, $tokens, $startPointer); + } + + /** + * Finds all enum values contained in $tokens in range $startPointer to + * $endPointer. + * + * The returned array uses token pointers as keys and value names as values. + * + * @param int $startPointer + * @param int $endPointer + * @param array $tokens + * @param string $eolChar + * @return array + */ + private function getValues($startPointer, $endPointer, array $tokens, $eolChar) + { + $valueTokenPointer = null; + $enumValue = ''; + $values = []; + $skipTypes = [T_COMMENT, T_WHITESPACE]; + + for ($i = $startPointer + 1; $i < $endPointer; ++$i) { + if (in_array($tokens[$i]['code'], $skipTypes)) { + //NOP This is a token that we have to skip + continue; + } + + //add current tokens content to enum value if we have a string + if ($tokens[$i]['code'] === T_STRING) { + $enumValue .= $tokens[$i]['content']; + + //and store the pointer if we have not done it already + if ($valueTokenPointer === null) { + $valueTokenPointer = $i; + } + } + + //consume directive if we have found one + if ($tokens[$i]['code'] === T_DOC_COMMENT_TAG) { + $i = $this->seekEndOfDirective($tokens, $i); + } + + //if current token has a line break, we have found the end of the value definition + if (strpos($tokens[$i]['content'], $eolChar) !== false) { + $values[$valueTokenPointer] = trim($enumValue); + $enumValue = ''; + $valueTokenPointer = null; + } + } + + return $values; + } +} diff --git a/Magento2/Sniffs/GraphQL/ValidFieldNameSniff.php b/Magento2/Sniffs/GraphQL/ValidFieldNameSniff.php new file mode 100644 index 00000000..f638c228 --- /dev/null +++ b/Magento2/Sniffs/GraphQL/ValidFieldNameSniff.php @@ -0,0 +1,44 @@ +snake_case. + */ +class ValidFieldNameSniff extends AbstractGraphQLSniff +{ + /** + * @inheritDoc + */ + public function register() + { + return [T_VARIABLE]; + } + + /** + * @inheritDoc + */ + public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + $name = $tokens[$stackPtr]['content']; + + if (!$this->isSnakeCase($name)) { + $type = 'Field'; + $error = '%s name "%s" is not in snake_case format'; + $data = [ + $type, + $name, + ]; + $phpcsFile->addError($error, $stackPtr, 'NotSnakeCase', $data); + $phpcsFile->recordMetric($stackPtr, 'SnakeCase field name', 'no'); + } else { + $phpcsFile->recordMetric($stackPtr, 'SnakeCase field name', 'yes'); + } + } +} diff --git a/Magento2/Sniffs/GraphQL/ValidTopLevelFieldNameSniff.php b/Magento2/Sniffs/GraphQL/ValidTopLevelFieldNameSniff.php new file mode 100644 index 00000000..eacbc10d --- /dev/null +++ b/Magento2/Sniffs/GraphQL/ValidTopLevelFieldNameSniff.php @@ -0,0 +1,49 @@ +cameCase. + */ +class ValidTopLevelFieldNameSniff extends AbstractGraphQLSniff +{ + /** + * @inheritDoc + */ + public function register() + { + return [T_FUNCTION]; + } + + /** + * @inheritDoc + */ + public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + //compose function name by making use of the next strings that we find until we hit a non-string token + $name = ''; + for ($i=$stackPtr+1; $tokens[$i]['code'] === T_STRING; ++$i) { + $name .= $tokens[$i]['content']; + } + + if (strlen($name) > 0 && !$this->isCamelCase($name)) { + $type = ucfirst($tokens[$stackPtr]['content']); + $error = '%s name "%s" is not in PascalCase format'; + $data = [ + $type, + $name, + ]; + $phpcsFile->addError($error, $stackPtr, 'NotCamelCase', $data); + $phpcsFile->recordMetric($stackPtr, 'CamelCase top level field name', 'no'); + } else { + $phpcsFile->recordMetric($stackPtr, 'CamelCase top level field name', 'yes'); + } + } +} diff --git a/Magento2/Sniffs/GraphQL/ValidTypeNameSniff.php b/Magento2/Sniffs/GraphQL/ValidTypeNameSniff.php new file mode 100644 index 00000000..b80ada40 --- /dev/null +++ b/Magento2/Sniffs/GraphQL/ValidTypeNameSniff.php @@ -0,0 +1,53 @@ +type, interface and enum) that are not specified in + * UpperCamelCase. + */ +class ValidTypeNameSniff extends AbstractGraphQLSniff +{ + /** + * @inheritDoc + */ + public function register() + { + return [T_CLASS, T_INTERFACE]; + } + + /** + * @inheritDoc + */ + public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + //compose entity name by making use of the next strings that we find until we hit a non-string token + $name = ''; + for ($i=$stackPtr+1; $tokens[$i]['code'] === T_STRING; ++$i) { + $name .= $tokens[$i]['content']; + } + + $valid = Common::isCamelCaps($name, true, true, false); + + if ($valid === false) { + $type = ucfirst($tokens[$stackPtr]['content']); + $error = '%s name "%s" is not in PascalCase format'; + $data = [ + $type, + $name, + ]; + $phpcsFile->addError($error, $stackPtr, 'NotCamelCaps', $data); + $phpcsFile->recordMetric($stackPtr, 'PascalCase class name', 'no'); + } else { + $phpcsFile->recordMetric($stackPtr, 'PascalCase class name', 'yes'); + } + } +} diff --git a/Magento2/Tests/GraphQL/AbstractGraphQLSniffUnitTestCase.php b/Magento2/Tests/GraphQL/AbstractGraphQLSniffUnitTestCase.php new file mode 100644 index 00000000..19bcd586 --- /dev/null +++ b/Magento2/Tests/GraphQL/AbstractGraphQLSniffUnitTestCase.php @@ -0,0 +1,33 @@ +extensions = array_merge( + $config->extensions, + [ + 'graphqls' => 'GRAPHQL' + ] + ); + + //and write back to a global that is used in base class + $GLOBALS['PHP_CODESNIFFER_CONFIG'] = $config; + } +} diff --git a/Magento2/Tests/GraphQL/ValidArgumentNameUnitTest.graphqls b/Magento2/Tests/GraphQL/ValidArgumentNameUnitTest.graphqls new file mode 100644 index 00000000..e2128c9b --- /dev/null +++ b/Magento2/Tests/GraphQL/ValidArgumentNameUnitTest.graphqls @@ -0,0 +1,59 @@ +type Foo { + bar( + # Valid argument names + validArgumentName: Int + validArgumentNameWith1Number: Int + validArgumentNameWith12345Numbers: Int + validArgumentNameEndingWithNumber5: Int + validArgumentNameWithUPPERCASE: Int + validargumentwithlowercase: Int + # Invalid argument names + InvalidArgumentNameUpperCamelCase: Int + INVALIDARGUMENTNAMEUPPERCASE: Int + invalid_argument_name_snake_case: Int + 5invalidArgumentNameStatingWithNumber: Int + ): String +} +interface Foo { + bar( + # Valid argument names + validArgumentName: Int + validArgumentNameWith1Number: Int + validArgumentNameWith12345Numbers: Int + validArgumentNameEndingWithNumber5: Int + validArgumentNameWithUPPERCASE: Int + validargumentwithlowercase: Int + # Invalid argument names + InvalidArgumentNameUpperCamelCase: Int + INVALIDARGUMENTNAMEUPPERCASE: Int + invalid_argument_name_snake_case: Int + 5invalidArgumentNameStatingWithNumber: Int + ): String +} + +# +# Make sure that directives on arguments do not lead to false positives +# +type Foo @doc(descripton: "Foo Bar Baz") { + myfield ( + # Valid arguments + validArgument: String = "My fair lady" @doc( + # A comment + description: "This is a valid argument, spanned over multiple lines." + ) @unparametrizedDirective + validArgumentWithDefaultValue: Int = 20 @doc(description: "This is another valid argument with a default value.") + validArgumentListType: [String] @doc(description: "This is a valid argument that uses a list type.") + validArgumentNonNullType: String! @doc(description: "This is a valid argument that uses a non null type.") + validArgumentNonNullListType: [String]! @doc(description: "This is a valid argument that uses a non null type.") + validArgumentNonNullTypeInListType: [String!] @doc(description: "This is a valid argument that uses a non null type within a list.") + validArgumentNonNullTypeInNonNullListType: [String!]! @doc(description: "This is a valid argument that uses a non null type within a non null list type.") + # Invalid argument + invalid_argument: String @doc(description: "This is an invalid argument."), + invalid_argument_with_default_value: Int = 20 @doc(description: "This is another invalid argument with a default value") + invalid_argument_list_type: [String] @doc(description: "This is an invalid argument that uses a list type.") + invalid_argument_non_null_type: String! @doc(description: "This is an invalid argument that uses a non null type.") + invalid_argument_non_null_list_type: [String]! @doc(description: "This is an invalid argument that uses a non null type.") + invalid_argument_non_null_type_in_list_type: [String!] @doc(description: "This is an invalid argument that uses a non null type within a list.") + invalid_argument_non_null_type_in_non_null_list_type: [String!]! @doc(description: "This is a valid argument that uses a non null type within a non null list type.") + ): String @doc(description: "Foo Bar") +} diff --git a/Magento2/Tests/GraphQL/ValidArgumentNameUnitTest.php b/Magento2/Tests/GraphQL/ValidArgumentNameUnitTest.php new file mode 100644 index 00000000..c0f41e5b --- /dev/null +++ b/Magento2/Tests/GraphQL/ValidArgumentNameUnitTest.php @@ -0,0 +1,44 @@ + 1, + 12 => 1, + 13 => 1, + 14 => 1, + 27 => 1, + 28 => 1, + 29 => 1, + 30 => 1, + 51 => 1, + 52 => 1, + 53 => 1, + 54 => 1, + 55 => 1, + 56 => 1, + 57 => 1, + ]; + } + + /** + * @inheritDoc + */ + protected function getWarningList() + { + return []; + } +} diff --git a/Magento2/Tests/GraphQL/ValidEnumValueUnitTest.graphqls b/Magento2/Tests/GraphQL/ValidEnumValueUnitTest.graphqls new file mode 100644 index 00000000..5f70754a --- /dev/null +++ b/Magento2/Tests/GraphQL/ValidEnumValueUnitTest.graphqls @@ -0,0 +1,27 @@ +enum Foo { + # Valid values + VALID_SCREAMING_SNAKE_CASE_VALUE + VALID_SCREAMING_SNAKE_CASE_VALUE_WITH_1_NUMBER + VALID_SCREAMING_SNAKE_CASE_VALUE_WITH_12345_NUMBERS + VALID_SCREAMING_SNAKE_CASE_VALUE_ENDING_WITH_NUMBER_5 + VALIDUPPERCASEVALUE + VALID_SCREMING_CASE_VALUE_WITH_DIRECTIVE @doc(description: "This is a valid enum value with a directive") + VALID_SCREMING_CASE_VALUE_WITH_TWO_DIRECTIVES @doc( + description: "This is a valid enum value with a directive" + ) @unparametrizedDirective + + # Invalid values + 1_INVALID_SCREAMING_SNAKE_CASE_VALUE_STARTING_WITH_NUMBER + invalidCamelCaseValue + InvalidCamelCaseCapsValue + invalidlowercasevalue + invalidCamelCaseValueWithDirective @doc(description: "This is an invalid enum value with a directive") + invalidCamelCaseValueWithTwoDirectives @doc( + description: "This is an invalid enum value with a directive" + ) @unparametrizedDirective +} + +# Ignored although it triggers a T_CLASS token +type Bar { + some_field: String +} \ No newline at end of file diff --git a/Magento2/Tests/GraphQL/ValidEnumValueUnitTest.php b/Magento2/Tests/GraphQL/ValidEnumValueUnitTest.php new file mode 100644 index 00000000..57e6ffed --- /dev/null +++ b/Magento2/Tests/GraphQL/ValidEnumValueUnitTest.php @@ -0,0 +1,35 @@ + 1, + 15 => 1, + 16 => 1, + 17 => 1, + 18 => 1, + 19 => 1, + ]; + } + + /** + * @inheritDoc + */ + protected function getWarningList() + { + return []; + } +} diff --git a/Magento2/Tests/GraphQL/ValidFieldNameUnitTest.graphqls b/Magento2/Tests/GraphQL/ValidFieldNameUnitTest.graphqls new file mode 100644 index 00000000..54b496ac --- /dev/null +++ b/Magento2/Tests/GraphQL/ValidFieldNameUnitTest.graphqls @@ -0,0 +1,31 @@ +type FooType { + # Valid snake case field names + valid_snake_case_field: String + validlowercasefield: String + valid_snake_case_field_ending_with_number_5: String + valid_snake_case_field_with_ending_numbers_12345: String + valid_snake_case_field_5_with_number5_inline: String + + # Incorrect use of snake case + INVALIDUPPERCASEFIELD: String + INVALID_SCREAMING_SNAKE_CASE_FIELD: String + Invalid_Upper_Snake_Case_Field: String + invalid_mixed_case_FIELD: String + InvalidCameCaseFieldName: String +} + +interface FooInterface { + # Valid snake case field names + valid_snake_case_field: String + validlowercasefield: String + valid_snake_case_field_ending_with_number_5: String + valid_snake_case_field_with_ending_numbers_12345: String + valid_snake_case_field_5_with_number5_inline: String + + # Incorrect use of snake case + INVALIDUPPERCASEFIELD: String + INVALID_SCREAMING_SNAKE_CASE_FIELD: String + Invalid_Upper_Snake_Case_Field: String + invalid_mixed_case_FIELD: String + InvalidCameCaseFieldName: String +} diff --git a/Magento2/Tests/GraphQL/ValidFieldNameUnitTest.php b/Magento2/Tests/GraphQL/ValidFieldNameUnitTest.php new file mode 100644 index 00000000..6e55e399 --- /dev/null +++ b/Magento2/Tests/GraphQL/ValidFieldNameUnitTest.php @@ -0,0 +1,39 @@ + 1, + 11 => 1, + 12 => 1, + 13 => 1, + 14 => 1, + 26 => 1, + 27 => 1, + 28 => 1, + 29 => 1, + 30 => 1, + ]; + } + + /** + * @inheritDoc + */ + protected function getWarningList() + { + return []; + } +} diff --git a/Magento2/Tests/GraphQL/ValidTopLevelFieldNameUnitTest.graphqls b/Magento2/Tests/GraphQL/ValidTopLevelFieldNameUnitTest.graphqls new file mode 100644 index 00000000..39d16c92 --- /dev/null +++ b/Magento2/Tests/GraphQL/ValidTopLevelFieldNameUnitTest.graphqls @@ -0,0 +1,35 @@ +# Valid names +query validCamelCaseQuery {} +mutation validCamelCaseMutation {} + +# Incorrect use of camel cyse +query InvalidCamelCaseQuery {} +query invalid_Camel_Case_Query_With_Underscores {} +mutation InvalidCamelCaseMutation {} +mutation invalid_Camel_Case_Mutation_With_Underscores {} + +# All lower case +query validlowercasequery {} +mutation validlowercasemutation {} + +# All upper case +query INVALIDUPPERCASEQUERY {} +mutation INVALIDUPPERCASEMUTATION {} + +# Mix camel case with uppercase +query validCamelCaseQueryWithUPPERCASE {} +mutation validCamelCaseMutationWithUPPERCASE {} + +# Usage of numeric characters +query validCamelCaseQueryWith1Number {} +query validCamelCaseQueryWith12345Numbers {} +query validCamelCaseQueryEndingWithNumber4 {} +query 5invalidCamelCaseQueryStartingWithNumber {} +mutation validCamelCaseMutationWith1Number {} +mutation validCamelCaseMutationWith12345Numbers {} +mutation validCamelCaseMutationEndingWithNumber4 {} +mutation 5invalidCamelCaseMutationStartingWithNumber {} + +# Empty names (valid by definition of GraphQL) +query {} +mutation {} \ No newline at end of file diff --git a/Magento2/Tests/GraphQL/ValidTopLevelFieldNameUnitTest.php b/Magento2/Tests/GraphQL/ValidTopLevelFieldNameUnitTest.php new file mode 100644 index 00000000..be37ddd2 --- /dev/null +++ b/Magento2/Tests/GraphQL/ValidTopLevelFieldNameUnitTest.php @@ -0,0 +1,37 @@ + 1, + 7 => 1, + 8 => 1, + 9 => 1, + 16 => 1, + 17 => 1, + 27 => 1, + 31 => 1, + ]; + } + + /** + * @inheritDoc + */ + protected function getWarningList() + { + return []; + } +} diff --git a/Magento2/Tests/GraphQL/ValidTypeNameUnitTest.graphqls b/Magento2/Tests/GraphQL/ValidTypeNameUnitTest.graphqls new file mode 100644 index 00000000..1fba0386 --- /dev/null +++ b/Magento2/Tests/GraphQL/ValidTypeNameUnitTest.graphqls @@ -0,0 +1,44 @@ +# Valid type names. +type ValidCamelCaseType {} +interface ValidCamelCaseInterface {} +enum ValidCamelCaseEnum {} + +# Incorrect usage of camel case. +type invalidCamelCaseType {} +type Invalid_Camel_Case_Type_With_Underscores {} +interface invalidCamelCaseInterface {} +interface Invalid_Camel_Case_Interface_With_Underscores {} +enum invalidCamelCaseEnum {} +enum Invalid_Camel_Case_Enum_With_Underscores {} + +# All lowercase +type invalidlowercasetype {} +interface invalidlowercaseinterface {} +enum invalidlowercaseenum {} + +# All uppercase +type VALIDUPPERCASETYPE {} +type INVALID_UPPERCASE_TYPE_WITH_UNDERSCORES {} +interface VALIDUPPERCASEINTERFACE {} +interface INVALID_UPPERCASE_INTERFACE_WITH_UNDERSCORES {} +enum VALIDUPPERCASECENUM {} +enum INVALID_UPPERCASE_ENUM_WITH_UNDERSCORES {} + +# Mix camel case with uppercase +type ValidCamelCaseTypeWITHUPPERCASE {} +interface ValidCamelCaseInterfaceWITHUPPERCASE {} +enum ValidCamelCaseEnumWITHUPPERCASE {} + +# Usage of numeric characters +type ValidCamelCaseTypeWith1Number {} +type ValidCamelCaseTypeWith12345Numbers {} +type 5InvalidCamelCaseTypeStartingWithNumber {} +type ValidCamelCaseTypeEndingWithNumber4 {} +interface ValidCamelCaseInterfaceWith1Number {} +interface ValidCamelCaseInterfaceWith12345Numbers {} +interface 5InvalidCamelCaseInterfaceStartingWithNumber {} +interface ValidCamelCaseInterfaceEndingWithNumber4 {} +enum ValidCamelCaseEnumWith1Number {} +enum ValidCamelCaseEnumWith12345Numbers {} +enum 5InvalidCamelCaseEnumStartingWithNumber {} +enum ValidCamelCaseEnumEndingWithNumber4 {} \ No newline at end of file diff --git a/Magento2/Tests/GraphQL/ValidTypeNameUnitTest.php b/Magento2/Tests/GraphQL/ValidTypeNameUnitTest.php new file mode 100644 index 00000000..035934e0 --- /dev/null +++ b/Magento2/Tests/GraphQL/ValidTypeNameUnitTest.php @@ -0,0 +1,56 @@ + + */ + protected function getErrorList() + { + return [ + 7 => 1, + 8 => 1, + 9 => 1, + 10 => 1, + 11 => 1, + 12 => 1, + 15 => 1, + 16 => 1, + 17 => 1, + 21 => 1, + 23 => 1, + 25 => 1, + 35 => 1, + 39 => 1, + 43 => 1, + ]; + } + + /** + * Returns the lines where warnings should occur. + * + * The key of the array should represent the line number and the value + * should represent the number of warnings that should occur on that line. + * + * @return array + */ + protected function getWarningList() + { + return []; + } +} diff --git a/Magento2/ruleset.xml b/Magento2/ruleset.xml index 246b6fe1..9d65e166 100644 --- a/Magento2/ruleset.xml +++ b/Magento2/ruleset.xml @@ -3,7 +3,7 @@ Magento Coding Standard - + @@ -364,6 +364,30 @@ 6 warning + + 6 + warning + + + 6 + warning + + + + 0 + warning + + + 6 + warning + + + 6 + warning + 6 warning diff --git a/PHP_CodeSniffer/Tokenizers/GRAPHQL.php b/PHP_CodeSniffer/Tokenizers/GRAPHQL.php new file mode 100644 index 00000000..8dd5fcae --- /dev/null +++ b/PHP_CodeSniffer/Tokenizers/GRAPHQL.php @@ -0,0 +1,288 @@ +webonyx/graphql-php. null values + * are automatically mapped to T_STRING but are noted as null in this list to improve + * maintenance at a glance. + * + * @var array + */ + private $tokenTypeMap = [ + Token::AT => 'T_DOC_COMMENT_TAG', + Token::BANG => 'T_BOOLEAN_NOT', + Token::BLOCK_STRING => 'T_COMMENT', + Token::BRACE_L => 'T_OPEN_CURLY_BRACKET', + Token::BRACE_R => 'T_CLOSE_CURLY_BRACKET', + Token::BRACKET_L => 'T_OPEN_SQUARE_BRACKET', + Token::BRACKET_R => 'T_CLOSE_SQUARE_BRACKET', + Token::COLON => 'T_COLON', + Token::COMMENT => 'T_COMMENT', + Token::DOLLAR => 'T_DOLLAR', + Token::EOF => 'T_CLOSE_TAG', + Token::EQUALS => 'T_EQUAL', + Token::FLOAT => null, + Token::INT => null, + Token::NAME => 'T_STRING', + Token::PAREN_L => 'T_OPEN_PARENTHESIS', + Token::PAREN_R => 'T_CLOSE_PARENTHESIS', + Token::PIPE => null, + Token::SPREAD => 'T_ELLIPSIS', + Token::SOF => 'T_OPEN_TAG', + Token::STRING => 'T_STRING', + ]; + + /** + * Defines how special keywords are mapped to PHP token types + * + * @var array + */ + private $keywordTokenTypeMap = [ + 'enum' => 'T_CLASS', + 'extend' => 'T_EXTENDS', + 'interface' => 'T_INTERFACE', + 'implements' => 'T_IMPLEMENTS', + 'type' => 'T_CLASS', + 'union' => 'T_CLASS', + 'query' => 'T_FUNCTION', + 'mutation' => 'T_FUNCTION', + ]; + + /** + * @inheritDoc + */ + public function processAdditional() + { + $this->logVerbose('*** START ADDITIONAL GRAPHQL PROCESSING ***'); + + $this->fixErroneousKeywordTokens(); + $this->processFields(); + + $this->logVerbose('*** END ADDITIONAL GRAPHQL PROCESSING ***'); + } + + /** + * @inheritDoc + */ + protected function tokenize($string) + { + $this->logVerbose('*** START GRAPHQL TOKENIZING ***'); + + $string = str_replace($this->eolChar, "\n", $string); + $tokens = []; + $source = new Source($string); + $lexer = new Lexer($source); + + do { + $kind = $lexer->token->kind; + $value = $lexer->token->value ?: ''; + + //if we have encountered a keyword, we convert it + //otherwise we translate the token or default it to T_STRING + if ($kind === Token::NAME && isset($this->keywordTokenTypeMap[$value])) { + $tokenType = $this->keywordTokenTypeMap[$value]; + } elseif (isset($this->tokenTypeMap[$kind])) { + $tokenType = $this->tokenTypeMap[$kind]; + } else { + $tokenType = 'T_STRING'; + } + + //some GraphQL tokens need special handling + switch ($kind) { + case Token::AT: + case Token::BRACE_L: + case Token::BRACE_R: + case Token::PAREN_L: + case Token::PAREN_R: + case Token::COLON: + case Token::BRACKET_L: + case Token::BRACKET_R: + case Token::BANG: + $value = $kind; + break; + default: + //NOP + } + + //finally we create the PHP token + $token = [ + 'code' => constant($tokenType), + 'type' => $tokenType, + 'content' => $value, + ]; + $line = $lexer->token->line; + + $lexer->advance(); + + //if line has changed (and we're not on start of file) we have to append at least one line break to current + //token's content otherwise PHP_CodeSniffer will screw up line numbers + if ($lexer->token->line !== $line && $kind !== Token::SOF) { + $token['content'] .= $this->eolChar; + } + $tokens[] = $token; + $tokens = array_merge( + $tokens, + $this->getNewLineTokens($line, $lexer->token->line) + ); + } while ($lexer->token->kind !== Token::EOF); + + $this->logVerbose('*** END GRAPHQL TOKENIZING ***'); + return $tokens; + } + + /** + * Fixes that keywords may be used as field, argument etc. names and could thus have been marked as special tokens + * while tokenizing. + */ + private function fixErroneousKeywordTokens() + { + $processingCodeBlock = false; + $numTokens = count($this->tokens); + + for ($i = 0; $i < $numTokens; ++$i) { + $tokenCode = $this->tokens[$i]['code']; + $tokenContent = $this->tokens[$i]['content']; + + switch (true) { + case $tokenCode === T_OPEN_CURLY_BRACKET: + //we have hit the beginning of a code block + $processingCodeBlock = true; + break; + case $tokenCode === T_CLOSE_CURLY_BRACKET: + //we have hit the end of a code block + $processingCodeBlock = false; + break; + case $processingCodeBlock + && $tokenCode !== T_STRING + && isset($this->keywordTokenTypeMap[$tokenContent]): + //we have hit a keyword within a code block that is of wrong token type + $this->tokens[$i]['code'] = T_STRING; + $this->tokens[$i]['type'] = 'T_STRING'; + break; + default: + //NOP All operations have already been executed + } + } + } + + /** + * Returns tokens of empty new lines for the range $lineStart to $lineEnd + * + * @param int $lineStart + * @param int $lineEnd + * @return array + */ + private function getNewLineTokens($lineStart, $lineEnd) + { + $amount = ($lineEnd - $lineStart) - 1; + $tokens = []; + + for ($i = 0; $i < $amount; ++$i) { + $tokens[] = [ + 'code' => T_WHITESPACE, + 'type' => 'T_WHITESPACE', + 'content' => $this->eolChar, + ]; + } + + return $tokens; + } + + /** + * Returns whether the token under $stackPointer is a field. + * + * We consider a token to be a field if: + *
    + *
  • its direct successor is of type {@link T_COLON}
  • + *
  • it has a list of arguments followed by a {@link T_COLON}
  • + *
+ * + * @param int $stackPointer + * @return bool + */ + private function isFieldToken($stackPointer) + { + //bail out if current token is nested in a parenthesis, since fields cannot be contained in parenthesises + if (isset($this->tokens[$stackPointer]['nested_parenthesis'])) { + return false; + } + + $nextToken = $this->tokens[$stackPointer + 1]; + + //if next token is an opening parenthesis, we advance to the token after the closing parenthesis + if ($nextToken['code'] === T_OPEN_PARENTHESIS) { + $nextPointer = $nextToken['parenthesis_closer'] + 1; + $nextToken = $this->tokens[$nextPointer]; + } + + //return whether current token is a string and next token is a colon + return $this->tokens[$stackPointer]['code'] === T_STRING && $nextToken['code'] === T_COLON; + } + + /** + * Logs $message if {@link PHP_CODESNIFFER_VERBOSITY} is greater than $level. + * + * @param string $message + * @param int $level + */ + private function logVerbose($message, $level = 1) + { + if (PHP_CODESNIFFER_VERBOSITY > $level) { + printf("\t%s" . PHP_EOL, $message); + } + } + + /** + * Processes field tokens, setting their type to {@link T_VARIABLE}. + */ + private function processFields() + { + $processingEntity = false; + $numTokens = count($this->tokens); + $entityTypes = [T_CLASS, T_INTERFACE]; + $skipTypes = [T_COMMENT, T_WHITESPACE]; + + for ($i = 0; $i < $numTokens; ++$i) { + $tokenCode = $this->tokens[$i]['code']; + + //process current token + switch (true) { + case in_array($tokenCode, $skipTypes): + //we have hit a token that needs to be skipped -> NOP + break; + case in_array($tokenCode, $entityTypes) && $this->tokens[$i]['content'] !== 'enum': + //we have hit an entity declaration + $processingEntity = true; + break; + case $tokenCode === T_CLOSE_CURLY_BRACKET: + //we have hit the end of an entity declaration + $processingEntity = false; + break; + case $processingEntity && $this->isFieldToken($i): + //we have hit a field + $this->tokens[$i]['code'] = T_VARIABLE; + $this->tokens[$i]['type'] = 'T_VARIABLE'; + break; + default: + //NOP All operations have already been executed + } + } + } +} diff --git a/composer.json b/composer.json index d67474e9..492357e1 100644 --- a/composer.json +++ b/composer.json @@ -9,11 +9,17 @@ "version": "4", "require": { "php": ">=5.6.0", - "squizlabs/php_codesniffer": "^3.4" + "squizlabs/php_codesniffer": "^3.4", + "webonyx/graphql-php": ">=0.12.6 <1.0" }, "require-dev": { "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, + "autoload": { + "classmap": [ + "PHP_CodeSniffer/Tokenizers/" + ] + }, "scripts": { "post-install-cmd": "vendor/bin/phpcs --config-set installed_paths ../../..", "post-update-cmd": "vendor/bin/phpcs --config-set installed_paths ../../.." diff --git a/composer.lock b/composer.lock index 416f08c7..3bc96315 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "dd5642c8fa31b4c4fdcf19135002185f", + "content-hash": "397eb37a3ebf83c48685aeb1c77f12b7", "packages": [ { "name": "squizlabs/php_codesniffer", @@ -56,6 +56,51 @@ "standards" ], "time": "2019-04-10T23:49:02+00:00" + }, + { + "name": "webonyx/graphql-php", + "version": "v0.12.6", + "source": { + "type": "git", + "url": "https://github.com/webonyx/graphql-php.git", + "reference": "4c545e5ec4fc37f6eb36c19f5a0e7feaf5979c95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/4c545e5ec4fc37f6eb36c19f5a0e7feaf5979c95", + "reference": "4c545e5ec4fc37f6eb36c19f5a0e7feaf5979c95", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.6" + }, + "require-dev": { + "phpunit/phpunit": "^4.8", + "psr/http-message": "^1.0", + "react/promise": "2.*" + }, + "suggest": { + "psr/http-message": "To use standard GraphQL server", + "react/promise": "To leverage async resolving on React PHP platform" + }, + "type": "library", + "autoload": { + "psr-4": { + "GraphQL\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP port of GraphQL reference implementation", + "homepage": "https://github.com/webonyx/graphql-php", + "keywords": [ + "api", + "graphql" + ], + "time": "2018-09-02T14:59:54+00:00" } ], "packages-dev": [