mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(react-compiler): treat zero-argument new Date as impure (#26894)
Ports [React's Date purity check](https://github.com/react/react/commit/ff8f88fcd2fb6e5db70e08c674c04b4314747d65) exactly, including the upstream fixtures and equivalent lint coverage. Zero-argument Date calls are impure; construction with a timestamp remains allowed. AI disclosure: Codex was used to port, review, and test this change.
This commit is contained in:
13 files changed
+202
-17
No files matched your search
@@ -63,6 +63,14 @@ fn test() {
|
||||
use crate::tester::Tester;
|
||||
|
||||
let pass = vec![
|
||||
// ---- ImpureFunctionCallsRule-test.ts ----
|
||||
// Date constructed from a timestamp is not flagged
|
||||
"
|
||||
function Component({ timestamp }) {
|
||||
const date = new Date(timestamp);
|
||||
return <Foo date={date} />;
|
||||
}
|
||||
",
|
||||
// ---- RustBackend-test.ts ----
|
||||
// Basic component compiles without errors
|
||||
"
|
||||
@@ -82,6 +90,15 @@ function Component() {
|
||||
const rand = Math.random();
|
||||
return <Foo date={date} now={now} rand={rand} />;
|
||||
}
|
||||
",
|
||||
// Zero-argument Date constructor is impure
|
||||
"
|
||||
function Component() {
|
||||
const date = new Date();
|
||||
const time = new Date().getTime();
|
||||
const year = new Date().getFullYear();
|
||||
return <Foo date={date} time={time} year={year} />;
|
||||
}
|
||||
",
|
||||
];
|
||||
|
||||
|
||||
@@ -34,3 +34,36 @@ source: crates/oxc_linter/src/tester.rs
|
||||
╰────
|
||||
help: `Math.random` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component re-renders
|
||||
note: React Compiler skipped optimizing this component or hook. Additional guidance: https://react.dev/reference/eslint-plugin-react-hooks/lints/purity
|
||||
|
||||
⚠ react(purity): Cannot call impure function during render
|
||||
╭─[purity.tsx:3:16]
|
||||
2 │ function Component() {
|
||||
3 │ const date = new Date();
|
||||
· ─────┬────
|
||||
· ╰── Cannot call impure function
|
||||
4 │ const time = new Date().getTime();
|
||||
╰────
|
||||
help: `Date` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component re-renders
|
||||
note: React Compiler skipped optimizing this component or hook. Additional guidance: https://react.dev/reference/eslint-plugin-react-hooks/lints/purity
|
||||
|
||||
⚠ react(purity): Cannot call impure function during render
|
||||
╭─[purity.tsx:4:16]
|
||||
3 │ const date = new Date();
|
||||
4 │ const time = new Date().getTime();
|
||||
· ─────┬────
|
||||
· ╰── Cannot call impure function
|
||||
5 │ const year = new Date().getFullYear();
|
||||
╰────
|
||||
help: `Date` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component re-renders
|
||||
note: React Compiler skipped optimizing this component or hook. Additional guidance: https://react.dev/reference/eslint-plugin-react-hooks/lints/purity
|
||||
|
||||
⚠ react(purity): Cannot call impure function during render
|
||||
╭─[purity.tsx:5:16]
|
||||
4 │ const time = new Date().getTime();
|
||||
5 │ const year = new Date().getFullYear();
|
||||
· ─────┬────
|
||||
· ╰── Cannot call impure function
|
||||
6 │ return <Foo date={date} time={time} year={year} />;
|
||||
╰────
|
||||
help: `Date` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component re-renders
|
||||
note: React Compiler skipped optimizing this component or hook. Additional guidance: https://react.dev/reference/eslint-plugin-react-hooks/lints/purity
|
||||
@@ -0,0 +1,8 @@
|
||||
// @validateNoImpureFunctionsInRender
|
||||
|
||||
function Component() {
|
||||
const date = new Date();
|
||||
const time = new Date().getTime();
|
||||
const year = new Date().getFullYear();
|
||||
return <Foo date={date} time={time} year={year} />;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// @validateNoImpureFunctionsInRender @enableNewMutationAliasingModel
|
||||
|
||||
function Component() {
|
||||
const date = new Date();
|
||||
const time = new Date().getTime();
|
||||
const year = new Date().getFullYear();
|
||||
return <Foo date={date} time={time} year={year} />;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// @validateNoImpureFunctionsInRender @enableNewMutationAliasingModel
|
||||
|
||||
function Component({timestamp}) {
|
||||
const date = new Date(timestamp);
|
||||
return <Foo date={date} />;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// @validateNoImpureFunctionsInRender
|
||||
|
||||
function Component({timestamp}) {
|
||||
const date = new Date(timestamp);
|
||||
return <Foo date={date} />;
|
||||
}
|
||||
@@ -1689,21 +1689,6 @@ const TYPED_GLOBAL_OBJECTS: &[GlobalObjectDef] = &[
|
||||
},
|
||||
)],
|
||||
},
|
||||
// Date
|
||||
GlobalObjectDef {
|
||||
name: "Date",
|
||||
props: &[Method(
|
||||
"now",
|
||||
MethodDef {
|
||||
rest_param: Some(Effect::Read),
|
||||
return_type: TypeDef::Poly,
|
||||
return_value_kind: ValueKind::Mutable,
|
||||
impure: true,
|
||||
canonical_name: Some("Date.now"),
|
||||
..MethodDef::DEFAULT
|
||||
},
|
||||
)],
|
||||
},
|
||||
// console
|
||||
GlobalObjectDef {
|
||||
name: "console",
|
||||
@@ -1752,13 +1737,47 @@ fn build_typed_globals(
|
||||
) -> Vec<(Ident<'static>, Type<'static>)> {
|
||||
let mut typed_globals: Vec<(Ident, Type)> = Vec::new();
|
||||
|
||||
// Object, Array, Math, performance, Date, console
|
||||
// Object, Array, Math, performance, console
|
||||
for def in TYPED_GLOBAL_OBJECTS {
|
||||
let global = add_object_from_def(shapes, Some(Ident::from(def.name)), def.props);
|
||||
typed_globals.push((Ident::from(def.name), global.clone()));
|
||||
globals.insert(Ident::from(def.name), global);
|
||||
}
|
||||
|
||||
// Date: constructor is a function so `new Date()` gets a call signature.
|
||||
// Zero-arg construction reads the clock (`impure_if_no_args`); `Date.now`
|
||||
// remains a static method on the same shape.
|
||||
let date_now = add_method(
|
||||
shapes,
|
||||
&MethodDef {
|
||||
rest_param: Some(Effect::Read),
|
||||
return_type: TypeDef::Poly,
|
||||
return_value_kind: ValueKind::Mutable,
|
||||
impure: true,
|
||||
canonical_name: Some("Date.now"),
|
||||
..MethodDef::DEFAULT
|
||||
},
|
||||
None,
|
||||
false,
|
||||
);
|
||||
let date_global = add_function(
|
||||
shapes,
|
||||
vec![(Ident::from("now"), date_now)],
|
||||
FunctionSignatureBuilder {
|
||||
rest_param: Some(Effect::Read),
|
||||
return_type: Type::Poly,
|
||||
return_value_kind: ValueKind::Mutable,
|
||||
impure: true,
|
||||
impure_if_no_args: true,
|
||||
canonical_name: Some(Cow::Borrowed("Date")),
|
||||
..Default::default()
|
||||
},
|
||||
Some(Ident::from("Date")),
|
||||
false,
|
||||
);
|
||||
typed_globals.push((Ident::from("Date"), date_global.clone()));
|
||||
globals.insert(Ident::from("Date"), date_global);
|
||||
|
||||
// Simple global functions returning Primitive
|
||||
for name in PRIMITIVE_GLOBAL_FNS {
|
||||
let f = add_method(shapes, &PURE_PRIMITIVE_FN, None, false);
|
||||
|
||||
@@ -118,6 +118,9 @@ pub struct FunctionSignature {
|
||||
pub no_alias: bool,
|
||||
pub mutable_only_if_operands_are_mutable: bool,
|
||||
pub impure: bool,
|
||||
/// When true, `impure` only applies if the call/construct has no arguments.
|
||||
/// Example: `new Date()` reads the clock and is impure, `new Date(timestamp)` is not.
|
||||
pub impure_if_no_args: bool,
|
||||
pub known_incompatible: Option<Cow<'static, str>>,
|
||||
pub canonical_name: Option<Cow<'static, str>>,
|
||||
/// Aliasing signature in config form. Full parsing into AliasingSignature
|
||||
@@ -244,6 +247,7 @@ pub fn add_function<'a>(
|
||||
no_alias: sig.no_alias,
|
||||
mutable_only_if_operands_are_mutable: sig.mutable_only_if_operands_are_mutable,
|
||||
impure: sig.impure,
|
||||
impure_if_no_args: sig.impure_if_no_args,
|
||||
known_incompatible: sig.known_incompatible,
|
||||
canonical_name: sig.canonical_name,
|
||||
aliasing: sig.aliasing,
|
||||
@@ -275,6 +279,7 @@ pub fn add_hook<'a>(
|
||||
no_alias: sig.no_alias,
|
||||
mutable_only_if_operands_are_mutable: false,
|
||||
impure: false,
|
||||
impure_if_no_args: false,
|
||||
known_incompatible: sig.known_incompatible,
|
||||
canonical_name: None,
|
||||
aliasing: sig.aliasing,
|
||||
@@ -327,6 +332,7 @@ pub struct FunctionSignatureBuilder<'a> {
|
||||
pub no_alias: bool,
|
||||
pub mutable_only_if_operands_are_mutable: bool,
|
||||
pub impure: bool,
|
||||
pub impure_if_no_args: bool,
|
||||
pub known_incompatible: Option<Cow<'static, str>>,
|
||||
pub canonical_name: Option<Cow<'static, str>>,
|
||||
pub aliasing: Option<AliasingSignatureConfig>,
|
||||
@@ -344,6 +350,7 @@ impl Default for FunctionSignatureBuilder<'_> {
|
||||
no_alias: false,
|
||||
mutable_only_if_operands_are_mutable: false,
|
||||
impure: false,
|
||||
impure_if_no_args: false,
|
||||
known_incompatible: None,
|
||||
canonical_name: None,
|
||||
aliasing: None,
|
||||
|
||||
+4
-1
@@ -2485,7 +2485,10 @@ fn compute_effects_for_legacy_signature<'a>(
|
||||
reason: return_value_reason,
|
||||
});
|
||||
|
||||
if signature.impure && env.config.validate_no_impure_functions_in_render {
|
||||
if signature.impure
|
||||
&& env.config.validate_no_impure_functions_in_render
|
||||
&& (!signature.impure_if_no_args || args.is_empty())
|
||||
{
|
||||
let diagnostic =
|
||||
diagnostics::impure_function(signature.canonical_name.as_deref(), span.copied());
|
||||
let error = env.intern_aliasing_diagnostic(receiver.identifier, diagnostic);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/error.invalid-new-date-in-render.js
|
||||
---
|
||||
Diagnostics:
|
||||
|
||||
OxcDiagnostic { inner: OxcDiagnosticInner { message: "Cannot call impure function during render", labels: [LabeledSpan { label: Some("Cannot call impure function"), span: Span { start: 77, end: 87 }, primary: true }], help: Some("`Date` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component re-renders"), note: Some("React Compiler skipped optimizing this component or hook"), severity: Error, code: OxcCode { scope: Some("react-compiler"), number: Some("Purity") }, url: Some("https://react.dev/reference/eslint-plugin-react-hooks/lints/purity") } }
|
||||
OxcDiagnostic { inner: OxcDiagnosticInner { message: "Cannot call impure function during render", labels: [LabeledSpan { label: Some("Cannot call impure function"), span: Span { start: 104, end: 114 }, primary: true }], help: Some("`Date` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component re-renders"), note: Some("React Compiler skipped optimizing this component or hook"), severity: Error, code: OxcCode { scope: Some("react-compiler"), number: Some("Purity") }, url: Some("https://react.dev/reference/eslint-plugin-react-hooks/lints/purity") } }
|
||||
OxcDiagnostic { inner: OxcDiagnosticInner { message: "Cannot call impure function during render", labels: [LabeledSpan { label: Some("Cannot call impure function"), span: Span { start: 141, end: 151 }, primary: true }], help: Some("`Date` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component re-renders"), note: Some("React Compiler skipped optimizing this component or hook"), severity: Error, code: OxcCode { scope: Some("react-compiler"), number: Some("Purity") }, url: Some("https://react.dev/reference/eslint-plugin-react-hooks/lints/purity") } }
|
||||
|
||||
No changes.
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
---
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/new-mutability/error.invalid-new-date-in-render.js
|
||||
---
|
||||
Diagnostics:
|
||||
|
||||
OxcDiagnostic { inner: OxcDiagnosticInner { message: "Cannot call impure function during render", labels: [LabeledSpan { label: Some("Cannot call impure function"), span: Span { start: 109, end: 119 }, primary: true }], help: Some("`Date` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component re-renders"), note: Some("React Compiler skipped optimizing this component or hook"), severity: Error, code: OxcCode { scope: Some("react-compiler"), number: Some("Purity") }, url: Some("https://react.dev/reference/eslint-plugin-react-hooks/lints/purity") } }
|
||||
OxcDiagnostic { inner: OxcDiagnosticInner { message: "Cannot call impure function during render", labels: [LabeledSpan { label: Some("Cannot call impure function"), span: Span { start: 136, end: 146 }, primary: true }], help: Some("`Date` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component re-renders"), note: Some("React Compiler skipped optimizing this component or hook"), severity: Error, code: OxcCode { scope: Some("react-compiler"), number: Some("Purity") }, url: Some("https://react.dev/reference/eslint-plugin-react-hooks/lints/purity") } }
|
||||
OxcDiagnostic { inner: OxcDiagnosticInner { message: "Cannot call impure function during render", labels: [LabeledSpan { label: Some("Cannot call impure function"), span: Span { start: 173, end: 183 }, primary: true }], help: Some("`Date` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component re-renders"), note: Some("React Compiler skipped optimizing this component or hook"), severity: Error, code: OxcCode { scope: Some("react-compiler"), number: Some("Purity") }, url: Some("https://react.dev/reference/eslint-plugin-react-hooks/lints/purity") } }
|
||||
|
||||
No changes.
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/new-mutability/valid-new-date-from-timestamp.js
|
||||
---
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
// @validateNoImpureFunctionsInRender @enableNewMutationAliasingModel
|
||||
function Component(t0) {
|
||||
const $ = _c(4);
|
||||
const { timestamp } = t0;
|
||||
let t1;
|
||||
if ($[0] !== timestamp) {
|
||||
t1 = new Date(timestamp);
|
||||
$[0] = timestamp;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const date = t1;
|
||||
let t2;
|
||||
if ($[2] !== date) {
|
||||
t2 = <Foo date={date} />;
|
||||
$[2] = date;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/valid-new-date-from-timestamp.js
|
||||
---
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
// @validateNoImpureFunctionsInRender
|
||||
function Component(t0) {
|
||||
const $ = _c(4);
|
||||
const { timestamp } = t0;
|
||||
let t1;
|
||||
if ($[0] !== timestamp) {
|
||||
t1 = new Date(timestamp);
|
||||
$[0] = timestamp;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const date = t1;
|
||||
let t2;
|
||||
if ($[2] !== date) {
|
||||
t2 = <Foo date={date} />;
|
||||
$[2] = date;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
Reference in new issue
Block a user