Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved parse to support converting comments into descriptions #1900

Merged
merged 3 commits into from
Aug 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 2 additions & 3 deletions packages/load/src/load-typedefs/parse.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Source, printSchemaWithDirectives, fixSchemaAst } from '@graphql-tools/utils';
import { Source, printSchemaWithDirectives, fixSchemaAst, parseGraphQLSDL } from '@graphql-tools/utils';
import { printWithComments, resetComments } from '@graphql-tools/merge';
import { parse, Source as GraphQLSource } from 'graphql';
import { filterKind } from '../filter-document-kind';

type Options = any;
Expand Down Expand Up @@ -67,7 +66,7 @@ function parseSchema(input: Input) {

function parseRawSDL(input: Input) {
if (input.source.rawSDL) {
input.source.document = parse(new GraphQLSource(input.source.rawSDL, input.source.location), input.options);
input.source.document = parseGraphQLSDL(input.source.location, input.source.rawSDL, input.options).document;
}
}

Expand Down
3 changes: 2 additions & 1 deletion packages/loaders/graphql-file/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export class GraphQLFileLoader implements UniversalLoader<GraphQLFileLoaderOptio
},
};
}
return parseGraphQLSDL(pointer, rawSDL.trim(), options);

return parseGraphQLSDL(pointer, rawSDL, options);
}
}
8 changes: 4 additions & 4 deletions packages/schema/src/buildSchemaFromTypeDefinitions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { parse, extendSchema, buildASTSchema, GraphQLSchema, DocumentNode, ASTNode } from 'graphql';
import { extendSchema, buildASTSchema, GraphQLSchema, DocumentNode, ASTNode } from 'graphql';

import { ITypeDefinitions, GraphQLParseOptions } from '@graphql-tools/utils';
import { ITypeDefinitions, GraphQLParseOptions, parseGraphQLSDL } from '@graphql-tools/utils';

import { extractExtensionDefinitions, filterExtensionDefinitions } from './extensionDefinitions';
import { concatenateTypeDefs } from './concatenateTypeDefs';
Expand Down Expand Up @@ -33,9 +33,9 @@ export function buildDocumentFromTypeDefinitions(
): DocumentNode {
let document: DocumentNode;
if (typeof typeDefinitions === 'string') {
document = parse(typeDefinitions, parseOptions);
document = parseGraphQLSDL('', typeDefinitions, parseOptions).document;
} else if (Array.isArray(typeDefinitions)) {
document = parse(concatenateTypeDefs(typeDefinitions), parseOptions);
document = parseGraphQLSDL('', concatenateTypeDefs(typeDefinitions), parseOptions).document;
} else if (isDocumentNode(typeDefinitions)) {
document = typeDefinitions;
} else {
Expand Down
5 changes: 3 additions & 2 deletions packages/utils/src/loaders.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { DocumentNode, GraphQLSchema, ParseOptions, BuildSchemaOptions } from 'graphql';
import { DocumentNode, GraphQLSchema, BuildSchemaOptions } from 'graphql';
import { GraphQLSchemaValidationOptions } from 'graphql/type/schema';
import { ExtendedParseOptions } from './parse-graphql-sdl';

export interface Source {
document?: DocumentNode;
Expand All @@ -8,7 +9,7 @@ export interface Source {
location?: string;
}

export type SingleFileOptions = ParseOptions &
export type SingleFileOptions = ExtendedParseOptions &
GraphQLSchemaValidationOptions &
BuildSchemaOptions & {
cwd?: string;
Expand Down
5 changes: 3 additions & 2 deletions packages/utils/src/parse-graphql-json.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { buildClientSchema, parse, ParseOptions } from 'graphql';
import { buildClientSchema, ParseOptions } from 'graphql';
import { GraphQLSchemaValidationOptions } from 'graphql/type/schema';
import { printSchemaWithDirectives } from './print-schema-with-directives';
import { Source } from './loaders';
import { SchemaPrintOptions } from './types';
import { parseGraphQLSDL } from './parse-graphql-sdl';

function stripBOM(content: string): string {
content = content.toString();
Expand Down Expand Up @@ -44,7 +45,7 @@ export function parseGraphQLJSON(

return {
location,
document: parse(rawSDL, options),
document: parseGraphQLSDL(location, rawSDL, options).document,
rawSDL,
schema,
};
Expand Down
133 changes: 129 additions & 4 deletions packages/utils/src/parse-graphql-sdl.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,46 @@
import { ParseOptions, DocumentNode, parse, Kind, Source as GraphQLSource } from 'graphql';
import {
ParseOptions,
DocumentNode,
Kind,
TokenKind,
ASTNode,
parse,
Source as GraphQLSource,
visit,
isTypeSystemDefinitionNode,
StringValueNode,
print,
} from 'graphql';
import { dedentBlockStringValue } from 'graphql/language/blockString';

export function parseGraphQLSDL(location: string, rawSDL: string, options: ParseOptions) {
export interface ExtendedParseOptions extends ParseOptions {
/**
* Set to `true` in order to convert all GraphQL comments (marked with # sign) to descriptions (""")
* GraphQL has built-in support for transforming descriptions to comments (with `print`), but not while
* parsing. Turning the flag on will support the other way as well (`parse`)
*/
commentDescriptions?: boolean;
}

export function parseGraphQLSDL(location: string, rawSDL: string, options: ExtendedParseOptions = {}) {
let document: DocumentNode;
const sdl: string = rawSDL;
let sdlModified = false;

try {
document = parse(new GraphQLSource(rawSDL, location), options);
if (options.commentDescriptions && sdl.includes('#')) {
sdlModified = true;
document = transformCommentsToDescriptions(rawSDL, options);

// If noLocation=true, we need to make sure to print and parse it again, to remove locations,
// since `transformCommentsToDescriptions` must have locations set in order to transform the comments
// into descriptions.
if (options.noLocation) {
document = parse(print(document), options);
}
} else {
document = parse(new GraphQLSource(sdl, location), options);
}
} catch (e) {
if (e.message.includes('EOF')) {
document = {
Expand All @@ -14,9 +51,97 @@ export function parseGraphQLSDL(location: string, rawSDL: string, options: Parse
throw e;
}
}

return {
location,
document,
rawSDL,
rawSDL: sdlModified ? print(document) : sdl,
};
}

export function getLeadingCommentBlock(node: ASTNode): void | string {
const loc = node.loc;

if (!loc) {
return;
}

const comments = [];
let token = loc.startToken.prev;

while (
token != null &&
token.kind === TokenKind.COMMENT &&
token.next &&
token.prev &&
token.line + 1 === token.next.line &&
token.line !== token.prev.line
) {
const value = String(token.value);
comments.push(value);
token = token.prev;
}

return comments.length > 0 ? comments.reverse().join('\n') : undefined;
}

export function transformCommentsToDescriptions(
sourceSdl: string,
options: ExtendedParseOptions = {}
): DocumentNode | null {
const parsedDoc = parse(sourceSdl, {
...options,
noLocation: false,
});
const modifiedDoc = visit(parsedDoc, {
leave: (node: ASTNode) => {
if (isDescribable(node)) {
const rawValue = getLeadingCommentBlock(node);

if (rawValue !== undefined) {
const commentsBlock = dedentBlockStringValue('\n' + rawValue);
const isBlock = commentsBlock.includes('\n');

if (!node.description) {
return {
...node,
description: {
kind: Kind.STRING,
value: commentsBlock,
block: isBlock,
},
};
} else {
return {
...node,
description: {
...node.description,
value: node.description.value + '\n' + commentsBlock,
block: true,
},
};
}
}
}
},
});

return modifiedDoc;
}

type DiscriminateUnion<T, U> = T extends U ? T : never;
type DescribableASTNodes = DiscriminateUnion<
ASTNode,
{
description?: StringValueNode;
}
>;

export function isDescribable(node: ASTNode): node is DescribableASTNodes {
return (
isTypeSystemDefinitionNode(node) ||
node.kind === Kind.FIELD_DEFINITION ||
node.kind === Kind.INPUT_VALUE_DEFINITION ||
node.kind === Kind.ENUM_VALUE_DEFINITION
);
}
107 changes: 107 additions & 0 deletions packages/utils/tests/__snapshots__/parse-graphql-sdl.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`parse sdl comment parsing should transform comments to descriptions correctly on all available nodes 1`] = `
"\\"test type comment\\"
type Type {
\\"test field comment\\"
f1: String!
\\"\\"\\"
Line 1
Line 2
\\"\\"\\"
f2: String!
\\"\\"\\"
line 2
Line 1
\\"\\"\\"
f3: String!
}

extend type Type {
\\"test extension field comment\\"
f4: String!
}

type OtherType implements Node {
id: ID!
f: String!
}

\\"input test\\"
input Input {
\\"Input field test\\"
f: String
}

\\"Enum test\\"
enum Enum {
\\"Enum value test\\"
V1
V2
}

\\"Union test\\"
union Union = Type | OtherType

\\"Inferface test\\"
interface Node {
id: ID!
}

\\"Custom scalar test\\"
scalar Date
"
`;

exports[`parse sdl comment parsing should transform comments to descriptions correctly on all available nodes with noLocation=true 1`] = `
"\\"test type comment\\"
type Type {
\\"test field comment\\"
f1: String!
\\"\\"\\"
Line 1
Line 2
\\"\\"\\"
f2: String!
\\"\\"\\"
line 2
Line 1
\\"\\"\\"
f3: String!
}

extend type Type {
\\"test extension field comment\\"
f4: String!
}

type OtherType implements Node {
id: ID!
f: String!
}

\\"input test\\"
input Input {
\\"Input field test\\"
f: String
}

\\"Enum test\\"
enum Enum {
\\"Enum value test\\"
V1
V2
}

\\"Union test\\"
union Union = Type | OtherType

\\"Inferface test\\"
interface Node {
id: ID!
}

\\"Custom scalar test\\"
scalar Date
"
`;