diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts index f9caa844f3..51715b3c1e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -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, diff --git a/compiler/packages/babel-plugin-react-compiler/src/Utils/Keyword.ts b/compiler/packages/babel-plugin-react-compiler/src/Utils/Keyword.ts new file mode 100644 index 0000000000..3964f4accc --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Utils/Keyword.ts @@ -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) + ); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ecma/error.reserved-words.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ecma/error.reserved-words.expect.md new file mode 100644 index 0000000000..a6ee8a798b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ecma/error.reserved-words.expect.md @@ -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 diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ecma/error.reserved-words.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ecma/error.reserved-words.ts new file mode 100644 index 0000000000..2937ba8df5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ecma/error.reserved-words.ts @@ -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; +}