diff --git a/src/index.ts b/src/index.ts index ddc799e2ba..1f80cf51f3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -230,7 +230,6 @@ export { printSourceLocation, // Lex Lexer, - SchemaCoordinateLexer, TokenKind, // Parse parse, @@ -262,7 +261,6 @@ export { export type { ParseOptions, - ParseSchemaCoordinateOptions, SourceLocation, // Visitor utilities ASTVisitor, diff --git a/src/language/__tests__/lexer-test.ts b/src/language/__tests__/lexer-test.ts index 433d3c4181..f324a20a24 100644 --- a/src/language/__tests__/lexer-test.ts +++ b/src/language/__tests__/lexer-test.ts @@ -9,11 +9,7 @@ import { inspect } from '../../jsutils/inspect.js'; import { GraphQLError } from '../../error/GraphQLError.js'; import type { Token } from '../ast.js'; -import { - isPunctuatorTokenKind, - Lexer, - SchemaCoordinateLexer, -} from '../lexer.js'; +import { isPunctuatorTokenKind, Lexer } from '../lexer.js'; import { Source } from '../source.js'; import { TokenKind } from '../tokenKind.js'; @@ -1193,33 +1189,6 @@ describe('Lexer', () => { }); }); -describe('SchemaCoordinateLexer', () => { - it('can be stringified', () => { - const lexer = new SchemaCoordinateLexer(new Source('Name.field')); - expect(Object.prototype.toString.call(lexer)).to.equal( - '[object SchemaCoordinateLexer]', - ); - }); - - it('tracks a schema coordinate', () => { - const lexer = new SchemaCoordinateLexer(new Source('Name.field')); - expect(lexer.advance()).to.contain({ - kind: TokenKind.NAME, - start: 0, - end: 4, - value: 'Name', - }); - }); - - it('forbids ignored tokens', () => { - const lexer = new SchemaCoordinateLexer(new Source('\nName.field')); - expectToThrowJSON(() => lexer.advance()).to.deep.equal({ - message: 'Syntax Error: Invalid character: U+000A.', - locations: [{ line: 1, column: 1 }], - }); - }); -}); - describe('isPunctuatorTokenKind', () => { function isPunctuatorToken(text: string) { return isPunctuatorTokenKind(lexOne(text).kind); diff --git a/src/language/__tests__/parser-test.ts b/src/language/__tests__/parser-test.ts index e8dd914f71..c0d247ddf5 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: 18 }], + locations: [{ line: 1, column: 19 }], }); }); diff --git a/src/language/__tests__/printer-test.ts b/src/language/__tests__/printer-test.ts index a7a604bcba..589d9bfc8d 100644 --- a/src/language/__tests__/printer-test.ts +++ b/src/language/__tests__/printer-test.ts @@ -301,24 +301,16 @@ 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( - 'Name.field(arg:)', + expect(print(parseSchemaCoordinate(' Name '))).to.equal('Name'); + expect(print(parseSchemaCoordinate(' Name . field '))).to.equal( + 'Name.field', ); - 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 ( arg: )'))).to.equal( + 'Name.field(arg:)', ); - expect(() => print(parseSchemaCoordinate('Name .field'))).to.throw( - 'Syntax Error: Invalid character: " "', + expect(print(parseSchemaCoordinate(' @ name '))).to.equal('@name'); + expect(print(parseSchemaCoordinate(' @ name (arg:) '))).to.equal( + '@name(arg:)', ); }); }); diff --git a/src/language/__tests__/schemaCoordinates-test.ts b/src/language/__tests__/schemaCoordinates-test.ts new file mode 100644 index 0000000000..4999f0387e --- /dev/null +++ b/src/language/__tests__/schemaCoordinates-test.ts @@ -0,0 +1,109 @@ +import { expect } from 'chai'; +import { it } from 'mocha'; +import { parseSchemaCoordinate } from '../schemaCoordinate.js' +import { + expectJSON, +} from '../../__testUtils__/expectJSON.js'; +import { Kind } from '../kinds.js'; +import { Source } from '../source.js'; + +it('parses Name', () => { + const result = parseSchemaCoordinate('@@MyType'); + expectJSON(result).toDeepEqual({ + kind: Kind.TYPE_COORDINATE, + name: { + kind: Kind.NAME, + value: 'MyType', + }, + }); +}); + +it('parses Name . Name', () => { + const result = parseSchemaCoordinate('MyType.field'); + expectJSON(result).toDeepEqual({ + kind: Kind.MEMBER_COORDINATE, + name: { + kind: Kind.NAME, + value: 'MyType', + }, + memberName: { + kind: Kind.NAME, + value: 'field', + }, + }); +}); + +it('rejects Name . Name . Name', () => { + expect(() => parseSchemaCoordinate('MyType.field.deep')) + .to.throw() + .to.deep.include({ + message: 'Syntax Error: Expected , found ..', + }); +}); + +it('parses Name . Name ( Name : )', () => { + const result = parseSchemaCoordinate('MyType.field(arg:)'); + expectJSON(result).toDeepEqual({ + kind: Kind.ARGUMENT_COORDINATE, + name: { + kind: Kind.NAME, + value: 'MyType', + }, + fieldName: { + kind: Kind.NAME, + value: 'field', + }, + argumentName: { + kind: Kind.NAME, + value: 'arg', + }, + }); +}); + +it('rejects Name . Name ( Name : Name )', () => { + expect(() => parseSchemaCoordinate('MyType.field(arg: value)')) + .to.throw() + .to.deep.include({ + message: 'Syntax Error: Invalid character: " ".', + }); +}); + +it('parses @ Name', () => { + const result = parseSchemaCoordinate('@myDirective'); + expectJSON(result).toDeepEqual({ + kind: Kind.DIRECTIVE_COORDINATE, + name: { + kind: Kind.NAME, + value: 'myDirective', + }, + }); +}); + +it('parses @ Name ( Name : )', () => { + const result = parseSchemaCoordinate('@myDirective(arg:)'); + expectJSON(result).toDeepEqual({ + kind: Kind.DIRECTIVE_ARGUMENT_COORDINATE, + name: { + kind: Kind.NAME, + value: 'myDirective', + }, + argumentName: { + kind: Kind.NAME, + value: 'arg', + }, + }); +}); + +it('rejects @ Name . Name', () => { + expect(() => parseSchemaCoordinate('@myDirective.field')) + .to.throw() + .to.deep.include({ + message: 'Syntax Error: Expected , found ..', + }); +}); + +it('accepts a Source object', () => { + expect(parseSchemaCoordinate('MyType')).to.deep.equal( + parseSchemaCoordinate(new Source('MyType')), + ); +}); diff --git a/src/language/index.ts b/src/language/index.ts index 1f2eff6bb7..c5620b4948 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, SchemaCoordinateLexer } from './lexer.js'; +export { Lexer } from './lexer.js'; export { parse, @@ -20,7 +20,7 @@ export { parseType, parseSchemaCoordinate, } from './parser.js'; -export type { ParseOptions, ParseSchemaCoordinateOptions } from './parser.js'; +export type { ParseOptions } from './parser.js'; export { print } from './printer.js'; diff --git a/src/language/lexer.ts b/src/language/lexer.ts index 4a2228e285..49ca8e4267 100644 --- a/src/language/lexer.ts +++ b/src/language/lexer.ts @@ -62,7 +62,7 @@ export class Lexer { /** * Looks ahead and returns the next non-ignored token, but does not change - * the current Lexer token. + * the state of Lexer. */ lookahead(): Token { let token = this.token; @@ -83,27 +83,6 @@ export class Lexer { } return token; } - - 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)}.`, - ); - } } /** @@ -116,7 +95,6 @@ export function isPunctuatorTokenKind(kind: TokenKind): boolean { kind === TokenKind.AMP || kind === TokenKind.PAREN_L || kind === TokenKind.PAREN_R || - kind === TokenKind.DOT || kind === TokenKind.SPREAD || kind === TokenKind.COLON || kind === TokenKind.EQUALS || @@ -172,8 +150,10 @@ function isTrailingSurrogate(code: number): boolean { * * Printable ASCII is printed quoted, while other points are printed in Unicode * code point form (ie. U+1234). + * + * @internal */ -function printCodePointAt(lexer: Lexer, location: number): string { +export function printCodePointAt(lexer: Lexer, location: number): string { const code = lexer.source.body.codePointAt(location); if (code === undefined) { @@ -238,7 +218,6 @@ function readNextToken(lexer: Lexer, start: number): Token { case 0x0009: // \t case 0x0020: // case 0x002c: // , - lexer.validateIgnoredToken(position); ++position; continue; // LineTerminator :: @@ -246,13 +225,11 @@ 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 { @@ -263,7 +240,6 @@ function readNextToken(lexer: Lexer, start: number): Token { continue; // Comment case 0x0023: // # - lexer.validateIgnoredToken(position); return readComment(lexer, position); // Token :: // - Punctuator @@ -272,11 +248,7 @@ function readNextToken(lexer: Lexer, start: number): Token { // - FloatValue // - StringValue // - // Punctuator :: - // - DotPunctuator - // - OtherPunctuator - // - // OtherPunctuator :: one of ! $ & ( ) ... : = @ [ ] { | } + // Punctuator :: one of ! $ & ( ) ... : = @ [ ] { | } case 0x0021: // ! return createToken(lexer, TokenKind.BANG, position, position + 1); case 0x0024: // $ @@ -287,14 +259,14 @@ function readNextToken(lexer: Lexer, start: number): Token { return createToken(lexer, TokenKind.PAREN_L, position, position + 1); case 0x0029: // ) return createToken(lexer, TokenKind.PAREN_R, position, position + 1); - case 0x002e: { - // . - const nextCode = body.charCodeAt(position + 1); - if (nextCode === 0x002e && body.charCodeAt(position + 2) === 0x002e) { + case 0x002e: // . + if ( + body.charCodeAt(position + 1) === 0x002e && + body.charCodeAt(position + 2) === 0x002e + ) { return createToken(lexer, TokenKind.SPREAD, position, position + 3); } - return readDot(lexer, position); - } + break; case 0x003a: // : return createToken(lexer, TokenKind.COLON, position, position + 1); case 0x003d: // = @@ -338,43 +310,14 @@ function readNextToken(lexer: Lexer, start: number): Token { code === 0x0027 ? 'Unexpected single quote character (\'), did you mean to use a double quote (")?' : isUnicodeScalarValue(code) || isSupplementaryCodePoint(body, position) - ? `Unexpected character: ${printCodePointAt(lexer, position)}.` - : `Invalid character: ${printCodePointAt(lexer, position)}.`, + ? `Unexpected character: ${printCodePointAt(lexer, position)}.` + : `Invalid character: ${printCodePointAt(lexer, position)}.`, ); } return createToken(lexer, TokenKind.EOF, bodyLength, bodyLength); } -/** - * Reads a dot token with helpful messages for negative lookahead. - * - * DotPunctuator :: `.` [lookahead != {`.`, Digit}] - */ -function readDot(lexer: Lexer, start: number): Token { - const nextCode = lexer.source.body.charCodeAt(start + 1); - // Full Stop (.) - if (nextCode === 0x002e) { - throw syntaxError( - lexer.source, - start, - 'Unexpected "..", did you mean "..."?', - ); - } - if (isDigit(nextCode)) { - const digits = lexer.source.body.slice( - start + 1, - readDigits(lexer, start + 1, nextCode), - ); - throw syntaxError( - lexer.source, - start, - `Invalid number, expected digit before ".", did you mean "0.${digits}"?`, - ); - } - return createToken(lexer, TokenKind.DOT, start, start + 1); -} - /** * Reads a comment token from the source file. * @@ -734,10 +677,10 @@ function readHexDigit(code: number): number { return code >= 0x0030 && code <= 0x0039 // 0-9 ? code - 0x0030 : code >= 0x0041 && code <= 0x0046 // A-F - ? code - 0x0037 - : code >= 0x0061 && code <= 0x0066 // a-f - ? code - 0x0057 - : -1; + ? code - 0x0037 + : code >= 0x0061 && code <= 0x0066 // a-f + ? code - 0x0057 + : -1; } /** @@ -888,8 +831,10 @@ function readBlockString(lexer: Lexer, start: number): Token { * Name :: * - NameStart NameContinue* [lookahead != NameContinue] * ``` + * + * @internal */ -function readName(lexer: Lexer, start: number): Token { +export function readName(lexer: Lexer, start: number): Token { const body = lexer.source.body; const bodyLength = body.length; let position = start + 1; diff --git a/src/language/parser.ts b/src/language/parser.ts index 5cf3e14d21..c1a2d1cd8c 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -4,7 +4,6 @@ import type { GraphQLError } from '../error/GraphQLError.js'; import { syntaxError } from '../error/syntaxError.js'; import type { - ArgumentCoordinateNode, ArgumentNode, BooleanValueNode, ConstArgumentNode, @@ -13,10 +12,7 @@ import type { ConstObjectFieldNode, ConstObjectValueNode, ConstValueNode, - DefinitionNode, - DirectiveArgumentCoordinateNode, - DirectiveCoordinateNode, - DirectiveDefinitionNode, + DefinitionNode DirectiveNode, DocumentNode, EnumTypeDefinitionNode, @@ -38,7 +34,6 @@ import type { IntValueNode, ListTypeNode, ListValueNode, - MemberCoordinateNode, NamedTypeNode, NameNode, NonNullTypeNode, @@ -51,14 +46,12 @@ import type { OperationTypeDefinitionNode, ScalarTypeDefinitionNode, ScalarTypeExtensionNode, - SchemaCoordinateNode, SchemaDefinitionNode, SchemaExtensionNode, SelectionNode, SelectionSetNode, StringValueNode, Token, - TypeCoordinateNode, TypeNode, TypeSystemExtensionNode, UnionTypeDefinitionNode, @@ -70,11 +63,7 @@ import type { import { Location, OperationTypeNode } from './ast.js'; import { DirectiveLocation } from './directiveLocation.js'; import { Kind } from './kinds.js'; -import { - isPunctuatorTokenKind, - Lexer, - SchemaCoordinateLexer, -} from './lexer.js'; +import { isPunctuatorTokenKind, Lexer } from './lexer.js'; import { isSource, Source } from './source.js'; import { TokenKind } from './tokenKind.js'; @@ -118,24 +107,6 @@ 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; } /** @@ -210,30 +181,6 @@ export function parseType( return type; } -/** - * Given a string containing a GraphQL Schema Coordinate (ex. `Type.field`), - * parse the AST for that schema coordinate. - * Throws GraphQLError if a syntax error is encountered. - * - * Consider providing the results to the utility function: - * resolveASTSchemaCoordinate(). Or calling resolveSchemaCoordinate() directly - * with an unparsed source. - */ -export function parseSchemaCoordinate( - source: string | Source, - options?: ParseSchemaCoordinateOptions, -): SchemaCoordinateNode { - // Ignored tokens are excluded syntax for a Schema Coordinate. - const parser = new Parser(source, { - ...options, - Lexer: SchemaCoordinateLexer, - }); - parser.expectToken(TokenKind.SOF); - const coordinate = parser.parseSchemaCoordinate(); - parser.expectToken(TokenKind.EOF); - return coordinate; -} - /** * This class is exported only to assist people in implementing their own parsers * without duplicating too much code and should be used only as last resort for cases @@ -253,8 +200,7 @@ export class Parser { constructor(source: string | Source, options: ParseOptions = {}) { const sourceObj = isSource(source) ? source : new Source(source); - const LexerClass = options.Lexer ?? Lexer; - this._lexer = new LexerClass(sourceObj); + this._lexer = new Lexer(sourceObj); this._options = options; this._tokenCounter = 0; } @@ -1486,68 +1432,6 @@ export class Parser { throw this.unexpected(start); } - // Schema Coordinates - - /** - * SchemaCoordinate : - * - Name - * - Name . Name - * - Name . Name ( Name : ) - * - @ Name - * - @ Name ( Name : ) - */ - parseSchemaCoordinate(): SchemaCoordinateNode { - const start = this._lexer.token; - const ofDirective = this.expectOptionalToken(TokenKind.AT); - const name = this.parseName(); - let memberName: NameNode | undefined; - if (!ofDirective && this.expectOptionalToken(TokenKind.DOT)) { - memberName = this.parseName(); - } - let argumentName: NameNode | undefined; - if ( - (ofDirective || memberName) && - this.expectOptionalToken(TokenKind.PAREN_L) - ) { - argumentName = this.parseName(); - this.expectToken(TokenKind.COLON); - this.expectToken(TokenKind.PAREN_R); - } - - if (ofDirective) { - if (argumentName) { - return this.node(start, { - kind: Kind.DIRECTIVE_ARGUMENT_COORDINATE, - name, - argumentName, - }); - } - return this.node(start, { - kind: Kind.DIRECTIVE_COORDINATE, - name, - }); - } else if (memberName) { - if (argumentName) { - return this.node(start, { - kind: Kind.ARGUMENT_COORDINATE, - name, - fieldName: memberName, - argumentName, - }); - } - return this.node(start, { - kind: Kind.MEMBER_COORDINATE, - name, - memberName, - }); - } - - return this.node(start, { - kind: Kind.TYPE_COORDINATE, - name, - }); - } - // Core parsing utility functions /** diff --git a/src/language/schemaCoordinate.ts b/src/language/schemaCoordinate.ts new file mode 100644 index 0000000000..2ebef20447 --- /dev/null +++ b/src/language/schemaCoordinate.ts @@ -0,0 +1,110 @@ +import { isSource, Source } from './source.js'; +import { GraphQLError } from '../error/GraphQLError.js'; +import { readName as _readName, printCodePointAt, Lexer } from './lexer.js'; +import { Token } from './ast.js'; +import { Kind } from './kinds.js'; + +import type { + SchemaCoordinateNode, +} from './ast.js'; + +function syntaxError( + source: Source, + description: string, +): GraphQLError { + return new GraphQLError(`Syntax Error: ${description}`, { + source, + }); +} + +function readName(body: string): Token { + const lexer = new Lexer(new Source(body)); + const token = _readName(lexer, 0); + return token; +} + +function readNameOnly(body: string): Token { + const source = new Source(body); + const lexer = new Lexer(source); + const token = _readName(lexer, 0); + if (body.length > token.end) { + throw syntaxError(source, `Invalid character: ${printCodePointAt(lexer, token.end)}`); + } + return token; +} + +/** + * SchemaCoordinate : + * - Name + * - Name . Name + * - Name . Name ( Name : ) + * - @ Name + * - @ Name ( Name : ) + */ +export function parseSchemaCoordinate(source: string | Source): SchemaCoordinateNode { + const sourceObj = isSource(source) ? source : new Source(source); + + const names = sourceObj.body.split('.'); + + if (names.length === 1) { + if (names[0].startsWith('@')) { + const name = readName(names[0].slice(1)); + const argument = names[0].slice(1 + name.end); + + // - @ Name + if (argument.length === 0) { + return { + kind: Kind.DIRECTIVE_COORDINATE, + name: { kind: Kind.NAME, value: name.value }, + } + } + + // - @ Name ( Name : ) + if (!(argument.startsWith('(') && argument.endsWith(':)'))) { + throw syntaxError(sourceObj, 'Unexpected characters'); + } + const argumentValue = readNameOnly(argument.slice(1, -2)).value; + return { + kind: Kind.DIRECTIVE_ARGUMENT_COORDINATE, + argumentName: { kind: Kind.NAME, value: argumentValue }, + name: { kind: Kind.NAME, value: name.value }, + } + } + + // - Name + const name = readNameOnly(names[0]); + return { + kind: Kind.TYPE_COORDINATE, + name: { kind: Kind.NAME, value: name.value }, + } + } + + if (names.length === 2) { + const typeName = readNameOnly(names[0]); + const fieldName = readName(names[1]); + const argument = names[1].slice(fieldName.end); + + // - @ Name . Name + if (argument.length === 0) { + return { + kind: Kind.MEMBER_COORDINATE, + memberName: { kind: Kind.NAME, value: fieldName.value }, + name: { kind: Kind.NAME, value: typeName.value }, + } + } + + // - Name . Name ( Name : ) + if (!(argument.startsWith('(') && argument.endsWith(':)'))) { + throw syntaxError(sourceObj, 'Unexpected characters'); + } + const argumentValue = readNameOnly(argument.slice(1, -2)).value; + return { + kind: Kind.ARGUMENT_COORDINATE, + name: { kind: Kind.NAME, value: typeName.value }, + fieldName: { kind: Kind.NAME, value: fieldName.value }, + argumentName: { kind: Kind.NAME, value: argumentValue }, + } + } + + throw syntaxError(sourceObj, 'Expected , found ..'); +} \ No newline at end of file