diff --git a/benchmark/fixtures.js b/benchmark/fixtures.js index d057a80526..d665e552d0 100644 --- a/benchmark/fixtures.js +++ b/benchmark/fixtures.js @@ -8,6 +8,11 @@ exports.bigSchemaSDL = fs.readFileSync( 'utf8', ); +exports.bigDocument = fs.readFileSync( + path.join(__dirname, 'kitchen-sink.graphql'), + 'utf8', +); + exports.bigSchemaIntrospectionResult = JSON.parse( fs.readFileSync(path.join(__dirname, 'github-schema.json'), 'utf8'), ); diff --git a/benchmark/kitchen-sink.graphql b/benchmark/kitchen-sink.graphql new file mode 100644 index 0000000000..c1bb2d8038 --- /dev/null +++ b/benchmark/kitchen-sink.graphql @@ -0,0 +1,69 @@ +# Copyright (c) 2015-present, Facebook, Inc. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +query queryName($foo: ComplexType, $site: Site = MOBILE) @onQuery { + whoever123is: node(id: [123, 456]) { + id + ... on User @onInlineFragment { + field2 { + id + alias: field1(first: 10, after: $foo) @include(if: $foo) { + id + ...frag @onFragmentSpread + } + } + } + ... @skip(unless: $foo) { + id + } + ... { + id + } + } +} + +mutation likeStory @onMutation { + like(story: 123) @onField { + story { + id @onField + } + } +} + +subscription StoryLikeSubscription($input: StoryLikeSubscribeInput) +@onSubscription { + storyLikeSubscribe(input: $input) { + story { + likers { + count + } + likeSentence { + text + } + } + } +} + +fragment frag on Friend @onFragmentDefinition { + foo( + size: $site + bar: 12 + obj: { + key: "value" + block: """ + block string uses \""" + """ + } + ) +} + +query teeny { + unnamed(truthy: true, falsey: false, nullish: null) + query +} + +query tiny { + __typename +} diff --git a/benchmark/printer-benchmark.js b/benchmark/printer-benchmark.js new file mode 100644 index 0000000000..efe5d46e58 --- /dev/null +++ b/benchmark/printer-benchmark.js @@ -0,0 +1,16 @@ +'use strict'; + +const { parse } = require('graphql/language/parser.js'); +const { print } = require('graphql/language/printer.js'); + +const { bigDocument } = require('./fixtures.js'); + +const document = parse(bigDocument); + +module.exports = { + name: 'Print kitchen-sink query', + count: 1000, + measure() { + print(document); + }, +}; diff --git a/cspell.yml b/cspell.yml index ff26b0902b..8382683188 100644 --- a/cspell.yml +++ b/cspell.yml @@ -51,6 +51,7 @@ words: # TODO: contribute upstream - deno - codecov + - falsey # Website tech - Nextra diff --git a/src/language/printer.ts b/src/language/printer.ts index e95c118d8b..1eb690ca8c 100644 --- a/src/language/printer.ts +++ b/src/language/printer.ts @@ -29,10 +29,10 @@ const printDocASTReducer: ASTReducer = { OperationDefinition: { leave(node) { const varDefs = wrap('(', join(node.variableDefinitions, ', '), ')'); - const prefix = join( + const prefix = maybeJoin( [ node.operation, - join([node.name, varDefs]), + maybeJoin([node.name, varDefs]), join(node.directives, ' '), ], ' ', @@ -63,7 +63,7 @@ const printDocASTReducer: ASTReducer = { argsLine = prefix + wrap('(\n', indent(join(args, '\n')), '\n)'); } - return join([argsLine, join(directives, ' '), selectionSet], ' '); + return maybeJoin([argsLine, join(directives, ' '), selectionSet], ' '); }, }, @@ -78,7 +78,7 @@ const printDocASTReducer: ASTReducer = { InlineFragment: { leave: ({ typeCondition, directives, selectionSet }) => - join( + maybeJoin( [ '...', wrap('on ', typeCondition), @@ -137,7 +137,7 @@ const printDocASTReducer: ASTReducer = { SchemaDefinition: { leave: ({ description, directives, operationTypes }) => wrap('', description, '\n') + - join(['schema', join(directives, ' '), block(operationTypes)], ' '), + maybeJoin(['schema', join(directives, ' '), block(operationTypes)], ' '), }, OperationTypeDefinition: { @@ -147,13 +147,13 @@ const printDocASTReducer: ASTReducer = { ScalarTypeDefinition: { leave: ({ description, name, directives }) => wrap('', description, '\n') + - join(['scalar', name, join(directives, ' ')], ' '), + maybeJoin(['scalar', name, join(directives, ' ')], ' '), }, ObjectTypeDefinition: { leave: ({ description, name, interfaces, directives, fields }) => wrap('', description, '\n') + - join( + maybeJoin( [ 'type', name, @@ -180,7 +180,7 @@ const printDocASTReducer: ASTReducer = { InputValueDefinition: { leave: ({ description, name, type, defaultValue, directives }) => wrap('', description, '\n') + - join( + maybeJoin( [name + ': ' + type, wrap('= ', defaultValue), join(directives, ' ')], ' ', ), @@ -189,7 +189,7 @@ const printDocASTReducer: ASTReducer = { InterfaceTypeDefinition: { leave: ({ description, name, interfaces, directives, fields }) => wrap('', description, '\n') + - join( + maybeJoin( [ 'interface', name, @@ -204,7 +204,7 @@ const printDocASTReducer: ASTReducer = { UnionTypeDefinition: { leave: ({ description, name, directives, types }) => wrap('', description, '\n') + - join( + maybeJoin( ['union', name, join(directives, ' '), wrap('= ', join(types, ' | '))], ' ', ), @@ -213,18 +213,19 @@ const printDocASTReducer: ASTReducer = { EnumTypeDefinition: { leave: ({ description, name, directives, values }) => wrap('', description, '\n') + - join(['enum', name, join(directives, ' '), block(values)], ' '), + maybeJoin(['enum', name, join(directives, ' '), block(values)], ' '), }, EnumValueDefinition: { leave: ({ description, name, directives }) => - wrap('', description, '\n') + join([name, join(directives, ' ')], ' '), + wrap('', description, '\n') + + maybeJoin([name, join(directives, ' ')], ' '), }, InputObjectTypeDefinition: { leave: ({ description, name, directives, fields }) => wrap('', description, '\n') + - join(['input', name, join(directives, ' '), block(fields)], ' '), + maybeJoin(['input', name, join(directives, ' '), block(fields)], ' '), }, DirectiveDefinition: { @@ -242,7 +243,7 @@ const printDocASTReducer: ASTReducer = { SchemaExtension: { leave: ({ directives, operationTypes }) => - join( + maybeJoin( ['extend schema', join(directives, ' '), block(operationTypes)], ' ', ), @@ -250,12 +251,12 @@ const printDocASTReducer: ASTReducer = { ScalarTypeExtension: { leave: ({ name, directives }) => - join(['extend scalar', name, join(directives, ' ')], ' '), + maybeJoin(['extend scalar', name, join(directives, ' ')], ' '), }, ObjectTypeExtension: { leave: ({ name, interfaces, directives, fields }) => - join( + maybeJoin( [ 'extend type', name, @@ -269,7 +270,7 @@ const printDocASTReducer: ASTReducer = { InterfaceTypeExtension: { leave: ({ name, interfaces, directives, fields }) => - join( + maybeJoin( [ 'extend interface', name, @@ -283,7 +284,7 @@ const printDocASTReducer: ASTReducer = { UnionTypeExtension: { leave: ({ name, directives, types }) => - join( + maybeJoin( [ 'extend union', name, @@ -296,12 +297,18 @@ const printDocASTReducer: ASTReducer = { EnumTypeExtension: { leave: ({ name, directives, values }) => - join(['extend enum', name, join(directives, ' '), block(values)], ' '), + maybeJoin( + ['extend enum', name, join(directives, ' '), block(values)], + ' ', + ), }, InputObjectTypeExtension: { leave: ({ name, directives, fields }) => - join(['extend input', name, join(directives, ' '), block(fields)], ' '), + maybeJoin( + ['extend input', name, join(directives, ' '), block(fields)], + ' ', + ), }, }; @@ -309,17 +316,47 @@ const printDocASTReducer: ASTReducer = { * Given maybeArray, print an empty string if it is null or empty, otherwise * print all items together separated by separator if provided */ -function join( - maybeArray: Maybe>, +function maybeJoin( + maybeArray: ReadonlyArray, separator = '', ): string { - return maybeArray?.filter((x) => x).join(separator) ?? ''; + const list = maybeArray.filter((x) => x); + const listLength = list.length; + let result = ''; + for (let i = 0; i < listLength; i++) { + if (i === listLength - 1) { + result += list[i]; + } else { + result += list[i] + separator; + } + } + return result; +} + +function join( + list: ReadonlyArray | undefined, + separator: string, +): string { + if (!list) { + return ''; + } + const listLength = list.length; + let result = ''; + for (let i = 0; i < listLength; i++) { + if (i === listLength - 1) { + result += list[i]; + } else { + result += list[i] + separator; + } + } + + return result; } /** * Given array, print each item on its own line, wrapped in an indented `{ }` block. */ -function block(array: Maybe>): string { +function block(array: ReadonlyArray | undefined): string { return wrap('{\n', indent(join(array, '\n')), '\n}'); } diff --git a/src/language/visitor.ts b/src/language/visitor.ts index daf96497bf..1e0c56e4a8 100644 --- a/src/language/visitor.ts +++ b/src/language/visitor.ts @@ -17,6 +17,8 @@ type KindVisitor = { | EnterLeaveVisitor; }; +const ALL_KINDS = Object.values(Kind); + interface EnterLeaveVisitor { readonly enter?: ASTVisitFn; readonly leave?: ASTVisitFn; @@ -182,7 +184,7 @@ export function visit( visitorKeys: ASTVisitorKeyMap = QueryDocumentKeys, ): any { const enterLeaveMap = new Map>(); - for (const kind of Object.values(Kind)) { + for (const kind of ALL_KINDS) { enterLeaveMap.set(kind, getEnterLeaveForKind(visitor, kind)); } @@ -202,7 +204,7 @@ export function visit( do { index++; const isLeaving = index === keys.length; - const isEdited = isLeaving && edits.length !== 0; + const isEdited = edits.length !== 0; if (isLeaving) { key = ancestors.length === 0 ? undefined : path[path.length - 1]; node = parent; @@ -222,10 +224,7 @@ export function visit( } } } else { - node = Object.defineProperties( - {}, - Object.getOwnPropertyDescriptors(node), - ); + node = { ...node }; for (const [editKey, editValue] of edits) { node[editKey] = editValue; } @@ -267,7 +266,7 @@ export function visit( } else if (result !== undefined) { edits.push([key, result]); if (!isLeaving) { - if (isNode(result)) { + if (isNode(result) && result !== node) { node = result; } else { path.pop();