Skip to content

Commit

Permalink
refactor: Improve print performance (#24)
Browse files Browse the repository at this point in the history
* Fix incorrect AST alias

* Update printer with new indent logic

* Simplify OperationDefinition printer

* Replace selection set generic printers

* Fix unrelated bench name

* Replace map + join

* Remove variableDefinitions that were accidentally copied over

* Add changeset
  • Loading branch information
kitten authored Apr 2, 2024
1 parent 896f588 commit 4f3e17a
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 79 deletions.
5 changes: 5 additions & 0 deletions .changeset/three-buttons-eat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@0no-co/graphql.web': patch
---

Improve printer performance.
2 changes: 1 addition & 1 deletion src/__tests__/visitor.bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as graphql17 from 'graphql17';
import kitchenSinkAST from './fixtures/kitchen_sink.json';
import { visit } from '../visitor';

describe('print (kitchen sink AST)', () => {
describe('visit (kitchen sink AST)', () => {
bench('@0no-co/graphql.web', () => {
visit(kitchenSinkAST, {
Field: formatNode,
Expand Down
2 changes: 1 addition & 1 deletion src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ export type FloatValueNode = Or<
>;

export type StringValueNode = Or<
GraphQL.FloatValueNode,
GraphQL.StringValueNode,
{
readonly kind: Kind.STRING;
readonly value: string;
Expand Down
204 changes: 127 additions & 77 deletions src/printer.ts
Original file line number Diff line number Diff line change
@@ -1,133 +1,183 @@
import type { ASTNode } from './ast';
import type {
ASTNode,
NameNode,
DocumentNode,
VariableNode,
SelectionSetNode,
FieldNode,
ArgumentNode,
FragmentSpreadNode,
InlineFragmentNode,
VariableDefinitionNode,
OperationDefinitionNode,
FragmentDefinitionNode,
IntValueNode,
FloatValueNode,
StringValueNode,
BooleanValueNode,
NullValueNode,
EnumValueNode,
ListValueNode,
ObjectValueNode,
ObjectFieldNode,
DirectiveNode,
NamedTypeNode,
ListTypeNode,
NonNullTypeNode,
} from './ast';

export function printString(string: string) {
function mapJoin<T>(value: readonly T[], joiner: string, mapper: (value: T) => string): string {
let out = '';
for (let index = 0; index < value.length; index++) {
if (index) out += joiner;
out += mapper(value[index]);
}
return out;
}

function printString(string: string) {
return JSON.stringify(string);
}

export function printBlockString(string: string) {
function printBlockString(string: string) {
return '"""\n' + string.replace(/"""/g, '\\"""') + '\n"""';
}

const hasItems = <T>(array: ReadonlyArray<T> | undefined | null): array is ReadonlyArray<T> =>
!!(array && array.length);

const MAX_LINE_LENGTH = 80;

const nodes: {
[NodeT in ASTNode as NodeT['kind']]?: (node: NodeT) => string;
} = {
OperationDefinition(node) {
if (
node.operation === 'query' &&
!node.name &&
!hasItems(node.variableDefinitions) &&
!hasItems(node.directives)
) {
return nodes.SelectionSet!(node.selectionSet);
}
let LF = '\n';

const nodes = {
OperationDefinition(node: OperationDefinitionNode): string {
let out: string = node.operation;
if (node.name) out += ' ' + node.name.value;
if (hasItems(node.variableDefinitions)) {
if (node.variableDefinitions && node.variableDefinitions.length) {
if (!node.name) out += ' ';
out += '(' + node.variableDefinitions.map(nodes.VariableDefinition!).join(', ') + ')';
out += '(' + mapJoin(node.variableDefinitions, ', ', nodes.VariableDefinition) + ')';
}
if (hasItems(node.directives)) out += ' ' + node.directives.map(nodes.Directive!).join(' ');
return out + ' ' + nodes.SelectionSet!(node.selectionSet);
},
VariableDefinition(node) {
let out = nodes.Variable!(node.variable) + ': ' + print(node.type);
if (node.defaultValue) out += ' = ' + print(node.defaultValue);
if (hasItems(node.directives)) out += ' ' + node.directives.map(nodes.Directive!).join(' ');
if (node.directives && node.directives.length)
out += ' ' + mapJoin(node.directives, ' ', nodes.Directive);
return out !== 'query'
? out + ' ' + nodes.SelectionSet(node.selectionSet)
: nodes.SelectionSet(node.selectionSet);
},
VariableDefinition(node: VariableDefinitionNode): string {
let out = nodes.Variable!(node.variable) + ': ' + _print(node.type);
if (node.defaultValue) out += ' = ' + _print(node.defaultValue);
if (node.directives && node.directives.length)
out += ' ' + mapJoin(node.directives, ' ', nodes.Directive);
return out;
},
Field(node) {
let out = (node.alias ? node.alias.value + ': ' : '') + node.name.value;
if (hasItems(node.arguments)) {
const args = node.arguments.map(nodes.Argument!);
const argsLine = out + '(' + args.join(', ') + ')';
out =
argsLine.length > MAX_LINE_LENGTH
? out + '(\n ' + args.join('\n').replace(/\n/g, '\n ') + '\n)'
: argsLine;
Field(node: FieldNode): string {
let out = node.alias ? node.alias.value + ': ' + node.name.value : node.name.value;
if (node.arguments && node.arguments.length) {
const args = mapJoin(node.arguments, ', ', nodes.Argument);
if (out.length + args.length + 2 > MAX_LINE_LENGTH) {
out +=
'(' +
(LF += ' ') +
mapJoin(node.arguments, LF, nodes.Argument) +
(LF = LF.slice(0, -2)) +
')';
} else {
out += '(' + args + ')';
}
}
if (hasItems(node.directives)) out += ' ' + node.directives.map(nodes.Directive!).join(' ');
return node.selectionSet ? out + ' ' + nodes.SelectionSet!(node.selectionSet) : out;
if (node.directives && node.directives.length)
out += ' ' + mapJoin(node.directives, ' ', nodes.Directive);
if (node.selectionSet) out += ' ' + nodes.SelectionSet(node.selectionSet);
return out;
},
StringValue(node) {
return node.block ? printBlockString(node.value) : printString(node.value);
StringValue(node: StringValueNode): string {
if (node.block) {
return printBlockString(node.value).replace(/\n/g, LF);
} else {
return printString(node.value);
}
},
BooleanValue(node) {
BooleanValue(node: BooleanValueNode): string {
return '' + node.value;
},
NullValue(_node) {
NullValue(_node: NullValueNode): string {
return 'null';
},
IntValue(node) {
IntValue(node: IntValueNode): string {
return node.value;
},
FloatValue(node) {
FloatValue(node: FloatValueNode): string {
return node.value;
},
EnumValue(node) {
EnumValue(node: EnumValueNode): string {
return node.value;
},
Name(node) {
Name(node: NameNode): string {
return node.value;
},
Variable(node) {
Variable(node: VariableNode): string {
return '$' + node.name.value;
},
ListValue(node) {
return '[' + node.values.map(print).join(', ') + ']';
ListValue(node: ListValueNode): string {
return '[' + mapJoin(node.values, ', ', _print) + ']';
},
ObjectValue(node) {
return '{' + node.fields.map(nodes.ObjectField!).join(', ') + '}';
ObjectValue(node: ObjectValueNode): string {
return '{' + mapJoin(node.fields, ', ', nodes.ObjectField) + '}';
},
ObjectField(node) {
return node.name.value + ': ' + print(node.value);
ObjectField(node: ObjectFieldNode): string {
return node.name.value + ': ' + _print(node.value);
},
Document(node) {
return hasItems(node.definitions) ? node.definitions.map(print).join('\n\n') : '';
Document(node: DocumentNode): string {
if (!node.definitions || !node.definitions.length) return '';
return mapJoin(node.definitions, '\n\n', _print);
},
SelectionSet(node) {
return '{\n ' + node.selections.map(print).join('\n').replace(/\n/g, '\n ') + '\n}';
SelectionSet(node: SelectionSetNode): string {
return '{' + (LF += ' ') + mapJoin(node.selections, LF, _print) + (LF = LF.slice(0, -2)) + '}';
},
Argument(node) {
return node.name.value + ': ' + print(node.value);
Argument(node: ArgumentNode): string {
return node.name.value + ': ' + _print(node.value);
},
FragmentSpread(node) {
FragmentSpread(node: FragmentSpreadNode): string {
let out = '...' + node.name.value;
if (hasItems(node.directives)) out += ' ' + node.directives.map(nodes.Directive!).join(' ');
if (node.directives && node.directives.length)
out += ' ' + mapJoin(node.directives, ' ', nodes.Directive);
return out;
},
InlineFragment(node) {
InlineFragment(node: InlineFragmentNode): string {
let out = '...';
if (node.typeCondition) out += ' on ' + node.typeCondition.name.value;
if (hasItems(node.directives)) out += ' ' + node.directives.map(nodes.Directive!).join(' ');
return out + ' ' + print(node.selectionSet);
if (node.directives && node.directives.length)
out += ' ' + mapJoin(node.directives, ' ', nodes.Directive);
out += ' ' + nodes.SelectionSet(node.selectionSet);
return out;
},
FragmentDefinition(node) {
FragmentDefinition(node: FragmentDefinitionNode): string {
let out = 'fragment ' + node.name.value;
out += ' on ' + node.typeCondition.name.value;
if (hasItems(node.directives)) out += ' ' + node.directives.map(nodes.Directive!).join(' ');
return out + ' ' + print(node.selectionSet);
if (node.directives && node.directives.length)
out += ' ' + mapJoin(node.directives, ' ', nodes.Directive);
return out + ' ' + nodes.SelectionSet(node.selectionSet);
},
Directive(node) {
Directive(node: DirectiveNode): string {
let out = '@' + node.name.value;
if (hasItems(node.arguments)) out += '(' + node.arguments.map(nodes.Argument!).join(', ') + ')';
if (node.arguments && node.arguments.length)
out += '(' + mapJoin(node.arguments, ', ', nodes.Argument) + ')';
return out;
},
NamedType(node) {
NamedType(node: NamedTypeNode): string {
return node.name.value;
},
ListType(node) {
return '[' + print(node.type) + ']';
ListType(node: ListTypeNode): string {
return '[' + _print(node.type) + ']';
},
NonNullType(node) {
return print(node.type) + '!';
NonNullType(node: NonNullTypeNode): string {
return _print(node.type) + '!';
},
};
} as const;

export function print(node: ASTNode): string {
return nodes[node.kind] ? (nodes as any)[node.kind]!(node) : '';
const _print = (node: ASTNode): string => nodes[node.kind](node);

function print(node: ASTNode): string {
LF = '\n';
return nodes[node.kind] ? nodes[node.kind](node) : '';
}

export { print, printString, printBlockString };

0 comments on commit 4f3e17a

Please sign in to comment.