mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[tests][hir-typer] Add hir-tests fixture
--- Currently, we run type inference passes early in the pipeline and do not check inference output in any tests, test fixtures, or verifier passes. In fact, the only ways to view inferred types are (1) locally add a test fixture with`@only` and inspect console logs or (2) scroll to the relevant section on a playground example. However, inferred types and effects significantly affect the output of later passes (Alias / MutableRange analysis, InferReactiveIdentifiers, etc), and we have already found some bugs due to incorrect inference (e.g. #1274). This PR add the `typer-tests` fixture with the following goals 1. Record relevant current compiler type + effect inference output. 2. Have relatively stable output (with respect to changes in HIR and PrintHIR). - we try to achieve this by annotating the source code.
This commit is contained in:
@@ -28,32 +28,10 @@ wasmFolder(
|
||||
path.join(__dirname, "..", "..", "node_modules", "@hpcc-js", "wasm", "dist")
|
||||
);
|
||||
|
||||
const Pragma_RE = /\/\/\s*@enable\((\w+)\)$/gm;
|
||||
const FlowPragmas = [/\/\/\s@flow$/gm, /\*\s@flow$/gm];
|
||||
|
||||
describe("React Forget (HIR version)", () => {
|
||||
generateTestsFromFixtures(
|
||||
path.join(__dirname, "fixtures", "compiler"),
|
||||
(input, file, options) => {
|
||||
const matches = input.matchAll(Pragma_RE);
|
||||
|
||||
for (const match of matches) {
|
||||
const [, key, value] = match;
|
||||
switch (key) {
|
||||
case "Pass":
|
||||
// do something with value;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`unknown pragma: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
let useFlow: boolean = false;
|
||||
for (const flowPragma of FlowPragmas) {
|
||||
useFlow ||= !!input.match(flowPragma);
|
||||
}
|
||||
|
||||
let language: "flow" | "typescript" = useFlow ? "flow" : "typescript";
|
||||
let items: Array<TestOutput> = [];
|
||||
let error: Error | null = null;
|
||||
if (options.debug) {
|
||||
@@ -61,7 +39,7 @@ describe("React Forget (HIR version)", () => {
|
||||
}
|
||||
try {
|
||||
items.push({
|
||||
js: runReactForgetBabelPlugin(input, file, language, {
|
||||
js: runReactForgetBabelPlugin(input, file, options.language, {
|
||||
enableOnlyOnUseForgetDirective:
|
||||
options.enableOnlyOnUseForgetDirective,
|
||||
environment: {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component(props) {
|
||||
const x = props.a + 3;
|
||||
const y = foo(x);
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## HIR
|
||||
|
||||
```javascript
|
||||
bb0 (block):
|
||||
[1] mutate $17 = LoadLocal read props$16
|
||||
[2] mutate $18:TPrimitive = PropertyLoad read $17.a
|
||||
[3] mutate $19:TPrimitive = 3
|
||||
[4] mutate $20:TPrimitive = Binary read $18:TPrimitive + read $19:TPrimitive
|
||||
[5] store $22:TPrimitive = StoreLocal Const mutate x$21:TPrimitive = read $20:TPrimitive
|
||||
[6] mutate $23:TFunction = Global foo
|
||||
[7] mutate $24:TPrimitive = LoadLocal read x$21:TPrimitive
|
||||
[8] mutate $25 = Call read $23:TFunction(read $24:TPrimitive)
|
||||
[9] store $27 = StoreLocal Const mutate y$26 = capture $25
|
||||
[10] mutate $28:TPrimitive = LoadLocal read x$21:TPrimitive
|
||||
[11] mutate $29 = LoadLocal capture y$26
|
||||
[12] store $30 = Array [read $28:TPrimitive, capture $29]
|
||||
[13] Return freeze $30
|
||||
```
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
function Component(props) {
|
||||
const x = props.a + 3;
|
||||
const y = foo(x);
|
||||
return [x, y];
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component(props) {
|
||||
const x = props.a + 3;
|
||||
const y = foo(x);
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## HIR
|
||||
|
||||
```javascript
|
||||
bb0 (block):
|
||||
[1] mutate $17 = LoadLocal read props$16
|
||||
[2] mutate $18:TPrimitive = PropertyLoad read $17.a
|
||||
[3] mutate $19:TPrimitive = 3
|
||||
[4] mutate $20:TPrimitive = Binary read $18:TPrimitive + read $19:TPrimitive
|
||||
[5] store $22:TPrimitive = StoreLocal Const mutate x$21:TPrimitive = read $20:TPrimitive
|
||||
[6] mutate $23:TFunction = Global foo
|
||||
[7] mutate $24:TPrimitive = LoadLocal read x$21:TPrimitive
|
||||
[8] mutate $25 = Call read $23:TFunction(read $24:TPrimitive)
|
||||
[9] store $27 = StoreLocal Const mutate y$26 = capture $25
|
||||
[10] mutate $28:TPrimitive = LoadLocal read x$21:TPrimitive
|
||||
[11] mutate $29 = LoadLocal capture y$26
|
||||
[12] store $30:TObject = Object { x: read $28:TPrimitive, y: capture $29 }
|
||||
[13] Return freeze $30:TObject
|
||||
```
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
function Component(props) {
|
||||
const x = props.a + 3;
|
||||
const y = foo(x);
|
||||
return { x, y };
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
import * as t from "@babel/types";
|
||||
import { parse } from "@babel/parser";
|
||||
import traverse, { NodePath } from "@babel/traverse";
|
||||
import path from "path";
|
||||
|
||||
import * as CompilerPipeline from "../CompilerPipeline";
|
||||
import { Effect, ValueKind } from "../HIR";
|
||||
import { printFunction } from "../HIR/PrintHIR";
|
||||
import { EnvironmentOptions } from "../HIR/Environment";
|
||||
import { Result, Ok, Err } from "../Utils/Result";
|
||||
import { toggleLogging } from "../Utils/logger";
|
||||
import generateTestsFromFixtures from "./test-utils/generateTestsFromFixtures";
|
||||
import invariant from "invariant";
|
||||
|
||||
// TODO: make pipeline names an enum
|
||||
// Currently, this is the last pass that operates on hir
|
||||
const LAST_HIR_PASS = "LeaveSSA";
|
||||
|
||||
describe("React Forget (HIR version)", () => {
|
||||
generateTestsFromFixtures(
|
||||
path.join(__dirname, "fixtures", "hir"),
|
||||
(input, file, options) => {
|
||||
if (options.debug) {
|
||||
toggleLogging(options.debug);
|
||||
}
|
||||
|
||||
const compileResult = compile(input, options.language, {
|
||||
customHooks: new Map([
|
||||
[
|
||||
"useFreeze",
|
||||
{
|
||||
name: "useFreeze",
|
||||
kind: "Custom",
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
},
|
||||
],
|
||||
]),
|
||||
});
|
||||
|
||||
if (compileResult.isErr()) {
|
||||
const error = compileResult.unwrapErr();
|
||||
error.message = `Expected fixture '${file}' to succeed but it failed with error:\n\n${error.message}`;
|
||||
throw error;
|
||||
}
|
||||
const transformedSources = compileResult.unwrap().map(({ hir }) => {
|
||||
return `
|
||||
## HIR
|
||||
|
||||
${wrapWithTripleBackticks(hir, "javascript")}
|
||||
|
||||
`.trim();
|
||||
});
|
||||
if (transformedSources === null || transformedSources.length === 0) {
|
||||
throw new Error(`Expected at least one output for file '${file}'.`);
|
||||
}
|
||||
return `
|
||||
## Input
|
||||
|
||||
${wrapWithTripleBackticks(input, "javascript")}
|
||||
|
||||
${transformedSources.join("\n")}
|
||||
`;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
type CompileResult = {
|
||||
hir: string;
|
||||
};
|
||||
|
||||
function compile(
|
||||
source: string,
|
||||
language: "flow" | "typescript",
|
||||
compilerEnv: Partial<EnvironmentOptions>
|
||||
): Result<Array<CompileResult>, Error> {
|
||||
const transformedFns = new Array<CompileResult>();
|
||||
const babelAsts = parseFunctions(source, language);
|
||||
if (babelAsts.isErr()) {
|
||||
return babelAsts;
|
||||
}
|
||||
|
||||
try {
|
||||
for (const ast of babelAsts.unwrap()) {
|
||||
let hirString: string | null = null;
|
||||
for (const result of CompilerPipeline.run(ast, compilerEnv)) {
|
||||
switch (result.kind) {
|
||||
case "hir": {
|
||||
if (result.name === LAST_HIR_PASS) {
|
||||
hirString = printFunction(result.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
invariant(
|
||||
hirString !== null,
|
||||
`Expected to find pass with name ${LAST_HIR_PASS}`
|
||||
);
|
||||
transformedFns.push({ hir: hirString });
|
||||
}
|
||||
return Ok(transformedFns);
|
||||
} catch (e) {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
function parseFunctions(
|
||||
source: string,
|
||||
language: "flow" | "typescript"
|
||||
): Result<Array<NodePath<t.FunctionDeclaration>>, Error> {
|
||||
const items: Array<NodePath<t.FunctionDeclaration>> = [];
|
||||
try {
|
||||
const ast = parse(source, {
|
||||
plugins: [language, "jsx"],
|
||||
sourceType: "module",
|
||||
});
|
||||
traverse(ast, {
|
||||
// Only collect top-level functions
|
||||
FunctionDeclaration(nodePath) {
|
||||
items.push(nodePath);
|
||||
nodePath.skip();
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
return Err(e);
|
||||
}
|
||||
return Ok(items);
|
||||
}
|
||||
|
||||
function wrapWithTripleBackticks(s: string, ext?: string) {
|
||||
return `\`\`\`${ext ?? ""}
|
||||
${s}
|
||||
\`\`\``;
|
||||
}
|
||||
@@ -49,6 +49,7 @@ export default function generateTestsFromFixtures(
|
||||
debug: boolean;
|
||||
enableOnlyOnUseForgetDirective: boolean;
|
||||
gatingModule: string | null;
|
||||
language: "flow" | "typescript";
|
||||
}
|
||||
) => string
|
||||
) {
|
||||
@@ -111,6 +112,7 @@ export default function generateTestsFromFixtures(
|
||||
debug,
|
||||
enableOnlyOnUseForgetDirective,
|
||||
gatingModule,
|
||||
language: parseLanguage(input),
|
||||
});
|
||||
} else {
|
||||
receivedOutput = "<<input deleted>>";
|
||||
@@ -146,6 +148,15 @@ export default function generateTestsFromFixtures(
|
||||
});
|
||||
}
|
||||
|
||||
const FlowPragmas = [/\/\/\s@flow$/gm, /\*\s@flow$/gm];
|
||||
function parseLanguage(source: string): "flow" | "typescript" {
|
||||
let useFlow: boolean = false;
|
||||
for (const flowPragma of FlowPragmas) {
|
||||
useFlow ||= !!source.match(flowPragma);
|
||||
}
|
||||
return useFlow ? "flow" : "typescript";
|
||||
}
|
||||
|
||||
function determineSnapshotMode() {
|
||||
// Determine which snapshot mode we're in: ignoring snapshots,
|
||||
// updating new files only, or updating all files
|
||||
|
||||
Reference in New Issue
Block a user