diff --git a/Magento2/Sniffs/GraphQL/AbstractGraphQLSniff.php b/Magento2/Sniffs/GraphQL/AbstractGraphQLSniff.php index b03cffe8..b9e95fae 100644 --- a/Magento2/Sniffs/GraphQL/AbstractGraphQLSniff.php +++ b/Magento2/Sniffs/GraphQL/AbstractGraphQLSniff.php @@ -10,6 +10,7 @@ * * To obtain a valid license for using this software please contact us at license@techdivision.com */ + namespace Magento2\Sniffs\GraphQL; use PHP_CodeSniffer\Sniffs\Sniff; @@ -38,13 +39,38 @@ protected function isCamelCase($name) } /** - * Returns whether $name is strictly lower case, potentially separated by underscores. + * 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) + protected function isSnakeCase($name, $upperCase = false) { - return preg_match('/^[a-z][a-z0-9_]*$/', $name); + $pattern = $upperCase ? '/^[A-Z][A-Z0-9_]*$/' : '/^[a-z][a-z0-9_]*$/'; + return preg_match($pattern, $name); + } + + /** + * 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 index ffb1f8d4..03de433f 100644 --- a/Magento2/Sniffs/GraphQL/ValidArgumentNameSniff.php +++ b/Magento2/Sniffs/GraphQL/ValidArgumentNameSniff.php @@ -3,6 +3,7 @@ * Copyright © Magento. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento2\Sniffs\GraphQL; use PHP_CodeSniffer\Files\File; @@ -64,7 +65,7 @@ public function process(File $phpcsFile, $stackPtr) } /** - * Finds all argument names contained in $tokens range $startPointer and + * Finds all argument names contained in $tokens in range $startPointer to * $endPointer. * * @param int $startPointer @@ -80,7 +81,7 @@ private function getArguments($startPointer, $endPointer, array $tokens) $skipTypes = [T_COMMENT, T_WHITESPACE]; for ($i = $startPointer + 1; $i < $endPointer; ++$i) { - //skip comment tokens + //skip some tokens if (in_array($tokens[$i]['code'], $skipTypes)) { continue; } @@ -110,15 +111,6 @@ private function getArguments($startPointer, $endPointer, array $tokens) */ private function getCloseParenthesisPointer($stackPointer, array $tokens) { - $numTokens = count($tokens); - - for ($i = $stackPointer + 1; $i < $numTokens; ++$i) { - if ($tokens[$i]['code'] === T_CLOSE_PARENTHESIS) { - return $i; - } - } - - //if we came here we could not find the closing parenthesis - return false; + return $this->seekToken(T_CLOSE_PARENTHESIS, $tokens, $stackPointer); } } diff --git a/Magento2/Sniffs/GraphQL/ValidEnumValueSniff.php b/Magento2/Sniffs/GraphQL/ValidEnumValueSniff.php new file mode 100644 index 00000000..b7c702c7 --- /dev/null +++ b/Magento2/Sniffs/GraphQL/ValidEnumValueSniff.php @@ -0,0 +1,134 @@ +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 $value) { + $pointer = $value[0]; + $name = $value[1]; + + if (!$this->isSnakeCase($name, true)) { + $type = 'Enum value'; + $error = '%s "%s" is not in SCREAMING_SNAKE_CASE format'; + $data = [ + $type, + $name, + ]; + + $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. + * + * @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) { + //skip some tokens + if (in_array($tokens[$i]['code'], $skipTypes)) { + continue; + } + $enumValue .= $tokens[$i]['content']; + + if ($valueTokenPointer === null) { + $valueTokenPointer = $i; + } + + if (strpos($enumValue, $eolChar) !== false) { + $values[] = [$valueTokenPointer, rtrim($enumValue, $eolChar)]; + $enumValue = ''; + $valueTokenPointer = null; + } + } + + return $values; + } +} diff --git a/Magento2/Tests/GraphQL/ValidEnumValueUnitTest.graphqls b/Magento2/Tests/GraphQL/ValidEnumValueUnitTest.graphqls new file mode 100644 index 00000000..994e75f2 --- /dev/null +++ b/Magento2/Tests/GraphQL/ValidEnumValueUnitTest.graphqls @@ -0,0 +1,19 @@ +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 + + # Invalid values + 1_INVALID_SCREAMING_SNAKE_CASE_VALUE_STARTING_WITH_NUMBER + invalidCamelCaseValue + InvalidCamelCaseCapsValue + invalidlowercasevalue +} + +# 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..9d577f68 --- /dev/null +++ b/Magento2/Tests/GraphQL/ValidEnumValueUnitTest.php @@ -0,0 +1,34 @@ + 1, + 11 => 1, + 12 => 1, + 13 => 1, + ]; + } + + /** + * @inheritDoc + */ + protected function getWarningList() + { + return []; + } +}