diff --git a/src/language/__tests__/parser-test.ts b/src/language/__tests__/parser-test.ts index 34e9dff4b9..b3c4a6a417 100644 --- a/src/language/__tests__/parser-test.ts +++ b/src/language/__tests__/parser-test.ts @@ -11,6 +11,7 @@ import { kitchenSinkQuery } from '../../__testUtils__/kitchenSinkQuery.js'; import { inspect } from '../../jsutils/inspect.js'; import { Kind } from '../kinds.js'; +import type { ParseOptions } from '../parser.js'; import { parse, parseConstValue, parseType, parseValue } from '../parser.js'; import { Source } from '../source.js'; import { TokenKind } from '../tokenKind.js'; @@ -19,8 +20,8 @@ function parseCCN(source: string) { return parse(source, { experimentalClientControlledNullability: true }); } -function expectSyntaxError(text: string) { - return expectToThrowJSON(() => parse(text)); +function expectSyntaxError(text: string, options?: ParseOptions | undefined) { + return expectToThrowJSON(() => parse(text, options)); } describe('Parser', () => { @@ -73,6 +74,13 @@ describe('Parser', () => { message: 'Syntax Error: Expected Name, found String "".', locations: [{ line: 1, column: 3 }], }); + + expectSyntaxError('{ ""', { + experimentalParseStringLiteralAliases: true, + }).to.deep.include({ + message: 'Syntax Error: Expected ":", found .', + locations: [{ line: 1, column: 5 }], + }); }); it('parse provides useful error when using source', () => { diff --git a/src/language/parser.ts b/src/language/parser.ts index cd9345f6dd..aaf9d03694 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -130,6 +130,23 @@ export interface ParseOptions { * future. */ experimentalClientControlledNullability?: boolean | undefined; + + /** + * EXPERIMENTAL: + * + * If enabled, the parser will understand and parse StringValues as aliases. + * + * The syntax looks like the following: + * + * ```graphql + * { + * "alias": field + * } + * ``` + * Note: this feature is experimental and may change or be removed in the + * future. + */ + experimentalParseStringLiteralAliases?: boolean | undefined; } /** @@ -236,6 +253,17 @@ export class Parser { }); } + /** + * Converts a string lex token into a name parse node. + */ + parseStringLiteralAlias(): NameNode { + const token = this.expectToken(TokenKind.STRING); + return this.node(token, { + kind: Kind.NAME, + value: token.value, + }); + } + // Implements the parsing rules in the Document section. /** @@ -454,14 +482,23 @@ export class Parser { parseField(): FieldNode { const start = this._lexer.token; - const nameOrAlias = this.parseName(); let alias; let name; - if (this.expectOptionalToken(TokenKind.COLON)) { - alias = nameOrAlias; + if ( + this._options.experimentalParseStringLiteralAliases && + this.peek(TokenKind.STRING) + ) { + alias = this.parseStringLiteralAlias(); + this.expectToken(TokenKind.COLON); name = this.parseName(); } else { - name = nameOrAlias; + const nameOrAlias = this.parseName(); + if (this.expectOptionalToken(TokenKind.COLON)) { + alias = nameOrAlias; + name = this.parseName(); + } else { + name = nameOrAlias; + } } return this.node(start, { diff --git a/src/validation/__tests__/OverlappingFieldsCanBeMergedRule-test.ts b/src/validation/__tests__/OverlappingFieldsCanBeMergedRule-test.ts index ecb56a15cf..a795905916 100644 --- a/src/validation/__tests__/OverlappingFieldsCanBeMergedRule-test.ts +++ b/src/validation/__tests__/OverlappingFieldsCanBeMergedRule-test.ts @@ -1,5 +1,7 @@ import { describe, it } from 'mocha'; +import type { ParseOptions } from '../../language/parser.js'; + import type { GraphQLSchema } from '../../type/schema.js'; import { buildSchema } from '../../utilities/buildASTSchema.js'; @@ -11,12 +13,16 @@ import { expectValidationErrorsWithSchema, } from './harness.js'; -function expectErrors(queryStr: string) { - return expectValidationErrors(OverlappingFieldsCanBeMergedRule, queryStr); +function expectErrors(queryStr: string, options?: ParseOptions | undefined) { + return expectValidationErrors( + OverlappingFieldsCanBeMergedRule, + queryStr, + options, + ); } -function expectValid(queryStr: string) { - expectErrors(queryStr).toDeepEqual([]); +function expectValid(queryStr: string, options?: ParseOptions | undefined) { + expectErrors(queryStr, options).toDeepEqual([]); } function expectErrorsWithSchema(schema: GraphQLSchema, queryStr: string) { @@ -242,6 +248,39 @@ describe('Validate: Overlapping fields can be merged', () => { ]); }); + it('Same literal aliases allowed on same field targets', () => { + expectValid( + ` + fragment sameLiteralAliasesWithSameFieldTargets on Dog { + "fido": name + fido: name + } + `, + { experimentalParseStringLiteralAliases: true }, + ); + }); + + it('Same literal aliases with different field targets', () => { + expectErrors( + ` + fragment sameLiteralAliasesWithDifferentFieldTargets on Dog { + "fido": name + fido: nickname + } + `, + { experimentalParseStringLiteralAliases: true }, + ).toDeepEqual([ + { + message: + 'Fields "fido" conflict because "name" and "nickname" are different fields. Use different aliases on the fields to fetch both if this was intentional.', + locations: [ + { line: 3, column: 9 }, + { line: 4, column: 9 }, + ], + }, + ]); + }); + it('Same aliases allowed on non-overlapping fields', () => { // This is valid since no object can be both a "Dog" and a "Cat", thus // these fields can never overlap. diff --git a/src/validation/__tests__/harness.ts b/src/validation/__tests__/harness.ts index b7710ff9d9..00856ae3d0 100644 --- a/src/validation/__tests__/harness.ts +++ b/src/validation/__tests__/harness.ts @@ -2,6 +2,7 @@ import { expectJSON } from '../../__testUtils__/expectJSON.js'; import type { Maybe } from '../../jsutils/Maybe.js'; +import type { ParseOptions } from '../../language/parser.js'; import { parse } from '../../language/parser.js'; import type { GraphQLSchema } from '../../type/schema.js'; @@ -128,17 +129,18 @@ export function expectValidationErrorsWithSchema( schema: GraphQLSchema, rule: ValidationRule, queryStr: string, + options?: ParseOptions | undefined, ): any { - const doc = parse(queryStr); + const doc = parse(queryStr, options); const errors = validate(schema, doc, [rule]); return expectJSON(errors); } export function expectValidationErrors( rule: ValidationRule, - queryStr: string, + queryStr: string, options?: ParseOptions | undefined, ): any { - return expectValidationErrorsWithSchema(testSchema, rule, queryStr); + return expectValidationErrorsWithSchema(testSchema, rule, queryStr, options); } export function expectSDLValidationErrors(