Skip to content

Commit 49eba18

Browse files
committed
no ignored tokens for schema coordinates
1 parent d580d7f commit 49eba18

File tree

4 files changed

+67
-14
lines changed

4 files changed

+67
-14
lines changed

src/language/__tests__/parser-test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -751,11 +751,11 @@ describe('Parser', () => {
751751
});
752752

753753
it('rejects Name . Name ( Name : Name )', () => {
754-
expect(() => parseSchemaCoordinate('MyType.field(arg: value)'))
754+
expect(() => parseSchemaCoordinate('MyType.field(arg:value)'))
755755
.to.throw()
756756
.to.deep.include({
757757
message: 'Syntax Error: Expected ")", found Name "value".',
758-
locations: [{ line: 1, column: 19 }],
758+
locations: [{ line: 1, column: 18 }],
759759
});
760760
});
761761

src/language/__tests__/printer-test.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -301,16 +301,24 @@ describe('Printer: Query document', () => {
301301
});
302302

303303
it('prints schema coordinates', () => {
304-
expect(print(parseSchemaCoordinate(' Name '))).to.equal('Name');
305-
expect(print(parseSchemaCoordinate(' Name . field '))).to.equal(
306-
'Name.field',
307-
);
308-
expect(print(parseSchemaCoordinate(' Name . field ( arg: )'))).to.equal(
304+
expect(print(parseSchemaCoordinate('Name'))).to.equal('Name');
305+
expect(print(parseSchemaCoordinate('Name.field'))).to.equal('Name.field');
306+
expect(print(parseSchemaCoordinate('Name.field(arg:)'))).to.equal(
309307
'Name.field(arg:)',
310308
);
311-
expect(print(parseSchemaCoordinate(' @ name '))).to.equal('@name');
312-
expect(print(parseSchemaCoordinate(' @ name (arg:) '))).to.equal(
313-
'@name(arg:)',
309+
expect(print(parseSchemaCoordinate('@name'))).to.equal('@name');
310+
expect(print(parseSchemaCoordinate('@name(arg:)'))).to.equal('@name(arg:)');
311+
});
312+
313+
it('throws syntax error for ignored tokens in schema coordinates', () => {
314+
expect(() => print(parseSchemaCoordinate('# foo\nName'))).to.throw(
315+
'Syntax Error: Invalid character: "#"',
316+
);
317+
expect(() => print(parseSchemaCoordinate('\nName'))).to.throw(
318+
'Syntax Error: Invalid character: U+000A.',
319+
);
320+
expect(() => print(parseSchemaCoordinate('Name .field'))).to.throw(
321+
'Syntax Error: Invalid character: " "',
314322
);
315323
});
316324
});

src/language/lexer.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@ import { isDigit, isNameContinue, isNameStart } from './characterClasses.js';
66
import type { Source } from './source.js';
77
import { TokenKind } from './tokenKind.js';
88

9+
/**
10+
* Configuration options to control lexer behavior
11+
*/
12+
export interface LexerOptions {
13+
/**
14+
* By default, ignored tokens are valid syntax and ignored when lexing.
15+
* This may be disabled for certain grammars that specifically disallow
16+
* ignored tokens (e.g. schema coordinates).
17+
*/
18+
noIgnoredTokens?: boolean | undefined;
19+
}
20+
921
/**
1022
* Given a Source object, creates a Lexer for that source.
1123
* A Lexer is a stateful stream generator in that every time
@@ -37,14 +49,17 @@ export class Lexer {
3749
*/
3850
lineStart: number;
3951

40-
constructor(source: Source) {
52+
protected _options: LexerOptions;
53+
54+
constructor(source: Source, options: LexerOptions = {}) {
4155
const startOfFileToken = new Token(TokenKind.SOF, 0, 0, 0, 0);
4256

4357
this.source = source;
4458
this.lastToken = startOfFileToken;
4559
this.token = startOfFileToken;
4660
this.line = 1;
4761
this.lineStart = 0;
62+
this._options = options;
4863
}
4964

5065
get [Symbol.toStringTag]() {
@@ -83,6 +98,16 @@ export class Lexer {
8398
}
8499
return token;
85100
}
101+
102+
validateIgnoredToken(position: number): void {
103+
if (this._options.noIgnoredTokens === true) {
104+
throw syntaxError(
105+
this.source,
106+
position,
107+
`Invalid character: ${printCodePointAt(this, position)}.`,
108+
);
109+
}
110+
}
86111
}
87112

88113
/**
@@ -217,18 +242,21 @@ function readNextToken(lexer: Lexer, start: number): Token {
217242
case 0x0009: // \t
218243
case 0x0020: // <space>
219244
case 0x002c: // ,
245+
lexer.validateIgnoredToken(position);
220246
++position;
221247
continue;
222248
// LineTerminator ::
223249
// - "New Line (U+000A)"
224250
// - "Carriage Return (U+000D)" [lookahead != "New Line (U+000A)"]
225251
// - "Carriage Return (U+000D)" "New Line (U+000A)"
226252
case 0x000a: // \n
253+
lexer.validateIgnoredToken(position);
227254
++position;
228255
++lexer.line;
229256
lexer.lineStart = position;
230257
continue;
231258
case 0x000d: // \r
259+
lexer.validateIgnoredToken(position);
232260
if (body.charCodeAt(position + 1) === 0x000a) {
233261
position += 2;
234262
} else {
@@ -239,6 +267,7 @@ function readNextToken(lexer: Lexer, start: number): Token {
239267
continue;
240268
// Comment
241269
case 0x0023: // #
270+
lexer.validateIgnoredToken(position);
242271
return readComment(lexer, position);
243272
// Token ::
244273
// - Punctuator

src/language/parser.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import type {
7070
import { Location, OperationTypeNode } from './ast.js';
7171
import { DirectiveLocation } from './directiveLocation.js';
7272
import { Kind } from './kinds.js';
73+
import type { LexerOptions } from './lexer.js';
7374
import { isPunctuatorTokenKind, Lexer } from './lexer.js';
7475
import { isSource, Source } from './source.js';
7576
import { TokenKind } from './tokenKind.js';
@@ -78,6 +79,14 @@ import { TokenKind } from './tokenKind.js';
7879
* Configuration options to control parser behavior
7980
*/
8081
export interface ParseOptions {
82+
/**
83+
* By default, ignored tokens are valid syntax and ignored when lexing.
84+
* This may be disabled for certain grammars that specifically disallow
85+
* ignored tokens (e.g. schema coordinates).
86+
* This is passed down to the lexer to enforce.
87+
*/
88+
noIgnoredTokens?: boolean | undefined;
89+
8190
/**
8291
* By default, the parser creates AST nodes that know the location
8392
* in the source that they correspond to. This configuration flag
@@ -199,9 +208,12 @@ export function parseType(
199208
*/
200209
export function parseSchemaCoordinate(
201210
source: string | Source,
202-
options?: ParseOptions,
211+
options: ParseOptions & { noIgnoredTokens?: true } = {},
203212
): SchemaCoordinateNode {
204-
const parser = new Parser(source, options);
213+
// Ignored tokens are excluded syntax for the schema coordinates.
214+
const _options = { ...options, noIgnoredTokens: true };
215+
216+
const parser = new Parser(source, _options);
205217
parser.expectToken(TokenKind.SOF);
206218
const coordinate = parser.parseSchemaCoordinate();
207219
parser.expectToken(TokenKind.EOF);
@@ -227,7 +239,11 @@ export class Parser {
227239
constructor(source: string | Source, options: ParseOptions = {}) {
228240
const sourceObj = isSource(source) ? source : new Source(source);
229241

230-
this._lexer = new Lexer(sourceObj);
242+
const lexerOptions: LexerOptions = {
243+
noIgnoredTokens: options.noIgnoredTokens ?? false,
244+
};
245+
246+
this._lexer = new Lexer(sourceObj, lexerOptions);
231247
this._options = options;
232248
this._tokenCounter = 0;
233249
}

0 commit comments

Comments
 (0)