From 425ba0ad6d3ebd1779f6a658dcbf5c666d054948 Mon Sep 17 00:00:00 2001 From: Joseph Savona <6425824+josephsavona@users.noreply.github.com> Date: Fri, 22 Aug 2025 09:59:28 -0700 Subject: [PATCH] [compiler] Script to produce markdown of lint rule docs (#34260) The docs site is in a separate repo, but this gives us a semi-automated way to update the docs about our lint rules. The script generates markdown files from the rule definitions which we can then manually copy/paste into the docs site somewhere. In the future we can automate this fully. --- compiler/package.json | 3 +- .../src/CompilerError.ts | 49 +++++++++++++------ compiler/scripts/build-eslint-docs.js | 32 ++++++++++++ 3 files changed, 68 insertions(+), 16 deletions(-) create mode 100644 compiler/scripts/build-eslint-docs.js diff --git a/compiler/package.json b/compiler/package.json index e3c3ee8c7f..4492b70210 100644 --- a/compiler/package.json +++ b/compiler/package.json @@ -19,7 +19,8 @@ "test": "yarn workspaces run test", "snap": "yarn workspace babel-plugin-react-compiler run snap", "snap:build": "yarn workspace snap run build", - "npm:publish": "node scripts/release/publish" + "npm:publish": "node scripts/release/publish", + "eslint-docs": "yarn workspace babel-plugin-react-compiler build && node scripts/build-eslint-docs.js" }, "dependencies": { "fs-extra": "^4.0.2", diff --git a/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts b/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts index 5940dbb4fd..964217c399 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts @@ -7,9 +7,10 @@ import * as t from '@babel/types'; import {codeFrameColumns} from '@babel/code-frame'; -import type {SourceLocation} from './HIR'; +import {type SourceLocation} from './HIR'; import {Err, Ok, Result} from './Utils/Result'; import {assertExhaustive} from './Utils/utils'; +import invariant from 'invariant'; export enum ErrorSeverity { /** @@ -628,7 +629,18 @@ export type LintRule = { recommended: boolean; }; +const RULE_NAME_PATTERN = /^[a-z]+(-[a-z]+)*$/; + export function getRuleForCategory(category: ErrorCategory): LintRule { + const rule = getRuleForCategoryImpl(category); + invariant( + RULE_NAME_PATTERN.test(rule.name), + `Invalid rule name, got '${rule.name}' but rules must match ${RULE_NAME_PATTERN.toString()}`, + ); + return rule; +} + +function getRuleForCategoryImpl(category: ErrorCategory): LintRule { switch (category) { case ErrorCategory.AutomaticEffectDependencies: { return { @@ -636,7 +648,7 @@ export function getRuleForCategory(category: ErrorCategory): LintRule { name: 'automatic-effect-dependencies', description: 'Verifies that automatic effect dependencies are compiled if opted-in', - recommended: true, + recommended: false, }; } case ErrorCategory.CapitalizedCalls: { @@ -652,7 +664,7 @@ export function getRuleForCategory(category: ErrorCategory): LintRule { return { category, name: 'config', - description: 'Validates the configuration', + description: 'Validates the compiler configuration options', recommended: true, }; } @@ -678,7 +690,7 @@ export function getRuleForCategory(category: ErrorCategory): LintRule { category, name: 'set-state-in-effect', description: - 'Validates against calling setState synchronously in an effect', + 'Validates against calling setState synchronously in an effect, which can lead to re-renders that degrade performance', recommended: true, }; } @@ -687,7 +699,7 @@ export function getRuleForCategory(category: ErrorCategory): LintRule { category, name: 'error-boundaries', description: - 'Validates usage of error boundaries instead of try/catch for errors in JSX', + 'Validates usage of error boundaries instead of try/catch for errors in child components', recommended: true, }; } @@ -711,7 +723,8 @@ export function getRuleForCategory(category: ErrorCategory): LintRule { return { category, name: 'gating', - description: 'Validates configuration of gating mode', + description: + 'Validates configuration of [gating mode](https://react.dev/reference/react-compiler/gating)', recommended: true, }; } @@ -720,7 +733,8 @@ export function getRuleForCategory(category: ErrorCategory): LintRule { category, name: 'globals', description: - 'Validates against assignment/mutation of globals during render', + 'Validates against assignment/mutation of globals during render, part of ensuring that ' + + '[side effects must render outside of render](https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)', recommended: true, }; } @@ -742,7 +756,7 @@ export function getRuleForCategory(category: ErrorCategory): LintRule { category, name: 'immutability', description: - 'Validates that immutable values (props, state, etc) are not mutated', + 'Validates against mutating props, state, and other values that [are immutable](https://react.dev/reference/rules/components-and-hooks-must-be-pure#props-and-state-are-immutable)', recommended: true, }; } @@ -759,7 +773,9 @@ export function getRuleForCategory(category: ErrorCategory): LintRule { category, name: 'preserve-manual-memoization', description: - 'Validates that existing manual memoized is preserved by the compiler', + 'Validates that existing manual memoized is preserved by the compiler. ' + + 'React Compiler will only compile components and hooks if its inference ' + + '[matches or exceeds the existing manual memoization](https://react.dev/learn/react-compiler/introduction#what-should-i-do-about-usememo-usecallback-and-reactmemo)', recommended: true, }; } @@ -768,7 +784,7 @@ export function getRuleForCategory(category: ErrorCategory): LintRule { category, name: 'purity', description: - 'Validates that the component/hook is pure, and does not call known-impure functions', + 'Validates that [components/hooks are pure](https://react.dev/reference/rules/components-and-hooks-must-be-pure) by checking that they do not call known-impure functions', recommended: true, }; } @@ -777,7 +793,7 @@ export function getRuleForCategory(category: ErrorCategory): LintRule { category, name: 'refs', description: - 'Validates correct usage of refs, not reading/writing during render', + 'Validates correct usage of refs, not reading/writing during render. See the "pitfalls" section in [`useRef()` usage](https://react.dev/reference/react/useRef#usage)', recommended: true, }; } @@ -785,7 +801,8 @@ export function getRuleForCategory(category: ErrorCategory): LintRule { return { category, name: 'set-state-in-render', - description: 'Validates against setting state during render', + description: + 'Validates against setting state during render, which can trigger additional renders and potential infinite render loops', recommended: true, }; } @@ -794,7 +811,7 @@ export function getRuleForCategory(category: ErrorCategory): LintRule { category, name: 'static-components', description: - 'Validates that components are static, not recreated every render', + 'Validates that components are static, not recreated every render. Components that are recreated dynamically can reset state and trigger excessive re-rendering', recommended: true, }; } @@ -826,7 +843,8 @@ export function getRuleForCategory(category: ErrorCategory): LintRule { return { category, name: 'unsupported-syntax', - description: 'Validates against syntax that we do not plan to support', + description: + 'Validates against syntax that we do not plan to support in React Compiler', recommended: true, }; } @@ -834,7 +852,8 @@ export function getRuleForCategory(category: ErrorCategory): LintRule { return { category, name: 'use-memo', - description: 'Validates usage of the useMemo() hook', + description: + 'Validates usage of the useMemo() hook against common mistakes. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.', recommended: true, }; } diff --git a/compiler/scripts/build-eslint-docs.js b/compiler/scripts/build-eslint-docs.js new file mode 100644 index 0000000000..d8d59096d9 --- /dev/null +++ b/compiler/scripts/build-eslint-docs.js @@ -0,0 +1,32 @@ +const ReactCompiler = require('../packages/babel-plugin-react-compiler/dist'); + +const combinedRules = [ + { + name: 'rules-of-hooks', + recommended: true, + description: + 'Validates that components and hooks follow the [Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks)', + }, + { + name: 'exhaustive-deps', + recommended: true, + description: + 'Validates that hooks which accept dependency arrays (`useMemo()`, `useCallback()`, `useEffect()`, etc) ' + + 'list all referenced variables in their dependency array. Referencing a value without including it in the ' + + 'dependency array can lead to stale UI or callbacks.', + }, + ...ReactCompiler.LintRules, +]; + +const printed = combinedRules + .filter(rule => rule.recommended) + .map(rule => { + return ` +## \`react-hooks/${rule.name}\` + +${rule.description} + `.trim(); + }) + .join('\n\n'); + +console.log(printed);