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/__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/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 44abc05197..4a2228e285 100644 --- a/src/language/lexer.ts +++ b/src/language/lexer.ts @@ -83,6 +83,27 @@ 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)}.`, + ); + } } /** @@ -217,6 +238,7 @@ function readNextToken(lexer: Lexer, start: number): Token { case 0x0009: // \t case 0x0020: // case 0x002c: // , + lexer.validateIgnoredToken(position); ++position; continue; // LineTerminator :: @@ -224,11 +246,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 +263,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..b1c914b68a 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -70,7 +70,11 @@ import type { import { Location, OperationTypeNode } from './ast.js'; import { DirectiveLocation } from './directiveLocation.js'; import { Kind } from './kinds.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'; @@ -114,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; } /** @@ -199,9 +221,11 @@ export function parseType( */ export function parseSchemaCoordinate( source: string | Source, - options?: ParseOptions, + options?: ParseSchemaCoordinateOptions, ): SchemaCoordinateNode { - const parser = new Parser(source, options); + // Ignored tokens are excluded syntax for the schema coordinates. + const _options = { ...options, Lexer: SchemaCoordinateLexer }; + const parser = new Parser(source, _options); parser.expectToken(TokenKind.SOF); const coordinate = parser.parseSchemaCoordinate(); parser.expectToken(TokenKind.EOF); @@ -227,7 +251,8 @@ export class Parser { constructor(source: string | Source, options: ParseOptions = {}) { const sourceObj = isSource(source) ? source : new Source(source); - this._lexer = new Lexer(sourceObj); + const LexerClass = options.Lexer ?? Lexer; + this._lexer = new LexerClass(sourceObj); this._options = options; this._tokenCounter = 0; }