chore: hoist functions higher if possible (#14812)

This commit is contained in:
Simen Bekkhus
2023-12-30 01:06:15 +00:00
committed by GitHub
parent 62db098719
commit 8ad6c037c9
18 changed files with 142 additions and 124 deletions
+3 -1
View File
@@ -236,6 +236,7 @@ module.exports = {
'sort-keys': 'off',
'unicorn/prefer-number-properties': 'off',
'unicorn/no-static-only-class': 'off',
'unicorn/consistent-function-scoping': 'off',
},
},
// demonstration of matchers usage
@@ -344,6 +345,7 @@ module.exports = {
],
rules: {
'@typescript-eslint/explicit-module-boundary-types': 'off',
'unicorn/consistent-function-scoping': 'off',
},
},
{
@@ -378,6 +380,7 @@ module.exports = {
],
rules: {
'import/no-extraneous-dependencies': 'off',
'unicorn/consistent-function-scoping': 'off',
},
},
{
@@ -706,7 +709,6 @@ module.exports = {
// TODO: turn on at some point
'unicorn/catch-error-name': 'off',
'unicorn/consistent-function-scoping': 'off',
'unicorn/error-message': 'off',
'unicorn/no-await-expression-member': 'off',
'unicorn/no-console-spaces': 'off',
@@ -46,7 +46,7 @@ FAIL __tests__/worksWithConcurrentMode.test.js
15 | });
16 |
at Function.failing (../../packages/jest-jasmine2/build/index.js:305:17)
at Function.failing (../../packages/jest-jasmine2/build/index.js:307:17)
at Suite.failing (__tests__/worksWithConcurrentMode.test.js:13:17)
at Object.describe (__tests__/worksWithConcurrentMode.test.js:8:1)
@@ -80,7 +80,7 @@ FAIL __tests__/worksWithConcurrentOnlyMode.test.js
15 | });
16 |
at Function.failing (../../packages/jest-jasmine2/build/index.js:305:17)
at Function.failing (../../packages/jest-jasmine2/build/index.js:307:17)
at Suite.failing (__tests__/worksWithConcurrentOnlyMode.test.js:13:22)
at Object.describe (__tests__/worksWithConcurrentOnlyMode.test.js:8:1)
+3
View File
@@ -1,5 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"emitDeclarationOnly": false,
"noEmit": true,
"experimentalDecorators": true,
"strict": true
},
+3
View File
@@ -1,5 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"emitDeclarationOnly": false,
"noEmit": true,
"strict": true
},
"include": ["./**/*"]
+4 -1
View File
@@ -1,6 +1,9 @@
{
"compilerOptions": {
"target": "ESNext",
"noEmit": true,
"module": "commonjs",
"jsx": "react-jsx"
}
},
"include": ["./**/*"]
}
+1 -1
View File
@@ -115,7 +115,7 @@
"test-types": "yarn tstyche && yarn test-ts --selectProjects type-tests",
"test": "yarn lint && yarn jest",
"typecheck": "yarn typecheck:examples && yarn typecheck:tests",
"typecheck:examples": "tsc -p examples/angular --noEmit && tsc -p examples/expect-extend --noEmit && tsc -p examples/typescript --noEmit",
"typecheck:examples": "tsc -p examples/angular && tsc -p examples/expect-extend && tsc -p examples/typescript",
"typecheck:tests": "tsc -b packages/{babel-jest,babel-plugin-jest-hoist,create-jest,diff-sequences,expect,expect-utils,jest-circus,jest-cli,jest-config,jest-console,jest-snapshot,jest-util,jest-validate,jest-watcher,jest-worker,pretty-format}/**/__tests__",
"verify-old-ts": "node ./scripts/verifyOldTs.mjs",
"verify-pnp": "node ./scripts/verifyPnP.mjs",
+46 -50
View File
@@ -313,6 +313,52 @@ const extractJestObjExprIfHoistable = (expr: NodePath): JestObjInfo | null => {
return null;
};
function visitBlock(block: NodePath<BlockStatement> | NodePath<Program>) {
// use a temporary empty statement instead of the real first statement, which may itself be hoisted
const [varsHoistPoint, callsHoistPoint] = block.unshiftContainer('body', [
emptyStatement(),
emptyStatement(),
]);
block.traverse({
CallExpression: visitCallExpr,
VariableDeclarator: visitVariableDeclarator,
// do not traverse into nested blocks, or we'll hoist calls in there out to this block
denylist: ['BlockStatement'],
});
callsHoistPoint.remove();
varsHoistPoint.remove();
function visitCallExpr(callExpr: NodePath<CallExpression>) {
if (hoistedJestGetters.has(callExpr.node)) {
const mockStmt = callExpr.getStatementParent();
if (mockStmt) {
const mockStmtParent = mockStmt.parentPath;
if (mockStmtParent.isBlock()) {
const mockStmtNode = mockStmt.node;
mockStmt.remove();
callsHoistPoint.insertBefore(mockStmtNode);
}
}
}
}
function visitVariableDeclarator(varDecl: NodePath<VariableDeclarator>) {
if (hoistedVariables.has(varDecl.node)) {
// should be assert function, but it's not. So let's cast below
varDecl.parentPath.assertVariableDeclaration();
const {kind, declarations} = varDecl.parent as VariableDeclaration;
if (declarations.length === 1) {
varDecl.parentPath.remove();
} else {
varDecl.remove();
}
varsHoistPoint.insertBefore(variableDeclaration(kind, [varDecl.node]));
}
}
}
/* eslint-disable sort-keys */
export default function jestHoist(): PluginObj<{
declareJestObjGetterIdentifier: () => Identifier;
@@ -360,56 +406,6 @@ export default function jestHoist(): PluginObj<{
post({path: program}) {
visitBlock(program);
program.traverse({BlockStatement: visitBlock});
function visitBlock(block: NodePath<BlockStatement> | NodePath<Program>) {
// use a temporary empty statement instead of the real first statement, which may itself be hoisted
const [varsHoistPoint, callsHoistPoint] = block.unshiftContainer(
'body',
[emptyStatement(), emptyStatement()],
);
block.traverse({
CallExpression: visitCallExpr,
VariableDeclarator: visitVariableDeclarator,
// do not traverse into nested blocks, or we'll hoist calls in there out to this block
denylist: ['BlockStatement'],
});
callsHoistPoint.remove();
varsHoistPoint.remove();
function visitCallExpr(callExpr: NodePath<CallExpression>) {
if (hoistedJestGetters.has(callExpr.node)) {
const mockStmt = callExpr.getStatementParent();
if (mockStmt) {
const mockStmtParent = mockStmt.parentPath;
if (mockStmtParent.isBlock()) {
const mockStmtNode = mockStmt.node;
mockStmt.remove();
callsHoistPoint.insertBefore(mockStmtNode);
}
}
}
}
function visitVariableDeclarator(
varDecl: NodePath<VariableDeclarator>,
) {
if (hoistedVariables.has(varDecl.node)) {
// should be assert function, but it's not. So let's cast below
varDecl.parentPath.assertVariableDeclaration();
const {kind, declarations} = varDecl.parent as VariableDeclaration;
if (declarations.length === 1) {
varDecl.parentPath.remove();
} else {
varDecl.remove();
}
varsHoistPoint.insertBefore(
variableDeclaration(kind, [varDecl.node]),
);
}
}
}
},
};
}
+2
View File
@@ -5,6 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
/* eslint-disable unicorn/consistent-function-scoping */
import {equals, iterableEquality} from '@jest/expect-utils';
import {getType, isPrimitive} from 'jest-get-type';
import {
+6 -6
View File
@@ -426,6 +426,12 @@ const buildTestPathPatterns = (
return testPathPatterns;
};
function printConfig(opts: Array<string>) {
const string = opts.map(ext => `'${ext}'`).join(', ');
return chalk.bold(`extensionsToTreatAsEsm: [${string}]`);
}
function validateExtensionsToTreatAsEsm(
extensionsToTreatAsEsm: Config.InitialOptions['extensionsToTreatAsEsm'],
) {
@@ -433,12 +439,6 @@ function validateExtensionsToTreatAsEsm(
return;
}
function printConfig(opts: Array<string>) {
const string = opts.map(ext => `'${ext}'`).join(', ');
return chalk.bold(`extensionsToTreatAsEsm: [${string}]`);
}
const extensionWithoutDot = extensionsToTreatAsEsm.some(
ext => !ext.startsWith('.'),
);
+4 -4
View File
@@ -54,6 +54,10 @@ const hasSCM = (changedFilesInfo: ChangedFiles) => {
return !noSCM;
};
function normalizePosix(filePath: string) {
return filePath.replace(/\\/g, '/');
}
export default class SearchSource {
private readonly _context: TestContext;
private _dependencyResolver: DependencyResolver | null;
@@ -299,10 +303,6 @@ export default class SearchSource {
const allFiles = this._context.hasteFS.getAllFiles();
const options = {nocase: true, windows: false};
function normalizePosix(filePath: string) {
return filePath.replace(/\\/g, '/');
}
paths = paths
.map(p => {
// micromatch works with forward slashes: https://github.com/micromatch/micromatch#backslashes
@@ -22,14 +22,14 @@ export default function getConfigsOfProjectsToRun(
});
}
const always = () => true;
function createProjectFilter(opts: {
ignoreProjects: Array<string> | undefined;
selectProjects: Array<string> | undefined;
}) {
const {selectProjects, ignoreProjects} = opts;
const always = () => true;
const selected = selectProjects
? (name: string | undefined) => name && selectProjects.includes(name)
: always;
+12 -12
View File
@@ -60,6 +60,18 @@ function isString(value: unknown): value is string {
return typeof value === 'string';
}
const timerIdToRef = (id: number) => ({
id,
ref() {
return this;
},
unref() {
return this;
},
});
const timerRefToId = (timer: Timer): number | undefined => timer?.id;
export default class NodeEnvironment implements JestEnvironment<Timer> {
context: Context | null;
fakeTimers: LegacyFakeTimers<Timer> | null;
@@ -159,18 +171,6 @@ export default class NodeEnvironment implements JestEnvironment<Timer> {
this.moduleMocker = new ModuleMocker(global);
const timerIdToRef = (id: number) => ({
id,
ref() {
return this;
},
unref() {
return this;
},
});
const timerRefToId = (timer: Timer): number | undefined => timer?.id;
this.fakeTimers = new LegacyFakeTimers({
config: projectConfig,
global,
@@ -431,11 +431,16 @@ export default class FakeTimers<TimerRef = unknown> {
}
}
private _createMocks() {
const fn = <T extends FunctionLike = UnknownFunction>(implementation?: T) =>
this._moduleMocker.fn(implementation);
#createMockFunction<T extends FunctionLike = UnknownFunction>(
implementation: T,
) {
return this._moduleMocker.fn(implementation.bind(this));
}
const promisifiableFakeSetTimeout = fn(this._fakeSetTimeout.bind(this));
private _createMocks() {
const promisifiableFakeSetTimeout = this.#createMockFunction(
this._fakeSetTimeout,
);
// @ts-expect-error: no index
promisifiableFakeSetTimeout[promisify.custom] = (
delay?: number,
@@ -444,14 +449,16 @@ export default class FakeTimers<TimerRef = unknown> {
new Promise(resolve => promisifiableFakeSetTimeout(resolve, delay, arg));
this._fakeTimerAPIs = {
cancelAnimationFrame: fn(this._fakeClearTimer.bind(this)),
clearImmediate: fn(this._fakeClearImmediate.bind(this)),
clearInterval: fn(this._fakeClearTimer.bind(this)),
clearTimeout: fn(this._fakeClearTimer.bind(this)),
nextTick: fn(this._fakeNextTick.bind(this)),
requestAnimationFrame: fn(this._fakeRequestAnimationFrame.bind(this)),
setImmediate: fn(this._fakeSetImmediate.bind(this)),
setInterval: fn(this._fakeSetInterval.bind(this)),
cancelAnimationFrame: this.#createMockFunction(this._fakeClearTimer),
clearImmediate: this.#createMockFunction(this._fakeClearImmediate),
clearInterval: this.#createMockFunction(this._fakeClearTimer),
clearTimeout: this.#createMockFunction(this._fakeClearTimer),
nextTick: this.#createMockFunction(this._fakeNextTick),
requestAnimationFrame: this.#createMockFunction(
this._fakeRequestAnimationFrame,
),
setImmediate: this.#createMockFunction(this._fakeSetImmediate),
setInterval: this.#createMockFunction(this._fakeSetInterval),
setTimeout: promisifiableFakeSetTimeout,
};
}
@@ -230,6 +230,7 @@ function makeConcurrent(
return spec;
};
// eslint-disable-next-line unicorn/consistent-function-scoping
const failing = () => {
throw new Error(
'Jest: `failing` tests are only supported in `jest-circus`.',
@@ -242,7 +243,7 @@ function makeConcurrent(
);
};
// each is bound after the function is made concurrent, so for now it is made noop
// eslint-disable-next-line @typescript-eslint/no-empty-function
// eslint-disable-next-line @typescript-eslint/no-empty-function,unicorn/consistent-function-scoping
concurrentFn.each = () => () => {};
concurrentFn.failing = failing;
+14 -14
View File
@@ -28,6 +28,20 @@ export type TreeNode = {
// eslint-disable-next-line @typescript-eslint/no-empty-function
const noop = () => {};
function getNodeWithoutChildrenHandler(node: TreeNode, enabled: boolean) {
return function fn(done: (error?: unknown) => void = noop) {
node.execute(done, enabled);
};
}
function hasNoEnabledTest(node: TreeNode): boolean {
return (
node.disabled ||
node.markedPending ||
(node.children?.every(hasNoEnabledTest) ?? false)
);
}
export default function treeProcessor(options: Options): void {
const {nodeComplete, nodeStart, queueRunnerFactory, runnableIds, tree} =
options;
@@ -43,12 +57,6 @@ export default function treeProcessor(options: Options): void {
: getNodeWithoutChildrenHandler(node, enabled);
}
function getNodeWithoutChildrenHandler(node: TreeNode, enabled: boolean) {
return function fn(done: (error?: unknown) => void = noop) {
node.execute(done, enabled);
};
}
function getNodeWithChildrenHandler(node: TreeNode, enabled: boolean) {
return async function fn(done: (error?: unknown) => void = noop) {
nodeStart(node);
@@ -62,14 +70,6 @@ export default function treeProcessor(options: Options): void {
};
}
function hasNoEnabledTest(node: TreeNode): boolean {
return (
node.disabled ||
node.markedPending ||
(node.children?.every(hasNoEnabledTest) ?? false)
);
}
function wrapChildren(node: TreeNode, enabled: boolean) {
if (!node.children) {
throw new Error('`node.children` is not defined.');
+9 -8
View File
@@ -72,13 +72,6 @@ export default class TestRunner extends EmittingTestRunner {
throw new CancelRun();
}
// `deepCyclicCopy` used here to avoid mem-leak
const sendMessageToJest: TestFileEvent = (eventName, args) =>
this.#eventEmitter.emit(
eventName,
deepCyclicCopy(args, {keepPrototype: false}),
);
await this.#eventEmitter.emit('test-file-start', [test]);
return runTest(
@@ -87,7 +80,7 @@ export default class TestRunner extends EmittingTestRunner {
test.context.config,
test.context.resolver,
this._context,
sendMessageToJest,
this.#sendMessageToJest,
);
})
.then(
@@ -208,6 +201,14 @@ export default class TestRunner extends EmittingTestRunner {
): UnsubscribeFn {
return this.#eventEmitter.on(eventName, listener);
}
#sendMessageToJest: TestFileEvent = async (eventName, args) => {
await this.#eventEmitter.emit(
eventName,
// `deepCyclicCopy` used here to avoid mem-leak
deepCyclicCopy(args, {keepPrototype: false}),
);
};
}
class CancelRun extends Error {
+3 -3
View File
@@ -1819,6 +1819,7 @@ export default class Runtime {
return this._moduleImplementation;
}
// eslint-disable-next-line unicorn/consistent-function-scoping
const createRequire = (modulePath: string | URL) => {
const filename =
typeof modulePath === 'string'
@@ -2195,6 +2196,7 @@ export default class Runtime {
this.restoreAllMocks();
return jestObject;
};
// eslint-disable-next-line unicorn/consistent-function-scoping
const _getFakeTimers = () => {
if (
this.isTornDown ||
@@ -2238,9 +2240,7 @@ export default class Runtime {
this.isolateModules(fn);
return jestObject;
};
const isolateModulesAsync = (fn: () => Promise<void>): Promise<void> => {
return this.isolateModulesAsync(fn);
};
const isolateModulesAsync = this.isolateModulesAsync.bind(this);
const fn = this._moduleMocker.fn.bind(this._moduleMocker);
const spyOn = this._moduleMocker.spyOn.bind(this._moduleMocker);
const mocked =
@@ -5,6 +5,13 @@
* LICENSE file in the root directory of this source tree.
*/
function cardTransform(offset, handWidth) {
const transform = `rotate(${offset * 4}deg) translateX(${
(offset - (Math.abs(offset) * offset) / 7) * Math.min(140, handWidth / 8)
}px)`;
return transform;
}
// Docusaurus v1 animation, reworked a bit for the Docusaurus v2 migration
// TODO maybe we can use React code instead of Vanilla JS now?
export function setupLandingAnimation() {
@@ -17,13 +24,6 @@ export function setupLandingAnimation() {
const hand = document.querySelector('.jest-hand');
const cards = hand.querySelectorAll('.jest-card');
function cardTransform(offset, handWidth) {
const transform = `rotate(${offset * 4}deg) translateX(${
(offset - (Math.abs(offset) * offset) / 7) * Math.min(140, handWidth / 8)
}px)`;
return transform;
}
function positionCards() {
const handWidth = hand.offsetWidth;
for (const card of cards) {