From ed6f97859f77637083a0dbeac12e7e15c2025179 Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Thu, 5 Jun 2025 12:51:00 -0500 Subject: [PATCH 1/6] fix coverage --- src/language/__tests__/printer-test.ts | 3 +++ src/utilities/__tests__/resolveSchemaCoordinate-test.ts | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/src/language/__tests__/printer-test.ts b/src/language/__tests__/printer-test.ts index 589d9bfc8d..9829184dd2 100644 --- a/src/language/__tests__/printer-test.ts +++ b/src/language/__tests__/printer-test.ts @@ -308,6 +308,9 @@ describe('Printer: Query document', () => { expect(print(parseSchemaCoordinate(' Name . field ( arg: )'))).to.equal( 'Name.field(arg:)', ); + expect(print(parseSchemaCoordinate(' Name :: Name '))).to.equal( + 'Name::Name', + ); expect(print(parseSchemaCoordinate(' @ name '))).to.equal('@name'); expect(print(parseSchemaCoordinate(' @ name (arg:) '))).to.equal( '@name(arg:)', diff --git a/src/utilities/__tests__/resolveSchemaCoordinate-test.ts b/src/utilities/__tests__/resolveSchemaCoordinate-test.ts index 0fa9cfdf10..f0fb5a884b 100644 --- a/src/utilities/__tests__/resolveSchemaCoordinate-test.ts +++ b/src/utilities/__tests__/resolveSchemaCoordinate-test.ts @@ -111,6 +111,14 @@ describe('resolveSchemaCoordinate', () => { expect( resolveSchemaCoordinate(schema, 'SearchFilter::UNKNOWN'), ).to.deep.equal(undefined); + + expect(() => resolveSchemaCoordinate(schema, 'Unknown::UNKNOWN')).to.throw( + 'Expected "Unknown" to be defined as a type in the schema.', + ); + + expect(() => resolveSchemaCoordinate(schema, 'Business::id')).to.throw( + 'Expected "Business" to be an Enum type.', + ); }); it('resolves a Field Argument', () => { From dc7e442121af560fe501220cb9917300c668c089 Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Fri, 6 Jun 2025 12:12:30 -0500 Subject: [PATCH 2/6] Revert "fix coverage" This reverts commit ed6f97859f77637083a0dbeac12e7e15c2025179. --- src/language/__tests__/printer-test.ts | 3 --- src/utilities/__tests__/resolveSchemaCoordinate-test.ts | 8 -------- 2 files changed, 11 deletions(-) diff --git a/src/language/__tests__/printer-test.ts b/src/language/__tests__/printer-test.ts index 9829184dd2..589d9bfc8d 100644 --- a/src/language/__tests__/printer-test.ts +++ b/src/language/__tests__/printer-test.ts @@ -308,9 +308,6 @@ describe('Printer: Query document', () => { expect(print(parseSchemaCoordinate(' Name . field ( arg: )'))).to.equal( 'Name.field(arg:)', ); - expect(print(parseSchemaCoordinate(' Name :: Name '))).to.equal( - 'Name::Name', - ); expect(print(parseSchemaCoordinate(' @ name '))).to.equal('@name'); expect(print(parseSchemaCoordinate(' @ name (arg:) '))).to.equal( '@name(arg:)', diff --git a/src/utilities/__tests__/resolveSchemaCoordinate-test.ts b/src/utilities/__tests__/resolveSchemaCoordinate-test.ts index f0fb5a884b..0fa9cfdf10 100644 --- a/src/utilities/__tests__/resolveSchemaCoordinate-test.ts +++ b/src/utilities/__tests__/resolveSchemaCoordinate-test.ts @@ -111,14 +111,6 @@ describe('resolveSchemaCoordinate', () => { expect( resolveSchemaCoordinate(schema, 'SearchFilter::UNKNOWN'), ).to.deep.equal(undefined); - - expect(() => resolveSchemaCoordinate(schema, 'Unknown::UNKNOWN')).to.throw( - 'Expected "Unknown" to be defined as a type in the schema.', - ); - - expect(() => resolveSchemaCoordinate(schema, 'Business::id')).to.throw( - 'Expected "Business" to be an Enum type.', - ); }); it('resolves a Field Argument', () => { From a2f8b9077185626a2aedb56a1ea966c87684aa83 Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Fri, 6 Jun 2025 17:08:27 -0500 Subject: [PATCH 3/6] Implement schema coordinate spec as of 2025-06-06 - Add support for meta-fields (e.g. `__typename`) - Add support for introspection types - Revert back from FieldCoordinate+ValueCoordinate -> MemberCoordinate --- src/language/__tests__/parser-test.ts | 22 +---- src/language/__tests__/predicates-test.ts | 3 +- src/language/ast.ts | 22 ++--- src/language/kinds_.ts | 7 +- src/language/lexer.ts | 9 -- src/language/parser.ts | 20 +---- src/language/predicates.ts | 3 +- src/language/printer.ts | 8 +- src/language/tokenKind.ts | 1 - .../__tests__/resolveSchemaCoordinate-test.ts | 68 +++++++++++++-- src/utilities/resolveSchemaCoordinate.ts | 82 +++++++------------ 11 files changed, 107 insertions(+), 138 deletions(-) diff --git a/src/language/__tests__/parser-test.ts b/src/language/__tests__/parser-test.ts index ba3ef79cd9..c0d247ddf5 100644 --- a/src/language/__tests__/parser-test.ts +++ b/src/language/__tests__/parser-test.ts @@ -703,14 +703,14 @@ describe('Parser', () => { it('parses Name . Name', () => { const result = parseSchemaCoordinate('MyType.field'); expectJSON(result).toDeepEqual({ - kind: Kind.FIELD_COORDINATE, + kind: Kind.MEMBER_COORDINATE, loc: { start: 0, end: 12 }, name: { kind: Kind.NAME, loc: { start: 0, end: 6 }, value: 'MyType', }, - fieldName: { + memberName: { kind: Kind.NAME, loc: { start: 7, end: 12 }, value: 'field', @@ -727,24 +727,6 @@ describe('Parser', () => { }); }); - it('parses Name :: Name', () => { - const result = parseSchemaCoordinate('MyEnum::value'); - expectJSON(result).toDeepEqual({ - kind: Kind.VALUE_COORDINATE, - loc: { start: 0, end: 13 }, - name: { - kind: Kind.NAME, - loc: { start: 0, end: 6 }, - value: 'MyEnum', - }, - valueName: { - kind: Kind.NAME, - loc: { start: 8, end: 13 }, - value: 'value', - }, - }); - }); - it('parses Name . Name ( Name : )', () => { const result = parseSchemaCoordinate('MyType.field(arg:)'); expectJSON(result).toDeepEqual({ diff --git a/src/language/__tests__/predicates-test.ts b/src/language/__tests__/predicates-test.ts index 7455fd73e4..57907d6aa6 100644 --- a/src/language/__tests__/predicates-test.ts +++ b/src/language/__tests__/predicates-test.ts @@ -148,9 +148,8 @@ describe('AST node predicates', () => { 'ArgumentCoordinate', 'DirectiveArgumentCoordinate', 'DirectiveCoordinate', - 'FieldCoordinate', + 'MemberCoordinate', 'TypeCoordinate', - 'ValueCoordinate', ]); }); }); diff --git a/src/language/ast.ts b/src/language/ast.ts index 268a2ddd98..812b988835 100644 --- a/src/language/ast.ts +++ b/src/language/ast.ts @@ -183,9 +183,8 @@ export type ASTNode = | EnumTypeExtensionNode | InputObjectTypeExtensionNode | TypeCoordinateNode - | FieldCoordinateNode + | MemberCoordinateNode | ArgumentCoordinateNode - | ValueCoordinateNode | DirectiveCoordinateNode | DirectiveArgumentCoordinateNode; @@ -296,9 +295,8 @@ export const QueryDocumentKeys: { // Schema Coordinates TypeCoordinate: ['name'], - FieldCoordinate: ['name', 'fieldName'], + MemberCoordinate: ['name', 'memberName'], ArgumentCoordinate: ['name', 'fieldName', 'argumentName'], - ValueCoordinate: ['name', 'valueName'], DirectiveCoordinate: ['name'], DirectiveArgumentCoordinate: ['name', 'argumentName'], }; @@ -781,9 +779,8 @@ export interface InputObjectTypeExtensionNode { export type SchemaCoordinateNode = | TypeCoordinateNode - | FieldCoordinateNode + | MemberCoordinateNode | ArgumentCoordinateNode - | ValueCoordinateNode | DirectiveCoordinateNode | DirectiveArgumentCoordinateNode; @@ -793,11 +790,11 @@ export interface TypeCoordinateNode { readonly name: NameNode; } -export interface FieldCoordinateNode { - readonly kind: typeof Kind.FIELD_COORDINATE; +export interface MemberCoordinateNode { + readonly kind: typeof Kind.MEMBER_COORDINATE; readonly loc?: Location; readonly name: NameNode; - readonly fieldName: NameNode; + readonly memberName: NameNode; } export interface ArgumentCoordinateNode { @@ -808,13 +805,6 @@ export interface ArgumentCoordinateNode { readonly argumentName: NameNode; } -export interface ValueCoordinateNode { - readonly kind: typeof Kind.VALUE_COORDINATE; - readonly loc?: Location; - readonly name: NameNode; - readonly valueName: NameNode; -} - export interface DirectiveCoordinateNode { readonly kind: typeof Kind.DIRECTIVE_COORDINATE; readonly loc?: Location; diff --git a/src/language/kinds_.ts b/src/language/kinds_.ts index 24d909fdfe..252feb6107 100644 --- a/src/language/kinds_.ts +++ b/src/language/kinds_.ts @@ -113,15 +113,12 @@ export type INPUT_OBJECT_TYPE_EXTENSION = typeof INPUT_OBJECT_TYPE_EXTENSION; export const TYPE_COORDINATE = 'TypeCoordinate'; export type TYPE_COORDINATE = typeof TYPE_COORDINATE; -export const FIELD_COORDINATE = 'FieldCoordinate'; -export type FIELD_COORDINATE = typeof FIELD_COORDINATE; +export const MEMBER_COORDINATE = 'MemberCoordinate'; +export type MEMBER_COORDINATE = typeof MEMBER_COORDINATE; export const ARGUMENT_COORDINATE = 'ArgumentCoordinate'; export type ARGUMENT_COORDINATE = typeof ARGUMENT_COORDINATE; -export const VALUE_COORDINATE = 'ValueCoordinate'; -export type VALUE_COORDINATE = typeof VALUE_COORDINATE; - export const DIRECTIVE_COORDINATE = 'DirectiveCoordinate'; export type DIRECTIVE_COORDINATE = typeof DIRECTIVE_COORDINATE; diff --git a/src/language/lexer.ts b/src/language/lexer.ts index a2d305e645..44abc05197 100644 --- a/src/language/lexer.ts +++ b/src/language/lexer.ts @@ -98,7 +98,6 @@ export function isPunctuatorTokenKind(kind: TokenKind): boolean { kind === TokenKind.DOT || kind === TokenKind.SPREAD || kind === TokenKind.COLON || - kind === TokenKind.TWO_COLON || kind === TokenKind.EQUALS || kind === TokenKind.AT || kind === TokenKind.BRACKET_L || @@ -272,14 +271,6 @@ function readNextToken(lexer: Lexer, start: number): Token { return readDot(lexer, position); } case 0x003a: // : - if (body.charCodeAt(position + 1) === 0x003a) { - return createToken( - lexer, - TokenKind.TWO_COLON, - position, - position + 2, - ); - } return createToken(lexer, TokenKind.COLON, position, position + 1); case 0x003d: // = return createToken(lexer, TokenKind.EQUALS, position, position + 1); diff --git a/src/language/parser.ts b/src/language/parser.ts index 31fa99d074..de049abeb5 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -23,7 +23,6 @@ import type { EnumTypeExtensionNode, EnumValueDefinitionNode, EnumValueNode, - FieldCoordinateNode, FieldDefinitionNode, FieldNode, FloatValueNode, @@ -39,6 +38,7 @@ import type { IntValueNode, ListTypeNode, ListValueNode, + MemberCoordinateNode, NamedTypeNode, NameNode, NonNullTypeNode, @@ -63,7 +63,6 @@ import type { TypeSystemExtensionNode, UnionTypeDefinitionNode, UnionTypeExtensionNode, - ValueCoordinateNode, ValueNode, VariableDefinitionNode, VariableNode, @@ -1467,7 +1466,6 @@ export class Parser { * - Name * - Name . Name * - Name . Name ( Name : ) - * - Name :: Name * - @ Name * - @ Name ( Name : ) */ @@ -1475,16 +1473,6 @@ export class Parser { const start = this._lexer.token; const ofDirective = this.expectOptionalToken(TokenKind.AT); const name = this.parseName(); - - if (!ofDirective && this.expectOptionalToken(TokenKind.TWO_COLON)) { - const valueName = this.parseName(); - return this.node(start, { - kind: Kind.VALUE_COORDINATE, - name, - valueName, - }); - } - let memberName: NameNode | undefined; if (!ofDirective && this.expectOptionalToken(TokenKind.DOT)) { memberName = this.parseName(); @@ -1520,10 +1508,10 @@ export class Parser { argumentName, }); } - return this.node(start, { - kind: Kind.FIELD_COORDINATE, + return this.node(start, { + kind: Kind.MEMBER_COORDINATE, name, - fieldName: memberName, + memberName, }); } diff --git a/src/language/predicates.ts b/src/language/predicates.ts index 488e9828f2..5146e8244e 100644 --- a/src/language/predicates.ts +++ b/src/language/predicates.ts @@ -117,9 +117,8 @@ export function isSchemaCoordinateNode( ): node is SchemaCoordinateNode { return ( node.kind === Kind.TYPE_COORDINATE || - node.kind === Kind.FIELD_COORDINATE || + node.kind === Kind.MEMBER_COORDINATE || node.kind === Kind.ARGUMENT_COORDINATE || - node.kind === Kind.VALUE_COORDINATE || node.kind === Kind.DIRECTIVE_COORDINATE || node.kind === Kind.DIRECTIVE_ARGUMENT_COORDINATE ); diff --git a/src/language/printer.ts b/src/language/printer.ts index 2701f8373b..823b14a02d 100644 --- a/src/language/printer.ts +++ b/src/language/printer.ts @@ -325,8 +325,8 @@ const printDocASTReducer: ASTReducer = { TypeCoordinate: { leave: ({ name }) => name }, - FieldCoordinate: { - leave: ({ name, fieldName }) => join([name, wrap('.', fieldName)]), + MemberCoordinate: { + leave: ({ name, memberName }) => join([name, wrap('.', memberName)]), }, ArgumentCoordinate: { @@ -334,10 +334,6 @@ const printDocASTReducer: ASTReducer = { join([name, wrap('.', fieldName), wrap('(', argumentName, ':)')]), }, - ValueCoordinate: { - leave: ({ name, valueName }) => join([name, wrap('::', valueName)]), - }, - DirectiveCoordinate: { leave: ({ name }) => join(['@', name]) }, DirectiveArgumentCoordinate: { diff --git a/src/language/tokenKind.ts b/src/language/tokenKind.ts index b14fe45a05..eae0972b81 100644 --- a/src/language/tokenKind.ts +++ b/src/language/tokenKind.ts @@ -13,7 +13,6 @@ export const TokenKind = { DOT: '.' as const, SPREAD: '...' as const, COLON: ':' as const, - TWO_COLON: '::' as const, EQUALS: '=' as const, AT: '@' as const, BRACKET_L: '[' as const, diff --git a/src/utilities/__tests__/resolveSchemaCoordinate-test.ts b/src/utilities/__tests__/resolveSchemaCoordinate-test.ts index 0fa9cfdf10..42d4310e0e 100644 --- a/src/utilities/__tests__/resolveSchemaCoordinate-test.ts +++ b/src/utilities/__tests__/resolveSchemaCoordinate-test.ts @@ -3,6 +3,7 @@ import { describe, it } from 'mocha'; import type { GraphQLEnumType, + GraphQLField, GraphQLInputObjectType, GraphQLObjectType, } from '../../type/definition.js'; @@ -71,16 +72,10 @@ describe('resolveSchemaCoordinate', () => { ); expect(() => resolveSchemaCoordinate(schema, 'String.field')).to.throw( - 'Expected "String" to be an Input Object, Object or Interface type.', + 'Expected "String" to be an Enum, Input Object, Object or Interface type.', ); }); - it('does not resolve meta-fields', () => { - expect( - resolveSchemaCoordinate(schema, 'Business.__typename'), - ).to.deep.equal(undefined); - }); - it('resolves a Input Field', () => { const type = schema.getType('SearchCriteria') as GraphQLInputObjectType; const inputField = type.getFields().filter; @@ -101,7 +96,7 @@ describe('resolveSchemaCoordinate', () => { const type = schema.getType('SearchFilter') as GraphQLEnumType; const enumValue = type.getValue('OPEN_NOW'); expect( - resolveSchemaCoordinate(schema, 'SearchFilter::OPEN_NOW'), + resolveSchemaCoordinate(schema, 'SearchFilter.OPEN_NOW'), ).to.deep.equal({ kind: 'EnumValue', type, @@ -109,7 +104,7 @@ describe('resolveSchemaCoordinate', () => { }); expect( - resolveSchemaCoordinate(schema, 'SearchFilter::UNKNOWN'), + resolveSchemaCoordinate(schema, 'SearchFilter.UNKNOWN'), ).to.deep.equal(undefined); }); @@ -186,4 +181,59 @@ describe('resolveSchemaCoordinate', () => { 'Expected "unknown" to be defined as a directive in the schema.', ); }); + + it('resolves a meta-field', () => { + const type = schema.getType('Business') as GraphQLObjectType; + const field = schema.getField(type, '__typename'); + expect( + resolveSchemaCoordinate(schema, 'Business.__typename'), + ).to.deep.equal({ + kind: 'Field', + type, + field, + }); + }); + + it('resolves a meta-field argument', () => { + const type = schema.getType('Query') as GraphQLObjectType; + const field = schema.getField(type, '__type') as GraphQLField; + const fieldArgument = field.args.find((arg) => arg.name === 'name'); + expect( + resolveSchemaCoordinate(schema, 'Query.__type(name:)'), + ).to.deep.equal({ + kind: 'FieldArgument', + type, + field, + fieldArgument, + }); + }); + + it('resolves an Introspection Type', () => { + expect(resolveSchemaCoordinate(schema, '__Type')).to.deep.equal({ + kind: 'NamedType', + type: schema.getType('__Type'), + }); + }); + + it('resolves an Introspection Type Field', () => { + const type = schema.getType('__Directive') as GraphQLObjectType; + const field = type.getFields().name; + expect(resolveSchemaCoordinate(schema, '__Directive.name')).to.deep.equal({ + kind: 'Field', + type, + field, + }); + }); + + it('resolves an Introspection Type Enum Value', () => { + const type = schema.getType('__DirectiveLocation') as GraphQLEnumType; + const enumValue = type.getValue('INLINE_FRAGMENT'); + expect( + resolveSchemaCoordinate(schema, '__DirectiveLocation.INLINE_FRAGMENT'), + ).to.deep.equal({ + kind: 'EnumValue', + type, + enumValue, + }); + }); }); diff --git a/src/utilities/resolveSchemaCoordinate.ts b/src/utilities/resolveSchemaCoordinate.ts index afebe13199..3613a07f16 100644 --- a/src/utilities/resolveSchemaCoordinate.ts +++ b/src/utilities/resolveSchemaCoordinate.ts @@ -4,10 +4,9 @@ import type { ArgumentCoordinateNode, DirectiveArgumentCoordinateNode, DirectiveCoordinateNode, - FieldCoordinateNode, + MemberCoordinateNode, SchemaCoordinateNode, TypeCoordinateNode, - ValueCoordinateNode, } from '../language/ast.js'; import { Kind } from '../language/kinds.js'; import { parseSchemaCoordinate } from '../language/parser.js'; @@ -119,37 +118,52 @@ function resolveTypeCoordinate( } /** - * FieldCoordinate : Name . Name + * MemberCoordinate : Name . Name */ -function resolveFieldCoordinate( +function resolveMemberCoordinate( schema: GraphQLSchema, - schemaCoordinate: FieldCoordinateNode, -): ResolvedField | ResolvedInputField | undefined { + schemaCoordinate: MemberCoordinateNode, +): ResolvedField | ResolvedInputField | ResolvedEnumValue | undefined { // 1. Let {typeName} be the value of the first {Name}. // 2. Let {type} be the type in the {schema} named {typeName}. const typeName = schemaCoordinate.name.value; const type = schema.getType(typeName); - // 3. Assert: {type} must exist, and must be an Input Object, Object or Interface type. + // 3. Assert: {type} must exist, and must be an Enum, Input Object, Object or Interface type. if (!type) { throw new Error( `Expected ${inspect(typeName)} to be defined as a type in the schema.`, ); } if ( + !isEnumType(type) && !isInputObjectType(type) && !isObjectType(type) && !isInterfaceType(type) ) { throw new Error( - `Expected ${inspect(typeName)} to be an Input Object, Object or Interface type.`, + `Expected ${inspect(typeName)} to be an Enum, Input Object, Object or Interface type.`, ); } - // 4. If {type} is an Input Object type: + // 4. If {type} is an Enum type: + if (isEnumType(type)) { + // 1. Let {enumValueName} be the value of the second {Name}. + const enumValueName = schemaCoordinate.memberName.value; + const enumValue = type.getValue(enumValueName); + + // 2. Return the enum value of {type} named {enumValueName}, or {null} if no such value exists. + if (enumValue == null) { + return; + } + + return { kind: 'EnumValue', type, enumValue }; + } + + // 5. Otherwise, if {type} is an Input Object type: if (isInputObjectType(type)) { // 1. Let {inputFieldName} be the value of the second {Name}. - const inputFieldName = schemaCoordinate.fieldName.value; + const inputFieldName = schemaCoordinate.memberName.value; const inputField = type.getFields()[inputFieldName]; // 2. Return the input field of {type} named {inputFieldName}, or {null} if no such input field exists. @@ -160,10 +174,10 @@ function resolveFieldCoordinate( return { kind: 'InputField', type, inputField }; } - // 5. Otherwise: + // 6. Otherwise: // 1. Let {fieldName} be the value of the second {Name}. - const fieldName = schemaCoordinate.fieldName.value; - const field = type.getFields()[fieldName]; + const fieldName = schemaCoordinate.memberName.value; + const field = schema.getField(type, fieldName); // 2. Return the field of {type} named {fieldName}, or {null} if no such field exists. if (field == null) { @@ -200,7 +214,7 @@ function resolveArgumentCoordinate( // 4. Let {fieldName} be the value of the second {Name}. // 5. Let {field} be the field of {type} named {fieldName}. const fieldName = schemaCoordinate.fieldName.value; - const field = type.getFields()[fieldName]; + const field = schema.getField(type, fieldName); // 7. Assert: {field} must exist. if (field == null) { @@ -223,40 +237,6 @@ function resolveArgumentCoordinate( return { kind: 'FieldArgument', type, field, fieldArgument }; } -/** - * ValueCoordinate : Name :: Name - */ -function resolveValueCoordinate( - schema: GraphQLSchema, - schemaCoordinate: ValueCoordinateNode, -): ResolvedEnumValue | undefined { - // 1. Let {typeName} be the value of the first {Name}. - // 2. Let {type} be the type in the {schema} named {typeName}. - const typeName = schemaCoordinate.name.value; - const type = schema.getType(typeName); - - // 3. Assert: {type} must exist, and must be an Enum type. - if (!type) { - throw new Error( - `Expected ${inspect(typeName)} to be defined as a type in the schema.`, - ); - } - if (!isEnumType(type)) { - throw new Error(`Expected ${inspect(typeName)} to be an Enum type.`); - } - - // 4. Let {enumValueName} be the value of the second {Name}. - const enumValueName = schemaCoordinate.valueName.value; - const enumValue = type.getValue(enumValueName); - - // 5. Return the enum value of {type} named {enumValueName}, or {null} if no such value exists. - if (enumValue == null) { - return; - } - - return { kind: 'EnumValue', type, enumValue }; -} - /** * DirectiveCoordinate : @ Name */ @@ -321,12 +301,10 @@ export function resolveASTSchemaCoordinate( switch (schemaCoordinate.kind) { case Kind.TYPE_COORDINATE: return resolveTypeCoordinate(schema, schemaCoordinate); - case Kind.FIELD_COORDINATE: - return resolveFieldCoordinate(schema, schemaCoordinate); + case Kind.MEMBER_COORDINATE: + return resolveMemberCoordinate(schema, schemaCoordinate); case Kind.ARGUMENT_COORDINATE: return resolveArgumentCoordinate(schema, schemaCoordinate); - case Kind.VALUE_COORDINATE: - return resolveValueCoordinate(schema, schemaCoordinate); case Kind.DIRECTIVE_COORDINATE: return resolveDirectiveCoordinate(schema, schemaCoordinate); case Kind.DIRECTIVE_ARGUMENT_COORDINATE: From d580d7f04f67094f4a0a8026e186523b5a268cd0 Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Wed, 25 Jun 2025 17:21:41 -0500 Subject: [PATCH 4/6] update spec wording --- src/utilities/resolveSchemaCoordinate.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/utilities/resolveSchemaCoordinate.ts b/src/utilities/resolveSchemaCoordinate.ts index 3613a07f16..db780930a0 100644 --- a/src/utilities/resolveSchemaCoordinate.ts +++ b/src/utilities/resolveSchemaCoordinate.ts @@ -109,7 +109,7 @@ function resolveTypeCoordinate( const typeName = schemaCoordinate.name.value; const type = schema.getType(typeName); - // 2. Return the type in the {schema} named {typeName}, or {null} if no such type exists. + // 2. Return the type in the {schema} named {typeName} if it exists. if (type == null) { return; } @@ -152,7 +152,7 @@ function resolveMemberCoordinate( const enumValueName = schemaCoordinate.memberName.value; const enumValue = type.getValue(enumValueName); - // 2. Return the enum value of {type} named {enumValueName}, or {null} if no such value exists. + // 2. Return the enum value of {type} named {enumValueName} if it exists. if (enumValue == null) { return; } @@ -166,7 +166,7 @@ function resolveMemberCoordinate( const inputFieldName = schemaCoordinate.memberName.value; const inputField = type.getFields()[inputFieldName]; - // 2. Return the input field of {type} named {inputFieldName}, or {null} if no such input field exists. + // 2. Return the input field of {type} named {inputFieldName} if it exists. if (inputField == null) { return; } @@ -179,7 +179,7 @@ function resolveMemberCoordinate( const fieldName = schemaCoordinate.memberName.value; const field = schema.getField(type, fieldName); - // 2. Return the field of {type} named {fieldName}, or {null} if no such field exists. + // 2. Return the field of {type} named {fieldName} if it exists. if (field == null) { return; } @@ -229,7 +229,7 @@ function resolveArgumentCoordinate( (arg) => arg.name === fieldArgumentName, ); - // 8. Return the argument of {field} named {fieldArgumentName}, or {null} if no such argument exists. + // 8. Return the argument of {field} named {fieldArgumentName} if it exists. if (fieldArgument == null) { return; } @@ -248,7 +248,7 @@ function resolveDirectiveCoordinate( const directiveName = schemaCoordinate.name.value; const directive = schema.getDirective(directiveName); - // 2. Return the directive in the {schema} named {directiveName}, or {null} if no such directive exists. + // 2. Return the directive in the {schema} named {directiveName} if it exists. if (!directive) { return; } @@ -283,7 +283,7 @@ function resolveDirectiveArgumentCoordinate( (arg) => arg.name === directiveArgumentName, ); - // 5. Return the argument of {directive} named {directiveArgumentName}, or {null} if no such argument exists. + // 5. Return the argument of {directive} named {directiveArgumentName} if it exists. if (!directiveArgument) { return; } From 49eba18d5eb44e3f912a3af253ef782f1f9388fe Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Fri, 4 Jul 2025 01:06:41 -0500 Subject: [PATCH 5/6] no ignored tokens for schema coordinates --- src/language/__tests__/parser-test.ts | 4 ++-- src/language/__tests__/printer-test.ts | 24 +++++++++++++------- src/language/lexer.ts | 31 +++++++++++++++++++++++++- src/language/parser.ts | 22 +++++++++++++++--- 4 files changed, 67 insertions(+), 14 deletions(-) diff --git a/src/language/__tests__/parser-test.ts b/src/language/__tests__/parser-test.ts index c0d247ddf5..e8dd914f71 100644 --- a/src/language/__tests__/parser-test.ts +++ b/src/language/__tests__/parser-test.ts @@ -751,11 +751,11 @@ describe('Parser', () => { }); it('rejects Name . Name ( Name : Name )', () => { - expect(() => parseSchemaCoordinate('MyType.field(arg: value)')) + expect(() => parseSchemaCoordinate('MyType.field(arg:value)')) .to.throw() .to.deep.include({ message: 'Syntax Error: Expected ")", found Name "value".', - locations: [{ line: 1, column: 19 }], + locations: [{ line: 1, column: 18 }], }); }); diff --git a/src/language/__tests__/printer-test.ts b/src/language/__tests__/printer-test.ts index 589d9bfc8d..a7a604bcba 100644 --- a/src/language/__tests__/printer-test.ts +++ b/src/language/__tests__/printer-test.ts @@ -301,16 +301,24 @@ describe('Printer: Query document', () => { }); it('prints schema coordinates', () => { - expect(print(parseSchemaCoordinate(' Name '))).to.equal('Name'); - expect(print(parseSchemaCoordinate(' Name . field '))).to.equal( - 'Name.field', - ); - expect(print(parseSchemaCoordinate(' Name . field ( arg: )'))).to.equal( + expect(print(parseSchemaCoordinate('Name'))).to.equal('Name'); + expect(print(parseSchemaCoordinate('Name.field'))).to.equal('Name.field'); + expect(print(parseSchemaCoordinate('Name.field(arg:)'))).to.equal( 'Name.field(arg:)', ); - expect(print(parseSchemaCoordinate(' @ name '))).to.equal('@name'); - expect(print(parseSchemaCoordinate(' @ name (arg:) '))).to.equal( - '@name(arg:)', + expect(print(parseSchemaCoordinate('@name'))).to.equal('@name'); + expect(print(parseSchemaCoordinate('@name(arg:)'))).to.equal('@name(arg:)'); + }); + + it('throws syntax error for ignored tokens in schema coordinates', () => { + expect(() => print(parseSchemaCoordinate('# foo\nName'))).to.throw( + 'Syntax Error: Invalid character: "#"', + ); + expect(() => print(parseSchemaCoordinate('\nName'))).to.throw( + 'Syntax Error: Invalid character: U+000A.', + ); + expect(() => print(parseSchemaCoordinate('Name .field'))).to.throw( + 'Syntax Error: Invalid character: " "', ); }); }); diff --git a/src/language/lexer.ts b/src/language/lexer.ts index 44abc05197..f0fc6e5fb5 100644 --- a/src/language/lexer.ts +++ b/src/language/lexer.ts @@ -6,6 +6,18 @@ import { isDigit, isNameContinue, isNameStart } from './characterClasses.js'; import type { Source } from './source.js'; import { TokenKind } from './tokenKind.js'; +/** + * Configuration options to control lexer behavior + */ +export interface LexerOptions { + /** + * By default, ignored tokens are valid syntax and ignored when lexing. + * This may be disabled for certain grammars that specifically disallow + * ignored tokens (e.g. schema coordinates). + */ + noIgnoredTokens?: boolean | undefined; +} + /** * Given a Source object, creates a Lexer for that source. * A Lexer is a stateful stream generator in that every time @@ -37,7 +49,9 @@ export class Lexer { */ lineStart: number; - constructor(source: Source) { + protected _options: LexerOptions; + + constructor(source: Source, options: LexerOptions = {}) { const startOfFileToken = new Token(TokenKind.SOF, 0, 0, 0, 0); this.source = source; @@ -45,6 +59,7 @@ export class Lexer { this.token = startOfFileToken; this.line = 1; this.lineStart = 0; + this._options = options; } get [Symbol.toStringTag]() { @@ -83,6 +98,16 @@ export class Lexer { } return token; } + + validateIgnoredToken(position: number): void { + if (this._options.noIgnoredTokens === true) { + throw syntaxError( + this.source, + position, + `Invalid character: ${printCodePointAt(this, position)}.`, + ); + } + } } /** @@ -217,6 +242,7 @@ function readNextToken(lexer: Lexer, start: number): Token { case 0x0009: // \t case 0x0020: // case 0x002c: // , + lexer.validateIgnoredToken(position); ++position; continue; // LineTerminator :: @@ -224,11 +250,13 @@ function readNextToken(lexer: Lexer, start: number): Token { // - "Carriage Return (U+000D)" [lookahead != "New Line (U+000A)"] // - "Carriage Return (U+000D)" "New Line (U+000A)" case 0x000a: // \n + lexer.validateIgnoredToken(position); ++position; ++lexer.line; lexer.lineStart = position; continue; case 0x000d: // \r + lexer.validateIgnoredToken(position); if (body.charCodeAt(position + 1) === 0x000a) { position += 2; } else { @@ -239,6 +267,7 @@ function readNextToken(lexer: Lexer, start: number): Token { continue; // Comment case 0x0023: // # + lexer.validateIgnoredToken(position); return readComment(lexer, position); // Token :: // - Punctuator diff --git a/src/language/parser.ts b/src/language/parser.ts index 5acfb4e85d..5335343810 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -70,6 +70,7 @@ import type { import { Location, OperationTypeNode } from './ast.js'; import { DirectiveLocation } from './directiveLocation.js'; import { Kind } from './kinds.js'; +import type { LexerOptions } from './lexer.js'; import { isPunctuatorTokenKind, Lexer } from './lexer.js'; import { isSource, Source } from './source.js'; import { TokenKind } from './tokenKind.js'; @@ -78,6 +79,14 @@ import { TokenKind } from './tokenKind.js'; * Configuration options to control parser behavior */ export interface ParseOptions { + /** + * By default, ignored tokens are valid syntax and ignored when lexing. + * This may be disabled for certain grammars that specifically disallow + * ignored tokens (e.g. schema coordinates). + * This is passed down to the lexer to enforce. + */ + noIgnoredTokens?: boolean | undefined; + /** * By default, the parser creates AST nodes that know the location * in the source that they correspond to. This configuration flag @@ -199,9 +208,12 @@ export function parseType( */ export function parseSchemaCoordinate( source: string | Source, - options?: ParseOptions, + options: ParseOptions & { noIgnoredTokens?: true } = {}, ): SchemaCoordinateNode { - const parser = new Parser(source, options); + // Ignored tokens are excluded syntax for the schema coordinates. + const _options = { ...options, noIgnoredTokens: true }; + + const parser = new Parser(source, _options); parser.expectToken(TokenKind.SOF); const coordinate = parser.parseSchemaCoordinate(); parser.expectToken(TokenKind.EOF); @@ -227,7 +239,11 @@ export class Parser { constructor(source: string | Source, options: ParseOptions = {}) { const sourceObj = isSource(source) ? source : new Source(source); - this._lexer = new Lexer(sourceObj); + const lexerOptions: LexerOptions = { + noIgnoredTokens: options.noIgnoredTokens ?? false, + }; + + this._lexer = new Lexer(sourceObj, lexerOptions); this._options = options; this._tokenCounter = 0; } From dac9234bc6952f90341f004026ea1adedf64ed99 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 4 Jul 2025 11:20:50 +0100 Subject: [PATCH 6/6] Subclass Lexer for schema coordinates --- src/index.ts | 2 ++ src/language/index.ts | 4 ++-- src/language/lexer.ts | 44 +++++++++++++++++++---------------------- src/language/parser.ts | 45 +++++++++++++++++++++++++----------------- 4 files changed, 51 insertions(+), 44 deletions(-) diff --git a/src/index.ts b/src/index.ts index 1f80cf51f3..ddc799e2ba 100644 --- a/src/index.ts +++ b/src/index.ts @@ -230,6 +230,7 @@ export { printSourceLocation, // Lex Lexer, + SchemaCoordinateLexer, TokenKind, // Parse parse, @@ -261,6 +262,7 @@ export { export type { ParseOptions, + ParseSchemaCoordinateOptions, SourceLocation, // Visitor utilities ASTVisitor, diff --git a/src/language/index.ts b/src/language/index.ts index c5620b4948..1f2eff6bb7 100644 --- a/src/language/index.ts +++ b/src/language/index.ts @@ -11,7 +11,7 @@ export { Kind } from './kinds.js'; export { TokenKind } from './tokenKind.js'; -export { Lexer } from './lexer.js'; +export { Lexer, SchemaCoordinateLexer } from './lexer.js'; export { parse, @@ -20,7 +20,7 @@ export { parseType, parseSchemaCoordinate, } from './parser.js'; -export type { ParseOptions } from './parser.js'; +export type { ParseOptions, ParseSchemaCoordinateOptions } from './parser.js'; export { print } from './printer.js'; diff --git a/src/language/lexer.ts b/src/language/lexer.ts index f0fc6e5fb5..4a2228e285 100644 --- a/src/language/lexer.ts +++ b/src/language/lexer.ts @@ -6,18 +6,6 @@ import { isDigit, isNameContinue, isNameStart } from './characterClasses.js'; import type { Source } from './source.js'; import { TokenKind } from './tokenKind.js'; -/** - * Configuration options to control lexer behavior - */ -export interface LexerOptions { - /** - * By default, ignored tokens are valid syntax and ignored when lexing. - * This may be disabled for certain grammars that specifically disallow - * ignored tokens (e.g. schema coordinates). - */ - noIgnoredTokens?: boolean | undefined; -} - /** * Given a Source object, creates a Lexer for that source. * A Lexer is a stateful stream generator in that every time @@ -49,9 +37,7 @@ export class Lexer { */ lineStart: number; - protected _options: LexerOptions; - - constructor(source: Source, options: LexerOptions = {}) { + constructor(source: Source) { const startOfFileToken = new Token(TokenKind.SOF, 0, 0, 0, 0); this.source = source; @@ -59,7 +45,6 @@ export class Lexer { this.token = startOfFileToken; this.line = 1; this.lineStart = 0; - this._options = options; } get [Symbol.toStringTag]() { @@ -99,14 +84,25 @@ export class Lexer { return token; } - validateIgnoredToken(position: number): void { - if (this._options.noIgnoredTokens === true) { - throw syntaxError( - this.source, - position, - `Invalid character: ${printCodePointAt(this, position)}.`, - ); - } + validateIgnoredToken(_position: number): void { + /* noop - ignored tokens are ignored */ + } +} + +/** + * As `Lexer`, but forbids ignored tokens as required of schema coordinates. + */ +export class SchemaCoordinateLexer extends Lexer { + override get [Symbol.toStringTag]() { + return 'SchemaCoordinateLexer'; + } + + override validateIgnoredToken(position: number): void { + throw syntaxError( + this.source, + position, + `Invalid character: ${printCodePointAt(this, position)}.`, + ); } } diff --git a/src/language/parser.ts b/src/language/parser.ts index 5335343810..b1c914b68a 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -70,8 +70,11 @@ import type { import { Location, OperationTypeNode } from './ast.js'; import { DirectiveLocation } from './directiveLocation.js'; import { Kind } from './kinds.js'; -import type { LexerOptions } from './lexer.js'; -import { isPunctuatorTokenKind, Lexer } from './lexer.js'; +import { + isPunctuatorTokenKind, + Lexer, + SchemaCoordinateLexer, +} from './lexer.js'; import { isSource, Source } from './source.js'; import { TokenKind } from './tokenKind.js'; @@ -79,14 +82,6 @@ import { TokenKind } from './tokenKind.js'; * Configuration options to control parser behavior */ export interface ParseOptions { - /** - * By default, ignored tokens are valid syntax and ignored when lexing. - * This may be disabled for certain grammars that specifically disallow - * ignored tokens (e.g. schema coordinates). - * This is passed down to the lexer to enforce. - */ - noIgnoredTokens?: boolean | undefined; - /** * By default, the parser creates AST nodes that know the location * in the source that they correspond to. This configuration flag @@ -123,6 +118,24 @@ export interface ParseOptions { * ``` */ experimentalFragmentArguments?: boolean | undefined; + + /** + * You may override the Lexer class used to lex the source; this is used by + * schema coordinates to introduce a lexer that forbids ignored tokens. + */ + Lexer?: typeof Lexer | undefined; +} + +/** + * Configuration options to control schema coordinate parser behavior + */ +export interface ParseSchemaCoordinateOptions { + /** + * By default, the parser creates AST nodes that know the location + * in the source that they correspond to. This configuration flag + * disables that behavior for performance or testing. + */ + noLocation?: boolean | undefined; } /** @@ -208,11 +221,10 @@ export function parseType( */ export function parseSchemaCoordinate( source: string | Source, - options: ParseOptions & { noIgnoredTokens?: true } = {}, + options?: ParseSchemaCoordinateOptions, ): SchemaCoordinateNode { // Ignored tokens are excluded syntax for the schema coordinates. - const _options = { ...options, noIgnoredTokens: true }; - + const _options = { ...options, Lexer: SchemaCoordinateLexer }; const parser = new Parser(source, _options); parser.expectToken(TokenKind.SOF); const coordinate = parser.parseSchemaCoordinate(); @@ -239,11 +251,8 @@ export class Parser { constructor(source: string | Source, options: ParseOptions = {}) { const sourceObj = isSource(source) ? source : new Source(source); - const lexerOptions: LexerOptions = { - noIgnoredTokens: options.noIgnoredTokens ?? false, - }; - - this._lexer = new Lexer(sourceObj, lexerOptions); + const LexerClass = options.Lexer ?? Lexer; + this._lexer = new LexerClass(sourceObj); this._options = options; this._tokenCounter = 0; }