Compare commits

..

2 Commits

Author SHA1 Message Date
Mofei Zhang
8419fbcda3 [compiler] Standarize CompilerDiagnostic descriptions (trailing periods)
Separated this out into its own PR since it's really a cosmetic change.

CompilerErrorDetail and CompilerDiagnostic now both expect `reason`/`category` and `description` to *not* have trailing periods.
2025-08-11 15:25:50 -07:00
Mofei Zhang
8c0829bda4 [compiler] Aggregate error reporting, separate eslint rules
All error messages that are not invariants, todos, or config validation errors are now moved to a registry.  This helps us check that error messages and linked documentation is uniform and up to date. This also lets us link error codes to linter reporting categories and break the single `react-compiler` rule up into many smaller rules.


Changes to error reporting in React Compiler:
- instead of writing raw error message, add a new ErrorCode to `CompilerErrorCodes`
- create errors by constructing a `CompilerErrorDetailOptions` with the correct error code, or calling `CompilerErrorDetail.fromCode()` / `CompilerDiagnostic.fromCode()`.

Changes to linter:
- `react-compiler` lint rule is now broken up into many smaller lint rules, each with their own test cases.
- linting no longer fails early when a non-fatal error is find. Instead, we now log and move on to lint for more errors!

Todos:
- Codebases previously using the eslint plugin may already have `react-compiler` eslint suppressions. Since this PR removes this rule, these codebases may get new eslint warnings on already-suppressed ranges. Some options I can think of:
  - Force everyone to add the new suppressions
  - Release a script to rewrite previous suppressions to the new suppressions. Codebases that had incomplete suppressions / waited to update may benefit here, as this still surfaces errors on previously-unseen suppressions
  - Hard code logic in our eslint plugin to avoid reporting if we see a `eslint-disable-next-line react-compiler`. This doesn't feel ideal
- Align on eslint plugin rule names and error code mappings
  - I aggregated a few error codes here. For example, reassigning and modifying local variables after render now have the same error code + message. See changes fixtures for more examples
- More test cases for linter rules. Will add these once lint rules are more finalized
2025-08-11 13:48:58 -07:00
588 changed files with 9588 additions and 27803 deletions

View File

@@ -1,7 +1,7 @@
{
"packages": ["packages/react", "packages/react-dom", "packages/react-server-dom-webpack", "packages/scheduler"],
"buildCommand": "download-build-in-codesandbox-ci",
"node": "20",
"node": "18",
"publishDirectory": {
"react": "build/oss-experimental/react",
"react-dom": "build/oss-experimental/react-dom",

View File

@@ -28,6 +28,3 @@ packages/react-devtools-shared/src/hooks/__tests__/__source__/__untransformed__/
packages/react-devtools-shell/dist
packages/react-devtools-timeline/dist
packages/react-devtools-timeline/static
# Imported third-party Flow types
flow-typed/

View File

@@ -468,7 +468,6 @@ module.exports = {
files: ['packages/react-server-dom-webpack/**/*.js'],
globals: {
__webpack_chunk_load__: 'readonly',
__webpack_get_script_filename__: 'readonly',
__webpack_require__: 'readonly',
},
},
@@ -547,10 +546,13 @@ module.exports = {
},
globals: {
$Call: 'readonly',
$ElementType: 'readonly',
$Flow$ModuleRef: 'readonly',
$FlowFixMe: 'readonly',
$Keys: 'readonly',
$NonMaybeType: 'readonly',
$PropertyType: 'readonly',
$ReadOnly: 'readonly',
$ReadOnlyArray: 'readonly',
$ArrayBufferView: 'readonly',
@@ -565,7 +567,6 @@ module.exports = {
BigInt: 'readonly',
BigInt64Array: 'readonly',
BigUint64Array: 'readonly',
CacheType: 'readonly',
Class: 'readonly',
ClientRect: 'readonly',
CopyInspectedElementPath: 'readonly',
@@ -580,15 +581,13 @@ module.exports = {
IteratorResult: 'readonly',
JSONValue: 'readonly',
JSResourceReference: 'readonly',
mixin$Animatable: 'readonly',
MouseEventHandler: 'readonly',
NavigateEvent: 'readonly',
PerformanceMeasureOptions: 'readonly',
PropagationPhases: 'readonly',
PropertyDescriptor: 'readonly',
PropertyDescriptorMap: 'readonly',
Proxy$traps: 'readonly',
React$AbstractComponent: 'readonly',
React$Component: 'readonly',
React$ComponentType: 'readonly',
React$Config: 'readonly',
React$Context: 'readonly',
React$Element: 'readonly',
@@ -620,6 +619,7 @@ module.exports = {
PropertyIndexedKeyframes: 'readonly',
KeyframeAnimationOptions: 'readonly',
GetAnimationsOptions: 'readonly',
Animatable: 'readonly',
ScrollTimeline: 'readonly',
EventListenerOptionsOrUseCapture: 'readonly',
FocusOptions: 'readonly',

View File

@@ -57,6 +57,8 @@ jobs:
key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }}
- run: npx playwright install --with-deps chromium
if: steps.cache_playwright_browsers.outputs.cache-hit != 'true'
- run: npx playwright install-deps
if: steps.cache_playwright_browsers.outputs.cache-hit == 'true'
- run: CI=true yarn test
- run: ls -R test-results
if: '!cancelled()'

View File

@@ -316,7 +316,7 @@ jobs:
if: steps.node_modules.outputs.cache-hit != 'true'
- run: ./scripts/react-compiler/build-compiler.sh && ./scripts/react-compiler/link-compiler.sh
- run: yarn workspace eslint-plugin-react-hooks test
# ----- BUILD -----
build_and_lint:
name: yarn build and lint
@@ -811,18 +811,9 @@ jobs:
pattern: _build_*
path: build
merge-multiple: true
- name: Check Playwright version
id: playwright_version
run: echo "playwright_version=$(npm ls @playwright/test | grep @playwright | sed 's/.*@//' | head -1)" >> "$GITHUB_OUTPUT"
- name: Cache Playwright Browsers for version ${{ steps.playwright_version.outputs.playwright_version }}
id: cache_playwright_browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }}
- name: Playwright install deps
if: steps.cache_playwright_browsers.outputs.cache-hit != 'true'
run: npx playwright install --with-deps chromium
- run: |
npx playwright install
sudo npx playwright install-deps
- run: ./scripts/ci/run_devtools_e2e_tests.js
env:
RELEASE_CHANNEL: experimental

View File

@@ -18,7 +18,6 @@ jobs:
permissions:
# Used to create a review and close PRs
pull-requests: write
contents: write
steps:
- name: Close PR
uses: actions/github-script@v7

View File

@@ -8,7 +8,6 @@ module.exports = {
'@babel/plugin-syntax-jsx',
'@babel/plugin-transform-flow-strip-types',
['@babel/plugin-transform-class-properties', {loose: true}],
['@babel/plugin-transform-private-methods', {loose: true}],
'@babel/plugin-transform-classes',
],
presets: [

View File

@@ -1,101 +0,0 @@
/**
* 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 MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
import type {editor} from 'monaco-editor';
import * as monaco from 'monaco-editor';
import {useState} from 'react';
import {Resizable} from 're-resizable';
import {useStore, useStoreDispatch} from '../StoreContext';
import {monacoOptions} from './monacoOptions';
import {
generateOverridePragmaFromConfig,
updateSourceWithOverridePragma,
} from '../../lib/configUtils';
loader.config({monaco});
export default function ConfigEditor(): JSX.Element {
const [, setMonaco] = useState<Monaco | null>(null);
const store = useStore();
const dispatchStore = useStoreDispatch();
const handleChange: (value: string | undefined) => void = async value => {
if (value === undefined) return;
try {
const newPragma = await generateOverridePragmaFromConfig(value);
const updatedSource = updateSourceWithOverridePragma(
store.source,
newPragma,
);
// Update the store with both the new config and updated source
dispatchStore({
type: 'updateFile',
payload: {
source: updatedSource,
config: value,
},
});
} catch (_) {
dispatchStore({
type: 'updateFile',
payload: {
source: store.source,
config: value,
},
});
}
};
const handleMount: (
_: editor.IStandaloneCodeEditor,
monaco: Monaco,
) => void = (_, monaco) => {
setMonaco(monaco);
const uri = monaco.Uri.parse(`file:///config.js`);
const model = monaco.editor.getModel(uri);
if (model) {
model.updateOptions({tabSize: 2});
}
};
return (
<div className="relative flex flex-col flex-none border-r border-gray-200">
<h2 className="p-4 duration-150 ease-in border-b cursor-default border-grey-200 font-light text-secondary">
Config Overrides
</h2>
<Resizable
minWidth={300}
maxWidth={600}
defaultSize={{width: 350, height: 'auto'}}
enable={{right: true}}
className="!h-[calc(100vh_-_3.5rem_-_4rem)]">
<MonacoEditor
path={'config.js'}
language={'javascript'}
value={store.config}
onMount={handleMount}
onChange={handleChange}
options={{
...monacoOptions,
lineNumbers: 'off',
folding: false,
renderLineHighlight: 'none',
scrollBeyondLastLine: false,
hideCursorInOverviewRuler: true,
overviewRulerBorder: false,
overviewRulerLanes: 0,
fontSize: 12,
}}
/>
</Resizable>
</div>
);
}

View File

@@ -37,7 +37,6 @@ import {
type Store,
} from '../../lib/stores';
import {useStore, useStoreDispatch} from '../StoreContext';
import ConfigEditor from './ConfigEditor';
import Input from './Input';
import {
CompilerOutput,
@@ -47,8 +46,6 @@ import {
} from './Output';
import {transformFromAstSync} from '@babel/core';
import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint';
import {useSearchParams} from 'next/navigation';
import {parseAndFormatConfig} from '../../lib/configUtils';
function parseInput(
input: string,
@@ -294,13 +291,7 @@ export default function Editor(): JSX.Element {
[deferredStore.source],
);
// TODO: Remove this once the config editor is more stable
const searchParams = useSearchParams();
const search = searchParams.get('showConfig');
const shouldShowConfig = search === 'true';
useMountEffect(() => {
// Initialize store
let mountStore: Store;
try {
mountStore = initStoreFromUrlOrLocalStorage();
@@ -316,17 +307,9 @@ export default function Editor(): JSX.Element {
});
mountStore = defaultStore;
}
parseAndFormatConfig(mountStore.source).then(config => {
dispatchStore({
type: 'setStore',
payload: {
store: {
...mountStore,
config,
},
},
});
dispatchStore({
type: 'setStore',
payload: {store: mountStore},
});
});
@@ -345,7 +328,6 @@ export default function Editor(): JSX.Element {
return (
<>
<div className="relative flex basis top-14">
{shouldShowConfig && <ConfigEditor />}
<div className={clsx('relative sm:basis-1/4')}>
<Input language={language} errors={errors} />
</div>

View File

@@ -17,7 +17,6 @@ import {useStore, useStoreDispatch} from '../StoreContext';
import {monacoOptions} from './monacoOptions';
// @ts-expect-error TODO: Make TS recognize .d.ts files, in addition to loading them with webpack.
import React$Types from '../../node_modules/@types/react/index.d.ts';
import {parseAndFormatConfig} from '../../lib/configUtils.ts';
loader.config({monaco});
@@ -80,17 +79,13 @@ export default function Input({errors, language}: Props): JSX.Element {
});
}, [monaco, language]);
const handleChange: (value: string | undefined) => void = async value => {
const handleChange: (value: string | undefined) => void = value => {
if (!value) return;
// Parse and format the config
const config = await parseAndFormatConfig(value);
dispatchStore({
type: 'updateFile',
payload: {
source: value,
config,
},
});
};

View File

@@ -56,7 +56,6 @@ type ReducerAction =
type: 'updateFile';
payload: {
source: string;
config?: string;
};
};
@@ -67,11 +66,10 @@ function storeReducer(store: Store, action: ReducerAction): Store {
return newStore;
}
case 'updateFile': {
const {source, config} = action.payload;
const {source} = action.payload;
const newStore = {
...store,
source,
config,
};
return newStore;
}

View File

@@ -1,87 +0,0 @@
/**
* 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 parserBabel from 'prettier/plugins/babel';
import prettierPluginEstree from 'prettier/plugins/estree';
import * as prettier from 'prettier/standalone';
import {parseConfigPragmaAsString} from '../../../packages/babel-plugin-react-compiler/src/Utils/TestUtils';
/**
* Parse config from pragma and format it with prettier
*/
export async function parseAndFormatConfig(source: string): Promise<string> {
const pragma = source.substring(0, source.indexOf('\n'));
let configString = parseConfigPragmaAsString(pragma);
if (configString !== '') {
configString = `(${configString})`;
}
try {
const formatted = await prettier.format(configString, {
semi: true,
parser: 'babel-ts',
plugins: [parserBabel, prettierPluginEstree],
});
return formatted;
} catch (error) {
console.error('Error formatting config:', error);
return ''; // Return empty string if not valid for now
}
}
function extractCurlyBracesContent(input: string): string {
const startIndex = input.indexOf('{');
const endIndex = input.lastIndexOf('}');
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
throw new Error('No outer curly braces found in input');
}
return input.slice(startIndex, endIndex + 1);
}
function cleanContent(content: string): string {
return content
.replace(/[\r\n]+/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
/**
* Generate a the override pragma comment from a formatted config object string
*/
export async function generateOverridePragmaFromConfig(
formattedConfigString: string,
): Promise<string> {
const content = extractCurlyBracesContent(formattedConfigString);
const cleanConfig = cleanContent(content);
// Format the config to ensure it's valid
await prettier.format(`(${cleanConfig})`, {
semi: false,
parser: 'babel-ts',
plugins: [parserBabel, prettierPluginEstree],
});
return `// @OVERRIDE:${cleanConfig}`;
}
/**
* Update the override pragma comment in source code.
*/
export function updateSourceWithOverridePragma(
source: string,
newPragma: string,
): string {
const firstLineEnd = source.indexOf('\n');
const firstLine = source.substring(0, firstLineEnd);
const pragmaRegex = /^\/\/\s*@/;
if (firstLineEnd !== -1 && pragmaRegex.test(firstLine.trim())) {
return newPragma + source.substring(firstLineEnd);
} else {
return newPragma + '\n' + source;
}
}

View File

@@ -15,10 +15,8 @@ export default function MyApp() {
export const defaultStore: Store = {
source: index,
config: '',
};
export const emptyStore: Store = {
source: '',
config: '',
};

View File

@@ -17,7 +17,6 @@ import {defaultStore} from '../defaultStore';
*/
export interface Store {
source: string;
config?: string;
}
export function encodeStore(store: Store): string {
return compressToEncodedURIComponent(JSON.stringify(store));
@@ -66,14 +65,5 @@ export function initStoreFromUrlOrLocalStorage(): Store {
const raw = decodeStore(encodedSource);
invariant(isValidStore(raw), 'Invalid Store');
// Add config property if missing for backwards compatibility
if (!('config' in raw)) {
return {
...raw,
config: '',
};
}
return raw;
}

View File

@@ -1,6 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -34,30 +34,26 @@
"invariant": "^2.2.4",
"lz-string": "^1.5.0",
"monaco-editor": "^0.52.0",
"next": "15.5.2",
"next": "^15.2.0-canary.64",
"notistack": "^3.0.0-alpha.7",
"prettier": "^3.3.3",
"pretty-format": "^29.3.1",
"re-resizable": "^6.9.16",
"react": "19.1.1",
"react-dom": "19.1.1"
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/node": "18.11.9",
"@types/react": "19.1.12",
"@types/react-dom": "19.1.9",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"autoprefixer": "^10.4.13",
"clsx": "^1.2.1",
"concurrently": "^7.4.0",
"eslint": "^8.28.0",
"eslint-config-next": "15.5.2",
"eslint-config-next": "^15.0.1",
"monaco-editor-webpack-plugin": "^7.1.0",
"postcss": "^8.4.31",
"tailwindcss": "^3.2.4",
"wait-on": "^7.2.0"
},
"resolutions": {
"@types/react": "19.1.12",
"@types/react-dom": "19.1.9"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -19,8 +19,7 @@
"test": "yarn workspaces run test",
"snap": "yarn workspace babel-plugin-react-compiler run snap",
"snap:build": "yarn workspace snap run build",
"npm:publish": "node scripts/release/publish",
"eslint-docs": "yarn workspace babel-plugin-react-compiler build && node scripts/build-eslint-docs.js"
"npm:publish": "node scripts/release/publish"
},
"dependencies": {
"fs-extra": "^4.0.2",

View File

@@ -7,76 +7,36 @@
import * as t from '@babel/types';
import {codeFrameColumns} from '@babel/code-frame';
import {type SourceLocation} from './HIR';
import type {SourceLocation} from './HIR';
import {Err, Ok, Result} from './Utils/Result';
import {assertExhaustive} from './Utils/utils';
import invariant from 'invariant';
export enum ErrorSeverity {
/**
* Invalid JS syntax, or valid syntax that is semantically invalid which may indicate some
* misunderstanding on the users part.
*/
InvalidJS = 'InvalidJS',
/**
* JS syntax that is not supported and which we do not plan to support. Developers should
* rewrite to use supported forms.
*/
UnsupportedJS = 'UnsupportedJS',
/**
* Code that breaks the rules of React.
*/
InvalidReact = 'InvalidReact',
/**
* Incorrect configuration of the compiler.
*/
InvalidConfig = 'InvalidConfig',
/**
* Code that can reasonably occur and that doesn't break any rules, but is unsafe to preserve
* memoization.
*/
CannotPreserveMemoization = 'CannotPreserveMemoization',
/**
* An API that is known to be incompatible with the compiler. Generally as a result of
* the library using "interior mutability", ie having a value whose referential identity
* stays the same but which provides access to values that can change. For example a
* function that doesn't change but returns different results, or an object that doesn't
* change identity but whose properties change.
*/
IncompatibleLibrary = 'IncompatibleLibrary',
/**
* Unhandled syntax that we don't support yet.
*/
Todo = 'Todo',
/**
* An unexpected internal error in the compiler that indicates critical issues that can panic
* the compiler.
*/
Invariant = 'Invariant',
}
import {ErrorSeverity} from './Utils/CompilerErrorSeverity';
import {
ErrorCode,
ErrorCodeDetails,
LinterCategory,
} from './Utils/CompilerErrorCodes';
export {ErrorSeverity};
export {ErrorCode, ErrorCodeDetails, LinterCategory};
export type CompilerDiagnosticOptions = {
category: ErrorCategory;
severity: ErrorSeverity;
reason: string;
description: string;
category: string;
description?: string | null | undefined;
details: Array<CompilerDiagnosticDetail>;
suggestions?: Array<CompilerSuggestion> | null | undefined;
linterCategory?: LinterCategory | null | undefined;
};
export type CompilerDiagnosticDetail =
/**
* A/the source of the error
*/
| {
kind: 'error';
loc: SourceLocation | null;
message: string;
}
| {
kind: 'hint';
message: string;
};
{
kind: 'error';
loc: SourceLocation | null;
message: string;
};
export enum CompilerSuggestionOperation {
InsertBefore,
@@ -100,14 +60,28 @@ export type CompilerSuggestion =
description: string;
};
export type CompilerErrorDetailOptions = {
category: ErrorCategory;
severity: ErrorSeverity;
export type PlainCompilerErrorDetailOptions = {
errorCode?: void;
reason: string;
description?: string | null | undefined;
severity:
| ErrorSeverity.Invariant
| ErrorSeverity.Todo
| ErrorSeverity.InvalidConfig;
loc: SourceLocation | null;
suggestions?: Array<CompilerSuggestion> | null | undefined;
};
export type CodedCompilerErrorDetailOptions = {
errorCode: ErrorCode;
description?: string | null | undefined;
loc: SourceLocation | null;
suggestions?: Array<CompilerSuggestion> | null | undefined;
linterCategory?: LinterCategory | null | undefined;
};
export type CompilerErrorDetailOptions =
| PlainCompilerErrorDetailOptions
| CodedCompilerErrorDetailOptions;
export type PrintErrorMessageOptions = {
/**
@@ -117,21 +91,74 @@ export type PrintErrorMessageOptions = {
eslint: boolean;
};
export function makeCompilerDiagnostic(
code: ErrorCode,
options?: {
description?: string;
suggestions?: Array<CompilerSuggestion> | null | undefined;
},
): CompilerDiagnostic {
return makeCompilerDiagnostic(code, options);
}
export class CompilerDiagnostic {
options: CompilerDiagnosticOptions;
constructor(options: CompilerDiagnosticOptions) {
/**
* Constructor is private to enforce that we either only create invariant diagnostics
* or use ErrorCodes
*/
private constructor(options: CompilerDiagnosticOptions) {
this.options = options;
}
static create(
options: Omit<CompilerDiagnosticOptions, 'details'>,
): CompilerDiagnostic {
static create<
T extends CompilerDiagnosticOptions & {severity: ErrorSeverity.Invariant},
>(options: Omit<T, 'details'>): CompilerDiagnostic {
return new CompilerDiagnostic({...options, details: []});
}
get reason(): CompilerDiagnosticOptions['reason'] {
return this.options.reason;
static fromCode(
code: ErrorCode,
options?: {
description?: string;
suggestions?: Array<CompilerSuggestion> | null | undefined;
details?: Array<CompilerDiagnosticDetail> | null | undefined;
},
): CompilerDiagnostic {
const errorEntry = ErrorCodeDetails[code];
let description = undefined;
if (errorEntry.description != null) {
description = endWithPeriod(errorEntry.description);
}
if (options?.description != null && options.description.length > 0) {
if (description != null && description.length > 0) {
description += ' ';
} else {
description = '';
}
description += endWithPeriod(options.description);
}
const diagnosticOptions: CompilerDiagnosticOptions = {
severity: errorEntry.severity,
category: errorEntry.reason,
description,
linterCategory: errorEntry.linterCategory,
suggestions: options?.suggestions,
details: options?.details ?? [],
};
return new CompilerDiagnostic(diagnosticOptions);
}
// TODO: remove after converting test fixtures to use printErrorMessage
serialize(): unknown {
return {options: {...this.options, linterCategory: undefined}};
}
get category(): CompilerDiagnosticOptions['category'] {
return this.options.category;
}
get description(): CompilerDiagnosticOptions['description'] {
return this.options.description;
@@ -142,8 +169,8 @@ export class CompilerDiagnostic {
get suggestions(): CompilerDiagnosticOptions['suggestions'] {
return this.options.suggestions;
}
get category(): ErrorCategory {
return this.options.category;
get linterCategory(): CompilerDiagnosticOptions['linterCategory'] {
return this.options.linterCategory;
}
withDetail(detail: CompilerDiagnosticDetail): CompilerDiagnostic {
@@ -152,20 +179,14 @@ export class CompilerDiagnostic {
}
primaryLocation(): SourceLocation | null {
const firstErrorDetail = this.options.details.filter(
d => d.kind === 'error',
)[0];
return firstErrorDetail != null && firstErrorDetail.kind === 'error'
? firstErrorDetail.loc
: null;
return this.options.details.filter(d => d.kind === 'error')[0]?.loc ?? null;
}
printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
const buffer = [
printErrorSummary(this.severity, this.reason),
'\n\n',
this.description,
];
const buffer = [printErrorSummary(this.severity, this.category)];
if (this.description != null) {
buffer.push(`\n\n${endWithPeriod(this.description)}`);
}
for (const detail of this.options.details) {
switch (detail.kind) {
case 'error': {
@@ -190,14 +211,9 @@ export class CompilerDiagnostic {
buffer.push(codeFrame);
break;
}
case 'hint': {
buffer.push('\n\n');
buffer.push(detail.message);
break;
}
default: {
assertExhaustive(
detail,
detail.kind,
`Unexpected detail kind ${(detail as any).kind}`,
);
}
@@ -207,9 +223,9 @@ export class CompilerDiagnostic {
}
toString(): string {
const buffer = [printErrorSummary(this.severity, this.reason)];
const buffer = [printErrorSummary(this.severity, this.category)];
if (this.description != null) {
buffer.push(`. ${this.description}.`);
buffer.push(`. ${endWithPeriod(this.description)}`);
}
const loc = this.primaryLocation();
if (loc != null && typeof loc !== 'symbol') {
@@ -230,13 +246,68 @@ export class CompilerErrorDetail {
this.options = options;
}
get reason(): CompilerErrorDetailOptions['reason'] {
return this.options.reason;
static fromCode(
code: ErrorCode,
details?: {
description?: string | null;
loc?: SourceLocation | null;
suggestions?: Array<CompilerSuggestion> | null | undefined;
},
): CompilerErrorDetail {
return new CompilerErrorDetail({
...details,
errorCode: code,
} as CodedCompilerErrorDetailOptions);
}
get description(): CompilerErrorDetailOptions['description'] {
return this.options.description;
// TODO: remove after converting test fixtures to use printErrorMessage
serialize(): unknown {
return {
options: {
reason: this.reason,
description: this.description,
severity: this.severity,
loc: this.loc,
suggestions: this.suggestions,
},
};
}
get severity(): CompilerErrorDetailOptions['severity'] {
get reason(): string {
if (this.options.errorCode != null) {
return ErrorCodeDetails[this.options.errorCode].reason;
} else {
return this.options.reason;
}
}
get description(): string | null | undefined {
if (this.options.errorCode != null) {
let description = undefined;
const errorEntry = ErrorCodeDetails[this.options.errorCode];
if (errorEntry.description != null) {
description = endWithPeriod(errorEntry.description);
}
if (
this.options.description != null &&
this.options.description.length > 0
) {
if (description != null && description.length > 0) {
description += ' ';
} else {
description = '';
}
description += endWithPeriod(this.options.description);
}
return description;
}
return this.options.description != null
? endWithPeriod(this.options.description)
: this.options.description;
}
get severity(): ErrorSeverity {
if (this.options.errorCode != null) {
return ErrorCodeDetails[this.options.errorCode].severity;
}
return this.options.severity;
}
get loc(): CompilerErrorDetailOptions['loc'] {
@@ -245,8 +316,11 @@ export class CompilerErrorDetail {
get suggestions(): CompilerErrorDetailOptions['suggestions'] {
return this.options.suggestions;
}
get category(): ErrorCategory {
return this.options.category;
get linterCategory(): LinterCategory | null | undefined {
if (this.options.errorCode != null) {
return ErrorCodeDetails[this.options.errorCode].linterCategory;
}
return undefined;
}
primaryLocation(): SourceLocation | null {
@@ -256,7 +330,7 @@ export class CompilerErrorDetail {
printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
const buffer = [printErrorSummary(this.severity, this.reason)];
if (this.description != null) {
buffer.push(`\n\n${this.description}.`);
buffer.push(`\n\n${endWithPeriod(this.description)}`);
}
const loc = this.loc;
if (loc != null && typeof loc !== 'symbol') {
@@ -281,7 +355,7 @@ export class CompilerErrorDetail {
toString(): string {
const buffer = [printErrorSummary(this.severity, this.reason)];
if (this.description != null) {
buffer.push(`. ${this.description}.`);
buffer.push(`. ${endWithPeriod(this.description)}`);
}
const loc = this.loc;
if (loc != null && typeof loc !== 'symbol') {
@@ -297,14 +371,13 @@ export class CompilerError extends Error {
static invariant(
condition: unknown,
options: Omit<CompilerErrorDetailOptions, 'severity' | 'category'>,
options: Omit<PlainCompilerErrorDetailOptions, 'severity'>,
): asserts condition {
if (!condition) {
const errors = new CompilerError();
errors.pushErrorDetail(
new CompilerErrorDetail({
...options,
category: ErrorCategory.Invariant,
severity: ErrorSeverity.Invariant,
}),
);
@@ -312,62 +385,43 @@ export class CompilerError extends Error {
}
}
static throwDiagnostic(options: CompilerDiagnosticOptions): never {
static throwDiagnostic(diagnostic: CompilerDiagnostic): never {
const errors = new CompilerError();
errors.pushDiagnostic(new CompilerDiagnostic(options));
errors.pushDiagnostic(diagnostic);
throw errors;
}
static throwTodo(
options: Omit<CompilerErrorDetailOptions, 'severity' | 'category'>,
options: Omit<PlainCompilerErrorDetailOptions, 'severity'>,
): never {
const errors = new CompilerError();
errors.pushErrorDetail(
new CompilerErrorDetail({
...options,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
}),
new CompilerErrorDetail({...options, severity: ErrorSeverity.Todo}),
);
throw errors;
}
static throwInvalidJS(
options: Omit<CompilerErrorDetailOptions, 'severity' | 'category'>,
static throwFromCode(
code: ErrorCode,
options?: {
description?: string | null;
loc?: SourceLocation | null;
suggestions?: Array<CompilerSuggestion> | null | undefined;
},
): never {
const errors = new CompilerError();
errors.pushErrorDetail(
new CompilerErrorDetail({
...options,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
}),
);
throw errors;
}
static throwInvalidReact(
options: Omit<CompilerErrorDetailOptions, 'severity'>,
): never {
const errors = new CompilerError();
errors.pushErrorDetail(
new CompilerErrorDetail({
...options,
severity: ErrorSeverity.InvalidReact,
}),
);
errors.pushErrorDetail(CompilerErrorDetail.fromCode(code, options));
throw errors;
}
static throwInvalidConfig(
options: Omit<CompilerErrorDetailOptions, 'severity' | 'category'>,
options: Omit<PlainCompilerErrorDetailOptions, 'severity'>,
): never {
const errors = new CompilerError();
errors.pushErrorDetail(
new CompilerErrorDetail({
...options,
severity: ErrorSeverity.InvalidConfig,
category: ErrorCategory.Config,
}),
);
throw errors;
@@ -430,14 +484,10 @@ export class CompilerError extends Error {
}
push(options: CompilerErrorDetailOptions): CompilerErrorDetail {
const detail = new CompilerErrorDetail({
category: options.category,
reason: options.reason,
description: options.description ?? null,
severity: options.severity,
suggestions: options.suggestions,
loc: typeof options.loc === 'symbol' ? null : options.loc,
});
if (options instanceof CompilerErrorDetail) {
return this.pushErrorDetail(options);
}
const detail = new CompilerErrorDetail(options);
return this.pushErrorDetail(detail);
}
@@ -446,6 +496,19 @@ export class CompilerError extends Error {
return detail;
}
pushErrorCode(
code: ErrorCode,
details?: {
description?: string | null;
loc?: SourceLocation | null;
suggestions?: Array<CompilerSuggestion> | null | undefined;
},
): CompilerErrorDetail {
const detail = CompilerErrorDetail.fromCode(code, details);
this.details.push(detail);
return detail;
}
hasErrors(): boolean {
return this.details.length > 0;
}
@@ -466,8 +529,7 @@ export class CompilerError extends Error {
case ErrorSeverity.InvalidJS:
case ErrorSeverity.InvalidReact:
case ErrorSeverity.InvalidConfig:
case ErrorSeverity.UnsupportedJS:
case ErrorSeverity.IncompatibleLibrary: {
case ErrorSeverity.UnsupportedJS: {
return true;
}
case ErrorSeverity.CannotPreserveMemoization:
@@ -515,9 +577,8 @@ function printErrorSummary(severity: ErrorSeverity, message: string): string {
severityCategory = 'Error';
break;
}
case ErrorSeverity.IncompatibleLibrary:
case ErrorSeverity.CannotPreserveMemoization: {
severityCategory = 'Compilation Skipped';
severityCategory = 'Memoization';
break;
}
case ErrorSeverity.Invariant: {
@@ -535,369 +596,9 @@ function printErrorSummary(severity: ErrorSeverity, message: string): string {
return `${severityCategory}: ${message}`;
}
/**
* See getRuleForCategory() for how these map to ESLint rules
*/
export enum ErrorCategory {
// Checking for valid hooks usage (non conditional, non-first class, non reactive, etc)
Hooks = 'Hooks',
// Checking for no capitalized calls (not definitively an error, hence separating)
CapitalizedCalls = 'CapitalizedCalls',
// Checking for static components
StaticComponents = 'StaticComponents',
// Checking for valid usage of manual memoization
UseMemo = 'UseMemo',
// Checking for higher order functions acting as factories for components/hooks
Factories = 'Factories',
// Checks that manual memoization is preserved
PreserveManualMemo = 'PreserveManualMemo',
// Checks for known incompatible libraries
IncompatibleLibrary = 'IncompatibleLibrary',
// Checking for no mutations of props, hook arguments, hook return values
Immutability = 'Immutability',
// Checking for assignments to globals
Globals = 'Globals',
// Checking for valid usage of refs, ie no access during render
Refs = 'Refs',
// Checks for memoized effect deps
EffectDependencies = 'EffectDependencies',
// Checks for no setState in effect bodies
EffectSetState = 'EffectSetState',
EffectDerivationsOfState = 'EffectDerivationsOfState',
// Validates against try/catch in place of error boundaries
ErrorBoundaries = 'ErrorBoundaries',
// Checking for pure functions
Purity = 'Purity',
// Validates against setState in render
RenderSetState = 'RenderSetState',
// Internal invariants
Invariant = 'Invariant',
// Todos
Todo = 'Todo',
// Syntax errors
Syntax = 'Syntax',
// Checks for use of unsupported syntax
UnsupportedSyntax = 'UnsupportedSyntax',
// Config errors
Config = 'Config',
// Gating error
Gating = 'Gating',
// Suppressions
Suppression = 'Suppression',
// Issues with auto deps
AutomaticEffectDependencies = 'AutomaticEffectDependencies',
// Issues with `fire`
Fire = 'Fire',
// fbt-specific issues
FBT = 'FBT',
}
export type LintRule = {
// Stores the category the rule corresponds to, used to filter errors when reporting
category: ErrorCategory;
/**
* The "name" of the rule as it will be used by developers to enable/disable, eg
* "eslint-disable-nest line <name>"
*/
name: string;
/**
* A description of the rule that appears somewhere in ESLint. This does not affect
* how error messages are formatted
*/
description: string;
/**
* If true, this rule will automatically appear in the default, "recommended" ESLint
* rule set. Otherwise it will be part of an `allRules` export that developers can
* use to opt-in to showing output of all possible rules.
*
* NOTE: not all validations are enabled by default! Setting this flag only affects
* whether a given rule is part of the recommended set. The corresponding validation
* also should be enabled by default if you want the error to actually show up!
*/
recommended: boolean;
};
const RULE_NAME_PATTERN = /^[a-z]+(-[a-z]+)*$/;
export function getRuleForCategory(category: ErrorCategory): LintRule {
const rule = getRuleForCategoryImpl(category);
invariant(
RULE_NAME_PATTERN.test(rule.name),
`Invalid rule name, got '${rule.name}' but rules must match ${RULE_NAME_PATTERN.toString()}`,
);
return rule;
}
function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
switch (category) {
case ErrorCategory.AutomaticEffectDependencies: {
return {
category,
name: 'automatic-effect-dependencies',
description:
'Verifies that automatic effect dependencies are compiled if opted-in',
recommended: false,
};
}
case ErrorCategory.CapitalizedCalls: {
return {
category,
name: 'capitalized-calls',
description:
'Validates against calling capitalized functions/methods instead of using JSX',
recommended: false,
};
}
case ErrorCategory.Config: {
return {
category,
name: 'config',
description: 'Validates the compiler configuration options',
recommended: true,
};
}
case ErrorCategory.EffectDependencies: {
return {
category,
name: 'memoized-effect-dependencies',
description: 'Validates that effect dependencies are memoized',
recommended: false,
};
}
case ErrorCategory.EffectDerivationsOfState: {
return {
category,
name: 'no-deriving-state-in-effects',
description:
'Validates against deriving values from state in an effect',
recommended: false,
};
}
case ErrorCategory.EffectSetState: {
return {
category,
name: 'set-state-in-effect',
description:
'Validates against calling setState synchronously in an effect, which can lead to re-renders that degrade performance',
recommended: true,
};
}
case ErrorCategory.ErrorBoundaries: {
return {
category,
name: 'error-boundaries',
description:
'Validates usage of error boundaries instead of try/catch for errors in child components',
recommended: true,
};
}
case ErrorCategory.Factories: {
return {
category,
name: 'component-hook-factories',
description:
'Validates against higher order functions defining nested components or hooks. ' +
'Components and hooks should be defined at the module level',
recommended: true,
};
}
case ErrorCategory.FBT: {
return {
category,
name: 'fbt',
description: 'Validates usage of fbt',
recommended: false,
};
}
case ErrorCategory.Fire: {
return {
category,
name: 'fire',
description: 'Validates usage of `fire`',
recommended: false,
};
}
case ErrorCategory.Gating: {
return {
category,
name: 'gating',
description:
'Validates configuration of [gating mode](https://react.dev/reference/react-compiler/gating)',
recommended: true,
};
}
case ErrorCategory.Globals: {
return {
category,
name: 'globals',
description:
'Validates against assignment/mutation of globals during render, part of ensuring that ' +
'[side effects must render outside of render](https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)',
recommended: true,
};
}
case ErrorCategory.Hooks: {
return {
category,
name: 'hooks',
description: 'Validates the rules of hooks',
/**
* TODO: the "Hooks" rule largely reimplements the "rules-of-hooks" non-compiler rule.
* We need to dedeupe these (moving the remaining bits into the compiler) and then enable
* this rule.
*/
recommended: false,
};
}
case ErrorCategory.Immutability: {
return {
category,
name: 'immutability',
description:
'Validates against mutating props, state, and other values that [are immutable](https://react.dev/reference/rules/components-and-hooks-must-be-pure#props-and-state-are-immutable)',
recommended: true,
};
}
case ErrorCategory.Invariant: {
return {
category,
name: 'invariant',
description: 'Internal invariants',
recommended: false,
};
}
case ErrorCategory.PreserveManualMemo: {
return {
category,
name: 'preserve-manual-memoization',
description:
'Validates that existing manual memoized is preserved by the compiler. ' +
'React Compiler will only compile components and hooks if its inference ' +
'[matches or exceeds the existing manual memoization](https://react.dev/learn/react-compiler/introduction#what-should-i-do-about-usememo-usecallback-and-reactmemo)',
recommended: true,
};
}
case ErrorCategory.Purity: {
return {
category,
name: 'purity',
description:
'Validates that [components/hooks are pure](https://react.dev/reference/rules/components-and-hooks-must-be-pure) by checking that they do not call known-impure functions',
recommended: true,
};
}
case ErrorCategory.Refs: {
return {
category,
name: 'refs',
description:
'Validates correct usage of refs, not reading/writing during render. See the "pitfalls" section in [`useRef()` usage](https://react.dev/reference/react/useRef#usage)',
recommended: true,
};
}
case ErrorCategory.RenderSetState: {
return {
category,
name: 'set-state-in-render',
description:
'Validates against setting state during render, which can trigger additional renders and potential infinite render loops',
recommended: true,
};
}
case ErrorCategory.StaticComponents: {
return {
category,
name: 'static-components',
description:
'Validates that components are static, not recreated every render. Components that are recreated dynamically can reset state and trigger excessive re-rendering',
recommended: true,
};
}
case ErrorCategory.Suppression: {
return {
category,
name: 'rule-suppression',
description: 'Validates against suppression of other rules',
recommended: false,
};
}
case ErrorCategory.Syntax: {
return {
category,
name: 'syntax',
description: 'Validates against invalid syntax',
recommended: false,
};
}
case ErrorCategory.Todo: {
return {
category,
name: 'todo',
description: 'Unimplemented features',
recommended: false,
};
}
case ErrorCategory.UnsupportedSyntax: {
return {
category,
name: 'unsupported-syntax',
description:
'Validates against syntax that we do not plan to support in React Compiler',
recommended: true,
};
}
case ErrorCategory.UseMemo: {
return {
category,
name: 'use-memo',
description:
'Validates usage of the useMemo() hook against common mistakes. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
recommended: true,
};
}
case ErrorCategory.IncompatibleLibrary: {
return {
category,
name: 'incompatible-library',
description:
'Validates against usage of libraries which are incompatible with memoization (manual or automatic)',
recommended: true,
};
}
default: {
assertExhaustive(category, `Unsupported category ${category}`);
}
function endWithPeriod(s: string): string {
if (s === '' || s.endsWith('.')) {
return s;
}
return `${s.replace(/\.+$/, '')}.`;
}
export const LintRules: Array<LintRule> = Object.keys(ErrorCategory).map(
category => getRuleForCategory(category as any),
);

View File

@@ -9,7 +9,7 @@ import {NodePath} from '@babel/core';
import * as t from '@babel/types';
import {Scope as BabelScope} from '@babel/traverse';
import {CompilerError, ErrorCategory, ErrorSeverity} from '../CompilerError';
import {CompilerError, ErrorSeverity} from '../CompilerError';
import {
EnvironmentConfig,
GeneratedSource,
@@ -38,11 +38,10 @@ export function validateRestrictedImports(
ImportDeclaration(importDeclPath) {
if (restrictedImports.has(importDeclPath.node.source.value)) {
error.push({
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
reason: 'Bailing out due to blocklisted import',
description: `Import from module ${importDeclPath.node.source.value}`,
loc: importDeclPath.node.loc ?? null,
severity: ErrorSeverity.Todo,
});
}
},
@@ -206,11 +205,10 @@ export class ProgramContext {
}
const error = new CompilerError();
error.push({
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
reason: 'Encountered conflicting global in generated program',
description: `Conflict from local binding ${name}`,
loc: scope.getBinding(name)?.path.node.loc ?? null,
severity: ErrorSeverity.Todo,
suggestions: null,
});
return Err(error);

View File

@@ -11,7 +11,7 @@ import {
CompilerDiagnostic,
CompilerError,
CompilerErrorDetail,
CompilerErrorDetailOptions,
PlainCompilerErrorDetailOptions,
} from '../CompilerError';
import {
EnvironmentConfig,
@@ -107,6 +107,8 @@ export type PluginOptions = {
* passes.
*
* Defaults to false
*
* TODO: rename this to lintOnly or something similar
*/
noEmit: boolean;
@@ -234,7 +236,10 @@ export type CompileErrorEvent = {
export type CompileDiagnosticEvent = {
kind: 'CompileDiagnostic';
fnLoc: t.SourceLocation | null;
detail: Omit<Omit<CompilerErrorDetailOptions, 'severity'>, 'suggestions'>;
detail: Omit<
Omit<PlainCompilerErrorDetailOptions, 'severity'>,
'suggestions'
>;
};
export type CompileSuccessEvent = {
kind: 'CompileSuccess';

View File

@@ -8,7 +8,7 @@
import {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import prettyFormat from 'pretty-format';
import {Logger, ProgramContext, SingleLineSuppressionRange} from '.';
import {Logger, ProgramContext} from '.';
import {
HIRFunction,
ReactiveFunction,
@@ -33,7 +33,9 @@ import {findContextIdentifiers} from '../HIR/FindContextIdentifiers';
import {
analyseFunctions,
dropManualMemoization,
inferMutableRanges,
inferReactivePlaces,
inferReferenceEffects,
inlineImmediatelyInvokedFunctionExpressions,
inferEffectDependencies,
} from '../Inference';
@@ -121,7 +123,6 @@ function run(
logger: Logger | null,
filename: string | null,
code: string | null,
suppressions: Array<SingleLineSuppressionRange>,
): CodegenFunction {
const contextIdentifiers = findContextIdentifiers(func);
const env = new Environment(
@@ -135,7 +136,6 @@ function run(
filename,
code,
programContext,
suppressions,
);
env.logger?.debugLogIRs?.({
kind: 'debug',
@@ -173,7 +173,7 @@ function runWithEnvironment(
!env.config.disableMemoizationForDebugging &&
!env.config.enableChangeDetectionForDebugging
) {
dropManualMemoization(hir).unwrap();
env.logOrThrowErrors(dropManualMemoization(hir));
log({kind: 'hir', name: 'DropManualMemoization', value: hir});
}
@@ -206,10 +206,10 @@ function runWithEnvironment(
if (env.isInferredMemoEnabled) {
if (env.config.validateHooksUsage) {
validateHooksUsage(hir).unwrap();
env.logOrThrowErrors(validateHooksUsage(hir));
}
if (env.config.validateNoCapitalizedCalls) {
validateNoCapitalizedCalls(hir).unwrap();
if (env.config.validateNoCapitalizedCalls != null) {
env.logOrThrowErrors(validateNoCapitalizedCalls(hir));
}
}
@@ -228,12 +228,23 @@ function runWithEnvironment(
analyseFunctions(hir);
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
if (env.isInferredMemoEnabled) {
if (mutabilityAliasingErrors.isErr()) {
throw mutabilityAliasingErrors.unwrapErr();
if (!env.config.enableNewMutationAliasingModel) {
const fnEffectResult = inferReferenceEffects(hir);
if (env.isInferredMemoEnabled) {
env.logOrThrowErrors(fnEffectResult);
}
log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
} else {
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
if (env.isInferredMemoEnabled) {
env.logOrThrowErrors(mutabilityAliasingErrors);
}
}
if (!env.config.enableNewMutationAliasingModel) {
validateLocalsNotReassignedAfterRender(hir);
}
// Note: Has to come after infer reference effects because "dead" code may still affect inference
@@ -248,15 +259,18 @@ function runWithEnvironment(
pruneMaybeThrows(hir);
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
const mutabilityAliasingRangeErrors = inferMutationAliasingRanges(hir, {
isFunctionExpression: false,
});
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
if (env.isInferredMemoEnabled) {
if (mutabilityAliasingRangeErrors.isErr()) {
throw mutabilityAliasingRangeErrors.unwrapErr();
if (!env.config.enableNewMutationAliasingModel) {
inferMutableRanges(hir);
log({kind: 'hir', name: 'InferMutableRanges', value: hir});
} else {
const mutabilityAliasingErrors = inferMutationAliasingRanges(hir, {
isFunctionExpression: false,
});
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
if (env.isInferredMemoEnabled) {
env.logOrThrowErrors(mutabilityAliasingErrors.map(() => undefined));
env.logOrThrowErrors(validateLocalsNotReassignedAfterRender(hir));
}
validateLocalsNotReassignedAfterRender(hir);
}
if (env.isInferredMemoEnabled) {
@@ -265,15 +279,15 @@ function runWithEnvironment(
}
if (env.config.validateRefAccessDuringRender) {
validateNoRefAccessInRender(hir).unwrap();
env.logOrThrowErrors(validateNoRefAccessInRender(hir));
}
if (env.config.validateNoSetStateInRender) {
validateNoSetStateInRender(hir).unwrap();
env.logOrThrowErrors(validateNoSetStateInRender(hir));
}
if (env.config.validateNoDerivedComputationsInEffects) {
validateNoDerivedComputationsInEffects(hir);
env.logOrThrowErrors(validateNoDerivedComputationsInEffects(hir));
}
if (env.config.validateNoSetStateInEffects) {
@@ -283,12 +297,19 @@ function runWithEnvironment(
if (env.config.validateNoJSXInTryStatements) {
env.logErrors(validateNoJSXInTryStatement(hir));
}
if (env.config.validateNoImpureFunctionsInRender) {
validateNoImpureFunctionsInRender(hir).unwrap();
if (
env.config.validateNoImpureFunctionsInRender &&
!env.config.enableNewMutationAliasingModel
) {
env.logOrThrowErrors(validateNoImpureFunctionsInRender(hir));
}
validateNoFreezingKnownMutableFunctions(hir).unwrap();
if (
env.config.validateNoFreezingKnownMutableFunctions ||
env.config.enableNewMutationAliasingModel
) {
env.logOrThrowErrors(validateNoFreezingKnownMutableFunctions(hir));
}
}
inferReactivePlaces(hir);
@@ -569,7 +590,6 @@ export function compileFn(
logger: Logger | null,
filename: string | null,
code: string | null,
singleLineSuppressions: Array<SingleLineSuppressionRange>,
): CodegenFunction {
return run(
func,
@@ -580,6 +600,5 @@ export function compileFn(
logger,
filename,
code,
singleLineSuppressions,
);
}

View File

@@ -7,12 +7,7 @@
import {NodePath} from '@babel/core';
import * as t from '@babel/types';
import {
CompilerError,
CompilerErrorDetail,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {CompilerError, ErrorSeverity} from '../CompilerError';
import {ExternalFunction, ReactFunctionType} from '../HIR/Environment';
import {CodegenFunction} from '../ReactiveScopes';
import {isComponentDeclaration} from '../Utils/ComponentDeclaration';
@@ -27,13 +22,13 @@ import {
import {CompilerReactTarget, PluginOptions} from './Options';
import {compileFn} from './Pipeline';
import {
filterSuppressionsThatAffectNode,
filterSuppressionsThatAffectFunction,
findProgramSuppressions,
SingleLineSuppressionRange,
suppressionsToCompilerError,
} from './Suppression';
import {GeneratedSource} from '../HIR';
import {Err, Ok, Result} from '../Utils/Result';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
export type CompilerPass = {
opts: PluginOptions;
@@ -103,13 +98,9 @@ function findDirectivesDynamicGating(
if (t.isValidIdentifier(maybeMatch[1])) {
result.push({directive, match: maybeMatch[1]});
} else {
errors.push({
reason: `Dynamic gating directive is not a valid JavaScript identifier`,
errors.pushErrorCode(ErrorCode.DYNAMIC_GATING_IS_NOT_IDENTIFIER, {
description: `Found '${directive.value.value}'`,
severity: ErrorSeverity.InvalidReact,
category: ErrorCategory.Gating,
loc: directive.loc ?? null,
suggestions: null,
});
}
}
@@ -118,15 +109,11 @@ function findDirectivesDynamicGating(
return Err(errors);
} else if (result.length > 1) {
const error = new CompilerError();
error.push({
reason: `Multiple dynamic gating directives found`,
error.pushErrorCode(ErrorCode.DYNAMIC_GATING_MULTIPLE_DIRECTIVES, {
description: `Expected a single directive but found [${result
.map(r => r.directive.value.value)
.join(', ')}]`,
severity: ErrorSeverity.InvalidReact,
category: ErrorCategory.Gating,
loc: result[0].directive.loc ?? null,
suggestions: null,
});
return Err(error);
} else if (result.length === 1) {
@@ -455,15 +442,12 @@ export function compileProgram(
if (programContext.hasModuleScopeOptOut) {
if (compiledFns.length > 0) {
const error = new CompilerError();
error.pushErrorDetail(
new CompilerErrorDetail({
reason:
'Unexpected compiled functions when module scope opt-out is present',
severity: ErrorSeverity.Invariant,
category: ErrorCategory.Invariant,
loc: null,
}),
);
error.push({
reason:
'Unexpected compiled functions when module scope opt-out is present',
severity: ErrorSeverity.Invariant,
loc: null,
});
handleError(error, programContext, null);
}
return null;
@@ -495,20 +479,7 @@ function findFunctionsToCompile(
): Array<CompileSource> {
const queue: Array<CompileSource> = [];
const traverseFunction = (fn: BabelFn, pass: CompilerPass): void => {
// In 'all' mode, compile only top level functions
if (
pass.opts.compilationMode === 'all' &&
fn.scope.getProgramParent() !== fn.scope.parent
) {
return;
}
const fnType = getReactFunctionType(fn, pass);
if (pass.opts.environment.validateNoDynamicallyCreatedComponentsOrHooks) {
validateNoDynamicallyCreatedComponentsOrHooks(fn, pass, programContext);
}
if (fnType === null || programContext.alreadyCompiled.has(fn.node)) {
return;
}
@@ -609,9 +580,7 @@ function processFn(
let compiledFn: CodegenFunction;
const compileResult = tryCompileFunction(fn, fnType, programContext);
if (compileResult.kind === 'error') {
if (directives.optOut != null) {
logError(compileResult.error, programContext, fn.node.loc ?? null);
} else {
if (directives.optOut == null) {
handleError(compileResult.error, programContext, fn.node.loc ?? null);
}
const retryResult = retryCompileFunction(fn, fnType, programContext);
@@ -692,17 +661,11 @@ function tryCompileFunction(
* Program node itself. We need to figure out whether an eslint suppression range
* applies to this function first.
*/
const suppressionsInFunction = filterSuppressionsThatAffectNode(
const suppressionsInFunction = filterSuppressionsThatAffectFunction(
programContext.suppressions,
fn,
);
const singleLineSuppressions = suppressionsInFunction.filter(
s => s.kind === 'single-line',
) as Array<SingleLineSuppressionRange>;
const multiLineSuppressions = suppressionsInFunction.filter(
s => s.kind === 'multi-line',
);
if (multiLineSuppressions.length > 0) {
if (suppressionsInFunction.length > 0) {
return {
kind: 'error',
error: suppressionsToCompilerError(suppressionsInFunction),
@@ -716,12 +679,11 @@ function tryCompileFunction(
fn,
programContext.opts.environment,
fnType,
'all_features',
programContext.opts.noEmit ? 'lint_only' : 'all_features',
programContext,
programContext.opts.logger,
programContext.filename,
programContext.code,
singleLineSuppressions,
),
};
} catch (err) {
@@ -760,7 +722,6 @@ function retryCompileFunction(
programContext.opts.logger,
programContext.filename,
programContext.code,
[], // ignore suppressions in the retry pipeline
);
if (!retryResult.hasFireRewrite && !retryResult.hasInferredEffect) {
@@ -831,16 +792,7 @@ function shouldSkipCompilation(
if (pass.opts.sources) {
if (pass.filename === null) {
const error = new CompilerError();
error.pushErrorDetail(
new CompilerErrorDetail({
reason: `Expected a filename but found none.`,
description:
"When the 'sources' config options is specified, the React compiler will only compile files with a name",
severity: ErrorSeverity.InvalidConfig,
category: ErrorCategory.Config,
loc: null,
}),
);
error.pushErrorCode(ErrorCode.FILENAME_NOT_SET);
handleError(error, pass, null);
return true;
}
@@ -861,73 +813,6 @@ function shouldSkipCompilation(
return false;
}
/**
* Validates that Components/Hooks are always defined at module level. This prevents scope reference
* errors that occur when the compiler attempts to optimize the nested component/hook while its
* parent function remains uncompiled.
*/
function validateNoDynamicallyCreatedComponentsOrHooks(
fn: BabelFn,
pass: CompilerPass,
programContext: ProgramContext,
): void {
const parentNameExpr = getFunctionName(fn);
const parentName =
parentNameExpr !== null && parentNameExpr.isIdentifier()
? parentNameExpr.node.name
: '<anonymous>';
const validateNestedFunction = (
nestedFn: NodePath<
t.FunctionDeclaration | t.FunctionExpression | t.ArrowFunctionExpression
>,
): void => {
if (
nestedFn.node === fn.node ||
programContext.alreadyCompiled.has(nestedFn.node)
) {
return;
}
if (nestedFn.scope.getProgramParent() !== nestedFn.scope.parent) {
const nestedFnType = getReactFunctionType(nestedFn as BabelFn, pass);
const nestedFnNameExpr = getFunctionName(nestedFn as BabelFn);
const nestedName =
nestedFnNameExpr !== null && nestedFnNameExpr.isIdentifier()
? nestedFnNameExpr.node.name
: '<anonymous>';
if (nestedFnType === 'Component' || nestedFnType === 'Hook') {
CompilerError.throwDiagnostic({
category: ErrorCategory.Factories,
severity: ErrorSeverity.InvalidReact,
reason: `Components and hooks cannot be created dynamically`,
description: `The function \`${nestedName}\` appears to be a React ${nestedFnType.toLowerCase()}, but it's defined inside \`${parentName}\`. Components and Hooks should always be declared at module scope`,
details: [
{
kind: 'error',
message: 'this function dynamically created a component/hook',
loc: parentNameExpr?.node.loc ?? fn.node.loc ?? null,
},
{
kind: 'error',
message: 'the component is created here',
loc: nestedFnNameExpr?.node.loc ?? nestedFn.node.loc ?? null,
},
],
});
}
}
nestedFn.skip();
};
fn.traverse({
FunctionDeclaration: validateNestedFunction,
FunctionExpression: validateNestedFunction,
ArrowFunctionExpression: validateNestedFunction,
});
}
function getReactFunctionType(
fn: BabelFn,
pass: CompilerPass,
@@ -966,6 +851,11 @@ function getReactFunctionType(
return componentSyntaxType;
}
case 'all': {
// Compile only top level functions
if (fn.scope.getProgramParent() !== fn.scope.parent) {
return null;
}
return getComponentOrHookLike(fn, hookPattern) ?? 'Other';
}
default: {

View File

@@ -11,8 +11,7 @@ import {
CompilerDiagnostic,
CompilerError,
CompilerSuggestionOperation,
ErrorCategory,
ErrorSeverity,
ErrorCode,
} from '../CompilerError';
import {assertExhaustive} from '../Utils/utils';
import {GeneratedSource} from '../HIR';
@@ -25,23 +24,11 @@ import {GeneratedSource} from '../HIR';
* The enable comment can be missing in the case where only a disable block is present, ie the rest
* of the file has potential React violations.
*/
export type SuppressionRange =
| {
kind: 'single-line';
source: SuppressionSource;
comment: t.Comment;
}
| {
kind: 'multi-line';
source: SuppressionSource;
disableComment: t.Comment;
enableComment: t.Comment | null;
};
export type SingleLineSuppressionRange = Extract<
SuppressionRange,
{kind: 'single-line'}
>;
export type SuppressionRange = {
disableComment: t.Comment;
enableComment: t.Comment | null;
source: SuppressionSource;
};
type SuppressionSource = 'Eslint' | 'Flow';
@@ -50,23 +37,15 @@ type SuppressionSource = 'Eslint' | 'Flow';
* 1. The suppression is within the function's body; or
* 2. The suppression wraps the function
*/
export function filterSuppressionsThatAffectNode<T extends SuppressionRange>(
suppressionRanges: Array<T>,
node: NodePath,
): Array<T> {
const suppressionsInScope: Array<T> = [];
const fnNode = node.node;
export function filterSuppressionsThatAffectFunction(
suppressionRanges: Array<SuppressionRange>,
fn: NodePath<t.Function>,
): Array<SuppressionRange> {
const suppressionsInScope: Array<SuppressionRange> = [];
const fnNode = fn.node;
for (const suppressionRange of suppressionRanges) {
const enableComment =
suppressionRange.kind === 'single-line'
? suppressionRange.comment
: suppressionRange.enableComment;
const disableComment =
suppressionRange.kind === 'single-line'
? suppressionRange.comment
: suppressionRange.disableComment;
if (
disableComment.start == null ||
suppressionRange.disableComment.start == null ||
fnNode.start == null ||
fnNode.end == null
) {
@@ -74,20 +53,22 @@ export function filterSuppressionsThatAffectNode<T extends SuppressionRange>(
}
// The suppression is within the function
if (
disableComment.start > fnNode.start &&
suppressionRange.disableComment.start > fnNode.start &&
// If there is no matching enable, the rest of the file has potential violations
(enableComment === null ||
(enableComment.end != null && enableComment.end < fnNode.end))
(suppressionRange.enableComment === null ||
(suppressionRange.enableComment.end != null &&
suppressionRange.enableComment.end < fnNode.end))
) {
suppressionsInScope.push(suppressionRange);
}
// The suppression wraps the function
if (
disableComment.start < fnNode.start &&
suppressionRange.disableComment.start < fnNode.start &&
// If there is no matching enable, the rest of the file has potential violations
(enableComment === null ||
(enableComment.end != null && enableComment.end > fnNode.end))
(suppressionRange.enableComment === null ||
(suppressionRange.enableComment.end != null &&
suppressionRange.enableComment.end > fnNode.end))
) {
suppressionsInScope.push(suppressionRange);
}
@@ -101,7 +82,9 @@ export function findProgramSuppressions(
flowSuppressions: boolean,
): Array<SuppressionRange> {
const suppressionRanges: Array<SuppressionRange> = [];
let suppression: SuppressionRange | null = null;
let disableComment: t.Comment | null = null;
let enableComment: t.Comment | null = null;
let source: SuppressionSource | null = null;
const rulePattern = `(${ruleNames.join('|')})`;
const disableNextLinePattern = new RegExp(
@@ -123,49 +106,42 @@ export function findProgramSuppressions(
* If we're already within a CommentBlock, we should not restart the range prematurely for a
* CommentLine within the block.
*/
suppression == null &&
disableComment == null &&
disableNextLinePattern.test(comment.value)
) {
suppression = {
kind: 'single-line',
comment,
source: 'Eslint',
};
disableComment = comment;
enableComment = comment;
source = 'Eslint';
}
if (
flowSuppressions &&
suppression == null &&
disableComment == null &&
flowSuppressionPattern.test(comment.value)
) {
suppression = {
kind: 'single-line',
comment,
source: 'Flow',
};
disableComment = comment;
enableComment = comment;
source = 'Flow';
}
if (disablePattern.test(comment.value)) {
suppression = {
kind: 'multi-line',
disableComment: comment,
enableComment: null,
source: 'Eslint',
};
disableComment = comment;
source = 'Eslint';
}
if (
enablePattern.test(comment.value) &&
suppression != null &&
suppression.kind === 'multi-line' &&
suppression.source === 'Eslint'
) {
suppression.enableComment = comment;
if (enablePattern.test(comment.value) && source === 'Eslint') {
enableComment = comment;
}
if (suppression != null) {
suppressionRanges.push(suppression);
suppression = null;
if (disableComment != null && source != null) {
suppressionRanges.push({
disableComment: disableComment,
enableComment: enableComment,
source,
});
disableComment = null;
enableComment = null;
source = null;
}
}
return suppressionRanges;
@@ -180,24 +156,21 @@ export function suppressionsToCompilerError(
});
const error = new CompilerError();
for (const suppressionRange of suppressionRanges) {
const disableComment =
suppressionRange.kind === 'single-line'
? suppressionRange.comment
: suppressionRange.disableComment;
if (disableComment.start == null || disableComment.end == null) {
if (
suppressionRange.disableComment.start == null ||
suppressionRange.disableComment.end == null
) {
continue;
}
let reason, suggestion;
switch (suppressionRange.source) {
case 'Eslint':
reason =
'React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled';
reason = ErrorCode.BAILOUT_ESLINT_SUPPRESSION;
suggestion =
'Remove the ESLint suppression and address the React error';
break;
case 'Flow':
reason =
'React Compiler has skipped optimizing this component because one or more React rule violations were reported by Flow';
reason = ErrorCode.BAILOUT_FLOW_SUPPRESSION;
suggestion = 'Remove the Flow suppression and address the React error';
break;
default:
@@ -207,21 +180,21 @@ export function suppressionsToCompilerError(
);
}
error.pushDiagnostic(
CompilerDiagnostic.create({
reason: reason,
description: `React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression \`${disableComment.value.trim()}\``,
severity: ErrorSeverity.InvalidReact,
category: ErrorCategory.Suppression,
CompilerDiagnostic.fromCode(reason, {
description: `Found suppression \`${suppressionRange.disableComment.value.trim()}\``,
suggestions: [
{
description: suggestion,
range: [disableComment.start, disableComment.end],
range: [
suppressionRange.disableComment.start,
suppressionRange.disableComment.end,
],
op: CompilerSuggestionOperation.Remove,
},
],
}).withDetail({
kind: 'error',
loc: disableComment.loc ?? null,
loc: suppressionRange.disableComment.loc ?? null,
message: 'Found React rule suppression',
}),
);

View File

@@ -8,31 +8,24 @@
import {NodePath} from '@babel/core';
import * as t from '@babel/types';
import {CompilerError, EnvironmentConfig, ErrorSeverity, Logger} from '..';
import {CompilerError, EnvironmentConfig, Logger} from '..';
import {getOrInsertWith} from '../Utils/utils';
import {Environment, GeneratedSource} from '../HIR';
import {DEFAULT_EXPORT} from '../HIR/Environment';
import {CompileProgramMetadata} from './Program';
import {
CompilerDiagnostic,
CompilerDiagnosticOptions,
ErrorCategory,
} from '../CompilerError';
import {CompilerDiagnostic} from '../CompilerError';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
function throwInvalidReact(
options: Omit<CompilerDiagnosticOptions, 'severity'>,
function logAndThrowDiagnostic(
diagnostic: CompilerDiagnostic,
{logger, filename}: TraversalState,
): never {
const detail: CompilerDiagnosticOptions = {
severity: ErrorSeverity.InvalidReact,
...options,
};
logger?.logEvent(filename, {
kind: 'CompileError',
fnLoc: null,
detail: new CompilerDiagnostic(detail),
detail: diagnostic,
});
CompilerError.throwDiagnostic(detail);
CompilerError.throwDiagnostic(diagnostic);
}
function isAutodepsSigil(
@@ -94,14 +87,9 @@ function assertValidEffectImportReference(
* as it may have already been transformed by the compiler (and not
* memoized).
*/
throwInvalidReact(
{
category: ErrorCategory.AutomaticEffectDependencies,
reason:
'Cannot infer dependencies of this effect. This will break your build!',
description:
'To resolve, either pass a dependency array or fix reported compiler bailout diagnostics.' +
(maybeErrorDiagnostic ? ` ${maybeErrorDiagnostic}` : ''),
logAndThrowDiagnostic(
CompilerDiagnostic.fromCode(ErrorCode.DID_NOT_INFER_DEPS, {
description: maybeErrorDiagnostic ? `${maybeErrorDiagnostic}` : '',
details: [
{
kind: 'error',
@@ -109,7 +97,7 @@ function assertValidEffectImportReference(
loc: parent.node.loc ?? GeneratedSource,
},
],
},
}),
context,
);
}
@@ -126,10 +114,8 @@ function assertValidFireImportReference(
paths[0],
context.transformErrors,
);
throwInvalidReact(
{
category: ErrorCategory.Fire,
reason: '[Fire] Untransformed reference to compiler-required feature.',
logAndThrowDiagnostic(
CompilerDiagnostic.fromCode(ErrorCode.CANNOT_COMPILE_FIRE, {
description:
'Either remove this `fire` call or ensure it is successfully transformed by the compiler' +
maybeErrorDiagnostic
@@ -142,7 +128,7 @@ function assertValidFireImportReference(
loc: paths[0].node.loc ?? GeneratedSource,
},
],
},
}),
context,
);
}

View File

@@ -1,4 +1,5 @@
import {CompilerError, SourceLocation} from '..';
import {ErrorCode} from '../CompilerError';
import {
ConcreteType,
printConcrete,
@@ -12,8 +13,8 @@ export function unsupportedLanguageFeature(
desc: string,
loc: SourceLocation,
): never {
CompilerError.throwInvalidJS({
reason: `Typedchecker does not currently support language feature: ${desc}`,
CompilerError.throwFromCode(ErrorCode.INVALID_JAVASCRIPT_AST, {
description: `Typedchecker does not currently support language feature: ${desc}`,
loc,
});
}
@@ -49,16 +50,16 @@ export function raiseUnificationErrors(
loc,
});
} else if (errs.length === 1) {
CompilerError.throwInvalidJS({
reason: `Unable to unify types because ${printUnificationError(errs[0])}`,
CompilerError.throwFromCode(ErrorCode.INVALID_JAVASCRIPT_AST, {
description: `Unable to unify types because ${printUnificationError(errs[0])}`,
loc,
});
} else {
const messages = errs
.map(err => `\t* ${printUnificationError(err)}`)
.join('\n');
CompilerError.throwInvalidJS({
reason: `Unable to unify types because:\n${messages}`,
CompilerError.throwFromCode(ErrorCode.INVALID_JAVASCRIPT_AST, {
description: `Unable to unify types because:\n${messages}`,
loc,
});
}
@@ -69,21 +70,21 @@ export function unresolvableTypeVariable(
id: VariableId,
loc: SourceLocation,
): never {
CompilerError.throwInvalidJS({
reason: `Unable to resolve free variable ${id} to a concrete type`,
CompilerError.throwFromCode(ErrorCode.INVALID_JAVASCRIPT_AST, {
description: `Unable to resolve free variable ${id} to a concrete type`,
loc,
});
}
export function cannotAddVoid(explicit: boolean, loc: SourceLocation): never {
if (explicit) {
CompilerError.throwInvalidJS({
reason: `Undefined is not a valid operand of \`+\``,
CompilerError.throwFromCode(ErrorCode.INVALID_JAVASCRIPT_AST, {
description: `Undefined is not a valid operand of \`+\``,
loc,
});
} else {
CompilerError.throwInvalidJS({
reason: `Value may be undefined, which is not a valid operand of \`+\``,
CompilerError.throwFromCode(ErrorCode.INVALID_JAVASCRIPT_AST, {
description: `Value may be undefined, which is not a valid operand of \`+\``,
loc,
});
}
@@ -93,8 +94,8 @@ export function unsupportedTypeAnnotation(
desc: string,
loc: SourceLocation,
): never {
CompilerError.throwInvalidJS({
reason: `Typedchecker does not currently support type annotation: ${desc}`,
CompilerError.throwFromCode(ErrorCode.INVALID_JAVASCRIPT_AST, {
description: `Typedchecker does not currently support type annotation: ${desc}`,
loc,
});
}
@@ -106,16 +107,16 @@ export function checkTypeArgumentArity(
loc: SourceLocation,
): void {
if (expected !== actual) {
CompilerError.throwInvalidJS({
reason: `Expected ${desc} to have ${expected} type parameters, got ${actual}`,
CompilerError.throwFromCode(ErrorCode.INVALID_JAVASCRIPT_AST, {
description: `Expected ${desc} to have ${expected} type parameters, got ${actual}`,
loc,
});
}
}
export function notAFunction(desc: string, loc: SourceLocation): void {
CompilerError.throwInvalidJS({
reason: `Cannot call ${desc} because it is not a function`,
CompilerError.throwFromCode(ErrorCode.INVALID_JAVASCRIPT_AST, {
description: `Cannot call ${desc} because it is not a function`,
loc,
});
}
@@ -124,8 +125,8 @@ export function notAPolymorphicFunction(
desc: string,
loc: SourceLocation,
): void {
CompilerError.throwInvalidJS({
reason: `Cannot call ${desc} with type arguments because it is not a polymorphic function`,
CompilerError.throwFromCode(ErrorCode.INVALID_JAVASCRIPT_AST, {
description: `Cannot call ${desc} with type arguments because it is not a polymorphic function`,
loc,
});
}

View File

@@ -12,7 +12,7 @@ import {
CompilerDiagnostic,
CompilerError,
CompilerSuggestionOperation,
ErrorCategory,
ErrorCode,
ErrorSeverity,
} from '../CompilerError';
import {Err, Ok, Result} from '../Utils/Result';
@@ -51,11 +51,6 @@ import {
} from './HIR';
import HIRBuilder, {Bindings, createTemporaryPlace} from './HIRBuilder';
import {BuiltInArrayId} from './ObjectShape';
import {
filterSuppressionsThatAffectNode,
SingleLineSuppressionRange,
suppressionsToCompilerError,
} from '../Entrypoint';
/*
* *******************************************************************************************
@@ -114,8 +109,7 @@ export function lower(
builder.errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.Invariant,
category: ErrorCategory.Invariant,
reason: 'Could not find binding',
category: 'Could not find binding',
description: `[BuildHIR] Could not find binding for param \`${param.node.name}\`.`,
}).withDetail({
kind: 'error',
@@ -177,15 +171,12 @@ export function lower(
);
} else {
builder.errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
reason: `Handle ${param.node.type} parameters`,
CompilerDiagnostic.fromCode(ErrorCode.UNKNOWN_FUNCTION_PARAMETERS, {
description: `[BuildHIR] Add support for ${param.node.type} parameters.`,
}).withDetail({
kind: 'error',
loc: param.node.loc ?? null,
message: 'Unsupported parameter type',
message: `Unsupported parameter type: ${param.node.type}`,
}),
);
}
@@ -209,15 +200,12 @@ export function lower(
directives = body.get('directives').map(d => d.node.value.value);
} else {
builder.errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
reason: `Unexpected function body kind`,
CompilerDiagnostic.fromCode(ErrorCode.INVALID_JAVASCRIPT_AST, {
description: `Expected function body to be an expression or a block statement, got \`${body.type}\`.`,
}).withDetail({
kind: 'error',
loc: body.node.loc ?? null,
message: 'Expected a block statement or expression',
message: 'Change this to a block statement or expression',
}),
);
}
@@ -242,42 +230,6 @@ export function lower(
null,
);
if (bindings == null) {
/**
* Any single-line suppressions which didn't get captured by a call expression
* are thrown as errors from the outermost function being compiled. This is to
* allow suppressions within function expressions that are passed to useEffect,
* eg
*
* ```
* useEffect(() => {
* console.log(foo);
* // eslint-disable-next-line react-hooks/exhaustive-deps
* }, []);
* ```
*
* Where we can't throw an error when exiting the function expression, but rather
* want that suppression to bubble up to the useEffect() call node.
*
* Whereas the following should error since it's at the top-level
*
* ```
* function Component() {
* const f = () => {
* // eslint-disable-next-line react-hooks/exhaustive-deps
* };
* }
* ```
*/
const suppressions = filterSuppressionsThatAffectNode(
env.suppressions,
func,
);
if (suppressions.length !== 0) {
throw suppressionsToCompilerError(suppressions);
}
}
return Ok({
id,
params,
@@ -318,7 +270,6 @@ function lowerStatement(
reason:
'(BuildHIR::lowerStatement) Support ThrowStatement inside of try/catch',
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -506,7 +457,6 @@ function lowerStatement(
} else if (!binding.path.isVariableDeclarator()) {
builder.errors.push({
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
reason: 'Unsupported declaration type for hoisting',
description: `variable "${binding.identifier.name}" declared with ${binding.path.type}`,
suggestions: null,
@@ -516,7 +466,6 @@ function lowerStatement(
} else {
builder.errors.push({
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
reason: 'Handle non-const declarations for hoisting',
description: `variable "${binding.identifier.name}" declared with ${binding.kind}`,
suggestions: null,
@@ -597,7 +546,6 @@ function lowerStatement(
reason:
'(BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement',
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -670,7 +618,6 @@ function lowerStatement(
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle empty test in ForStatement`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -819,13 +766,12 @@ function lowerStatement(
const testExpr = case_.get('test');
if (testExpr.node == null) {
if (hasDefault) {
builder.errors.push({
reason: `Expected at most one \`default\` branch in a switch statement, this code should have failed to parse`,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: case_.node.loc ?? null,
suggestions: null,
});
builder.errors.pushErrorCode(
ErrorCode.INVALID_SYNTAX_MULTIPLE_DEFAULTS,
{
loc: case_.node.loc ?? null,
},
);
break;
}
hasDefault = true;
@@ -895,7 +841,6 @@ function lowerStatement(
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle ${nodeKind} kinds in VariableDeclaration`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -924,7 +869,6 @@ function lowerStatement(
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Could not find binding for declaration.`,
severity: ErrorSeverity.Invariant,
category: ErrorCategory.Invariant,
loc: id.node.loc ?? null,
suggestions: null,
});
@@ -939,20 +883,20 @@ function lowerStatement(
if (builder.isContextIdentifier(id)) {
if (kind === InstructionKind.Const) {
const declRangeStart = declaration.parentPath.node.start!;
builder.errors.push({
reason: `Expect \`const\` declaration not to be reassigned`,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: id.node.loc ?? null,
suggestions: [
{
description: 'Change to a `let` declaration',
op: CompilerSuggestionOperation.Replace,
range: [declRangeStart, declRangeStart + 5], // "const".length
text: 'let',
},
],
});
builder.errors.pushErrorCode(
ErrorCode.INVALID_SYNTAX_REASSIGNED_CONST,
{
loc: id.node.loc ?? null,
suggestions: [
{
description: 'Change to a `let` declaration',
op: CompilerSuggestionOperation.Replace,
range: [declRangeStart, declRangeStart + 5], // "const".length
text: 'let',
},
],
},
);
}
lowerValueToTemporary(builder, {
kind: 'DeclareContext',
@@ -986,14 +930,13 @@ function lowerStatement(
}
}
} else {
builder.errors.push({
reason: `Expected variable declaration to be an identifier if no initializer was provided`,
description: `Got a \`${id.type}\``,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: stmt.node.loc ?? null,
suggestions: null,
});
builder.errors.pushErrorCode(
ErrorCode.INVALID_SYNTAX_BAD_VARIABLE_DECL,
{
description: `Got a \`${id.type}\``,
loc: stmt.node.loc ?? null,
},
);
}
}
return;
@@ -1099,7 +1042,6 @@ function lowerStatement(
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle for-await loops`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -1332,7 +1274,6 @@ function lowerStatement(
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle TryStatement without a catch clause`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -1342,7 +1283,6 @@ function lowerStatement(
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle TryStatement with a finalizer ('finally') clause`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -1432,13 +1372,8 @@ function lowerStatement(
return;
}
case 'WithStatement': {
builder.errors.push({
reason: `JavaScript 'with' syntax is not supported`,
description: `'with' syntax is considered deprecated and removed from JavaScript standards, consider alternatives`,
severity: ErrorSeverity.UnsupportedJS,
category: ErrorCategory.UnsupportedSyntax,
builder.errors.pushErrorCode(ErrorCode.UNSUPPORTED_WITH, {
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
@@ -1453,13 +1388,8 @@ function lowerStatement(
* and complex enough to support that we don't anticipate supporting anytime soon. Developers
* are encouraged to lift classes out of component/hook declarations.
*/
builder.errors.push({
reason: 'Inline `class` declarations are not supported',
description: `Move class declarations outside of components/hooks`,
severity: ErrorSeverity.UnsupportedJS,
category: ErrorCategory.UnsupportedSyntax,
builder.errors.pushErrorCode(ErrorCode.UNSUPPORTED_INNER_CLASS, {
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
@@ -1483,13 +1413,8 @@ function lowerStatement(
case 'ImportDeclaration':
case 'TSExportAssignment':
case 'TSImportEqualsDeclaration': {
builder.errors.push({
reason:
'JavaScript `import` and `export` statements may only appear at the top level of a module',
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
builder.errors.pushErrorCode(ErrorCode.INVALID_IMPORT_EXPORT, {
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
@@ -1499,13 +1424,8 @@ function lowerStatement(
return;
}
case 'TSNamespaceExportDeclaration': {
builder.errors.push({
reason:
'TypeScript `namespace` statements may only appear at the top level of a module',
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
builder.errors.pushErrorCode(ErrorCode.INVALID_TS_NAMESPACE, {
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
@@ -1582,7 +1502,6 @@ function lowerObjectPropertyKey(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: key.node.loc ?? null,
suggestions: null,
});
@@ -1608,7 +1527,6 @@ function lowerObjectPropertyKey(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: key.node.loc ?? null,
suggestions: null,
});
@@ -1666,7 +1584,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${valuePath.type} values in ObjectExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: valuePath.node.loc ?? null,
suggestions: null,
});
@@ -1693,7 +1610,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${propertyPath.node.kind} functions in ObjectExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: propertyPath.node.loc ?? null,
suggestions: null,
});
@@ -1715,7 +1631,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${propertyPath.type} properties in ObjectExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: propertyPath.node.loc ?? null,
suggestions: null,
});
@@ -1749,7 +1664,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${element.type} elements in ArrayExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: element.node.loc ?? null,
suggestions: null,
});
@@ -1766,13 +1680,8 @@ function lowerExpression(
const expr = exprPath as NodePath<t.NewExpression>;
const calleePath = expr.get('callee');
if (!calleePath.isExpression()) {
builder.errors.push({
reason: `Expected an expression as the \`new\` expression receiver (v8 intrinsics are not supported)`,
description: `Got a \`${calleePath.node.type}\``,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
builder.errors.pushErrorCode(ErrorCode.UNSUPPORTED_NEW_EXPRESSION, {
loc: calleePath.node.loc ?? null,
suggestions: null,
});
return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
}
@@ -1797,7 +1706,6 @@ function lowerExpression(
builder.errors.push({
reason: `Expected Expression, got ${calleePath.type} in CallExpression (v8 intrinsics not supported). This error is likely caused by a bug in React Compiler. Please file an issue`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: calleePath.node.loc ?? null,
suggestions: null,
});
@@ -1807,27 +1715,21 @@ function lowerExpression(
const memberExpr = lowerMemberExpression(builder, calleePath);
const propertyPlace = lowerValueToTemporary(builder, memberExpr.value);
const args = lowerArguments(builder, expr.get('arguments'));
const suppressions = consumeSuppressionOnNode(builder, expr);
return {
kind: 'MethodCall',
receiver: memberExpr.object,
property: {...propertyPlace},
args,
loc: exprLoc,
suppressions,
};
} else {
const callee = lowerExpressionToTemporary(builder, calleePath);
const args = lowerArguments(builder, expr.get('arguments'));
const suppressions = consumeSuppressionOnNode(builder, expr);
return {
kind: 'CallExpression',
callee,
args,
loc: exprLoc,
suppressions,
};
}
}
@@ -1838,7 +1740,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Expected Expression, got ${leftPath.type} lval in BinaryExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: leftPath.node.loc ?? null,
suggestions: null,
});
@@ -1851,7 +1752,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Pipe operator not supported`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: leftPath.node.loc ?? null,
suggestions: null,
});
@@ -1878,13 +1778,12 @@ function lowerExpression(
last = lowerExpressionToTemporary(builder, item);
}
if (last === null) {
builder.errors.push({
reason: `Expected sequence expression to have at least one expression`,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: expr.node.loc ?? null,
suggestions: null,
});
builder.errors.pushErrorCode(
ErrorCode.UNSUPPORTED_EMPTY_SEQUENCE_EXPRESSION,
{
loc: expr.node.loc ?? null,
},
);
} else {
lowerValueToTemporary(builder, {
kind: 'StoreLocal',
@@ -2094,7 +1993,6 @@ function lowerExpression(
reason: `(BuildHIR::lowerExpression) Unsupported syntax on the left side of an AssignmentExpression`,
description: `Expected an LVal, got: ${left.type}`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: left.node.loc ?? null,
suggestions: null,
});
@@ -2123,7 +2021,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${operator} operators in AssignmentExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: expr.node.loc ?? null,
suggestions: null,
});
@@ -2223,7 +2120,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Expected Identifier or MemberExpression, got ${expr.type} lval in AssignmentExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: expr.node.loc ?? null,
suggestions: null,
});
@@ -2263,7 +2159,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${attribute.type} attributes in JSXElement`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: attribute.node.loc ?? null,
suggestions: null,
});
@@ -2277,7 +2172,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Unexpected colon in attribute name \`${propName}\``,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: namePath.node.loc ?? null,
suggestions: null,
});
@@ -2308,7 +2202,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${valueExpr.type} attribute values in JSXElement`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: valueExpr.node?.loc ?? null,
suggestions: null,
});
@@ -2319,7 +2212,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${expression.type} expressions in JSXExpressionContainer within JSXElement`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: valueExpr.node.loc ?? null,
suggestions: null,
});
@@ -2375,19 +2267,18 @@ function lowerExpression(
});
for (const [name, locations] of Object.entries(fbtLocations)) {
if (locations.length > 1) {
CompilerError.throwDiagnostic({
severity: ErrorSeverity.Todo,
category: ErrorCategory.FBT,
reason: 'Support duplicate fbt tags',
description: `Support \`<${tagName}>\` tags with multiple \`<${tagName}:${name}>\` values`,
details: locations.map(loc => {
return {
kind: 'error',
message: `Multiple \`<${tagName}:${name}>\` tags found`,
loc,
};
CompilerError.throwDiagnostic(
CompilerDiagnostic.fromCode(ErrorCode.TODO_DUPLICATE_FBT_TAGS, {
description: `Support \`<${tagName}>\` tags with multiple \`<${tagName}:${name}>\` values`,
details: locations.map(loc => {
return {
kind: 'error',
message: `Multiple \`<${tagName}:${name}>\` tags found`,
loc,
};
}),
}),
});
);
}
}
}
@@ -2439,7 +2330,6 @@ function lowerExpression(
reason:
'(BuildHIR::lowerExpression) Handle tagged template with interpolations',
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2458,7 +2348,6 @@ function lowerExpression(
reason:
'(BuildHIR::lowerExpression) Handle tagged template where cooked value is different from raw value',
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2478,12 +2367,8 @@ function lowerExpression(
const quasis = expr.get('quasis');
if (subexprs.length !== quasis.length - 1) {
builder.errors.push({
reason: `Unexpected quasi and subexpression lengths in template literal`,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
builder.errors.pushErrorCode(ErrorCode.INVALID_QUASI_LENGTHS, {
loc: exprPath.node.loc ?? null,
suggestions: null,
});
return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
}
@@ -2492,7 +2377,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle TSType in TemplateLiteral.`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2532,26 +2416,23 @@ function lowerExpression(
};
}
} else {
builder.errors.push({
reason: `Only object properties can be deleted`,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: expr.node.loc ?? null,
suggestions: [
{
description: 'Remove this line',
range: [expr.node.start!, expr.node.end!],
op: CompilerSuggestionOperation.Remove,
},
],
});
builder.errors.pushErrorCode(
ErrorCode.INVALID_SYNTAX_DELETE_EXPRESSION,
{
loc: expr.node.loc ?? null,
suggestions: [
{
description: 'Remove this line',
range: [expr.node.start!, expr.node.end!],
op: CompilerSuggestionOperation.Remove,
},
],
},
);
return {kind: 'UnsupportedNode', node: expr.node, loc: exprLoc};
}
} else if (expr.node.operator === 'throw') {
builder.errors.push({
reason: `Throw expressions are not supported`,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
builder.errors.pushErrorCode(ErrorCode.UNSUPPORTED_THROW_EXPRESSION, {
loc: expr.node.loc ?? null,
suggestions: [
{
@@ -2673,7 +2554,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle UpdateExpression with ${argument.type} argument`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2682,7 +2562,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle UpdateExpression to variables captured within lambdas.`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2703,7 +2582,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Found an invalid UpdateExpression without a previously reported error`,
severity: ErrorSeverity.Invariant,
category: ErrorCategory.Invariant,
loc: exprLoc,
suggestions: null,
});
@@ -2713,7 +2591,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Support UpdateExpression where argument is a global`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprLoc,
suggestions: null,
});
@@ -2769,7 +2646,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle MetaProperty expressions other than import.meta`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2779,7 +2655,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${exprPath.type} expressions`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -3003,7 +2878,6 @@ function lowerOptionalCallExpression(
builder.enterReserved(consequent, () => {
const args = lowerArguments(builder, expr.get('arguments'));
const temp = buildTemporaryPlace(builder, loc);
const suppressions = consumeSuppressionOnNode(builder, expr);
if (callee.kind === 'CallExpression') {
builder.push({
id: makeInstructionId(0),
@@ -3013,7 +2887,6 @@ function lowerOptionalCallExpression(
callee: {...callee.callee},
args,
loc,
suppressions,
},
effects: null,
loc,
@@ -3028,7 +2901,6 @@ function lowerOptionalCallExpression(
property: {...callee.property},
args,
loc,
suppressions,
},
effects: null,
loc,
@@ -3080,7 +2952,6 @@ function lowerReorderableExpression(
builder.errors.push({
reason: `(BuildHIR::node.lowerReorderableExpression) Expression type \`${expr.type}\` cannot be safely reordered`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: expr.node.loc ?? null,
suggestions: null,
});
@@ -3277,7 +3148,6 @@ function lowerArguments(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${argPath.type} arguments in CallExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: argPath.node.loc ?? null,
suggestions: null,
});
@@ -3313,7 +3183,6 @@ function lowerMemberExpression(
builder.errors.push({
reason: `(BuildHIR::lowerMemberExpression) Handle ${propertyNode.type} property`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: propertyNode.node.loc ?? null,
suggestions: null,
});
@@ -3335,7 +3204,6 @@ function lowerMemberExpression(
builder.errors.push({
reason: `(BuildHIR::lowerMemberExpression) Expected Expression, got ${propertyNode.type} property`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: propertyNode.node.loc ?? null,
suggestions: null,
});
@@ -3391,11 +3259,8 @@ function lowerJsxElementName(
const name = exprPath.node.name.name;
const tag = `${namespace}:${name}`;
if (namespace.indexOf(':') !== -1 || name.indexOf(':') !== -1) {
builder.errors.push({
reason: `Expected JSXNamespacedName to have no colons in the namespace or name`,
builder.errors.pushErrorCode(ErrorCode.INVALID_JSX_NAMESPACED_NAME, {
description: `Got \`${namespace}\` : \`${name}\``,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -3410,7 +3275,6 @@ function lowerJsxElementName(
builder.errors.push({
reason: `(BuildHIR::lowerJsxElementName) Handle ${exprPath.type} tags`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -3509,7 +3373,6 @@ function lowerJsxElement(
builder.errors.push({
reason: `(BuildHIR::lowerJsxElement) Unhandled JsxElement, got: ${exprPath.type}`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -3692,12 +3555,7 @@ function lowerIdentifier(
}
default: {
if (binding.kind === 'Global' && binding.name === 'eval') {
builder.errors.push({
reason: `The 'eval' function is not supported`,
description:
'Eval is an anti-pattern in JavaScript, and the code executed cannot be evaluated by React Compiler',
severity: ErrorSeverity.UnsupportedJS,
category: ErrorCategory.UnsupportedSyntax,
builder.errors.pushErrorCode(ErrorCode.UNSUPPORTED_EVAL, {
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -3754,7 +3612,6 @@ function lowerIdentifierForAssignment(
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Could not find binding for declaration.`,
severity: ErrorSeverity.Invariant,
category: ErrorCategory.Invariant,
loc: path.node.loc ?? null,
suggestions: null,
});
@@ -3764,10 +3621,7 @@ function lowerIdentifierForAssignment(
binding.bindingKind === 'const' &&
kind === InstructionKind.Reassign
) {
builder.errors.push({
reason: `Cannot reassign a \`const\` variable`,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
builder.errors.pushErrorCode(ErrorCode.INVALID_SYNTAX_REASSIGNED_CONST, {
loc: path.node.loc ?? null,
description:
binding.identifier.name != null
@@ -3822,13 +3676,13 @@ function lowerAssignment(
let temporary;
if (builder.isContextIdentifier(lvalue)) {
if (kind === InstructionKind.Const && !isHoistedIdentifier) {
builder.errors.push({
reason: `Expected \`const\` declaration not to be reassigned`,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: lvalue.node.loc ?? null,
suggestions: null,
});
builder.errors.pushErrorCode(
ErrorCode.INVALID_SYNTAX_REASSIGNED_CONST,
{
loc: lvalue.node.loc ?? null,
suggestions: null,
},
);
}
if (
@@ -3839,8 +3693,8 @@ function lowerAssignment(
) {
builder.errors.push({
reason: `Unexpected context variable kind`,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
description: `Expected one of Const, Reassign, Let, Function, got ${kind}`,
severity: ErrorSeverity.Invariant,
loc: lvalue.node.loc ?? null,
suggestions: null,
});
@@ -3912,7 +3766,6 @@ function lowerAssignment(
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle ${property.type} properties in MemberExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: property.node.loc ?? null,
suggestions: null,
});
@@ -3925,7 +3778,6 @@ function lowerAssignment(
reason:
'(BuildHIR::lowerAssignment) Expected private name to appear as a non-computed property',
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: property.node.loc ?? null,
suggestions: null,
});
@@ -3991,7 +3843,6 @@ function lowerAssignment(
} else if (identifier.kind === 'Global') {
builder.errors.push({
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
reason:
'Expected reassignment of globals to enable forceTemporaries',
loc: element.node.loc ?? GeneratedSource,
@@ -4031,7 +3882,6 @@ function lowerAssignment(
} else if (identifier.kind === 'Global') {
builder.errors.push({
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
reason:
'Expected reassignment of globals to enable forceTemporaries',
loc: element.node.loc ?? GeneratedSource,
@@ -4105,7 +3955,6 @@ function lowerAssignment(
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle ${argument.node.type} rest element in ObjectPattern`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: argument.node.loc ?? null,
suggestions: null,
});
@@ -4137,7 +3986,6 @@ function lowerAssignment(
} else if (identifier.kind === 'Global') {
builder.errors.push({
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
reason:
'Expected reassignment of globals to enable forceTemporaries',
loc: property.node.loc ?? GeneratedSource,
@@ -4155,7 +4003,6 @@ function lowerAssignment(
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle ${property.type} properties in ObjectPattern`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: property.node.loc ?? null,
suggestions: null,
});
@@ -4165,7 +4012,6 @@ function lowerAssignment(
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: property.node.loc ?? null,
suggestions: null,
});
@@ -4180,7 +4026,6 @@ function lowerAssignment(
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Expected object property value to be an LVal, got: ${element.type}`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: element.node.loc ?? null,
suggestions: null,
});
@@ -4203,7 +4048,6 @@ function lowerAssignment(
} else if (identifier.kind === 'Global') {
builder.errors.push({
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
reason:
'Expected reassignment of globals to enable forceTemporaries',
loc: element.node.loc ?? GeneratedSource,
@@ -4353,7 +4197,6 @@ function lowerAssignment(
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle ${lvaluePath.type} assignments`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: lvaluePath.node.loc ?? null,
suggestions: null,
});
@@ -4527,23 +4370,3 @@ export function lowerType(node: t.FlowType | t.TSType): Type {
}
}
}
/**
* Extracts the (single-line) suppression comments from the environment that are scoped
* to within the given `node`, removing them from the environment's suppressions list.
*
* By calling this function depth-first, we can associate suppressions with the innermost
* call expression that they effect. Unconsumed suppressions are thrown at the parent
* function boundary.
*/
function consumeSuppressionOnNode(
builder: HIRBuilder,
node: NodePath,
): Array<SingleLineSuppressionRange> {
const env = builder.environment;
const suppressions = filterSuppressionsThatAffectNode(env.suppressions, node);
env.suppressions = env.suppressions.filter(
s => suppressions.indexOf(s) === -1,
);
return suppressions;
}

View File

@@ -1,91 +0,0 @@
/**
* 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 {Effect, ValueKind} from '..';
import {TypeConfig} from './TypeSchema';
/**
* Libraries developed before we officially documented the [Rules of React](https://react.dev/reference/rules)
* implement APIs which cannot be memoized safely, either via manual or automatic memoization.
*
* Any non-hook API that is designed to be called during render (not events/effects) should be safe to memoize:
*
* ```js
* function Component() {
* const {someFunction} = useLibrary();
* // it should always be safe to memoize functions like this
* const result = useMemo(() => someFunction(), [someFunction]);
* }
* ```
*
* However, some APIs implement "interior mutability" — mutating values rather than copying into a new value
* and setting state with the new value. Such functions (`someFunction()` in the example) could return different
* values even though the function itself is the same object. This breaks memoization, since React relies on
* the outer object (or function) changing if part of its value has changed.
*
* Given that we didn't have the Rules of React precisely documented prior to the introduction of React compiler,
* it's understandable that some libraries accidentally shipped APIs that break this rule. However, developers
* can easily run into pitfalls with these APIs. They may manually memoize them, which can break their app. Or
* they may try using React Compiler, and think that the compiler has broken their code.
*
* To help ensure that developers can successfully use the compiler with existing code, this file teaches the
* compiler about specific APIs that are known to be incompatible with memoization. We've tried to be as precise
* as possible.
*
* The React team is open to collaborating with library authors to help develop compatible versions of these APIs,
* and we have already reached out to the teams who own any API listed here to ensure they are aware of the issue.
*/
export function defaultModuleTypeProvider(
moduleName: string,
): TypeConfig | null {
switch (moduleName) {
case 'react-hook-form': {
return {
kind: 'object',
properties: {
useForm: {
kind: 'hook',
returnType: {
kind: 'object',
properties: {
// Only the `watch()` function returned by react-hook-form's `useForm()` API is incompatible
watch: {
kind: 'function',
positionalParams: [],
restParam: Effect.Read,
calleeEffect: Effect.Read,
returnType: {kind: 'type', name: 'Any'},
returnValueKind: ValueKind.Mutable,
knownIncompatible: `React Hook Form's \`useForm()\` API returns a \`watch()\` function which cannot be memoized safely.`,
},
},
},
},
},
};
}
case '@tanstack/react-table': {
return {
kind: 'object',
properties: {
/*
* Many of the properties of `useReactTable()`'s return value are incompatible, so we mark the entire hook
* as incompatible
*/
useReactTable: {
kind: 'hook',
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'type', name: 'Any'},
knownIncompatible: `TanStack Table's \`useReactTable()\` API returns functions that cannot be memoized safely`,
},
},
};
}
}
return null;
}

View File

@@ -9,11 +9,7 @@ import * as t from '@babel/types';
import {ZodError, z} from 'zod';
import {fromZodError} from 'zod-validation-error';
import {CompilerError} from '../CompilerError';
import {
Logger,
ProgramContext,
SingleLineSuppressionRange,
} from '../Entrypoint';
import {Logger, ProgramContext} from '../Entrypoint';
import {Err, Ok, Result} from '../Utils/Result';
import {
DEFAULT_GLOBALS,
@@ -54,7 +50,6 @@ import {
import {Scope as BabelScope, NodePath} from '@babel/traverse';
import {TypeSchema} from './TypeSchema';
import {FlowTypeEnv} from '../Flood/Types';
import {defaultModuleTypeProvider} from './DefaultModuleTypeProvider';
export const ReactElementSymbolSchema = z.object({
elementSymbol: z.union([
@@ -98,7 +93,7 @@ export const MacroSchema = z.union([
z.tuple([z.string(), z.array(MacroMethodSchema)]),
]);
export type CompilerMode = 'all_features' | 'no_inferred_memo';
export type CompilerMode = 'all_features' | 'no_inferred_memo' | 'lint_only';
export type Macro = z.infer<typeof MacroSchema>;
export type MacroMethod = z.infer<typeof MacroMethodSchema>;
@@ -255,6 +250,11 @@ export const EnvironmentConfigSchema = z.object({
*/
flowTypeProvider: z.nullable(z.function().args(z.string())).default(null),
/**
* Enable a new model for mutability and aliasing inference
*/
enableNewMutationAliasingModel: z.boolean().default(true),
/**
* Enables inference of optional dependency chains. Without this flag
* a property chain such as `props?.items?.foo` will infer as a dep on
@@ -655,13 +655,6 @@ export const EnvironmentConfigSchema = z.object({
* useMemo(() => { ... }, [...]);
*/
validateNoVoidUseMemo: z.boolean().default(false),
/**
* Validates that Components/Hooks are always defined at module level. This prevents scope
* reference errors that occur when the compiler attempts to optimize the nested component/hook
* while its parent function remains uncompiled.
*/
validateNoDynamicallyCreatedComponentsOrHooks: z.boolean().default(false),
});
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
@@ -706,7 +699,6 @@ export class Environment {
hasFireRewrite: boolean;
hasInferredEffect: boolean;
inferredEffectLocations: Set<SourceLocation> = new Set();
suppressions: Array<SingleLineSuppressionRange>;
#contextIdentifiers: Set<t.Identifier>;
#hoistedIdentifiers: Set<t.Identifier>;
@@ -725,7 +717,6 @@ export class Environment {
filename: string | null,
code: string | null,
programContext: ProgramContext,
suppressions: Array<SingleLineSuppressionRange>,
) {
this.#scope = scope;
this.fnType = fnType;
@@ -739,7 +730,6 @@ export class Environment {
this.#globals = new Map(DEFAULT_GLOBALS);
this.hasFireRewrite = false;
this.hasInferredEffect = false;
this.suppressions = suppressions;
if (
config.disableMemoizationForDebugging &&
@@ -839,6 +829,14 @@ export class Environment {
}
}
logOrThrowErrors(errors: Result<void, CompilerError>): void {
if (this.compilerMode === 'lint_only') {
this.logErrors(errors);
} else {
errors.unwrap();
}
}
isContextIdentifier(node: t.Identifier): boolean {
return this.#contextIdentifiers.has(node);
}
@@ -868,16 +866,10 @@ export class Environment {
#resolveModuleType(moduleName: string, loc: SourceLocation): Global | null {
let moduleType = this.#moduleTypes.get(moduleName);
if (moduleType === undefined) {
/*
* NOTE: Zod doesn't work when specifying a function as a default, so we have to
* fallback to the default value here
*/
const moduleTypeProvider =
this.config.moduleTypeProvider ?? defaultModuleTypeProvider;
if (moduleTypeProvider == null) {
if (this.config.moduleTypeProvider == null) {
return null;
}
const unparsedModuleConfig = moduleTypeProvider(moduleName);
const unparsedModuleConfig = this.config.moduleTypeProvider(moduleName);
if (unparsedModuleConfig != null) {
const parsedModuleConfig = TypeSchema.safeParse(unparsedModuleConfig);
if (!parsedModuleConfig.success) {

View File

@@ -1001,7 +1001,6 @@ export function installTypeConfig(
mutableOnlyIfOperandsAreMutable:
typeConfig.mutableOnlyIfOperandsAreMutable === true,
aliasing: typeConfig.aliasing,
knownIncompatible: typeConfig.knownIncompatible ?? null,
});
}
case 'hook': {
@@ -1020,7 +1019,6 @@ export function installTypeConfig(
returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen,
noAlias: typeConfig.noAlias === true,
aliasing: typeConfig.aliasing,
knownIncompatible: typeConfig.knownIncompatible ?? null,
});
}
case 'object': {

View File

@@ -7,7 +7,7 @@
import {BindingKind} from '@babel/traverse';
import * as t from '@babel/types';
import {CompilerError} from '../CompilerError';
import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError';
import {assertExhaustive} from '../Utils/utils';
import {Environment, ReactFunctionType} from './Environment';
import type {HookKind} from './ObjectShape';
@@ -15,7 +15,7 @@ import {Type, makeType} from './Types';
import {z} from 'zod';
import type {AliasingEffect} from '../Inference/AliasingEffects';
import {isReservedWord} from '../Utils/Keyword';
import {SingleLineSuppressionRange} from '../Entrypoint';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
/*
* *******************************************************************************************
@@ -283,13 +283,30 @@ export type HIRFunction = {
returnTypeAnnotation: t.FlowType | t.TSType | null;
returns: Place;
context: Array<Place>;
effects: Array<FunctionEffect> | null;
body: HIR;
generator: boolean;
async: boolean;
directives: Array<string>;
aliasingEffects: Array<AliasingEffect> | null;
aliasingEffects?: Array<AliasingEffect> | null;
};
export type FunctionEffect =
| {
kind: 'GlobalMutation';
error: CompilerErrorDetailOptions;
}
| {
kind: 'ReactMutation';
error: CompilerErrorDetailOptions;
}
| {
kind: 'ContextMutation';
places: ReadonlySet<Place>;
effect: Effect;
loc: SourceLocation;
};
/*
* Each reactive scope may have its own control-flow, so the instructions form
* a control-flow graph. The graph comprises a set of basic blocks which reference
@@ -844,7 +861,6 @@ export type MethodCall = {
property: Place;
args: Array<Place | SpreadPattern>;
loc: SourceLocation;
suppressions?: Array<SingleLineSuppressionRange>;
};
export type CallExpression = {
@@ -853,7 +869,6 @@ export type CallExpression = {
args: Array<Place | SpreadPattern>;
loc: SourceLocation;
typeArguments?: Array<t.FlowType>;
suppressions?: Array<SingleLineSuppressionRange>;
};
export type NewExpression = {
@@ -1308,18 +1323,18 @@ export function forkTemporaryIdentifier(
*/
export function makeIdentifierName(name: string): ValidatedIdentifier {
if (isReservedWord(name)) {
CompilerError.throwInvalidJS({
reason: 'Expected a non-reserved identifier name',
loc: GeneratedSource,
description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`,
suggestions: null,
});
CompilerError.throwFromCode(
ErrorCode.INVALID_SYNTAX_RESERVED_VARIABLE_NAME,
{
loc: GeneratedSource,
description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`,
},
);
} else {
CompilerError.invariant(t.isValidIdentifier(name), {
reason: `Expected a valid identifier name`,
loc: GeneratedSource,
description: `\`${name}\` is not a valid JavaScript identifier`,
suggestions: null,
});
}
return {

View File

@@ -7,7 +7,7 @@
import {Binding, NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {CompilerError, ErrorCategory, ErrorSeverity} from '../CompilerError';
import {CompilerDiagnostic, CompilerError} from '../CompilerError';
import {Environment} from './Environment';
import {
BasicBlock,
@@ -37,6 +37,7 @@ import {
mapTerminalSuccessors,
terminalFallthrough,
} from './visitors';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
/*
* *******************************************************************************************
@@ -308,36 +309,17 @@ export default class HIRBuilder {
resolveBinding(node: t.Identifier): Identifier {
if (node.name === 'fbt') {
CompilerError.throwDiagnostic({
severity: ErrorSeverity.Todo,
category: ErrorCategory.FBT,
reason: 'Support local variables named `fbt`',
description:
'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',
details: [
{
kind: 'error',
message: 'Rename to avoid conflict with fbt plugin',
loc: node.loc ?? GeneratedSource,
},
],
});
}
if (node.name === 'this') {
CompilerError.throwDiagnostic({
severity: ErrorSeverity.UnsupportedJS,
category: ErrorCategory.UnsupportedSyntax,
reason: '`this` is not supported syntax',
description:
'React Compiler does not support compiling functions that use `this`',
details: [
{
kind: 'error',
message: '`this` was used here',
loc: node.loc ?? GeneratedSource,
},
],
});
CompilerError.throwDiagnostic(
CompilerDiagnostic.fromCode(ErrorCode.TODO_CONFLICTING_FBT_IDENTIFIER, {
details: [
{
kind: 'error',
message: 'Rename to avoid conflict with fbt plugin',
loc: node.loc ?? GeneratedSource,
},
],
}),
);
}
const originalName = node.name;
let name = originalName;

View File

@@ -332,7 +332,6 @@ export type FunctionSignature = {
mutableOnlyIfOperandsAreMutable?: boolean;
impure?: boolean;
knownIncompatible?: string | null | undefined;
canonicalName?: string;

View File

@@ -554,11 +554,23 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
const context = instrValue.loweredFunc.func.context
.map(dep => printPlace(dep))
.join(',');
const effects =
instrValue.loweredFunc.func.effects
?.map(effect => {
if (effect.kind === 'ContextMutation') {
return `ContextMutation places=[${[...effect.places]
.map(place => printPlace(place))
.join(', ')}] effect=${effect.effect}`;
} else {
return `GlobalMutation`;
}
})
.join(', ') ?? '';
const aliasingEffects =
instrValue.loweredFunc.func.aliasingEffects
?.map(printAliasingEffect)
?.join(', ') ?? '';
value = `${kind} ${name} @context[${context}] @aliasingEffects=[${aliasingEffects}]\n${fn}`;
value = `${kind} ${name} @context[${context}] @effects[${effects}] @aliasingEffects=[${aliasingEffects}]\n${fn}`;
break;
}
case 'TaggedTemplateExpression': {
@@ -880,8 +892,7 @@ export function printType(type: Type): string {
if (type.kind === 'Object' && type.shapeId != null) {
return `:T${type.kind}<${type.shapeId}>`;
} else if (type.kind === 'Function' && type.shapeId != null) {
const returnType = printType(type.return);
return `:T${type.kind}<${type.shapeId}>()${returnType !== '' ? `: ${returnType}` : ''}`;
return `:T${type.kind}<${type.shapeId}>`;
} else {
return `:T${type.kind}`;
}
@@ -984,16 +995,16 @@ export function printAliasingEffect(effect: AliasingEffect): string {
case 'MutateConditionally':
case 'MutateTransitive':
case 'MutateTransitiveConditionally': {
return `${effect.kind} ${printPlaceForAliasEffect(effect.value)}${effect.kind === 'Mutate' && effect.reason?.kind === 'AssignCurrentProperty' ? ' (assign `.current`)' : ''}`;
return `${effect.kind} ${printPlaceForAliasEffect(effect.value)}`;
}
case 'MutateFrozen': {
return `MutateFrozen ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
return `MutateFrozen ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.category)}`;
}
case 'MutateGlobal': {
return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.category)}`;
}
case 'Impure': {
return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.category)}`;
}
case 'Render': {
return `Render ${printPlaceForAliasEffect(effect.place)}`;

View File

@@ -251,7 +251,6 @@ export type FunctionTypeConfig = {
impure?: boolean | null | undefined;
canonicalName?: string | null | undefined;
aliasing?: AliasingSignatureConfig | null | undefined;
knownIncompatible?: string | null | undefined;
};
export const FunctionTypeSchema: z.ZodType<FunctionTypeConfig> = z.object({
kind: z.literal('function'),
@@ -265,7 +264,6 @@ export const FunctionTypeSchema: z.ZodType<FunctionTypeConfig> = z.object({
impure: z.boolean().nullable().optional(),
canonicalName: z.string().nullable().optional(),
aliasing: AliasingSignatureSchema.nullable().optional(),
knownIncompatible: z.string().nullable().optional(),
});
export type HookTypeConfig = {
@@ -276,7 +274,6 @@ export type HookTypeConfig = {
returnValueKind?: ValueKind | null | undefined;
noAlias?: boolean | null | undefined;
aliasing?: AliasingSignatureConfig | null | undefined;
knownIncompatible?: string | null | undefined;
};
export const HookTypeSchema: z.ZodType<HookTypeConfig> = z.object({
kind: z.literal('hook'),
@@ -286,7 +283,6 @@ export const HookTypeSchema: z.ZodType<HookTypeConfig> = z.object({
returnValueKind: ValueKindSchema.nullable().optional(),
noAlias: z.boolean().nullable().optional(),
aliasing: AliasingSignatureSchema.nullable().optional(),
knownIncompatible: z.string().nullable().optional(),
});
export type BuiltInTypeConfig =

View File

@@ -50,7 +50,7 @@ export type AliasingEffect =
/**
* Mutate the value and any direct aliases (not captures). Errors if the value is not mutable.
*/
| {kind: 'Mutate'; value: Place; reason?: MutationReason | null}
| {kind: 'Mutate'; value: Place}
/**
* Mutate the value and any direct aliases (not captures), but only if the value is known mutable.
* This should be rare.
@@ -174,8 +174,6 @@ export type AliasingEffect =
place: Place;
};
export type MutationReason = {kind: 'AssignCurrentProperty'};
export function hashEffect(effect: AliasingEffect): string {
switch (effect.kind) {
case 'Apply': {
@@ -231,7 +229,7 @@ export function hashEffect(effect: AliasingEffect): string {
effect.kind,
effect.place.identifier.id,
effect.error.severity,
effect.error.reason,
effect.error.category,
effect.error.description,
printSourceLocation(effect.error.primaryLocation() ?? GeneratedSource),
].join(':');

View File

@@ -6,10 +6,20 @@
*/
import {CompilerError} from '../CompilerError';
import {Effect, HIRFunction, IdentifierId, makeInstructionId} from '../HIR';
import {
Effect,
HIRFunction,
Identifier,
IdentifierId,
LoweredFunction,
isRefOrRefValue,
makeInstructionId,
} from '../HIR';
import {deadCodeElimination} from '../Optimization';
import {inferReactiveScopeVariables} from '../ReactiveScopes';
import {rewriteInstructionKindsBasedOnReassignment} from '../SSA';
import {inferMutableRanges} from './InferMutableRanges';
import inferReferenceEffects from './InferReferenceEffects';
import {assertExhaustive} from '../Utils/utils';
import {inferMutationAliasingEffects} from './InferMutationAliasingEffects';
import {inferMutationAliasingRanges} from './InferMutationAliasingRanges';
@@ -20,7 +30,12 @@ export default function analyseFunctions(func: HIRFunction): void {
switch (instr.value.kind) {
case 'ObjectMethod':
case 'FunctionExpression': {
lowerWithMutationAliasing(instr.value.loweredFunc.func);
if (!func.env.config.enableNewMutationAliasingModel) {
lower(instr.value.loweredFunc.func);
infer(instr.value.loweredFunc);
} else {
lowerWithMutationAliasing(instr.value.loweredFunc.func);
}
/**
* Reset mutable range for outer inferReferenceEffects
@@ -125,3 +140,58 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
value: fn,
});
}
function lower(func: HIRFunction): void {
analyseFunctions(func);
inferReferenceEffects(func, {isFunctionExpression: true});
deadCodeElimination(func);
inferMutableRanges(func);
rewriteInstructionKindsBasedOnReassignment(func);
inferReactiveScopeVariables(func);
func.env.logger?.debugLogIRs?.({
kind: 'hir',
name: 'AnalyseFunction (inner)',
value: func,
});
}
function infer(loweredFunc: LoweredFunction): void {
for (const operand of loweredFunc.func.context) {
const identifier = operand.identifier;
CompilerError.invariant(operand.effect === Effect.Unknown, {
reason:
'[AnalyseFunctions] Expected Function context effects to not have been set',
loc: operand.loc,
});
if (isRefOrRefValue(identifier)) {
/*
* TODO: this is a hack to ensure we treat functions which reference refs
* as having a capture and therefore being considered mutable. this ensures
* the function gets a mutable range which accounts for anywhere that it
* could be called, and allows us to help ensure it isn't called during
* render
*/
operand.effect = Effect.Capture;
} else if (isMutatedOrReassigned(identifier)) {
/**
* Reflects direct reassignments, PropertyStores, and ConditionallyMutate
* (directly or through maybe-aliases)
*/
operand.effect = Effect.Capture;
} else {
operand.effect = Effect.Read;
}
}
}
function isMutatedOrReassigned(id: Identifier): boolean {
/*
* This check checks for mutation and reassingnment, so the usual check for
* mutation (ie, `mutableRange.end - mutableRange.start > 1`) isn't quite
* enough.
*
* We need to track re-assignments in context refs as we need to reflect the
* re-assignment back to the captured refs.
*/
return id.mutableRange.end > id.mutableRange.start;
}

View File

@@ -5,13 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {
CompilerDiagnostic,
CompilerError,
ErrorSeverity,
SourceLocation,
} from '..';
import {ErrorCategory} from '../CompilerError';
import {CompilerDiagnostic, CompilerError, SourceLocation} from '..';
import {
CallExpression,
Effect,
@@ -36,6 +30,7 @@ import {
makeInstructionId,
} from '../HIR';
import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
import {Result} from '../Utils/Result';
type ManualMemoCallee = {
@@ -255,7 +250,6 @@ function getManualMemoizationReplacement(
*/
args: [],
loc,
suppressions: [],
};
} else {
/*
@@ -301,13 +295,11 @@ function extractManualMemoizationArgs(
>;
if (fnPlace == null) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
severity: ErrorSeverity.InvalidReact,
reason: `Expected a callback function to be passed to ${kind}`,
description: `Expected a callback function to be passed to ${kind}`,
suggestions: null,
}).withDetail({
CompilerDiagnostic.fromCode(
kind === 'useMemo'
? ErrorCode.INVALID_USE_MEMO_NO_ARG0
: ErrorCode.INVALID_USE_CALLBACK_NO_ARG0,
).withDetail({
kind: 'error',
loc: instr.value.loc,
message: `Expected a callback function to be passed to ${kind}`,
@@ -317,13 +309,11 @@ function extractManualMemoizationArgs(
}
if (fnPlace.kind === 'Spread' || depsListPlace?.kind === 'Spread') {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
severity: ErrorSeverity.InvalidReact,
reason: `Unexpected spread argument to ${kind}`,
description: `Unexpected spread argument to ${kind}`,
suggestions: null,
}).withDetail({
CompilerDiagnostic.fromCode(
fnPlace.kind === 'Spread'
? ErrorCode.DYNAMIC_USE_MEMO_SPREAD_ARGUMENT
: ErrorCode.DYNAMIC_USE_CALLBACK_SPREAD_ARGUMENT,
).withDetail({
kind: 'error',
loc: instr.value.loc,
message: `Unexpected spread argument to ${kind}`,
@@ -338,13 +328,9 @@ function extractManualMemoizationArgs(
);
if (maybeDepsList == null) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
severity: ErrorSeverity.InvalidReact,
reason: `Expected the dependency list for ${kind} to be an array literal`,
description: `Expected the dependency list for ${kind} to be an array literal`,
suggestions: null,
}).withDetail({
CompilerDiagnostic.fromCode(
ErrorCode.DYNAMIC_MANUAL_MEMO_DEPENDENCY_LIST,
).withDetail({
kind: 'error',
loc: depsListPlace.loc,
message: `Expected the dependency list for ${kind} to be an array literal`,
@@ -357,13 +343,9 @@ function extractManualMemoizationArgs(
const maybeDep = sidemap.maybeDeps.get(dep.identifier.id);
if (maybeDep == null) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
severity: ErrorSeverity.InvalidReact,
reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
description: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
suggestions: null,
}).withDetail({
CompilerDiagnostic.fromCode(
ErrorCode.COMPLEX_MANUAL_MEMO_DEPENDENCY_LIST_ENTRY,
).withDetail({
kind: 'error',
loc: dep.loc,
message: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
@@ -463,17 +445,16 @@ export function dropManualMemoization(
if (funcToCheck !== undefined && funcToCheck.loweredFunc.func) {
if (!hasNonVoidReturn(funcToCheck.loweredFunc.func)) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: ErrorCategory.UseMemo,
reason: 'useMemo() callbacks must return a value',
description: `This ${
manualMemo.loadInstr.value.kind === 'PropertyLoad'
? 'React.useMemo'
: 'useMemo'
} callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects.`,
suggestions: null,
}).withDetail({
CompilerDiagnostic.fromCode(
ErrorCode.INVALID_USE_MEMO_CALLBACK_RETURN,
{
description: `This ${
manualMemo.loadInstr.value.kind === 'PropertyLoad'
? 'React.useMemo'
: 'useMemo'
} callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects.`,
},
).withDetail({
kind: 'error',
loc: instr.value.loc,
message: 'useMemo() callbacks must return a value',
@@ -504,13 +485,9 @@ export function dropManualMemoization(
*/
if (!sidemap.functions.has(fnPlace.identifier.id)) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
severity: ErrorSeverity.InvalidReact,
reason: `Expected the first argument to be an inline function expression`,
description: `Expected the first argument to be an inline function expression`,
suggestions: [],
}).withDetail({
CompilerDiagnostic.fromCode(
ErrorCode.DYNAMIC_MANUAL_MEMO_CALLBACK,
).withDetail({
kind: 'error',
loc: fnPlace.loc,
message: `Expected the first argument to be an inline function expression`,

View File

@@ -0,0 +1,134 @@
/**
* 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 {
Effect,
HIRFunction,
Identifier,
isMutableEffect,
isRefOrRefLikeMutableType,
makeInstructionId,
} from '../HIR/HIR';
import {eachInstructionValueOperand} from '../HIR/visitors';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
import DisjointSet from '../Utils/DisjointSet';
/**
* If a function captures a mutable value but never gets called, we don't infer a
* mutable range for that function. This means that we also don't alias the function
* with its mutable captures.
*
* This case is tricky, because we don't generally know for sure what is a mutation
* and what may just be a normal function call. For example:
*
* ```
* hook useFoo() {
* const x = makeObject();
* return () => {
* return readObject(x); // could be a mutation!
* }
* }
* ```
*
* If we pessimistically assume that all such cases are mutations, we'd have to group
* lots of memo scopes together unnecessarily. However, if there is definitely a mutation:
*
* ```
* hook useFoo(createEntryForKey) {
* const cache = new WeakMap();
* return (key) => {
* let entry = cache.get(key);
* if (entry == null) {
* entry = createEntryForKey(key);
* cache.set(key, entry); // known mutation!
* }
* return entry;
* }
* }
* ```
*
* Then we have to ensure that the function and its mutable captures alias together and
* end up in the same scope. However, aliasing together isn't enough if the function
* and operands all have empty mutable ranges (end = start + 1).
*
* This pass finds function expressions and object methods that have an empty mutable range
* and known-mutable operands which also don't have a mutable range, and ensures that the
* function and those operands are aliased together *and* that their ranges are updated to
* end after the function expression. This is sufficient to ensure that a reactive scope is
* created for the alias set.
*/
export function inferAliasForUncalledFunctions(
fn: HIRFunction,
aliases: DisjointSet<Identifier>,
): void {
for (const block of fn.body.blocks.values()) {
instrs: for (const instr of block.instructions) {
const {lvalue, value} = instr;
if (
value.kind !== 'ObjectMethod' &&
value.kind !== 'FunctionExpression'
) {
continue;
}
/*
* If the function is known to be mutated, we will have
* already aliased any mutable operands with it
*/
const range = lvalue.identifier.mutableRange;
if (range.end > range.start + 1) {
continue;
}
/*
* If the function already has operands with an active mutable range,
* then we don't need to do anything — the function will have already
* been visited and included in some mutable alias set. This case can
* also occur due to visiting the same function in an earlier iteration
* of the outer fixpoint loop.
*/
for (const operand of eachInstructionValueOperand(value)) {
if (isMutable(instr, operand)) {
continue instrs;
}
}
const operands: Set<Identifier> = new Set();
for (const effect of value.loweredFunc.func.effects ?? []) {
if (effect.kind !== 'ContextMutation') {
continue;
}
/*
* We're looking for known-mutations only, so we look at the effects
* rather than function context
*/
if (effect.effect === Effect.Store || effect.effect === Effect.Mutate) {
for (const operand of effect.places) {
/*
* It's possible that function effect analysis thinks there was a context mutation,
* but then InferReferenceEffects figures out some operands are globals and therefore
* creates a non-mutable effect for those operands.
* We should change InferReferenceEffects to swap the ContextMutation for a global
* mutation in that case, but for now we just filter them out here
*/
if (
isMutableEffect(operand.effect, operand.loc) &&
!isRefOrRefLikeMutableType(operand.identifier.type)
) {
operands.add(operand.identifier);
}
}
}
}
if (operands.size !== 0) {
operands.add(lvalue.identifier);
aliases.union([...operands]);
// Update mutable ranges, if the ranges are empty then a reactive scope isn't created
for (const operand of operands) {
operand.mutableRange.end = makeInstructionId(instr.id + 1);
}
}
}
}
}

View File

@@ -0,0 +1,68 @@
/**
* 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 {
HIRFunction,
Identifier,
Instruction,
isPrimitiveType,
Place,
} from '../HIR/HIR';
import DisjointSet from '../Utils/DisjointSet';
export type AliasSet = Set<Identifier>;
export function inferAliases(func: HIRFunction): DisjointSet<Identifier> {
const aliases = new DisjointSet<Identifier>();
for (const [_, block] of func.body.blocks) {
for (const instr of block.instructions) {
inferInstr(instr, aliases);
}
}
return aliases;
}
function inferInstr(
instr: Instruction,
aliases: DisjointSet<Identifier>,
): void {
const {lvalue, value: instrValue} = instr;
let alias: Place | null = null;
switch (instrValue.kind) {
case 'LoadLocal':
case 'LoadContext': {
if (isPrimitiveType(instrValue.place.identifier)) {
return;
}
alias = instrValue.place;
break;
}
case 'StoreLocal':
case 'StoreContext': {
alias = instrValue.value;
break;
}
case 'Destructure': {
alias = instrValue.value;
break;
}
case 'ComputedLoad':
case 'PropertyLoad': {
alias = instrValue.object;
break;
}
case 'TypeCastExpression': {
alias = instrValue.value;
break;
}
default:
return;
}
aliases.union([lvalue.identifier, alias.identifier]);
}

View File

@@ -0,0 +1,27 @@
/**
* 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 {HIRFunction, Identifier} from '../HIR/HIR';
import DisjointSet from '../Utils/DisjointSet';
export function inferAliasForPhis(
func: HIRFunction,
aliases: DisjointSet<Identifier>,
): void {
for (const [_, block] of func.body.blocks) {
for (const phi of block.phis) {
const isPhiMutatedAfterCreation: boolean =
phi.place.identifier.mutableRange.end >
(block.instructions.at(0)?.id ?? block.terminal.id);
if (isPhiMutatedAfterCreation) {
for (const [, operand] of phi.operands) {
aliases.union([phi.place.identifier, operand.identifier]);
}
}
}
}
}

View File

@@ -0,0 +1,68 @@
/**
* 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 {
Effect,
HIRFunction,
Identifier,
InstructionId,
Place,
} from '../HIR/HIR';
import {
eachInstructionLValue,
eachInstructionValueOperand,
} from '../HIR/visitors';
import DisjointSet from '../Utils/DisjointSet';
export function inferAliasForStores(
func: HIRFunction,
aliases: DisjointSet<Identifier>,
): void {
for (const [_, block] of func.body.blocks) {
for (const instr of block.instructions) {
const {value, lvalue} = instr;
const isStore =
lvalue.effect === Effect.Store ||
/*
* Some typed functions annotate callees or arguments
* as Effect.Store.
*/
![...eachInstructionValueOperand(value)].every(
operand => operand.effect !== Effect.Store,
);
if (!isStore) {
continue;
}
for (const operand of eachInstructionLValue(instr)) {
maybeAlias(aliases, lvalue, operand, instr.id);
}
for (const operand of eachInstructionValueOperand(value)) {
if (
operand.effect === Effect.Capture ||
operand.effect === Effect.Store
) {
maybeAlias(aliases, lvalue, operand, instr.id);
}
}
}
}
}
function maybeAlias(
aliases: DisjointSet<Identifier>,
lvalue: Place,
rvalue: Place,
id: InstructionId,
): void {
if (
lvalue.identifier.mutableRange.end > id + 1 ||
rvalue.identifier.mutableRange.end > id
) {
aliases.union([lvalue.identifier, rvalue.identifier]);
}
}

View File

@@ -0,0 +1,347 @@
/**
* 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 {
CompilerError,
CompilerErrorDetailOptions,
ErrorSeverity,
ValueKind,
} from '..';
import {
AbstractValue,
BasicBlock,
Effect,
Environment,
FunctionEffect,
Instruction,
InstructionValue,
Place,
ValueReason,
getHookKind,
isRefOrRefValue,
} from '../HIR';
import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
import {assertExhaustive} from '../Utils/utils';
interface State {
kind(place: Place): AbstractValue;
values(place: Place): Array<InstructionValue>;
isDefined(place: Place): boolean;
}
function inferOperandEffect(state: State, place: Place): null | FunctionEffect {
const value = state.kind(place);
CompilerError.invariant(value != null, {
reason: 'Expected operand to have a kind',
loc: null,
});
switch (place.effect) {
case Effect.Store:
case Effect.Mutate: {
if (isRefOrRefValue(place.identifier)) {
break;
} else if (value.kind === ValueKind.Context) {
CompilerError.invariant(value.context.size > 0, {
reason:
"[InferFunctionEffects] Expected Context-kind value's capture list to be non-empty.",
loc: place.loc,
});
return {
kind: 'ContextMutation',
loc: place.loc,
effect: place.effect,
places: value.context,
};
} else if (
value.kind !== ValueKind.Mutable &&
// We ignore mutations of primitives since this is not a React-specific problem
value.kind !== ValueKind.Primitive
) {
let errorCode = getWriteErrorReason(value);
return {
kind:
value.reason.size === 1 && value.reason.has(ValueReason.Global)
? 'GlobalMutation'
: 'ReactMutation',
error: {
errorCode,
description:
place.identifier.name !== null &&
place.identifier.name.kind === 'named'
? `Found mutation of \`${place.identifier.name.value}\``
: null,
loc: place.loc,
},
};
}
break;
}
}
return null;
}
function inheritFunctionEffects(
state: State,
place: Place,
): Array<FunctionEffect> {
const effects = inferFunctionInstrEffects(state, place);
return effects
.flatMap(effect => {
if (effect.kind === 'GlobalMutation' || effect.kind === 'ReactMutation') {
return [effect];
} else {
const effects: Array<FunctionEffect | null> = [];
CompilerError.invariant(effect.kind === 'ContextMutation', {
reason: 'Expected ContextMutation',
loc: null,
});
/**
* Contextual effects need to be replayed against the current inference
* state, which may know more about the value to which the effect applied.
* The main cases are:
* 1. The mutated context value is _still_ a context value in the current scope,
* so we have to continue propagating the original context mutation.
* 2. The mutated context value is a mutable value in the current scope,
* so the context mutation was fine and we can skip propagating the effect.
* 3. The mutated context value is an immutable value in the current scope,
* resulting in a non-ContextMutation FunctionEffect. We propagate that new,
* more detailed effect to the current function context.
*/
for (const place of effect.places) {
if (state.isDefined(place)) {
const replayedEffect = inferOperandEffect(state, {
...place,
loc: effect.loc,
effect: effect.effect,
});
if (replayedEffect != null) {
if (replayedEffect.kind === 'ContextMutation') {
// Case 1, still a context value so propagate the original effect
effects.push(effect);
} else {
// Case 3, immutable value so propagate the more precise effect
effects.push(replayedEffect);
}
} // else case 2, local mutable value so this effect was fine
}
}
return effects;
}
})
.filter((effect): effect is FunctionEffect => effect != null);
}
function inferFunctionInstrEffects(
state: State,
place: Place,
): Array<FunctionEffect> {
const effects: Array<FunctionEffect> = [];
const instrs = state.values(place);
CompilerError.invariant(instrs != null, {
reason: 'Expected operand to have instructions',
loc: null,
});
for (const instr of instrs) {
if (
(instr.kind === 'FunctionExpression' || instr.kind === 'ObjectMethod') &&
instr.loweredFunc.func.effects != null
) {
effects.push(...instr.loweredFunc.func.effects);
}
}
return effects;
}
function operandEffects(
state: State,
place: Place,
filterRenderSafe: boolean,
): Array<FunctionEffect> {
const functionEffects: Array<FunctionEffect> = [];
const effect = inferOperandEffect(state, place);
effect && functionEffects.push(effect);
functionEffects.push(...inheritFunctionEffects(state, place));
if (filterRenderSafe) {
return functionEffects.filter(effect => !isEffectSafeOutsideRender(effect));
} else {
return functionEffects;
}
}
export function inferInstructionFunctionEffects(
env: Environment,
state: State,
instr: Instruction,
): Array<FunctionEffect> {
const functionEffects: Array<FunctionEffect> = [];
switch (instr.value.kind) {
case 'JsxExpression': {
if (instr.value.tag.kind === 'Identifier') {
functionEffects.push(...operandEffects(state, instr.value.tag, false));
}
instr.value.children?.forEach(child =>
functionEffects.push(...operandEffects(state, child, false)),
);
for (const attr of instr.value.props) {
if (attr.kind === 'JsxSpreadAttribute') {
functionEffects.push(...operandEffects(state, attr.argument, false));
} else {
functionEffects.push(...operandEffects(state, attr.place, true));
}
}
break;
}
case 'ObjectMethod':
case 'FunctionExpression': {
/**
* If this function references other functions, propagate the referenced function's
* effects to this function.
*
* ```
* let f = () => global = true;
* let g = () => f();
* g();
* ```
*
* In this example, because `g` references `f`, we propagate the GlobalMutation from
* `f` to `g`. Thus, referencing `g` in `g()` will evaluate the GlobalMutation in the outer
* function effect context and report an error. But if instead we do:
*
* ```
* let f = () => global = true;
* let g = () => f();
* useEffect(() => g(), [g])
* ```
*
* Now `g`'s effects will be discarded since they're in a useEffect.
*/
for (const operand of eachInstructionOperand(instr)) {
instr.value.loweredFunc.func.effects ??= [];
instr.value.loweredFunc.func.effects.push(
...inferFunctionInstrEffects(state, operand),
);
}
break;
}
case 'MethodCall':
case 'CallExpression': {
let callee;
if (instr.value.kind === 'MethodCall') {
callee = instr.value.property;
functionEffects.push(
...operandEffects(state, instr.value.receiver, false),
);
} else {
callee = instr.value.callee;
}
functionEffects.push(...operandEffects(state, callee, false));
let isHook = getHookKind(env, callee.identifier) != null;
for (const arg of instr.value.args) {
const place = arg.kind === 'Identifier' ? arg : arg.place;
/*
* Join the effects of the argument with the effects of the enclosing function,
* unless the we're detecting a global mutation inside a useEffect hook
*/
functionEffects.push(...operandEffects(state, place, isHook));
}
break;
}
case 'StartMemoize':
case 'FinishMemoize':
case 'LoadLocal':
case 'StoreLocal': {
break;
}
case 'StoreGlobal': {
functionEffects.push({
kind: 'GlobalMutation',
error: {
errorCode: ErrorCode.INVALID_WRITE_GLOBAL,
loc: instr.loc,
},
});
break;
}
default: {
for (const operand of eachInstructionOperand(instr)) {
functionEffects.push(...operandEffects(state, operand, false));
}
}
}
return functionEffects;
}
export function inferTerminalFunctionEffects(
state: State,
block: BasicBlock,
): Array<FunctionEffect> {
const functionEffects: Array<FunctionEffect> = [];
for (const operand of eachTerminalOperand(block.terminal)) {
functionEffects.push(...operandEffects(state, operand, true));
}
return functionEffects;
}
export function transformFunctionEffectErrors(
functionEffects: Array<FunctionEffect>,
): Array<CompilerErrorDetailOptions> {
return functionEffects.map(eff => {
switch (eff.kind) {
case 'ReactMutation':
case 'GlobalMutation': {
return eff.error;
}
case 'ContextMutation': {
return {
severity: ErrorSeverity.Invariant,
reason: `Unexpected ContextMutation in top-level function effects`,
loc: eff.loc,
};
}
default:
assertExhaustive(
eff,
`Unexpected function effect kind \`${(eff as any).kind}\``,
);
}
});
}
function isEffectSafeOutsideRender(effect: FunctionEffect): boolean {
return effect.kind === 'GlobalMutation';
}
export function getWriteErrorReason(abstractValue: AbstractValue): ErrorCode {
if (abstractValue.reason.has(ValueReason.Global)) {
return ErrorCode.INVALID_WRITE_GLOBAL;
} else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
return ErrorCode.INVALID_WRITE_FROZEN_VALUE_JSX;
} else if (abstractValue.reason.has(ValueReason.Context)) {
return ErrorCode.INVALID_WRITE_IMMUTABLE_VALUE_USE_CONTEXT;
} else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
return ErrorCode.INVALID_WRITE_IMMUTABLE_VALUE_KNOWN_SIGNATURE;
} else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
return ErrorCode.INVALID_WRITE_IMMUTABLE_ARGS;
} else if (abstractValue.reason.has(ValueReason.State)) {
return ErrorCode.INVALID_WRITE_STATE;
} else if (abstractValue.reason.has(ValueReason.ReducerState)) {
return ErrorCode.INVALID_WRITE_REDUCER_STATE;
} else if (abstractValue.reason.has(ValueReason.Effect)) {
return ErrorCode.INVALID_WRITE_EFFECT_DEPENDENCY;
} else if (abstractValue.reason.has(ValueReason.HookCaptured)) {
return ErrorCode.INVALID_WRITE_HOOK_CAPTURED;
} else if (abstractValue.reason.has(ValueReason.HookReturn)) {
return ErrorCode.INVALID_WRITE_HOOK_RETURN;
} else {
return ErrorCode.INVALID_WRITE_GENERIC;
}
}

View File

@@ -0,0 +1,218 @@
/**
* 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 {
Effect,
HIRFunction,
Identifier,
InstructionId,
InstructionKind,
isArrayType,
isMapType,
isRefOrRefValue,
isSetType,
makeInstructionId,
Place,
} from '../HIR/HIR';
import {printPlace} from '../HIR/PrintHIR';
import {
eachInstructionLValue,
eachInstructionOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {assertExhaustive} from '../Utils/utils';
/*
* For each usage of a value in the given function, determines if the usage
* may be succeeded by a mutable usage of that same value and if so updates
* the usage to be mutable.
*
* Stated differently, this inference ensures that inferred capabilities of
* each reference are as follows:
* - freeze: the value is frozen at this point
* - readonly: the value is not modified at this point *or any subsequent
* point*
* - mutable: the value is modified at this point *or some subsequent point*.
*
* Note that this refines the capabilities inferered by InferReferenceCapability,
* which looks at individual references and not the lifetime of a value's mutability.
*
* == Algorithm
*
* TODO:
* 1. Forward data-flow analysis to determine aliasing. Unlike InferReferenceCapability
* which only tracks aliasing of top-level variables (`y = x`), this analysis needs
* to know if a value is aliased anywhere (`y.x = x`). The forward data flow tracks
* all possible locations which may have aliased a value. The concrete result is
* a mapping of each Place to the set of possibly-mutable values it may alias.
*
* ```
* const x = []; // {x: v0; v0: mutable []}
* const y = {}; // {x: v0, y: v1; v0: mutable [], v1: mutable []}
* y.x = x; // {x: v0, y: v1; v0: mutable [v1], v1: mutable [v0]}
* read(x); // {x: v0, y: v1; v0: mutable [v1], v1: mutable [v0]}
* mutate(y); // can infer that y mutates v0 and v1
* ```
*
* DONE:
* 2. Forward data-flow analysis to compute mutability liveness. Walk forwards over
* the CFG and track which values are mutated in a successor.
*
* ```
* mutate(y); // mutable y => v0, v1 mutated
* read(x); // x maps to v0, v1, those are in the mutated-later set, so x is mutable here
* ...
* ```
*/
function infer(place: Place, instrId: InstructionId): void {
if (!isRefOrRefValue(place.identifier)) {
place.identifier.mutableRange.end = makeInstructionId(instrId + 1);
}
}
function inferPlace(
place: Place,
instrId: InstructionId,
inferMutableRangeForStores: boolean,
): void {
switch (place.effect) {
case Effect.Unknown: {
throw new Error(`Found an unknown place ${printPlace(place)}}!`);
}
case Effect.Capture:
case Effect.Read:
case Effect.Freeze:
return;
case Effect.Store:
if (inferMutableRangeForStores) {
infer(place, instrId);
}
return;
case Effect.ConditionallyMutateIterator: {
const identifier = place.identifier;
if (
!isArrayType(identifier) &&
!isSetType(identifier) &&
!isMapType(identifier)
) {
infer(place, instrId);
}
return;
}
case Effect.ConditionallyMutate:
case Effect.Mutate: {
infer(place, instrId);
return;
}
default:
assertExhaustive(place.effect, `Unexpected ${printPlace(place)} effect`);
}
}
export function inferMutableLifetimes(
func: HIRFunction,
inferMutableRangeForStores: boolean,
): void {
/*
* Context variables only appear to mutate where they are assigned, but we need
* to force their range to start at their declaration. Track the declaring instruction
* id so that the ranges can be extended if/when they are reassigned
*/
const contextVariableDeclarationInstructions = new Map<
Identifier,
InstructionId
>();
for (const [_, block] of func.body.blocks) {
for (const phi of block.phis) {
const isPhiMutatedAfterCreation: boolean =
phi.place.identifier.mutableRange.end >
(block.instructions.at(0)?.id ?? block.terminal.id);
if (
inferMutableRangeForStores &&
isPhiMutatedAfterCreation &&
phi.place.identifier.mutableRange.start === 0
) {
for (const [, operand] of phi.operands) {
if (phi.place.identifier.mutableRange.start === 0) {
phi.place.identifier.mutableRange.start =
operand.identifier.mutableRange.start;
} else {
phi.place.identifier.mutableRange.start = makeInstructionId(
Math.min(
phi.place.identifier.mutableRange.start,
operand.identifier.mutableRange.start,
),
);
}
}
}
}
for (const instr of block.instructions) {
for (const operand of eachInstructionLValue(instr)) {
const lvalueId = operand.identifier;
/*
* lvalue start being mutable when they're initially assigned a
* value.
*/
lvalueId.mutableRange.start = instr.id;
/*
* Let's be optimistic and assume this lvalue is not mutable by
* default.
*/
lvalueId.mutableRange.end = makeInstructionId(instr.id + 1);
}
for (const operand of eachInstructionOperand(instr)) {
inferPlace(operand, instr.id, inferMutableRangeForStores);
}
if (
instr.value.kind === 'DeclareContext' ||
(instr.value.kind === 'StoreContext' &&
instr.value.lvalue.kind !== InstructionKind.Reassign &&
!contextVariableDeclarationInstructions.has(
instr.value.lvalue.place.identifier,
))
) {
/**
* Save declarations of context variables if they hasn't already been
* declared (due to hoisted declarations).
*/
contextVariableDeclarationInstructions.set(
instr.value.lvalue.place.identifier,
instr.id,
);
} else if (instr.value.kind === 'StoreContext') {
/*
* Else this is a reassignment, extend the range from the declaration (if present).
* Note that declarations may not be present for context variables that are reassigned
* within a function expression before (or without) a read of the same variable
*/
const declaration = contextVariableDeclarationInstructions.get(
instr.value.lvalue.place.identifier,
);
if (
declaration != null &&
!isRefOrRefValue(instr.value.lvalue.place.identifier)
) {
const range = instr.value.lvalue.place.identifier.mutableRange;
if (range.start === 0) {
range.start = declaration;
} else {
range.start = makeInstructionId(Math.min(range.start, declaration));
}
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
inferPlace(operand, block.terminal.id, inferMutableRangeForStores);
}
}
}

View File

@@ -0,0 +1,102 @@
/**
* 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 {HIRFunction, Identifier} from '../HIR/HIR';
import {inferAliasForUncalledFunctions} from './InerAliasForUncalledFunctions';
import {inferAliases} from './InferAlias';
import {inferAliasForPhis} from './InferAliasForPhis';
import {inferAliasForStores} from './InferAliasForStores';
import {inferMutableLifetimes} from './InferMutableLifetimes';
import {inferMutableRangesForAlias} from './InferMutableRangesForAlias';
import {inferTryCatchAliases} from './InferTryCatchAliases';
export function inferMutableRanges(ir: HIRFunction): void {
// Infer mutable ranges for non fields
inferMutableLifetimes(ir, false);
// Calculate aliases
const aliases = inferAliases(ir);
/*
* Calculate aliases for try/catch, where any value created
* in the try block could be aliased to the catch param
*/
inferTryCatchAliases(ir, aliases);
/*
* Eagerly canonicalize so that if nothing changes we can bail out
* after a single iteration
*/
let prevAliases: Map<Identifier, Identifier> = aliases.canonicalize();
while (true) {
// Infer mutable ranges for aliases that are not fields
inferMutableRangesForAlias(ir, aliases);
// Update aliasing information of fields
inferAliasForStores(ir, aliases);
// Update aliasing information of phis
inferAliasForPhis(ir, aliases);
const nextAliases = aliases.canonicalize();
if (areEqualMaps(prevAliases, nextAliases)) {
break;
}
prevAliases = nextAliases;
}
// Re-infer mutable ranges for all values
inferMutableLifetimes(ir, true);
/**
* The second inferMutableLifetimes() call updates mutable ranges
* of values to account for Store effects. Now we need to update
* all aliases of such values to extend their ranges as well. Note
* that the store only mutates the the directly aliased value and
* not any of its inner captured references. For example:
*
* ```
* let y;
* if (cond) {
* y = [];
* } else {
* y = [{}];
* }
* y.push(z);
* ```
*
* The Store effect from the `y.push` modifies the values that `y`
* directly aliases - the two arrays from the if/else branches -
* but does not modify values that `y` "contains" such as the
* object literal or `z`.
*/
prevAliases = aliases.canonicalize();
while (true) {
inferMutableRangesForAlias(ir, aliases);
inferAliasForPhis(ir, aliases);
inferAliasForUncalledFunctions(ir, aliases);
const nextAliases = aliases.canonicalize();
if (areEqualMaps(prevAliases, nextAliases)) {
break;
}
prevAliases = nextAliases;
}
}
function areEqualMaps<T, U>(a: Map<T, U>, b: Map<T, U>): boolean {
if (a.size !== b.size) {
return false;
}
for (const [key, value] of a) {
if (!b.has(key)) {
return false;
}
if (b.get(key) !== value) {
return false;
}
}
return true;
}

View File

@@ -0,0 +1,54 @@
/**
* 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 {
HIRFunction,
Identifier,
InstructionId,
isRefOrRefValue,
} from '../HIR/HIR';
import DisjointSet from '../Utils/DisjointSet';
export function inferMutableRangesForAlias(
_fn: HIRFunction,
aliases: DisjointSet<Identifier>,
): void {
const aliasSets = aliases.buildSets();
for (const aliasSet of aliasSets) {
/*
* Update mutableRange.end only if the identifiers have actually been
* mutated.
*/
const mutatingIdentifiers = [...aliasSet].filter(
id =>
id.mutableRange.end - id.mutableRange.start > 1 && !isRefOrRefValue(id),
);
if (mutatingIdentifiers.length > 0) {
// Find final instruction which mutates this alias set.
let lastMutatingInstructionId = 0;
for (const id of mutatingIdentifiers) {
if (id.mutableRange.end > lastMutatingInstructionId) {
lastMutatingInstructionId = id.mutableRange.end;
}
}
/*
* Update mutableRange.end for all aliases in this set ending before the
* last mutation.
*/
for (const alias of aliasSet) {
if (
alias.mutableRange.end < lastMutatingInstructionId &&
!isRefOrRefValue(alias)
) {
alias.mutableRange.end = lastMutatingInstructionId as InstructionId;
}
}
}
}
}

View File

@@ -9,7 +9,6 @@ import {
CompilerDiagnostic,
CompilerError,
Effect,
ErrorSeverity,
SourceLocation,
ValueKind,
} from '..';
@@ -19,7 +18,6 @@ import {
DeclarationId,
Environment,
FunctionExpression,
GeneratedSource,
HIRFunction,
Hole,
IdentifierId,
@@ -27,7 +25,6 @@ import {
InstructionKind,
InstructionValue,
isArrayType,
isJsxType,
isMapType,
isPrimitiveType,
isRefOrRefValue,
@@ -36,7 +33,6 @@ import {
Phi,
Place,
SpreadPattern,
Type,
ValueReason,
} from '../HIR';
import {
@@ -46,6 +42,12 @@ import {
eachTerminalSuccessor,
} from '../HIR/visitors';
import {Ok, Result} from '../Utils/Result';
import {
getArgumentEffect,
getFunctionCallSignature,
isKnownMutableEffect,
mergeValueKinds,
} from './InferReferenceEffects';
import {
assertExhaustive,
getOrInsertDefault,
@@ -62,15 +64,11 @@ import {
printSourceLocation,
} from '../HIR/PrintHIR';
import {FunctionSignature} from '../HIR/ObjectShape';
import {getWriteErrorReason} from './InferFunctionEffects';
import prettyFormat from 'pretty-format';
import {createTemporaryPlace} from '../HIR/HIRBuilder';
import {
AliasingEffect,
AliasingSignature,
hashEffect,
MutationReason,
} from './AliasingEffects';
import {ErrorCategory} from '../CompilerError';
import {AliasingEffect, AliasingSignature, hashEffect} from './AliasingEffects';
import {ErrorCode, ErrorCodeDetails} from '../Utils/CompilerErrorCodes';
const DEBUG = false;
@@ -444,38 +442,27 @@ function applySignature(
const value = state.kind(effect.value);
switch (value.kind) {
case ValueKind.Frozen: {
const reason = getWriteErrorReason({
const errorCode = getWriteErrorReason({
kind: value.kind,
reason: value.reason,
context: new Set(),
});
const variable =
effect.value.identifier.name !== null &&
effect.value.identifier.name.kind === 'named'
? `\`${effect.value.identifier.name.value}\``
: 'value';
const diagnostic = CompilerDiagnostic.create({
category: ErrorCategory.Immutability,
severity: ErrorSeverity.InvalidReact,
reason: 'This value cannot be modified',
description: `${reason}.`,
}).withDetail({
kind: 'error',
loc: effect.value.loc,
message: `${variable} cannot be modified`,
});
if (
effect.kind === 'Mutate' &&
effect.reason?.kind === 'AssignCurrentProperty'
) {
diagnostic.withDetail({
kind: 'hint',
message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`,
});
}
effects.push({
kind: 'MutateFrozen',
place: effect.value,
error: diagnostic,
// TODO: remove ERROR_CODE.INVALID_WRITE and update test fixtures
error: CompilerDiagnostic.fromCode(ErrorCode.INVALID_WRITE, {
description: ErrorCodeDetails[errorCode].reason,
}).withDetail({
kind: 'error',
loc: effect.value.loc,
message: `${variable} cannot be modified`,
}),
});
}
}
@@ -1038,23 +1025,20 @@ function applyEffect(
const hoistedAccess = context.hoistedContextDeclarations.get(
effect.value.identifier.declarationId,
);
const diagnostic = CompilerDiagnostic.create({
category: ErrorCategory.Immutability,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot access variable before it is declared',
description: `${variable ?? 'This variable'} is accessed before it is declared, which prevents the earlier access from updating when this value changes over time.`,
});
const diagnostic = CompilerDiagnostic.fromCode(
ErrorCode.INVALID_ACCESS_BEFORE_INIT,
);
if (hoistedAccess != null && hoistedAccess.loc != effect.value.loc) {
diagnostic.withDetail({
kind: 'error',
loc: hoistedAccess.loc,
message: `${variable ?? 'variable'} accessed before it is declared`,
message: `${variable ?? 'This variable'} is accessed before it is declared`,
});
}
diagnostic.withDetail({
kind: 'error',
loc: effect.value.loc,
message: `${variable ?? 'variable'} is declared here`,
message: `${variable ?? 'This variable'} is declared here`,
});
applyEffect(
@@ -1069,34 +1053,16 @@ function applyEffect(
effects,
);
} else {
const reason = getWriteErrorReason({
const errorCode = getWriteErrorReason({
kind: value.kind,
reason: value.reason,
context: new Set(),
});
const variable =
effect.value.identifier.name !== null &&
effect.value.identifier.name.kind === 'named'
? `\`${effect.value.identifier.name.value}\``
: 'value';
const diagnostic = CompilerDiagnostic.create({
category: ErrorCategory.Immutability,
severity: ErrorSeverity.InvalidReact,
reason: 'This value cannot be modified',
description: `${reason}.`,
}).withDetail({
kind: 'error',
loc: effect.value.loc,
message: `${variable} cannot be modified`,
});
if (
effect.kind === 'Mutate' &&
effect.reason?.kind === 'AssignCurrentProperty'
) {
diagnostic.withDetail({
kind: 'hint',
message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`,
});
}
applyEffect(
context,
state,
@@ -1106,7 +1072,14 @@ function applyEffect(
? 'MutateFrozen'
: 'MutateGlobal',
place: effect.value,
error: diagnostic,
// TODO: remove ERROR_CODE.INVALID_WRITE and update test fixtures
error: CompilerDiagnostic.fromCode(ErrorCode.INVALID_WRITE, {
description: ErrorCodeDetails[errorCode].reason,
}).withDetail({
kind: 'error',
loc: effect.value.loc,
message: `${variable} cannot be modified`,
}),
},
initialized,
effects,
@@ -1703,15 +1676,7 @@ function computeSignatureForInstruction(
}
case 'PropertyStore':
case 'ComputedStore': {
const mutationReason: MutationReason | null =
value.kind === 'PropertyStore' && value.property === 'current'
? {kind: 'AssignCurrentProperty'}
: null;
effects.push({
kind: 'Mutate',
value: value.object,
reason: mutationReason,
});
effects.push({kind: 'Mutate', value: value.object});
effects.push({
kind: 'Capture',
from: value.value,
@@ -1872,23 +1837,6 @@ function computeSignatureForInstruction(
});
}
}
for (const prop of value.props) {
if (
prop.kind === 'JsxAttribute' &&
prop.place.identifier.type.kind === 'Function' &&
(isJsxType(prop.place.identifier.type.return) ||
(prop.place.identifier.type.return.kind === 'Phi' &&
prop.place.identifier.type.return.operands.some(operand =>
isJsxType(operand),
)))
) {
// Any props which return jsx are assumed to be called during render
effects.push({
kind: 'Render',
place: prop.place,
});
}
}
}
break;
}
@@ -2054,16 +2002,12 @@ function computeSignatureForInstruction(
effects.push({
kind: 'MutateGlobal',
place: value.value,
error: CompilerDiagnostic.create({
category: ErrorCategory.Globals,
severity: ErrorSeverity.InvalidReact,
reason:
'Cannot reassign variables declared outside of the component/hook',
description: `Variable ${variable} is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)`,
}).withDetail({
error: CompilerDiagnostic.fromCode(
ErrorCode.INVALID_WRITE_GLOBAL,
).withDetail({
kind: 'error',
loc: instr.loc,
message: `${variable} cannot be reassigned`,
message: `${variable} should not be reassigned`,
}),
});
effects.push({kind: 'Assign', from: value.value, into: lvalue});
@@ -2089,7 +2033,7 @@ function computeSignatureForInstruction(
effects.push({
kind: 'Freeze',
value: operand,
reason: ValueReason.HookCaptured,
reason: ValueReason.Other,
});
}
}
@@ -2154,43 +2098,18 @@ function computeEffectsForLegacySignature(
effects.push({
kind: 'Impure',
place: receiver,
error: CompilerDiagnostic.create({
category: ErrorCategory.Purity,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot call impure function during render',
description:
(signature.canonicalName != null
? `\`${signature.canonicalName}\` is an impure function. `
: '') +
'Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
}).withDetail({
kind: 'error',
loc,
message: 'Cannot call impure function',
}),
error: CompilerDiagnostic.fromCode(ErrorCode.IMPURE_FUNCTIONS).withDetail(
{
kind: 'error',
loc,
message:
signature.canonicalName != null
? `\`${signature.canonicalName}\` is an impure function. `
: 'This is an impure function.',
},
),
});
}
if (signature.knownIncompatible != null && state.env.isInferredMemoEnabled) {
const errors = new CompilerError();
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.IncompatibleLibrary,
severity: ErrorSeverity.IncompatibleLibrary,
reason: 'Use of incompatible library',
description: [
'This API returns functions which cannot be memoized without leading to stale UI. ' +
'To prevent this, by default React Compiler will skip memoizing this component/hook. ' +
'However, you may see issues if values from this API are passed to other components/hooks that are ' +
'memoized.',
].join(''),
}).withDetail({
kind: 'error',
loc: receiver.loc,
message: signature.knownIncompatible,
}),
);
throw errors;
}
const stores: Array<Place> = [];
const captures: Array<Place> = [];
function visit(place: Place, effect: Effect): void {
@@ -2605,196 +2524,3 @@ export type AbstractValue = {
kind: ValueKind;
reason: ReadonlySet<ValueReason>;
};
export function getWriteErrorReason(abstractValue: AbstractValue): string {
if (abstractValue.reason.has(ValueReason.Global)) {
return 'Modifying a variable defined outside a component or hook is not allowed. Consider using an effect';
} else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
return 'Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX';
} else if (abstractValue.reason.has(ValueReason.Context)) {
return `Modifying a value returned from 'useContext()' is not allowed.`;
} else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
return 'Modifying a value returned from a function whose return value should not be mutated';
} else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
return 'Modifying component props or hook arguments is not allowed. Consider using a local variable instead';
} else if (abstractValue.reason.has(ValueReason.State)) {
return "Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead";
} else if (abstractValue.reason.has(ValueReason.ReducerState)) {
return "Modifying a value returned from 'useReducer()', which should not be modified directly. Use the dispatch function to update instead";
} else if (abstractValue.reason.has(ValueReason.Effect)) {
return 'Modifying a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the modification before calling useEffect()';
} else if (abstractValue.reason.has(ValueReason.HookCaptured)) {
return 'Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook';
} else if (abstractValue.reason.has(ValueReason.HookReturn)) {
return 'Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed';
} else {
return 'This modifies a variable that React considers immutable';
}
}
function getArgumentEffect(
signatureEffect: Effect | null,
arg: Place | SpreadPattern,
): Effect {
if (signatureEffect != null) {
if (arg.kind === 'Identifier') {
return signatureEffect;
} else if (
signatureEffect === Effect.Mutate ||
signatureEffect === Effect.ConditionallyMutate
) {
return signatureEffect;
} else {
// see call-spread-argument-mutable-iterator test fixture
if (signatureEffect === Effect.Freeze) {
CompilerError.throwTodo({
reason: 'Support spread syntax for hook arguments',
loc: arg.place.loc,
});
}
// effects[i] is Effect.Capture | Effect.Read | Effect.Store
return Effect.ConditionallyMutateIterator;
}
} else {
return Effect.ConditionallyMutate;
}
}
export function getFunctionCallSignature(
env: Environment,
type: Type,
): FunctionSignature | null {
if (type.kind !== 'Function') {
return null;
}
return env.getFunctionSignature(type);
}
export function isKnownMutableEffect(effect: Effect): boolean {
switch (effect) {
case Effect.Store:
case Effect.ConditionallyMutate:
case Effect.ConditionallyMutateIterator:
case Effect.Mutate: {
return true;
}
case Effect.Unknown: {
CompilerError.invariant(false, {
reason: 'Unexpected unknown effect',
description: null,
loc: GeneratedSource,
suggestions: null,
});
}
case Effect.Read:
case Effect.Capture:
case Effect.Freeze: {
return false;
}
default: {
assertExhaustive(effect, `Unexpected effect \`${effect}\``);
}
}
}
/**
* Joins two values using the following rules:
* == Effect Transitions ==
*
* Freezing an immutable value has not effect:
* ┌───────────────┐
* │ │
* ▼ │ Freeze
* ┌──────────────────────────┐ │
* │ Immutable │──┘
* └──────────────────────────┘
*
* Freezing a mutable or maybe-frozen value makes it frozen. Freezing a frozen
* value has no effect:
* ┌───────────────┐
* ┌─────────────────────────┐ Freeze │ │
* │ MaybeFrozen │────┐ ▼ │ Freeze
* └─────────────────────────┘ │ ┌──────────────────────────┐ │
* ├────▶│ Frozen │──┘
* │ └──────────────────────────┘
* ┌─────────────────────────┐ │
* │ Mutable │────┘
* └─────────────────────────┘
*
* == Join Lattice ==
* - immutable | mutable => mutable
* The justification is that immutable and mutable values are different types,
* and functions can introspect them to tell the difference (if the argument
* is null return early, else if its an object mutate it).
* - frozen | mutable => maybe-frozen
* Frozen values are indistinguishable from mutable values at runtime, so callers
* cannot dynamically avoid mutation of "frozen" values. If a value could be
* frozen we have to distinguish it from a mutable value. But it also isn't known
* frozen yet, so we distinguish as maybe-frozen.
* - immutable | frozen => frozen
* This is subtle and falls out of the above rules. If a value could be any of
* immutable, mutable, or frozen, then at runtime it could either be a primitive
* or a reference type, and callers can't distinguish frozen or not for reference
* types. To ensure that any sequence of joins btw those three states yields the
* correct maybe-frozen, these two have to produce a frozen value.
* - <any> | maybe-frozen => maybe-frozen
* - immutable | context => context
* - mutable | context => context
* - frozen | context => maybe-frozen
*
* ┌──────────────────────────┐
* │ Immutable │───┐
* └──────────────────────────┘ │
* │ ┌─────────────────────────┐
* ├───▶│ Frozen │──┐
* ┌──────────────────────────┐ │ └─────────────────────────┘ │
* │ Frozen │───┤ │ ┌─────────────────────────┐
* └──────────────────────────┘ │ ├─▶│ MaybeFrozen │
* │ ┌─────────────────────────┐ │ └─────────────────────────┘
* ├───▶│ MaybeFrozen │──┘
* ┌──────────────────────────┐ │ └─────────────────────────┘
* │ Mutable │───┘
* └──────────────────────────┘
*/
function mergeValueKinds(a: ValueKind, b: ValueKind): ValueKind {
if (a === b) {
return a;
} else if (a === ValueKind.MaybeFrozen || b === ValueKind.MaybeFrozen) {
return ValueKind.MaybeFrozen;
// after this a and b differ and neither are MaybeFrozen
} else if (a === ValueKind.Mutable || b === ValueKind.Mutable) {
if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
// frozen | mutable
return ValueKind.MaybeFrozen;
} else if (a === ValueKind.Context || b === ValueKind.Context) {
// context | mutable
return ValueKind.Context;
} else {
// mutable | immutable
return ValueKind.Mutable;
}
} else if (a === ValueKind.Context || b === ValueKind.Context) {
if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
// frozen | context
return ValueKind.MaybeFrozen;
} else {
// context | immutable
return ValueKind.Context;
}
} else if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
return ValueKind.Frozen;
} else if (a === ValueKind.Global || b === ValueKind.Global) {
return ValueKind.Global;
} else {
CompilerError.invariant(
a === ValueKind.Primitive && b == ValueKind.Primitive,
{
reason: `Unexpected value kind in mergeValues()`,
description: `Found kinds ${a} and ${b}`,
loc: GeneratedSource,
},
);
return ValueKind.Primitive;
}
}

View File

@@ -27,7 +27,7 @@ import {
} from '../HIR/visitors';
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
import {Err, Ok, Result} from '../Utils/Result';
import {AliasingEffect, MutationReason} from './AliasingEffects';
import {AliasingEffect} from './AliasingEffects';
/**
* This pass builds an abstract model of the heap and interprets the effects of the
@@ -101,7 +101,6 @@ export function inferMutationAliasingRanges(
transitive: boolean;
kind: MutationKind;
place: Place;
reason: MutationReason | null;
}> = [];
const renders: Array<{index: number; place: Place}> = [];
@@ -177,7 +176,6 @@ export function inferMutationAliasingRanges(
effect.kind === 'MutateTransitive'
? MutationKind.Definite
: MutationKind.Conditional,
reason: null,
place: effect.value,
});
} else if (
@@ -192,7 +190,6 @@ export function inferMutationAliasingRanges(
effect.kind === 'Mutate'
? MutationKind.Definite
: MutationKind.Conditional,
reason: effect.kind === 'Mutate' ? (effect.reason ?? null) : null,
place: effect.value,
});
} else if (
@@ -244,7 +241,6 @@ export function inferMutationAliasingRanges(
mutation.transitive,
mutation.kind,
mutation.place.loc,
mutation.reason,
errors,
);
}
@@ -271,7 +267,6 @@ export function inferMutationAliasingRanges(
functionEffects.push({
kind: 'Mutate',
value: {...place, loc: node.local.loc},
reason: node.mutationReason,
});
}
}
@@ -512,7 +507,6 @@ export function inferMutationAliasingRanges(
true,
MutationKind.Conditional,
into.loc,
null,
ignoredErrors,
);
for (const from of tracked) {
@@ -586,7 +580,6 @@ type Node = {
transitive: {kind: MutationKind; loc: SourceLocation} | null;
local: {kind: MutationKind; loc: SourceLocation} | null;
lastMutated: number;
mutationReason: MutationReason | null;
value:
| {kind: 'Object'}
| {kind: 'Phi'}
@@ -606,7 +599,6 @@ class AliasingState {
transitive: null,
local: null,
lastMutated: 0,
mutationReason: null,
value,
});
}
@@ -705,7 +697,6 @@ class AliasingState {
transitive: boolean,
startKind: MutationKind,
loc: SourceLocation,
reason: MutationReason | null,
errors: CompilerError,
): void {
const seen = new Map<Identifier, MutationKind>();
@@ -726,7 +717,6 @@ class AliasingState {
if (node == null) {
continue;
}
node.mutationReason ??= reason;
node.lastMutated = Math.max(node.lastMutated, index);
if (end != null) {
node.id.mutableRange.end = makeInstructionId(

View File

@@ -0,0 +1,49 @@
/**
* 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 {BlockId, HIRFunction, Identifier} from '../HIR';
import DisjointSet from '../Utils/DisjointSet';
/*
* Any values created within a try/catch block could be aliased to the try handler.
* Our lowering ensures that every instruction within a try block will be lowered into a
* basic block ending in a maybe-throw terminal that points to its catch block, so we can
* iterate such blocks and alias their instruction lvalues to the handler's param (if present).
*/
export function inferTryCatchAliases(
fn: HIRFunction,
aliases: DisjointSet<Identifier>,
): void {
const handlerParams: Map<BlockId, Identifier> = new Map();
for (const [_, block] of fn.body.blocks) {
if (
block.terminal.kind === 'try' &&
block.terminal.handlerBinding !== null
) {
handlerParams.set(
block.terminal.handler,
block.terminal.handlerBinding.identifier,
);
} else if (block.terminal.kind === 'maybe-throw') {
const handlerParam = handlerParams.get(block.terminal.handler);
if (handlerParam === undefined) {
/*
* There's no catch clause param, nothing to alias to so
* skip this block
*/
continue;
}
/*
* Otherwise alias all values created in this block to the
* catch clause param
*/
for (const instr of block.instructions) {
aliases.union([handlerParam, instr.lvalue.identifier]);
}
}
}
}

View File

@@ -7,6 +7,8 @@
export {default as analyseFunctions} from './AnalyseFunctions';
export {dropManualMemoization} from './DropManualMemoization';
export {inferMutableRanges} from './InferMutableRanges';
export {inferReactivePlaces} from './InferReactivePlaces';
export {default as inferReferenceEffects} from './InferReferenceEffects';
export {inlineImmediatelyInvokedFunctionExpressions} from './InlineImmediatelyInvokedFunctionExpressions';
export {inferEffectDependencies} from './InferEffectDependencies';

View File

@@ -42,7 +42,6 @@ import {
mapInstructionValueOperands,
mapTerminalOperands,
} from '../HIR/visitors';
import {ErrorCategory} from '../CompilerError';
type InlinedJsxDeclarationMap = Map<
DeclarationId,
@@ -84,7 +83,6 @@ export function inlineJsxTransform(
kind: 'CompileDiagnostic',
fnLoc: null,
detail: {
category: ErrorCategory.Todo,
reason: 'JSX Inlining is not supported on value blocks',
loc: instr.loc,
},

View File

@@ -255,6 +255,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
returnTypeAnnotation: null,
returns: createTemporaryPlace(env, GeneratedSource),
context: [],
effects: null,
body: {
entry: block.id,
blocks: new Map([[block.id, block]]),
@@ -262,7 +263,6 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
generator: false,
async: false,
directives: [],
aliasingEffects: [],
};
reversePostorderBlocks(fn.body);

View File

@@ -370,6 +370,7 @@ function emitOutlinedFn(
returnTypeAnnotation: null,
returns: createTemporaryPlace(env, GeneratedSource),
context: [],
effects: null,
body: {
entry: block.id,
blocks: new Map([[block.id, block]]),
@@ -377,7 +378,6 @@ function emitOutlinedFn(
generator: false,
async: false,
directives: [],
aliasingEffects: [],
};
return fn;
}

View File

@@ -175,41 +175,6 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
if (node != null) {
valueBlockNodes.set(fallthrough, node);
}
} else if (terminal.kind === 'goto') {
/**
* If we encounter a goto that is not to the natural fallthrough of the current
* block (not the topmost fallthrough on the stack), then this is a goto to a
* label. Any scopes that extend beyond the goto must be extended to include
* the labeled range, so that the break statement doesn't accidentally jump
* out of the scope. We do this by extending the start and end of the scope's
* range to the label and its fallthrough respectively.
*/
const start = activeBlockFallthroughRanges.find(
range => range.fallthrough === terminal.block,
);
if (start != null && start !== activeBlockFallthroughRanges.at(-1)) {
const fallthroughBlock = fn.body.blocks.get(start.fallthrough)!;
const firstId =
fallthroughBlock.instructions[0]?.id ?? fallthroughBlock.terminal.id;
for (const scope of activeScopes) {
/**
* activeScopes is only filtered at block start points, so some of the
* scopes may not actually be active anymore, ie we've past their end
* instruction. Only extend ranges for scopes that are actually active.
*
* TODO: consider pruning activeScopes per instruction
*/
if (scope.range.end <= terminal.id) {
continue;
}
scope.range.start = makeInstructionId(
Math.min(start.range.start, scope.range.start),
);
scope.range.end = makeInstructionId(
Math.max(firstId, scope.range.end),
);
}
}
}
/*

View File

@@ -13,7 +13,7 @@ import {
pruneUnusedLabels,
renameVariables,
} from '.';
import {CompilerError, ErrorCategory, ErrorSeverity} from '../CompilerError';
import {CompilerError, ErrorSeverity} from '../CompilerError';
import {Environment, ExternalFunction} from '../HIR';
import {
ArrayPattern,
@@ -2185,7 +2185,6 @@ function codegenInstructionValue(
(declarator.id as t.Identifier).name
}'`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: declarator.loc ?? null,
suggestions: null,
});
@@ -2194,7 +2193,6 @@ function codegenInstructionValue(
cx.errors.push({
reason: `(CodegenReactiveFunction::codegenInstructionValue) Handle conversion of ${stmt.type} to expression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: stmt.loc ?? null,
suggestions: null,
});

View File

@@ -24,6 +24,7 @@ import {
getHookKind,
isMutableEffect,
} from '../HIR';
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
import {assertExhaustive, getOrInsertDefault} from '../Utils/utils';
import {getPlaceScope, ReactiveScope} from '../HIR/HIR';
import {
@@ -34,7 +35,6 @@ import {
visitReactiveFunction,
} from './visitors';
import {printPlace} from '../HIR/PrintHIR';
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
/*
* This pass prunes reactive scopes that are not necessary to bound downstream computation.
@@ -411,9 +411,7 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
this.state = state;
this.options = {
memoizeJsxElements: !this.env.config.enableForest,
forceMemoizePrimitives:
this.env.config.enableForest ||
this.env.config.enablePreserveExistingMemoizationGuarantees,
forceMemoizePrimitives: this.env.config.enableForest,
};
}
@@ -536,23 +534,9 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
case 'JSXText':
case 'BinaryExpression':
case 'UnaryExpression': {
if (options.forceMemoizePrimitives) {
/**
* Because these instructions produce primitives we usually don't consider
* them as escape points: they are known to copy, not return references.
* However if we're forcing memoization of primitives then we mark these
* instructions as needing memoization and walk their rvalues to ensure
* any scopes transitively reachable from the rvalues are considered for
* memoization. Note: we may still prune primitive-producing scopes if
* they don't ultimately escape at all.
*/
const level = MemoizationLevel.Conditional;
return {
lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
rvalues: [...eachReactiveValueOperand(value)],
};
}
const level = MemoizationLevel.Never;
const level = options.forceMemoizePrimitives
? MemoizationLevel.Memoized
: MemoizationLevel.Never;
return {
// All of these instructions return a primitive value and never need to be memoized
lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
@@ -701,7 +685,9 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
}
case 'ComputedLoad':
case 'PropertyLoad': {
const level = MemoizationLevel.Conditional;
const level = options.forceMemoizePrimitives
? MemoizationLevel.Memoized
: MemoizationLevel.Conditional;
return {
// Indirection for the inner value, memoized if the value is
lvalues: lvalue !== null ? [{place: lvalue, level}] : [],

View File

@@ -42,7 +42,7 @@ import {
import {eachInstructionOperand} from '../HIR/visitors';
import {printSourceLocationLine} from '../HIR/PrintHIR';
import {USE_FIRE_FUNCTION_NAME} from '../HIR/Environment';
import {ErrorCategory} from '../CompilerError';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
/*
* TODO(jmbrown):
@@ -51,8 +51,6 @@ import {ErrorCategory} from '../CompilerError';
* - React.useEffect calls
*/
const CANNOT_COMPILE_FIRE = 'Cannot compile `fire`';
export function transformFire(fn: HIRFunction): void {
const context = new Context(fn.env);
replaceFireFunctions(fn, context);
@@ -134,7 +132,6 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
loc: value.loc,
description: null,
severity: ErrorSeverity.Invariant,
category: ErrorCategory.Invariant,
reason: '[InsertFire] No LoadGlobal found for useEffect call',
suggestions: null,
});
@@ -180,10 +177,7 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
loc: value.args[1].loc,
description:
'You must use an array literal for an effect dependency array when that effect uses `fire()`',
severity: ErrorSeverity.Invariant,
category: ErrorCategory.Fire,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
errorCode: ErrorCode.CANNOT_COMPILE_FIRE,
});
}
} else if (value.args.length > 1 && value.args[1].kind === 'Spread') {
@@ -191,10 +185,7 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
loc: value.args[1].place.loc,
description:
'You must use an array literal for an effect dependency array when that effect uses `fire()`',
severity: ErrorSeverity.Invariant,
category: ErrorCategory.Fire,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
errorCode: ErrorCode.CANNOT_COMPILE_FIRE,
});
}
}
@@ -227,7 +218,6 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
loc: value.loc,
description: null,
severity: ErrorSeverity.Invariant,
category: ErrorCategory.Invariant,
reason:
'[InsertFire] No loadLocal found for fire call argument',
suggestions: null,
@@ -248,12 +238,9 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
} else {
context.pushError({
loc: value.loc,
errorCode: ErrorCode.CANNOT_COMPILE_FIRE,
description:
'`fire()` can only receive a function call such as `fire(fn(a,b)). Method calls and other expressions are not allowed',
severity: ErrorSeverity.InvalidReact,
category: ErrorCategory.Fire,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
}
} else {
@@ -268,11 +255,8 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
}
context.pushError({
loc: value.loc,
errorCode: ErrorCode.CANNOT_COMPILE_FIRE,
description,
severity: ErrorSeverity.InvalidReact,
category: ErrorCategory.Fire,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
}
} else if (value.kind === 'CallExpression') {
@@ -401,10 +385,7 @@ function ensureNoRemainingCalleeCaptures(
description: `All uses of ${calleeName} must be either used with a fire() call in \
this effect or not used with a fire() call at all. ${calleeName} was used with fire() on line \
${printSourceLocationLine(calleeInfo.fireLoc)} in this effect`,
severity: ErrorSeverity.InvalidReact,
category: ErrorCategory.Fire,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
errorCode: ErrorCode.CANNOT_COMPILE_FIRE,
});
}
}
@@ -419,10 +400,7 @@ function ensureNoMoreFireUses(fn: HIRFunction, context: Context): void {
context.pushError({
loc: place.identifier.loc,
description: 'Cannot use `fire` outside of a useEffect function',
category: ErrorCategory.Fire,
severity: ErrorSeverity.Invariant,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
errorCode: ErrorCode.CANNOT_COMPILE_FIRE,
});
}
}

View File

@@ -777,15 +777,6 @@ class Unifier {
return {kind: 'Phi', operands: type.operands.map(o => this.get(o))};
}
if (type.kind === 'Function') {
return {
kind: 'Function',
isConstructor: type.isConstructor,
shapeId: type.shapeId,
return: this.get(type.return),
};
}
return type;
}
}

View File

@@ -0,0 +1,624 @@
import {ErrorSeverity} from './CompilerErrorSeverity';
export enum LinterCategory {
RULES_OF_HOOKS = 'rules-of-hooks',
IMPURE_FUNCTIONS = 'impure-functions',
JSX_IN_TRY = 'jsx-in-try',
NO_REF_ACCESS_IN_RENDER = 'no-ref-access-in-render',
VALIDATE_MANUAL_MEMO = 'validate-manual-memo',
DYNAMIC_MANUAL_MEMO = 'dynamic-manual-memo',
EXHAUSTIVE_DEPS = 'exhaustive-deps',
MEMOIZED_DEPENDENCIES = 'memoized-dependencies', // not real!
INVALID_WRITE = 'invalid-write',
CAPITALIZED_CALLS = 'capitalized-calls',
STATIC_COMPONENTS = 'static-components',
NO_SET_STATE_IN_RENDER = 'no-set-state-in-render',
NO_SET_STATE_IN_EFFECTS = 'no-set-state-in-effects',
UNNECESSARY_EFFECTS = 'unnecessary-effects',
UNSUPPORTED_SYNTAX = 'unsupported-syntax',
TODO_SYNTAX = 'todo-syntax',
COMPILER_CONFIG = 'compiler-config',
}
export enum ErrorCode {
HOOK_CALL_STATIC,
HOOK_INVALID_REFERENCE,
HOOK_CALL_REACTIVE,
HOOK_CALL_NOT_TOP_LEVEL,
IMPURE_FUNCTIONS,
JSX_IN_TRY,
NO_REF_ACCESS_IN_RENDER,
WRITE_AFTER_RENDER,
REASSIGN_AFTER_RENDER,
REASSIGN_IN_ASYNC,
INVALID_WRITE,
CAPITALIZED_CALLS,
STATIC_COMPONENTS,
INVALID_USE_MEMO_CALLBACK_RETURN,
INVALID_USE_MEMO_CALLBACK_PARAMETERS,
INVALID_USE_MEMO_CALLBACK_ASYNC,
INVALID_USE_MEMO_NO_ARG0,
INVALID_USE_CALLBACK_NO_ARG0,
DYNAMIC_MANUAL_MEMO_CALLBACK,
DYNAMIC_USE_MEMO_SPREAD_ARGUMENT,
DYNAMIC_USE_CALLBACK_SPREAD_ARGUMENT,
DYNAMIC_MANUAL_MEMO_DEPENDENCY_LIST,
COMPLEX_MANUAL_MEMO_DEPENDENCY_LIST_ENTRY,
INVALID_SET_STATE_IN_RENDER,
INVALID_SET_STATE_IN_MEMO,
INVALID_SET_STATE_IN_EFFECTS,
NO_DERIVED_COMPUTATIONS_IN_EFFECTS,
DYNAMIC_GATING_IS_NOT_IDENTIFIER,
DYNAMIC_GATING_MULTIPLE_DIRECTIVES,
FILENAME_NOT_SET,
INVALID_WRITE_GLOBAL,
INVALID_WRITE_FROZEN_VALUE_JSX,
INVALID_WRITE_IMMUTABLE_VALUE_USE_CONTEXT,
INVALID_WRITE_IMMUTABLE_VALUE_KNOWN_SIGNATURE,
INVALID_WRITE_IMMUTABLE_ARGS,
INVALID_WRITE_STATE,
INVALID_WRITE_REDUCER_STATE,
INVALID_WRITE_EFFECT_DEPENDENCY,
INVALID_WRITE_HOOK_CAPTURED,
INVALID_WRITE_HOOK_RETURN,
INVALID_ACCESS_BEFORE_INIT,
INVALID_WRITE_GENERIC,
MEMOIZED_EFFECT_DEPENDENCIES,
INVALID_SYNTAX_MULTIPLE_DEFAULTS,
INVALID_SYNTAX_REASSIGNED_CONST,
INVALID_SYNTAX_BAD_VARIABLE_DECL,
UNSUPPORTED_WITH,
UNSUPPORTED_INNER_CLASS,
INVALID_IMPORT_EXPORT,
INVALID_TS_NAMESPACE,
UNSUPPORTED_NEW_EXPRESSION,
UNSUPPORTED_EMPTY_SEQUENCE_EXPRESSION,
INVALID_QUASI_LENGTHS,
INVALID_SYNTAX_DELETE_EXPRESSION,
UNSUPPORTED_THROW_EXPRESSION,
INVALID_JSX_NAMESPACED_NAME,
UNSUPPORTED_EVAL,
INVALID_SYNTAX_RESERVED_VARIABLE_NAME,
INVALID_JAVASCRIPT_AST,
BAILOUT_ESLINT_SUPPRESSION,
BAILOUT_FLOW_SUPPRESSION,
MANUAL_MEMO_MUTATED_LATER,
MANUAL_MEMO_REMOVED,
MANUAL_MEMO_DEPENDENCIES_CONFLICT,
CANNOT_COMPILE_FIRE,
DID_NOT_INFER_DEPS,
/** Todo syntax */
TODO_CONFLICTING_FBT_IDENTIFIER,
TODO_DUPLICATE_FBT_TAGS,
UNKNOWN_FUNCTION_PARAMETERS,
}
type ErrorCodeType = {
code: ErrorCode;
description?: string;
severity: ErrorSeverity;
reason: string;
linterCategory: LinterCategory | null;
};
export const ErrorCodeDetails: Record<ErrorCode, ErrorCodeType> = {
[ErrorCode.HOOK_CALL_STATIC]: {
code: ErrorCode.HOOK_CALL_STATIC,
severity: ErrorSeverity.InvalidReact,
reason:
'Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)',
linterCategory: LinterCategory.RULES_OF_HOOKS,
},
[ErrorCode.HOOK_INVALID_REFERENCE]: {
code: ErrorCode.HOOK_INVALID_REFERENCE,
severity: ErrorSeverity.InvalidReact,
reason:
'Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values',
linterCategory: LinterCategory.RULES_OF_HOOKS,
},
[ErrorCode.HOOK_CALL_REACTIVE]: {
code: ErrorCode.HOOK_CALL_REACTIVE,
severity: ErrorSeverity.InvalidReact,
reason:
'Hooks must be the same function on every render, but this value may change over time to a different function. See https://react.dev/reference/rules/react-calls-components-and-hooks#dont-dynamically-use-hooks',
linterCategory: LinterCategory.RULES_OF_HOOKS,
},
[ErrorCode.HOOK_CALL_NOT_TOP_LEVEL]: {
code: ErrorCode.HOOK_CALL_NOT_TOP_LEVEL,
severity: ErrorSeverity.InvalidReact,
reason:
'Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)',
linterCategory: LinterCategory.RULES_OF_HOOKS,
},
[ErrorCode.IMPURE_FUNCTIONS]: {
code: ErrorCode.IMPURE_FUNCTIONS,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot call impure functions during render',
description:
'Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
linterCategory: LinterCategory.IMPURE_FUNCTIONS,
},
[ErrorCode.JSX_IN_TRY]: {
code: ErrorCode.JSX_IN_TRY,
severity: ErrorSeverity.InvalidReact,
reason: 'Avoid constructing JSX within try/catch',
description: `React does not immediately render components when JSX is rendered, so any errors from this component will not be caught by the try/catch. To catch errors in rendering a given component, wrap that component in an error boundary. (https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)`,
linterCategory: LinterCategory.JSX_IN_TRY,
},
[ErrorCode.NO_REF_ACCESS_IN_RENDER]: {
code: ErrorCode.NO_REF_ACCESS_IN_RENDER,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot access refs during render',
description:
'React refs are values that are not needed for rendering. Refs should only be accessed ' +
'outside of render, such as in event handlers or effects. ' +
'Accessing a ref value (the `current` property) during render can cause your component ' +
'not to update as expected (https://react.dev/reference/react/useRef)',
linterCategory: LinterCategory.NO_REF_ACCESS_IN_RENDER,
},
[ErrorCode.INVALID_SET_STATE_IN_EFFECTS]: {
code: ErrorCode.INVALID_SET_STATE_IN_EFFECTS,
severity: ErrorSeverity.InvalidReact,
reason:
'Calling setState synchronously within an effect can trigger cascading renders',
description:
'Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. ' +
'In general, the body of an effect should do one or both of the following:\n' +
'* Update external systems with the latest state from React.\n' +
'* Subscribe for updates from some external system, calling setState in a callback function when external state changes.\n\n' +
'Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. ' +
'(https://react.dev/learn/you-might-not-need-an-effect)',
linterCategory: LinterCategory.NO_SET_STATE_IN_EFFECTS,
},
[ErrorCode.WRITE_AFTER_RENDER]: {
code: ErrorCode.WRITE_AFTER_RENDER,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot modify local variables after render completes',
description: `This argument is a function which may reassign or mutate a variable after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead`,
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.REASSIGN_AFTER_RENDER]: {
code: ErrorCode.REASSIGN_AFTER_RENDER,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot reassign local variables after render completes',
description: `Reassigning local variables after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead`,
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.REASSIGN_IN_ASYNC]: {
code: ErrorCode.REASSIGN_IN_ASYNC,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot reassign variable in async function',
description:
'Reassigning a variable in an async function can cause inconsistent behavior on subsequent renders. Consider using state instead',
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.INVALID_WRITE]: {
code: ErrorCode.INVALID_WRITE,
severity: ErrorSeverity.InvalidReact,
reason: 'This value cannot be modified',
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.CAPITALIZED_CALLS]: {
code: ErrorCode.CAPITALIZED_CALLS,
severity: ErrorSeverity.InvalidReact,
reason:
'Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config',
linterCategory: LinterCategory.CAPITALIZED_CALLS,
},
[ErrorCode.STATIC_COMPONENTS]: {
code: ErrorCode.STATIC_COMPONENTS,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot create components during render',
linterCategory: LinterCategory.STATIC_COMPONENTS,
},
[ErrorCode.INVALID_USE_MEMO_NO_ARG0]: {
code: ErrorCode.INVALID_USE_MEMO_NO_ARG0,
severity: ErrorSeverity.InvalidReact,
reason: `Expected a callback function to be passed to useMemo`,
linterCategory: LinterCategory.VALIDATE_MANUAL_MEMO,
},
[ErrorCode.INVALID_USE_CALLBACK_NO_ARG0]: {
code: ErrorCode.INVALID_USE_CALLBACK_NO_ARG0,
severity: ErrorSeverity.InvalidReact,
reason: `Expected a callback function to be passed to useCallback`,
linterCategory: LinterCategory.VALIDATE_MANUAL_MEMO,
},
[ErrorCode.DYNAMIC_USE_MEMO_SPREAD_ARGUMENT]: {
code: ErrorCode.DYNAMIC_USE_MEMO_SPREAD_ARGUMENT,
severity: ErrorSeverity.InvalidReact,
reason: 'Unexpected spread argument to useMemo',
linterCategory: LinterCategory.DYNAMIC_MANUAL_MEMO,
},
[ErrorCode.DYNAMIC_USE_CALLBACK_SPREAD_ARGUMENT]: {
code: ErrorCode.DYNAMIC_USE_CALLBACK_SPREAD_ARGUMENT,
severity: ErrorSeverity.InvalidReact,
reason: 'Unexpected spread argument to useCallback',
linterCategory: LinterCategory.DYNAMIC_MANUAL_MEMO,
},
[ErrorCode.INVALID_USE_MEMO_CALLBACK_PARAMETERS]: {
code: ErrorCode.INVALID_USE_MEMO_CALLBACK_PARAMETERS,
severity: ErrorSeverity.InvalidReact,
reason: 'useMemo() callbacks may not accept parameters',
description:
'useMemo() callbacks are called by React to cache calculations across re-renders. They should not take parameters. Instead, directly reference the props, state, or local variables needed for the computation',
linterCategory: LinterCategory.VALIDATE_MANUAL_MEMO,
},
[ErrorCode.INVALID_USE_MEMO_CALLBACK_ASYNC]: {
code: ErrorCode.INVALID_USE_MEMO_CALLBACK_ASYNC,
severity: ErrorSeverity.InvalidReact,
reason: 'useMemo() callbacks may not be async or generator functions',
description:
'useMemo() callbacks are called once and must synchronously return a value',
linterCategory: LinterCategory.VALIDATE_MANUAL_MEMO,
},
[ErrorCode.INVALID_USE_MEMO_CALLBACK_RETURN]: {
code: ErrorCode.INVALID_USE_MEMO_CALLBACK_RETURN,
severity: ErrorSeverity.InvalidReact,
reason: 'useMemo() callbacks must return a value',
linterCategory: LinterCategory.VALIDATE_MANUAL_MEMO,
},
[ErrorCode.DYNAMIC_MANUAL_MEMO_CALLBACK]: {
code: ErrorCode.DYNAMIC_MANUAL_MEMO_CALLBACK,
severity: ErrorSeverity.InvalidReact,
reason: `Expected the first argument to be an inline function expression`,
linterCategory: LinterCategory.DYNAMIC_MANUAL_MEMO,
},
[ErrorCode.DYNAMIC_MANUAL_MEMO_DEPENDENCY_LIST]: {
code: ErrorCode.DYNAMIC_MANUAL_MEMO_DEPENDENCY_LIST,
severity: ErrorSeverity.InvalidReact,
reason: `Expected the dependency list of useMemo or useCallback to be an array literal`,
linterCategory: LinterCategory.DYNAMIC_MANUAL_MEMO,
},
[ErrorCode.COMPLEX_MANUAL_MEMO_DEPENDENCY_LIST_ENTRY]: {
code: ErrorCode.COMPLEX_MANUAL_MEMO_DEPENDENCY_LIST_ENTRY,
severity: ErrorSeverity.InvalidReact,
reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
linterCategory: LinterCategory.DYNAMIC_MANUAL_MEMO,
},
[ErrorCode.INVALID_SET_STATE_IN_RENDER]: {
code: ErrorCode.INVALID_SET_STATE_IN_RENDER,
severity: ErrorSeverity.InvalidReact,
reason: 'Calling setState during render may trigger an infinite loop',
description:
'Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState)',
linterCategory: LinterCategory.NO_SET_STATE_IN_RENDER,
},
[ErrorCode.INVALID_SET_STATE_IN_MEMO]: {
code: ErrorCode.INVALID_SET_STATE_IN_MEMO,
severity: ErrorSeverity.InvalidReact,
reason: 'Calling setState from useMemo may trigger an infinite loop',
description:
'Each time the memo callback is evaluated it will change state. This can cause a memoization dependency to change, running the memo function again and causing an infinite loop. Instead of setting state in useMemo(), prefer deriving the value during render. (https://react.dev/reference/react/useState)',
linterCategory: LinterCategory.NO_SET_STATE_IN_RENDER,
},
[ErrorCode.NO_DERIVED_COMPUTATIONS_IN_EFFECTS]: {
code: ErrorCode.NO_DERIVED_COMPUTATIONS_IN_EFFECTS,
severity: ErrorSeverity.InvalidReact,
reason:
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
linterCategory: LinterCategory.UNNECESSARY_EFFECTS,
},
/** Invalid writes */
[ErrorCode.INVALID_WRITE_GLOBAL]: {
code: ErrorCode.INVALID_WRITE_GLOBAL,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot reassign variables declared outside of the component/hook',
description:
'Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)',
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.INVALID_WRITE_FROZEN_VALUE_JSX]: {
code: ErrorCode.INVALID_WRITE_FROZEN_VALUE_JSX,
severity: ErrorSeverity.InvalidReact,
reason:
'Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX',
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.INVALID_WRITE_IMMUTABLE_VALUE_USE_CONTEXT]: {
code: ErrorCode.INVALID_WRITE_IMMUTABLE_VALUE_USE_CONTEXT,
severity: ErrorSeverity.InvalidReact,
reason: `Modifying a value returned from 'useContext()' is not allowed`,
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.INVALID_WRITE_IMMUTABLE_VALUE_KNOWN_SIGNATURE]: {
code: ErrorCode.INVALID_WRITE_IMMUTABLE_VALUE_KNOWN_SIGNATURE,
severity: ErrorSeverity.InvalidReact,
reason:
'Modifying a value returned from a function whose return value should not be mutated',
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.INVALID_WRITE_IMMUTABLE_ARGS]: {
code: ErrorCode.INVALID_WRITE_IMMUTABLE_ARGS,
severity: ErrorSeverity.InvalidReact,
reason:
'Modifying component props or hook arguments is not allowed. Consider using a local variable instead',
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.INVALID_WRITE_STATE]: {
code: ErrorCode.INVALID_WRITE_STATE,
severity: ErrorSeverity.InvalidReact,
reason:
"Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead",
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.INVALID_WRITE_REDUCER_STATE]: {
code: ErrorCode.INVALID_WRITE_REDUCER_STATE,
severity: ErrorSeverity.InvalidReact,
reason:
"Modifying a value returned from 'useReducer()', which should not be modified directly. Use the dispatch function to update instead",
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.INVALID_WRITE_EFFECT_DEPENDENCY]: {
code: ErrorCode.INVALID_WRITE_EFFECT_DEPENDENCY,
severity: ErrorSeverity.InvalidReact,
reason:
'Modifying a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the modification before calling useEffect()',
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.INVALID_WRITE_HOOK_CAPTURED]: {
code: ErrorCode.INVALID_WRITE_HOOK_CAPTURED,
severity: ErrorSeverity.InvalidReact,
reason:
'Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook',
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.INVALID_WRITE_HOOK_RETURN]: {
code: ErrorCode.INVALID_WRITE_HOOK_RETURN,
severity: ErrorSeverity.InvalidReact,
reason:
'Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed',
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.INVALID_ACCESS_BEFORE_INIT]: {
code: ErrorCode.INVALID_ACCESS_BEFORE_INIT,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot access variable before it is declared',
description: `Reading a variable before it is initialized will prevent the earlier access from updating when this value changes over time. Instead, move the variable access to after it has been initialized`,
linterCategory: LinterCategory.INVALID_WRITE,
},
[ErrorCode.INVALID_WRITE_GENERIC]: {
code: ErrorCode.INVALID_WRITE_GENERIC,
severity: ErrorSeverity.InvalidReact,
reason: 'This modifies a variable that React considers immutable',
linterCategory: LinterCategory.INVALID_WRITE,
},
/** Compiler Config */
[ErrorCode.DYNAMIC_GATING_IS_NOT_IDENTIFIER]: {
code: ErrorCode.DYNAMIC_GATING_IS_NOT_IDENTIFIER,
severity: ErrorSeverity.InvalidReact,
reason: 'Dynamic gating directive is not a valid JavaScript identifier',
linterCategory: LinterCategory.COMPILER_CONFIG,
},
[ErrorCode.DYNAMIC_GATING_MULTIPLE_DIRECTIVES]: {
code: ErrorCode.DYNAMIC_GATING_MULTIPLE_DIRECTIVES,
severity: ErrorSeverity.InvalidReact,
reason: 'Expected a single dynamic gating directive',
linterCategory: LinterCategory.COMPILER_CONFIG,
},
[ErrorCode.FILENAME_NOT_SET]: {
code: ErrorCode.FILENAME_NOT_SET,
severity: ErrorSeverity.InvalidConfig,
reason: `Expected a filename but found none`,
description:
"When the 'sources' config options is specified, the React compiler will only compile files with a name",
linterCategory: LinterCategory.COMPILER_CONFIG,
},
/** Effect dependencies */
[ErrorCode.MEMOIZED_EFFECT_DEPENDENCIES]: {
code: ErrorCode.MEMOIZED_EFFECT_DEPENDENCIES,
reason:
'React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior',
severity: ErrorSeverity.CannotPreserveMemoization,
linterCategory: LinterCategory.MEMOIZED_DEPENDENCIES,
},
/** Invalid / unsupported syntax */
[ErrorCode.INVALID_SYNTAX_MULTIPLE_DEFAULTS]: {
code: ErrorCode.INVALID_SYNTAX_MULTIPLE_DEFAULTS,
reason: `Expected at most one \`default\` branch in a switch statement, this code should have failed to parse`,
severity: ErrorSeverity.InvalidJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.INVALID_SYNTAX_REASSIGNED_CONST]: {
code: ErrorCode.INVALID_SYNTAX_REASSIGNED_CONST,
reason: `Expect \`const\` declaration not to be reassigned`,
severity: ErrorSeverity.InvalidJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.INVALID_SYNTAX_BAD_VARIABLE_DECL]: {
code: ErrorCode.INVALID_SYNTAX_BAD_VARIABLE_DECL,
reason: `Expected variable declaration to be an identifier if no initializer was provided`,
severity: ErrorSeverity.InvalidJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.UNSUPPORTED_WITH]: {
code: ErrorCode.UNSUPPORTED_WITH,
reason: `JavaScript 'with' syntax is not supported`,
description: `'with' syntax is considered deprecated and removed from JavaScript standards, consider alternatives`,
severity: ErrorSeverity.UnsupportedJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.UNSUPPORTED_INNER_CLASS]: {
code: ErrorCode.UNSUPPORTED_INNER_CLASS,
reason: 'Inline `class` declarations are not supported',
description: `Move class declarations outside of components/hooks`,
severity: ErrorSeverity.UnsupportedJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.INVALID_IMPORT_EXPORT]: {
code: ErrorCode.INVALID_IMPORT_EXPORT,
reason:
'JavaScript `import` and `export` statements may only appear at the top level of a module',
severity: ErrorSeverity.InvalidJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.INVALID_TS_NAMESPACE]: {
code: ErrorCode.INVALID_TS_NAMESPACE,
reason:
'TypeScript `namespace` statements may only appear at the top level of a module',
severity: ErrorSeverity.InvalidJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.UNSUPPORTED_NEW_EXPRESSION]: {
code: ErrorCode.UNSUPPORTED_NEW_EXPRESSION,
reason: `Expected an expression as the \`new\` expression receiver (v8 intrinsics are not supported)`,
severity: ErrorSeverity.InvalidJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.UNSUPPORTED_EMPTY_SEQUENCE_EXPRESSION]: {
code: ErrorCode.UNSUPPORTED_EMPTY_SEQUENCE_EXPRESSION,
reason: `Expected sequence expression to have at least one expression`,
severity: ErrorSeverity.InvalidJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.INVALID_QUASI_LENGTHS]: {
code: ErrorCode.INVALID_QUASI_LENGTHS,
reason: `Unexpected quasi and subexpression lengths in template literal`,
severity: ErrorSeverity.InvalidJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.INVALID_SYNTAX_DELETE_EXPRESSION]: {
code: ErrorCode.INVALID_SYNTAX_DELETE_EXPRESSION,
reason: `Only object properties can be deleted`,
severity: ErrorSeverity.InvalidJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.UNSUPPORTED_THROW_EXPRESSION]: {
code: ErrorCode.UNSUPPORTED_THROW_EXPRESSION,
reason: `Throw expressions are not supported`,
severity: ErrorSeverity.InvalidJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.INVALID_JSX_NAMESPACED_NAME]: {
code: ErrorCode.INVALID_JSX_NAMESPACED_NAME,
reason: `Expected JSXNamespacedName to have no colons in the namespace or name`,
severity: ErrorSeverity.InvalidJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.UNSUPPORTED_EVAL]: {
code: ErrorCode.UNSUPPORTED_EVAL,
reason: `The 'eval' function is not supported`,
description:
'Eval is an anti-pattern in JavaScript, and the code executed cannot be evaluated by React Compiler',
severity: ErrorSeverity.UnsupportedJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.INVALID_SYNTAX_RESERVED_VARIABLE_NAME]: {
code: ErrorCode.INVALID_SYNTAX_RESERVED_VARIABLE_NAME,
reason: 'Expected a non-reserved identifier name',
severity: ErrorSeverity.InvalidJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.INVALID_JAVASCRIPT_AST]: {
code: ErrorCode.INVALID_JAVASCRIPT_AST,
reason: 'Encountered invalid JavaScript',
severity: ErrorSeverity.InvalidJS,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.BAILOUT_ESLINT_SUPPRESSION]: {
code: ErrorCode.BAILOUT_ESLINT_SUPPRESSION,
reason:
'React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled',
description:
'React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior',
severity: ErrorSeverity.InvalidReact,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.MANUAL_MEMO_REMOVED]: {
code: ErrorCode.MANUAL_MEMO_REMOVED,
reason:
'Compilation skipped because existing memoization could not be preserved',
description:
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output',
severity: ErrorSeverity.CannotPreserveMemoization,
linterCategory: LinterCategory.TODO_SYNTAX,
},
/**
* This is left vague as fire is very experimental
*/
[ErrorCode.CANNOT_COMPILE_FIRE]: {
code: ErrorCode.CANNOT_COMPILE_FIRE,
reason: 'Cannot compile `fire`',
severity: ErrorSeverity.InvalidReact,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.DID_NOT_INFER_DEPS]: {
code: ErrorCode.DID_NOT_INFER_DEPS,
reason:
'Cannot infer dependencies of this effect. This will break your build!',
description:
'To resolve, either pass a dependency array or fix reported compiler bailout diagnostics',
severity: ErrorSeverity.InvalidReact,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.BAILOUT_FLOW_SUPPRESSION]: {
code: ErrorCode.BAILOUT_FLOW_SUPPRESSION,
reason:
'React Compiler has skipped optimizing this component because one or more React rule violations were reported by Flow',
description:
'React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior',
severity: ErrorSeverity.InvalidReact,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
/** Syntax React Compiler may eventually support */
[ErrorCode.TODO_CONFLICTING_FBT_IDENTIFIER]: {
code: ErrorCode.TODO_CONFLICTING_FBT_IDENTIFIER,
reason: 'Support local variables named `fbt`',
description:
'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',
severity: ErrorSeverity.Todo,
linterCategory: LinterCategory.TODO_SYNTAX,
},
[ErrorCode.TODO_DUPLICATE_FBT_TAGS]: {
code: ErrorCode.TODO_DUPLICATE_FBT_TAGS,
reason: 'Support duplicate fbt tags',
severity: ErrorSeverity.Todo,
linterCategory: LinterCategory.TODO_SYNTAX,
},
[ErrorCode.UNKNOWN_FUNCTION_PARAMETERS]: {
code: ErrorCode.UNKNOWN_FUNCTION_PARAMETERS,
severity: ErrorSeverity.Todo,
reason: 'Currently unsupported function parameter syntax',
linterCategory: LinterCategory.TODO_SYNTAX,
},
[ErrorCode.MANUAL_MEMO_MUTATED_LATER]: {
code: ErrorCode.MANUAL_MEMO_MUTATED_LATER,
reason:
'Compilation skipped because existing memoization could not be preserved',
description: [
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. ',
'This dependency may be mutated later, which could cause the value to change unexpectedly',
].join(''),
severity: ErrorSeverity.CannotPreserveMemoization,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
[ErrorCode.MANUAL_MEMO_DEPENDENCIES_CONFLICT]: {
code: ErrorCode.MANUAL_MEMO_DEPENDENCIES_CONFLICT,
reason:
'Compilation skipped because existing memoization could not be preserved',
description:
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. ' +
'The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected',
severity: ErrorSeverity.CannotPreserveMemoization,
linterCategory: LinterCategory.UNSUPPORTED_SYNTAX,
},
};

View File

@@ -0,0 +1,34 @@
export enum ErrorSeverity {
/**
* Invalid JS syntax, or valid syntax that is semantically invalid which may indicate some
* misunderstanding on the users part.
*/
InvalidJS = 'InvalidJS',
/**
* JS syntax that is not supported and which we do not plan to support. Developers should
* rewrite to use supported forms.
*/
UnsupportedJS = 'UnsupportedJS',
/**
* Code that breaks the rules of React.
*/
InvalidReact = 'InvalidReact',
/**
* Incorrect configuration of the compiler.
*/
InvalidConfig = 'InvalidConfig',
/**
* Code that can reasonably occur and that doesn't break any rules, but is unsafe to preserve
* memoization.
*/
CannotPreserveMemoization = 'CannotPreserveMemoization',
/**
* Unhandled syntax that we don't support yet.
*/
Todo = 'Todo',
/**
* An unexpected internal error in the compiler that indicates critical issues that can panic
* the compiler.
*/
Invariant = 'Invariant',
}

View File

@@ -90,13 +90,10 @@ export function Ok<T>(val: T): OkImpl<T> {
}
class OkImpl<T> implements Result<T, never> {
#val: T;
constructor(val: T) {
this.#val = val;
}
constructor(private val: T) {}
map<U>(fn: (val: T) => U): Result<U, never> {
return new OkImpl(fn(this.#val));
return new OkImpl(fn(this.val));
}
mapErr<F>(_fn: (val: never) => F): Result<T, F> {
@@ -104,15 +101,15 @@ class OkImpl<T> implements Result<T, never> {
}
mapOr<U>(_fallback: U, fn: (val: T) => U): U {
return fn(this.#val);
return fn(this.val);
}
mapOrElse<U>(_fallback: () => U, fn: (val: T) => U): U {
return fn(this.#val);
return fn(this.val);
}
andThen<U>(fn: (val: T) => Result<U, never>): Result<U, never> {
return fn(this.#val);
return fn(this.val);
}
and<U>(res: Result<U, never>): Result<U, never> {
@@ -136,30 +133,30 @@ class OkImpl<T> implements Result<T, never> {
}
expect(_msg: string): T {
return this.#val;
return this.val;
}
expectErr(msg: string): never {
throw new Error(`${msg}: ${this.#val}`);
throw new Error(`${msg}: ${this.val}`);
}
unwrap(): T {
return this.#val;
return this.val;
}
unwrapOr(_fallback: T): T {
return this.#val;
return this.val;
}
unwrapOrElse(_fallback: (val: never) => T): T {
return this.#val;
return this.val;
}
unwrapErr(): never {
if (this.#val instanceof Error) {
throw this.#val;
if (this.val instanceof Error) {
throw this.val;
}
throw new Error(`Can't unwrap \`Ok\` to \`Err\`: ${this.#val}`);
throw new Error(`Can't unwrap \`Ok\` to \`Err\`: ${this.val}`);
}
}
@@ -168,17 +165,14 @@ export function Err<E>(val: E): ErrImpl<E> {
}
class ErrImpl<E> implements Result<never, E> {
#val: E;
constructor(val: E) {
this.#val = val;
}
constructor(private val: E) {}
map<U>(_fn: (val: never) => U): Result<U, E> {
return this;
}
mapErr<F>(fn: (val: E) => F): Result<never, F> {
return new ErrImpl(fn(this.#val));
return new ErrImpl(fn(this.val));
}
mapOr<U>(fallback: U, _fn: (val: never) => U): U {
@@ -202,7 +196,7 @@ class ErrImpl<E> implements Result<never, E> {
}
orElse<F>(fn: (val: E) => ErrImpl<F>): Result<never, F> {
return fn(this.#val);
return fn(this.val);
}
isOk(): this is OkImpl<never> {
@@ -214,18 +208,18 @@ class ErrImpl<E> implements Result<never, E> {
}
expect(msg: string): never {
throw new Error(`${msg}: ${this.#val}`);
throw new Error(`${msg}: ${this.val}`);
}
expectErr(_msg: string): E {
return this.#val;
return this.val;
}
unwrap(): never {
if (this.#val instanceof Error) {
throw this.#val;
if (this.val instanceof Error) {
throw this.val;
}
throw new Error(`Can't unwrap \`Err\` to \`Ok\`: ${this.#val}`);
throw new Error(`Can't unwrap \`Err\` to \`Ok\`: ${this.val}`);
}
unwrapOr<T>(fallback: T): T {
@@ -233,10 +227,10 @@ class ErrImpl<E> implements Result<never, E> {
}
unwrapOrElse<T>(fallback: (val: E) => T): T {
return fallback(this.#val);
return fallback(this.val);
}
unwrapErr(): E {
return this.#val;
return this.val;
}
}

View File

@@ -182,11 +182,6 @@ export function parseConfigPragmaForTests(
environment?: PartialEnvironmentConfig;
},
): PluginOptions {
const overridePragma = parseConfigPragmaAsString(pragma);
if (overridePragma !== '') {
return parseConfigStringAsJS(overridePragma, defaults);
}
const environment = parseConfigPragmaEnvironmentForTest(
pragma,
defaults.environment ?? {},
@@ -222,90 +217,3 @@ export function parseConfigPragmaForTests(
}
return parsePluginOptions(options);
}
export function parseConfigPragmaAsString(pragma: string): string {
// Check if it's in JS override format
for (const {key, value: val} of splitPragma(pragma)) {
if (key === 'OVERRIDE' && val != null) {
return val;
}
}
return '';
}
function parseConfigStringAsJS(
configString: string,
defaults: {
compilationMode: CompilationMode;
environment?: PartialEnvironmentConfig;
},
): PluginOptions {
let parsedConfig: any;
try {
// Parse the JavaScript object literal
parsedConfig = new Function(`return ${configString}`)();
} catch (error) {
CompilerError.invariant(false, {
reason: 'Failed to parse config pragma as JavaScript object',
description: `Could not parse: ${configString}. Error: ${error}`,
loc: null,
suggestions: null,
});
}
console.log('OVERRIDE:', parsedConfig);
const environment = parseConfigPragmaEnvironmentForTest(
'',
defaults.environment ?? {},
);
const options: Record<keyof PluginOptions, unknown> = {
...defaultOptions,
panicThreshold: 'all_errors',
compilationMode: defaults.compilationMode,
environment,
};
// Apply parsed config, merging environment if it exists
if (parsedConfig.environment) {
const mergedEnvironment = {
...(options.environment as Record<string, unknown>),
...parsedConfig.environment,
};
// Validate environment config
const validatedEnvironment =
EnvironmentConfigSchema.safeParse(mergedEnvironment);
if (!validatedEnvironment.success) {
CompilerError.invariant(false, {
reason: 'Invalid environment configuration in config pragma',
description: `${fromZodError(validatedEnvironment.error)}`,
loc: null,
suggestions: null,
});
}
options.environment = validatedEnvironment.data;
}
// Apply other config options
for (const [key, value] of Object.entries(parsedConfig)) {
if (key === 'environment') {
continue;
}
if (hasOwnProperty(defaultOptions, key)) {
if (key === 'target' && value === 'donotuse_meta_internal') {
options[key] = {
kind: value,
runtimeModule: 'react',
};
} else {
options[key] = value;
}
}
}
return parsePluginOptions(options);
}

View File

@@ -6,12 +6,7 @@
*/
import * as t from '@babel/types';
import {
CompilerError,
CompilerErrorDetail,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {CompilerError, CompilerErrorDetail} from '../CompilerError';
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
import {isHookName} from '../HIR/Environment';
import {
@@ -28,6 +23,7 @@ import {
} from '../HIR/visitors';
import {assertExhaustive} from '../Utils/utils';
import {Result} from '../Utils/Result';
import {ErrorCode, ErrorCodeDetails} from '../Utils/CompilerErrorCodes';
/**
* Represents the possible kinds of value which may be stored at a given Place during
@@ -112,8 +108,6 @@ export function validateHooksUsage(
// Once a particular hook has a conditional call error, don't report any further issues for this hook
setKind(place, Kind.Error);
const reason =
'Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)';
const previousError =
typeof place.loc !== 'symbol' ? errorsByPlace.get(place.loc) : undefined;
@@ -121,16 +115,15 @@ export function validateHooksUsage(
* In some circumstances such as optional calls, we may first encounter a "hook may not be referenced as normal values" error.
* If that same place is also used as a conditional call, upgrade the error to a conditonal hook error
*/
if (previousError === undefined || previousError.reason !== reason) {
if (
previousError === undefined ||
previousError.reason !==
ErrorCodeDetails[ErrorCode.HOOK_CALL_STATIC].reason
) {
recordError(
place.loc,
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
description: null,
reason,
CompilerErrorDetail.fromCode(ErrorCode.HOOK_CALL_STATIC, {
loc: place.loc,
severity: ErrorSeverity.InvalidReact,
suggestions: null,
}),
);
}
@@ -141,14 +134,8 @@ export function validateHooksUsage(
if (previousError === undefined) {
recordError(
place.loc,
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
description: null,
reason:
'Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values',
CompilerErrorDetail.fromCode(ErrorCode.HOOK_INVALID_REFERENCE, {
loc: place.loc,
severity: ErrorSeverity.InvalidReact,
suggestions: null,
}),
);
}
@@ -159,15 +146,12 @@ export function validateHooksUsage(
if (previousError === undefined) {
recordError(
place.loc,
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
description: null,
reason:
'Hooks must be the same function on every render, but this value may change over time to a different function. See https://react.dev/reference/rules/react-calls-components-and-hooks#dont-dynamically-use-hooks',
loc: place.loc,
severity: ErrorSeverity.InvalidReact,
suggestions: null,
}),
CompilerErrorDetail.fromCode(
ErrorCodeDetails[ErrorCode.HOOK_CALL_REACTIVE].code,
{
loc: place.loc,
},
),
);
}
}
@@ -450,17 +434,10 @@ function visitFunctionExpression(errors: CompilerError, fn: HIRFunction): void {
: instr.value.property;
const hookKind = getHookKind(fn.env, callee.identifier);
if (hookKind != null) {
errors.pushErrorDetail(
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
severity: ErrorSeverity.InvalidReact,
reason:
'Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)',
loc: callee.loc,
description: `Cannot call ${hookKind === 'Custom' ? 'hook' : hookKind} within a function expression`,
suggestions: null,
}),
);
errors.pushErrorCode(ErrorCode.HOOK_CALL_NOT_TOP_LEVEL, {
loc: callee.loc,
description: `Cannot call ${hookKind === 'Custom' ? 'hook' : hookKind} within a function expression`,
});
}
break;
}

View File

@@ -5,22 +5,25 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..';
import {ErrorCategory} from '../CompilerError';
import {CompilerDiagnostic, CompilerError, Effect} from '..';
import {HIRFunction, IdentifierId, Place} from '../HIR';
import {
eachInstructionLValue,
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
import {Ok, Result} from '../Utils/Result';
/**
* Validates that local variables cannot be reassigned after render.
* This prevents a category of bugs in which a closure captures a
* binding from one render but does not update
*/
export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
export function validateLocalsNotReassignedAfterRender(
fn: HIRFunction,
): Result<void, CompilerError> {
const contextVariables = new Set<IdentifierId>();
const reassignment = getContextReassignment(
fn,
@@ -36,19 +39,15 @@ export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
? `\`${reassignment.identifier.name.value}\``
: 'variable';
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Immutability,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot reassign variable after render completes',
description: `Reassigning ${variable} after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.`,
}).withDetail({
CompilerDiagnostic.fromCode(ErrorCode.REASSIGN_AFTER_RENDER).withDetail({
kind: 'error',
loc: reassignment.loc,
message: `Cannot reassign ${variable} after render completes`,
}),
);
throw errors;
return errors.asResult();
}
return Ok(undefined);
}
function getContextReassignment(
@@ -92,13 +91,9 @@ function getContextReassignment(
? `\`${reassignment.identifier.name.value}\``
: 'variable';
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Immutability,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot reassign variable in async function',
description:
'Reassigning a variable in an async function can cause inconsistent behavior on subsequent renders. Consider using state instead',
}).withDetail({
CompilerDiagnostic.fromCode(
ErrorCode.REASSIGN_IN_ASYNC,
).withDetail({
kind: 'error',
loc: reassignment.loc,
message: `Cannot reassign ${variable}`,

View File

@@ -5,8 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, ErrorSeverity} from '..';
import {ErrorCategory} from '../CompilerError';
import {CompilerError} from '..';
import {
Identifier,
Instruction,
@@ -23,6 +22,7 @@ import {
ReactiveFunctionVisitor,
visitReactiveFunction,
} from '../ReactiveScopes/visitors';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
import {Result} from '../Utils/Result';
/**
@@ -109,13 +109,8 @@ class Visitor extends ReactiveFunctionVisitor<CompilerError> {
isUnmemoized(deps.identifier, this.scopes))
) {
state.push({
category: ErrorCategory.EffectDependencies,
reason:
'React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior',
description: null,
severity: ErrorSeverity.CannotPreserveMemoization,
errorCode: ErrorCode.MEMOIZED_EFFECT_DEPENDENCIES,
loc: typeof instruction.loc !== 'symbol' ? instruction.loc : null,
suggestions: null,
});
}
}

View File

@@ -5,10 +5,10 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, EnvironmentConfig, ErrorSeverity} from '..';
import {ErrorCategory} from '../CompilerError';
import {CompilerError, EnvironmentConfig} from '..';
import {HIRFunction, IdentifierId} from '../HIR';
import {DEFAULT_GLOBALS} from '../HIR/Globals';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
import {Result} from '../Utils/Result';
export function validateNoCapitalizedCalls(
@@ -34,8 +34,6 @@ export function validateNoCapitalizedCalls(
const errors = new CompilerError();
const capitalLoadGlobals = new Map<IdentifierId, string>();
const capitalizedProperties = new Map<IdentifierId, string>();
const reason =
'Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config';
for (const [, block] of fn.body.blocks) {
for (const {lvalue, value} of block.instructions) {
switch (value.kind) {
@@ -56,12 +54,9 @@ export function validateNoCapitalizedCalls(
const calleeIdentifier = value.callee.identifier.id;
const calleeName = capitalLoadGlobals.get(calleeIdentifier);
if (calleeName != null) {
CompilerError.throwInvalidReact({
category: ErrorCategory.CapitalizedCalls,
reason,
errors.pushErrorCode(ErrorCode.CAPITALIZED_CALLS, {
description: `${calleeName} may be a component.`,
loc: value.loc,
suggestions: null,
});
}
break;
@@ -80,13 +75,9 @@ export function validateNoCapitalizedCalls(
const propertyIdentifier = value.property.identifier.id;
const propertyName = capitalizedProperties.get(propertyIdentifier);
if (propertyName != null) {
errors.push({
category: ErrorCategory.CapitalizedCalls,
severity: ErrorSeverity.InvalidReact,
reason,
errors.pushErrorCode(ErrorCode.CAPITALIZED_CALLS, {
description: `${propertyName} may be a component.`,
loc: value.loc,
suggestions: null,
});
}
break;

View File

@@ -5,8 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, ErrorSeverity, SourceLocation} from '..';
import {ErrorCategory} from '../CompilerError';
import {CompilerError, SourceLocation} from '..';
import {
ArrayExpression,
BlockId,
@@ -20,6 +19,8 @@ import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
import {Result} from '../Utils/Result';
/**
* Validates that useEffect is not used for derived computations which could/should
@@ -44,7 +45,9 @@ import {
* const fullName = firstName + ' ' + lastName;
* ```
*/
export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
export function validateNoDerivedComputationsInEffects(
fn: HIRFunction,
): Result<void, CompilerError> {
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
const functions: Map<IdentifierId, FunctionExpression> = new Map();
const locals: Map<IdentifierId, IdentifierId> = new Map();
@@ -97,9 +100,7 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
}
}
}
if (errors.hasErrors()) {
throw errors;
}
return errors.asResult();
}
function validateEffect(
@@ -219,14 +220,6 @@ function validateEffect(
}
for (const loc of setStateLocations) {
errors.push({
category: ErrorCategory.EffectDerivationsOfState,
reason:
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
description: null,
severity: ErrorSeverity.InvalidReact,
loc,
suggestions: null,
});
errors.pushErrorCode(ErrorCode.NO_DERIVED_COMPUTATIONS_IN_EFFECTS, {loc});
}
}

View File

@@ -5,11 +5,13 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..';
import {ErrorCategory} from '../CompilerError';
import {CompilerDiagnostic, CompilerError, Effect} from '..';
import {ErrorCode} from '../CompilerError';
import {
FunctionEffect,
HIRFunction,
IdentifierId,
isMutableEffect,
isRefOrRefLikeMutableType,
Place,
} from '../HIR';
@@ -17,8 +19,8 @@ import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {AliasingEffect} from '../Inference/AliasingEffects';
import {Result} from '../Utils/Result';
import {Iterable_some} from '../Utils/utils';
/**
* Validates that functions with known mutations (ie due to types) cannot be passed
@@ -49,14 +51,14 @@ export function validateNoFreezingKnownMutableFunctions(
const errors = new CompilerError();
const contextMutationEffects: Map<
IdentifierId,
Extract<AliasingEffect, {kind: 'Mutate'} | {kind: 'MutateTransitive'}>
Extract<FunctionEffect, {kind: 'ContextMutation'}>
> = new Map();
function visitOperand(operand: Place): void {
if (operand.effect === Effect.Freeze) {
const effect = contextMutationEffects.get(operand.identifier.id);
if (effect != null) {
const place = effect.value;
const place = [...effect.places][0];
const variable =
place != null &&
place.identifier.name != null &&
@@ -64,12 +66,7 @@ export function validateNoFreezingKnownMutableFunctions(
? `\`${place.identifier.name.value}\``
: 'a local variable';
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Immutability,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot modify local variables after render completes',
description: `This argument is a function which may reassign or mutate ${variable} after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.`,
})
CompilerDiagnostic.fromCode(ErrorCode.WRITE_AFTER_RENDER)
.withDetail({
kind: 'error',
loc: operand.loc,
@@ -77,7 +74,7 @@ export function validateNoFreezingKnownMutableFunctions(
})
.withDetail({
kind: 'error',
loc: effect.value.loc,
loc: effect.loc,
message: `This modifies ${variable}`,
}),
);
@@ -108,7 +105,27 @@ export function validateNoFreezingKnownMutableFunctions(
break;
}
case 'FunctionExpression': {
if (value.loweredFunc.func.aliasingEffects != null) {
const knownMutation = (value.loweredFunc.func.effects ?? []).find(
effect => {
return (
effect.kind === 'ContextMutation' &&
(effect.effect === Effect.Store ||
effect.effect === Effect.Mutate) &&
Iterable_some(effect.places, place => {
return (
isMutableEffect(place.effect, place.loc) &&
!isRefOrRefLikeMutableType(place.identifier.type)
);
})
);
},
);
if (knownMutation && knownMutation.kind === 'ContextMutation') {
contextMutationEffects.set(lvalue.identifier.id, knownMutation);
} else if (
fn.env.config.enableNewMutationAliasingModel &&
value.loweredFunc.func.aliasingEffects != null
) {
const context = new Set(
value.loweredFunc.func.context.map(p => p.identifier.id),
);
@@ -129,7 +146,12 @@ export function validateNoFreezingKnownMutableFunctions(
context.has(effect.value.identifier.id) &&
!isRefOrRefLikeMutableType(effect.value.identifier.type)
) {
contextMutationEffects.set(lvalue.identifier.id, effect);
contextMutationEffects.set(lvalue.identifier.id, {
kind: 'ContextMutation',
effect: Effect.Mutate,
loc: effect.value.loc,
places: new Set([effect.value]),
});
break effects;
}
break;

View File

@@ -5,10 +5,10 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
import {ErrorCategory} from '../CompilerError';
import {CompilerDiagnostic, CompilerError} from '..';
import {HIRFunction} from '../HIR';
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
import {Result} from '../Utils/Result';
/**
@@ -36,20 +36,13 @@ export function validateNoImpureFunctionsInRender(
);
if (signature != null && signature.impure === true) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Purity,
reason: 'Cannot call impure function during render',
description:
(signature.canonicalName != null
? `\`${signature.canonicalName}\` is an impure function. `
: '') +
'Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
severity: ErrorSeverity.InvalidReact,
suggestions: null,
}).withDetail({
CompilerDiagnostic.fromCode(ErrorCode.IMPURE_FUNCTIONS).withDetail({
kind: 'error',
loc: callee.loc,
message: 'Cannot call impure function',
message:
signature.canonicalName != null
? `\`${signature.canonicalName}\` is an impure function. `
: 'This is an impure function.',
}),
);
}

View File

@@ -5,9 +5,9 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
import {ErrorCategory} from '../CompilerError';
import {CompilerDiagnostic, CompilerError} from '..';
import {BlockId, HIRFunction} from '../HIR';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
import {Result} from '../Utils/Result';
import {retainWhere} from '../Utils/utils';
@@ -36,12 +36,7 @@ export function validateNoJSXInTryStatement(
case 'JsxExpression':
case 'JsxFragment': {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.ErrorBoundaries,
severity: ErrorSeverity.InvalidReact,
reason: 'Avoid constructing JSX within try/catch',
description: `React does not immediately render components when JSX is rendered, so any errors from this component will not be caught by the try/catch. To catch errors in rendering a given component, wrap that component in an error boundary. (https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)`,
}).withDetail({
CompilerDiagnostic.fromCode(ErrorCode.JSX_IN_TRY).withDetail({
kind: 'error',
loc: value.loc,
message: 'Avoid constructing JSX within try/catch',

View File

@@ -5,12 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {
CompilerDiagnostic,
CompilerError,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {CompilerDiagnostic, CompilerError} from '../CompilerError';
import {
BlockId,
HIRFunction,
@@ -27,6 +22,7 @@ import {
eachPatternOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
import {Err, Ok, Result} from '../Utils/Result';
import {retainWhere} from '../Utils/utils';
@@ -468,12 +464,9 @@ function validateNoRefAccessInRenderImpl(
if (fnType.fn.readRefEffect) {
didError = true;
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Refs,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetail({
CompilerDiagnostic.fromCode(
ErrorCode.NO_REF_ACCESS_IN_RENDER,
).withDetail({
kind: 'error',
loc: callee.loc,
message: `This function accesses a ref value`,
@@ -732,16 +725,13 @@ function destructure(
function guardCheck(errors: CompilerError, operand: Place, env: Env): void {
if (env.get(operand.identifier.id)?.kind === 'Guard') {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Refs,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetail({
kind: 'error',
loc: operand.loc,
message: `Cannot access ref value during render`,
}),
CompilerDiagnostic.fromCode(ErrorCode.NO_REF_ACCESS_IN_RENDER).withDetail(
{
kind: 'error',
loc: operand.loc,
message: `Cannot access ref value during render`,
},
),
);
}
}
@@ -757,16 +747,13 @@ function validateNoRefValueAccess(
(type?.kind === 'Structure' && type.fn?.readRefEffect)
) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Refs,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetail({
kind: 'error',
loc: (type.kind === 'RefValue' && type.loc) || operand.loc,
message: `Cannot access ref value during render`,
}),
CompilerDiagnostic.fromCode(ErrorCode.NO_REF_ACCESS_IN_RENDER).withDetail(
{
kind: 'error',
loc: (type.kind === 'RefValue' && type.loc) || operand.loc,
message: `Cannot access ref value during render`,
},
),
);
}
}
@@ -784,16 +771,13 @@ function validateNoRefPassedToFunction(
(type?.kind === 'Structure' && type.fn?.readRefEffect)
) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Refs,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetail({
kind: 'error',
loc: (type.kind === 'RefValue' && type.loc) || loc,
message: `Passing a ref to a function may read its value during render`,
}),
CompilerDiagnostic.fromCode(ErrorCode.NO_REF_ACCESS_IN_RENDER).withDetail(
{
kind: 'error',
loc: (type.kind === 'RefValue' && type.loc) || loc,
message: `Passing a ref to a function may read its value during render`,
},
),
);
}
}
@@ -807,16 +791,13 @@ function validateNoRefUpdate(
const type = destructure(env.get(operand.identifier.id));
if (type?.kind === 'Ref' || type?.kind === 'RefValue') {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Refs,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetail({
kind: 'error',
loc: (type.kind === 'RefValue' && type.loc) || loc,
message: `Cannot update ref during render`,
}),
CompilerDiagnostic.fromCode(ErrorCode.NO_REF_ACCESS_IN_RENDER).withDetail(
{
kind: 'error',
loc: (type.kind === 'RefValue' && type.loc) || loc,
message: `Cannot update ref during render`,
},
),
);
}
}
@@ -829,22 +810,13 @@ function validateNoDirectRefValueAccess(
const type = destructure(env.get(operand.identifier.id));
if (type?.kind === 'RefValue') {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Refs,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetail({
kind: 'error',
loc: type.loc ?? operand.loc,
message: `Cannot access ref value during render`,
}),
CompilerDiagnostic.fromCode(ErrorCode.NO_REF_ACCESS_IN_RENDER).withDetail(
{
kind: 'error',
loc: type.loc ?? operand.loc,
message: `Cannot access ref value during render`,
},
),
);
}
}
const ERROR_DESCRIPTION =
'React refs are values that are not needed for rendering. Refs should only be accessed ' +
'outside of render, such as in event handlers or effects. ' +
'Accessing a ref value (the `current` property) during render can cause your component ' +
'not to update as expected (https://react.dev/reference/react/useRef)';

View File

@@ -5,12 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {
CompilerDiagnostic,
CompilerError,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {CompilerDiagnostic, CompilerError} from '../CompilerError';
import {
HIRFunction,
IdentifierId,
@@ -21,6 +16,7 @@ import {
Place,
} from '../HIR';
import {eachInstructionValueOperand} from '../HIR/visitors';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
import {Result} from '../Utils/Result';
/**
@@ -96,20 +92,9 @@ export function validateNoSetStateInEffects(
const setState = setStateFunctions.get(arg.identifier.id);
if (setState !== undefined) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.EffectSetState,
reason:
'Calling setState synchronously within an effect can trigger cascading renders',
description:
'Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. ' +
'In general, the body of an effect should do one or both of the following:\n' +
'* Update external systems with the latest state from React.\n' +
'* Subscribe for updates from some external system, calling setState in a callback function when external state changes.\n\n' +
'Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. ' +
'(https://react.dev/learn/you-might-not-need-an-effect)',
severity: ErrorSeverity.InvalidReact,
suggestions: null,
}).withDetail({
CompilerDiagnostic.fromCode(
ErrorCode.INVALID_SET_STATE_IN_EFFECTS,
).withDetail({
kind: 'error',
loc: setState.loc,
message:

View File

@@ -5,15 +5,11 @@
* LICENSE file in the root directory of this source tree.
*/
import {
CompilerDiagnostic,
CompilerError,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {CompilerDiagnostic, CompilerError} from '../CompilerError';
import {HIRFunction, IdentifierId, isSetStateType} from '../HIR';
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
import {eachInstructionValueOperand} from '../HIR/visitors';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
import {Result} from '../Utils/Result';
/**
@@ -128,15 +124,9 @@ function validateNoSetStateInRenderImpl(
) {
if (activeManualMemoId !== null) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.RenderSetState,
reason:
'Calling setState from useMemo may trigger an infinite loop',
description:
'Each time the memo callback is evaluated it will change state. This can cause a memoization dependency to change, running the memo function again and causing an infinite loop. Instead of setting state in useMemo(), prefer deriving the value during render. (https://react.dev/reference/react/useState)',
severity: ErrorSeverity.InvalidReact,
suggestions: null,
}).withDetail({
CompilerDiagnostic.fromCode(
ErrorCode.INVALID_SET_STATE_IN_MEMO,
).withDetail({
kind: 'error',
loc: callee.loc,
message: 'Found setState() within useMemo()',
@@ -144,18 +134,12 @@ function validateNoSetStateInRenderImpl(
);
} else if (unconditionalBlocks.has(block.id)) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.RenderSetState,
reason:
'Calling setState during render may trigger an infinite loop',
description:
'Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState)',
severity: ErrorSeverity.InvalidReact,
suggestions: null,
}).withDetail({
CompilerDiagnostic.fromCode(
ErrorCode.INVALID_SET_STATE_IN_RENDER,
).withDetail({
kind: 'error',
loc: callee.loc,
message: 'Found setState() in render',
message: 'Found setState() call here',
}),
);
}

View File

@@ -5,12 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {
CompilerDiagnostic,
CompilerError,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {CompilerDiagnostic, CompilerError, ErrorCode} from '../CompilerError';
import {
DeclarationId,
Effect,
@@ -281,13 +276,8 @@ function validateInferredDep(
}
}
errorState.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.PreserveManualMemo,
severity: ErrorSeverity.CannotPreserveMemoization,
reason: 'Existing memoization could not be preserved',
description: [
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. ',
'The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. ',
CompilerDiagnostic.fromCode(ErrorCode.MANUAL_MEMO_DEPENDENCIES_CONFLICT, {
description:
DEBUG ||
// If the dependency is a named variable then we can report it. Otherwise only print in debug mode
(dep.identifier.name != null && dep.identifier.name.kind === 'named')
@@ -299,11 +289,8 @@ function validateInferredDep(
errorDiagnostic
? getCompareDependencyResultDescription(errorDiagnostic)
: 'Inferred dependency not present in source'
}.`
}`
: '',
]
.join('')
.trim(),
suggestions: null,
}).withDetail({
kind: 'error',
@@ -535,15 +522,9 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
!this.prunedScopes.has(identifier.scope.id)
) {
state.errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.PreserveManualMemo,
severity: ErrorSeverity.CannotPreserveMemoization,
reason: 'Existing memoization could not be preserved',
description: [
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. ',
'This dependency may be mutated later, which could cause the value to change unexpectedly.',
].join(''),
}).withDetail({
CompilerDiagnostic.fromCode(
ErrorCode.MANUAL_MEMO_MUTATED_LATER,
).withDetail({
kind: 'error',
loc,
message: 'This dependency may be modified later',
@@ -583,18 +564,10 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
for (const identifier of decls) {
if (isUnmemoized(identifier, this.scopes)) {
state.errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.PreserveManualMemo,
severity: ErrorSeverity.CannotPreserveMemoization,
reason: 'Existing memoization could not be preserved',
description: [
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. ',
DEBUG
? `${printIdentifier(identifier)} was not memoized.`
: '',
]
.join('')
.trim(),
CompilerDiagnostic.fromCode(ErrorCode.MANUAL_MEMO_REMOVED, {
description: DEBUG
? `${printIdentifier(identifier)} was not memoized`
: '',
}).withDetail({
kind: 'error',
loc,

View File

@@ -5,13 +5,9 @@
* LICENSE file in the root directory of this source tree.
*/
import {
CompilerDiagnostic,
CompilerError,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {CompilerDiagnostic, CompilerError} from '../CompilerError';
import {HIRFunction, IdentifierId, SourceLocation} from '../HIR';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
import {Result} from '../Utils/Result';
/**
@@ -65,10 +61,7 @@ export function validateStaticComponents(
);
if (location != null) {
error.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.StaticComponents,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot create components during render',
CompilerDiagnostic.fromCode(ErrorCode.STATIC_COMPONENTS, {
description: `Components created during render will reset their state each time they are created. Declare components outside of render. `,
})
.withDetail({

View File

@@ -5,21 +5,14 @@
* LICENSE file in the root directory of this source tree.
*/
import {
CompilerDiagnostic,
CompilerError,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {SuppressionRange, suppressionsToCompilerError} from '../Entrypoint';
import {CompilerDiagnostic, CompilerError} from '../CompilerError';
import {FunctionExpression, HIRFunction, IdentifierId} from '../HIR';
import {ErrorCode} from '../Utils/CompilerErrorCodes';
import {Result} from '../Utils/Result';
export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
const errors = new CompilerError();
const suppressions: Array<SuppressionRange> = [];
const useMemos = new Set<IdentifierId>();
const effects = new Set<IdentifierId>();
const react = new Set<IdentifierId>();
const functions = new Map<IdentifierId, FunctionExpression>();
for (const [, block] of fn.body.blocks) {
@@ -28,12 +21,6 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
case 'LoadGlobal': {
if (value.binding.name === 'useMemo') {
useMemos.add(lvalue.identifier.id);
} else if (
value.binding.name === 'useEffect' ||
value.binding.name === 'useLayoutEffect' ||
value.binding.name === 'useInsertionEffect'
) {
effects.add(lvalue.identifier.id);
} else if (value.binding.name === 'React') {
react.add(lvalue.identifier.id);
}
@@ -43,12 +30,6 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
if (react.has(value.object.identifier.id)) {
if (value.property === 'useMemo') {
useMemos.add(lvalue.identifier.id);
} else if (
value.property === 'useEffect' ||
value.property === 'useLayoutEffect' ||
value.property === 'useInsertionEffect'
) {
effects.add(lvalue.identifier.id);
}
}
break;
@@ -62,18 +43,9 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
// Is the function being called useMemo, with at least 1 argument?
const callee =
value.kind === 'CallExpression'
? value.callee.identifier
: value.property.identifier;
const isEffect = effects.has(callee.id);
if (
!isEffect &&
value.suppressions != null &&
value.suppressions.length !== 0
) {
suppressions.push(...value.suppressions);
}
const isUseMemo = useMemos.has(callee.id);
? value.callee.identifier.id
: value.property.identifier.id;
const isUseMemo = useMemos.has(callee);
if (!isUseMemo || value.args.length === 0) {
continue;
}
@@ -98,14 +70,9 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
? firstParam.loc
: firstParam.place.loc;
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
severity: ErrorSeverity.InvalidReact,
reason: 'useMemo() callbacks may not accept parameters',
description:
'useMemo() callbacks are called by React to cache calculations across re-renders. They should not take parameters. Instead, directly reference the props, state, or local variables needed for the computation.',
suggestions: null,
}).withDetail({
CompilerDiagnostic.fromCode(
ErrorCode.INVALID_USE_MEMO_CALLBACK_PARAMETERS,
).withDetail({
kind: 'error',
loc,
message: 'Callbacks with parameters are not supported',
@@ -115,15 +82,9 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
if (body.loweredFunc.func.async || body.loweredFunc.func.generator) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
severity: ErrorSeverity.InvalidReact,
reason:
'useMemo() callbacks may not be async or generator functions',
description:
'useMemo() callbacks are called once and must synchronously return a value.',
suggestions: null,
}).withDetail({
CompilerDiagnostic.fromCode(
ErrorCode.INVALID_USE_MEMO_CALLBACK_ASYNC,
).withDetail({
kind: 'error',
loc: body.loc,
message: 'Async and generator functions are not supported',
@@ -136,9 +97,5 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
}
}
}
if (suppressions.length !== 0) {
const suppressionError = suppressionsToCompilerError(suppressions);
errors.merge(suppressionError);
}
return errors.asResult();
}

View File

@@ -46,16 +46,14 @@ function useFoo(t0) {
t1 = $[0];
}
let items = t1;
if ($[1] !== cond) {
bb0: {
if (cond) {
items = [];
} else {
break bb0;
}
items.push(2);
bb0: if ($[1] !== cond) {
if (cond) {
items = [];
} else {
break bb0;
}
items.push(2);
$[1] = cond;
$[2] = items;
} else {

View File

@@ -1,39 +0,0 @@
## Input
```javascript
// @enableNewMutationAliasingModel:false
function Component() {
const foo = () => {
someGlobal = true;
};
// spreading a function is weird, but it doesn't call the function so this is allowed
return <div {...foo} />;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
function Component() {
const $ = _c(1);
const foo = _temp;
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <div {...foo} />;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
function _temp() {
someGlobal = true;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,107 @@
## Input
```javascript
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
/**
* 1. `InferMutableRanges` derives the mutable range of identifiers and their
* aliases from `LoadLocal`, `PropertyLoad`, etc
* - After this pass, y's mutable range only extends to `arrayPush(x, y)`
* - We avoid assigning mutable ranges to loads after y's mutable range, as
* these are working with an immutable value. As a result, `LoadLocal y` and
* `PropertyLoad y` do not get mutable ranges
* 2. `InferReactiveScopeVariables` extends mutable ranges and creates scopes,
* as according to the 'co-mutation' of different values
* - Here, we infer that
* - `arrayPush(y, x)` might alias `x` and `y` to each other
* - `setPropertyKey(x, ...)` may mutate both `x` and `y`
* - This pass correctly extends the mutable range of `y`
* - Since we didn't run `InferMutableRange` logic again, the LoadLocal /
* PropertyLoads still don't have a mutable range
*
* Note that the this bug is an edge case. Compiler output is only invalid for:
* - function expressions with
* `enableTransitivelyFreezeFunctionExpressions:false`
* - functions that throw and get retried without clearing the memocache
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}</div>
* Forget:
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
*/
function useFoo({a, b}: {a: number, b: number}) {
const x = [];
const y = {value: a};
arrayPush(x, y); // x and y co-mutate
const y_alias = y;
const cb = () => y_alias.value;
setPropertyByKey(x[0], 'value', b); // might overwrite y.value
return <Stringify cb={cb} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: 2, b: 10}],
sequentialRenders: [
{a: 2, b: 10},
{a: 2, b: 11},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { arrayPush, setPropertyByKey, Stringify } from "shared-runtime";
function useFoo(t0) {
const $ = _c(5);
const { a, b } = t0;
let t1;
if ($[0] !== a || $[1] !== b) {
const x = [];
const y = { value: a };
arrayPush(x, y);
const y_alias = y;
let t2;
if ($[3] !== y_alias.value) {
t2 = () => y_alias.value;
$[3] = y_alias.value;
$[4] = t2;
} else {
t2 = $[4];
}
const cb = t2;
setPropertyByKey(x[0], "value", b);
t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
$[0] = a;
$[1] = b;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ a: 2, b: 10 }],
sequentialRenders: [
{ a: 2, b: 10 },
{ a: 2, b: 11 },
],
};
```

View File

@@ -0,0 +1,53 @@
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
/**
* 1. `InferMutableRanges` derives the mutable range of identifiers and their
* aliases from `LoadLocal`, `PropertyLoad`, etc
* - After this pass, y's mutable range only extends to `arrayPush(x, y)`
* - We avoid assigning mutable ranges to loads after y's mutable range, as
* these are working with an immutable value. As a result, `LoadLocal y` and
* `PropertyLoad y` do not get mutable ranges
* 2. `InferReactiveScopeVariables` extends mutable ranges and creates scopes,
* as according to the 'co-mutation' of different values
* - Here, we infer that
* - `arrayPush(y, x)` might alias `x` and `y` to each other
* - `setPropertyKey(x, ...)` may mutate both `x` and `y`
* - This pass correctly extends the mutable range of `y`
* - Since we didn't run `InferMutableRange` logic again, the LoadLocal /
* PropertyLoads still don't have a mutable range
*
* Note that the this bug is an edge case. Compiler output is only invalid for:
* - function expressions with
* `enableTransitivelyFreezeFunctionExpressions:false`
* - functions that throw and get retried without clearing the memocache
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}</div>
* Forget:
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
*/
function useFoo({a, b}: {a: number, b: number}) {
const x = [];
const y = {value: a};
arrayPush(x, y); // x and y co-mutate
const y_alias = y;
const cb = () => y_alias.value;
setPropertyByKey(x[0], 'value', b); // might overwrite y.value
return <Stringify cb={cb} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: 2, b: 10}],
sequentialRenders: [
{a: 2, b: 10},
{a: 2, b: 11},
],
};

View File

@@ -0,0 +1,87 @@
## Input
```javascript
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
import {setPropertyByKey, Stringify} from 'shared-runtime';
/**
* Variation of bug in `bug-aliased-capture-aliased-mutate`
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
* Forget:
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
*/
function useFoo({a}: {a: number, b: number}) {
const arr = [];
const obj = {value: a};
setPropertyByKey(obj, 'arr', arr);
const obj_alias = obj;
const cb = () => obj_alias.arr.length;
for (let i = 0; i < a; i++) {
arr.push(i);
}
return <Stringify cb={cb} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: 2}],
sequentialRenders: [{a: 2}, {a: 3}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { setPropertyByKey, Stringify } from "shared-runtime";
function useFoo(t0) {
const $ = _c(4);
const { a } = t0;
let t1;
if ($[0] !== a) {
const arr = [];
const obj = { value: a };
setPropertyByKey(obj, "arr", arr);
const obj_alias = obj;
let t2;
if ($[2] !== obj_alias.arr.length) {
t2 = () => obj_alias.arr.length;
$[2] = obj_alias.arr.length;
$[3] = t2;
} else {
t2 = $[3];
}
const cb = t2;
for (let i = 0; i < a; i++) {
arr.push(i);
}
t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
$[0] = a;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ a: 2 }],
sequentialRenders: [{ a: 2 }, { a: 3 }],
};
```

View File

@@ -0,0 +1,34 @@
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
import {setPropertyByKey, Stringify} from 'shared-runtime';
/**
* Variation of bug in `bug-aliased-capture-aliased-mutate`
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
* Forget:
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
*/
function useFoo({a}: {a: number, b: number}) {
const arr = [];
const obj = {value: a};
setPropertyByKey(obj, 'arr', arr);
const obj_alias = obj;
const cb = () => obj_alias.arr.length;
for (let i = 0; i < a; i++) {
arr.push(i);
}
return <Stringify cb={cb} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: 2}],
sequentialRenders: [{a: 2}, {a: 3}],
};

View File

@@ -85,11 +85,19 @@ import { makeArray, mutate } from "shared-runtime";
* used when we analyze CallExpressions.
*/
function Component(t0) {
const $ = _c(3);
const $ = _c(5);
const { foo, bar } = t0;
let t1;
if ($[0] !== foo) {
t1 = { foo };
$[0] = foo;
$[1] = t1;
} else {
t1 = $[1];
}
const x = t1;
let y;
if ($[0] !== bar || $[1] !== foo) {
const x = { foo };
if ($[2] !== bar || $[3] !== x) {
y = { bar };
const f0 = function () {
const a = makeArray(y);
@@ -100,11 +108,11 @@ function Component(t0) {
f0();
mutate(y.x);
$[0] = bar;
$[1] = foo;
$[2] = y;
$[2] = bar;
$[3] = x;
$[4] = y;
} else {
y = $[2];
y = $[4];
}
return y;
}

View File

@@ -0,0 +1,92 @@
## Input
```javascript
// @enableNewMutationAliasingModel:false
import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
/**
* Fixture showing an edge case for ReactiveScope variable propagation.
*
* Found differences in evaluator results
* Non-forget (expected):
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* Forget:
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* [[ (exception in render) Error: invariant broken ]]
*
*/
function Component() {
const obj = CONST_TRUE ? {inner: {value: 'hello'}} : null;
const boxedInner = [obj?.inner];
useIdentity(null);
mutate(obj);
if (boxedInner[0] !== obj?.inner) {
throw new Error('invariant broken');
}
return <Stringify obj={obj} inner={boxedInner} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{arg: 0}],
sequentialRenders: [{arg: 0}, {arg: 1}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
import { CONST_TRUE, Stringify, mutate, useIdentity } from "shared-runtime";
/**
* Fixture showing an edge case for ReactiveScope variable propagation.
*
* Found differences in evaluator results
* Non-forget (expected):
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* Forget:
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* [[ (exception in render) Error: invariant broken ]]
*
*/
function Component() {
const $ = _c(4);
const obj = CONST_TRUE ? { inner: { value: "hello" } } : null;
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = [obj?.inner];
$[0] = t0;
} else {
t0 = $[0];
}
const boxedInner = t0;
useIdentity(null);
mutate(obj);
if (boxedInner[0] !== obj?.inner) {
throw new Error("invariant broken");
}
let t1;
if ($[1] !== boxedInner || $[2] !== obj) {
t1 = <Stringify obj={obj} inner={boxedInner} />;
$[1] = boxedInner;
$[2] = obj;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ arg: 0 }],
sequentialRenders: [{ arg: 0 }, { arg: 1 }],
};
```

View File

@@ -0,0 +1,31 @@
// @enableNewMutationAliasingModel:false
import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
/**
* Fixture showing an edge case for ReactiveScope variable propagation.
*
* Found differences in evaluator results
* Non-forget (expected):
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* Forget:
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* [[ (exception in render) Error: invariant broken ]]
*
*/
function Component() {
const obj = CONST_TRUE ? {inner: {value: 'hello'}} : null;
const boxedInner = [obj?.inner];
useIdentity(null);
mutate(obj);
if (boxedInner[0] !== obj?.inner) {
throw new Error('invariant broken');
}
return <Stringify obj={obj} inner={boxedInner} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{arg: 0}],
sequentialRenders: [{arg: 0}, {arg: 1}],
};

View File

@@ -0,0 +1,110 @@
## Input
```javascript
// @enableNewMutationAliasingModel:false
import {identity, mutate} from 'shared-runtime';
/**
* Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
* with the mutation hoisted to a named variable instead of being directly
* inlined into the Object key.
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* Forget:
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
*/
function Component(props) {
const key = {};
const tmp = (mutate(key), key);
const context = {
// Here, `tmp` is frozen (as it's inferred to be a primitive/string)
[tmp]: identity([props.value]),
};
mutate(key);
return [context, key];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
sequentialRenders: [{value: 42}, {value: 42}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
import { identity, mutate } from "shared-runtime";
/**
* Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
* with the mutation hoisted to a named variable instead of being directly
* inlined into the Object key.
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* Forget:
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
*/
function Component(props) {
const $ = _c(8);
let key;
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
key = {};
t0 = (mutate(key), key);
$[0] = key;
$[1] = t0;
} else {
key = $[0];
t0 = $[1];
}
const tmp = t0;
let t1;
if ($[2] !== props.value) {
t1 = identity([props.value]);
$[2] = props.value;
$[3] = t1;
} else {
t1 = $[3];
}
let t2;
if ($[4] !== t1) {
t2 = { [tmp]: t1 };
$[4] = t1;
$[5] = t2;
} else {
t2 = $[5];
}
const context = t2;
mutate(key);
let t3;
if ($[6] !== context) {
t3 = [context, key];
$[6] = context;
$[7] = t3;
} else {
t3 = $[7];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 42 }],
sequentialRenders: [{ value: 42 }, { value: 42 }],
};
```

View File

@@ -0,0 +1,32 @@
// @enableNewMutationAliasingModel:false
import {identity, mutate} from 'shared-runtime';
/**
* Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
* with the mutation hoisted to a named variable instead of being directly
* inlined into the Object key.
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* Forget:
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
*/
function Component(props) {
const key = {};
const tmp = (mutate(key), key);
const context = {
// Here, `tmp` is frozen (as it's inferred to be a primitive/string)
[tmp]: identity([props.value]),
};
mutate(key);
return [context, key];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
sequentialRenders: [{value: 42}, {value: 42}],
};

View File

@@ -1,55 +0,0 @@
## Input
```javascript
function Component(props) {
useEffect(
() => {
console.log(props.value);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
return <div />;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(4);
let t0;
if ($[0] !== props.value) {
t0 = () => {
console.log(props.value);
};
$[0] = props.value;
$[1] = t0;
} else {
t0 = $[1];
}
let t1;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t1 = [];
$[2] = t1;
} else {
t1 = $[2];
}
useEffect(t0, t1);
let t2;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t2 = <div />;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,10 +0,0 @@
function Component(props) {
useEffect(
() => {
console.log(props.value);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
return <div />;
}

View File

@@ -1,52 +0,0 @@
## Input
```javascript
function Component(props) {
useEffect(() => {
console.log(props.value);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div />;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(4);
let t0;
if ($[0] !== props.value) {
t0 = () => {
console.log(props.value);
};
$[0] = props.value;
$[1] = t0;
} else {
t0 = $[1];
}
let t1;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t1 = [];
$[2] = t1;
} else {
t1 = $[2];
}
useEffect(t0, t1);
let t2;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t2 = <div />;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,7 +0,0 @@
function Component(props) {
useEffect(() => {
console.log(props.value);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div />;
}

View File

@@ -1,77 +0,0 @@
## Input
```javascript
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
function Component(props) {
const result = useMemo(
() => makeObject(props.value).value + 1,
[props.value]
);
console.log(result);
return 'ok';
}
function makeObject(value) {
console.log(value);
return {value};
}
export const TODO_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},
],
};
```
## Code
```javascript
// @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 TODO_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 },
],
};
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,32 +0,0 @@
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
function Component(props) {
const result = useMemo(
() => makeObject(props.value).value + 1,
[props.value]
);
console.log(result);
return 'ok';
}
function makeObject(value) {
console.log(value);
return {value};
}
export const TODO_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},
],
};

View File

@@ -1,81 +0,0 @@
## Input
```javascript
// @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},
],
};
```
## Code
```javascript
// @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 },
],
};
```
### Eval output
(kind: ok) "ok"
"ok"
"ok"
"ok"
"ok"
"ok"
"ok"
"ok"
logs: [42,43,42,43,3.14,4.140000000000001,3.14,4.140000000000001,42,43,3.14,4.140000000000001,42,43,3.14,4.140000000000001]

View File

@@ -1,29 +0,0 @@
// @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},
],
};

View File

@@ -24,18 +24,9 @@ function useThing(fn) {
```
Found 1 error:
Error: `this` is not supported syntax
Error: Expected a non-reserved identifier name
React Compiler does not support compiling functions that use `this`
error.reserved-words.ts:8:28
6 |
7 | if (ref.current === null) {
> 8 | ref.current = function (this: unknown, ...args) {
| ^^^^^^^^^^^^^ `this` was used here
9 | return fnRef.current.call(this, ...args);
10 | };
11 | }
`this` is a reserved word in JavaScript and cannot be used as an identifier name.
```

View File

@@ -19,13 +19,13 @@ Found 1 error:
Error: Cannot reassign variables declared outside of the component/hook
Variable `someGlobal` is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)
Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render).
error.assign-global-in-component-tag-function.ts:3:4
1 | function Component() {
2 | const Foo = () => {
> 3 | someGlobal = true;
| ^^^^^^^^^^ `someGlobal` cannot be reassigned
| ^^^^^^^^^^ `someGlobal` should not be reassigned
4 | };
5 | return <Foo />;
6 | }

View File

@@ -22,13 +22,13 @@ Found 1 error:
Error: Cannot reassign variables declared outside of the component/hook
Variable `someGlobal` is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)
Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render).
error.assign-global-in-jsx-children.ts:3:4
1 | function Component() {
2 | const foo = () => {
> 3 | someGlobal = true;
| ^^^^^^^^^^ `someGlobal` cannot be reassigned
| ^^^^^^^^^^ `someGlobal` should not be reassigned
4 | };
5 | // Children are generally access/called during render, so
6 | // modifying a global in a children function is almost

View File

@@ -0,0 +1,35 @@
## Input
```javascript
// @enableNewMutationAliasingModel:false
function Component() {
const foo = () => {
someGlobal = true;
};
return <div {...foo} />;
}
```
## Error
```
Found 1 error:
Error: Cannot reassign variables declared outside of the component/hook
Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render).
error.assign-global-in-jsx-spread-attribute.ts:4:4
2 | function Component() {
3 | const foo = () => {
> 4 | someGlobal = true;
| ^^^^^^^^^^ Cannot reassign variables declared outside of the component/hook
5 | };
6 | return <div {...foo} />;
7 | }
```

Some files were not shown because too many files have changed in this diff Show More