import { expect } from 'chai';
import { describe, it } from 'mocha';
import { dedent } from '../../__testUtils__/dedent.js';
import { expectToThrowJSON } from '../../__testUtils__/expectJSON.js';
import { inspect } from '../../jsutils/inspect.js';
import { GraphQLError } from '../../error/GraphQLError.js';
import type { Token } from '../ast.js';
import { isPunctuatorTokenKind, Lexer } from '../lexer.js';
import { Source } from '../source.js';
import { TokenKind } from '../tokenKind.js';
function lexOne(str: string) {
const lexer = new Lexer(new Source(str));
return lexer.advance();
}
function lexSecond(str: string) {
const lexer = new Lexer(new Source(str));
lexer.advance();
return lexer.advance();
}
function expectSyntaxError(text: string) {
return expectToThrowJSON(() => lexSecond(text));
}
describe('Lexer', () => {
it('ignores BOM header', () => {
expect(lexOne('\uFEFF foo')).to.contain({
kind: TokenKind.NAME,
start: 2,
end: 5,
value: 'foo',
});
});
it('tracks line breaks', () => {
expect(lexOne('foo')).to.contain({
kind: TokenKind.NAME,
start: 0,
end: 3,
line: 1,
column: 1,
value: 'foo',
});
expect(lexOne('\nfoo')).to.contain({
kind: TokenKind.NAME,
start: 1,
end: 4,
line: 2,
column: 1,
value: 'foo',
});
expect(lexOne('\rfoo')).to.contain({
kind: TokenKind.NAME,
start: 1,
end: 4,
line: 2,
column: 1,
value: 'foo',
});
expect(lexOne('\r\nfoo')).to.contain({
kind: TokenKind.NAME,
start: 2,
end: 5,
line: 2,
column: 1,
value: 'foo',
});
expect(lexOne('\n\rfoo')).to.contain({
kind: TokenKind.NAME,
start: 2,
end: 5,
line: 3,
column: 1,
value: 'foo',
});
expect(lexOne('\r\r\n\nfoo')).to.contain({
kind: TokenKind.NAME,
start: 4,
end: 7,
line: 4,
column: 1,
value: 'foo',
});
expect(lexOne('\n\n\r\rfoo')).to.contain({
kind: TokenKind.NAME,
start: 4,
end: 7,
line: 5,
column: 1,
value: 'foo',
});
});
it('records line and column', () => {
expect(lexOne('\n \r\n \r foo\n')).to.contain({
kind: TokenKind.NAME,
start: 8,
end: 11,
line: 4,
column: 3,
value: 'foo',
});
});
it('can be Object.toStringified, JSON.stringified, or jsutils.inspected', () => {
const lexer = new Lexer(new Source('foo'));
const token = lexer.advance();
expect(Object.prototype.toString.call(lexer)).to.equal('[object Lexer]');
expect(Object.prototype.toString.call(token)).to.equal('[object Token]');
expect(JSON.stringify(token)).to.equal(
'{"kind":"Name","value":"foo","line":1,"column":1}',
);
expect(inspect(token)).to.equal(
'{ kind: "Name", value: "foo", line: 1, column: 1 }',
);
});
it('skips whitespace and comments', () => {
expect(
lexOne(`
foo
`),
).to.contain({
kind: TokenKind.NAME,
start: 6,
end: 9,
value: 'foo',
});
expect(lexOne('\t\tfoo\t\t')).to.contain({
kind: TokenKind.NAME,
start: 2,
end: 5,
value: 'foo',
});
expect(
lexOne(`
#comment
foo#comment
`),
).to.contain({
kind: TokenKind.NAME,
start: 18,
end: 21,
value: 'foo',
});
expect(lexOne(',,,foo,,,')).to.contain({
kind: TokenKind.NAME,
start: 3,
end: 6,
value: 'foo',
});
});
it('errors respect whitespace', () => {
let caughtError;
try {
lexOne(['', '', ' ~', ''].join('\n'));
} catch (error) {
caughtError = error;
}
expect(String(caughtError)).to.equal(dedent`
Syntax Error: Unexpected character: "~".
GraphQL request:3:2
2 |
3 | ~
| ^
4 |
`);
});
it('updates line numbers in error for file context', () => {
let caughtError;
try {
const str = ['', '', ' ~', ''].join('\n');
const source = new Source(str, 'foo.js', { line: 11, column: 12 });
new Lexer(source).advance();
} catch (error) {
caughtError = error;
}
expect(String(caughtError)).to.equal(dedent`
Syntax Error: Unexpected character: "~".
foo.js:13:6
12 |
13 | ~
| ^
14 |
`);
});
it('updates column numbers in error for file context', () => {
let caughtError;
try {
const source = new Source('~', 'foo.js', { line: 1, column: 5 });
new Lexer(source).advance();
} catch (error) {
caughtError = error;
}
expect(String(caughtError)).to.equal(dedent`
Syntax Error: Unexpected character: "~".
foo.js:1:5
1 | ~
| ^
`);
});
it('lexes strings', () => {
expect(lexOne('""')).to.contain({
kind: TokenKind.STRING,
start: 0,
end: 2,
value: '',
});
expect(lexOne('"simple"')).to.contain({
kind: TokenKind.STRING,
start: 0,
end: 8,
value: 'simple',
});
expect(lexOne('" white space "')).to.contain({
kind: TokenKind.STRING,
start: 0,
end: 15,
value: ' white space ',
});
expect(lexOne('"quote \\""')).to.contain({
kind: TokenKind.STRING,
start: 0,
end: 10,
value: 'quote "',
});
expect(lexOne('"escaped \\n\\r\\b\\t\\f"')).to.contain({
kind: TokenKind.STRING,
start: 0,
end: 20,
value: 'escaped \n\r\b\t\f',
});
expect(lexOne('"slashes \\\\ \\/"')).to.contain({
kind: TokenKind.STRING,
start: 0,
end: 15,
value: 'slashes \\ /',
});
expect(lexOne('"unescaped unicode outside BMP \u{1f600}"')).to.contain({
kind: TokenKind.STRING,
start: 0,
end: 34,
value: 'unescaped unicode outside BMP \u{1f600}',
});
expect(
lexOne('"unescaped maximal unicode outside BMP \u{10ffff}"'),
).to.contain({
kind: TokenKind.STRING,
start: 0,
end: 42,
value: 'unescaped maximal unicode outside BMP \u{10ffff}',
});
expect(lexOne('"unicode \\u1234\\u5678\\u90AB\\uCDEF"')).to.contain({
kind: TokenKind.STRING,
start: 0,
end: 34,
value: 'unicode \u1234\u5678\u90AB\uCDEF',
});
expect(lexOne('"unicode \\u{1234}\\u{5678}\\u{90AB}\\u{CDEF}"')).to.contain(
{
kind: TokenKind.STRING,
start: 0,
end: 42,
value: 'unicode \u1234\u5678\u90AB\uCDEF',
},
);
expect(
lexOne('"string with unicode escape outside BMP \\u{1F600}"'),
).to.contain({
kind: TokenKind.STRING,
start: 0,
end: 50,
value: 'string with unicode escape outside BMP \u{1f600}',
});
expect(lexOne('"string with minimal unicode escape \\u{0}"')).to.contain({
kind: TokenKind.STRING,
start: 0,
end: 42,
value: 'string with minimal unicode escape \u{0}',
});
expect(
lexOne('"string with maximal unicode escape \\u{10FFFF}"'),
).to.contain({
kind: TokenKind.STRING,
start: 0,
end: 47,
value: 'string with maximal unicode escape \u{10FFFF}',
});
expect(
lexOne('"string with maximal minimal unicode escape \\u{00000000}"'),
).to.contain({
kind: TokenKind.STRING,
start: 0,
end: 57,
value: 'string with maximal minimal unicode escape \u{0}',
});
expect(
lexOne('"string with unicode surrogate pair escape \\uD83D\\uDE00"'),
).to.contain({
kind: TokenKind.STRING,
start: 0,
end: 56,
value: 'string with unicode surrogate pair escape \u{1f600}',
});
expect(
lexOne('"string with minimal surrogate pair escape \\uD800\\uDC00"'),
).to.contain({
kind: TokenKind.STRING,
start: 0,
end: 56,
value: 'string with minimal surrogate pair escape \u{10000}',
});
expect(
lexOne('"string with maximal surrogate pair escape \\uDBFF\\uDFFF"'),
).to.contain({
kind: TokenKind.STRING,
start: 0,
end: 56,
value: 'string with maximal surrogate pair escape \u{10FFFF}',
});
});
it('lex reports useful string errors', () => {
expectSyntaxError('"').to.deep.equal({
message: 'Syntax Error: Unterminated string.',
locations: [{ line: 1, column: 2 }],
});
expectSyntaxError('"""').to.deep.equal({
message: 'Syntax Error: Unterminated string.',
locations: [{ line: 1, column: 4 }],
});
expectSyntaxError('""""').to.deep.equal({
message: 'Syntax Error: Unterminated string.',
locations: [{ line: 1, column: 5 }],
});
expectSyntaxError('"no end quote').to.deep.equal({
message: 'Syntax Error: Unterminated string.',
locations: [{ line: 1, column: 14 }],
});
expectSyntaxError("'single quotes'").to.deep.equal({
message:
'Syntax Error: Unexpected single quote character (\'), did you mean to use a double quote (")?',
locations: [{ line: 1, column: 1 }],
});
expectSyntaxError('"bad surrogate \uDEAD"').to.deep.equal({
message: 'Syntax Error: Invalid character within String: U+DEAD.',
locations: [{ line: 1, column: 16 }],
});
expectSyntaxError('"bad high surrogate pair \uDEAD\uDEAD"').to.deep.equal({
message: 'Syntax Error: Invalid character within String: U+DEAD.',
locations: [{ line: 1, column: 26 }],
});
expectSyntaxError('"bad low surrogate pair \uD800\uD800"').to.deep.equal({
message: 'Syntax Error: Invalid character within String: U+D800.',
locations: [{ line: 1, column: 25 }],
});
expectSyntaxError('"multi\nline"').to.deep.equal({
message: 'Syntax Error: Unterminated string.',
locations: [{ line: 1, column: 7 }],
});
expectSyntaxError('"multi\rline"').to.deep.equal({
message: 'Syntax Error: Unterminated string.',
locations: [{ line: 1, column: 7 }],
});
expectSyntaxError('"bad \\z esc"').to.deep.equal({
message: 'Syntax Error: Invalid character escape sequence: "\\z".',
locations: [{ line: 1, column: 6 }],
});
expectSyntaxError('"bad \\x esc"').to.deep.equal({
message: 'Syntax Error: Invalid character escape sequence: "\\x".',
locations: [{ line: 1, column: 6 }],
});
expectSyntaxError('"bad \\u1 esc"').to.deep.equal({
message: 'Syntax Error: Invalid Unicode escape sequence: "\\u1 es".',
locations: [{ line: 1, column: 6 }],
});
expectSyntaxError('"bad \\u0XX1 esc"').to.deep.equal({
message: 'Syntax Error: Invalid Unicode escape sequence: "\\u0XX1".',
locations: [{ line: 1, column: 6 }],
});
expectSyntaxError('"bad \\uXXXX esc"').to.deep.equal({
message: 'Syntax Error: Invalid Unicode escape sequence: "\\uXXXX".',
locations: [{ line: 1, column: 6 }],
});
expectSyntaxError('"bad \\uFXXX esc"').to.deep.equal({
message: 'Syntax Error: Invalid Unicode escape sequence: "\\uFXXX".',
locations: [{ line: 1, column: 6 }],
});
expectSyntaxError('"bad \\uXXXF esc"').to.deep.equal({
message: 'Syntax Error: Invalid Unicode escape sequence: "\\uXXXF".',
locations: [{ line: 1, column: 6 }],
});
expectSyntaxError('"bad \\u{} esc"').to.deep.equal({
message: 'Syntax Error: Invalid Unicode escape sequence: "\\u{}".',
locations: [{ line: 1, column: 6 }],
});
expectSyntaxError('"bad \\u{FXXX} esc"').to.deep.equal({
message: 'Syntax Error: Invalid Unicode escape sequence: "\\u{FX".',
locations: [{ line: 1, column: 6 }],
});
expectSyntaxError('"bad \\u{FFFF esc"').to.deep.equal({
message: 'Syntax Error: Invalid Unicode escape sequence: "\\u{FFFF ".',
locations: [{ line: 1, column: 6 }],
});
expectSyntaxError('"bad \\u{FFFF"').to.deep.equal({
message: 'Syntax Error: Invalid Unicode escape sequence: "\\u{FFFF"".',
locations: [{ line: 1, column: 6 }],
});
expectSyntaxError('"too high \\u{110000} esc"').to.deep.equal({
message: 'Syntax Error: Invalid Unicode escape sequence: "\\u{110000}".',
locations: [{ line: 1, column: 11 }],
});
expectSyntaxError('"way too high \\u{12345678} esc"').to.deep.equal({
message:
'Syntax Error: Invalid Unicode escape sequence: "\\u{12345678}".',
locations: [{ line: 1, column: 15 }],
});
expectSyntaxError('"too long \\u{000000000} esc"').to.deep.equal({
message:
'Syntax Error: Invalid Unicode escape sequence: "\\u{000000000".',
locations: [{ line: 1, column: 11 }],
});
expectSyntaxError('"bad surrogate \\uDEAD esc"').to.deep.equal({
message: 'Syntax Error: Invalid Unicode escape sequence: "\\uDEAD".',
locations: [{ line: 1, column: 16 }],
});
expectSyntaxError('"bad surrogate \\u{DEAD} esc"').to.deep.equal({
message: 'Syntax Error: Invalid Unicode escape sequence: "\\u{DEAD}".',
locations: [{ line: 1, column: 16 }],
});
expectSyntaxError(
'"cannot use braces for surrogate pair \\u{D83D}\\u{DE00} esc"',
).to.deep.equal({
message: 'Syntax Error: Invalid Unicode escape sequence: "\\u{D83D}".',
locations: [{ line: 1, column: 39 }],
});
expectSyntaxError(
'"bad high surrogate pair \\uDEAD\\uDEAD esc"',
).to.deep.equal({
message: 'Syntax Error: Invalid Unicode escape sequence: "\\uDEAD".',
locations: [{ line: 1, column: 26 }],
});
expectSyntaxError(
'"bad low surrogate pair \\uD800\\uD800 esc"',
).to.deep.equal({
message: 'Syntax Error: Invalid Unicode escape sequence: "\\uD800".',
locations: [{ line: 1, column: 25 }],
});
expectSyntaxError(
'"cannot escape half a pair \uD83D\\uDE00 esc"',
).to.deep.equal({
message: 'Syntax Error: Invalid character within String: U+D83D.',
locations: [{ line: 1, column: 28 }],
});
expectSyntaxError(
'"cannot escape half a pair \\uD83D\uDE00 esc"',
).to.deep.equal({
message: 'Syntax Error: Invalid Unicode escape sequence: "\\uD83D".',
locations: [{ line: 1, column: 28 }],
});
expectSyntaxError('"bad \\uD83D\\not an escape"').to.deep.equal({
message: 'Syntax Error: Invalid Unicode escape sequence: "\\uD83D".',
locations: [{ line: 1, column: 6 }],
});
});
it('lexes block strings', () => {
expect(lexOne('""""""')).to.contain({
kind: TokenKind.BLOCK_STRING,
start: 0,
end: 6,
line: 1,
column: 1,
value: '',
});
expect(lexOne('"""simple"""')).to.contain({
kind: TokenKind.BLOCK_STRING,
start: 0,
end: 12,
line: 1,
column: 1,
value: 'simple',
});
expect(lexOne('""" white space """')).to.contain({
kind: TokenKind.BLOCK_STRING,
start: 0,
end: 19,
line: 1,
column: 1,
value: ' white space ',
});
expect(lexOne('"""contains " quote"""')).to.contain({
kind: TokenKind.BLOCK_STRING,
start: 0,
end: 22,
line: 1,
column: 1,
value: 'contains " quote',
});
expect(lexOne('"""contains \\""" triple quote"""')).to.contain({
kind: TokenKind.BLOCK_STRING,
start: 0,
end: 32,
line: 1,
column: 1,
value: 'contains """ triple quote',
});
expect(lexOne('"""multi\nline"""')).to.contain({
kind: TokenKind.BLOCK_STRING,
start: 0,
end: 16,
line: 1,
column: 1,
value: 'multi\nline',
});
expect(lexOne('"""multi\rline\r\nnormalized"""')).to.contain({
kind: TokenKind.BLOCK_STRING,
start: 0,
end: 28,
line: 1,
column: 1,
value: 'multi\nline\nnormalized',
});
expect(lexOne('"""unescaped \\n\\r\\b\\t\\f\\u1234"""')).to.contain({
kind: TokenKind.BLOCK_STRING,
start: 0,
end: 32,
line: 1,
column: 1,
value: 'unescaped \\n\\r\\b\\t\\f\\u1234',
});
expect(lexOne('"""unescaped unicode outside BMP \u{1f600}"""')).to.contain({
kind: TokenKind.BLOCK_STRING,
start: 0,
end: 38,
line: 1,
column: 1,
value: 'unescaped unicode outside BMP \u{1f600}',
});
expect(lexOne('"""slashes \\\\ \\/"""')).to.contain({
kind: TokenKind.BLOCK_STRING,
start: 0,
end: 19,
line: 1,
column: 1,
value: 'slashes \\\\ \\/',
});
expect(
lexOne(`"""
spans
multiple
lines
"""`),
).to.contain({
kind: TokenKind.BLOCK_STRING,
start: 0,
end: 68,
line: 1,
column: 1,
value: 'spans\n multiple\n lines',
});
});
it('advance line after lexing multiline block string', () => {
expect(
lexSecond(`"""
spans
multiple
lines
\n """ second_token`),
).to.contain({
kind: TokenKind.NAME,
start: 71,
end: 83,
line: 8,
column: 6,
value: 'second_token',
});
expect(
lexSecond(
[
'""" \n',
'spans \r\n',
'multiple \n\r',
'lines \n\n',
'"""\n second_token',
].join(''),
),
).to.contain({
kind: TokenKind.NAME,
start: 37,
end: 49,
line: 8,
column: 2,
value: 'second_token',
});
});
it('lex reports useful block string errors', () => {
expectSyntaxError('"""').to.deep.equal({
message: 'Syntax Error: Unterminated string.',
locations: [{ line: 1, column: 4 }],
});
expectSyntaxError('"""no end quote').to.deep.equal({
message: 'Syntax Error: Unterminated string.',
locations: [{ line: 1, column: 16 }],
});
expectSyntaxError('"""contains invalid surrogate \uDEAD"""').to.deep.equal({
message: 'Syntax Error: Invalid character within String: U+DEAD.',
locations: [{ line: 1, column: 31 }],
});
});
it('lexes numbers', () => {
expect(lexOne('4')).to.contain({
kind: TokenKind.INT,
start: 0,
end: 1,
value: '4',
});
expect(lexOne('4.123')).to.contain({
kind: TokenKind.FLOAT,
start: 0,
end: 5,
value: '4.123',
});
expect(lexOne('-4')).to.contain({
kind: TokenKind.INT,
start: 0,
end: 2,
value: '-4',
});
expect(lexOne('9')).to.contain({
kind: TokenKind.INT,
start: 0,
end: 1,
value: '9',
});
expect(lexOne('0')).to.contain({
kind: TokenKind.INT,
start: 0,
end: 1,
value: '0',
});
expect(lexOne('-4.123')).to.contain({
kind: TokenKind.FLOAT,
start: 0,
end: 6,
value: '-4.123',
});
expect(lexOne('0.123')).to.contain({
kind: TokenKind.FLOAT,
start: 0,
end: 5,
value: '0.123',
});
expect(lexOne('123e4')).to.contain({
kind: TokenKind.FLOAT,
start: 0,
end: 5,
value: '123e4',
});
expect(lexOne('123E4')).to.contain({
kind: TokenKind.FLOAT,
start: 0,
end: 5,
value: '123E4',
});
expect(lexOne('123e-4')).to.contain({
kind: TokenKind.FLOAT,
start: 0,
end: 6,
value: '123e-4',
});
expect(lexOne('123e+4')).to.contain({
kind: TokenKind.FLOAT,
start: 0,
end: 6,
value: '123e+4',
});
expect(lexOne('-1.123e4')).to.contain({
kind: TokenKind.FLOAT,
start: 0,
end: 8,
value: '-1.123e4',
});
expect(lexOne('-1.123E4')).to.contain({
kind: TokenKind.FLOAT,
start: 0,
end: 8,
value: '-1.123E4',
});
expect(lexOne('-1.123e-4')).to.contain({
kind: TokenKind.FLOAT,
start: 0,
end: 9,
value: '-1.123e-4',
});
expect(lexOne('-1.123e+4')).to.contain({
kind: TokenKind.FLOAT,
start: 0,
end: 9,
value: '-1.123e+4',
});
expect(lexOne('-1.123e4567')).to.contain({
kind: TokenKind.FLOAT,
start: 0,
end: 11,
value: '-1.123e4567',
});
});
it('lex reports useful number errors', () => {
expectSyntaxError('00').to.deep.equal({
message: 'Syntax Error: Invalid number, unexpected digit after 0: "0".',
locations: [{ line: 1, column: 2 }],
});
expectSyntaxError('01').to.deep.equal({
message: 'Syntax Error: Invalid number, unexpected digit after 0: "1".',
locations: [{ line: 1, column: 2 }],
});
expectSyntaxError('01.23').to.deep.equal({
message: 'Syntax Error: Invalid number, unexpected digit after 0: "1".',
locations: [{ line: 1, column: 2 }],
});
expectSyntaxError('+1').to.deep.equal({
message: 'Syntax Error: Unexpected character: "+".',
locations: [{ line: 1, column: 1 }],
});
expectSyntaxError('1.').to.deep.equal({
message: 'Syntax Error: Invalid number, expected digit but got: <EOF>.',
locations: [{ line: 1, column: 3 }],
});
expectSyntaxError('1e').to.deep.equal({
message: 'Syntax Error: Invalid number, expected digit but got: <EOF>.',
locations: [{ line: 1, column: 3 }],
});
expectSyntaxError('1E').to.deep.equal({
message: 'Syntax Error: Invalid number, expected digit but got: <EOF>.',
locations: [{ line: 1, column: 3 }],
});
expectSyntaxError('1.e1').to.deep.equal({
message: 'Syntax Error: Invalid number, expected digit but got: "e".',
locations: [{ line: 1, column: 3 }],
});
expectSyntaxError('.123').to.deep.equal({
message: 'Syntax Error: Unexpected character: ".".',
locations: [{ line: 1, column: 1 }],
});
expectSyntaxError('1.A').to.deep.equal({
message: 'Syntax Error: Invalid number, expected digit but got: "A".',
locations: [{ line: 1, column: 3 }],
});
expectSyntaxError('-A').to.deep.equal({
message: 'Syntax Error: Invalid number, expected digit but got: "A".',
locations: [{ line: 1, column: 2 }],
});
expectSyntaxError('1.0e').to.deep.equal({
message: 'Syntax Error: Invalid number, expected digit but got: <EOF>.',
locations: [{ line: 1, column: 5 }],
});
expectSyntaxError('1.0eA').to.deep.equal({
message: 'Syntax Error: Invalid number, expected digit but got: "A".',
locations: [{ line: 1, column: 5 }],
});
expectSyntaxError('1.0e"').to.deep.equal({
message: "Syntax Error: Invalid number, expected digit but got: '\"'.",
locations: [{ line: 1, column: 5 }],
});
expectSyntaxError('1.2e3e').to.deep.equal({
message: 'Syntax Error: Invalid number, expected digit but got: "e".',
locations: [{ line: 1, column: 6 }],
});
expectSyntaxError('1.2e3.4').to.deep.equal({
message: 'Syntax Error: Invalid number, expected digit but got: ".".',
locations: [{ line: 1, column: 6 }],
});
expectSyntaxError('1.23.4').to.deep.equal({
message: 'Syntax Error: Invalid number, expected digit but got: ".".',
locations: [{ line: 1, column: 5 }],
});
});
it('lex does not allow name-start after a number', () => {
expectSyntaxError('0xF1').to.deep.equal({
message: 'Syntax Error: Invalid number, expected digit but got: "x".',
locations: [{ line: 1, column: 2 }],
});
expectSyntaxError('0b10').to.deep.equal({
message: 'Syntax Error: Invalid number, expected digit but got: "b".',
locations: [{ line: 1, column: 2 }],
});
expectSyntaxError('123abc').to.deep.equal({
message: 'Syntax Error: Invalid number, expected digit but got: "a".',
locations: [{ line: 1, column: 4 }],
});
expectSyntaxError('1_234').to.deep.equal({
message: 'Syntax Error: Invalid number, expected digit but got: "_".',
locations: [{ line: 1, column: 2 }],
});
expectSyntaxError('1\u00DF').to.deep.equal({
message: 'Syntax Error: Unexpected character: U+00DF.',
locations: [{ line: 1, column: 2 }],
});
expectSyntaxError('1.23f').to.deep.equal({
message: 'Syntax Error: Invalid number, expected digit but got: "f".',
locations: [{ line: 1, column: 5 }],
});
expectSyntaxError('1.234_5').to.deep.equal({
message: 'Syntax Error: Invalid number, expected digit but got: "_".',
locations: [{ line: 1, column: 6 }],
});
});
it('lexes punctuation', () => {
expect(lexOne('!')).to.contain({
kind: TokenKind.BANG,
start: 0,
end: 1,
value: undefined,
});
expect(lexOne('?')).to.contain({
kind: TokenKind.QUESTION_MARK,
start: 0,
end: 1,
value: undefined,
});
expect(lexOne('$')).to.contain({
kind: TokenKind.DOLLAR,
start: 0,
end: 1,
value: undefined,
});
expect(lexOne('(')).to.contain({
kind: TokenKind.PAREN_L,
start: 0,
end: 1,
value: undefined,
});
expect(lexOne(')')).to.contain({
kind: TokenKind.PAREN_R,
start: 0,
end: 1,
value: undefined,
});
expect(lexOne('...')).to.contain({
kind: TokenKind.SPREAD,
start: 0,
end: 3,
value: undefined,
});
expect(lexOne(':')).to.contain({
kind: TokenKind.COLON,
start: 0,
end: 1,
value: undefined,
});
expect(lexOne('=')).to.contain({
kind: TokenKind.EQUALS,
start: 0,
end: 1,
value: undefined,
});
expect(lexOne('@')).to.contain({
kind: TokenKind.AT,
start: 0,
end: 1,
value: undefined,
});
expect(lexOne('[')).to.contain({
kind: TokenKind.BRACKET_L,
start: 0,
end: 1,
value: undefined,
});
expect(lexOne(']')).to.contain({
kind: TokenKind.BRACKET_R,
start: 0,
end: 1,
value: undefined,
});
expect(lexOne('{')).to.contain({
kind: TokenKind.BRACE_L,
start: 0,
end: 1,
value: undefined,
});
expect(lexOne('|')).to.contain({
kind: TokenKind.PIPE,
start: 0,
end: 1,
value: undefined,
});
expect(lexOne('}')).to.contain({
kind: TokenKind.BRACE_R,
start: 0,
end: 1,
value: undefined,
});
});
it('lex reports useful unknown character error', () => {
expectSyntaxError('..').to.deep.equal({
message: 'Syntax Error: Unexpected character: ".".',
locations: [{ line: 1, column: 1 }],
});
expectSyntaxError('~').to.deep.equal({
message: 'Syntax Error: Unexpected character: "~".',
locations: [{ line: 1, column: 1 }],
});
expectSyntaxError('\x00').to.deep.equal({
message: 'Syntax Error: Unexpected character: U+0000.',
locations: [{ line: 1, column: 1 }],
});
expectSyntaxError('\b').to.deep.equal({
message: 'Syntax Error: Unexpected character: U+0008.',
locations: [{ line: 1, column: 1 }],
});
expectSyntaxError('\u00AA').to.deep.equal({
message: 'Syntax Error: Unexpected character: U+00AA.',
locations: [{ line: 1, column: 1 }],
});
expectSyntaxError('\u0AAA').to.deep.equal({
message: 'Syntax Error: Unexpected character: U+0AAA.',
locations: [{ line: 1, column: 1 }],
});
expectSyntaxError('\u203B').to.deep.equal({
message: 'Syntax Error: Unexpected character: U+203B.',
locations: [{ line: 1, column: 1 }],
});
expectSyntaxError('\u{1f600}').to.deep.equal({
message: 'Syntax Error: Unexpected character: U+1F600.',
locations: [{ line: 1, column: 1 }],
});
expectSyntaxError('\uD83D\uDE00').to.deep.equal({
message: 'Syntax Error: Unexpected character: U+1F600.',
locations: [{ line: 1, column: 1 }],
});
expectSyntaxError('\uD800\uDC00').to.deep.equal({
message: 'Syntax Error: Unexpected character: U+10000.',
locations: [{ line: 1, column: 1 }],
});
expectSyntaxError('\uDBFF\uDFFF').to.deep.equal({
message: 'Syntax Error: Unexpected character: U+10FFFF.',
locations: [{ line: 1, column: 1 }],
});
expectSyntaxError('\uDEAD').to.deep.equal({
message: 'Syntax Error: Invalid character: U+DEAD.',
locations: [{ line: 1, column: 1 }],
});
});
it('lex reports useful information for dashes in names', () => {
const source = new Source('a-b');
const lexer = new Lexer(source);
const firstToken = lexer.advance();
expect(firstToken).to.contain({
kind: TokenKind.NAME,
start: 0,
end: 1,
value: 'a',
});
expect(() => lexer.advance())
.throw(GraphQLError)
.that.deep.include({
message: 'Syntax Error: Invalid number, expected digit but got: "b".',
locations: [{ line: 1, column: 3 }],
});
});
it('produces double linked list of tokens, including comments', () => {
const source = new Source(`
{
#comment
field
}
`);
const lexer = new Lexer(source);
const startToken = lexer.token;
let endToken;
do {
endToken = lexer.advance();
expect(endToken.kind).to.not.equal(TokenKind.COMMENT);
} while (endToken.kind !== TokenKind.EOF);
expect(startToken.prev).to.equal(null);
expect(endToken.next).to.equal(null);
const tokens = [];
for (let tok: Token | null = startToken; tok; tok = tok.next) {
if (tokens.length) {
expect(tok.prev).to.equal(tokens[tokens.length - 1]);
}
tokens.push(tok);
}
expect(tokens.map((tok) => tok.kind)).to.deep.equal([
TokenKind.SOF,
TokenKind.BRACE_L,
TokenKind.COMMENT,
TokenKind.NAME,
TokenKind.BRACE_R,
TokenKind.EOF,
]);
});
it('lexes comments', () => {
expect(lexOne('# Comment').prev).to.contain({
kind: TokenKind.COMMENT,
start: 0,
end: 9,
value: ' Comment',
});
expect(lexOne('# Comment\nAnother line').prev).to.contain({
kind: TokenKind.COMMENT,
start: 0,
end: 9,
value: ' Comment',
});
expect(lexOne('# Comment\r\nAnother line').prev).to.contain({
kind: TokenKind.COMMENT,
start: 0,
end: 9,
value: ' Comment',
});
expect(lexOne('# Comment \u{1f600}').prev).to.contain({
kind: TokenKind.COMMENT,
start: 0,
end: 12,
value: ' Comment \u{1f600}',
});
expectSyntaxError('# Invalid surrogate \uDEAD').to.deep.equal({
message: 'Syntax Error: Invalid character: U+DEAD.',
locations: [{ line: 1, column: 21 }],
});
});
});
describe('isPunctuatorTokenKind', () => {
function isPunctuatorToken(text: string) {
return isPunctuatorTokenKind(lexOne(text).kind);
}
it('returns true for punctuator tokens', () => {
expect(isPunctuatorToken('!')).to.equal(true);
expect(isPunctuatorToken('?')).to.equal(true);
expect(isPunctuatorToken('$')).to.equal(true);
expect(isPunctuatorToken('&')).to.equal(true);
expect(isPunctuatorToken('(')).to.equal(true);
expect(isPunctuatorToken(')')).to.equal(true);
expect(isPunctuatorToken('...')).to.equal(true);
expect(isPunctuatorToken(':')).to.equal(true);
expect(isPunctuatorToken('=')).to.equal(true);
expect(isPunctuatorToken('@')).to.equal(true);
expect(isPunctuatorToken('[')).to.equal(true);
expect(isPunctuatorToken(']')).to.equal(true);
expect(isPunctuatorToken('{')).to.equal(true);
expect(isPunctuatorToken('|')).to.equal(true);
expect(isPunctuatorToken('}')).to.equal(true);
});
it('returns false for non-punctuator tokens', () => {
expect(isPunctuatorToken('')).to.equal(false);
expect(isPunctuatorToken('name')).to.equal(false);
expect(isPunctuatorToken('1')).to.equal(false);
expect(isPunctuatorToken('3.14')).to.equal(false);
expect(isPunctuatorToken('"str"')).to.equal(false);
expect(isPunctuatorToken('"""str"""')).to.equal(false);
});
});