/**
* 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.
*/
import {ErrorSeverity} from 'babel-plugin-react-compiler/src';
import {RuleTester as ESLintTester} from 'eslint';
import ReactCompilerRule from '../src/rules/ReactCompilerRule';
/**
* A string template tag that removes padding from the left side of multi-line strings
* @param {Array} strings array of code strings (only one expected)
*/
function normalizeIndent(strings: TemplateStringsArray): string {
const codeLines = strings[0].split('\n');
const leftPadding = codeLines[1].match(/\s+/)![0];
return codeLines.map(line => line.slice(leftPadding.length)).join('\n');
}
type CompilerTestCases = {
valid: ESLintTester.ValidTestCase[];
invalid: ESLintTester.InvalidTestCase[];
};
const tests: CompilerTestCases = {
valid: [
{
name: 'Basic example',
code: normalizeIndent`
function foo(x, y) {
if (x) {
return foo(false, y);
}
return [y * 10];
}
`,
},
{
name: 'Violation with Flow suppression',
code: `
// Valid since error already suppressed with flow.
function useHookWithHook() {
if (cond) {
// $FlowFixMe[react-rule-hook]
useConditionalHook();
}
}
`,
},
{
name: 'Basic example with component syntax',
code: normalizeIndent`
export default component HelloWorld(
text: string = 'Hello!',
onClick: () => void,
) {
return
{text}
;
}
`,
},
{
name: 'Unsupported syntax',
code: normalizeIndent`
function foo(x) {
var y = 1;
return y * x;
}
`,
},
{
// OK because invariants are only meant for the compiler team's consumption
name: '[Invariant] Defined after use',
code: normalizeIndent`
function Component(props) {
let y = function () {
m(x);
};
let x = { a };
m(x);
return y;
}
`,
},
{
name: "Classes don't throw",
code: normalizeIndent`
class Foo {
#bar() {}
}
`,
},
],
invalid: [
{
name: 'Reportable levels can be configured',
options: [{reportableLevels: new Set([ErrorSeverity.Todo])}],
code: normalizeIndent`
function Foo(x) {
var y = 1;
return {y * x}
;
}`,
errors: [
{
message: /Handle var kinds in VariableDeclaration/,
},
],
},
{
name: '[InvalidReact] ESlint suppression',
// Indentation is intentionally weird so it doesn't add extra whitespace
code: normalizeIndent`
function Component(props) {
// eslint-disable-next-line react-hooks/rules-of-hooks
return {props.foo}
;
}`,
errors: [
{
message: /React Compiler has skipped optimizing this component/,
suggestions: [
{
output: normalizeIndent`
function Component(props) {
return {props.foo}
;
}`,
},
],
},
{
message:
"Definition for rule 'react-hooks/rules-of-hooks' was not found.",
},
],
},
{
name: 'Multiple diagnostics are surfaced',
options: [
{
reportableLevels: new Set([
ErrorSeverity.Todo,
ErrorSeverity.InvalidReact,
]),
},
],
code: normalizeIndent`
function Foo(x) {
var y = 1;
return {y * x}
;
}
function Bar(props) {
props.a.b = 2;
return {props.c}
}`,
errors: [
{
message: /Handle var kinds in VariableDeclaration/,
},
{
message: /Mutating component props or hook arguments is not allowed/,
},
],
},
{
name: 'Test experimental/unstable report all bailouts mode',
options: [
{
reportableLevels: new Set([ErrorSeverity.InvalidReact]),
__unstable_donotuse_reportAllBailouts: true,
},
],
code: normalizeIndent`
function Foo(x) {
var y = 1;
return {y * x}
;
}`,
errors: [
{
message: /Handle var kinds in VariableDeclaration/,
},
],
},
{
name: "'use no forget' does not disable eslint rule",
code: normalizeIndent`
let count = 0;
function Component() {
'use no forget';
count = count + 1;
return Hello world {count}
}
`,
errors: [
{
message:
/Unexpected reassignment of a variable which was defined outside of the component/,
},
],
},
{
name: "Unused 'use no forget' directive is reported when no errors are present on components",
code: normalizeIndent`
function Component() {
'use no forget';
return Hello world
}
`,
errors: [
{
message: "Unused 'use no forget' directive",
suggestions: [
{
output:
// yuck
'\nfunction Component() {\n \n return Hello world
\n}\n',
},
],
},
],
},
{
name: "Unused 'use no forget' directive is reported when no errors are present on non-components or hooks",
code: normalizeIndent`
function notacomponent() {
'use no forget';
return 1 + 1;
}
`,
errors: [
{
message: "Unused 'use no forget' directive",
suggestions: [
{
output:
// yuck
'\nfunction notacomponent() {\n \n return 1 + 1;\n}\n',
},
],
},
],
},
{
name: 'Pipeline errors are reported',
code: normalizeIndent`
import useMyEffect from 'useMyEffect';
import {AUTODEPS} from 'react';
function Component({a}) {
'use no memo';
useMyEffect(() => console.log(a.b), AUTODEPS);
return Hello world
;
}
`,
options: [
{
environment: {
inferEffectDependencies: [
{
function: {
source: 'useMyEffect',
importSpecifierName: 'default',
},
autodepsIndex: 1,
},
],
},
},
],
errors: [
{
message: /Cannot infer dependencies of this effect/,
},
],
},
],
};
const eslintTester = new ESLintTester({
parser: require.resolve('hermes-eslint'),
parserOptions: {
ecmaVersion: 2015,
sourceType: 'module',
enableExperimentalComponentSyntax: true,
},
});
eslintTester.run('react-compiler', ReactCompilerRule, tests);