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:
Boshen committed 2026-09-26 15:26:15 +00:00
1 parent 571cfa3365
commit e996e6c04f
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,
@@ -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.
@@ -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.
@@ -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;
}