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": [