From 5785c08c8d19b69ca25d76b836052ec7ee63cd47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Va=C5=A1ek=20Henzl?= Date: Sun, 12 Sep 2021 17:12:57 +1200 Subject: [PATCH 1/6] Add `StringBlock::print()` method Equivalent of `printBlockString()` function from `graphql-js` --- src/Language/BlockString.php | 37 ++++++++++++++ tests/Language/BlockStringTest.php | 81 ++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) diff --git a/src/Language/BlockString.php b/src/Language/BlockString.php index 6ccafae60..a6e9840fc 100644 --- a/src/Language/BlockString.php +++ b/src/Language/BlockString.php @@ -12,6 +12,8 @@ use function mb_strlen; use function mb_substr; use function preg_split; +use function str_contains; +use function str_replace; class BlockString { @@ -103,4 +105,39 @@ public static function getIndentation(string $value): int return $commonIndent ?? 0; } + + /** + * Print a block string in the indented block form by adding a leading and + * trailing blank line. However, if a block string starts with whitespace and is + * a single-line, adding a leading blank line would strip that whitespace. + */ + public static function print( + string $value, + string $indentation = '', + bool $preferMultipleLines = false + ): string { + $valueLength = mb_strlen($value); + $isSingleLine = ! str_contains($value, "\n"); + $hasLeadingSpace = $value[0] === ' ' || $value[0] === '\t'; + $hasTrailingQuote = $value[$valueLength - 1] === '"'; + $hasTrailingSlash = $value[$valueLength - 1] === '\\'; + $printAsMultipleLines = + ! $isSingleLine || + $hasTrailingQuote || + $hasTrailingSlash || + $preferMultipleLines; + + $result = ''; + // Format a multi-line block quote to account for leading space. + if ($printAsMultipleLines && ! ($isSingleLine && $hasLeadingSpace)) { + $result .= "\n" . $indentation; + } + + $result .= $indentation !== '' ? str_replace("\n", "\n" . $indentation, $value) : $value; + if ($printAsMultipleLines) { + $result .= "\n"; + } + + return '"""' . str_replace('"""', '\\"""', $result) . '"""'; + } } diff --git a/tests/Language/BlockStringTest.php b/tests/Language/BlockStringTest.php index 0db5a4eb4..1dcf81905 100644 --- a/tests/Language/BlockStringTest.php +++ b/tests/Language/BlockStringTest.php @@ -174,4 +174,85 @@ public function testDoNotTakeEmptyLinesIntoAccount(): void self::assertEquals(1, BlockString::getIndentation("a\n\n b")); self::assertEquals(2, BlockString::getIndentation("a\n \n b")); } + + // describe('printBlockString') + + /** + * @see it('do not escape characters') + */ + public function testDoNotEscapeCharacters(): void + { + $str = "\" \\ / \u{8} \f \n \r \t"; // \u{8} === \b + + self::assertEquals("\"\"\"\n" . $str . "\n\"\"\"", BlockString::print($str)); + } + + /** + * @see it('by default print block strings as single line') + */ + public function testByDefaultPrintBlockStringsAsSingleLine(): void + { + $str = 'one liner'; + + self::assertEquals('"""one liner"""', BlockString::print($str)); + self::assertEquals("\"\"\"\none liner\n\"\"\"", BlockString::print($str, '', true)); + } + + /** + * @see it('correctly prints single-line with leading space') + */ + public function testCorrectlyPrintsSingleLineWithLeadingSpace(): void + { + $str = ' space-led string'; + + self::assertEquals('""" space-led string"""', BlockString::print($str)); + self::assertEquals("\"\"\" space-led string\n\"\"\"", BlockString::print($str, '', true)); + } + + /** + * @see it('correctly prints single-line with leading space and quotation') + */ + public function testCorrectlyPrintsSingleLineWithLeadingSpaceAndQuotation(): void + { + $str = ' space-led value "quoted string"'; + + self::assertEquals("\"\"\" space-led value \"quoted string\"\n\"\"\"", BlockString::print($str)); + self::assertEquals("\"\"\" space-led value \"quoted string\"\n\"\"\"", BlockString::print($str)); + } + + /** + * @see it('correctly prints single-line with trailing backslash') + */ + public function testCorrectlyPrintsSingleLineWithTrailingBackslash(): void + { + $str = 'backslash \\'; + + self::assertEquals("\"\"\"\nbackslash \\\n\"\"\"", BlockString::print($str)); + self::assertEquals("\"\"\"\nbackslash \\\n\"\"\"", BlockString::print($str, '', true)); + } + + /** + * @see it('correctly prints string with a first line indentation') + */ + public function testCorrectlyPrintsStringWithAFirstLineIndentation(): void + { + $str = self::joinLines( + ' first ', + ' line ', + 'indentation', + ' string', + ); + + self::assertEquals( + self::joinLines( + '"""', + ' first ', + ' line ', + 'indentation', + ' string', + '"""', + ), + BlockString::print($str) + ); + } } From 55b8f8ce1a4d36a09502ad6c8a958f22b5c12d22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Va=C5=A1ek=20Henzl?= Date: Sun, 12 Sep 2021 20:47:40 +1200 Subject: [PATCH 2/6] Unify `Printer` and `SchemaPrinter` output for block strings --- src/Language/Printer.php | 26 +---------------- src/Utils/SchemaPrinter.php | 43 +++++----------------------- tests/Language/SchemaPrinterTest.php | 8 ++---- tests/Utils/SchemaPrinterTest.php | 38 +++++++----------------- 4 files changed, 20 insertions(+), 95 deletions(-) diff --git a/src/Language/Printer.php b/src/Language/Printer.php index 61f4d5d78..3800c555d 100644 --- a/src/Language/Printer.php +++ b/src/Language/Printer.php @@ -55,7 +55,6 @@ use function count; use function implode; use function json_encode; -use function preg_replace; use function str_replace; use function strlen; use function strpos; @@ -408,7 +407,7 @@ protected function p(?Node $node, bool $isDescription = false): string case $node instanceof StringValueNode: if ($node->block) { - return $this->printBlockString($node->value, $isDescription); + return BlockString::print($node->value, $isDescription ? '' : ' '); } return json_encode($node->value); @@ -518,27 +517,4 @@ protected function join(array $parts, string $separator = ''): string { return implode($separator, array_filter($parts)); } - - /** - * Print a block string in the indented block form by adding a leading and - * trailing blank line. However, if a block string starts with whitespace and is - * a single-line, adding a leading blank line would strip that whitespace. - */ - protected function printBlockString(string $value, bool $isDescription): string - { - $escaped = str_replace('"""', '\\"""', $value); - - $startsWithWhitespace = $value[0] === ' ' || $value[0] === "\t"; - $doesNotEndWithNewline = strpos($value, "\n") === false; - - if ($startsWithWhitespace && $doesNotEndWithNewline) { - return '"""' . preg_replace('/"$/', "\"\n", $escaped) . '"""'; - } - - $content = $isDescription - ? $escaped - : $this->indent($escaped); - - return '"""' . "\n" . $content . "\n" . '"""'; - } } diff --git a/src/Utils/SchemaPrinter.php b/src/Utils/SchemaPrinter.php index e1a0d2850..7ced31791 100644 --- a/src/Utils/SchemaPrinter.php +++ b/src/Utils/SchemaPrinter.php @@ -5,6 +5,7 @@ namespace GraphQL\Utils; use GraphQL\Error\Error; +use GraphQL\Language\BlockString; use GraphQL\Language\Printer; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; @@ -34,7 +35,6 @@ use function sprintf; use function str_replace; use function strlen; -use function substr; /** * Given an instance of Schema, prints it in schema definition language. @@ -167,46 +167,17 @@ protected static function printDescription(array $options, $def, string $indenta return ''; } - $lines = static::descriptionLines($def->description, 120 - strlen($indentation)); if (isset($options['commentDescriptions'])) { - return static::printDescriptionWithComments($lines, $indentation, $firstInBlock); - } - - $description = $indentation !== '' && ! $firstInBlock - ? "\n" . $indentation . '"""' - : $indentation . '"""'; - - // In some circumstances, a single line can be used for the description. - if ( - count($lines) === 1 && - mb_strlen($lines[0]) < 70 && - substr($lines[0], -1) !== '"' - ) { - return $description . static::escapeQuote($lines[0]) . "\"\"\"\n"; - } + $lines = static::descriptionLines($def->description, 120 - strlen($indentation)); - // Format a multi-line block quote to account for leading space. - $hasLeadingSpace = isset($lines[0]) && - ( - substr($lines[0], 0, 1) === ' ' || - substr($lines[0], 0, 1) === '\t' - ); - if (! $hasLeadingSpace) { - $description .= "\n"; - } - - $lineLength = count($lines); - for ($i = 0; $i < $lineLength; $i++) { - if ($i !== 0 || ! $hasLeadingSpace) { - $description .= $indentation; - } - - $description .= static::escapeQuote($lines[$i]) . "\n"; + return static::printDescriptionWithComments($lines, $indentation, $firstInBlock); } - $description .= $indentation . "\"\"\"\n"; + $preferMultipleLines = mb_strlen($def->description) > 70; + $blockString = BlockString::print($def->description, '', $preferMultipleLines); + $prefix = $indentation !== '' && ! $firstInBlock ? "\n" . $indentation : $indentation; - return $description; + return $prefix . str_replace("\n", "\n" . $indentation, $blockString) . "\n"; } /** diff --git a/tests/Language/SchemaPrinterTest.php b/tests/Language/SchemaPrinterTest.php index 7652365ce..3c2e0ef4e 100644 --- a/tests/Language/SchemaPrinterTest.php +++ b/tests/Language/SchemaPrinterTest.php @@ -62,13 +62,9 @@ public function testPrintsKitchenSink(): void """ type Foo implements Bar & Baz & Two { one: Type - """ - This is a description of the `two` field. - """ + """This is a description of the `two` field.""" two( - """ - This is a description of the `argument` argument. - """ + """This is a description of the `argument` argument.""" argument: InputType! ): Type three(argument: InputType, other: String): Int diff --git a/tests/Utils/SchemaPrinterTest.php b/tests/Utils/SchemaPrinterTest.php index 144b972f4..2ae3091c8 100644 --- a/tests/Utils/SchemaPrinterTest.php +++ b/tests/Utils/SchemaPrinterTest.php @@ -942,9 +942,7 @@ public function testPrintIntrospectionSchema(): void """Marks an element of a GraphQL schema as no longer supported.""" directive @deprecated( """ - Explains why this element was deprecated, usually also including a suggestion - for how to access supported similar data. Formatted using the Markdown syntax - (as specified by [CommonMark](https://commonmark.org/). + Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax (as specified by [CommonMark](https://commonmark.org/). """ reason: String = "No longer supported" ) on FIELD_DEFINITION | ENUM_VALUE @@ -952,10 +950,7 @@ public function testPrintIntrospectionSchema(): void """ A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document. -In some cases, you need to provide options to alter GraphQL's execution behavior -in ways field arguments will not suffice, such as conditionally including or -skipping a field. Directives provide this by describing additional information -to the executor. +In some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor. """ type __Directive { name: String! @@ -966,8 +961,7 @@ public function testPrintIntrospectionSchema(): void } """ -A Directive can be adjacent to many parts of the GraphQL language, a -__DirectiveLocation describes one such possible adjacencies. +A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies. """ enum __DirectiveLocation { """Location adjacent to a query operation.""" @@ -1029,9 +1023,7 @@ enum __DirectiveLocation { } """ -One possible value for a given Enum. Enum values are unique values, not a -placeholder for a string or numeric value. However an Enum value is returned in -a JSON response as a string. +One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string. """ type __EnumValue { name: String! @@ -1041,8 +1033,7 @@ enum __DirectiveLocation { } """ -Object and Interface types are described by a list of Fields, each of which has -a name, potentially a list of arguments, and a return type. +Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type. """ type __Field { name: String! @@ -1054,9 +1045,7 @@ enum __DirectiveLocation { } """ -Arguments provided to Fields or Directives and the input fields of an -InputObject are represented as Input Values which describe their type and -optionally a default value. +Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value. """ type __InputValue { name: String! @@ -1070,9 +1059,7 @@ enum __DirectiveLocation { } """ -A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all -available types and directives on the server, as well as the entry points for -query, mutation, and subscription operations. +A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations. """ type __Schema { """A list of all types supported by this server.""" @@ -1096,14 +1083,9 @@ enum __DirectiveLocation { } """ -The fundamental unit of any GraphQL Schema is the type. There are many kinds of -types in GraphQL as represented by the `__TypeKind` enum. - -Depending on the kind of a type, certain fields describe information about that -type. Scalar types provide no information beyond a name and description, while -Enum types provide their values. Object and Interface types provide the fields -they describe. Abstract types, Union and Interface, provide the Object types -possible at runtime. List and NonNull types compose other types. +The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum. + +Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types. """ type __Type { kind: __TypeKind! From 833fba80e0e74a31595603d1b27eee26dbe3ed7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Va=C5=A1ek=20Henzl?= Date: Sun, 12 Sep 2021 20:58:11 +1200 Subject: [PATCH 3/6] Remove unnecessary tests They have been removed from `graphql-js` as well. The use-cases are covered by `BlockStringTest`. --- tests/Language/PrinterTest.php | 80 ------------------------------- tests/Utils/SchemaPrinterTest.php | 57 ---------------------- 2 files changed, 137 deletions(-) diff --git a/tests/Language/PrinterTest.php b/tests/Language/PrinterTest.php index 65b20720e..15fd6e5fc 100644 --- a/tests/Language/PrinterTest.php +++ b/tests/Language/PrinterTest.php @@ -119,67 +119,6 @@ public function testPrintsFragmentWithVariableDirectives(): void self::assertEquals($expected, Printer::doPrint($queryAstWithVariableDirective)); } - /** - * @see it('correctly prints single-line with leading space') - */ - public function testCorrectlyPrintsSingleLineBlockStringsWithLeadingSpace(): void - { - $mutationAstWithArtifacts = Parser::parse( - '{ field(arg: """ space-led value""") }' - ); - $expected = '{ - field(arg: """ space-led value""") -} -'; - self::assertEquals($expected, Printer::doPrint($mutationAstWithArtifacts)); - } - - /** - * @see it('correctly prints string with a first line indentation') - */ - public function testCorrectlyPrintsBlockStringsWithAFirstLineIndentation(): void - { - $mutationAstWithArtifacts = Parser::parse( - '{ - field(arg: """ - first - line - indentation - """) -}' - ); - $expected = '{ - field(arg: """ - first - line - indentation - """) -} -'; - self::assertEquals($expected, Printer::doPrint($mutationAstWithArtifacts)); - } - - /** - * @see it('correctly prints single-line with leading space and quotation') - */ - public function testCorrectlyPrintsSingleLineWithLeadingSpaceAndQuotation(): void - { - $mutationAstWithArtifacts = Parser::parse(' - { - field(arg: """ space-led value "quoted string" - """) - } - '); - $expected = <<description); } - /** - * @see it('Does not one-line print a description that ends with a quote') - */ - public function testDoesNotOneLinePrintADescriptionThatEndsWithAQuote(): void - { - $description = 'This field is "awesome"'; - $output = $this->printSingleFieldSchema([ - 'type' => Type::string(), - 'description' => $description, - ]); - - self::assertEquals( - ' -type Query { - """ - This field is "awesome" - """ - singleField: String -} -', - $output - ); - - /** @var ObjectType $recreatedRoot */ - $recreatedRoot = BuildSchema::build($output)->getTypeMap()['Query']; - $recreatedField = $recreatedRoot->getFields()['singleField']; - self::assertEquals($description, $recreatedField->description); - } - - /** - * @see it('Preserves leading spaces when printing a description') - */ - public function testPReservesLeadingSpacesWhenPrintingADescription(): void - { - $description = ' This field is "awesome"'; - $output = $this->printSingleFieldSchema([ - 'type' => Type::string(), - 'description' => $description, - ]); - - self::assertEquals( - ' -type Query { - """ This field is "awesome" - """ - singleField: String -} -', - $output - ); - - /** @var ObjectType $recreatedRoot */ - $recreatedRoot = BuildSchema::build($output)->getTypeMap()['Query']; - $recreatedField = $recreatedRoot->getFields()['singleField']; - self::assertEquals($description, $recreatedField->description); - } - /** * @see it('Print Introspection Schema') */ From ea2e7ec4d010076371054f3ee110e0d27c61ee2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Va=C5=A1ek=20Henzl?= Date: Tue, 14 Sep 2021 19:42:39 +1200 Subject: [PATCH 4/6] Fix code for PHP 7.4 --- src/Language/BlockString.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Language/BlockString.php b/src/Language/BlockString.php index a6e9840fc..9a4438c80 100644 --- a/src/Language/BlockString.php +++ b/src/Language/BlockString.php @@ -12,8 +12,8 @@ use function mb_strlen; use function mb_substr; use function preg_split; -use function str_contains; use function str_replace; +use function strpos; class BlockString { @@ -117,7 +117,7 @@ public static function print( bool $preferMultipleLines = false ): string { $valueLength = mb_strlen($value); - $isSingleLine = ! str_contains($value, "\n"); + $isSingleLine = strpos($value, "\n") === false; $hasLeadingSpace = $value[0] === ' ' || $value[0] === '\t'; $hasTrailingQuote = $value[$valueLength - 1] === '"'; $hasTrailingSlash = $value[$valueLength - 1] === '\\'; From eb0675e506d26d99f92c4553bb66f50b6624351b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Va=C5=A1ek=20Henzl?= Date: Tue, 14 Sep 2021 20:19:09 +1200 Subject: [PATCH 5/6] Remove unnecessary `SchemaPrinter::escapeQuote()` --- src/Utils/SchemaPrinter.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Utils/SchemaPrinter.php b/src/Utils/SchemaPrinter.php index 7ced31791..aa4051311 100644 --- a/src/Utils/SchemaPrinter.php +++ b/src/Utils/SchemaPrinter.php @@ -235,11 +235,6 @@ protected static function printDescriptionWithComments(array $lines, string $ind return $description; } - protected static function escapeQuote(string $line): string - { - return str_replace('"""', '\\"""', $line); - } - /** * @param array $options * @param array $args From 1b2f90dfc7cb3b5508a6b7ed16bae09938cd30c3 Mon Sep 17 00:00:00 2001 From: spawnia Date: Thu, 30 Sep 2021 12:09:45 +0200 Subject: [PATCH 6/6] Codestyle --- src/Language/BlockString.php | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/Language/BlockString.php b/src/Language/BlockString.php index 9a4438c80..1b60dc639 100644 --- a/src/Language/BlockString.php +++ b/src/Language/BlockString.php @@ -122,22 +122,29 @@ public static function print( $hasTrailingQuote = $value[$valueLength - 1] === '"'; $hasTrailingSlash = $value[$valueLength - 1] === '\\'; $printAsMultipleLines = - ! $isSingleLine || - $hasTrailingQuote || - $hasTrailingSlash || - $preferMultipleLines; + ! $isSingleLine + || $hasTrailingQuote + || $hasTrailingSlash + || $preferMultipleLines; $result = ''; // Format a multi-line block quote to account for leading space. - if ($printAsMultipleLines && ! ($isSingleLine && $hasLeadingSpace)) { + if ( + $printAsMultipleLines + && ! ($isSingleLine && $hasLeadingSpace) + ) { $result .= "\n" . $indentation; } - $result .= $indentation !== '' ? str_replace("\n", "\n" . $indentation, $value) : $value; + $result .= $indentation !== '' + ? str_replace("\n", "\n" . $indentation, $value) + : $value; if ($printAsMultipleLines) { $result .= "\n"; } - return '"""' . str_replace('"""', '\\"""', $result) . '"""'; + return '"""' + . str_replace('"""', '\\"""', $result) + . '"""'; } }