Skip to content

Commit 04dd13e

Browse files
authored
No {Ignored} tokens when parsing schema coordinates (#4450)
1 parent b70005f commit 04dd13e

File tree

7 files changed

+110
-17
lines changed

7 files changed

+110
-17
lines changed

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ export {
230230
printSourceLocation,
231231
// Lex
232232
Lexer,
233+
SchemaCoordinateLexer,
233234
TokenKind,
234235
// Parse
235236
parse,
@@ -261,6 +262,7 @@ export {
261262

262263
export type {
263264
ParseOptions,
265+
ParseSchemaCoordinateOptions,
264266
SourceLocation,
265267
// Visitor utilities
266268
ASTVisitor,

src/language/__tests__/lexer-test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import { inspect } from '../../jsutils/inspect.js';
99
import { GraphQLError } from '../../error/GraphQLError.js';
1010

1111
import type { Token } from '../ast.js';
12-
import { isPunctuatorTokenKind, Lexer } from '../lexer.js';
12+
import {
13+
isPunctuatorTokenKind,
14+
Lexer,
15+
SchemaCoordinateLexer,
16+
} from '../lexer.js';
1317
import { Source } from '../source.js';
1418
import { TokenKind } from '../tokenKind.js';
1519

@@ -1189,6 +1193,33 @@ describe('Lexer', () => {
11891193
});
11901194
});
11911195

1196+
describe('SchemaCoordinateLexer', () => {
1197+
it('can be stringified', () => {
1198+
const lexer = new SchemaCoordinateLexer(new Source('Name.field'));
1199+
expect(Object.prototype.toString.call(lexer)).to.equal(
1200+
'[object SchemaCoordinateLexer]',
1201+
);
1202+
});
1203+
1204+
it('tracks a schema coordinate', () => {
1205+
const lexer = new SchemaCoordinateLexer(new Source('Name.field'));
1206+
expect(lexer.advance()).to.contain({
1207+
kind: TokenKind.NAME,
1208+
start: 0,
1209+
end: 4,
1210+
value: 'Name',
1211+
});
1212+
});
1213+
1214+
it('forbids ignored tokens', () => {
1215+
const lexer = new SchemaCoordinateLexer(new Source('\nName.field'));
1216+
expectToThrowJSON(() => lexer.advance()).to.deep.equal({
1217+
message: 'Syntax Error: Invalid character: U+000A.',
1218+
locations: [{ line: 1, column: 1 }],
1219+
});
1220+
});
1221+
});
1222+
11921223
describe('isPunctuatorTokenKind', () => {
11931224
function isPunctuatorToken(text: string) {
11941225
return isPunctuatorTokenKind(lexOne(text).kind);

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/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export { Kind } from './kinds.js';
1111

1212
export { TokenKind } from './tokenKind.js';
1313

14-
export { Lexer } from './lexer.js';
14+
export { Lexer, SchemaCoordinateLexer } from './lexer.js';
1515

1616
export {
1717
parse,
@@ -20,7 +20,7 @@ export {
2020
parseType,
2121
parseSchemaCoordinate,
2222
} from './parser.js';
23-
export type { ParseOptions } from './parser.js';
23+
export type { ParseOptions, ParseSchemaCoordinateOptions } from './parser.js';
2424

2525
export { print } from './printer.js';
2626

src/language/lexer.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,27 @@ export class Lexer {
8383
}
8484
return token;
8585
}
86+
87+
validateIgnoredToken(_position: number): void {
88+
/* noop - ignored tokens are ignored */
89+
}
90+
}
91+
92+
/**
93+
* As `Lexer`, but forbids ignored tokens as required of schema coordinates.
94+
*/
95+
export class SchemaCoordinateLexer extends Lexer {
96+
override get [Symbol.toStringTag]() {
97+
return 'SchemaCoordinateLexer';
98+
}
99+
100+
override validateIgnoredToken(position: number): void {
101+
throw syntaxError(
102+
this.source,
103+
position,
104+
`Invalid character: ${printCodePointAt(this, position)}.`,
105+
);
106+
}
86107
}
87108

88109
/**
@@ -217,18 +238,21 @@ function readNextToken(lexer: Lexer, start: number): Token {
217238
case 0x0009: // \t
218239
case 0x0020: // <space>
219240
case 0x002c: // ,
241+
lexer.validateIgnoredToken(position);
220242
++position;
221243
continue;
222244
// LineTerminator ::
223245
// - "New Line (U+000A)"
224246
// - "Carriage Return (U+000D)" [lookahead != "New Line (U+000A)"]
225247
// - "Carriage Return (U+000D)" "New Line (U+000A)"
226248
case 0x000a: // \n
249+
lexer.validateIgnoredToken(position);
227250
++position;
228251
++lexer.line;
229252
lexer.lineStart = position;
230253
continue;
231254
case 0x000d: // \r
255+
lexer.validateIgnoredToken(position);
232256
if (body.charCodeAt(position + 1) === 0x000a) {
233257
position += 2;
234258
} else {
@@ -239,6 +263,7 @@ function readNextToken(lexer: Lexer, start: number): Token {
239263
continue;
240264
// Comment
241265
case 0x0023: // #
266+
lexer.validateIgnoredToken(position);
242267
return readComment(lexer, position);
243268
// Token ::
244269
// - Punctuator

src/language/parser.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,11 @@ import type {
7070
import { Location, OperationTypeNode } from './ast.js';
7171
import { DirectiveLocation } from './directiveLocation.js';
7272
import { Kind } from './kinds.js';
73-
import { isPunctuatorTokenKind, Lexer } from './lexer.js';
73+
import {
74+
isPunctuatorTokenKind,
75+
Lexer,
76+
SchemaCoordinateLexer,
77+
} from './lexer.js';
7478
import { isSource, Source } from './source.js';
7579
import { TokenKind } from './tokenKind.js';
7680

@@ -114,6 +118,24 @@ export interface ParseOptions {
114118
* ```
115119
*/
116120
experimentalFragmentArguments?: boolean | undefined;
121+
122+
/**
123+
* You may override the Lexer class used to lex the source; this is used by
124+
* schema coordinates to introduce a lexer that forbids ignored tokens.
125+
*/
126+
Lexer?: typeof Lexer | undefined;
127+
}
128+
129+
/**
130+
* Configuration options to control schema coordinate parser behavior
131+
*/
132+
export interface ParseSchemaCoordinateOptions {
133+
/**
134+
* By default, the parser creates AST nodes that know the location
135+
* in the source that they correspond to. This configuration flag
136+
* disables that behavior for performance or testing.
137+
*/
138+
noLocation?: boolean | undefined;
117139
}
118140

119141
/**
@@ -199,9 +221,13 @@ export function parseType(
199221
*/
200222
export function parseSchemaCoordinate(
201223
source: string | Source,
202-
options?: ParseOptions,
224+
options?: ParseSchemaCoordinateOptions,
203225
): SchemaCoordinateNode {
204-
const parser = new Parser(source, options);
226+
// Ignored tokens are excluded syntax for a Schema Coordinate.
227+
const parser = new Parser(source, {
228+
...options,
229+
Lexer: SchemaCoordinateLexer,
230+
});
205231
parser.expectToken(TokenKind.SOF);
206232
const coordinate = parser.parseSchemaCoordinate();
207233
parser.expectToken(TokenKind.EOF);
@@ -227,7 +253,8 @@ export class Parser {
227253
constructor(source: string | Source, options: ParseOptions = {}) {
228254
const sourceObj = isSource(source) ? source : new Source(source);
229255

230-
this._lexer = new Lexer(sourceObj);
256+
const LexerClass = options.Lexer ?? Lexer;
257+
this._lexer = new LexerClass(sourceObj);
231258
this._options = options;
232259
this._tokenCounter = 0;
233260
}

0 commit comments

Comments
 (0)