UNPKG

@rushstack/eslint-plugin

Version:

An ESLint plugin providing supplementary rules for use with the @rushstack/eslint-config package

136 lines 6.2 kB
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as hoistJestMockPatterns from './hoistJestMockPatterns'; // Jest APIs that need to be hoisted // Based on HOIST_METHODS from ts-jest const HOIST_METHODS = ['mock', 'unmock', 'enableAutomock', 'disableAutomock', 'deepUnmock']; const hoistJestMock = { defaultOptions: [], meta: { type: 'problem', messages: { 'error-unhoisted-jest-mock': "Jest's module mocking APIs must be called before regular imports. Move this call so that it precedes" + ' the import found on line {{importLine}}.' }, schema: [ { type: 'object', additionalProperties: false } ], docs: { description: 'Require Jest module mocking APIs to be called before other modules are imported.' + ' Jest module mocking APIs such as "jest.mock(\'./example\')" must be called before the associated module' + ' is imported, otherwise they will have no effect. Transpilers such as ts-jest and babel-jest automatically' + ' "hoist" these calls, however this can produce counterintuitive results. Instead, the hoist-jest-mocks' + ' lint rule requires developers to manually hoist these calls. For technical background, please read the' + ' Jest documentation here: https://jestjs.io/docs/en/es6-class-mocks', recommended: 'recommended', url: 'https://www.npmjs.com/package/@rushstack/eslint-plugin' } }, create: (context) => { // Returns true for a statement such as "jest.mock()" that needs to precede // module imports (i.e. be "hoisted"). function isHoistableJestCall(node) { if (node === undefined) { return false; } const captures = {}; if (hoistJestMockPatterns.jestCallExpression.match(node, captures)) { if (captures.methodName && HOIST_METHODS.indexOf(captures.methodName) >= 0) { return true; } } // Recurse into some common expression-combining syntaxes switch (node.type) { case AST_NODE_TYPES.CallExpression: return isHoistableJestCall(node.callee); case AST_NODE_TYPES.MemberExpression: return isHoistableJestCall(node.object); case AST_NODE_TYPES.LogicalExpression: return isHoistableJestCall(node.left) || isHoistableJestCall(node.right); } return false; } // Given part of an expression, walk upwards in the tree and find the containing statement function findOuterStatement(node) { let current = node; while (current.parent) { switch (current.parent.type) { // Statements are always found inside a block: case AST_NODE_TYPES.Program: case AST_NODE_TYPES.BlockStatement: case AST_NODE_TYPES.TSModuleBlock: return current; } current = current.parent; } return node; } // This tracks the first require() or import expression that we found in the file. let firstImportNode = undefined; // Avoid reporting more than one error for a given statement. // Example: jest.mock('a').mock('b'); const reportedStatements = new Set(); return { CallExpression: (node) => { if (firstImportNode === undefined) { // EXAMPLE: const x = require('x') if (hoistJestMockPatterns.requireCallExpression.match(node)) { firstImportNode = node; } } if (firstImportNode) { // EXAMPLE: jest.mock() if (isHoistableJestCall(node)) { const outerStatement = findOuterStatement(node); if (!reportedStatements.has(outerStatement)) { reportedStatements.add(outerStatement); context.report({ node, messageId: 'error-unhoisted-jest-mock', data: { importLine: firstImportNode.loc.start.line } }); } } } }, ImportExpression: (node) => { if (firstImportNode === undefined) { // EXAMPLE: const x = import('x'); if (hoistJestMockPatterns.importExpression.match(node)) { firstImportNode = node; } } }, ImportDeclaration: (node) => { if (firstImportNode === undefined) { // EXAMPLE: import { X } from "Y"; // IGNORE: import type { X } from "Y"; if (node.importKind !== 'type') { firstImportNode = node; } } }, ExportDeclaration: (node) => { if (firstImportNode === undefined) { // EXAMPLE: export * from "Y"; // IGNORE: export type { Y } from "Y"; if (node.exportKind !== 'type') { firstImportNode = node; } } }, TSImportEqualsDeclaration: (node) => { if (firstImportNode === undefined) { // EXAMPLE: import x = require("x"); firstImportNode = node; } } }; } }; export { hoistJestMock }; //# sourceMappingURL=hoist-jest-mock.js.map