react/compiler/packages/eslint-plugin-react-compiler/__tests__/PluginTest-test.ts
Joseph Savona 7d29ecbeb2
[compiler] Aggregate error reporting, separate eslint rules (#34176)
NOTE: this is a merged version of @mofeiZ's original PR along with my
edits per offline discussion. The description is updated to reflect the
latest approach.

The key problem we're trying to solve with this PR is to allow
developers more control over the compiler's various validations. The
idea is to have a number of rules targeting a specific category of
issues, such as enforcing immutability of props/state/etc or disallowing
access to refs during render. We don't want to have to run the compiler
again for every single rule, though, so @mofeiZ added an LRU cache that
caches the full compilation output of N most recent files. The first
rule to run on a given file will cause it to get cached, and then
subsequent rules can pull from the cache, with each rule filtering down
to its specific category of errors.

For the categories, I went through and assigned a category roughly 1:1
to existing validations, and then used my judgement on some places that
felt distinct enough to warrant a separate error. Every error in the
compiler now has to supply both a severity (for legacy reasons) and a
category (for ESLint). Each category corresponds 1:1 to a ESLint rule
definition, so that the set of rules is automatically populated based on
the defined categories.

Categories include a flag for whether they should be in the recommended
set or not.

Note that as with the original version of this PR, only
eslint-plugin-react-compiler is changed. We still have to update the
main lint rule.

## Test Plan

* Created a sample project using ESLint v9 and verified that the plugin
can be configured correctly and detects errors
* Edited `fixtures/eslint-v9` and introduced errors, verified that the w
latest config changes in that fixture it correctly detects the errors
* In the sample project, confirmed that the LRU caching is correctly
caching compiler output, ie compiling files just once.

Co-authored-by: Mofei Zhang <feifei0@meta.com>
2025-08-21 14:53:34 -07:00

159 lines
4.1 KiB
TypeScript

/**
* 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 {
ErrorCategory,
getRuleForCategory,
} from 'babel-plugin-react-compiler/src/CompilerError';
import {
normalizeIndent,
testRule,
makeTestCaseError,
TestRecommendedRules,
} from './shared-utils';
import {allRules} from '../src/rules/ReactCompilerRule';
testRule('plugin-recommended', TestRecommendedRules, {
valid: [
{
name: 'Basic example with component syntax',
code: normalizeIndent`
export default component HelloWorld(
text: string = 'Hello!',
onClick: () => void,
) {
return <div onClick={onClick}>{text}</div>;
}
`,
},
{
// 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: [
{
// TODO: actually return multiple diagnostics in this case
name: 'Multiple diagnostic kinds from the same function are surfaced',
code: normalizeIndent`
import Child from './Child';
function Component() {
const result = cond ?? useConditionalHook();
return <>
{Child(result)}
</>;
}
`,
errors: [
makeTestCaseError('Hooks must always be called in a consistent order'),
],
},
{
name: 'Multiple diagnostics within the same file are surfaced',
code: normalizeIndent`
function useConditional1() {
'use memo';
return cond ?? useConditionalHook();
}
function useConditional2(props) {
'use memo';
return props.cond && useConditionalHook();
}`,
errors: [
makeTestCaseError('Hooks must always be called in a consistent order'),
makeTestCaseError('Hooks must always be called in a consistent order'),
],
},
{
name: "'use no forget' does not disable eslint rule",
code: normalizeIndent`
let count = 0;
function Component() {
'use no forget';
return cond ?? useConditionalHook();
}
`,
errors: [
makeTestCaseError('Hooks must always be called in a consistent order'),
],
},
{
name: 'Multiple non-fatal useMemo diagnostics are surfaced',
code: normalizeIndent`
import {useMemo, useState} from 'react';
function Component({item, cond}) {
const [prevItem, setPrevItem] = useState(item);
const [state, setState] = useState(0);
useMemo(() => {
if (cond) {
setPrevItem(item);
setState(0);
}
}, [cond, item, init]);
return <Child x={state} />;
}`,
errors: [makeTestCaseError('useMemo() callbacks must return a value')],
},
{
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 <div>Hello world</div>;
}
`,
options: [
{
environment: {
inferEffectDependencies: [
{
function: {
source: 'useMyEffect',
importSpecifierName: 'default',
},
autodepsIndex: 1,
},
],
},
},
],
errors: [
{
message: /Cannot infer dependencies of this effect/,
},
],
},
],
});