mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
NOTE: this is a merged version of @mofeiZ's original PR along with my edits per offline discussion. The description is updated to reflect the latest approach. The key problem we're trying to solve with this PR is to allow developers more control over the compiler's various validations. The idea is to have a number of rules targeting a specific category of issues, such as enforcing immutability of props/state/etc or disallowing access to refs during render. We don't want to have to run the compiler again for every single rule, though, so @mofeiZ added an LRU cache that caches the full compilation output of N most recent files. The first rule to run on a given file will cause it to get cached, and then subsequent rules can pull from the cache, with each rule filtering down to its specific category of errors. For the categories, I went through and assigned a category roughly 1:1 to existing validations, and then used my judgement on some places that felt distinct enough to warrant a separate error. Every error in the compiler now has to supply both a severity (for legacy reasons) and a category (for ESLint). Each category corresponds 1:1 to a ESLint rule definition, so that the set of rules is automatically populated based on the defined categories. Categories include a flag for whether they should be in the recommended set or not. Note that as with the original version of this PR, only eslint-plugin-react-compiler is changed. We still have to update the main lint rule. ## Test Plan * Created a sample project using ESLint v9 and verified that the plugin can be configured correctly and detects errors * Edited `fixtures/eslint-v9` and introduced errors, verified that the w latest config changes in that fixture it correctly detects the errors * In the sample project, confirmed that the LRU caching is correctly caching compiler output, ie compiling files just once. Co-authored-by: Mofei Zhang <feifei0@meta.com>
265 lines
8.7 KiB
TypeScript
265 lines
8.7 KiB
TypeScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
import {CompilerDiagnostic} from '../CompilerError';
|
|
import {
|
|
FunctionExpression,
|
|
GeneratedSource,
|
|
Hole,
|
|
IdentifierId,
|
|
ObjectMethod,
|
|
Place,
|
|
SourceLocation,
|
|
SpreadPattern,
|
|
ValueKind,
|
|
ValueReason,
|
|
} from '../HIR';
|
|
import {FunctionSignature} from '../HIR/ObjectShape';
|
|
import {printSourceLocation} from '../HIR/PrintHIR';
|
|
|
|
/**
|
|
* `AliasingEffect` describes a set of "effects" that an instruction/terminal has on one or
|
|
* more values in a program. These effects include mutation of values, freezing values,
|
|
* tracking data flow between values, and other specialized cases.
|
|
*/
|
|
export type AliasingEffect =
|
|
/**
|
|
* Marks the given value and its direct aliases as frozen.
|
|
*
|
|
* Captured values are *not* considered frozen, because we cannot be sure that a previously
|
|
* captured value will still be captured at the point of the freeze.
|
|
*
|
|
* For example:
|
|
* const x = {};
|
|
* const y = [x];
|
|
* y.pop(); // y dosn't contain x anymore!
|
|
* freeze(y);
|
|
* mutate(x); // safe to mutate!
|
|
*
|
|
* The exception to this is FunctionExpressions - since it is impossible to change which
|
|
* value a function closes over[1] we can transitively freeze functions and their captures.
|
|
*
|
|
* [1] Except for `let` values that are reassigned and closed over by a function, but we
|
|
* handle this explicitly with StoreContext/LoadContext.
|
|
*/
|
|
| {kind: 'Freeze'; value: Place; reason: ValueReason}
|
|
/**
|
|
* Mutate the value and any direct aliases (not captures). Errors if the value is not mutable.
|
|
*/
|
|
| {kind: 'Mutate'; value: Place; reason?: MutationReason | null}
|
|
/**
|
|
* Mutate the value and any direct aliases (not captures), but only if the value is known mutable.
|
|
* This should be rare.
|
|
*
|
|
* TODO: this is only used for IteratorNext, but even then MutateTransitiveConditionally is more
|
|
* correct for iterators of unknown types.
|
|
*/
|
|
| {kind: 'MutateConditionally'; value: Place}
|
|
/**
|
|
* Mutate the value, any direct aliases, and any transitive captures. Errors if the value is not mutable.
|
|
*/
|
|
| {kind: 'MutateTransitive'; value: Place}
|
|
/**
|
|
* Mutates any of the value, its direct aliases, and its transitive captures that are mutable.
|
|
*/
|
|
| {kind: 'MutateTransitiveConditionally'; value: Place}
|
|
/**
|
|
* Records information flow from `from` to `into` in cases where local mutation of the destination
|
|
* will *not* mutate the source:
|
|
*
|
|
* - Capture a -> b and Mutate(b) X=> (does not imply) Mutate(a)
|
|
* - Capture a -> b and MutateTransitive(b) => (does imply) Mutate(a)
|
|
*
|
|
* Example: `array.push(item)`. Information from item is captured into array, but there is not a
|
|
* direct aliasing, and local mutations of array will not modify item.
|
|
*/
|
|
| {kind: 'Capture'; from: Place; into: Place}
|
|
/**
|
|
* Records information flow from `from` to `into` in cases where local mutation of the destination
|
|
* *will* mutate the source:
|
|
*
|
|
* - Alias a -> b and Mutate(b) => (does imply) Mutate(a)
|
|
* - Alias a -> b and MutateTransitive(b) => (does imply) Mutate(a)
|
|
*
|
|
* Example: `c = identity(a)`. We don't know what `identity()` returns so we can't use Assign.
|
|
* But we have to assume that it _could_ be returning its input, such that a local mutation of
|
|
* c could be mutating a.
|
|
*/
|
|
| {kind: 'Alias'; from: Place; into: Place}
|
|
|
|
/**
|
|
* Indicates the potential for information flow from `from` to `into`. This is used for a specific
|
|
* case: functions with unknown signatures. If the compiler sees a call such as `foo(x)`, it has to
|
|
* consider several possibilities (which may depend on the arguments):
|
|
* - foo(x) returns a new mutable value that does not capture any information from x.
|
|
* - foo(x) returns a new mutable value that *does* capture information from x.
|
|
* - foo(x) returns x itself, ie foo is the identity function
|
|
*
|
|
* The same is true of functions that take multiple arguments: `cond(a, b, c)` could conditionally
|
|
* return b or c depending on the value of a.
|
|
*
|
|
* To represent this case, MaybeAlias represents the fact that an aliasing relationship could exist.
|
|
* Any mutations that flow through this relationship automatically become conditional.
|
|
*/
|
|
| {kind: 'MaybeAlias'; from: Place; into: Place}
|
|
|
|
/**
|
|
* Records direct assignment: `into = from`.
|
|
*/
|
|
| {kind: 'Assign'; from: Place; into: Place}
|
|
/**
|
|
* Creates a value of the given type at the given place
|
|
*/
|
|
| {kind: 'Create'; into: Place; value: ValueKind; reason: ValueReason}
|
|
/**
|
|
* Creates a new value with the same kind as the starting value.
|
|
*/
|
|
| {kind: 'CreateFrom'; from: Place; into: Place}
|
|
/**
|
|
* Immutable data flow, used for escape analysis. Does not influence mutable range analysis:
|
|
*/
|
|
| {kind: 'ImmutableCapture'; from: Place; into: Place}
|
|
/**
|
|
* Calls the function at the given place with the given arguments either captured or aliased,
|
|
* and captures/aliases the result into the given place.
|
|
*/
|
|
| {
|
|
kind: 'Apply';
|
|
receiver: Place;
|
|
function: Place;
|
|
mutatesFunction: boolean;
|
|
args: Array<Place | SpreadPattern | Hole>;
|
|
into: Place;
|
|
signature: FunctionSignature | null;
|
|
loc: SourceLocation;
|
|
}
|
|
/**
|
|
* Constructs a function value with the given captures. The mutability of the function
|
|
* will be determined by the mutability of the capture values when evaluated.
|
|
*/
|
|
| {
|
|
kind: 'CreateFunction';
|
|
captures: Array<Place>;
|
|
function: FunctionExpression | ObjectMethod;
|
|
into: Place;
|
|
}
|
|
/**
|
|
* Mutation of a value known to be immutable
|
|
*/
|
|
| {kind: 'MutateFrozen'; place: Place; error: CompilerDiagnostic}
|
|
/**
|
|
* Mutation of a global
|
|
*/
|
|
| {
|
|
kind: 'MutateGlobal';
|
|
place: Place;
|
|
error: CompilerDiagnostic;
|
|
}
|
|
/**
|
|
* Indicates a side-effect that is not safe during render
|
|
*/
|
|
| {kind: 'Impure'; place: Place; error: CompilerDiagnostic}
|
|
/**
|
|
* Indicates that a given place is accessed during render. Used to distingush
|
|
* hook arguments that are known to be called immediately vs those used for
|
|
* event handlers/effects, and for JSX values known to be called during render
|
|
* (tags, children) vs those that may be events/effect (other props).
|
|
*/
|
|
| {
|
|
kind: 'Render';
|
|
place: Place;
|
|
};
|
|
|
|
export type MutationReason = {kind: 'AssignCurrentProperty'};
|
|
|
|
export function hashEffect(effect: AliasingEffect): string {
|
|
switch (effect.kind) {
|
|
case 'Apply': {
|
|
return [
|
|
effect.kind,
|
|
effect.receiver.identifier.id,
|
|
effect.function.identifier.id,
|
|
effect.mutatesFunction,
|
|
effect.args
|
|
.map(a => {
|
|
if (a.kind === 'Hole') {
|
|
return '';
|
|
} else if (a.kind === 'Identifier') {
|
|
return a.identifier.id;
|
|
} else {
|
|
return `...${a.place.identifier.id}`;
|
|
}
|
|
})
|
|
.join(','),
|
|
effect.into.identifier.id,
|
|
].join(':');
|
|
}
|
|
case 'CreateFrom':
|
|
case 'ImmutableCapture':
|
|
case 'Assign':
|
|
case 'Alias':
|
|
case 'Capture':
|
|
case 'MaybeAlias': {
|
|
return [
|
|
effect.kind,
|
|
effect.from.identifier.id,
|
|
effect.into.identifier.id,
|
|
].join(':');
|
|
}
|
|
case 'Create': {
|
|
return [
|
|
effect.kind,
|
|
effect.into.identifier.id,
|
|
effect.value,
|
|
effect.reason,
|
|
].join(':');
|
|
}
|
|
case 'Freeze': {
|
|
return [effect.kind, effect.value.identifier.id, effect.reason].join(':');
|
|
}
|
|
case 'Impure':
|
|
case 'Render': {
|
|
return [effect.kind, effect.place.identifier.id].join(':');
|
|
}
|
|
case 'MutateFrozen':
|
|
case 'MutateGlobal': {
|
|
return [
|
|
effect.kind,
|
|
effect.place.identifier.id,
|
|
effect.error.severity,
|
|
effect.error.reason,
|
|
effect.error.description,
|
|
printSourceLocation(effect.error.primaryLocation() ?? GeneratedSource),
|
|
].join(':');
|
|
}
|
|
case 'Mutate':
|
|
case 'MutateConditionally':
|
|
case 'MutateTransitive':
|
|
case 'MutateTransitiveConditionally': {
|
|
return [effect.kind, effect.value.identifier.id].join(':');
|
|
}
|
|
case 'CreateFunction': {
|
|
return [
|
|
effect.kind,
|
|
effect.into.identifier.id,
|
|
// return places are a unique way to identify functions themselves
|
|
effect.function.loweredFunc.func.returns.identifier.id,
|
|
effect.captures.map(p => p.identifier.id).join(','),
|
|
].join(':');
|
|
}
|
|
}
|
|
}
|
|
|
|
export type AliasingSignature = {
|
|
receiver: IdentifierId;
|
|
params: Array<IdentifierId>;
|
|
rest: IdentifierId | null;
|
|
returns: IdentifierId;
|
|
effects: Array<AliasingEffect>;
|
|
temporaries: Array<Place>;
|
|
};
|