Skip to content

Commit

Permalink
Improved parse to support converting comments into descriptions (#1900
Browse files Browse the repository at this point in the history
)

* added support for `commentDescriptions` in custom parse

* fixes

* lint fixes
  • Loading branch information
dotansimha committed Aug 11, 2020
1 parent 640de85 commit 46c5700
Show file tree
Hide file tree
Showing 8 changed files with 336 additions and 16 deletions.
5 changes: 2 additions & 3 deletions packages/load/src/load-typedefs/parse.ts
@@ -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
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
"
`;

0 comments on commit 46c5700

Please sign in to comment.