Compare commits

...

2 Commits

Author SHA1 Message Date
Joe Savona
1517c63a78 [compiler] Enable additional lints by default
Enable more validations to help catch bad patterns, but only in the linter. These rules are already enabled by default in the compiler _if_ violations could produce unsafe output.
2025-07-24 14:43:03 -07:00
Joe Savona
5fbe88ab40 [compiler] Validate against setState in all effect types 2025-07-24 14:43:03 -07:00
14 changed files with 45 additions and 27 deletions

View File

@@ -94,7 +94,7 @@ import {validateLocalsNotReassignedAfterRender} from '../Validation/ValidateLoca
import {outlineFunctions} from '../Optimization/OutlineFunctions'; import {outlineFunctions} from '../Optimization/OutlineFunctions';
import {propagatePhiTypes} from '../TypeInference/PropagatePhiTypes'; import {propagatePhiTypes} from '../TypeInference/PropagatePhiTypes';
import {lowerContextAccess} from '../Optimization/LowerContextAccess'; import {lowerContextAccess} from '../Optimization/LowerContextAccess';
import {validateNoSetStateInPassiveEffects} from '../Validation/ValidateNoSetStateInPassiveEffects'; import {validateNoSetStateInEffects} from '../Validation/ValidateNoSetStateInEffects';
import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement'; import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement';
import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHIR'; import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHIR';
import {outlineJSX} from '../Optimization/OutlineJsx'; import {outlineJSX} from '../Optimization/OutlineJsx';
@@ -292,8 +292,8 @@ function runWithEnvironment(
validateNoSetStateInRender(hir).unwrap(); validateNoSetStateInRender(hir).unwrap();
} }
if (env.config.validateNoSetStateInPassiveEffects) { if (env.config.validateNoSetStateInEffects) {
env.logErrors(validateNoSetStateInPassiveEffects(hir)); env.logErrors(validateNoSetStateInEffects(hir));
} }
if (env.config.validateNoJSXInTryStatements) { if (env.config.validateNoJSXInTryStatements) {

View File

@@ -318,10 +318,10 @@ export const EnvironmentConfigSchema = z.object({
validateNoSetStateInRender: z.boolean().default(true), validateNoSetStateInRender: z.boolean().default(true),
/** /**
* Validates that setState is not called directly within a passive effect (useEffect). * Validates that setState is not called synchronously within an effect (useEffect and friends).
* Scheduling a setState (with an event listener, subscription, etc) is valid. * Scheduling a setState (with an event listener, subscription, etc) is valid.
*/ */
validateNoSetStateInPassiveEffects: z.boolean().default(false), validateNoSetStateInEffects: z.boolean().default(false),
/** /**
* Validates against creating JSX within a try block and recommends using an error boundary * Validates against creating JSX within a try block and recommends using an error boundary

View File

@@ -11,20 +11,22 @@ import {
IdentifierId, IdentifierId,
isSetStateType, isSetStateType,
isUseEffectHookType, isUseEffectHookType,
isUseInsertionEffectHookType,
isUseLayoutEffectHookType,
Place, Place,
} from '../HIR'; } from '../HIR';
import {eachInstructionValueOperand} from '../HIR/visitors'; import {eachInstructionValueOperand} from '../HIR/visitors';
import {Result} from '../Utils/Result'; import {Result} from '../Utils/Result';
/** /**
* Validates against calling setState in the body of a *passive* effect (useEffect), * Validates against calling setState in the body of an effect (useEffect and friends),
* while allowing calling setState in callbacks scheduled by the effect. * while allowing calling setState in callbacks scheduled by the effect.
* *
* Calling setState during execution of a useEffect triggers a re-render, which is * Calling setState during execution of a useEffect triggers a re-render, which is
* often bad for performance and frequently has more efficient and straightforward * often bad for performance and frequently has more efficient and straightforward
* alternatives. See https://react.dev/learn/you-might-not-need-an-effect for examples. * alternatives. See https://react.dev/learn/you-might-not-need-an-effect for examples.
*/ */
export function validateNoSetStateInPassiveEffects( export function validateNoSetStateInEffects(
fn: HIRFunction, fn: HIRFunction,
): Result<void, CompilerError> { ): Result<void, CompilerError> {
const setStateFunctions: Map<IdentifierId, Place> = new Map(); const setStateFunctions: Map<IdentifierId, Place> = new Map();
@@ -79,7 +81,11 @@ export function validateNoSetStateInPassiveEffects(
instr.value.kind === 'MethodCall' instr.value.kind === 'MethodCall'
? instr.value.receiver ? instr.value.receiver
: instr.value.callee; : instr.value.callee;
if (isUseEffectHookType(callee.identifier)) { if (
isUseEffectHookType(callee.identifier) ||
isUseLayoutEffectHookType(callee.identifier) ||
isUseInsertionEffectHookType(callee.identifier)
) {
const arg = instr.value.args[0]; const arg = instr.value.args[0];
if (arg !== undefined && arg.kind === 'Identifier') { if (arg !== undefined && arg.kind === 'Identifier') {
const setState = setStateFunctions.get(arg.identifier.id); const setState = setStateFunctions.get(arg.identifier.id);

View File

@@ -2,7 +2,7 @@
## Input ## Input
```javascript ```javascript
// @loggerTestOnly @validateNoSetStateInPassiveEffects // @loggerTestOnly @validateNoSetStateInEffects
import {useEffect, useState} from 'react'; import {useEffect, useState} from 'react';
function Component() { function Component() {
@@ -24,7 +24,7 @@ function Component() {
## Code ## Code
```javascript ```javascript
import { c as _c } from "react/compiler-runtime"; // @loggerTestOnly @validateNoSetStateInPassiveEffects import { c as _c } from "react/compiler-runtime"; // @loggerTestOnly @validateNoSetStateInEffects
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
function Component() { function Component() {
@@ -65,8 +65,8 @@ function _temp(s) {
## Logs ## Logs
``` ```
{"kind":"CompileError","detail":{"options":{"reason":"Calling setState directly within a useEffect causes cascading renders and is not recommended. Consider alternatives to useEffect. (https://react.dev/learn/you-might-not-need-an-effect)","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":13,"column":4,"index":272},"end":{"line":13,"column":5,"index":273},"filename":"invalid-setState-in-useEffect-transitive.ts","identifierName":"g"}}},"fnLoc":null} {"kind":"CompileError","detail":{"options":{"reason":"Calling setState directly within a useEffect causes cascading renders and is not recommended. Consider alternatives to useEffect. (https://react.dev/learn/you-might-not-need-an-effect)","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":13,"column":4,"index":265},"end":{"line":13,"column":5,"index":266},"filename":"invalid-setState-in-useEffect-transitive.ts","identifierName":"g"}}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":99},"end":{"line":16,"column":1,"index":300},"filename":"invalid-setState-in-useEffect-transitive.ts"},"fnName":"Component","memoSlots":2,"memoBlocks":2,"memoValues":2,"prunedMemoBlocks":0,"prunedMemoValues":0} {"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":92},"end":{"line":16,"column":1,"index":293},"filename":"invalid-setState-in-useEffect-transitive.ts"},"fnName":"Component","memoSlots":2,"memoBlocks":2,"memoValues":2,"prunedMemoBlocks":0,"prunedMemoValues":0}
``` ```
### Eval output ### Eval output

View File

@@ -1,4 +1,4 @@
// @loggerTestOnly @validateNoSetStateInPassiveEffects // @loggerTestOnly @validateNoSetStateInEffects
import {useEffect, useState} from 'react'; import {useEffect, useState} from 'react';
function Component() { function Component() {

View File

@@ -2,7 +2,7 @@
## Input ## Input
```javascript ```javascript
// @loggerTestOnly @validateNoSetStateInPassiveEffects // @loggerTestOnly @validateNoSetStateInEffects
import {useEffect, useState} from 'react'; import {useEffect, useState} from 'react';
function Component() { function Component() {
@@ -18,7 +18,7 @@ function Component() {
## Code ## Code
```javascript ```javascript
import { c as _c } from "react/compiler-runtime"; // @loggerTestOnly @validateNoSetStateInPassiveEffects import { c as _c } from "react/compiler-runtime"; // @loggerTestOnly @validateNoSetStateInEffects
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
function Component() { function Component() {
@@ -45,8 +45,8 @@ function _temp(s) {
## Logs ## Logs
``` ```
{"kind":"CompileError","detail":{"options":{"reason":"Calling setState directly within a useEffect causes cascading renders and is not recommended. Consider alternatives to useEffect. (https://react.dev/learn/you-might-not-need-an-effect)","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":7,"column":4,"index":187},"end":{"line":7,"column":12,"index":195},"filename":"invalid-setState-in-useEffect.ts","identifierName":"setState"}}},"fnLoc":null} {"kind":"CompileError","detail":{"options":{"reason":"Calling setState directly within a useEffect causes cascading renders and is not recommended. Consider alternatives to useEffect. (https://react.dev/learn/you-might-not-need-an-effect)","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":7,"column":4,"index":180},"end":{"line":7,"column":12,"index":188},"filename":"invalid-setState-in-useEffect.ts","identifierName":"setState"}}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":99},"end":{"line":10,"column":1,"index":232},"filename":"invalid-setState-in-useEffect.ts"},"fnName":"Component","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0} {"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":92},"end":{"line":10,"column":1,"index":225},"filename":"invalid-setState-in-useEffect.ts"},"fnName":"Component","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0}
``` ```
### Eval output ### Eval output

View File

@@ -1,4 +1,4 @@
// @loggerTestOnly @validateNoSetStateInPassiveEffects // @loggerTestOnly @validateNoSetStateInEffects
import {useEffect, useState} from 'react'; import {useEffect, useState} from 'react';
function Component() { function Component() {

View File

@@ -2,7 +2,7 @@
## Input ## Input
```javascript ```javascript
// @validateNoSetStateInPassiveEffects // @validateNoSetStateInEffects
import {useEffect, useState} from 'react'; import {useEffect, useState} from 'react';
function Component() { function Component() {
@@ -26,7 +26,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code ## Code
```javascript ```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInPassiveEffects import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
function Component() { function Component() {

View File

@@ -1,4 +1,4 @@
// @validateNoSetStateInPassiveEffects // @validateNoSetStateInEffects
import {useEffect, useState} from 'react'; import {useEffect, useState} from 'react';
function Component() { function Component() {

View File

@@ -2,7 +2,7 @@
## Input ## Input
```javascript ```javascript
// @validateNoSetStateInPassiveEffects // @validateNoSetStateInEffects
import {useEffect, useState} from 'react'; import {useEffect, useState} from 'react';
function Component() { function Component() {
@@ -23,7 +23,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code ## Code
```javascript ```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInPassiveEffects import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
function Component() { function Component() {

View File

@@ -1,4 +1,4 @@
// @validateNoSetStateInPassiveEffects // @validateNoSetStateInEffects
import {useEffect, useState} from 'react'; import {useEffect, useState} from 'react';
function Component() { function Component() {

View File

@@ -15,11 +15,11 @@ describe('parseConfigPragmaForTests()', () => {
// Validate defaults first to make sure that the parser is getting the value from the pragma, // Validate defaults first to make sure that the parser is getting the value from the pragma,
// and not just missing it and getting the default value // and not just missing it and getting the default value
expect(defaultConfig.enableUseTypeAnnotations).toBe(false); expect(defaultConfig.enableUseTypeAnnotations).toBe(false);
expect(defaultConfig.validateNoSetStateInPassiveEffects).toBe(false); expect(defaultConfig.validateNoSetStateInEffects).toBe(false);
expect(defaultConfig.validateNoSetStateInRender).toBe(true); expect(defaultConfig.validateNoSetStateInRender).toBe(true);
const config = parseConfigPragmaForTests( const config = parseConfigPragmaForTests(
'@enableUseTypeAnnotations @validateNoSetStateInPassiveEffects:true @validateNoSetStateInRender:false', '@enableUseTypeAnnotations @validateNoSetStateInEffects:true @validateNoSetStateInRender:false',
{compilationMode: defaultOptions.compilationMode}, {compilationMode: defaultOptions.compilationMode},
); );
expect(config).toEqual({ expect(config).toEqual({
@@ -28,7 +28,7 @@ describe('parseConfigPragmaForTests()', () => {
environment: { environment: {
...defaultOptions.environment, ...defaultOptions.environment,
enableUseTypeAnnotations: true, enableUseTypeAnnotations: true,
validateNoSetStateInPassiveEffects: true, validateNoSetStateInEffects: true,
validateNoSetStateInRender: false, validateNoSetStateInRender: false,
enableResetCacheOnSourceFileChanges: false, enableResetCacheOnSourceFileChanges: false,
}, },

View File

@@ -107,6 +107,12 @@ const COMPILER_OPTIONS: Partial<PluginOptions> = {
flowSuppressions: false, flowSuppressions: false,
environment: validateEnvironmentConfig({ environment: validateEnvironmentConfig({
validateRefAccessDuringRender: false, validateRefAccessDuringRender: false,
validateNoSetStateInRender: true,
validateNoSetStateInEffects: true,
validateNoJSXInTryStatements: true,
validateNoImpureFunctionsInRender: true,
validateStaticComponents: true,
validateNoFreezingKnownMutableFunctions: true,
}), }),
}; };

View File

@@ -109,6 +109,12 @@ const COMPILER_OPTIONS: Partial<PluginOptions> = {
flowSuppressions: false, flowSuppressions: false,
environment: validateEnvironmentConfig({ environment: validateEnvironmentConfig({
validateRefAccessDuringRender: false, validateRefAccessDuringRender: false,
validateNoSetStateInRender: true,
validateNoSetStateInEffects: true,
validateNoJSXInTryStatements: true,
validateNoImpureFunctionsInRender: true,
validateStaticComponents: true,
validateNoFreezingKnownMutableFunctions: true,
}), }),
}; };