Compare commits

..

1 Commits

Author SHA1 Message Date
Joe Savona
a2bc92a8be [compiler] Handle empty list of eslint suppression rules 2025-08-28 14:52:57 -07:00
8 changed files with 222 additions and 235 deletions

View File

@@ -135,7 +135,12 @@ export type PluginOptions = {
*/
eslintSuppressionRules: Array<string> | null | undefined;
/**
* Whether to report "suppression" errors for Flow suppressions. If false, suppression errors
* are only emitted for ESLint suppressions
*/
flowSuppressions: boolean;
/*
* Ignore 'use no forget' annotations. Helpful during testing but should not be used in production.
*/

View File

@@ -87,12 +87,18 @@ export function findProgramSuppressions(
let enableComment: t.Comment | null = null;
let source: SuppressionSource | null = null;
const rulePattern = `(${ruleNames.join('|')})`;
const disableNextLinePattern = new RegExp(
`eslint-disable-next-line ${rulePattern}`,
);
const disablePattern = new RegExp(`eslint-disable ${rulePattern}`);
const enablePattern = new RegExp(`eslint-enable ${rulePattern}`);
let disableNextLinePattern: RegExp | null = null;
let disablePattern: RegExp | null = null;
let enablePattern: RegExp | null = null;
if (ruleNames.length !== 0) {
const rulePattern = `(${ruleNames.join('|')})`;
disableNextLinePattern = new RegExp(
`eslint-disable-next-line ${rulePattern}`,
);
disablePattern = new RegExp(`eslint-disable ${rulePattern}`);
enablePattern = new RegExp(`eslint-enable ${rulePattern}`);
}
const flowSuppressionPattern = new RegExp(
'\\$(FlowFixMe\\w*|FlowExpectedError|FlowIssue)\\[react\\-rule',
);
@@ -108,6 +114,7 @@ export function findProgramSuppressions(
* CommentLine within the block.
*/
disableComment == null &&
disableNextLinePattern != null &&
disableNextLinePattern.test(comment.value)
) {
disableComment = comment;
@@ -125,12 +132,16 @@ export function findProgramSuppressions(
source = 'Flow';
}
if (disablePattern.test(comment.value)) {
if (disablePattern != null && disablePattern.test(comment.value)) {
disableComment = comment;
source = 'Eslint';
}
if (enablePattern.test(comment.value) && source === 'Eslint') {
if (
enablePattern != null &&
enablePattern.test(comment.value) &&
source === 'Eslint'
) {
enableComment = comment;
}

View File

@@ -25,6 +25,7 @@ import {
IdentifierId,
Instruction,
InstructionKind,
InstructionValue,
isArrayType,
isJsxType,
isMapType,
@@ -56,6 +57,7 @@ import {
printAliasingSignature,
printIdentifier,
printInstruction,
printInstructionValue,
printPlace,
printSourceLocation,
} from '../HIR/PrintHIR';
@@ -106,11 +108,11 @@ export function inferMutationAliasingEffects(
const statesByBlock: Map<BlockId, InferenceState> = new Map();
for (const ref of fn.context) {
const value: AliasingEffect = {
kind: 'Create',
into: ref,
value: ValueKind.Context,
reason: ValueReason.Other,
// TODO: using InstructionValue as a bit of a hack, but it's pragmatic
const value: InstructionValue = {
kind: 'ObjectExpression',
properties: [],
loc: ref.loc,
};
initialState.initialize(value, {
kind: ValueKind.Context,
@@ -143,11 +145,10 @@ export function inferMutationAliasingEffects(
}
if (ref != null) {
const place = ref.kind === 'Identifier' ? ref : ref.place;
const value: AliasingEffect = {
kind: 'Create',
into: place,
value: ValueKind.Mutable,
reason: ValueReason.Other,
const value: InstructionValue = {
kind: 'ObjectExpression',
properties: [],
loc: place.loc,
};
initialState.initialize(value, {
kind: ValueKind.Mutable,
@@ -264,6 +265,8 @@ function findHoistedContextDeclarations(
class Context {
internedEffects: Map<string, AliasingEffect> = new Map();
instructionSignatureCache: Map<Instruction, InstructionSignature> = new Map();
effectInstructionValueCache: Map<AliasingEffect, InstructionValue> =
new Map();
applySignatureCache: Map<
AliasingSignature,
Map<AliasingEffect, Array<AliasingEffect> | null>
@@ -315,11 +318,10 @@ function inferParam(
paramKind: AbstractValue,
): void {
const place = param.kind === 'Identifier' ? param : param.place;
const value: AliasingEffect = {
kind: 'Create',
into: place,
value: paramKind.kind,
reason: ValueReason.Other,
const value: InstructionValue = {
kind: 'Primitive',
loc: place.loc,
value: undefined,
};
initialState.initialize(value, paramKind);
initialState.define(place, value);
@@ -540,11 +542,20 @@ function applyEffect(
});
initialized.add(effect.into.identifier.id);
state.initialize(effect, {
let value = context.effectInstructionValueCache.get(effect);
if (value == null) {
value = {
kind: 'ObjectExpression',
properties: [],
loc: effect.into.loc,
};
context.effectInstructionValueCache.set(effect, value);
}
state.initialize(value, {
kind: effect.value,
reason: new Set([effect.reason]),
});
state.define(effect.into, effect);
state.define(effect.into, value);
effects.push(effect);
break;
}
@@ -571,11 +582,20 @@ function applyEffect(
initialized.add(effect.into.identifier.id);
const fromValue = state.kind(effect.from);
state.initialize(effect, {
let value = context.effectInstructionValueCache.get(effect);
if (value == null) {
value = {
kind: 'ObjectExpression',
properties: [],
loc: effect.into.loc,
};
context.effectInstructionValueCache.set(effect, value);
}
state.initialize(value, {
kind: fromValue.kind,
reason: new Set(fromValue.reason),
});
state.define(effect.into, effect);
state.define(effect.into, value);
switch (fromValue.kind) {
case ValueKind.Primitive:
case ValueKind.Global: {
@@ -663,11 +683,11 @@ function applyEffect(
operand.effect = Effect.Read;
}
}
state.initialize(effect, {
state.initialize(effect.function, {
kind: isMutable ? ValueKind.Mutable : ValueKind.Frozen,
reason: new Set([]),
});
state.define(effect.into, effect);
state.define(effect.into, effect.function);
for (const capture of effect.captures) {
applyEffect(
context,
@@ -773,20 +793,38 @@ function applyEffect(
initialized,
effects,
);
state.initialize(effect, {
let value = context.effectInstructionValueCache.get(effect);
if (value == null) {
value = {
kind: 'Primitive',
value: undefined,
loc: effect.from.loc,
};
context.effectInstructionValueCache.set(effect, value);
}
state.initialize(value, {
kind: fromKind,
reason: new Set(fromValue.reason),
});
state.define(effect.into, effect);
state.define(effect.into, value);
break;
}
case ValueKind.Global:
case ValueKind.Primitive: {
state.initialize(effect, {
let value = context.effectInstructionValueCache.get(effect);
if (value == null) {
value = {
kind: 'Primitive',
value: undefined,
loc: effect.from.loc,
};
context.effectInstructionValueCache.set(effect, value);
}
state.initialize(value, {
kind: fromKind,
reason: new Set(fromValue.reason),
});
state.define(effect.into, effect);
state.define(effect.into, value);
break;
}
default: {
@@ -801,15 +839,14 @@ function applyEffect(
const functionValues = state.values(effect.function);
if (
functionValues.length === 1 &&
functionValues[0].kind === 'CreateFunction' &&
functionValues[0].function.kind === 'FunctionExpression' &&
functionValues[0].function.loweredFunc.func.aliasingEffects != null
functionValues[0].kind === 'FunctionExpression' &&
functionValues[0].loweredFunc.func.aliasingEffects != null
) {
/*
* We're calling a locally declared function, we already know it's effects!
* We just have to substitute in the args for the params
*/
const functionExpr = functionValues[0].function;
const functionExpr = functionValues[0];
let signature = context.functionSignatureCache.get(functionExpr);
if (signature == null) {
signature = buildSignatureFromFunctionExpression(
@@ -1099,19 +1136,19 @@ class InferenceState {
#isFunctionExpression: boolean;
// The kind of each value, based on its allocation site
#values: Map<AliasingEffect, AbstractValue>;
#values: Map<InstructionValue, AbstractValue>;
/*
* The set of values pointed to by each identifier. This is a set
* to accomodate phi points (where a variable may have different
* values from different control flow paths).
*/
#variables: Map<IdentifierId, Set<AliasingEffect>>;
#variables: Map<IdentifierId, Set<InstructionValue>>;
constructor(
env: Environment,
isFunctionExpression: boolean,
values: Map<AliasingEffect, AbstractValue>,
variables: Map<IdentifierId, Set<AliasingEffect>>,
values: Map<InstructionValue, AbstractValue>,
variables: Map<IdentifierId, Set<InstructionValue>>,
) {
this.env = env;
this.#isFunctionExpression = isFunctionExpression;
@@ -1131,11 +1168,18 @@ class InferenceState {
}
// (Re)initializes a @param value with its default @param kind.
initialize(value: AliasingEffect, kind: AbstractValue): void {
initialize(value: InstructionValue, kind: AbstractValue): void {
CompilerError.invariant(value.kind !== 'LoadLocal', {
reason:
'[InferMutationAliasingEffects] Expected all top-level identifiers to be defined as variables, not values',
description: null,
loc: value.loc,
suggestions: null,
});
this.#values.set(value, kind);
}
values(place: Place): Array<AliasingEffect> {
values(place: Place): Array<InstructionValue> {
const values = this.#variables.get(place.identifier.id);
CompilerError.invariant(values != null, {
reason: `[InferMutationAliasingEffects] Expected value kind to be initialized`,
@@ -1198,13 +1242,13 @@ class InferenceState {
}
// Defines (initializing or updating) a variable with a specific kind of value.
define(place: Place, value: AliasingEffect): void {
define(place: Place, value: InstructionValue): void {
CompilerError.invariant(this.#values.has(value), {
reason: `[InferMutationAliasingEffects] Expected value to be initialized at '${printSourceLocation(
place.loc,
value.loc,
)}'`,
description: printAliasingEffect(value),
loc: place.loc,
description: printInstructionValue(value),
loc: value.loc,
suggestions: null,
});
this.#variables.set(place.identifier.id, new Set([value]));
@@ -1244,17 +1288,17 @@ class InferenceState {
}
}
freezeValue(value: AliasingEffect, reason: ValueReason): void {
freezeValue(value: InstructionValue, reason: ValueReason): void {
this.#values.set(value, {
kind: ValueKind.Frozen,
reason: new Set([reason]),
});
if (
value.kind === 'CreateFunction' &&
value.kind === 'FunctionExpression' &&
(this.env.config.enablePreserveExistingMemoizationGuarantees ||
this.env.config.enableTransitivelyFreezeFunctionExpressions)
) {
for (const place of value.function.loweredFunc.func.context) {
for (const place of value.loweredFunc.func.context) {
this.freeze(place, reason);
}
}
@@ -1329,8 +1373,8 @@ class InferenceState {
* termination.
*/
merge(other: InferenceState): InferenceState | null {
let nextValues: Map<AliasingEffect, AbstractValue> | null = null;
let nextVariables: Map<IdentifierId, Set<AliasingEffect>> | null = null;
let nextValues: Map<InstructionValue, AbstractValue> | null = null;
let nextVariables: Map<IdentifierId, Set<InstructionValue>> | null = null;
for (const [id, thisValue] of this.#values) {
const otherValue = other.#values.get(id);
@@ -1354,7 +1398,7 @@ class InferenceState {
for (const [id, thisValues] of this.#variables) {
const otherValues = other.#variables.get(id);
if (otherValues !== undefined) {
let mergedValues: Set<AliasingEffect> | null = null;
let mergedValues: Set<InstructionValue> | null = null;
for (const otherValue of otherValues) {
if (!thisValues.has(otherValue)) {
mergedValues = mergedValues ?? new Set(thisValues);
@@ -1407,8 +1451,8 @@ class InferenceState {
*/
debug(): any {
const result: any = {values: {}, variables: {}};
const objects: Map<AliasingEffect, number> = new Map();
function identify(value: AliasingEffect): number {
const objects: Map<InstructionValue, number> = new Map();
function identify(value: InstructionValue): number {
let id = objects.get(value);
if (id == null) {
id = objects.size;
@@ -1420,7 +1464,7 @@ class InferenceState {
const id = identify(value);
result.values[id] = {
abstract: this.debugAbstractValue(kind),
value: printAliasingEffect(value),
value: printInstructionValue(value),
};
}
for (const [variable, values] of this.#variables) {
@@ -1437,7 +1481,7 @@ class InferenceState {
}
inferPhi(phi: Phi): void {
const values: Set<AliasingEffect> = new Set();
const values: Set<InstructionValue> = new Set();
for (const [_, operand] of phi.operands) {
const operandValues = this.#variables.get(operand.identifier.id);
// This is a backedge that will be handled later by State.merge
@@ -2303,9 +2347,8 @@ function areArgumentsImmutableAndNonMutating(
const values = state.values(place);
for (const value of values) {
if (
value.kind === 'CreateFunction' &&
value.function.kind === 'FunctionExpression' &&
value.function.loweredFunc.func.params.some(param => {
value.kind === 'FunctionExpression' &&
value.loweredFunc.func.params.some(param => {
const place = param.kind === 'Identifier' ? param : param.place;
const range = place.identifier.mutableRange;
return range.end > range.start + 1;
@@ -2498,47 +2541,10 @@ function computeEffectsForSignature(
break;
}
case 'CreateFunction': {
const applyInto = substitutions.get(effect.into.identifier.id);
if (applyInto == null || applyInto.length !== 1) {
return null;
}
const captures: Array<Place> = [];
for (let i = 0; i < effect.captures.length; i++) {
const substitution = substitutions.get(
effect.captures[i].identifier.id,
);
if (substitution == null || substitution.length !== 1) {
return null;
}
captures.push(substitution[0]);
}
const context: Array<Place> = [];
const originalContext = effect.function.loweredFunc.func.context;
for (let i = 0; i < originalContext.length; i++) {
const substitution = substitutions.get(
originalContext[i].identifier.id,
);
if (substitution == null || substitution.length !== 1) {
return null;
}
context.push(substitution[0]);
}
effects.push({
kind: 'CreateFunction',
into: applyInto[0],
function: {
...effect.function,
loweredFunc: {
...effect.function.loweredFunc,
func: {
...effect.function.loweredFunc.func,
context,
},
},
},
captures,
CompilerError.throwTodo({
reason: `Support CreateFrom effects in signatures`,
loc: receiver.loc,
});
break;
}
default: {
assertExhaustive(

View File

@@ -141,7 +141,7 @@ export function inferMutationAliasingRanges(
} else if (effect.kind === 'CreateFunction') {
state.create(effect.into, {
kind: 'Function',
effect,
function: effect.function.loweredFunc.func,
});
} else if (effect.kind === 'CreateFrom') {
state.createFrom(index++, effect.from, effect.into);
@@ -156,7 +156,7 @@ export function inferMutationAliasingRanges(
* invariant here.
*/
if (!state.nodes.has(effect.into.identifier)) {
state.create(effect.into, {kind: 'Assign'});
state.create(effect.into, {kind: 'Object'});
}
state.assign(index++, effect.from, effect.into);
} else if (effect.kind === 'Alias') {
@@ -474,99 +474,6 @@ export function inferMutationAliasingRanges(
}
}
const tracked: Array<Place> = [];
for (const param of [...fn.params, ...fn.context, fn.returns]) {
const place = param.kind === 'Identifier' ? param : param.place;
tracked.push(place);
}
const returned: Set<Node> = new Set();
const queue: Array<Node> = [state.nodes.get(fn.returns.identifier)!];
const seen: Set<Node> = new Set();
while (queue.length !== 0) {
const node = queue.pop()!;
if (seen.has(node)) {
continue;
}
seen.add(node);
for (const id of node.aliases.keys()) {
queue.push(state.nodes.get(id)!);
}
for (const id of node.createdFrom.keys()) {
queue.push(state.nodes.get(id)!);
}
if (node.id.id === fn.returns.identifier.id) {
continue;
}
switch (node.value.kind) {
case 'Assign':
case 'CreateFrom': {
break;
}
case 'Phi':
case 'Object':
case 'Function': {
returned.add(node);
break;
}
default: {
assertExhaustive(
node.value,
`Unexpected node value kind '${(node.value as any).kind}'`,
);
}
}
}
const returnedValues = [...returned];
if (
returnedValues.length === 1 &&
returnedValues[0].value.kind === 'Object' &&
tracked.some(place => place.identifier.id === returnedValues[0].id.id)
) {
const from = tracked.find(
place => place.identifier.id === returnedValues[0].id.id,
)!;
functionEffects.push({
kind: 'Assign',
from,
into: fn.returns,
});
} else if (
returnedValues.length === 1 &&
returnedValues[0].value.kind === 'Function'
) {
const outerContext = new Set(fn.context.map(p => p.identifier.id));
const effect = returnedValues[0].value.effect;
functionEffects.push({
kind: 'CreateFunction',
function: {
...effect.function,
loweredFunc: {
func: {
...effect.function.loweredFunc.func,
context: effect.function.loweredFunc.func.context.filter(p =>
outerContext.has(p.identifier.id),
),
},
},
},
captures: effect.captures.filter(p => outerContext.has(p.identifier.id)),
into: fn.returns,
});
} else {
const returns = fn.returns.identifier;
functionEffects.push({
kind: 'Create',
into: fn.returns,
value: isPrimitiveType(returns)
? ValueKind.Primitive
: isJsxType(returns.type)
? ValueKind.Frozen
: ValueKind.Mutable,
reason: ValueReason.KnownReturnSignature,
});
}
/**
* Part 3
* Finish populating the externally visible effects. Above we bubble-up the side effects
@@ -574,12 +481,28 @@ export function inferMutationAliasingRanges(
* Here we populate an effect to create the return value as well as populating alias/capture
* effects for how data flows between the params, context vars, and return.
*/
const returns = fn.returns.identifier;
functionEffects.push({
kind: 'Create',
into: fn.returns,
value: isPrimitiveType(returns)
? ValueKind.Primitive
: isJsxType(returns.type)
? ValueKind.Frozen
: ValueKind.Mutable,
reason: ValueReason.KnownReturnSignature,
});
/**
* Determine precise data-flow effects by simulating transitive mutations of the params/
* captures and seeing what other params/context variables are affected. Anything that
* would be transitively mutated needs a capture relationship.
*/
const tracked: Array<Place> = [];
const ignoredErrors = new CompilerError();
for (const param of [...fn.params, ...fn.context, fn.returns]) {
const place = param.kind === 'Identifier' ? param : param.place;
tracked.push(place);
}
for (const into of tracked) {
const mutationIndex = index++;
state.mutate(
@@ -665,14 +588,9 @@ type Node = {
lastMutated: number;
mutationReason: MutationReason | null;
value:
| {kind: 'Assign'}
| {kind: 'CreateFrom'}
| {kind: 'Object'}
| {kind: 'Phi'}
| {
kind: 'Function';
effect: Extract<AliasingEffect, {kind: 'CreateFunction'}>;
};
| {kind: 'Function'; function: HIRFunction};
};
class AliasingState {
nodes: Map<Identifier, Node> = new Map();
@@ -694,7 +612,7 @@ class AliasingState {
}
createFrom(index: number, from: Place, into: Place): void {
this.create(into, {kind: 'CreateFrom'});
this.create(into, {kind: 'Object'});
const fromNode = this.nodes.get(from.identifier);
const toNode = this.nodes.get(into.identifier);
if (fromNode == null || toNode == null) {
@@ -756,10 +674,7 @@ class AliasingState {
continue;
}
if (node.value.kind === 'Function') {
appendFunctionErrors(
errors,
node.value.effect.function.loweredFunc.func,
);
appendFunctionErrors(errors, node.value.function);
}
for (const [alias, when] of node.createdFrom) {
if (when >= index) {
@@ -823,10 +738,7 @@ class AliasingState {
node.transitive == null &&
node.local == null
) {
appendFunctionErrors(
errors,
node.value.effect.function.loweredFunc.func,
);
appendFunctionErrors(errors, node.value.function);
}
if (transitive) {
if (node.transitive == null || node.transitive.kind < kind) {

View File

@@ -0,0 +1,53 @@
## Input
```javascript
// @eslintSuppressionRules:[]
// The suppression here shouldn't cause compilation to get skipped
// Previously we had a bug where an empty list of suppressions would
// create a regexp that matched any suppression
function Component(props) {
'use forget';
// eslint-disable-next-line foo/not-react-related
return <div>{props.text}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{text: 'Hello'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @eslintSuppressionRules:[]
// The suppression here shouldn't cause compilation to get skipped
// Previously we had a bug where an empty list of suppressions would
// create a regexp that matched any suppression
function Component(props) {
"use forget";
const $ = _c(2);
let t0;
if ($[0] !== props.text) {
t0 = <div>{props.text}</div>;
$[0] = props.text;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ text: "Hello" }],
};
```
### Eval output
(kind: ok) <div>Hello</div>

View File

@@ -0,0 +1,15 @@
// @eslintSuppressionRules:[]
// The suppression here shouldn't cause compilation to get skipped
// Previously we had a bug where an empty list of suppressions would
// create a regexp that matched any suppression
function Component(props) {
'use forget';
// eslint-disable-next-line foo/not-react-related
return <div>{props.text}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{text: 'Hello'}],
};

View File

@@ -1,7 +0,0 @@
function Component() {
const f = () => () => {
global.property = true;
};
f()();
return <div>Ooops</div>;
}

View File

@@ -55,7 +55,7 @@ import { makeArray, Stringify, useIdentity } from "shared-runtime";
*/
function Foo(t0) {
"use memo";
const $ = _c(5);
const $ = _c(3);
const { b } = t0;
const fnFactory = () => () => {
@@ -66,26 +66,18 @@ function Foo(t0) {
useIdentity();
const fn = fnFactory();
let t1;
if ($[0] !== b) {
t1 = makeArray(b);
$[0] = b;
$[1] = t1;
} else {
t1 = $[1];
}
const arr = t1;
const arr = makeArray(b);
fn(arr);
let t2;
if ($[2] !== arr || $[3] !== myVar) {
t2 = <Stringify cb={myVar} value={arr} shouldInvokeFns={true} />;
$[2] = arr;
$[3] = myVar;
$[4] = t2;
let t1;
if ($[0] !== arr || $[1] !== myVar) {
t1 = <Stringify cb={myVar} value={arr} shouldInvokeFns={true} />;
$[0] = arr;
$[1] = myVar;
$[2] = t1;
} else {
t2 = $[4];
t1 = $[2];
}
return t2;
return t1;
}
function _temp2() {
return console.log("b");