Skip to content
Open
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
7 changes: 7 additions & 0 deletions .changeset/empty-record-comparator-reset.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"oxlint-plugin-react-doctor": patch
"eslint-plugin-react-doctor": patch
"react-doctor": patch
---

Preserve structurally compared empty-record state resets that converge in an update lifecycle while continuing to report opaque state producers.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// rule: no-did-update-set-state
// verdict: pass
// weakness: library-idiom
// source: synthetic empty-record convergence regression
// oxlint-disable react/no-did-update-set-state -- fixture exercises React Doctor lifecycle diagnostics

import { Component } from "react";
import compare from "react-fast-compare";

interface EmptyRecordState {
record: Record<string, boolean>;
}

export class EmptyRecordPanel extends Component<Record<string, never>, EmptyRecordState> {
state: EmptyRecordState = { record: { enabled: true } };

componentDidUpdate(previousProps: Record<string, never>, previousState: EmptyRecordState) {
if (!compare(previousState.record, this.state.record)) {
this.setState({ record: {} });
}
}

render() {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// rule: no-did-update-set-state
// weakness: name-heuristic
// source: synthetic erased type metadata around convergent empty-record reset
// verdict: pass

type Note = { nested: { Function: string } };
import React from "react";
import compare from "react-fast-compare";
export class Panel extends React.Component {
state = { hidden: true, record: { enabled: true }, other: 0 };
componentDidUpdate(previousProps, previousState) {
if (!compare(previousState.record, this.state.record)) {
this.setState({ record: {} });
}
}
render() {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// rule: no-did-update-set-state
// weakness: name-heuristic
// source: synthetic erased type metadata around convergent empty-record reset
// verdict: pass

interface Note {
nested: { Object: string };
}
import React from "react";
import compare from "react-fast-compare";
export class Panel extends React.Component {
state = { hidden: true, record: { enabled: true }, other: 0 };
componentDidUpdate(previousProps, previousState) {
if (!compare(previousState.record, this.state.record)) {
this.setState({ record: {} });
}
}
render() {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// rule: no-did-update-set-state
// verdict: fail
// weakness: unstable-comparator
// source: synthetic opaque state-record producer
// oxlint-disable react/no-did-update-set-state -- fixture exercises React Doctor lifecycle diagnostics

import { Component } from "react";
import compare from "react-fast-compare";

declare const computeRecord: () => Record<string, boolean>;

interface EmptyRecordState {
record: Record<string, boolean>;
}

export class OpaqueRecordPanel extends Component<Record<string, never>, EmptyRecordState> {
state: EmptyRecordState = { record: { enabled: true } };

componentDidUpdate(previousProps: Record<string, never>, previousState: EmptyRecordState) {
if (!compare(previousState.record, this.state.record)) {
this.setState({ record: computeRecord() });
}
}

render() {
return null;
}
}
3 changes: 3 additions & 0 deletions packages/fuzz/src/snippet-pools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,9 @@ export const LIBRARY_SNIPPET_POOL = [

// Module scope — SSR hazards, guard aliases, contexts, caches, styled.
export const MODULE_SCOPE_SNIPPET_POOL = [
`import { Component } from "react"; import compareFuzzRecord from "react-fast-compare"; declare const computeFuzzRecord: () => Record<string, boolean>; export class FuzzOpaqueRecordReset extends Component { state = { record: { enabled: true } }; componentDidUpdate(previousProps, previousState) { if (!compareFuzzRecord(previousState.record, this.state.record)) this.setState({ record: computeFuzzRecord() }); } render() { return null; } }`,
"type Note = { nested: { Function: string } };\nimport React from 'react';\nimport compare from 'react-fast-compare';\nexport class Panel extends React.Component {\n state = { hidden: true, record: { enabled: true }, other: 0 };\n componentDidUpdate(previousProps, previousState) {\nif (!compare(previousState.record, this.state.record)) { this.setState({ record: {} }); }\n }\n render() { return null; }\n}\n",
"interface Note { nested: { Object: string } };\nimport React from 'react';\nimport compare from 'react-fast-compare';\nexport class Panel extends React.Component {\n state = { hidden: true, record: { enabled: true }, other: 0 };\n componentDidUpdate(previousProps, previousState) {\nif (!compare(previousState.record, this.state.record)) { this.setState({ record: {} }); }\n }\n render() { return null; }\n}\n",
`import { create as createFuzzZustandStore } from "zustand"; export const useFuzzZustandStore = createFuzzZustandStore(() => ({ items: [] })); export const fuzzZustandSelection = useFuzzZustandStore((state) => ({ items: state.items }));`,
`import { create as createFuzzZustandTarget } from "zustand"; const useFuzzZustandTarget = createFuzzZustandTarget(() => ({ value: 0, other: 0 })); export const FuzzZustandWholeStoreTarget = () => { const { value: fuzzZustandValue } = useFuzzZustandTarget(); return <output>{fuzzZustandValue}</output>; };`,
`import { create as createFuzzZustandGetStore } from "zustand"; const useFuzzZustandGetStore = createFuzzZustandGetStore((_set, get) => ({ count: get().count }));`,
Expand Down
Loading
Loading