Skip to content

Conversation

@josephsavona
Copy link
Member

@josephsavona josephsavona commented Aug 12, 2025

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.

@meta-cla meta-cla bot added the CLA Signed label Aug 12, 2025
@github-actions github-actions bot added the React Core Team Opened by a member of the React Core Team label Aug 12, 2025
@react-sizebot
Copy link

react-sizebot commented Aug 12, 2025

Comparing: d73b6f1...bc33b62

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.js = 6.68 kB 6.68 kB = 1.83 kB 1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js = 530.18 kB 530.18 kB = 93.39 kB 93.39 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.69 kB 6.69 kB = 1.83 kB 1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js = 655.82 kB 655.81 kB = 115.31 kB 115.30 kB
facebook-www/ReactDOM-prod.classic.js = 675.59 kB 675.58 kB = 118.54 kB 118.54 kB
facebook-www/ReactDOM-prod.modern.js = 666.02 kB 666.00 kB = 116.87 kB 116.86 kB
oss-experimental/react-debug-tools/cjs/react-debug-tools.development.js = 33.29 kB 32.49 kB = 5.92 kB 5.85 kB
oss-stable-semver/react-debug-tools/cjs/react-debug-tools.development.js = 33.29 kB 32.49 kB = 5.92 kB 5.85 kB
oss-stable/react-debug-tools/cjs/react-debug-tools.development.js = 33.29 kB 32.49 kB = 5.92 kB 5.85 kB
oss-experimental/react-debug-tools/cjs/react-debug-tools.production.js = 29.70 kB 28.95 kB = 5.80 kB 5.73 kB
oss-stable-semver/react-debug-tools/cjs/react-debug-tools.production.js = 29.70 kB 28.95 kB = 5.80 kB 5.73 kB
oss-stable/react-debug-tools/cjs/react-debug-tools.production.js = 29.70 kB 28.95 kB = 5.80 kB 5.73 kB
react-native/shims/ReactNativeViewConfigRegistry.js = 3.69 kB 3.55 kB = 1.32 kB 1.23 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable-semver/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.production.js +0.92% 2,025.39 kB 2,044.07 kB +0.80% 294.03 kB 296.40 kB
oss-stable/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.production.js +0.92% 2,025.39 kB 2,044.07 kB +0.80% 294.03 kB 296.40 kB
oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.production.js +0.92% 2,025.58 kB 2,044.25 kB +0.80% 294.06 kB 296.42 kB
oss-stable-semver/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js +0.92% 2,029.87 kB 2,048.54 kB +0.80% 295.03 kB 297.40 kB
oss-stable/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js +0.92% 2,029.87 kB 2,048.54 kB +0.80% 295.03 kB 297.40 kB
oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js +0.92% 2,030.05 kB 2,048.72 kB +0.80% 295.05 kB 297.43 kB
oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-client.node.production.js = 65.47 kB 65.32 kB = 12.83 kB 12.79 kB
oss-stable-semver/react-server-dom-esm/cjs/react-server-dom-esm-client.node.production.js = 64.91 kB 64.77 kB = 12.72 kB 12.68 kB
oss-stable/react-server-dom-esm/cjs/react-server-dom-esm-client.node.production.js = 64.91 kB 64.77 kB = 12.72 kB 12.68 kB
oss-experimental/react-server-dom-parcel/cjs/react-server-dom-parcel-client.node.production.js = 66.16 kB 65.99 kB = 13.03 kB 13.02 kB
oss-stable-semver/react-server-dom-parcel/cjs/react-server-dom-parcel-client.node.production.js = 65.60 kB 65.43 kB = 12.92 kB 12.91 kB
oss-stable/react-server-dom-parcel/cjs/react-server-dom-parcel-client.node.production.js = 65.60 kB 65.43 kB = 12.92 kB 12.91 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.production.js = 69.47 kB 69.25 kB = 13.71 kB 13.67 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.node.production.js = 69.35 kB 69.13 kB = 13.68 kB 13.64 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.production.js = 68.91 kB 68.70 kB = 13.59 kB 13.56 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.production.js = 68.91 kB 68.70 kB = 13.59 kB 13.56 kB
oss-stable-semver/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.node.production.js = 68.79 kB 68.58 kB = 13.57 kB 13.54 kB
oss-stable/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.node.production.js = 68.79 kB 68.58 kB = 13.57 kB 13.54 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.unbundled.production.js = 68.29 kB 68.08 kB = 13.47 kB 13.42 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.unbundled.production.js = 67.73 kB 67.52 kB = 13.35 kB 13.32 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.unbundled.production.js = 67.73 kB 67.52 kB = 13.35 kB 13.32 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-client.edge.development.js = 168.79 kB 168.20 kB = 30.22 kB 30.14 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.edge.development.js = 168.66 kB 168.07 kB = 30.19 kB 30.11 kB
oss-experimental/react-server-dom-parcel/cjs/react-server-dom-parcel-client.edge.development.js = 165.51 kB 164.92 kB = 29.69 kB 29.61 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-client.edge.development.js = 126.87 kB 126.28 kB = 23.47 kB 23.39 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-client.edge.development.js = 126.87 kB 126.28 kB = 23.47 kB 23.39 kB
oss-stable-semver/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.edge.development.js = 126.74 kB 126.15 kB = 23.43 kB 23.36 kB
oss-stable/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.edge.development.js = 126.74 kB 126.15 kB = 23.43 kB 23.36 kB
oss-stable-semver/react-server-dom-parcel/cjs/react-server-dom-parcel-client.edge.development.js = 123.60 kB 123.01 kB = 22.92 kB 22.85 kB
oss-stable/react-server-dom-parcel/cjs/react-server-dom-parcel-client.edge.development.js = 123.60 kB 123.01 kB = 22.92 kB 22.85 kB
oss-experimental/react-server-dom-parcel/cjs/react-server-dom-parcel-client.node.development.js = 171.18 kB 170.25 kB = 30.38 kB 30.31 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.development.js = 174.40 kB 173.38 kB = 30.92 kB 30.87 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.node.development.js = 174.27 kB 173.25 kB = 30.89 kB 30.83 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.unbundled.development.js = 172.92 kB 171.91 kB = 30.65 kB 30.58 kB
oss-stable-semver/react-server-dom-parcel/cjs/react-server-dom-parcel-client.node.development.js = 129.30 kB 128.37 kB = 23.67 kB 23.59 kB
oss-stable/react-server-dom-parcel/cjs/react-server-dom-parcel-client.node.development.js = 129.30 kB 128.37 kB = 23.67 kB 23.59 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.development.js = 132.52 kB 131.50 kB = 24.25 kB 24.16 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.development.js = 132.52 kB 131.50 kB = 24.25 kB 24.16 kB
oss-stable-semver/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.node.development.js = 132.39 kB 131.38 kB = 24.22 kB 24.13 kB
oss-stable/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.node.development.js = 132.39 kB 131.38 kB = 24.22 kB 24.13 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.unbundled.development.js = 131.04 kB 130.03 kB = 23.97 kB 23.88 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.unbundled.development.js = 131.04 kB 130.03 kB = 23.97 kB 23.88 kB
oss-experimental/react-debug-tools/cjs/react-debug-tools.development.js = 33.29 kB 32.49 kB = 5.92 kB 5.85 kB
oss-stable-semver/react-debug-tools/cjs/react-debug-tools.development.js = 33.29 kB 32.49 kB = 5.92 kB 5.85 kB
oss-stable/react-debug-tools/cjs/react-debug-tools.development.js = 33.29 kB 32.49 kB = 5.92 kB 5.85 kB
oss-experimental/react-debug-tools/cjs/react-debug-tools.production.js = 29.70 kB 28.95 kB = 5.80 kB 5.73 kB
oss-stable-semver/react-debug-tools/cjs/react-debug-tools.production.js = 29.70 kB 28.95 kB = 5.80 kB 5.73 kB
oss-stable/react-debug-tools/cjs/react-debug-tools.production.js = 29.70 kB 28.95 kB = 5.80 kB 5.73 kB
react-native/shims/ReactNativeViewConfigRegistry.js = 3.69 kB 3.55 kB = 1.32 kB 1.23 kB

Generated by 🚫 dangerJS against bc33b62

@josephsavona josephsavona changed the title [compiler] Simplify mapping of errors to lints [compiler] Aggregate error reporting, separate eslint rules Aug 12, 2025
@josephsavona josephsavona force-pushed the pr34176 branch 11 times, most recently from fbaad2c to 3e5400f Compare August 19, 2025 22:54

class OkImpl<T> implements Result<T, never> {
constructor(private val: T) {}
#val: T;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the only place we use this syntax and rollup didn't like it, and it was easier to switch to private identifiers than deal with more babel plugins

Comment on lines +24 to +26
...Object.fromEntries(
Object.keys(recommendedRules).map(name => ['react-hooks/' + name, 'error']),
),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but by default only the recommended rules are activated. and this is all driven dynamically based on the categories

Comment on lines +836 to +849
default: {
assertExhaustive(category, `Unsupported category ${category}`);
}
}
}

export const LintRules: Array<LintRule> = Object.keys(ErrorCategory).map(
category => getRuleForCategory(category as any),
);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getRuleForCategory() function with its exhaustive switch, and the dynamic iteration over ErrorCategory, mean we statically enforce that we have an ESLint rule for every category. LintRules is what powers the set of rules in the plugin.

Copy link
Member

@poteto poteto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming the cache does work and can be shared across our multiple rules, lgtm! thanks so much for this work @josephsavona and @mofeiZ!

entry.sourceCode === sourceCode.text &&
isDeepStrictEqual(entry.userOpts, userOpts)
) {
return entry;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just curious, can we include a test plan in the PR showing that we do get cache hits?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I verified locally that we see one cache miss followed by lots of cache hits, so it looks like this is working

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.
@josephsavona
Copy link
Member Author

In case this gets lost on the next Sapling push:

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.

@josephsavona josephsavona merged commit 7d29ecb into main Aug 21, 2025
256 of 284 checks passed
github-actions bot pushed a commit that referenced this pull request Aug 21, 2025
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>

DiffTrain build for [7d29ecb](7d29ecb)
github-actions bot pushed a commit that referenced this pull request Aug 21, 2025
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>

DiffTrain build for [7d29ecb](7d29ecb)
poteto added a commit that referenced this pull request Sep 5, 2025
With #34176 we now have granular lint rules created for each compiler ErrorCategory. However, we had remnants of our old error severities still in use which makes reporting errors quite clunky. Previously you would need to specify both a category and severity which often ended up being the same.

This PR moves severity definition into our rules which are generated from our categories. For now I decided to defer "upgrading" categories from a simple string to a sum type since we are only using severities to map errors to eslint severity.
poteto added a commit that referenced this pull request Sep 5, 2025
With #34176 we now have granular lint rules created for each compiler ErrorCategory. However, we had remnants of our old error severities still in use which makes reporting errors quite clunky. Previously you would need to specify both a category and severity which often ended up being the same.

This PR moves severity definition into our rules which are generated from our categories. For now I decided to defer "upgrading" categories from a simple string to a sum type since we are only using severities to map errors to eslint severity.
poteto added a commit that referenced this pull request Sep 6, 2025
With #34176 we now have granular lint rules created for each compiler
ErrorCategory. However, we had remnants of our old error severities
still in use which makes reporting errors quite clunky. Previously you
would need to specify both a category and severity which often ended up
being the same.

This PR moves severity definition into our rules which are generated
from our categories. For now I decided to defer "upgrading" categories
from a simple string to a sum type since we are only using severities to
map errors to eslint severity.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34401).
* #34409
* #34404
* #34403
* #34402
* __->__ #34401
github-actions bot pushed a commit that referenced this pull request Sep 6, 2025
With #34176 we now have granular lint rules created for each compiler
ErrorCategory. However, we had remnants of our old error severities
still in use which makes reporting errors quite clunky. Previously you
would need to specify both a category and severity which often ended up
being the same.

This PR moves severity definition into our rules which are generated
from our categories. For now I decided to defer "upgrading" categories
from a simple string to a sum type since we are only using severities to
map errors to eslint severity.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34401).
* #34409
* #34404
* #34403
* #34402
* __->__ #34401

DiffTrain build for [60d9b97](60d9b97)
github-actions bot pushed a commit that referenced this pull request Sep 6, 2025
With #34176 we now have granular lint rules created for each compiler
ErrorCategory. However, we had remnants of our old error severities
still in use which makes reporting errors quite clunky. Previously you
would need to specify both a category and severity which often ended up
being the same.

This PR moves severity definition into our rules which are generated
from our categories. For now I decided to defer "upgrading" categories
from a simple string to a sum type since we are only using severities to
map errors to eslint severity.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34401).
* #34409
* #34404
* #34403
* #34402
* __->__ #34401

DiffTrain build for [60d9b97](60d9b97)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed React Core Team Opened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants