Repository: facebook/react
Pull request: [compiler] Emit more specific error when making identifiers with reserved words


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.


Changed files:
filename: compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
patch: @@ -14,6 +14,7 @@ import type {HookKind} from './ObjectShape';
 import {Type, makeType} from './Types';
 import {z} from 'zod';
 import type {AliasingEffect} from '../Inference/AliasingEffects';
+import {isReservedWord} from '../Utils/Keyword';
 
 /*
  * *******************************************************************************************
@@ -1320,12 +1321,21 @@ export function forkTemporaryIdentifier(
  * original source code.
  */
 export function makeIdentifierName(name: string): ValidatedIdentifier {
-  CompilerError.invariant(t.isValidIdentifier(name), {
-    reason: `Expected a valid identifier name`,
-    loc: GeneratedSource,
-    description: `\`${name}\` is not a valid JavaScript identifier`,
-    suggestions: null,
-  });
+  if (isReservedWord(name)) {
+    CompilerError.throwInvalidJS({
+      reason: 'Expected a non-reserved identifier name',
+      loc: GeneratedSource,
+      description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`,
+      suggestions: null,
+    });
+  } else {
+    CompilerError.invariant(t.isValidIdentifier(name), {
+      reason: `Expected a valid identifier name`,
+      loc: GeneratedSource,
+      description: `\`${name}\` is not a valid JavaScript identifier`,
+      suggestions: null,
+    });
+  }
   return {
     kind: 'named',
     value: name as ValidIdentifierName,

filename: compiler/packages/babel-plugin-react-compiler/src/Utils/Keyword.ts
patch: @@ -0,0 +1,87 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+/**
+ * https://tc39.es/ecma262/multipage/ecmascript-language-lexical-grammar.html#sec-keywords-and-reserved-words
+ */
+
+/**
+ * Note: `await` and `yield` are contextually allowed as identifiers.
+ *   await: reserved inside async functions and modules
+ *   yield: reserved inside generator functions
+ *
+ * Note: `async` is not reserved.
+ */
+const RESERVED_WORDS = new Set([
+  'break',
+  'case',
+  'catch',
+  'class',
+  'const',
+  'continue',
+  'debugger',
+  'default',
+  'delete',
+  'do',
+  'else',
+  'enum',
+  'export',
+  'extends',
+  'false',
+  'finally',
+  'for',
+  'function',
+  'if',
+  'import',
+  'in',
+  'instanceof',
+  'new',
+  'null',
+  'return',
+  'super',
+  'switch',
+  'this',
+  'throw',
+  'true',
+  'try',
+  'typeof',
+  'var',
+  'void',
+  'while',
+  'with',
+]);
+
+/**
+ * Reserved when a module has a 'use strict' directive.
+ */
+const STRICT_MODE_RESERVED_WORDS = new Set([
+  'let',
+  'static',
+  'implements',
+  'interface',
+  'package',
+  'private',
+  'protected',
+  'public',
+]);
+/**
+ * The names arguments and eval are not keywords, but they are subject to some restrictions in
+ * strict mode code.
+ */
+const STRICT_MODE_RESTRICTED_WORDS = new Set(['eval', 'arguments']);
+
+/**
+ * Conservative check for whether an identifer name is reserved or not. We assume that code is
+ * written with strict mode.
+ */
+export function isReservedWord(identifierName: string): boolean {
+  return (
+    RESERVED_WORDS.has(identifierName) ||
+    STRICT_MODE_RESERVED_WORDS.has(identifierName) ||
+    STRICT_MODE_RESTRICTED_WORDS.has(identifierName)
+  );
+}

filename: compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ecma/error.reserved-words.expect.md
patch: @@ -0,0 +1,32 @@
+
+## Input
+
+```javascript
+import {useRef} from 'react';
+
+function useThing(fn) {
+  const fnRef = useRef(fn);
+  const ref = useRef(null);
+
+  if (ref.current === null) {
+    ref.current = function (this: unknown, ...args) {
+      return fnRef.current.call(this, ...args);
+    };
+  }
+  return ref.current;
+}
+
+```
+
+
+## Error
+
+```
+Found 1 error:
+
+Error: Expected a non-reserved identifier name
+
+`this` is a reserved word in JavaScript and cannot be used as an identifier name.
+```
+          
+      
\ No newline at end of file

filename: compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ecma/error.reserved-words.ts
patch: @@ -0,0 +1,13 @@
+import {useRef} from 'react';
+
+function useThing(fn) {
+  const fnRef = useRef(fn);
+  const ref = useRef(null);
+
+  if (ref.current === null) {
+    ref.current = function (this: unknown, ...args) {
+      return fnRef.current.call(this, ...args);
+    };
+  }
+  return ref.current;
+}