@samitouri / QOS-React / commits / 52612a7cbd

[compiler] Emit more specific error when making identifiers with reserved words (#34080)

This currently throws an invariant which may be misleading. I checked the ecma262 spec and used the same list of reserved words in our check. To err on the side of being conservative, we also error when strict mode reserved words are used.

lauren committed Aug 1, 2025 at 15:10 UTC 52612a7cbdd8e1fee9599478247f78725869ebad
4 files changed +148 -6
compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
+16 -6
@@ -14,6 +14,7 @@ import type {HookKind} from './ObjectShape';
14 import {Type, makeType} from './Types';
15 import {z} from 'zod';
16 import type {AliasingEffect} from '../Inference/AliasingEffects';
17 +import {isReservedWord} from '../Utils/Keyword';
18
19 /*
20 * *******************************************************************************************
@@ -1320,12 +1321,21 @@ export function forkTemporaryIdentifier(
1321 * original source code.
1322 */
1323 export function makeIdentifierName(name: string): ValidatedIdentifier {
1323 - CompilerError.invariant(t.isValidIdentifier(name), {
1324 - reason: `Expected a valid identifier name`,
1325 - loc: GeneratedSource,
1326 - description: `\`${name}\` is not a valid JavaScript identifier`,
1327 - suggestions: null,
1328 - });
1324 + if (isReservedWord(name)) {
1325 + CompilerError.throwInvalidJS({
1326 + reason: 'Expected a non-reserved identifier name',
1327 + loc: GeneratedSource,
1328 + description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`,
1329 + suggestions: null,
1330 + });
1331 + } else {
1332 + CompilerError.invariant(t.isValidIdentifier(name), {
1333 + reason: `Expected a valid identifier name`,
1334 + loc: GeneratedSource,
1335 + description: `\`${name}\` is not a valid JavaScript identifier`,
1336 + suggestions: null,
1337 + });
1338 + }
1339 return {
1340 kind: 'named',
1341 value: name as ValidIdentifierName,
compiler/packages/babel-plugin-react-compiler/src/Utils/Keyword.ts new
+87
@@ -0,0 +1,87 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +/**
9 + * https://tc39.es/ecma262/multipage/ecmascript-language-lexical-grammar.html#sec-keywords-and-reserved-words
10 + */
11 +
12 +/**
13 + * Note: `await` and `yield` are contextually allowed as identifiers.
14 + * await: reserved inside async functions and modules
15 + * yield: reserved inside generator functions
16 + *
17 + * Note: `async` is not reserved.
18 + */
19 +const RESERVED_WORDS = new Set([
20 + 'break',
21 + 'case',
22 + 'catch',
23 + 'class',
24 + 'const',
25 + 'continue',
26 + 'debugger',
27 + 'default',
28 + 'delete',
29 + 'do',
30 + 'else',
31 + 'enum',
32 + 'export',
33 + 'extends',
34 + 'false',
35 + 'finally',
36 + 'for',
37 + 'function',
38 + 'if',
39 + 'import',
40 + 'in',
41 + 'instanceof',
42 + 'new',
43 + 'null',
44 + 'return',
45 + 'super',
46 + 'switch',
47 + 'this',
48 + 'throw',
49 + 'true',
50 + 'try',
51 + 'typeof',
52 + 'var',
53 + 'void',
54 + 'while',
55 + 'with',
56 +]);
57 +
58 +/**
59 + * Reserved when a module has a 'use strict' directive.
60 + */
61 +const STRICT_MODE_RESERVED_WORDS = new Set([
62 + 'let',
63 + 'static',
64 + 'implements',
65 + 'interface',
66 + 'package',
67 + 'private',
68 + 'protected',
69 + 'public',
70 +]);
71 +/**
72 + * The names arguments and eval are not keywords, but they are subject to some restrictions in
73 + * strict mode code.
74 + */
75 +const STRICT_MODE_RESTRICTED_WORDS = new Set(['eval', 'arguments']);
76 +
77 +/**
78 + * Conservative check for whether an identifer name is reserved or not. We assume that code is
79 + * written with strict mode.
80 + */
81 +export function isReservedWord(identifierName: string): boolean {
82 + return (
83 + RESERVED_WORDS.has(identifierName) ||
84 + STRICT_MODE_RESERVED_WORDS.has(identifierName) ||
85 + STRICT_MODE_RESTRICTED_WORDS.has(identifierName)
86 + );
87 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ecma/error.reserved-words.expect.md new
+32
@@ -0,0 +1,32 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {useRef} from 'react';
6 +
7 +function useThing(fn) {
8 + const fnRef = useRef(fn);
9 + const ref = useRef(null);
10 +
11 + if (ref.current === null) {
12 + ref.current = function (this: unknown, ...args) {
13 + return fnRef.current.call(this, ...args);
14 + };
15 + }
16 + return ref.current;
17 +}
18 +
19 +```
20 +
21 +
22 +## Error
23 +
24 +```
25 +Found 1 error:
26 +
27 +Error: Expected a non-reserved identifier name
28 +
29 +`this` is a reserved word in JavaScript and cannot be used as an identifier name.
30 +```
31 +
32 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ecma/error.reserved-words.ts new
+13
@@ -0,0 +1,13 @@
1 +import {useRef} from 'react';
2 +
3 +function useThing(fn) {
4 + const fnRef = useRef(fn);
5 + const ref = useRef(null);
6 +
7 + if (ref.current === null) {
8 + ref.current = function (this: unknown, ...args) {
9 + return fnRef.current.call(this, ...args);
10 + };
11 + }
12 + return ref.current;
13 +}