Skip to content

Commit 22b9504

Browse files
assertValidName: share character classes with lexer (#3287)
1 parent 0c7165a commit 22b9504

File tree

5 files changed

+91
-53
lines changed

5 files changed

+91
-53
lines changed

src/language/characterClasses.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* ```
3+
* Digit :: one of
4+
* - `0` `1` `2` `3` `4` `5` `6` `7` `8` `9`
5+
* ```
6+
* @internal
7+
*/
8+
export function isDigit(code: number): boolean {
9+
return code >= 0x0030 && code <= 0x0039;
10+
}
11+
12+
/**
13+
* ```
14+
* Letter :: one of
15+
* - `A` `B` `C` `D` `E` `F` `G` `H` `I` `J` `K` `L` `M`
16+
* - `N` `O` `P` `Q` `R` `S` `T` `U` `V` `W` `X` `Y` `Z`
17+
* - `a` `b` `c` `d` `e` `f` `g` `h` `i` `j` `k` `l` `m`
18+
* - `n` `o` `p` `q` `r` `s` `t` `u` `v` `w` `x` `y` `z`
19+
* ```
20+
* @internal
21+
*/
22+
export function isLetter(code: number): boolean {
23+
return (
24+
(code >= 0x0061 && code <= 0x007a) || // A-Z
25+
(code >= 0x0041 && code <= 0x005a) // a-z
26+
);
27+
}
28+
29+
/**
30+
* ```
31+
* NameStart ::
32+
* - Letter
33+
* - `_`
34+
* ```
35+
* @internal
36+
*/
37+
export function isNameStart(code: number): boolean {
38+
return isLetter(code) || code === 0x005f;
39+
}
40+
41+
/**
42+
* ```
43+
* NameContinue ::
44+
* - Letter
45+
* - Digit
46+
* - `_`
47+
* ```
48+
* @internal
49+
*/
50+
export function isNameContinue(code: number): boolean {
51+
return isLetter(code) || isDigit(code) || code === 0x005f;
52+
}

src/language/lexer.ts

Lines changed: 2 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { TokenKindEnum } from './tokenKind';
55
import { Token } from './ast';
66
import { TokenKind } from './tokenKind';
77
import { dedentBlockStringValue } from './blockString';
8+
import { isDigit, isNameStart, isNameContinue } from './characterClasses';
89

910
/**
1011
* Given a Source object, creates a Lexer for that source.
@@ -836,15 +837,6 @@ function readBlockString(lexer: Lexer, start: number): Token {
836837
* ```
837838
* Name ::
838839
* - NameStart NameContinue* [lookahead != NameContinue]
839-
*
840-
* NameStart ::
841-
* - Letter
842-
* - `_`
843-
*
844-
* NameContinue ::
845-
* - Letter
846-
* - Digit
847-
* - `_`
848840
* ```
849841
*/
850842
function readName(lexer: Lexer, start: number): Token {
@@ -854,8 +846,7 @@ function readName(lexer: Lexer, start: number): Token {
854846

855847
while (position < bodyLength) {
856848
const code = body.charCodeAt(position);
857-
// NameContinue
858-
if (isLetter(code) || isDigit(code) || code === 0x005f) {
849+
if (isNameContinue(code)) {
859850
++position;
860851
} else {
861852
break;
@@ -870,33 +861,3 @@ function readName(lexer: Lexer, start: number): Token {
870861
body.slice(start, position),
871862
);
872863
}
873-
874-
function isNameStart(code: number): boolean {
875-
return isLetter(code) || code === 0x005f;
876-
}
877-
878-
/**
879-
* ```
880-
* Digit :: one of
881-
* - `0` `1` `2` `3` `4` `5` `6` `7` `8` `9`
882-
* ```
883-
*/
884-
function isDigit(code: number): boolean {
885-
return code >= 0x0030 && code <= 0x0039;
886-
}
887-
888-
/**
889-
* ```
890-
* Letter :: one of
891-
* - `A` `B` `C` `D` `E` `F` `G` `H` `I` `J` `K` `L` `M`
892-
* - `N` `O` `P` `Q` `R` `S` `T` `U` `V` `W` `X` `Y` `Z`
893-
* - `a` `b` `c` `d` `e` `f` `g` `h` `i` `j` `k` `l` `m`
894-
* - `n` `o` `p` `q` `r` `s` `t` `u` `v` `w` `x` `y` `z`
895-
* ```
896-
*/
897-
function isLetter(code: number): boolean {
898-
return (
899-
(code >= 0x0061 && code <= 0x007a) || // A-Z
900-
(code >= 0x0041 && code <= 0x005a) // a-z
901-
);
902-
}

src/type/__tests__/validation-test.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ describe('Type System: Objects must have fields', () => {
493493
expectJSON(validateSchema(schema)).to.deep.equal([
494494
{
495495
message:
496-
'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "bad-name-with-dashes" does not.',
496+
'Names must only contain [_a-zA-Z0-9] but "bad-name-with-dashes" does not.',
497497
},
498498
]);
499499
});
@@ -535,7 +535,7 @@ describe('Type System: Fields args must be properly named', () => {
535535
expectJSON(validateSchema(schema)).to.deep.equal([
536536
{
537537
message:
538-
'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "bad-name-with-dashes" does not.',
538+
'Names must only contain [_a-zA-Z0-9] but "bad-name-with-dashes" does not.',
539539
},
540540
]);
541541
});
@@ -968,24 +968,22 @@ describe('Type System: Enum types must be well defined', () => {
968968
const schema1 = schemaWithEnum({ '#value': {} });
969969
expectJSON(validateSchema(schema1)).to.deep.equal([
970970
{
971-
message:
972-
'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "#value" does not.',
971+
message: 'Names must start with [_a-zA-Z] but "#value" does not.',
973972
},
974973
]);
975974

976975
const schema2 = schemaWithEnum({ '1value': {} });
977976
expectJSON(validateSchema(schema2)).to.deep.equal([
978977
{
979-
message:
980-
'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "1value" does not.',
978+
message: 'Names must start with [_a-zA-Z] but "1value" does not.',
981979
},
982980
]);
983981

984982
const schema3 = schemaWithEnum({ 'KEBAB-CASE': {} });
985983
expectJSON(validateSchema(schema3)).to.deep.equal([
986984
{
987985
message:
988-
'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "KEBAB-CASE" does not.',
986+
'Names must only contain [_a-zA-Z0-9] but "KEBAB-CASE" does not.',
989987
},
990988
]);
991989

src/utilities/__tests__/assertValidName-test.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,21 @@ describe('assertValidName()', () => {
1919
expect(() => assertValidName({})).to.throw('Expected name to be a string.');
2020
});
2121

22+
it('throws on empty strings', () => {
23+
expect(() => assertValidName('')).to.throw(
24+
'Expected name to be a non-empty string.',
25+
);
26+
});
27+
2228
it('throws for names with invalid characters', () => {
23-
expect(() => assertValidName('>--()-->')).to.throw(/Names must match/);
29+
expect(() => assertValidName('>--()-->')).to.throw(
30+
'Names must only contain [_a-zA-Z0-9] but ">--()-->" does not.',
31+
);
32+
});
33+
34+
it('throws for names starting with invalid characters', () => {
35+
expect(() => assertValidName('42MeaningsOfLife')).to.throw(
36+
'Names must start with [_a-zA-Z] but "42MeaningsOfLife" does not.',
37+
);
2438
});
2539
});

src/utilities/assertValidName.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { devAssert } from '../jsutils/devAssert';
22

33
import { GraphQLError } from '../error/GraphQLError';
4-
5-
const NAME_RX = /^[_a-zA-Z][_a-zA-Z0-9]*$/;
4+
import { isNameStart, isNameContinue } from '../language/characterClasses';
65

76
/**
87
* Upholds the spec rules about naming.
@@ -20,14 +19,28 @@ export function assertValidName(name: string): string {
2019
*/
2120
export function isValidNameError(name: string): GraphQLError | undefined {
2221
devAssert(typeof name === 'string', 'Expected name to be a string.');
22+
2323
if (name.startsWith('__')) {
2424
return new GraphQLError(
2525
`Name "${name}" must not begin with "__", which is reserved by GraphQL introspection.`,
2626
);
2727
}
28-
if (!NAME_RX.test(name)) {
28+
29+
if (name.length === 0) {
30+
return new GraphQLError('Expected name to be a non-empty string.');
31+
}
32+
33+
for (let i = 1; i < name.length; ++i) {
34+
if (!isNameContinue(name.charCodeAt(i))) {
35+
return new GraphQLError(
36+
`Names must only contain [_a-zA-Z0-9] but "${name}" does not.`,
37+
);
38+
}
39+
}
40+
41+
if (!isNameStart(name.charCodeAt(0))) {
2942
return new GraphQLError(
30-
`Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "${name}" does not.`,
43+
`Names must start with [_a-zA-Z] but "${name}" does not.`,
3144
);
3245
}
3346
}

0 commit comments

Comments
 (0)