Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions plugins/eslint-plugin-react-x/src/rules/purity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to the `react-x/purity` rule will be documented in this file
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

### Fixed

- Treat `new Date(...)` calls with arguments as impure during render to match the React Compiler spec. Closes #1759.

## [5.5.3-beta.1] - 2026-04-27

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ Detection uses only `IMPURE_FUNCS` and `IMPURE_CTORS`. Although `lib.ts` also ex
- **ESLint**: only names present in `IMPURE_FUNCS` or `IMPURE_CTORS` are candidates.
- **ESLint aliases**: `resolveBuiltinObjectName` follows simple variable-initializer chains to a global root, such as `const M = Math` and `const D = Date`.
- **ESLint shadowing**: parameters, imports, function declarations, and other local definitions do not resolve as built-ins; implicit or unresolved globals do.
- **ESLint constructors**: constructors in `IMPURE_CTORS` are reported, except `new Date(arg)` is allowed when at least one argument is present; zero-argument `new Date()` is reported.
- **ESLint constructors**: constructors in `IMPURE_CTORS` are reported, including `new Date(...)` with or without arguments.

**Verdict**: Alias, shadowing, and the `new Date(arg)` exception are explicit ESLint behaviors. The cited React fixture does not establish corresponding upstream behavior.
**Verdict**: Alias and shadowing are explicit ESLint behaviors. `new Date(...)` constructor calls are reported like the other constructors in `IMPURE_CTORS`.

## 3. Reporting

Expand All @@ -69,7 +69,7 @@ There is no current upstream dual-location diagnostic to reproduce. The React ex

- React's active branch, option gate, signature check, `Purity` category, reason, and single call location.
- The standalone React validator is present but not connected to `Pipeline.ts`.
- ESLint's exclusive use of `IMPURE_FUNCS` / `IMPURE_CTORS`, component-hook ownership check, alias resolution, shadowing handling, and `new Date(arg)` exception.
- ESLint's exclusive use of `IMPURE_FUNCS` / `IMPURE_CTORS`, component-hook ownership check, alias resolution, shadowing handling, and Date constructor handling.

### Fixture-verified

Expand Down
86 changes: 44 additions & 42 deletions plugins/eslint-plugin-react-x/src/rules/purity/purity.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,41 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [{ messageId: "default" }],
},
{
code: tsx`
function Component() {
const date = new Date("2024-01-01");
return <div>{date.toISOString()}</div>;
}
`,
errors: [{ messageId: "default" }],
},
{
code: tsx`
function Component({ timestamp }: { timestamp: number }) {
const date = new Date(timestamp);
return <div>{date.toISOString()}</div>;
}
`,
errors: [{ messageId: "default" }],
},
{
code: tsx`
function Component({ year, month, day }: { year: number; month: number; day: number }) {
const date = new Date(year, month, day);
return <div>{date.toISOString()}</div>;
}
`,
errors: [{ messageId: "default" }],
},
{
code: tsx`
function Component({ myDate }: { myDate: string }) {
return new Date(myDate) < new Date("2020-01-01") ? <span>one</span> : <span>two</span>;
}
`,
errors: [{ messageId: "default" }, { messageId: "default" }],
},
{
code: tsx`
function Component() {
Expand Down Expand Up @@ -528,6 +563,15 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [{ messageId: "default" }],
},
{
code: tsx`
function useFormattedDate(input: string) {
const date = new Date(input);
return date.toLocaleDateString();
}
`,
errors: [{ messageId: "default" }],
},
{
code: tsx`
function useStorage() {
Expand Down Expand Up @@ -1423,48 +1467,6 @@ ruleTester.run(RULE_NAME, rule, {
`,
},
// -------------------------------------------------------------------------
// new Date(arg) with arguments is pure (deterministic)
// -------------------------------------------------------------------------
{
code: tsx`
function Component({ myDate }: { myDate: string }) {
return new Date(myDate) < new Date("2020-01-01") ? <span>one</span> : <span>two</span>;
}
`,
},
{
code: tsx`
function Component() {
const date = new Date("2024-01-01");
return <div>{date.toISOString()}</div>;
}
`,
},
{
code: tsx`
function Component({ timestamp }: { timestamp: number }) {
const date = new Date(timestamp);
return <div>{date.toISOString()}</div>;
}
`,
},
{
code: tsx`
function useFormattedDate(input: string) {
const date = new Date(input);
return date.toLocaleDateString();
}
`,
},
{
code: tsx`
function Component({ year, month, day }: { year: number; month: number; day: number }) {
const date = new Date(year, month, day);
return <div>{date.toISOString()}</div>;
}
`,
},
// -------------------------------------------------------------------------
// Local shadowing of builtins (should NOT be flagged)
// -------------------------------------------------------------------------
{
Expand Down
3 changes: 0 additions & 3 deletions plugins/eslint-plugin-react-x/src/rules/purity/purity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,6 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
const builtinName = resolveBuiltinObjectName(context, expr);
if (builtinName == null) return;
if (!IMPURE_CTORS.has(builtinName)) return;
// `new Date(arg)` with arguments is pure (deterministic),
// only `new Date()` without arguments is impure (depends on current time).
if (builtinName === "Date" && node.arguments.length > 0) return;
const func = Traverse.findParent(node, Check.isFunction);
if (func == null) return;
nEntries.push({ func, node });
Expand Down
Loading