mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
perf(react-compiler): skip passes lint does not need
Original PR: #26657 (React Compiler part)
This commit is contained in:
1 parent
61fe2af7f2
commit
fd68928a88
22 files changed
+349
-31
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"categories": { "correctness": "off" },
|
||||||
|
"plugins": ["react"],
|
||||||
|
"rules": { "react/globals": "error" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
let someGlobal = false;
|
||||||
|
|
||||||
|
// Not a component or hook, so the compiler does not lint it.
|
||||||
|
export function helper() {
|
||||||
|
someGlobal = true;
|
||||||
|
return someGlobal;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
let someGlobal = false;
|
||||||
|
|
||||||
|
// An anonymous component wrapped in `React.memo` is linted.
|
||||||
|
export const Component = React.memo(function (props) {
|
||||||
|
someGlobal = true;
|
||||||
|
return <div>{props.text}</div>;
|
||||||
|
});
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { createElement, useState } from "react";
|
||||||
|
|
||||||
|
let someGlobal = false;
|
||||||
|
|
||||||
|
// A PascalCase component that calls hooks is linted in a non-JSX file too.
|
||||||
|
export function Provider(props) {
|
||||||
|
const [value] = useState(0);
|
||||||
|
someGlobal = true;
|
||||||
|
return createElement("div", null, value, props.children);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
let someGlobal = false;
|
||||||
|
|
||||||
|
// A hook is linted even without JSX.
|
||||||
|
export function useFoo() {
|
||||||
|
someGlobal = true;
|
||||||
|
return useState(0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Exit code
|
||||||
|
1
|
||||||
|
|
||||||
|
# stdout
|
||||||
|
```
|
||||||
|
x react(globals): Cannot reassign variables declared outside of the component/hook
|
||||||
|
,-[files/memo.jsx:5:3]
|
||||||
|
4 | export const Component = React.memo(function (props) {
|
||||||
|
5 | someGlobal = true;
|
||||||
|
: ^^^^^|^^^^
|
||||||
|
: `-- `someGlobal` cannot be reassigned
|
||||||
|
6 | return <div>{props.text}</div>;
|
||||||
|
`----
|
||||||
|
help: Variable `someGlobal` is declared outside of the component/hook. Reassigning this value during render is a side effect which can cause unpredictable behavior. If this variable is used in
|
||||||
|
rendering, use useState instead. Otherwise, update it in an effect
|
||||||
|
note: React Compiler skipped optimizing this component or hook. Additional guidance: https://react.dev/reference/eslint-plugin-react-hooks/lints/globals
|
||||||
|
|
||||||
|
x react(globals): Cannot reassign variables declared outside of the component/hook
|
||||||
|
,-[files/provider.ts:8:3]
|
||||||
|
7 | const [value] = useState(0);
|
||||||
|
8 | someGlobal = true;
|
||||||
|
: ^^^^^|^^^^
|
||||||
|
: `-- `someGlobal` cannot be reassigned
|
||||||
|
9 | return createElement("div", null, value, props.children);
|
||||||
|
`----
|
||||||
|
help: Variable `someGlobal` is declared outside of the component/hook. Reassigning this value during render is a side effect which can cause unpredictable behavior. If this variable is used in
|
||||||
|
rendering, use useState instead. Otherwise, update it in an effect
|
||||||
|
note: React Compiler skipped optimizing this component or hook. Additional guidance: https://react.dev/reference/eslint-plugin-react-hooks/lints/globals
|
||||||
|
|
||||||
|
x react(globals): Cannot reassign variables declared outside of the component/hook
|
||||||
|
,-[files/use-foo.js:7:3]
|
||||||
|
6 | export function useFoo() {
|
||||||
|
7 | someGlobal = true;
|
||||||
|
: ^^^^^|^^^^
|
||||||
|
: `-- `someGlobal` cannot be reassigned
|
||||||
|
8 | return useState(0);
|
||||||
|
`----
|
||||||
|
help: Variable `someGlobal` is declared outside of the component/hook. Reassigning this value during render is a side effect which can cause unpredictable behavior. If this variable is used in
|
||||||
|
rendering, use useState instead. Otherwise, update it in an effect
|
||||||
|
note: React Compiler skipped optimizing this component or hook. Additional guidance: https://react.dev/reference/eslint-plugin-react-hooks/lints/globals
|
||||||
|
|
||||||
|
Found 0 warnings and 3 errors.
|
||||||
|
Finished in Xms on 4 files with 1 rules using X threads.
|
||||||
|
```
|
||||||
|
|
||||||
|
# stderr
|
||||||
|
```
|
||||||
|
```
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"categories": { "correctness": "off" },
|
||||||
|
"plugins": ["react"],
|
||||||
|
"rules": { "react/todo": "error" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export function Foo() {
|
||||||
|
function foo() {
|
||||||
|
return bar();
|
||||||
|
}
|
||||||
|
function bar() {
|
||||||
|
return 42;
|
||||||
|
}
|
||||||
|
return <div fn1={foo} fn2={bar} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Exit code
|
||||||
|
1
|
||||||
|
|
||||||
|
# stdout
|
||||||
|
```
|
||||||
|
x react(todo): [PruneHoistedContexts] Rewrite hoisted function references
|
||||||
|
,-[files/index.jsx:3:12]
|
||||||
|
2 | function foo() {
|
||||||
|
3 | return bar();
|
||||||
|
: ^^^
|
||||||
|
4 | }
|
||||||
|
`----
|
||||||
|
help: Rewrite the highlighted code using syntax supported by React Compiler
|
||||||
|
note: React Compiler skipped optimizing this component or hook. Additional guidance: https://react.dev/reference/eslint-plugin-react-hooks/lints/unsupported-syntax
|
||||||
|
|
||||||
|
Found 0 warnings and 1 error.
|
||||||
|
Finished in Xms on 1 file with 1 rules using X threads.
|
||||||
|
```
|
||||||
|
|
||||||
|
# stderr
|
||||||
|
```
|
||||||
|
```
|
||||||
@@ -347,14 +347,18 @@ impl Linter {
|
|||||||
let mut timing_recorder = TIMINGS.then(|| RuleTimingRecorder::with_capacity(rules.len()));
|
let mut timing_recorder = TIMINGS.then(|| RuleTimingRecorder::with_capacity(rules.len()));
|
||||||
|
|
||||||
// The shared React Compiler run takes its environment additions from the
|
// The shared React Compiler run takes its environment additions from the
|
||||||
// `react/capitalized-calls` rule's resolved options.
|
// `react/capitalized-calls` rule's resolved options, and runs the passes
|
||||||
let react_compiler_env_options = rules
|
// that only report `Todo` and `Invariant` when their rules are enabled.
|
||||||
|
let mut react_compiler_env_options = rules
|
||||||
.iter()
|
.iter()
|
||||||
.find_map(|(rule, _)| match rule {
|
.find_map(|(rule, _)| match rule {
|
||||||
RuleEnum::ReactCapitalizedCalls(rule) => Some(rule.react_compiler_env_options()),
|
RuleEnum::ReactCapitalizedCalls(rule) => Some(rule.react_compiler_env_options()),
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
react_compiler_env_options.report_todo_and_invariant = rules
|
||||||
|
.iter()
|
||||||
|
.any(|(rule, _)| matches!(rule, RuleEnum::ReactTodo(_) | RuleEnum::ReactInvariant(_)));
|
||||||
|
|
||||||
let mut ctx_host = Rc::new(
|
let mut ctx_host = Rc::new(
|
||||||
ContextHost::new(path, context_sub_hosts, allocator, self.options, config)
|
ContextHost::new(path, context_sub_hosts, allocator, self.options, config)
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ impl CapitalizedCalls {
|
|||||||
ReactCompilerEnvOptions {
|
ReactCompilerEnvOptions {
|
||||||
capitalized_calls_allow: self.0.allow.clone(),
|
capitalized_calls_allow: self.0.allow.clone(),
|
||||||
capitalized_calls_allow_pattern: self.0.allow_pattern.clone(),
|
capitalized_calls_allow_pattern: self.0.allow_pattern.clone(),
|
||||||
|
..ReactCompilerEnvOptions::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,15 +14,18 @@ use crate::{
|
|||||||
|
|
||||||
/// Environment additions to the shared React Compiler run that come from rule
|
/// Environment additions to the shared React Compiler run that come from rule
|
||||||
/// configuration, extracted once per resolved config in `Linter::run` (from the
|
/// configuration, extracted once per resolved config in `Linter::run` (from the
|
||||||
/// `react/capitalized-calls` rule's options). Config-derived rather than
|
/// `react/capitalized-calls` rule's options and whether `react/todo` or
|
||||||
/// per-run rule state, so the single shared run stays valid for any
|
/// `react/invariant` is enabled). Config-derived rather than per-run rule state,
|
||||||
/// combination of enabled rules.
|
/// so the single shared run stays valid for any combination of enabled rules.
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct ReactCompilerEnvOptions {
|
pub struct ReactCompilerEnvOptions {
|
||||||
/// Extra allowed names for `validateNoCapitalizedCalls`.
|
/// Extra allowed names for `validateNoCapitalizedCalls`.
|
||||||
pub capitalized_calls_allow: Vec<String>,
|
pub capitalized_calls_allow: Vec<String>,
|
||||||
/// Allowed-name pattern for `validateNoCapitalizedCalls`.
|
/// Allowed-name pattern for `validateNoCapitalizedCalls`.
|
||||||
pub capitalized_calls_allow_pattern: Option<Regex>,
|
pub capitalized_calls_allow_pattern: Option<Regex>,
|
||||||
|
/// Whether `react/todo` or `react/invariant` is enabled. Otherwise lint skips
|
||||||
|
/// the compiler passes that only report those categories.
|
||||||
|
pub report_todo_and_invariant: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The compiler options the React Compiler family of rules lints with — `lint`
|
/// The compiler options the React Compiler family of rules lints with — `lint`
|
||||||
@@ -68,6 +71,7 @@ fn react_compiler_plugin_options(env_options: &ReactCompilerEnvOptions) -> Plugi
|
|||||||
validate_exhaustive_effect_dependencies: ExhaustiveEffectDepsMode::All,
|
validate_exhaustive_effect_dependencies: ExhaustiveEffectDepsMode::All,
|
||||||
..EnvironmentConfig::default()
|
..EnvironmentConfig::default()
|
||||||
},
|
},
|
||||||
|
lint_skip_reactive_scope_passes: !env_options.report_todo_and_invariant,
|
||||||
..PluginOptions::default()
|
..PluginOptions::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
// @validateExhaustiveEffectDependencies:"all" @outputMode:"lint" @panicThreshold:"none" @lintSkipReactiveScopePasses
|
||||||
|
import {useEffect, useRef, useState} from 'react';
|
||||||
|
|
||||||
|
// Without useMemo/useCallback, lint skips the reactive-scope passes but still
|
||||||
|
// infers reactive places for the effect: props are required dependencies,
|
||||||
|
// while the state setter and the ref are stable and may be omitted.
|
||||||
|
function Component({value, other}) {
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
const ref = useRef(null);
|
||||||
|
useEffect(() => {
|
||||||
|
setCount(value);
|
||||||
|
ref.current = other;
|
||||||
|
}, []);
|
||||||
|
return <div>{count}</div>;
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
// @validateExhaustiveEffectDependencies:"all" @outputMode:"lint" @panicThreshold:"none" @lintSkipReactiveScopePasses
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
// The effect hook is only recognisable by its type: the callee is a local
|
||||||
|
// destructured from the React namespace, not a `useEffect` load or property.
|
||||||
|
function Component({value}) {
|
||||||
|
const {useEffect} = React;
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(value);
|
||||||
|
}, []);
|
||||||
|
return <div>{value}</div>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// @outputMode:"lint" @panicThreshold:"none"
|
||||||
|
import {Stringify} from 'shared-runtime';
|
||||||
|
|
||||||
|
// Lint reports the Todo that a pass after validation records for the hoisted
|
||||||
|
// function reference.
|
||||||
|
function Foo() {
|
||||||
|
function foo() {
|
||||||
|
return bar();
|
||||||
|
}
|
||||||
|
function bar() {
|
||||||
|
return 42;
|
||||||
|
}
|
||||||
|
return <Stringify fn1={foo} fn2={bar} />;
|
||||||
|
}
|
||||||
@@ -154,6 +154,11 @@ pub struct PluginOptions {
|
|||||||
/// on top of the built-in `"use no memo"` / `"use no forget"`.
|
/// on top of the built-in `"use no memo"` / `"use no forget"`.
|
||||||
pub custom_opt_out_directives: Option<Vec<String>>,
|
pub custom_opt_out_directives: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// In lint output mode, skip the reactive-scope passes for functions without
|
||||||
|
/// `useMemo`/`useCallback`. Those passes only report `Todo` and `Invariant`
|
||||||
|
/// diagnostics, so set this only when those diagnostics are not needed.
|
||||||
|
pub lint_skip_reactive_scope_passes: bool,
|
||||||
|
|
||||||
/// Compiler environment: feature flags, validation toggles, and custom hook/type
|
/// Compiler environment: feature flags, validation toggles, and custom hook/type
|
||||||
/// definitions for the compilation passes. See [`EnvironmentConfig`].
|
/// definitions for the compilation passes. See [`EnvironmentConfig`].
|
||||||
pub environment: EnvironmentConfig,
|
pub environment: EnvironmentConfig,
|
||||||
@@ -175,6 +180,7 @@ impl Default for PluginOptions {
|
|||||||
flow_suppressions: true,
|
flow_suppressions: true,
|
||||||
ignore_use_no_forget: false,
|
ignore_use_no_forget: false,
|
||||||
custom_opt_out_directives: None,
|
custom_opt_out_directives: None,
|
||||||
|
lint_skip_reactive_scope_passes: false,
|
||||||
environment: EnvironmentConfig::default(),
|
environment: EnvironmentConfig::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ use crate::react_compiler_hir::environment::Environment;
|
|||||||
use crate::react_compiler_hir::environment::OutputMode;
|
use crate::react_compiler_hir::environment::OutputMode;
|
||||||
use crate::react_compiler_hir::environment_config::{EnvironmentConfig, ExhaustiveEffectDepsMode};
|
use crate::react_compiler_hir::environment_config::{EnvironmentConfig, ExhaustiveEffectDepsMode};
|
||||||
use crate::react_compiler_hir::{
|
use crate::react_compiler_hir::{
|
||||||
ReactFunctionType, assert_consistent_identifiers, assert_terminal_preds_exist,
|
HirFunction, InstructionValue, ReactFunctionType, assert_consistent_identifiers,
|
||||||
assert_terminal_successors_exist, assert_valid_block_nesting,
|
assert_terminal_preds_exist, assert_terminal_successors_exist, assert_valid_block_nesting,
|
||||||
|
is_use_effect_hook_type, is_use_insertion_effect_hook_type, is_use_layout_effect_hook_type,
|
||||||
};
|
};
|
||||||
use crate::react_compiler_inference::align_method_call_scopes;
|
use crate::react_compiler_inference::align_method_call_scopes;
|
||||||
use crate::react_compiler_inference::align_object_method_scopes;
|
use crate::react_compiler_inference::align_object_method_scopes;
|
||||||
@@ -231,6 +232,31 @@ pub fn compile_fn<'a, const EMIT: bool>(
|
|||||||
validate_no_freezing_known_mutable_functions(&hir, &mut env);
|
validate_no_freezing_known_mutable_functions(&hir, &mut env);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lint does not emit a rewrite. Without useMemo/useCallback there is no
|
||||||
|
// manual memoization to validate, so callers that do not need `Todo` and
|
||||||
|
// `Invariant` diagnostics can skip the reactive-scope passes below, which
|
||||||
|
// only report those. Reactive places are still inferred when an effect hook
|
||||||
|
// call needs its dependencies checked.
|
||||||
|
if !EMIT
|
||||||
|
&& env.output_mode == OutputMode::Lint
|
||||||
|
&& context.opts.lint_skip_reactive_scope_passes
|
||||||
|
&& !hir_has_start_memoize(&hir)
|
||||||
|
{
|
||||||
|
if env.enable_validations() {
|
||||||
|
if env.config.validate_static_components {
|
||||||
|
let errors = validate_static_components(&hir, &env.functions);
|
||||||
|
log_errors_as_events(&errors, context);
|
||||||
|
}
|
||||||
|
if env.config.validate_exhaustive_effect_dependencies != ExhaustiveEffectDepsMode::Off
|
||||||
|
&& calls_effect_hook(&hir, &env)
|
||||||
|
{
|
||||||
|
infer_reactive_places(&mut hir, &mut env)?;
|
||||||
|
validate_exhaustive_dependencies(&mut hir, &mut env)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return finish_lint_fn(&mut env, context);
|
||||||
|
}
|
||||||
|
|
||||||
infer_reactive_places(&mut hir, &mut env)?;
|
infer_reactive_places(&mut hir, &mut env)?;
|
||||||
|
|
||||||
if env.enable_validations()
|
if env.enable_validations()
|
||||||
@@ -384,3 +410,45 @@ pub fn compile_fn<'a, const EMIT: bool>(
|
|||||||
fn log_errors_as_events(errors: &Diagnostics, context: &mut ProgramContext) {
|
fn log_errors_as_events(errors: &Diagnostics, context: &mut ProgramContext) {
|
||||||
context.diagnostics.extend(errors.iter().cloned());
|
context.diagnostics.extend(errors.iter().cloned());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn hir_has_start_memoize(hir: &HirFunction<'_>) -> bool {
|
||||||
|
hir.instructions
|
||||||
|
.iter()
|
||||||
|
.any(|instruction| matches!(instruction.value, InstructionValue::StartMemoize { .. }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `hir` calls an effect hook, by type — the calls
|
||||||
|
/// [`validate_exhaustive_dependencies`] checks. Nested functions are not
|
||||||
|
/// scanned because the validator does not check effect calls inside them.
|
||||||
|
fn calls_effect_hook(hir: &HirFunction<'_>, env: &Environment<'_>) -> bool {
|
||||||
|
hir.instructions.iter().any(|instruction| {
|
||||||
|
let callee = match &instruction.value {
|
||||||
|
InstructionValue::CallExpression { callee, .. } => callee,
|
||||||
|
InstructionValue::MethodCall { property, .. } => property,
|
||||||
|
_ => return false,
|
||||||
|
};
|
||||||
|
let ty = &env.types[env.identifiers[callee.identifier].type_];
|
||||||
|
is_use_effect_hook_type(ty)
|
||||||
|
|| is_use_layout_effect_hook_type(ty)
|
||||||
|
|| is_use_insertion_effect_hook_type(ty)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_lint_fn<'a>(
|
||||||
|
env: &mut Environment<'a>,
|
||||||
|
context: &mut ProgramContext<'a>,
|
||||||
|
) -> Result<Option<CodegenFunction<'a>>, Diagnostics> {
|
||||||
|
if env.config.throw_unknown_exception_testonly {
|
||||||
|
return Err(Diagnostics::from(diagnostics::invariant_unexpected_error()));
|
||||||
|
}
|
||||||
|
if env.has_errors() {
|
||||||
|
if let Some(uid_names) = env.take_uid_known_names() {
|
||||||
|
context.merge_uid_known_names(&uid_names);
|
||||||
|
}
|
||||||
|
return Err(env.take_errors());
|
||||||
|
}
|
||||||
|
if let Some(uid_names) = env.take_uid_known_names() {
|
||||||
|
context.merge_uid_known_names(&uid_names);
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
@@ -2065,24 +2065,22 @@ fn find_functions_to_compile<'b, 'ast>(
|
|||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
/// Cheap, sound pre-check for [`find_functions_to_compile`]: `false` means the
|
/// Cheap, sound pre-check for [`find_functions_to_compile`]: `false` means the
|
||||||
/// discovery walk cannot queue anything, so the compile is a no-op. Built from
|
/// discovery walk cannot queue anything, so the compile is a no-op.
|
||||||
/// data `Semantic` already computed instead of walking the AST, and delegating
|
|
||||||
/// all judgment to discovery's own helpers:
|
|
||||||
///
|
///
|
||||||
/// - every discoverable function creates a function scope, so the node behind
|
/// `precise` (compile/emit) runs each function through [`try_make_compile_source`]
|
||||||
/// each function scope is run through [`try_make_compile_source`] with the
|
/// so files like `react.development.js` — hook *implementations*, not callers —
|
||||||
/// name discovery would infer — own id for declarations, the directly
|
/// do not pay a discovery walk that queues nothing. Lint uses a name-only
|
||||||
/// enclosing `const Foo = ...` declarator for expressions/arrows, the only
|
/// over-approximation: a PascalCase / `useX` name or a non-empty directive list
|
||||||
/// name sources discovery uses outside forwardRef/memo. This covers the
|
/// is enough; the JSX/hook body scan is left to discovery.
|
||||||
/// named and directive-opt-in selection paths, nested functions included;
|
///
|
||||||
/// - the forwardRef/memo path needs an identifier named `memo`, `forwardRef`,
|
/// The forwardRef/memo path needs an identifier named `memo`, `forwardRef`,
|
||||||
/// or `React` in callee position of a call [`get_callee_name_if_react_api`]
|
/// or `React` in callee position of a call [`get_callee_name_if_react_api`]
|
||||||
/// recognizes, so checking the reference shapes of those three names —
|
/// recognizes, so checking the reference shapes of those three names —
|
||||||
/// bindings and unresolved globals — covers wrapped anonymous functions.
|
/// bindings and unresolved globals — covers wrapped anonymous functions.
|
||||||
///
|
///
|
||||||
/// Over-approximation is fine (the walk then finds an empty queue); a missed
|
/// Over-approximation is fine (the walk then finds an empty queue); a missed
|
||||||
/// witness is not, since a skipped file is never compiled.
|
/// witness is not, since a skipped file is never compiled.
|
||||||
fn may_have_functions_to_compile(semantic: &Semantic, opts: &PluginOptions) -> bool {
|
fn may_have_functions_to_compile(semantic: &Semantic, opts: &PluginOptions, precise: bool) -> bool {
|
||||||
// 'all' mode compiles every top-level function; always walk.
|
// 'all' mode compiles every top-level function; always walk.
|
||||||
if opts.compilation_mode == CompilationMode::All {
|
if opts.compilation_mode == CompilationMode::All {
|
||||||
return true;
|
return true;
|
||||||
@@ -2104,9 +2102,7 @@ fn may_have_functions_to_compile(semantic: &Semantic, opts: &PluginOptions) -> b
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Named components/hooks and directive opt-ins: run the node behind every
|
// Named components/hooks and directive opt-ins.
|
||||||
// function scope through discovery's candidate constructor. parent_callee
|
|
||||||
// is None — wrapper-selected functions are witnessed by reference shapes.
|
|
||||||
let mut discarded = FxHashSet::default();
|
let mut discarded = FxHashSet::default();
|
||||||
for scope_id in scoping.scope_descendants_from_root() {
|
for scope_id in scoping.scope_descendants_from_root() {
|
||||||
if !scoping.scope_flags(scope_id).is_function() {
|
if !scoping.scope_flags(scope_id).is_function() {
|
||||||
@@ -2124,7 +2120,6 @@ fn may_have_functions_to_compile(semantic: &Semantic, opts: &PluginOptions) -> b
|
|||||||
(declarator_name_for(nodes, node.id()), OriginalFnKind::FunctionExpression)
|
(declarator_name_for(nodes, node.id()), OriginalFnKind::FunctionExpression)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// A nameless function without directives can never classify.
|
|
||||||
if name.is_none()
|
if name.is_none()
|
||||||
&& func.body.as_ref().is_none_or(|body| body.directives.is_empty())
|
&& func.body.as_ref().is_none_or(|body| body.directives.is_empty())
|
||||||
{
|
{
|
||||||
@@ -2143,8 +2138,21 @@ fn may_have_functions_to_compile(semantic: &Semantic, opts: &PluginOptions) -> b
|
|||||||
}
|
}
|
||||||
_ => continue,
|
_ => continue,
|
||||||
};
|
};
|
||||||
if try_make_compile_source(fn_node, name, original_kind, None, opts, &mut discarded)
|
if precise {
|
||||||
.is_some()
|
if try_make_compile_source(fn_node, name, original_kind, None, opts, &mut discarded)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if name.is_some_and(|n| is_hook_name(n) || is_component_name(n))
|
||||||
|
|| match fn_node {
|
||||||
|
FunctionNode::Function(func) => {
|
||||||
|
func.body.as_ref().is_some_and(|body| !body.directives.is_empty())
|
||||||
|
}
|
||||||
|
FunctionNode::Arrow(arrow) => {
|
||||||
|
arrow.get_function_body().is_some_and(|body| !body.directives.is_empty())
|
||||||
|
}
|
||||||
|
}
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -3143,7 +3151,7 @@ pub fn compile_program<'a, const EMIT: bool>(
|
|||||||
// Find all functions to compile. An empty queue means no work, so return
|
// Find all functions to compile. An empty queue means no work, so return
|
||||||
// before all the setup below, which only compilation needs. The pre-check
|
// before all the setup below, which only compilation needs. The pre-check
|
||||||
// decides emptiness from semantic data without walking the AST.
|
// decides emptiness from semantic data without walking the AST.
|
||||||
if !may_have_functions_to_compile(semantic, &options) {
|
if !may_have_functions_to_compile(semantic, &options, EMIT) {
|
||||||
return CompileResult::Success { output: None, diagnostics: Diagnostics::new() };
|
return CompileResult::Success { output: None, diagnostics: Diagnostics::new() };
|
||||||
}
|
}
|
||||||
let queue = find_functions_to_compile(program, &options);
|
let queue = find_functions_to_compile(program, &options);
|
||||||
@@ -3193,10 +3201,10 @@ pub fn compile_program<'a, const EMIT: bool>(
|
|||||||
let ast = AstBuilder::new(allocator);
|
let ast = AstBuilder::new(allocator);
|
||||||
let scope = ScopeResolver::new(semantic, allocator);
|
let scope = ScopeResolver::new(semantic, allocator);
|
||||||
|
|
||||||
// Initialize known referenced names from scope bindings for UID collision detection
|
|
||||||
context.init_from_scope(&scope);
|
|
||||||
|
|
||||||
if EMIT {
|
if EMIT {
|
||||||
|
// Seed UID collision detection with every binding in the file. Lint
|
||||||
|
// does not generate identifiers, so skip the walk.
|
||||||
|
context.init_from_scope(&scope);
|
||||||
// Pre-register instrumentation imports to get stable local names.
|
// Pre-register instrumentation imports to get stable local names.
|
||||||
// These are needed before compilation so codegen can use the correct names.
|
// These are needed before compilation so codegen can use the correct names.
|
||||||
let (instrument_fn_name, instrument_gating_name) = if let Some(ref instrument_config) =
|
let (instrument_fn_name, instrument_gating_name) = if let Some(ref instrument_config) =
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ fn parse_pragma(source: &str) -> (SourceType, PluginOptions) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"outputMode" => options.output_mode = value.and_then(|v| unquote(v).parse().ok()),
|
"outputMode" => options.output_mode = value.and_then(|v| unquote(v).parse().ok()),
|
||||||
|
"lintSkipReactiveScopePasses" => options.lint_skip_reactive_scope_passes = true,
|
||||||
// Fixed test default (upstream `testComplexPluginOptionDefaults`).
|
// Fixed test default (upstream `testComplexPluginOptionDefaults`).
|
||||||
"gating" => {
|
"gating" => {
|
||||||
options.gating = Some(GatingConfig {
|
options.gating = Some(GatingConfig {
|
||||||
|
|||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||||
|
input_file: crates/oxc_react_compiler/fixtures/exhaustive-deps/lint-effect-deps-without-manual-memo.js
|
||||||
|
---
|
||||||
|
Diagnostics:
|
||||||
|
|
||||||
|
OxcDiagnostic { inner: OxcDiagnosticInner { message: "Found missing effect dependencies", labels: [LabeledSpan { label: Some("This dependency list is missing values used by the callback"), span: Span { start: 570, end: 572 }, primary: true }, LabeledSpan { label: Some("Missing dependency `other`"), span: Span { start: 558, end: 563 }, primary: false }, LabeledSpan { label: Some("Missing dependency `value`"), span: Span { start: 532, end: 537 }, primary: false }], help: Some("Missing dependencies can cause an effect to fire less often than it should"), note: Some("React Compiler skipped optimizing this component or hook"), severity: Error, code: OxcCode { scope: Some("react-compiler"), number: Some("EffectExhaustiveDependencies") }, url: Some("https://react.dev/reference/eslint-plugin-react-hooks/lints/exhaustive-deps") } }
|
||||||
|
|
||||||
|
// @validateExhaustiveEffectDependencies:"all" @outputMode:"lint" @panicThreshold:"none" @lintSkipReactiveScopePasses
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
// Without useMemo/useCallback, lint skips the reactive-scope passes but still
|
||||||
|
// infers reactive places for the effect: props are required dependencies,
|
||||||
|
// while the state setter and the ref are stable and may be omitted.
|
||||||
|
function Component({ value, other }) {
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
const ref = useRef(null);
|
||||||
|
useEffect(() => {
|
||||||
|
setCount(value);
|
||||||
|
ref.current = other;
|
||||||
|
}, []);
|
||||||
|
return <div>{count}</div>;
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||||
|
input_file: crates/oxc_react_compiler/fixtures/exhaustive-deps/lint-effect-hook-destructured-from-react.js
|
||||||
|
---
|
||||||
|
Diagnostics:
|
||||||
|
|
||||||
|
OxcDiagnostic { inner: OxcDiagnosticInner { message: "Hooks may not be referenced as normal values; they must be called", labels: [LabeledSpan { label: Some("This Hook is used as a value"), span: Span { start: 365, end: 374 }, primary: true }], help: Some("Call the Hook directly instead of passing or storing it as a value"), note: Some("React Compiler skipped optimizing this component or hook"), severity: Error, code: OxcCode { scope: Some("react-compiler"), number: Some("Hooks") }, url: Some("https://react.dev/reference/eslint-plugin-react-hooks/lints/rules-of-hooks") } }
|
||||||
|
OxcDiagnostic { inner: OxcDiagnosticInner { message: "Found missing effect dependencies", labels: [LabeledSpan { label: Some("This dependency list is missing values used by the callback"), span: Span { start: 412, end: 414 }, primary: true }, LabeledSpan { label: Some("Missing dependency `value`"), span: Span { start: 399, end: 404 }, primary: false }], help: Some("Missing dependencies can cause an effect to fire less often than it should"), note: Some("React Compiler skipped optimizing this component or hook"), severity: Error, code: OxcCode { scope: Some("react-compiler"), number: Some("EffectExhaustiveDependencies") }, url: Some("https://react.dev/reference/eslint-plugin-react-hooks/lints/exhaustive-deps") } }
|
||||||
|
|
||||||
|
// @validateExhaustiveEffectDependencies:"all" @outputMode:"lint" @panicThreshold:"none" @lintSkipReactiveScopePasses
|
||||||
|
import * as React from "react";
|
||||||
|
// The effect hook is only recognisable by its type: the callee is a local
|
||||||
|
// destructured from the React namespace, not a `useEffect` load or property.
|
||||||
|
function Component({ value }) {
|
||||||
|
const { useEffect } = React;
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(value);
|
||||||
|
}, []);
|
||||||
|
return <div>{value}</div>;
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||||
|
input_file: crates/oxc_react_compiler/fixtures/lint-reports-todo-from-reactive-scope-passes.js
|
||||||
|
---
|
||||||
|
Diagnostics:
|
||||||
|
|
||||||
|
OxcDiagnostic { inner: OxcDiagnosticInner { message: "[PruneHoistedContexts] Rewrite hoisted function references", labels: [LabeledSpan { label: None, span: Span { start: 236, end: 239 }, primary: true }], help: Some("Rewrite the highlighted code using syntax supported by React Compiler"), note: Some("React Compiler skipped optimizing this component or hook"), severity: Advice, code: OxcCode { scope: Some("react-compiler"), number: Some("Todo") }, url: Some("https://react.dev/reference/eslint-plugin-react-hooks/lints/unsupported-syntax") } }
|
||||||
|
|
||||||
|
// @outputMode:"lint" @panicThreshold:"none"
|
||||||
|
import { Stringify } from "shared-runtime";
|
||||||
|
// Lint reports the Todo that a pass after validation records for the hoisted
|
||||||
|
// function reference.
|
||||||
|
function Foo() {
|
||||||
|
function foo() {
|
||||||
|
return bar();
|
||||||
|
}
|
||||||
|
function bar() {
|
||||||
|
return 42;
|
||||||
|
}
|
||||||
|
return <Stringify fn1={foo} fn2={bar} />;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user