mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(react_compiler): render unchanged programs as source in fixture snapshots (#24129)
## Root cause
The `oxc_react_compiler` snapshot harness (`tests/snapshot.rs`) rendered a clean no-op compile as the literal `No changes.` marker:
```rust
if result.changed {
out.push_str(&Codegen::new().build(&program).code);
} else {
out.push_str("No changes.");
}
```
Upstream's `snap` runner instead **always** re-emits the program as `## Code`, and only replaces it with `## Error` on a hard error. So for every fixture where oxc "bails" with `No changes.`, the fork's golden shows the reprinted source — a permanent, opaque diff.
Investigating the **rules-of-hooks** batch showed this is purely a harness-representation gap, not a compiler bug:
- All 17 "bailing" rules-of-hooks fixtures carry `@expectNothingCompiled` (e.g. top-level statements, classes, or `@compilationMode:"infer"` functions that aren't components/hooks). oxc correctly compiles nothing — matching the fork, which also compiles nothing and just reprints the source.
- Every rules-of-hooks **error** fixture already matches the fork on category, message text, and source location (Rules-of-Hooks validation is a complete port).
- Every rules-of-hooks **compiling** fixture already matches the fork structurally; residual diffs are pure codegen formatting (trailing commas, JSX paren-wrapping) which are out of scope.
## Fix
Test-only change. On a clean no-op (no parse errors **and** no compile diagnostics), codegen the unchanged program instead of the marker, mirroring the upstream runner. The marker is kept when an error was reported, since upstream emits `## Error` with no code there — and reprinting a parse-recovered AST would be misleading.
```rust
let clean = parsed.diagnostics.is_empty() && result.diagnostics.as_slice().is_empty();
if result.changed || clean {
out.push_str(&Codegen::new().build(&program).code);
} else {
out.push_str("No changes.");
}
```
No compiler source is touched, so there is no risk to the compiler, napi bindings, or other consumers. The snapshot test still catches any future behavior change.
## Fixtures changed
54 snapshots that previously said `No changes.` now render the reprinted source:
- **17 rules-of-hooks** (this batch) — all `@expectNothingCompiled`; now match the fork's `## Code` modulo whitespace.
- **37 elsewhere** (guarded corpus-wide no-ops: `infer-*`, `use-no-forget-*`, `use-no-memo-*`, `gating/*`, `effect-derived-computations/*`, `valid-setState-*`, etc.).
52 of the 54 match the fork's `## Code` modulo formatting. The 11 parse-error fixtures (`error.todo-*`) keep the marker as intended.
## Remaining work
Two of the changed snapshots move **closer** to the fork but retain **pre-existing, out-of-batch** compiler gaps (unchanged by this PR, now just more visible):
- `ignore-use-no-forget.js` — oxc doesn't implement the `@ignoreUseNoForget` plugin option, so it honors the `'use no forget'` opt-out and skips; the fork ignores the opt-out and fully compiles.
- `valid-setState-in-effect-from-ref-function-call.js` — in `@outputMode:"lint"` the fork reprints with an SSA-renamed shadowed param (`ref` → `ref_0`); oxc leaves the source unchanged.
Both are separate from Rules-of-Hooks and left to their respective batches.
This commit is contained in:
55 files changed
+647
-55
No files matched your search
@@ -70,7 +70,15 @@ fn run_fixture(source: &str) -> String {
|
||||
}
|
||||
|
||||
push_diagnostics(&mut out, "Diagnostics", result.diagnostics.as_slice());
|
||||
if result.changed {
|
||||
// Mirror the upstream `snap` runner, which always re-emits the program as
|
||||
// `## Code` unless a hard error turns the output into `## Error`. So when the
|
||||
// compiler cleanly declines to change anything (e.g. `@expectNothingCompiled`,
|
||||
// or a file with no React-like functions), echo the reprinted source rather
|
||||
// than the `No changes.` marker. The marker is kept only when an error was
|
||||
// reported (parse failure or a compile diagnostic), where upstream emits no
|
||||
// code — and echoing a parse-recovered AST would be misleading.
|
||||
let clean = parsed.diagnostics.is_empty() && result.diagnostics.as_slice().is_empty();
|
||||
if result.changed || clean {
|
||||
out.push_str(&Codegen::new().build(&program).code);
|
||||
} else {
|
||||
out.push_str("No changes.");
|
||||
|
||||
@@ -2,4 +2,16 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/class-component-with-render-helper.js
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
class Component {
|
||||
_renderMessage = () => {
|
||||
const Message = () => {
|
||||
const message = this.state.message;
|
||||
return <div>{message}</div>;
|
||||
};
|
||||
return <Message />;
|
||||
};
|
||||
render() {
|
||||
return this._renderMessage();
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,12 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/custom-opt-out-directive.tsx
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled @customOptOutDirectives:["use todo memo"]
|
||||
function Component() {
|
||||
"use todo memo";
|
||||
return <div>hello world!</div>;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: []
|
||||
};
|
||||
+26
-1
@@ -2,4 +2,29 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/dont-memoize-primitive-function-call-non-escaping.js
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
|
||||
import { useMemo } from "react";
|
||||
import { makeObject_Primitives, ValidateMemoization } from "shared-runtime";
|
||||
function Component(props) {
|
||||
const result = makeObject(props.value).value + 1;
|
||||
console.log(result);
|
||||
return "ok";
|
||||
}
|
||||
function makeObject(value) {
|
||||
console.log(value);
|
||||
return { value };
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: 42 }],
|
||||
sequentialRenders: [
|
||||
{ value: 42 },
|
||||
{ value: 42 },
|
||||
{ value: 3.14 },
|
||||
{ value: 3.14 },
|
||||
{ value: 42 },
|
||||
{ value: 3.14 },
|
||||
{ value: 42 },
|
||||
{ value: 3.14 }
|
||||
]
|
||||
};
|
||||
+15
-1
@@ -2,4 +2,18 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/effect-derived-computations/derived-state-from-prop-setter-call-outside-effect-no-error.js
|
||||
---
|
||||
No changes.
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
|
||||
import { useEffect, useState } from "react";
|
||||
function Component({ initialName }) {
|
||||
const [name, setName] = useState("");
|
||||
useEffect(() => {
|
||||
setName(initialName);
|
||||
}, [initialName]);
|
||||
return <div>
|
||||
<input value={name} onChange={(e) => setName(e.target.value)} />
|
||||
</div>;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ initialName: "John" }]
|
||||
};
|
||||
+16
-1
@@ -2,4 +2,19 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/effect-derived-computations/derived-state-from-prop-setter-used-outside-effect-no-error.js
|
||||
---
|
||||
No changes.
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
|
||||
import { useEffect, useState } from "react";
|
||||
function MockComponent({ onSet }) {
|
||||
return <div onClick={() => onSet("clicked")}>Mock Component</div>;
|
||||
}
|
||||
function Component({ propValue }) {
|
||||
const [value, setValue] = useState(null);
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
}, [propValue]);
|
||||
return <MockComponent onSet={setValue} />;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ propValue: "test" }]
|
||||
};
|
||||
+14
-1
@@ -2,4 +2,17 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/effect-derived-computations/derived-state-from-ref-and-state-no-error.js
|
||||
---
|
||||
No changes.
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
export default function Component({ test }) {
|
||||
const [local, setLocal] = useState("");
|
||||
const myRef = useRef(null);
|
||||
useEffect(() => {
|
||||
setLocal(myRef.current + test);
|
||||
}, [test]);
|
||||
return <>{local}</>;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ test: "testString" }]
|
||||
};
|
||||
+17
-1
@@ -2,4 +2,20 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/effect-derived-computations/effect-contains-prop-function-call-no-error.js
|
||||
---
|
||||
No changes.
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
|
||||
import { useEffect, useState } from "react";
|
||||
function Component({ propValue, onChange }) {
|
||||
const [value, setValue] = useState(null);
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
onChange();
|
||||
}, [propValue]);
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{
|
||||
propValue: "test",
|
||||
onChange: () => {}
|
||||
}]
|
||||
};
|
||||
+17
-1
@@ -2,4 +2,20 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/effect-derived-computations/effect-with-cleanup-function-depending-on-derived-computation-value.js
|
||||
---
|
||||
No changes.
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
|
||||
import { useEffect, useState } from "react";
|
||||
function Component(file: File) {
|
||||
const [imageUrl, setImageUrl] = useState(null);
|
||||
/*
|
||||
* Cleaning up the variable or a source of the variable used to setState
|
||||
* inside the effect communicates that we always need to clean up something
|
||||
* which is a valid use case for useEffect. In which case we want to
|
||||
* avoid an throwing
|
||||
*/
|
||||
useEffect(() => {
|
||||
const imageUrlPrepared = URL.createObjectURL(file);
|
||||
setImageUrl(imageUrlPrepared);
|
||||
return () => URL.revokeObjectURL(imageUrlPrepared);
|
||||
}, [file]);
|
||||
return <Image src={imageUrl} xstyle={styles.imageSizeLimits} />;
|
||||
}
|
||||
+14
-1
@@ -2,4 +2,17 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/effect-derived-computations/effect-with-global-function-call-no-error.js
|
||||
---
|
||||
No changes.
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
|
||||
import { useEffect, useState } from "react";
|
||||
function Component({ propValue }) {
|
||||
const [value, setValue] = useState(null);
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
globalCall();
|
||||
}, [propValue]);
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ propValue: "test" }]
|
||||
};
|
||||
+7
-1
@@ -2,4 +2,10 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/effect-derived-computations/from-props-setstate-in-effect-no-error.js
|
||||
---
|
||||
No changes.
|
||||
// @validateNoDerivedComputationsInEffects_exp @enableTreatSetIdentifiersAsStateSetters @loggerTestOnly @outputMode:"lint"
|
||||
function Component({ setParentState, prop }) {
|
||||
useEffect(() => {
|
||||
setParentState(prop);
|
||||
}, [prop]);
|
||||
return <div>{prop}</div>;
|
||||
}
|
||||
+18
-1
@@ -2,4 +2,21 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/effect-derived-computations/ref-conditional-in-effect-no-error.js
|
||||
---
|
||||
No changes.
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly @outputMode:"lint"
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
export default function Component({ test }) {
|
||||
const [local, setLocal] = useState(0);
|
||||
const myRef = useRef(null);
|
||||
useEffect(() => {
|
||||
if (myRef.current) {
|
||||
setLocal(test);
|
||||
} else {
|
||||
setLocal(test + test);
|
||||
}
|
||||
}, [test]);
|
||||
return <>{local}</>;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ test: 4 }]
|
||||
};
|
||||
@@ -2,4 +2,11 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/fn-name-no-leak-to-nested-arrow.js
|
||||
---
|
||||
No changes.
|
||||
// @loggerTestOnly @outputMode:"lint"
|
||||
function Component() {
|
||||
return null;
|
||||
}
|
||||
export const ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ onChange: () => {} }]
|
||||
};
|
||||
@@ -2,4 +2,12 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/gating/dynamic-gating-noemit.js
|
||||
---
|
||||
No changes.
|
||||
// @dynamicGating:{"source":"shared-runtime"} @outputMode:"lint"
|
||||
function Foo() {
|
||||
"use memo if(getTrue)";
|
||||
return <div>hello world</div>;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{}]
|
||||
};
|
||||
+3
-1
@@ -2,4 +2,6 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/gating/repro-no-gating-import-without-compiled-functions.js
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled @gating
|
||||
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
|
||||
export default 42;
|
||||
@@ -2,4 +2,13 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/ignore-use-no-forget.js
|
||||
---
|
||||
No changes.
|
||||
// @ignoreUseNoForget
|
||||
function Component(prop) {
|
||||
"use no forget";
|
||||
const result = prop.x.toFixed();
|
||||
return <div>{result}</div>;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ x: 1 }]
|
||||
};
|
||||
+9
-1
@@ -2,4 +2,12 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/infer-dont-compile-components-with-multiple-params.js
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// Takes multiple parameters - not a component!
|
||||
function Component(foo, bar) {
|
||||
return <div />;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [null, null]
|
||||
};
|
||||
@@ -2,4 +2,13 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/infer-no-component-annot.ts
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
import { useIdentity, identity } from "shared-runtime";
|
||||
function Component(fakeProps: number) {
|
||||
const x = useIdentity(fakeProps);
|
||||
return identity(x);
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [42]
|
||||
};
|
||||
@@ -2,4 +2,19 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/infer-no-component-nested-jsx.js
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
function Component(props) {
|
||||
const result = f(props);
|
||||
function helper() {
|
||||
return <foo />;
|
||||
}
|
||||
helper();
|
||||
return result;
|
||||
}
|
||||
function f(props) {
|
||||
return props;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}]
|
||||
};
|
||||
@@ -2,4 +2,15 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/infer-no-component-obj-return.js
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
function Component(props) {
|
||||
const ignore = <foo />;
|
||||
return { foo: f(props) };
|
||||
}
|
||||
function f(props) {
|
||||
return props;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}]
|
||||
};
|
||||
+6
-1
@@ -2,4 +2,9 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/infer-skip-components-without-hooks-or-jsx.js
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// This component is skipped bc it doesn't call any hooks or
|
||||
// use JSX:
|
||||
function Component(props) {
|
||||
return render();
|
||||
}
|
||||
+5
-1
@@ -2,4 +2,8 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/repro-bailout-nopanic-shouldnt-outline.js
|
||||
---
|
||||
No changes.
|
||||
// @panicThreshold:"none"
|
||||
"use no memo";
|
||||
function Foo() {
|
||||
return <button onClick={() => alert("hello!")}>Click me!</button>;
|
||||
}
|
||||
+12
-1
@@ -2,4 +2,15 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/rules-of-hooks/rules-of-hooks-0592bd574811.js
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// Regression test for some internal code.
|
||||
// This shows how the "callback rule" is more relaxed,
|
||||
// and doesn't kick in unless we're confident we're in
|
||||
// a component or a hook.
|
||||
function makeListener(instance) {
|
||||
each(pixelsWithInferredEvents, (pixel) => {
|
||||
if (useExtendedSelector(pixel.id) && extendedButton) {
|
||||
foo();
|
||||
}
|
||||
});
|
||||
}
|
||||
+8
-1
@@ -2,4 +2,11 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/rules-of-hooks/rules-of-hooks-2bec02ac982b.js
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// Valid because hooks can call hooks.
|
||||
function createHook() {
|
||||
return function useHook() {
|
||||
useHook1();
|
||||
useHook2();
|
||||
};
|
||||
}
|
||||
+7
-1
@@ -2,4 +2,10 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/rules-of-hooks/rules-of-hooks-33a6e23edac1.js
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// Valid because hooks can use hooks.
|
||||
function createHook() {
|
||||
return function useHookWithHook() {
|
||||
useHook();
|
||||
};
|
||||
}
|
||||
+7
-1
@@ -2,4 +2,10 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/rules-of-hooks/rules-of-hooks-8f1c2c3f71c9.js
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// Valid because components can use hooks.
|
||||
function createComponentWithHook() {
|
||||
return function ComponentWithHook() {
|
||||
useHook();
|
||||
};
|
||||
}
|
||||
+8
-1
@@ -2,4 +2,11 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/rules-of-hooks/rules-of-hooks-df4d750736f3.js
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled
|
||||
// Valid because they're not matching use[A-Z].
|
||||
fooState();
|
||||
_use();
|
||||
_useState();
|
||||
use_hook();
|
||||
// also valid because it's not matching the PascalCase namespace
|
||||
jest.useFakeTimer();
|
||||
+9
-1
@@ -2,4 +2,12 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/rules-of-hooks/rules-of-hooks-dfde14171fcd.js
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled
|
||||
// Valid because classes can call functions.
|
||||
// We don't consider these to be hooks.
|
||||
class C {
|
||||
m() {
|
||||
this.useHook();
|
||||
super.useHook();
|
||||
}
|
||||
}
|
||||
+8
-1
@@ -2,4 +2,11 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/rules-of-hooks/rules-of-hooks-fe6042f7628b.js
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// This is valid because "use"-prefixed functions called in
|
||||
// unnamed function arguments are not assumed to be hooks.
|
||||
unknownFunction(function(foo, bar) {
|
||||
if (foo) {
|
||||
useNotAHook(bar);
|
||||
}
|
||||
});
|
||||
+13
-1
@@ -2,4 +2,16 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/rules-of-hooks/todo.invalid.invalid-rules-of-hooks-191029ac48c8.js
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
// Invalid because it's dangerous.
|
||||
// Normally, this would crash, but not if you use inline requires.
|
||||
// This *must* be invalid.
|
||||
// It's expected to have some false positives, but arguably
|
||||
// they are confusing anyway due to the use*() convention
|
||||
// already being associated with Hooks.
|
||||
useState();
|
||||
if (foo) {
|
||||
const foo = React.useCallback(() => {});
|
||||
}
|
||||
useCustomHook();
|
||||
+11
-1
@@ -2,4 +2,14 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/rules-of-hooks/todo.invalid.invalid-rules-of-hooks-206e2811c87c.js
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
// This is a false positive (it's valid) that unfortunately
|
||||
// we cannot avoid. Prefer to rename it to not start with "use"
|
||||
class Foo extends Component {
|
||||
render() {
|
||||
if (cond) {
|
||||
FooStore.useFeatureFlag();
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-1
@@ -2,4 +2,14 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/rules-of-hooks/todo.invalid.invalid-rules-of-hooks-28a7111f56a7.js
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
// Technically this is a false positive.
|
||||
// We *could* make it valid (and it used to be).
|
||||
//
|
||||
// However, top-level Hook-like calls can be very dangerous
|
||||
// in environments with inline requires because they can mask
|
||||
// the runtime error by accident.
|
||||
// So we prefer to disallow it despite the false positive.
|
||||
const { createHistory, useBasename } = require("history-2.1.2");
|
||||
const browserHistory = useBasename(createHistory)({ basename: "/" });
|
||||
+7
-1
@@ -2,4 +2,10 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/rules-of-hooks/todo.invalid.invalid-rules-of-hooks-2c51251df67a.js
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
(class {
|
||||
useHook() {
|
||||
useState();
|
||||
}
|
||||
});
|
||||
+7
-1
@@ -2,4 +2,10 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/rules-of-hooks/todo.invalid.invalid-rules-of-hooks-8303403b8e4c.js
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
class ClassComponentWithHook extends React.Component {
|
||||
render() {
|
||||
React.useState();
|
||||
}
|
||||
}
|
||||
+9
-1
@@ -2,4 +2,12 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/rules-of-hooks/todo.invalid.invalid-rules-of-hooks-99b5c750d1d1.js
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
class ClassComponentWithFeatureFlag extends React.Component {
|
||||
render() {
|
||||
if (foo) {
|
||||
useFeatureFlag();
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -2,4 +2,10 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/rules-of-hooks/todo.invalid.invalid-rules-of-hooks-9c79feec4b9b.js
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
(class {
|
||||
h = () => {
|
||||
useState();
|
||||
};
|
||||
});
|
||||
+8
-1
@@ -2,4 +2,11 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/rules-of-hooks/todo.invalid.invalid-rules-of-hooks-acb56658fe7e.js
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
class C {
|
||||
m() {
|
||||
This.useHook();
|
||||
Super.useHook();
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -2,4 +2,10 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/rules-of-hooks/todo.invalid.invalid-rules-of-hooks-ddeca9708b63.js
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
(class {
|
||||
i() {
|
||||
useState();
|
||||
}
|
||||
});
|
||||
+7
-1
@@ -2,4 +2,10 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/rules-of-hooks/todo.invalid.invalid-rules-of-hooks-e69ffce323c3.js
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
(class {
|
||||
useHook = () => {
|
||||
useState();
|
||||
};
|
||||
});
|
||||
@@ -2,4 +2,20 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/skip-useMemoCache.js
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled
|
||||
import { c as useMemoCache } from "react/compiler-runtime";
|
||||
function Component(props) {
|
||||
const $ = useMemoCache();
|
||||
let x;
|
||||
if ($[0] === undefined) {
|
||||
x = [props.value];
|
||||
$[0] = x;
|
||||
} else {
|
||||
x = $[0];
|
||||
}
|
||||
return x;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: 42 }]
|
||||
};
|
||||
+9
-1
@@ -2,4 +2,12 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/typescript-this-param-in-uncompiled-function.ts
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
export function Decorate() {
|
||||
return function(_target: object, _key: string, descriptor: PropertyDescriptor) {
|
||||
const original = descriptor.value;
|
||||
descriptor.value = function(this: unknown) {
|
||||
return original.apply(this, []);
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -2,4 +2,12 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/use-memo-noemit.js
|
||||
---
|
||||
No changes.
|
||||
// @outputMode:"lint"
|
||||
function Foo() {
|
||||
"use memo";
|
||||
return <button onClick={() => alert("hello!")}>Click me!</button>;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: []
|
||||
};
|
||||
@@ -2,4 +2,10 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/use-no-forget-module-level.js
|
||||
---
|
||||
No changes.
|
||||
"use no forget";
|
||||
export default function foo(x, y) {
|
||||
if (x) {
|
||||
return foo(false, y);
|
||||
}
|
||||
return [y * 10];
|
||||
}
|
||||
@@ -2,4 +2,13 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/use-no-forget-with-no-errors.js
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled
|
||||
function Component() {
|
||||
"use no forget";
|
||||
return <div>Hello World</div>;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
isComponent: true
|
||||
};
|
||||
@@ -2,4 +2,10 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/use-no-memo-module-level.js
|
||||
---
|
||||
No changes.
|
||||
"use no memo";
|
||||
export default function foo(x, y) {
|
||||
if (x) {
|
||||
return foo(false, y);
|
||||
}
|
||||
return [y * 10];
|
||||
}
|
||||
+6
-1
@@ -2,4 +2,9 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/use-no-memo-module-scope-usememo-function-scope.js
|
||||
---
|
||||
No changes.
|
||||
// @compilationMode:"all"
|
||||
"use no memo";
|
||||
function TestComponent({ x }) {
|
||||
"use memo";
|
||||
return <Button>{x}</Button>;
|
||||
}
|
||||
@@ -2,4 +2,14 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/use-no-memo-simple.js
|
||||
---
|
||||
No changes.
|
||||
// @expectNothingCompiled
|
||||
function Component(props) {
|
||||
"use no memo";
|
||||
let x = [props.foo];
|
||||
return <div x={x}>"foo"</div>;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ foo: 1 }],
|
||||
isComponent: true
|
||||
};
|
||||
+15
-1
@@ -2,4 +2,18 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/valid-set-state-in-useEffect-from-ref.js
|
||||
---
|
||||
No changes.
|
||||
// @validateNoSetStateInEffects @outputMode:"lint"
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
function Tooltip() {
|
||||
const ref = useRef(null);
|
||||
const [tooltipHeight, setTooltipHeight] = useState(0);
|
||||
useEffect(() => {
|
||||
const { height } = ref.current.getBoundingClientRect();
|
||||
setTooltipHeight(height);
|
||||
}, []);
|
||||
return tooltipHeight;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Tooltip,
|
||||
params: []
|
||||
};
|
||||
+14
-1
@@ -2,4 +2,17 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/valid-setState-in-effect-from-ref-arithmetic.js
|
||||
---
|
||||
No changes.
|
||||
// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects @outputMode:"lint"
|
||||
import { useState, useRef, useLayoutEffect } from "react";
|
||||
function Component() {
|
||||
const ref = useRef({ size: 5 });
|
||||
const [computedSize, setComputedSize] = useState(0);
|
||||
useLayoutEffect(() => {
|
||||
setComputedSize(ref.current.size * 10);
|
||||
}, []);
|
||||
return computedSize;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: []
|
||||
};
|
||||
+21
-1
@@ -2,4 +2,24 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/valid-setState-in-effect-from-ref-array-index.js
|
||||
---
|
||||
No changes.
|
||||
// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects @outputMode:"lint"
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
function Component() {
|
||||
const ref = useRef([
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5
|
||||
]);
|
||||
const [value, setValue] = useState(0);
|
||||
useEffect(() => {
|
||||
const index = 2;
|
||||
setValue(ref.current[index]);
|
||||
}, []);
|
||||
return value;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: []
|
||||
};
|
||||
+20
-1
@@ -2,4 +2,23 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/valid-setState-in-effect-from-ref-function-call.js
|
||||
---
|
||||
No changes.
|
||||
// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects @outputMode:"lint"
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
function Component() {
|
||||
const ref = useRef(null);
|
||||
const [width, setWidth] = useState(0);
|
||||
useEffect(() => {
|
||||
function getBoundingRect(ref) {
|
||||
if (ref.current) {
|
||||
return ref.current.getBoundingClientRect?.()?.width ?? 100;
|
||||
}
|
||||
return 100;
|
||||
}
|
||||
setWidth(getBoundingRect(ref));
|
||||
}, []);
|
||||
return width;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: []
|
||||
};
|
||||
+48
-1
@@ -2,4 +2,51 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/valid-setState-in-useEffect-controlled-by-ref-value.js
|
||||
---
|
||||
No changes.
|
||||
// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects @loggerTestOnly @compilationMode:"infer" @outputMode:"lint"
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
function Component({ x, y }) {
|
||||
const previousXRef = useRef(null);
|
||||
const previousYRef = useRef(null);
|
||||
const [data, setData] = useState(null);
|
||||
useEffect(() => {
|
||||
const previousX = previousXRef.current;
|
||||
previousXRef.current = x;
|
||||
const previousY = previousYRef.current;
|
||||
previousYRef.current = y;
|
||||
if (!areEqual(x, previousX) || !areEqual(y, previousY)) {
|
||||
const data = load({
|
||||
x,
|
||||
y
|
||||
});
|
||||
setData(data);
|
||||
}
|
||||
}, [x, y]);
|
||||
return data;
|
||||
}
|
||||
function areEqual(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
function load({ x, y }) {
|
||||
return x * y;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{
|
||||
x: 0,
|
||||
y: 0
|
||||
}],
|
||||
sequentialRenders: [
|
||||
{
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
{
|
||||
x: 1,
|
||||
y: 0
|
||||
},
|
||||
{
|
||||
x: 1,
|
||||
y: 1
|
||||
}
|
||||
]
|
||||
};
|
||||
+16
-1
@@ -2,4 +2,19 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/valid-setState-in-useEffect-listener-transitive.js
|
||||
---
|
||||
No changes.
|
||||
// @validateNoSetStateInEffects @outputMode:"lint"
|
||||
import { useEffect, useState } from "react";
|
||||
function Component() {
|
||||
const [state, setState] = useState(0);
|
||||
useEffect(() => {
|
||||
const f = () => {
|
||||
setState();
|
||||
};
|
||||
setTimeout(() => f(), 10);
|
||||
});
|
||||
return state;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}]
|
||||
};
|
||||
+13
-1
@@ -2,4 +2,16 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/valid-setState-in-useEffect-listener.js
|
||||
---
|
||||
No changes.
|
||||
// @validateNoSetStateInEffects @outputMode:"lint"
|
||||
import { useEffect, useState } from "react";
|
||||
function Component() {
|
||||
const [state, setState] = useState(0);
|
||||
useEffect(() => {
|
||||
setTimeout(setState, 10);
|
||||
});
|
||||
return state;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}]
|
||||
};
|
||||
+15
-1
@@ -2,4 +2,18 @@
|
||||
source: crates/oxc_react_compiler/tests/snapshot.rs
|
||||
input_file: crates/oxc_react_compiler/fixtures/valid-setState-in-useLayoutEffect-from-ref.js
|
||||
---
|
||||
No changes.
|
||||
// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects @outputMode:"lint"
|
||||
import { useState, useRef, useLayoutEffect } from "react";
|
||||
function Tooltip() {
|
||||
const ref = useRef(null);
|
||||
const [tooltipHeight, setTooltipHeight] = useState(0);
|
||||
useLayoutEffect(() => {
|
||||
const { height } = ref.current.getBoundingClientRect();
|
||||
setTooltipHeight(height);
|
||||
}, []);
|
||||
return tooltipHeight;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Tooltip,
|
||||
params: []
|
||||
};
|
||||
Reference in new issue
Block a user