fix: prevent asset conflicts between React and Grid.js versions

Add coexistence checks to all enqueue methods to prevent loading
both React and Grid.js assets simultaneously.

Changes:
- ReactAdmin.php: Only enqueue React assets when ?react=1
- Init.php: Skip Grid.js when React active on admin pages
- Form.php, Coupon.php, Access.php: Restore classic assets when ?react=0
- Customer.php, Product.php, License.php: Add coexistence checks

Now the toggle between Classic and React versions works correctly.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dwindown
2026-04-18 17:02:14 +07:00
parent bd9cdac02e
commit e8fbfb14c1
74973 changed files with 6658406 additions and 71 deletions

15
node_modules/eslint-plugin-jest/lib/globals.json generated vendored Normal file
View File

@@ -0,0 +1,15 @@
{
"afterAll": false,
"afterEach": false,
"beforeAll": false,
"beforeEach": false,
"describe": false,
"expect": false,
"fit": false,
"it": false,
"jest": false,
"test": false,
"xdescribe": false,
"xit": false,
"xtest": false
}

87
node_modules/eslint-plugin-jest/lib/index.js generated vendored Normal file
View File

@@ -0,0 +1,87 @@
"use strict";
var _fs = require("fs");
var _path = require("path");
var _package = require("../package.json");
var _globals = _interopRequireDefault(require("./globals.json"));
var snapshotProcessor = _interopRequireWildcard(require("./processors/snapshot-processor"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
// v5 of `@typescript-eslint/experimental-utils` removed this
// copied from https://github.com/babel/babel/blob/d8da63c929f2d28c401571e2a43166678c555bc4/packages/babel-helpers/src/helpers.js#L602-L606
/* istanbul ignore next */
const interopRequireDefault = obj => obj && obj.__esModule ? obj : {
default: obj
};
const importDefault = moduleName =>
// eslint-disable-next-line @typescript-eslint/no-require-imports
interopRequireDefault(require(moduleName)).default;
const rulesDir = (0, _path.join)(__dirname, 'rules');
const excludedFiles = ['__tests__', 'detectJestVersion', 'utils'];
const rules = Object.fromEntries((0, _fs.readdirSync)(rulesDir).map(rule => (0, _path.parse)(rule).name).filter(rule => !excludedFiles.includes(rule)).map(rule => [rule, importDefault((0, _path.join)(rulesDir, rule))]));
const recommendedRules = Object.fromEntries(Object.entries(rules).filter(([, rule]) => rule.meta.docs.recommended).map(([name, rule]) => [`jest/${name}`, rule.meta.docs.recommended]));
const allRules = Object.fromEntries(Object.entries(rules).filter(([, rule]) => !rule.meta.deprecated).map(([name]) => [`jest/${name}`, 'error']));
const plugin = {
meta: {
name: _package.name,
version: _package.version
},
// ugly cast for now to keep TypeScript happy since
// we don't have types for flat config yet
configs: {},
environments: {
globals: {
globals: _globals.default
}
},
processors: {
snapshots: snapshotProcessor,
'.snap': snapshotProcessor
},
rules
};
const createRCConfig = rules => ({
plugins: ['jest'],
env: {
'jest/globals': true
},
rules
});
const createFlatConfig = rules => ({
plugins: {
jest: plugin
},
languageOptions: {
globals: _globals.default
},
rules
});
plugin.configs = {
all: createRCConfig(allRules),
recommended: createRCConfig(recommendedRules),
style: createRCConfig({
'jest/no-alias-methods': 'warn',
'jest/prefer-to-be': 'error',
'jest/prefer-to-contain': 'error',
'jest/prefer-to-have-length': 'error'
}),
'flat/all': createFlatConfig(allRules),
'flat/recommended': createFlatConfig(recommendedRules),
'flat/style': createFlatConfig({
'jest/no-alias-methods': 'warn',
'jest/prefer-to-be': 'error',
'jest/prefer-to-contain': 'error',
'jest/prefer-to-have-length': 'error'
}),
'flat/snapshots': {
// @ts-expect-error this is introduced in flat config
files: ['**/*.snap'],
plugins: {
jest: plugin
},
processor: 'jest/snapshots'
}
};
module.exports = plugin;

View File

@@ -0,0 +1,20 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.preprocess = exports.postprocess = exports.meta = void 0;
var _package = require("../../package.json");
// https://eslint.org/docs/developer-guide/working-with-plugins#processors-in-plugins
// https://github.com/typescript-eslint/typescript-eslint/issues/808
const meta = exports.meta = {
name: _package.name,
version: _package.version
};
const preprocess = source => [source];
exports.preprocess = preprocess;
const postprocess = messages =>
// snapshot files should only be linted with snapshot specific rules
messages[0].filter(message => message.ruleId === 'jest/no-large-snapshots');
exports.postprocess = postprocess;

View File

@@ -0,0 +1,101 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
const buildFixer = (callee, nodeName, preferredTestKeyword) => fixer => [fixer.replaceText(callee.type === _utils.AST_NODE_TYPES.MemberExpression ? callee.object : callee, getPreferredNodeName(nodeName, preferredTestKeyword))];
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Enforce `test` and `it` usage conventions',
recommended: false
},
fixable: 'code',
messages: {
consistentMethod: "Prefer using '{{ testKeyword }}' instead of '{{ oppositeTestKeyword }}'",
consistentMethodWithinDescribe: "Prefer using '{{ testKeywordWithinDescribe }}' instead of '{{ oppositeTestKeyword }}' within describe"
},
schema: [{
type: 'object',
properties: {
fn: {
enum: [_utils2.TestCaseName.it, _utils2.TestCaseName.test]
},
withinDescribe: {
enum: [_utils2.TestCaseName.it, _utils2.TestCaseName.test]
}
},
additionalProperties: false
}],
type: 'suggestion'
},
defaultOptions: [{
fn: _utils2.TestCaseName.test,
withinDescribe: _utils2.TestCaseName.it
}],
create(context) {
const configObj = context.options[0] || {};
const testKeyword = configObj.fn || _utils2.TestCaseName.test;
const testKeywordWithinDescribe = configObj.withinDescribe || configObj.fn || _utils2.TestCaseName.it;
let describeNestingLevel = 0;
return {
CallExpression(node) {
const jestFnCall = (0, _utils2.parseJestFnCall)(node, context);
if (!jestFnCall) {
return;
}
if (jestFnCall.type === 'describe') {
describeNestingLevel++;
return;
}
const funcNode = node.callee.type === _utils.AST_NODE_TYPES.TaggedTemplateExpression ? node.callee.tag : node.callee.type === _utils.AST_NODE_TYPES.CallExpression ? node.callee.callee : node.callee;
if (jestFnCall.type === 'test' && describeNestingLevel === 0 && !jestFnCall.name.endsWith(testKeyword)) {
const oppositeTestKeyword = getOppositeTestKeyword(testKeyword);
context.report({
messageId: 'consistentMethod',
node: node.callee,
data: {
testKeyword,
oppositeTestKeyword
},
fix: buildFixer(funcNode, jestFnCall.name, testKeyword)
});
}
if (jestFnCall.type === 'test' && describeNestingLevel > 0 && !jestFnCall.name.endsWith(testKeywordWithinDescribe)) {
const oppositeTestKeyword = getOppositeTestKeyword(testKeywordWithinDescribe);
context.report({
messageId: 'consistentMethodWithinDescribe',
node: node.callee,
data: {
testKeywordWithinDescribe,
oppositeTestKeyword
},
fix: buildFixer(funcNode, jestFnCall.name, testKeywordWithinDescribe)
});
}
},
'CallExpression:exit'(node) {
if ((0, _utils2.isTypeOfJestFnCall)(node, context, ['describe'])) {
describeNestingLevel--;
}
}
};
}
});
function getPreferredNodeName(nodeName, preferredTestKeyword) {
if (nodeName === _utils2.TestCaseName.fit) {
return 'test.only';
}
return nodeName.startsWith('f') || nodeName.startsWith('x') ? nodeName.charAt(0) + preferredTestKeyword : preferredTestKeyword;
}
function getOppositeTestKeyword(test) {
if (test === _utils2.TestCaseName.test) {
return _utils2.TestCaseName.it;
}
return _utils2.TestCaseName.test;
}

View File

@@ -0,0 +1,104 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
/*
* This implementation is adapted from eslint-plugin-jasmine.
* MIT license, Remco Haszing.
*/
/**
* Checks if node names returned by getNodeName matches any of the given star patterns
* Pattern examples:
* request.*.expect
* request.**.expect
* request.**.expect*
*/
function matchesAssertFunctionName(nodeName, patterns) {
return patterns.some(p => new RegExp(`^${p.split('.').map(x => {
if (x === '**') {
return '[a-z\\d\\.]*';
}
return x.replace(/\*/gu, '[a-z\\d]*');
}).join('\\.')}(\\.|$)`, 'ui').test(nodeName));
}
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Enforce assertion to be made in a test body',
recommended: 'warn'
},
messages: {
noAssertions: 'Test has no assertions'
},
schema: [{
type: 'object',
properties: {
assertFunctionNames: {
type: 'array',
items: [{
type: 'string'
}]
},
additionalTestBlockFunctions: {
type: 'array',
items: {
type: 'string'
}
}
},
additionalProperties: false
}],
type: 'suggestion'
},
defaultOptions: [{
assertFunctionNames: ['expect'],
additionalTestBlockFunctions: []
}],
create(context, [{
assertFunctionNames = ['expect'],
additionalTestBlockFunctions = []
}]) {
const unchecked = [];
function checkCallExpressionUsed(nodes) {
for (const node of nodes) {
const index = node.type === _utils.AST_NODE_TYPES.CallExpression ? unchecked.indexOf(node) : -1;
if (node.type === _utils.AST_NODE_TYPES.FunctionDeclaration) {
const declaredVariables = (0, _utils2.getDeclaredVariables)(context, node);
const testCallExpressions = (0, _utils2.getTestCallExpressionsFromDeclaredVariables)(declaredVariables, context);
checkCallExpressionUsed(testCallExpressions);
}
if (index !== -1) {
unchecked.splice(index, 1);
break;
}
}
}
return {
CallExpression(node) {
const name = (0, _utils2.getNodeName)(node.callee) ?? '';
if ((0, _utils2.isTypeOfJestFnCall)(node, context, ['test']) || additionalTestBlockFunctions.includes(name)) {
if (node.callee.type === _utils.AST_NODE_TYPES.MemberExpression && (0, _utils2.isSupportedAccessor)(node.callee.property, 'todo')) {
return;
}
unchecked.push(node);
} else if (matchesAssertFunctionName(name, assertFunctionNames)) {
// Return early in case of nested `it` statements.
checkCallExpressionUsed((0, _utils2.getAncestors)(context, node));
}
},
'Program:exit'() {
unchecked.forEach(node => context.report({
messageId: 'noAssertions',
node: node.callee
}));
}
};
}
});

View File

@@ -0,0 +1,71 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Enforces a maximum number assertion calls in a test body',
recommended: false
},
messages: {
exceededMaxAssertion: 'Too many assertion calls ({{ count }}) - maximum allowed is {{ max }}'
},
type: 'suggestion',
schema: [{
type: 'object',
properties: {
max: {
type: 'integer',
minimum: 1
}
},
additionalProperties: false
}]
},
defaultOptions: [{
max: 5
}],
create(context, [{
max
}]) {
let count = 0;
const maybeResetCount = node => {
var _node$parent;
const isTestFn = ((_node$parent = node.parent) === null || _node$parent === void 0 ? void 0 : _node$parent.type) !== _utils.AST_NODE_TYPES.CallExpression || (0, _utils2.isTypeOfJestFnCall)(node.parent, context, ['test']);
if (isTestFn) {
count = 0;
}
};
return {
FunctionExpression: maybeResetCount,
'FunctionExpression:exit': maybeResetCount,
ArrowFunctionExpression: maybeResetCount,
'ArrowFunctionExpression:exit': maybeResetCount,
CallExpression(node) {
var _jestFnCall$head$node;
const jestFnCall = (0, _utils2.parseJestFnCall)(node, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'expect' || ((_jestFnCall$head$node = jestFnCall.head.node.parent) === null || _jestFnCall$head$node === void 0 ? void 0 : _jestFnCall$head$node.type) === _utils.AST_NODE_TYPES.MemberExpression) {
return;
}
count += 1;
if (count > max) {
context.report({
node,
messageId: 'exceededMaxAssertion',
data: {
count,
max
}
});
}
}
};
}
});

View File

@@ -0,0 +1,61 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("./utils");
var _default = exports.default = (0, _utils.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Enforces a maximum depth to nested describe calls',
recommended: false
},
messages: {
exceededMaxDepth: 'Too many nested describe calls ({{ depth }}) - maximum allowed is {{ max }}'
},
type: 'suggestion',
schema: [{
type: 'object',
properties: {
max: {
type: 'integer',
minimum: 0
}
},
additionalProperties: false
}]
},
defaultOptions: [{
max: 5
}],
create(context, [{
max
}]) {
const describes = [];
return {
CallExpression(node) {
if ((0, _utils.isTypeOfJestFnCall)(node, context, ['describe'])) {
describes.unshift(node);
if (describes.length > max) {
context.report({
node,
messageId: 'exceededMaxDepth',
data: {
depth: describes.length,
max
}
});
}
}
},
'CallExpression:exit'(node) {
if (describes[0] === node) {
describes.shift();
}
}
};
}
});

View File

@@ -0,0 +1,64 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("./utils");
var _default = exports.default = (0, _utils.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Disallow alias methods',
recommended: 'error'
},
messages: {
replaceAlias: `Replace {{ alias }}() with its canonical name of {{ canonical }}()`
},
fixable: 'code',
type: 'suggestion',
schema: []
},
defaultOptions: [],
create(context) {
// map of jest matcher aliases & their canonical names
const methodNames = {
toBeCalled: 'toHaveBeenCalled',
toBeCalledTimes: 'toHaveBeenCalledTimes',
toBeCalledWith: 'toHaveBeenCalledWith',
lastCalledWith: 'toHaveBeenLastCalledWith',
nthCalledWith: 'toHaveBeenNthCalledWith',
toReturn: 'toHaveReturned',
toReturnTimes: 'toHaveReturnedTimes',
toReturnWith: 'toHaveReturnedWith',
lastReturnedWith: 'toHaveLastReturnedWith',
nthReturnedWith: 'toHaveNthReturnedWith',
toThrowError: 'toThrow'
};
return {
CallExpression(node) {
const jestFnCall = (0, _utils.parseJestFnCall)(node, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'expect') {
return;
}
const {
matcher
} = jestFnCall;
const alias = (0, _utils.getAccessorValue)(matcher);
if (alias in methodNames) {
const canonical = methodNames[alias];
context.report({
messageId: 'replaceAlias',
data: {
alias,
canonical
},
node: matcher,
fix: fixer => [(0, _utils.replaceAccessorFixer)(fixer, matcher, canonical)]
});
}
}
};
}
});

View File

@@ -0,0 +1,44 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("./utils");
function hasTests(node) {
return /^\s*[xf]?(test|it|describe)(\.\w+|\[['"]\w+['"]\])?\s*\(/mu.test(node.value);
}
var _default = exports.default = (0, _utils.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Disallow commented out tests',
recommended: 'warn'
},
messages: {
commentedTests: 'Some tests seem to be commented'
},
schema: [],
type: 'suggestion'
},
defaultOptions: [],
create(context) {
const sourceCode = (0, _utils.getSourceCode)(context);
function checkNode(node) {
if (!hasTests(node)) {
return;
}
context.report({
messageId: 'commentedTests',
node
});
}
return {
Program() {
const comments = sourceCode.getAllComments();
comments.forEach(checkNode);
}
};
}
});

View File

@@ -0,0 +1,82 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
const isCatchCall = node => node.callee.type === _utils.AST_NODE_TYPES.MemberExpression && (0, _utils2.isSupportedAccessor)(node.callee.property, 'catch');
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
description: 'Disallow calling `expect` conditionally',
category: 'Best Practices',
recommended: 'error'
},
messages: {
conditionalExpect: 'Avoid calling `expect` conditionally`'
},
type: 'problem',
schema: []
},
defaultOptions: [],
create(context) {
let conditionalDepth = 0;
let inTestCase = false;
let inPromiseCatch = false;
const increaseConditionalDepth = () => inTestCase && conditionalDepth++;
const decreaseConditionalDepth = () => inTestCase && conditionalDepth--;
return {
FunctionDeclaration(node) {
const declaredVariables = (0, _utils2.getDeclaredVariables)(context, node);
const testCallExpressions = (0, _utils2.getTestCallExpressionsFromDeclaredVariables)(declaredVariables, context);
if (testCallExpressions.length > 0) {
inTestCase = true;
}
},
CallExpression(node) {
const {
type: jestFnCallType
} = (0, _utils2.parseJestFnCall)(node, context) ?? {};
if (jestFnCallType === 'test') {
inTestCase = true;
}
if (isCatchCall(node)) {
inPromiseCatch = true;
}
if (inTestCase && jestFnCallType === 'expect' && conditionalDepth > 0) {
context.report({
messageId: 'conditionalExpect',
node
});
}
if (inPromiseCatch && jestFnCallType === 'expect') {
context.report({
messageId: 'conditionalExpect',
node
});
}
},
'CallExpression:exit'(node) {
if ((0, _utils2.isTypeOfJestFnCall)(node, context, ['test'])) {
inTestCase = false;
}
if (isCatchCall(node)) {
inPromiseCatch = false;
}
},
CatchClause: increaseConditionalDepth,
'CatchClause:exit': decreaseConditionalDepth,
IfStatement: increaseConditionalDepth,
'IfStatement:exit': decreaseConditionalDepth,
SwitchStatement: increaseConditionalDepth,
'SwitchStatement:exit': decreaseConditionalDepth,
ConditionalExpression: increaseConditionalDepth,
'ConditionalExpression:exit': decreaseConditionalDepth,
LogicalExpression: increaseConditionalDepth,
'LogicalExpression:exit': decreaseConditionalDepth
};
}
});

View File

@@ -0,0 +1,50 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("./utils");
var _default = exports.default = (0, _utils.createRule)({
name: __filename,
meta: {
docs: {
description: 'Disallow conditional logic in tests',
category: 'Best Practices',
recommended: false
},
messages: {
conditionalInTest: 'Avoid having conditionals in tests'
},
type: 'problem',
schema: []
},
defaultOptions: [],
create(context) {
let inTestCase = false;
const maybeReportConditional = node => {
if (inTestCase) {
context.report({
messageId: 'conditionalInTest',
node
});
}
};
return {
CallExpression(node) {
if ((0, _utils.isTypeOfJestFnCall)(node, context, ['test'])) {
inTestCase = true;
}
},
'CallExpression:exit'(node) {
if ((0, _utils.isTypeOfJestFnCall)(node, context, ['test'])) {
inTestCase = false;
}
},
IfStatement: maybeReportConditional,
SwitchStatement: maybeReportConditional,
ConditionalExpression: maybeReportConditional,
LogicalExpression: maybeReportConditional
};
}
});

View File

@@ -0,0 +1,63 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("./utils");
function isJestSetTimeout(jestFnCall) {
return jestFnCall.type === 'jest' && jestFnCall.members.length === 1 && (0, _utils.isIdentifier)(jestFnCall.members[0], 'setTimeout');
}
var _default = exports.default = (0, _utils.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Disallow confusing usages of jest.setTimeout',
recommended: false
},
messages: {
globalSetTimeout: '`jest.setTimeout` should be call in `global` scope',
multipleSetTimeouts: 'Do not call `jest.setTimeout` multiple times, as only the last call will have an effect',
orderSetTimeout: '`jest.setTimeout` should be placed before any other jest methods'
},
type: 'problem',
schema: []
},
defaultOptions: [],
create(context) {
let seenJestTimeout = false;
let shouldEmitOrderSetTimeout = false;
return {
CallExpression(node) {
const jestFnCall = (0, _utils.parseJestFnCall)(node, context);
if (!jestFnCall) {
return;
}
if (!isJestSetTimeout(jestFnCall)) {
shouldEmitOrderSetTimeout = true;
return;
}
if (!['global', 'module'].includes((0, _utils.getScope)(context, node).type)) {
context.report({
messageId: 'globalSetTimeout',
node
});
}
if (shouldEmitOrderSetTimeout) {
context.report({
messageId: 'orderSetTimeout',
node
});
}
if (seenJestTimeout) {
context.report({
messageId: 'multipleSetTimeouts',
node
});
}
seenJestTimeout = true;
}
};
}
});

View File

@@ -0,0 +1,84 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
const parseJestVersion = rawVersion => {
if (typeof rawVersion === 'number') {
return rawVersion;
}
const [majorVersion] = rawVersion.split('.');
return parseInt(majorVersion, 10);
};
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Disallow use of deprecated functions',
recommended: 'error'
},
messages: {
deprecatedFunction: '`{{ deprecation }}` has been deprecated in favor of `{{ replacement }}`'
},
type: 'suggestion',
schema: [],
fixable: 'code'
},
defaultOptions: [],
create(context) {
var _context$settings;
const jestVersion = parseJestVersion(((_context$settings = context.settings) === null || _context$settings === void 0 || (_context$settings = _context$settings.jest) === null || _context$settings === void 0 ? void 0 : _context$settings.version) || (0, _utils2.detectJestVersion)());
const deprecations = {
...(jestVersion >= 15 && {
'jest.resetModuleRegistry': 'jest.resetModules'
}),
...(jestVersion >= 17 && {
'jest.addMatchers': 'expect.extend'
}),
...(jestVersion >= 21 && {
'require.requireMock': 'jest.requireMock',
'require.requireActual': 'jest.requireActual'
}),
...(jestVersion >= 22 && {
'jest.runTimersToTime': 'jest.advanceTimersByTime'
}),
...(jestVersion >= 26 && {
'jest.genMockFromModule': 'jest.createMockFromModule'
})
};
return {
CallExpression(node) {
if (node.callee.type !== _utils.AST_NODE_TYPES.MemberExpression) {
return;
}
const deprecation = (0, _utils2.getNodeName)(node);
if (!deprecation || !(deprecation in deprecations)) {
return;
}
const replacement = deprecations[deprecation];
const {
callee
} = node;
context.report({
messageId: 'deprecatedFunction',
data: {
deprecation,
replacement
},
node,
fix(fixer) {
let [name, func] = replacement.split('.');
if (callee.property.type === _utils.AST_NODE_TYPES.Literal) {
func = `'${func}'`;
}
return [fixer.replaceText(callee.object, name), fixer.replaceText(callee.property, func)];
}
});
}
};
}
});

View File

@@ -0,0 +1,93 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("./utils");
var _default = exports.default = (0, _utils.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Disallow disabled tests',
recommended: 'warn'
},
messages: {
missingFunction: 'Test is missing function argument',
pending: 'Call to pending()',
pendingSuite: 'Call to pending() within test suite',
pendingTest: 'Call to pending() within test',
disabledSuite: 'Disabled test suite',
disabledTest: 'Disabled test'
},
schema: [],
type: 'suggestion'
},
defaultOptions: [],
create(context) {
let suiteDepth = 0;
let testDepth = 0;
return {
CallExpression(node) {
const jestFnCall = (0, _utils.parseJestFnCall)(node, context);
if (!jestFnCall) {
return;
}
if (jestFnCall.type === 'describe') {
suiteDepth++;
}
if (jestFnCall.type === 'test') {
testDepth++;
if (node.arguments.length < 2 && jestFnCall.members.every(s => (0, _utils.getAccessorValue)(s) !== 'todo')) {
context.report({
messageId: 'missingFunction',
node
});
}
}
if (
// the only jest functions that are with "x" are "xdescribe", "xtest", and "xit"
jestFnCall.name.startsWith('x') || jestFnCall.members.some(s => (0, _utils.getAccessorValue)(s) === 'skip')) {
context.report({
messageId: jestFnCall.type === 'describe' ? 'disabledSuite' : 'disabledTest',
node
});
}
},
'CallExpression:exit'(node) {
const jestFnCall = (0, _utils.parseJestFnCall)(node, context);
if (!jestFnCall) {
return;
}
if (jestFnCall.type === 'describe') {
suiteDepth--;
}
if (jestFnCall.type === 'test') {
testDepth--;
}
},
'CallExpression[callee.name="pending"]'(node) {
if ((0, _utils.resolveScope)((0, _utils.getScope)(context, node), 'pending')) {
return;
}
if (testDepth > 0) {
context.report({
messageId: 'pendingTest',
node
});
} else if (suiteDepth > 0) {
context.report({
messageId: 'pendingSuite',
node
});
} else {
context.report({
messageId: 'pending',
node
});
}
}
};
}
});

View File

@@ -0,0 +1,122 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
const findCallbackArg = (node, isJestEach, context) => {
if (isJestEach) {
return node.arguments[1];
}
const jestFnCall = (0, _utils2.parseJestFnCall)(node, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) === 'hook' && node.arguments.length >= 1) {
return node.arguments[0];
}
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) === 'test' && node.arguments.length >= 2) {
return node.arguments[1];
}
return null;
};
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Disallow using a callback in asynchronous tests and hooks',
recommended: 'error'
},
messages: {
noDoneCallback: 'Return a Promise instead of relying on callback parameter',
suggestWrappingInPromise: 'Wrap in `new Promise({{ callback }} => ...`',
useAwaitInsteadOfCallback: 'Use await instead of callback in async functions'
},
schema: [],
type: 'suggestion',
hasSuggestions: true
},
defaultOptions: [],
create(context) {
return {
CallExpression(node) {
var _getNodeName;
// done is the second argument for it.each, not the first
const isJestEach = ((_getNodeName = (0, _utils2.getNodeName)(node.callee)) === null || _getNodeName === void 0 ? void 0 : _getNodeName.endsWith('.each')) ?? false;
if (isJestEach && node.callee.type !== _utils.AST_NODE_TYPES.TaggedTemplateExpression) {
// isJestEach but not a TaggedTemplateExpression, so this must be
// the `jest.each([])()` syntax which this rule doesn't support due
// to its complexity (see jest-community/eslint-plugin-jest#710)
return;
}
const callback = findCallbackArg(node, isJestEach, context);
const callbackArgIndex = Number(isJestEach);
if (!callback || !(0, _utils2.isFunction)(callback) || callback.params.length !== 1 + callbackArgIndex) {
return;
}
const argument = callback.params[callbackArgIndex];
if (argument.type !== _utils.AST_NODE_TYPES.Identifier) {
context.report({
node: argument,
messageId: 'noDoneCallback'
});
return;
}
if (callback.async) {
context.report({
node: argument,
messageId: 'useAwaitInsteadOfCallback'
});
return;
}
context.report({
node: argument,
messageId: 'noDoneCallback',
suggest: [{
messageId: 'suggestWrappingInPromise',
data: {
callback: argument.name
},
fix(fixer) {
var _tokenAfterLastParam;
const {
body,
params
} = callback;
const sourceCode = (0, _utils2.getSourceCode)(context);
const firstBodyToken = sourceCode.getFirstToken(body);
const lastBodyToken = sourceCode.getLastToken(body);
const [firstParam] = params;
const lastParam = params[params.length - 1];
const tokenBeforeFirstParam = sourceCode.getTokenBefore(firstParam);
let tokenAfterLastParam = sourceCode.getTokenAfter(lastParam);
if (((_tokenAfterLastParam = tokenAfterLastParam) === null || _tokenAfterLastParam === void 0 ? void 0 : _tokenAfterLastParam.value) === ',') {
tokenAfterLastParam = sourceCode.getTokenAfter(tokenAfterLastParam);
}
/* istanbul ignore if */
if (!firstBodyToken || !lastBodyToken || !tokenBeforeFirstParam || !tokenAfterLastParam) {
throw new Error(`Unexpected null when attempting to fix ${context.getFilename()} - please file a github issue at https://github.com/jest-community/eslint-plugin-jest`);
}
let argumentFix = fixer.replaceText(firstParam, '()');
if (tokenBeforeFirstParam.value === '(' && tokenAfterLastParam.value === ')') {
argumentFix = fixer.removeRange([tokenBeforeFirstParam.range[1], tokenAfterLastParam.range[0]]);
}
const newCallback = argument.name;
let beforeReplacement = `new Promise(${newCallback} => `;
let afterReplacement = ')';
let replaceBefore = true;
if (body.type === _utils.AST_NODE_TYPES.BlockStatement) {
const keyword = 'return';
beforeReplacement = `${keyword} ${beforeReplacement}{`;
afterReplacement += '}';
replaceBefore = false;
}
return [argumentFix, replaceBefore ? fixer.insertTextBefore(firstBodyToken, beforeReplacement) : fixer.insertTextAfter(firstBodyToken, beforeReplacement), fixer.insertTextAfter(lastBodyToken, afterReplacement)];
}
}]
});
}
};
}
});

View File

@@ -0,0 +1,55 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("./utils");
var _default = exports.default = (0, _utils.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Disallow duplicate setup and teardown hooks',
recommended: false
},
messages: {
noDuplicateHook: 'Duplicate {{hook}} in describe block'
},
schema: [],
type: 'suggestion'
},
defaultOptions: [],
create(context) {
const hookContexts = [{}];
return {
CallExpression(node) {
var _jestFnCall$name;
const jestFnCall = (0, _utils.parseJestFnCall)(node, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) === 'describe') {
hookContexts.push({});
}
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'hook') {
return;
}
const currentLayer = hookContexts[hookContexts.length - 1];
currentLayer[_jestFnCall$name = jestFnCall.name] || (currentLayer[_jestFnCall$name] = 0);
currentLayer[jestFnCall.name] += 1;
if (currentLayer[jestFnCall.name] > 1) {
context.report({
messageId: 'noDuplicateHook',
data: {
hook: jestFnCall.name
},
node
});
}
},
'CallExpression:exit'(node) {
if ((0, _utils.isTypeOfJestFnCall)(node, context, ['describe'])) {
hookContexts.pop();
}
}
};
}
});

63
node_modules/eslint-plugin-jest/lib/rules/no-export.js generated vendored Normal file
View File

@@ -0,0 +1,63 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Disallow using `exports` in files containing tests',
recommended: 'error'
},
messages: {
unexpectedExport: `Do not export from a test file`
},
type: 'suggestion',
schema: []
},
defaultOptions: [],
create(context) {
const exportNodes = [];
let hasTestCase = false;
return {
'Program:exit'() {
if (hasTestCase && exportNodes.length > 0) {
for (const node of exportNodes) {
context.report({
node,
messageId: 'unexpectedExport'
});
}
}
},
CallExpression(node) {
if ((0, _utils2.isTypeOfJestFnCall)(node, context, ['test'])) {
hasTestCase = true;
}
},
'ExportNamedDeclaration, ExportDefaultDeclaration'(node) {
exportNodes.push(node);
},
'AssignmentExpression > MemberExpression'(node) {
let {
object,
property
} = node;
if (object.type === _utils.AST_NODE_TYPES.MemberExpression) {
({
object,
property
} = object);
}
if ('name' in object && object.name === 'module' && property.type === _utils.AST_NODE_TYPES.Identifier && /^exports?$/u.test(property.name)) {
exportNodes.push(node);
}
}
};
}
});

View File

@@ -0,0 +1,65 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Disallow focused tests',
recommended: 'error'
},
messages: {
focusedTest: 'Unexpected focused test',
suggestRemoveFocus: 'Remove focus from test'
},
schema: [],
type: 'suggestion',
hasSuggestions: true
},
defaultOptions: [],
create(context) {
return {
CallExpression(node) {
const jestFnCall = (0, _utils2.parseJestFnCall)(node, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'test' && (jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'describe') {
return;
}
if (jestFnCall.name.startsWith('f')) {
context.report({
messageId: 'focusedTest',
node: jestFnCall.head.node,
suggest: [{
messageId: 'suggestRemoveFocus',
fix(fixer) {
// don't apply the fixer if we're an aliased import
if (jestFnCall.head.type === 'import' && jestFnCall.name !== jestFnCall.head.local) {
return null;
}
return fixer.removeRange([node.range[0], node.range[0] + 1]);
}
}]
});
return;
}
const onlyNode = jestFnCall.members.find(s => (0, _utils2.getAccessorValue)(s) === 'only');
if (!onlyNode) {
return;
}
context.report({
messageId: 'focusedTest',
node: onlyNode,
suggest: [{
messageId: 'suggestRemoveFocus',
fix: fixer => fixer.removeRange([onlyNode.range[0] - 1, onlyNode.range[1] + Number(onlyNode.type !== _utils.AST_NODE_TYPES.Identifier)])
}]
});
}
};
}
});

52
node_modules/eslint-plugin-jest/lib/rules/no-hooks.js generated vendored Normal file
View File

@@ -0,0 +1,52 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("./utils");
var _default = exports.default = (0, _utils.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Disallow setup and teardown hooks',
recommended: false
},
messages: {
unexpectedHook: "Unexpected '{{ hookName }}' hook"
},
schema: [{
type: 'object',
properties: {
allow: {
type: 'array',
contains: ['beforeAll', 'beforeEach', 'afterAll', 'afterEach']
}
},
additionalProperties: false
}],
type: 'suggestion'
},
defaultOptions: [{
allow: []
}],
create(context, [{
allow = []
}]) {
return {
CallExpression(node) {
const jestFnCall = (0, _utils.parseJestFnCall)(node, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) === 'hook' && !allow.includes(jestFnCall.name)) {
context.report({
node,
messageId: 'unexpectedHook',
data: {
hookName: jestFnCall.name
}
});
}
}
};
}
});

View File

@@ -0,0 +1,75 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("./utils");
const newDescribeContext = () => ({
describeTitles: [],
testTitles: []
});
var _default = exports.default = (0, _utils.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Disallow identical titles',
recommended: 'error'
},
messages: {
multipleTestTitle: 'Test title is used multiple times in the same describe block',
multipleDescribeTitle: 'Describe block title is used multiple times in the same describe block'
},
schema: [],
type: 'suggestion'
},
defaultOptions: [],
create(context) {
const contexts = [newDescribeContext()];
return {
CallExpression(node) {
const currentLayer = contexts[contexts.length - 1];
const jestFnCall = (0, _utils.parseJestFnCall)(node, context);
if (!jestFnCall) {
return;
}
if (jestFnCall.type === 'describe') {
contexts.push(newDescribeContext());
}
if (jestFnCall.members.find(s => (0, _utils.isSupportedAccessor)(s, 'each'))) {
return;
}
const [argument] = node.arguments;
if (!argument || !(0, _utils.isStringNode)(argument)) {
return;
}
const title = (0, _utils.getStringValue)(argument);
if (jestFnCall.type === 'test') {
if (currentLayer.testTitles.includes(title)) {
context.report({
messageId: 'multipleTestTitle',
node: argument
});
}
currentLayer.testTitles.push(title);
}
if (jestFnCall.type !== 'describe') {
return;
}
if (currentLayer.describeTitles.includes(title)) {
context.report({
messageId: 'multipleDescribeTitle',
node: argument
});
}
currentLayer.describeTitles.push(title);
},
'CallExpression:exit'(node) {
if ((0, _utils.isTypeOfJestFnCall)(node, context, ['describe'])) {
contexts.pop();
}
}
};
}
});

86
node_modules/eslint-plugin-jest/lib/rules/no-if.js generated vendored Normal file
View File

@@ -0,0 +1,86 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
const testCaseNames = new Set([...Object.keys(_utils2.TestCaseName), 'it.only', 'it.only', 'it.skip', 'it.skip', 'test.only', 'test.only', 'test.skip', 'test.skip', 'fit.concurrent']);
const isTestFunctionExpression = node => node.parent !== undefined && node.parent.type === _utils.AST_NODE_TYPES.CallExpression && testCaseNames.has((0, _utils2.getNodeName)(node.parent.callee));
const conditionName = {
[_utils.AST_NODE_TYPES.ConditionalExpression]: 'conditional',
[_utils.AST_NODE_TYPES.SwitchStatement]: 'switch',
[_utils.AST_NODE_TYPES.IfStatement]: 'if'
};
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
description: 'Disallow conditional logic',
category: 'Best Practices',
recommended: false
},
messages: {
conditionalInTest: 'Test should not contain {{ condition }} statements'
},
deprecated: true,
replacedBy: ['no-conditional-in-test'],
schema: [],
type: 'suggestion'
},
defaultOptions: [],
create(context) {
const stack = [];
function validate(node) {
const lastElementInStack = stack[stack.length - 1];
if (stack.length === 0 || !lastElementInStack) {
return;
}
context.report({
data: {
condition: conditionName[node.type]
},
messageId: 'conditionalInTest',
node
});
}
return {
CallExpression(node) {
const jestFnCall = (0, _utils2.parseJestFnCall)(node, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) === 'test') {
stack.push(true);
if (jestFnCall.members.some(s => (0, _utils2.getAccessorValue)(s) === 'each')) {
stack.push(true);
}
}
},
FunctionExpression(node) {
stack.push(isTestFunctionExpression(node));
},
FunctionDeclaration(node) {
const declaredVariables = (0, _utils2.getDeclaredVariables)(context, node);
const testCallExpressions = (0, _utils2.getTestCallExpressionsFromDeclaredVariables)(declaredVariables, context);
stack.push(testCallExpressions.length > 0);
},
ArrowFunctionExpression(node) {
stack.push(isTestFunctionExpression(node));
},
IfStatement: validate,
SwitchStatement: validate,
ConditionalExpression: validate,
'CallExpression:exit'() {
stack.pop();
},
'FunctionExpression:exit'() {
stack.pop();
},
'FunctionDeclaration:exit'() {
stack.pop();
},
'ArrowFunctionExpression:exit'() {
stack.pop();
}
};
}
});

View File

@@ -0,0 +1,45 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Disallow string interpolation inside snapshots',
recommended: 'error'
},
messages: {
noInterpolation: 'Do not use string interpolation inside of snapshots'
},
schema: [],
type: 'problem'
},
defaultOptions: [],
create(context) {
return {
CallExpression(node) {
const jestFnCall = (0, _utils2.parseJestFnCall)(node, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'expect') {
return;
}
if (['toMatchInlineSnapshot', 'toThrowErrorMatchingInlineSnapshot'].includes((0, _utils2.getAccessorValue)(jestFnCall.matcher))) {
// Check all since the optional 'propertyMatchers' argument might be present
jestFnCall.args.forEach(argument => {
if (argument.type === _utils.AST_NODE_TYPES.TemplateLiteral && argument.expressions.length > 0) {
context.report({
messageId: 'noInterpolation',
node: argument
});
}
});
}
}
};
}
});

View File

@@ -0,0 +1,142 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Disallow Jasmine globals',
recommended: 'error'
},
messages: {
illegalGlobal: 'Illegal usage of global `{{ global }}`, prefer `{{ replacement }}`',
illegalMethod: 'Illegal usage of `{{ method }}`, prefer `{{ replacement }}`',
illegalFail: 'Illegal usage of `fail`, prefer throwing an error, or the `done.fail` callback',
illegalPending: 'Illegal usage of `pending`, prefer explicitly skipping a test using `test.skip`',
illegalJasmine: 'Illegal usage of jasmine global'
},
fixable: 'code',
schema: [],
type: 'suggestion'
},
defaultOptions: [],
create(context) {
return {
CallExpression(node) {
const {
callee
} = node;
const calleeName = (0, _utils2.getNodeName)(callee);
if (!calleeName) {
return;
}
if (calleeName === 'spyOn' || calleeName === 'spyOnProperty' || calleeName === 'fail' || calleeName === 'pending') {
if ((0, _utils2.resolveScope)((0, _utils2.getScope)(context, node), calleeName)) {
// It's a local variable, not a jasmine global.
return;
}
switch (calleeName) {
case 'spyOn':
case 'spyOnProperty':
context.report({
node,
messageId: 'illegalGlobal',
data: {
global: calleeName,
replacement: 'jest.spyOn'
}
});
break;
case 'fail':
context.report({
node,
messageId: 'illegalFail'
});
break;
case 'pending':
context.report({
node,
messageId: 'illegalPending'
});
break;
}
return;
}
if (callee.type === _utils.AST_NODE_TYPES.MemberExpression && calleeName.startsWith('jasmine.')) {
const functionName = calleeName.replace('jasmine.', '');
if (functionName === 'any' || functionName === 'anything' || functionName === 'arrayContaining' || functionName === 'objectContaining' || functionName === 'stringMatching') {
context.report({
fix: fixer => [fixer.replaceText(callee.object, 'expect')],
node,
messageId: 'illegalMethod',
data: {
method: calleeName,
replacement: `expect.${functionName}`
}
});
return;
}
if (functionName === 'addMatchers') {
context.report({
node,
messageId: 'illegalMethod',
data: {
method: calleeName,
replacement: 'expect.extend'
}
});
return;
}
if (functionName === 'createSpy') {
context.report({
node,
messageId: 'illegalMethod',
data: {
method: calleeName,
replacement: 'jest.fn'
}
});
return;
}
context.report({
node,
messageId: 'illegalJasmine'
});
}
},
MemberExpression(node) {
if ((0, _utils2.isSupportedAccessor)(node.object, 'jasmine')) {
const {
parent,
property
} = node;
if (parent && parent.type === _utils.AST_NODE_TYPES.AssignmentExpression) {
if ((0, _utils2.isSupportedAccessor)(property, 'DEFAULT_TIMEOUT_INTERVAL')) {
const {
right
} = parent;
if (right.type === _utils.AST_NODE_TYPES.Literal) {
context.report({
fix: fixer => [fixer.replaceText(parent, `jest.setTimeout(${right.value})`)],
node,
messageId: 'illegalJasmine'
});
return;
}
}
context.report({
node,
messageId: 'illegalJasmine'
});
}
}
}
};
}
});

View File

@@ -0,0 +1,102 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _path = require("path");
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
const reportOnViolation = (context, node, {
maxSize: lineLimit = 50,
allowedSnapshots = {}
}) => {
const startLine = node.loc.start.line;
const endLine = node.loc.end.line;
const lineCount = endLine - startLine;
const allPathsAreAbsolute = Object.keys(allowedSnapshots).every(_path.isAbsolute);
if (!allPathsAreAbsolute) {
throw new Error('All paths for allowedSnapshots must be absolute. You can use JS config and `path.resolve`');
}
let isAllowed = false;
if (node.type === _utils.AST_NODE_TYPES.ExpressionStatement && 'left' in node.expression && node.expression.left.type === _utils.AST_NODE_TYPES.MemberExpression && (0, _utils2.isSupportedAccessor)(node.expression.left.property)) {
const fileName = (0, _utils2.getFilename)(context);
const allowedSnapshotsInFile = allowedSnapshots[fileName];
if (allowedSnapshotsInFile) {
const snapshotName = (0, _utils2.getAccessorValue)(node.expression.left.property);
isAllowed = allowedSnapshotsInFile.some(name => {
if (name instanceof RegExp) {
return name.test(snapshotName);
}
return snapshotName === name;
});
}
}
if (!isAllowed && lineCount > lineLimit) {
context.report({
messageId: lineLimit === 0 ? 'noSnapshot' : 'tooLongSnapshots',
data: {
lineLimit,
lineCount
},
node
});
}
};
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Disallow large snapshots',
recommended: false
},
messages: {
noSnapshot: '`{{ lineCount }}`s should begin with lowercase',
tooLongSnapshots: 'Expected Jest snapshot to be smaller than {{ lineLimit }} lines but was {{ lineCount }} lines long'
},
type: 'suggestion',
schema: [{
type: 'object',
properties: {
maxSize: {
type: 'number'
},
inlineMaxSize: {
type: 'number'
},
allowedSnapshots: {
type: 'object',
additionalProperties: {
type: 'array'
}
}
},
additionalProperties: false
}]
},
defaultOptions: [{}],
create(context, [options]) {
if (context.getFilename().endsWith('.snap')) {
return {
ExpressionStatement(node) {
reportOnViolation(context, node, options);
}
};
}
return {
CallExpression(node) {
const jestFnCall = (0, _utils2.parseJestFnCall)(node, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'expect') {
return;
}
if (['toMatchInlineSnapshot', 'toThrowErrorMatchingInlineSnapshot'].includes((0, _utils2.getAccessorValue)(jestFnCall.matcher)) && jestFnCall.args.length) {
reportOnViolation(context, jestFnCall.args[0], {
...options,
maxSize: options.inlineMaxSize ?? options.maxSize
});
}
}
};
}
});

View File

@@ -0,0 +1,48 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _path = require("path");
var _utils = require("./utils");
const mocksDirName = '__mocks__';
const isMockPath = path => path.split(_path.posix.sep).includes(mocksDirName);
const isMockImportLiteral = expression => (0, _utils.isStringNode)(expression) && isMockPath((0, _utils.getStringValue)(expression));
var _default = exports.default = (0, _utils.createRule)({
name: __filename,
meta: {
type: 'problem',
docs: {
category: 'Best Practices',
description: 'Disallow manually importing from `__mocks__`',
recommended: 'error'
},
messages: {
noManualImport: `Mocks should not be manually imported from a ${mocksDirName} directory. Instead use \`jest.mock\` and import from the original module path`
},
schema: []
},
defaultOptions: [],
create(context) {
return {
ImportDeclaration(node) {
if (isMockImportLiteral(node.source)) {
context.report({
node,
messageId: 'noManualImport'
});
}
},
'CallExpression[callee.name="require"]'(node) {
const [arg] = node.arguments;
if (arg && isMockImportLiteral(arg)) {
context.report({
node: arg,
messageId: 'noManualImport'
});
}
}
};
}
});

View File

@@ -0,0 +1,55 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("./utils");
const messages = {
restrictedJestMethod: 'Use of `{{ restriction }}` is disallowed',
restrictedJestMethodWithMessage: '{{ message }}'
};
var _default = exports.default = (0, _utils.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Disallow specific `jest.` methods',
recommended: false
},
type: 'suggestion',
schema: [{
type: 'object',
additionalProperties: {
type: ['string', 'null']
}
}],
messages
},
defaultOptions: [{}],
create(context, [restrictedMethods]) {
return {
CallExpression(node) {
const jestFnCall = (0, _utils.parseJestFnCall)(node, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'jest' || jestFnCall.members.length === 0) {
return;
}
const method = (0, _utils.getAccessorValue)(jestFnCall.members[0]);
if (method in restrictedMethods) {
const message = restrictedMethods[method];
context.report({
messageId: message ? 'restrictedJestMethodWithMessage' : 'restrictedJestMethod',
data: {
message,
restriction: method
},
loc: {
start: jestFnCall.members[0].loc.start,
end: jestFnCall.members[jestFnCall.members.length - 1].loc.end
}
});
}
}
};
}
});

View File

@@ -0,0 +1,62 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("./utils");
const isChainRestricted = (chain, restriction) => {
if (_utils.ModifierName.hasOwnProperty(restriction) || restriction.endsWith('.not')) {
return chain.startsWith(restriction);
}
return chain === restriction;
};
var _default = exports.default = (0, _utils.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Disallow specific matchers & modifiers',
recommended: false
},
type: 'suggestion',
schema: [{
type: 'object',
additionalProperties: {
type: ['string', 'null']
}
}],
messages: {
restrictedChain: 'Use of `{{ restriction }}` is disallowed',
restrictedChainWithMessage: '{{ message }}'
}
},
defaultOptions: [{}],
create(context, [restrictedChains]) {
return {
CallExpression(node) {
const jestFnCall = (0, _utils.parseJestFnCall)(node, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'expect') {
return;
}
const chain = jestFnCall.members.map(nod => (0, _utils.getAccessorValue)(nod)).join('.');
for (const [restriction, message] of Object.entries(restrictedChains)) {
if (isChainRestricted(chain, restriction)) {
context.report({
messageId: message ? 'restrictedChainWithMessage' : 'restrictedChain',
data: {
message,
restriction
},
loc: {
start: jestFnCall.members[0].loc.start,
end: jestFnCall.members[jestFnCall.members.length - 1].loc.end
}
});
break;
}
}
}
};
}
});

View File

@@ -0,0 +1,122 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
const getBlockType = (statement, context) => {
const func = statement.parent;
/* istanbul ignore if */
if (!func) {
throw new Error(`Unexpected BlockStatement. No parent defined. - please file a github issue at https://github.com/jest-community/eslint-plugin-jest`);
}
// functionDeclaration: function func() {}
if (func.type === _utils.AST_NODE_TYPES.FunctionDeclaration) {
return 'function';
}
if ((0, _utils2.isFunction)(func) && func.parent) {
const expr = func.parent;
// arrow function or function expr
if (expr.type === _utils.AST_NODE_TYPES.VariableDeclarator) {
return 'function';
}
// if it's not a variable, it will be callExpr, we only care about describe
if (expr.type === _utils.AST_NODE_TYPES.CallExpression && (0, _utils2.isTypeOfJestFnCall)(expr, context, ['describe'])) {
return 'describe';
}
}
return null;
};
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Disallow using `expect` outside of `it` or `test` blocks',
recommended: 'error'
},
messages: {
unexpectedExpect: 'Expect must be inside of a test block'
},
type: 'suggestion',
schema: [{
properties: {
additionalTestBlockFunctions: {
type: 'array',
items: {
type: 'string'
}
}
},
additionalProperties: false
}]
},
defaultOptions: [{
additionalTestBlockFunctions: []
}],
create(context, [{
additionalTestBlockFunctions = []
}]) {
const callStack = [];
const isCustomTestBlockFunction = node => additionalTestBlockFunctions.includes((0, _utils2.getNodeName)(node) || '');
return {
CallExpression(node) {
const jestFnCall = (0, _utils2.parseJestFnCall)(node, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) === 'expect') {
var _jestFnCall$head$node;
if (((_jestFnCall$head$node = jestFnCall.head.node.parent) === null || _jestFnCall$head$node === void 0 ? void 0 : _jestFnCall$head$node.type) === _utils.AST_NODE_TYPES.MemberExpression && jestFnCall.members.length === 1 && !['assertions', 'hasAssertions'].includes((0, _utils2.getAccessorValue)(jestFnCall.members[0]))) {
return;
}
const parent = callStack[callStack.length - 1];
if (!parent || parent === _utils2.DescribeAlias.describe) {
context.report({
node,
messageId: 'unexpectedExpect'
});
}
return;
}
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) === 'test' || isCustomTestBlockFunction(node)) {
callStack.push('test');
}
if (node.callee.type === _utils.AST_NODE_TYPES.TaggedTemplateExpression) {
callStack.push('template');
}
},
'CallExpression:exit'(node) {
const top = callStack[callStack.length - 1];
if (top === 'test' && ((0, _utils2.isTypeOfJestFnCall)(node, context, ['test']) || isCustomTestBlockFunction(node)) && node.callee.type !== _utils.AST_NODE_TYPES.MemberExpression || top === 'template' && node.callee.type === _utils.AST_NODE_TYPES.TaggedTemplateExpression) {
callStack.pop();
}
},
BlockStatement(statement) {
const blockType = getBlockType(statement, context);
if (blockType) {
callStack.push(blockType);
}
},
'BlockStatement:exit'(statement) {
if (callStack[callStack.length - 1] === getBlockType(statement, context)) {
callStack.pop();
}
},
ArrowFunctionExpression(node) {
var _node$parent;
if (((_node$parent = node.parent) === null || _node$parent === void 0 ? void 0 : _node$parent.type) !== _utils.AST_NODE_TYPES.CallExpression) {
callStack.push('arrow');
}
},
'ArrowFunctionExpression:exit'() {
if (callStack[callStack.length - 1] === 'arrow') {
callStack.pop();
}
}
};
}
});

View File

@@ -0,0 +1,50 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Require using `.only` and `.skip` over `f` and `x`',
recommended: 'error'
},
messages: {
usePreferredName: 'Use "{{ preferredNodeName }}" instead'
},
fixable: 'code',
schema: [],
type: 'suggestion'
},
defaultOptions: [],
create(context) {
return {
CallExpression(node) {
const jestFnCall = (0, _utils2.parseJestFnCall)(node, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'describe' && (jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'test') {
return;
}
if (jestFnCall.name[0] !== 'f' && jestFnCall.name[0] !== 'x') {
return;
}
const preferredNodeName = [jestFnCall.name.slice(1), jestFnCall.name[0] === 'f' ? 'only' : 'skip', ...jestFnCall.members.map(s => (0, _utils2.getAccessorValue)(s))].join('.');
const funcNode = node.callee.type === _utils.AST_NODE_TYPES.TaggedTemplateExpression ? node.callee.tag : node.callee.type === _utils.AST_NODE_TYPES.CallExpression ? node.callee.callee : node.callee;
context.report({
messageId: 'usePreferredName',
node: node.callee,
data: {
preferredNodeName
},
fix(fixer) {
return [fixer.replaceText(funcNode, preferredNodeName)];
}
});
}
};
}
});

View File

@@ -0,0 +1,64 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
const getBody = args => {
const [, secondArg] = args;
if (secondArg && (0, _utils2.isFunction)(secondArg) && secondArg.body.type === _utils.AST_NODE_TYPES.BlockStatement) {
return secondArg.body.body;
}
return [];
};
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Disallow explicitly returning from tests',
recommended: false
},
messages: {
noReturnValue: 'Jest tests should not return a value'
},
schema: [],
type: 'suggestion'
},
defaultOptions: [],
create(context) {
return {
CallExpression(node) {
if (!(0, _utils2.isTypeOfJestFnCall)(node, context, ['test'])) {
return;
}
const body = getBody(node.arguments);
const returnStmt = body.find(t => t.type === _utils.AST_NODE_TYPES.ReturnStatement);
if (!returnStmt) {
return;
}
context.report({
messageId: 'noReturnValue',
node: returnStmt
});
},
FunctionDeclaration(node) {
const declaredVariables = (0, _utils2.getDeclaredVariables)(context, node);
const testCallExpressions = (0, _utils2.getTestCallExpressionsFromDeclaredVariables)(declaredVariables, context);
if (testCallExpressions.length === 0) {
return;
}
const returnStmt = node.body.body.find(t => t.type === _utils.AST_NODE_TYPES.ReturnStatement);
if (!returnStmt) {
return;
}
context.report({
messageId: 'noReturnValue',
node: returnStmt
});
}
};
}
});

View File

@@ -0,0 +1,69 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
const findModuleName = node => {
if (node.type === _utils.AST_NODE_TYPES.Literal && typeof node.value === 'string') {
return node;
}
return null;
};
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Disallow using `jest.mock()` factories without an explicit type parameter',
recommended: false
},
messages: {
addTypeParameterToModuleMock: 'Add a type parameter to the mock factory such as `typeof import({{ moduleName }})`'
},
fixable: 'code',
schema: [],
type: 'suggestion'
},
defaultOptions: [],
create(context) {
return {
CallExpression(node) {
const {
callee,
typeParameters
} = node;
if (callee.type !== _utils.AST_NODE_TYPES.MemberExpression) {
return;
}
const {
property
} = callee;
if (node.arguments.length === 2 && (0, _utils2.isTypeOfJestFnCall)(node, context, ['jest']) && (0, _utils2.isSupportedAccessor)(property) && ['mock', 'doMock'].includes((0, _utils2.getAccessorValue)(property))) {
const [nameNode, factoryNode] = node.arguments;
const hasTypeParameter = typeParameters !== undefined && typeParameters.params.length > 0;
const hasReturnType = (0, _utils2.isFunction)(factoryNode) && factoryNode.returnType !== undefined;
if (hasTypeParameter || hasReturnType) {
return;
}
const moduleName = findModuleName(nameNode);
context.report({
messageId: 'addTypeParameterToModuleMock',
data: {
moduleName: (moduleName === null || moduleName === void 0 ? void 0 : moduleName.raw) ?? './module-name'
},
node,
fix(fixer) {
if (!moduleName) {
return [];
}
return [fixer.insertTextAfter(callee, `<typeof import(${moduleName.raw})>`)];
}
});
}
}
};
}
});

View File

@@ -0,0 +1,49 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("./utils");
var _default = exports.default = (0, _utils.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Suggest using `toBeCalledWith()` or `toHaveBeenCalledWith()`',
recommended: false
},
messages: {
preferCalledWith: 'Prefer {{ matcherName }}With(/* expected args */)'
},
type: 'suggestion',
schema: []
},
defaultOptions: [],
create(context) {
return {
CallExpression(node) {
const jestFnCall = (0, _utils.parseJestFnCall)(node, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'expect') {
return;
}
if (jestFnCall.modifiers.some(nod => (0, _utils.getAccessorValue)(nod) === 'not')) {
return;
}
const {
matcher
} = jestFnCall;
const matcherName = (0, _utils.getAccessorValue)(matcher);
if (['toBeCalled', 'toHaveBeenCalled'].includes(matcherName)) {
context.report({
data: {
matcherName
},
messageId: 'preferCalledWith',
node: matcher
});
}
}
};
}
});

View File

@@ -0,0 +1,111 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
const isString = node => {
return (0, _utils2.isStringNode)(node) || node.type === _utils.AST_NODE_TYPES.TemplateLiteral;
};
const isComparingToString = expression => {
return isString(expression.left) || isString(expression.right);
};
const invertOperator = operator => {
switch (operator) {
case '>':
return '<=';
case '<':
return '>=';
case '>=':
return '<';
case '<=':
return '>';
}
return null;
};
const determineMatcher = (operator, negated) => {
const op = negated ? invertOperator(operator) : operator;
switch (op) {
case '>':
return 'toBeGreaterThan';
case '<':
return 'toBeLessThan';
case '>=':
return 'toBeGreaterThanOrEqual';
case '<=':
return 'toBeLessThanOrEqual';
}
return null;
};
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Suggest using the built-in comparison matchers',
recommended: false
},
messages: {
useToBeComparison: 'Prefer using `{{ preferredMatcher }}` instead'
},
fixable: 'code',
type: 'suggestion',
schema: []
},
defaultOptions: [],
create(context) {
return {
CallExpression(node) {
const jestFnCall = (0, _utils2.parseJestFnCall)(node, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'expect' || jestFnCall.args.length === 0) {
return;
}
const {
parent: expect
} = jestFnCall.head.node;
if ((expect === null || expect === void 0 ? void 0 : expect.type) !== _utils.AST_NODE_TYPES.CallExpression) {
return;
}
const {
arguments: [comparison],
range: [, expectCallEnd]
} = expect;
const {
matcher
} = jestFnCall;
const matcherArg = (0, _utils2.getFirstMatcherArg)(jestFnCall);
if ((comparison === null || comparison === void 0 ? void 0 : comparison.type) !== _utils.AST_NODE_TYPES.BinaryExpression || isComparingToString(comparison) || !_utils2.EqualityMatcher.hasOwnProperty((0, _utils2.getAccessorValue)(matcher)) || !(0, _utils2.isBooleanLiteral)(matcherArg)) {
return;
}
const [modifier] = jestFnCall.modifiers;
const hasNot = jestFnCall.modifiers.some(nod => (0, _utils2.getAccessorValue)(nod) === 'not');
const preferredMatcher = determineMatcher(comparison.operator, matcherArg.value === hasNot);
if (!preferredMatcher) {
return;
}
context.report({
fix(fixer) {
const sourceCode = (0, _utils2.getSourceCode)(context);
// preserve the existing modifier if it's not a negation
const modifierText = modifier && (0, _utils2.getAccessorValue)(modifier) !== 'not' ? `.${(0, _utils2.getAccessorValue)(modifier)}` : '';
return [
// replace the comparison argument with the left-hand side of the comparison
fixer.replaceText(comparison, sourceCode.getText(comparison.left)),
// replace the current matcher & modifier with the preferred matcher
fixer.replaceTextRange([expectCallEnd, matcher.parent.range[1]], `${modifierText}.${preferredMatcher}`),
// replace the matcher argument with the right-hand side of the comparison
fixer.replaceText(matcherArg, sourceCode.getText(comparison.right))];
},
messageId: 'useToBeComparison',
data: {
preferredMatcher
},
node: matcher
});
}
};
}
});

View File

@@ -0,0 +1,79 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("./utils");
var _default = exports.default = (0, _utils.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Prefer using `.each` rather than manual loops',
recommended: false
},
messages: {
preferEach: 'prefer using `{{ fn }}.each` rather than a manual loop'
},
type: 'suggestion',
schema: []
},
defaultOptions: [],
create(context) {
const jestFnCalls = [];
let inTestCaseCall = false;
const recommendFn = () => {
if (jestFnCalls.length === 1 && jestFnCalls[0] === 'test') {
return 'it';
}
return 'describe';
};
const enterForLoop = () => {
if (jestFnCalls.length === 0 || inTestCaseCall) {
return;
}
jestFnCalls.length = 0;
};
const exitForLoop = node => {
if (jestFnCalls.length === 0 || inTestCaseCall) {
return;
}
context.report({
node,
messageId: 'preferEach',
data: {
fn: recommendFn()
}
});
jestFnCalls.length = 0;
};
return {
ForStatement: enterForLoop,
'ForStatement:exit': exitForLoop,
ForInStatement: enterForLoop,
'ForInStatement:exit': exitForLoop,
ForOfStatement: enterForLoop,
'ForOfStatement:exit': exitForLoop,
CallExpression(node) {
const {
type: jestFnCallType
} = (0, _utils.parseJestFnCall)(node, context) ?? {};
if (jestFnCallType === 'hook' || jestFnCallType === 'describe' || jestFnCallType === 'test') {
jestFnCalls.push(jestFnCallType);
}
if (jestFnCallType === 'test') {
inTestCaseCall = true;
}
},
'CallExpression:exit'(node) {
const {
type: jestFnCallType
} = (0, _utils.parseJestFnCall)(node, context) ?? {};
if (jestFnCallType === 'test') {
inTestCaseCall = false;
}
}
};
}
});

View File

@@ -0,0 +1,87 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Suggest using the built-in equality matchers',
recommended: false
},
messages: {
useEqualityMatcher: 'Prefer using one of the equality matchers instead',
suggestEqualityMatcher: 'Use `{{ equalityMatcher }}`'
},
hasSuggestions: true,
type: 'suggestion',
schema: []
},
defaultOptions: [],
create(context) {
return {
CallExpression(node) {
const jestFnCall = (0, _utils2.parseJestFnCall)(node, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'expect' || jestFnCall.args.length === 0) {
return;
}
const {
parent: expect
} = jestFnCall.head.node;
if ((expect === null || expect === void 0 ? void 0 : expect.type) !== _utils.AST_NODE_TYPES.CallExpression) {
return;
}
const {
arguments: [comparison],
range: [, expectCallEnd]
} = expect;
const {
matcher
} = jestFnCall;
const matcherArg = (0, _utils2.getFirstMatcherArg)(jestFnCall);
if ((comparison === null || comparison === void 0 ? void 0 : comparison.type) !== _utils.AST_NODE_TYPES.BinaryExpression || comparison.operator !== '===' && comparison.operator !== '!==' || !_utils2.EqualityMatcher.hasOwnProperty((0, _utils2.getAccessorValue)(matcher)) || !(0, _utils2.isBooleanLiteral)(matcherArg)) {
return;
}
const matcherValue = matcherArg.value;
const [modifier] = jestFnCall.modifiers;
const hasNot = jestFnCall.modifiers.some(nod => (0, _utils2.getAccessorValue)(nod) === 'not');
// we need to negate the expectation if the current expected
// value is itself negated by the "not" modifier
const addNotModifier = (comparison.operator === '!==' ? !matcherValue : matcherValue) === hasNot;
const buildFixer = equalityMatcher => fixer => {
const sourceCode = (0, _utils2.getSourceCode)(context);
// preserve the existing modifier if it's not a negation
let modifierText = modifier && (0, _utils2.getAccessorValue)(modifier) !== 'not' ? `.${(0, _utils2.getAccessorValue)(modifier)}` : '';
if (addNotModifier) {
modifierText += `.${_utils2.ModifierName.not}`;
}
return [
// replace the comparison argument with the left-hand side of the comparison
fixer.replaceText(comparison, sourceCode.getText(comparison.left)),
// replace the current matcher & modifier with the preferred matcher
fixer.replaceTextRange([expectCallEnd, matcher.parent.range[1]], `${modifierText}.${equalityMatcher}`),
// replace the matcher argument with the right-hand side of the comparison
fixer.replaceText(matcherArg, sourceCode.getText(comparison.right))];
};
context.report({
messageId: 'useEqualityMatcher',
suggest: ['toBe', 'toEqual', 'toStrictEqual'].map(equalityMatcher => ({
messageId: 'suggestEqualityMatcher',
data: {
equalityMatcher
},
fix: buildFixer(equalityMatcher)
})),
node: matcher
});
}
};
}
});

View File

@@ -0,0 +1,204 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
const isFirstStatement = node => {
let parent = node;
while (parent) {
var _parent$parent, _parent$parent2;
if (((_parent$parent = parent.parent) === null || _parent$parent === void 0 ? void 0 : _parent$parent.type) === _utils.AST_NODE_TYPES.BlockStatement) {
return parent.parent.body[0] === parent;
}
// if we've hit an arrow function, then it must have a single expression
// as its body, as otherwise we would have hit the block statement already
if (((_parent$parent2 = parent.parent) === null || _parent$parent2 === void 0 ? void 0 : _parent$parent2.type) === _utils.AST_NODE_TYPES.ArrowFunctionExpression) {
return true;
}
parent = parent.parent;
}
/* istanbul ignore next */
throw new Error(`Could not find BlockStatement - please file a github issue at https://github.com/jest-community/eslint-plugin-jest`);
};
const suggestRemovingExtraArguments = (context, func, from) => ({
messageId: 'suggestRemovingExtraArguments',
fix: fixer => (0, _utils2.removeExtraArgumentsFixer)(fixer, context, func, from)
});
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Suggest using `expect.assertions()` OR `expect.hasAssertions()`',
recommended: false
},
messages: {
hasAssertionsTakesNoArguments: '`expect.hasAssertions` expects no arguments',
assertionsRequiresOneArgument: '`expect.assertions` excepts a single argument of type number',
assertionsRequiresNumberArgument: 'This argument should be a number',
haveExpectAssertions: 'Every test should have either `expect.assertions(<number of assertions>)` or `expect.hasAssertions()` as its first expression',
suggestAddingHasAssertions: 'Add `expect.hasAssertions()`',
suggestAddingAssertions: 'Add `expect.assertions(<number of assertions>)`',
suggestRemovingExtraArguments: 'Remove extra arguments'
},
type: 'suggestion',
hasSuggestions: true,
schema: [{
type: 'object',
properties: {
onlyFunctionsWithAsyncKeyword: {
type: 'boolean'
},
onlyFunctionsWithExpectInLoop: {
type: 'boolean'
},
onlyFunctionsWithExpectInCallback: {
type: 'boolean'
}
},
additionalProperties: false
}]
},
defaultOptions: [{
onlyFunctionsWithAsyncKeyword: false,
onlyFunctionsWithExpectInLoop: false,
onlyFunctionsWithExpectInCallback: false
}],
create(context, [options]) {
let expressionDepth = 0;
let hasExpectInCallback = false;
let hasExpectInLoop = false;
let hasExpectAssertionsAsFirstStatement = false;
let inTestCaseCall = false;
let inForLoop = false;
const shouldCheckFunction = testFunction => {
if (!options.onlyFunctionsWithAsyncKeyword && !options.onlyFunctionsWithExpectInLoop && !options.onlyFunctionsWithExpectInCallback) {
return true;
}
if (options.onlyFunctionsWithAsyncKeyword) {
if (testFunction.async) {
return true;
}
}
if (options.onlyFunctionsWithExpectInLoop) {
if (hasExpectInLoop) {
return true;
}
}
if (options.onlyFunctionsWithExpectInCallback) {
if (hasExpectInCallback) {
return true;
}
}
return false;
};
const checkExpectHasAssertions = (expectFnCall, func) => {
if ((0, _utils2.getAccessorValue)(expectFnCall.members[0]) === 'hasAssertions') {
if (expectFnCall.args.length) {
context.report({
messageId: 'hasAssertionsTakesNoArguments',
node: expectFnCall.matcher,
suggest: [suggestRemovingExtraArguments(context, func, 0)]
});
}
return;
}
if (expectFnCall.args.length !== 1) {
let {
loc
} = expectFnCall.matcher;
const suggest = [];
if (expectFnCall.args.length) {
loc = expectFnCall.args[1].loc;
suggest.push(suggestRemovingExtraArguments(context, func, 1));
}
context.report({
messageId: 'assertionsRequiresOneArgument',
suggest,
loc
});
return;
}
const [arg] = expectFnCall.args;
if (arg.type === _utils.AST_NODE_TYPES.Literal && typeof arg.value === 'number' && Number.isInteger(arg.value)) {
return;
}
context.report({
messageId: 'assertionsRequiresNumberArgument',
node: arg
});
};
const enterExpression = () => inTestCaseCall && expressionDepth++;
const exitExpression = () => inTestCaseCall && expressionDepth--;
const enterForLoop = () => inForLoop = true;
const exitForLoop = () => inForLoop = false;
return {
FunctionExpression: enterExpression,
'FunctionExpression:exit': exitExpression,
ArrowFunctionExpression: enterExpression,
'ArrowFunctionExpression:exit': exitExpression,
ForStatement: enterForLoop,
'ForStatement:exit': exitForLoop,
ForInStatement: enterForLoop,
'ForInStatement:exit': exitForLoop,
ForOfStatement: enterForLoop,
'ForOfStatement:exit': exitForLoop,
CallExpression(node) {
const jestFnCall = (0, _utils2.parseJestFnCall)(node, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) === 'test') {
inTestCaseCall = true;
return;
}
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) === 'expect' && inTestCaseCall) {
var _jestFnCall$head$node;
if (expressionDepth === 1 && isFirstStatement(node) && ((_jestFnCall$head$node = jestFnCall.head.node.parent) === null || _jestFnCall$head$node === void 0 ? void 0 : _jestFnCall$head$node.type) === _utils.AST_NODE_TYPES.MemberExpression && jestFnCall.members.length === 1 && ['assertions', 'hasAssertions'].includes((0, _utils2.getAccessorValue)(jestFnCall.members[0]))) {
checkExpectHasAssertions(jestFnCall, node);
hasExpectAssertionsAsFirstStatement = true;
}
if (inForLoop) {
hasExpectInLoop = true;
}
if (expressionDepth > 1) {
hasExpectInCallback = true;
}
}
},
'CallExpression:exit'(node) {
if (!(0, _utils2.isTypeOfJestFnCall)(node, context, ['test'])) {
return;
}
inTestCaseCall = false;
if (node.arguments.length < 2) {
return;
}
const [, testFn] = node.arguments;
if (!(0, _utils2.isFunction)(testFn) || !shouldCheckFunction(testFn)) {
return;
}
hasExpectInLoop = false;
hasExpectInCallback = false;
if (hasExpectAssertionsAsFirstStatement) {
hasExpectAssertionsAsFirstStatement = false;
return;
}
const suggestions = [];
if (testFn.body.type === _utils.AST_NODE_TYPES.BlockStatement) {
suggestions.push(['suggestAddingHasAssertions', 'expect.hasAssertions();'], ['suggestAddingAssertions', 'expect.assertions();']);
}
context.report({
messageId: 'haveExpectAssertions',
node,
suggest: suggestions.map(([messageId, text]) => ({
messageId,
fix: fixer => fixer.insertTextBeforeRange([testFn.body.range[0] + 1, testFn.body.range[1]], text)
}))
});
}
};
}
});

View File

@@ -0,0 +1,49 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Prefer `await expect(...).resolves` over `expect(await ...)` syntax',
recommended: false
},
fixable: 'code',
messages: {
expectResolves: 'Use `await expect(...).resolves instead'
},
schema: [],
type: 'suggestion'
},
defaultOptions: [],
create: context => ({
CallExpression(node) {
const jestFnCall = (0, _utils2.parseJestFnCall)(node, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'expect') {
return;
}
const {
parent
} = jestFnCall.head.node;
if ((parent === null || parent === void 0 ? void 0 : parent.type) !== _utils.AST_NODE_TYPES.CallExpression) {
return;
}
const [awaitNode] = parent.arguments;
if ((awaitNode === null || awaitNode === void 0 ? void 0 : awaitNode.type) === _utils.AST_NODE_TYPES.AwaitExpression) {
context.report({
node: awaitNode,
messageId: 'expectResolves',
fix(fixer) {
return [fixer.insertTextBefore(parent, 'await '), fixer.removeRange([awaitNode.range[0], awaitNode.argument.range[0]]), fixer.insertTextAfter(parent, '.resolves')];
}
});
}
}
})
});

View File

@@ -0,0 +1,69 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("./utils");
const HooksOrder = ['beforeAll', 'beforeEach', 'afterEach', 'afterAll'];
var _default = exports.default = (0, _utils.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Prefer having hooks in a consistent order',
recommended: false
},
messages: {
reorderHooks: `\`{{ currentHook }}\` hooks should be before any \`{{ previousHook }}\` hooks`
},
schema: [],
type: 'suggestion'
},
defaultOptions: [],
create(context) {
let previousHookIndex = -1;
let inHook = false;
return {
CallExpression(node) {
if (inHook) {
// Ignore everything that is passed into a hook
return;
}
const jestFnCall = (0, _utils.parseJestFnCall)(node, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'hook') {
// Reset the previousHookIndex when encountering something different from a hook
previousHookIndex = -1;
return;
}
inHook = true;
const currentHook = jestFnCall.name;
const currentHookIndex = HooksOrder.indexOf(currentHook);
if (currentHookIndex < previousHookIndex) {
context.report({
messageId: 'reorderHooks',
node,
data: {
previousHook: HooksOrder[previousHookIndex],
currentHook
}
});
return;
}
previousHookIndex = currentHookIndex;
},
'CallExpression:exit'(node) {
if ((0, _utils.isTypeOfJestFnCall)(node, context, ['hook'])) {
inHook = false;
return;
}
if (inHook) {
return;
}
// Reset the previousHookIndex when encountering something different from a hook
previousHookIndex = -1;
}
};
}
});

View File

@@ -0,0 +1,43 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("./utils");
var _default = exports.default = (0, _utils.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Suggest having hooks before any test cases',
recommended: false
},
messages: {
noHookOnTop: 'Hooks should come before test cases'
},
schema: [],
type: 'suggestion'
},
defaultOptions: [],
create(context) {
const hooksContext = [false];
return {
CallExpression(node) {
if ((0, _utils.isTypeOfJestFnCall)(node, context, ['test'])) {
hooksContext[hooksContext.length - 1] = true;
}
if (hooksContext[hooksContext.length - 1] && (0, _utils.isTypeOfJestFnCall)(node, context, ['hook'])) {
context.report({
messageId: 'noHookOnTop',
node
});
}
hooksContext.push(false);
},
'CallExpression:exit'() {
hooksContext.pop();
}
};
}
});

View File

@@ -0,0 +1,116 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("./utils");
const hasStringAsFirstArgument = node => node.arguments[0] && (0, _utils.isStringNode)(node.arguments[0]);
const populateIgnores = ignore => {
const ignores = [];
if (ignore.includes(_utils.DescribeAlias.describe)) {
ignores.push(...Object.keys(_utils.DescribeAlias));
}
if (ignore.includes(_utils.TestCaseName.test)) {
ignores.push(...Object.keys(_utils.TestCaseName).filter(k => k.endsWith(_utils.TestCaseName.test)));
}
if (ignore.includes(_utils.TestCaseName.it)) {
ignores.push(...Object.keys(_utils.TestCaseName).filter(k => k.endsWith(_utils.TestCaseName.it)));
}
return ignores;
};
var _default = exports.default = (0, _utils.createRule)({
name: __filename,
meta: {
type: 'suggestion',
docs: {
description: 'Enforce lowercase test names',
category: 'Best Practices',
recommended: false
},
fixable: 'code',
messages: {
unexpectedLowercase: '`{{ method }}`s should begin with lowercase'
},
schema: [{
type: 'object',
properties: {
ignore: {
type: 'array',
items: {
enum: [_utils.DescribeAlias.describe, _utils.TestCaseName.test, _utils.TestCaseName.it]
},
additionalItems: false
},
allowedPrefixes: {
type: 'array',
items: {
type: 'string'
},
additionalItems: false
},
ignoreTopLevelDescribe: {
type: 'boolean',
default: false
}
},
additionalProperties: false
}]
},
defaultOptions: [{
ignore: [],
allowedPrefixes: [],
ignoreTopLevelDescribe: false
}],
create(context, [{
ignore = [],
allowedPrefixes = [],
ignoreTopLevelDescribe
}]) {
const ignores = populateIgnores(ignore);
let numberOfDescribeBlocks = 0;
return {
CallExpression(node) {
const jestFnCall = (0, _utils.parseJestFnCall)(node, context);
if (!jestFnCall || !hasStringAsFirstArgument(node)) {
return;
}
if (jestFnCall.type === 'describe') {
numberOfDescribeBlocks++;
if (ignoreTopLevelDescribe && numberOfDescribeBlocks === 1) {
return;
}
} else if (jestFnCall.type !== 'test') {
return;
}
const [firstArg] = node.arguments;
const description = (0, _utils.getStringValue)(firstArg);
if (allowedPrefixes.some(name => description.startsWith(name))) {
return;
}
const firstCharacter = description.charAt(0);
if (!firstCharacter || firstCharacter === firstCharacter.toLowerCase() || ignores.includes(jestFnCall.name)) {
return;
}
context.report({
messageId: 'unexpectedLowercase',
node: node.arguments[0],
data: {
method: jestFnCall.name
},
fix(fixer) {
const description = (0, _utils.getStringValue)(firstArg);
const rangeIgnoringQuotes = [firstArg.range[0] + 1, firstArg.range[1] - 1];
const newDescription = description.substring(0, 1).toLowerCase() + description.substring(1);
return [fixer.replaceTextRange(rangeIgnoringQuotes, newDescription)];
}
});
},
'CallExpression:exit'(node) {
if ((0, _utils.isTypeOfJestFnCall)(node, context, ['describe'])) {
numberOfDescribeBlocks--;
}
}
};
}
});

View File

@@ -0,0 +1,89 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
const withOnce = (name, addOnce) => {
return `${name}${addOnce ? 'Once' : ''}`;
};
const findSingleReturnArgumentNode = fnNode => {
var _fnNode$body$body$;
if (fnNode.body.type !== _utils.AST_NODE_TYPES.BlockStatement) {
return fnNode.body;
}
if (((_fnNode$body$body$ = fnNode.body.body[0]) === null || _fnNode$body$body$ === void 0 ? void 0 : _fnNode$body$body$.type) === _utils.AST_NODE_TYPES.ReturnStatement) {
return fnNode.body.body[0].argument;
}
return null;
};
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Prefer mock resolved/rejected shorthands for promises',
recommended: false
},
messages: {
useMockShorthand: 'Prefer {{ replacement }}'
},
schema: [],
type: 'suggestion',
fixable: 'code'
},
defaultOptions: [],
create(context) {
const report = (property, isOnce, outerArgNode, innerArgNode = outerArgNode) => {
if ((innerArgNode === null || innerArgNode === void 0 ? void 0 : innerArgNode.type) !== _utils.AST_NODE_TYPES.CallExpression) {
return;
}
const argName = (0, _utils2.getNodeName)(innerArgNode);
if (argName !== 'Promise.resolve' && argName !== 'Promise.reject') {
return;
}
const replacement = withOnce(argName.endsWith('reject') ? 'mockRejectedValue' : 'mockResolvedValue', isOnce);
context.report({
node: property,
messageId: 'useMockShorthand',
data: {
replacement
},
fix(fixer) {
const sourceCode = (0, _utils2.getSourceCode)(context);
// there shouldn't be more than one argument, but if there is don't try
// fixing since we have no idea what to do with the extra arguments
if (innerArgNode.arguments.length > 1) {
return null;
}
return [fixer.replaceText(property, replacement), fixer.replaceText(outerArgNode,
// the value argument for both Promise methods is optional,
// whereas for Jest they're required so use an explicit undefined
// if no argument is being passed to the call we're replacing
innerArgNode.arguments.length === 1 ? sourceCode.getText(innerArgNode.arguments[0]) : 'undefined')];
}
});
};
return {
CallExpression(node) {
if (node.callee.type !== _utils.AST_NODE_TYPES.MemberExpression || !(0, _utils2.isSupportedAccessor)(node.callee.property) || node.arguments.length === 0) {
return;
}
const mockFnName = (0, _utils2.getAccessorValue)(node.callee.property);
const isOnce = mockFnName.endsWith('Once');
if (mockFnName === withOnce('mockReturnValue', isOnce)) {
report(node.callee.property, isOnce, node.arguments[0]);
} else if (mockFnName === withOnce('mockImplementation', isOnce)) {
const [arg] = node.arguments;
if (!(0, _utils2.isFunction)(arg) || arg.params.length !== 0) {
return;
}
report(node.callee.property, isOnce, arg, findSingleReturnArgumentNode(arg));
}
}
};
}
});

View File

@@ -0,0 +1,113 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("./utils");
const snapshotMatchers = ['toMatchSnapshot', 'toThrowErrorMatchingSnapshot'];
const snapshotMatcherNames = snapshotMatchers;
const isSnapshotMatcherWithoutHint = expectFnCall => {
if (expectFnCall.args.length === 0) {
return true;
}
// this matcher only supports one argument which is the hint
if (!(0, _utils.isSupportedAccessor)(expectFnCall.matcher, 'toMatchSnapshot')) {
return expectFnCall.args.length !== 1;
}
// if we're being passed two arguments,
// the second one should be the hint
if (expectFnCall.args.length === 2) {
return false;
}
const [arg] = expectFnCall.args;
// the first argument to `toMatchSnapshot` can be _either_ a snapshot hint or
// an object with asymmetric matchers, so we can't just assume that the first
// argument is a hint when it's by itself.
return !(0, _utils.isStringNode)(arg);
};
const messages = {
missingHint: 'You should provide a hint for this snapshot'
};
var _default = exports.default = (0, _utils.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Prefer including a hint with external snapshots',
recommended: false
},
messages,
type: 'suggestion',
schema: [{
type: 'string',
enum: ['always', 'multi']
}]
},
defaultOptions: ['multi'],
create(context, [mode]) {
const snapshotMatchers = [];
const depths = [];
let expressionDepth = 0;
const reportSnapshotMatchersWithoutHints = () => {
for (const snapshotMatcher of snapshotMatchers) {
if (isSnapshotMatcherWithoutHint(snapshotMatcher)) {
context.report({
messageId: 'missingHint',
node: snapshotMatcher.matcher
});
}
}
};
const enterExpression = () => {
expressionDepth++;
};
const exitExpression = () => {
expressionDepth--;
if (mode === 'always') {
reportSnapshotMatchersWithoutHints();
snapshotMatchers.length = 0;
}
if (mode === 'multi' && expressionDepth === 0) {
if (snapshotMatchers.length > 1) {
reportSnapshotMatchersWithoutHints();
}
snapshotMatchers.length = 0;
}
};
return {
'Program:exit'() {
enterExpression();
exitExpression();
},
FunctionExpression: enterExpression,
'FunctionExpression:exit': exitExpression,
ArrowFunctionExpression: enterExpression,
'ArrowFunctionExpression:exit': exitExpression,
'CallExpression:exit'(node) {
if ((0, _utils.isTypeOfJestFnCall)(node, context, ['describe', 'test'])) {
/* istanbul ignore next */
expressionDepth = depths.pop() ?? 0;
}
},
CallExpression(node) {
const jestFnCall = (0, _utils.parseJestFnCall)(node, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'expect') {
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) === 'describe' || (jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) === 'test') {
depths.push(expressionDepth);
expressionDepth = 0;
}
return;
}
const matcherName = (0, _utils.getAccessorValue)(jestFnCall.matcher);
if (!snapshotMatcherNames.includes(matcherName)) {
return;
}
snapshotMatchers.push(jestFnCall);
}
};
}
});

View File

@@ -0,0 +1,83 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
const findNodeObject = node => {
if ('object' in node) {
return node.object;
}
if (node.callee.type === _utils.AST_NODE_TYPES.MemberExpression) {
return node.callee.object;
}
return null;
};
const getJestFnCall = node => {
if (node.type !== _utils.AST_NODE_TYPES.CallExpression && node.type !== _utils.AST_NODE_TYPES.MemberExpression) {
return null;
}
const obj = findNodeObject(node);
if (!obj) {
return null;
}
if (obj.type === _utils.AST_NODE_TYPES.Identifier) {
return node.type === _utils.AST_NODE_TYPES.CallExpression && (0, _utils2.getNodeName)(node.callee) === 'jest.fn' ? node : null;
}
return getJestFnCall(obj);
};
const getAutoFixMockImplementation = (jestFnCall, context) => {
var _jestFnCall$parent;
const hasMockImplementationAlready = ((_jestFnCall$parent = jestFnCall.parent) === null || _jestFnCall$parent === void 0 ? void 0 : _jestFnCall$parent.type) === _utils.AST_NODE_TYPES.MemberExpression && jestFnCall.parent.property.type === _utils.AST_NODE_TYPES.Identifier && jestFnCall.parent.property.name === 'mockImplementation';
if (hasMockImplementationAlready) {
return '';
}
const [arg] = jestFnCall.arguments;
const argSource = arg && (0, _utils2.getSourceCode)(context).getText(arg);
return argSource ? `.mockImplementation(${argSource})` : '.mockImplementation()';
};
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Suggest using `jest.spyOn()`',
recommended: false
},
messages: {
useJestSpyOn: 'Use jest.spyOn() instead'
},
fixable: 'code',
schema: [],
type: 'suggestion'
},
defaultOptions: [],
create(context) {
return {
AssignmentExpression(node) {
const {
left,
right
} = node;
if (left.type !== _utils.AST_NODE_TYPES.MemberExpression) {
return;
}
const jestFnCall = getJestFnCall(right);
if (!jestFnCall) {
return;
}
context.report({
node,
messageId: 'useJestSpyOn',
fix(fixer) {
const leftPropQuote = left.property.type === _utils.AST_NODE_TYPES.Identifier && !left.computed ? "'" : '';
const mockImplementation = getAutoFixMockImplementation(jestFnCall, context);
return [fixer.insertTextBefore(left, `jest.spyOn(`), fixer.replaceTextRange([left.object.range[1], left.property.range[0]], `, ${leftPropQuote}`), fixer.replaceTextRange([left.property.range[1], jestFnCall.range[1]], `${leftPropQuote})${mockImplementation}`)];
}
});
}
};
}
});

View File

@@ -0,0 +1,48 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("./utils");
var _default = exports.default = (0, _utils.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Suggest using `toStrictEqual()`',
recommended: false
},
messages: {
useToStrictEqual: 'Use `toStrictEqual()` instead',
suggestReplaceWithStrictEqual: 'Replace with `toStrictEqual()`'
},
type: 'suggestion',
schema: [],
hasSuggestions: true
},
defaultOptions: [],
create(context) {
return {
CallExpression(node) {
const jestFnCall = (0, _utils.parseJestFnCall)(node, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'expect') {
return;
}
const {
matcher
} = jestFnCall;
if ((0, _utils.isSupportedAccessor)(matcher, 'toEqual')) {
context.report({
messageId: 'useToStrictEqual',
node: matcher,
suggest: [{
messageId: 'suggestReplaceWithStrictEqual',
fix: fixer => [(0, _utils.replaceAccessorFixer)(fixer, matcher, _utils.EqualityMatcher.toStrictEqual)]
}]
});
}
}
};
}
});

View File

@@ -0,0 +1,101 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
const isNullLiteral = node => node.type === _utils.AST_NODE_TYPES.Literal && node.value === null;
/**
* Checks if the given `ParsedEqualityMatcherCall` is a call to one of the equality matchers,
* with a `null` literal as the sole argument.
*/
const isNullEqualityMatcher = expectFnCall => isNullLiteral((0, _utils2.getFirstMatcherArg)(expectFnCall));
const isFirstArgumentIdentifier = (expectFnCall, name) => (0, _utils2.isIdentifier)((0, _utils2.getFirstMatcherArg)(expectFnCall), name);
const shouldUseToBe = expectFnCall => {
let firstArg = (0, _utils2.getFirstMatcherArg)(expectFnCall);
if (firstArg.type === _utils.AST_NODE_TYPES.UnaryExpression && firstArg.operator === '-') {
firstArg = firstArg.argument;
}
if (firstArg.type === _utils.AST_NODE_TYPES.Literal) {
// regex literals are classed as literals, but they're actually objects
// which means "toBe" will give different results than other matchers
return !('regex' in firstArg);
}
return firstArg.type === _utils.AST_NODE_TYPES.TemplateLiteral;
};
const reportPreferToBe = (context, whatToBe, expectFnCall, func, modifierNode) => {
context.report({
messageId: `useToBe${whatToBe}`,
fix(fixer) {
var _expectFnCall$args;
const fixes = [(0, _utils2.replaceAccessorFixer)(fixer, expectFnCall.matcher, `toBe${whatToBe}`)];
if ((_expectFnCall$args = expectFnCall.args) !== null && _expectFnCall$args !== void 0 && _expectFnCall$args.length && whatToBe !== '') {
fixes.push((0, _utils2.removeExtraArgumentsFixer)(fixer, context, func, 0));
}
if (modifierNode) {
fixes.push(fixer.removeRange([modifierNode.range[0] - 1, modifierNode.range[1]]));
}
return fixes;
},
node: expectFnCall.matcher
});
};
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Suggest using `toBe()` for primitive literals',
recommended: false
},
messages: {
useToBe: 'Use `toBe` when expecting primitive literals',
useToBeUndefined: 'Use `toBeUndefined` instead',
useToBeDefined: 'Use `toBeDefined` instead',
useToBeNull: 'Use `toBeNull` instead',
useToBeNaN: 'Use `toBeNaN` instead'
},
fixable: 'code',
type: 'suggestion',
schema: []
},
defaultOptions: [],
create(context) {
return {
CallExpression(node) {
const jestFnCall = (0, _utils2.parseJestFnCall)(node, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'expect') {
return;
}
const matcherName = (0, _utils2.getAccessorValue)(jestFnCall.matcher);
const notModifier = jestFnCall.modifiers.find(nod => (0, _utils2.getAccessorValue)(nod) === 'not');
if (notModifier && ['toBeUndefined', 'toBeDefined'].includes(matcherName)) {
reportPreferToBe(context, matcherName === 'toBeDefined' ? 'Undefined' : 'Defined', jestFnCall, node, notModifier);
return;
}
if (!_utils2.EqualityMatcher.hasOwnProperty(matcherName) || jestFnCall.args.length === 0) {
return;
}
if (isNullEqualityMatcher(jestFnCall)) {
reportPreferToBe(context, 'Null', jestFnCall, node);
return;
}
if (isFirstArgumentIdentifier(jestFnCall, 'undefined')) {
const name = notModifier ? 'Defined' : 'Undefined';
reportPreferToBe(context, name, jestFnCall, node, notModifier);
return;
}
if (isFirstArgumentIdentifier(jestFnCall, 'NaN')) {
reportPreferToBe(context, 'NaN', jestFnCall, node);
return;
}
if (shouldUseToBe(jestFnCall) && matcherName !== _utils2.EqualityMatcher.toBe) {
reportPreferToBe(context, '', jestFnCall, node);
}
}
};
}
});

View File

@@ -0,0 +1,82 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
/**
* Checks if the given `node` is a `CallExpression` representing the calling
* of an `includes`-like method that can be 'fixed' (using `toContain`).
*
* @param {CallExpression} node
*
* @return {node is FixableIncludesCallExpression}
*/
const isFixableIncludesCallExpression = node => node.type === _utils.AST_NODE_TYPES.CallExpression && node.callee.type === _utils.AST_NODE_TYPES.MemberExpression && (0, _utils2.isSupportedAccessor)(node.callee.property, 'includes') && (0, _utils2.hasOnlyOneArgument)(node) && node.arguments[0].type !== _utils.AST_NODE_TYPES.SpreadElement;
// expect(array.includes(<value>)[not.]{toBe,toEqual}(<boolean>)
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Suggest using `toContain()`',
recommended: false
},
messages: {
useToContain: 'Use toContain() instead'
},
fixable: 'code',
type: 'suggestion',
schema: []
},
defaultOptions: [],
create(context) {
return {
CallExpression(node) {
const jestFnCall = (0, _utils2.parseJestFnCall)(node, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'expect' || jestFnCall.args.length === 0) {
return;
}
const {
parent: expect
} = jestFnCall.head.node;
if ((expect === null || expect === void 0 ? void 0 : expect.type) !== _utils.AST_NODE_TYPES.CallExpression) {
return;
}
const {
arguments: [includesCall],
range: [, expectCallEnd]
} = expect;
const {
matcher
} = jestFnCall;
const matcherArg = (0, _utils2.getFirstMatcherArg)(jestFnCall);
if (!includesCall || matcherArg.type === _utils.AST_NODE_TYPES.SpreadElement || !_utils2.EqualityMatcher.hasOwnProperty((0, _utils2.getAccessorValue)(matcher)) || !(0, _utils2.isBooleanLiteral)(matcherArg) || !isFixableIncludesCallExpression(includesCall)) {
return;
}
const hasNot = jestFnCall.modifiers.some(nod => (0, _utils2.getAccessorValue)(nod) === 'not');
context.report({
fix(fixer) {
const sourceCode = (0, _utils2.getSourceCode)(context);
// we need to negate the expectation if the current expected
// value is itself negated by the "not" modifier
const addNotModifier = matcherArg.value === hasNot;
return [
// remove the "includes" call entirely
fixer.removeRange([includesCall.callee.property.range[0] - 1, includesCall.range[1]]),
// replace the current matcher with "toContain", adding "not" if needed
fixer.replaceTextRange([expectCallEnd, matcher.parent.range[1]], addNotModifier ? `.${_utils2.ModifierName.not}.toContain` : '.toContain'),
// replace the matcher argument with the value from the "includes"
fixer.replaceText(jestFnCall.args[0], sourceCode.getText(includesCall.arguments[0]))];
},
messageId: 'useToContain',
node: matcher
});
}
};
}
});

View File

@@ -0,0 +1,59 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Suggest using `toHaveLength()`',
recommended: false
},
messages: {
useToHaveLength: 'Use toHaveLength() instead'
},
fixable: 'code',
type: 'suggestion',
schema: []
},
defaultOptions: [],
create(context) {
return {
CallExpression(node) {
const jestFnCall = (0, _utils2.parseJestFnCall)(node, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'expect') {
return;
}
const {
parent: expect
} = jestFnCall.head.node;
if ((expect === null || expect === void 0 ? void 0 : expect.type) !== _utils.AST_NODE_TYPES.CallExpression) {
return;
}
const [argument] = expect.arguments;
const {
matcher
} = jestFnCall;
if (!_utils2.EqualityMatcher.hasOwnProperty((0, _utils2.getAccessorValue)(matcher)) || (argument === null || argument === void 0 ? void 0 : argument.type) !== _utils.AST_NODE_TYPES.MemberExpression || !(0, _utils2.isSupportedAccessor)(argument.property, 'length')) {
return;
}
context.report({
fix(fixer) {
return [
// remove the "length" property accessor
fixer.removeRange([argument.property.range[0] - 1, argument.range[1]]),
// replace the current matcher with "toHaveLength"
fixer.replaceTextRange([matcher.parent.object.range[1], matcher.parent.range[1]], '.toHaveLength')];
},
messageId: 'useToHaveLength',
node: matcher
});
}
};
}
});

View File

@@ -0,0 +1,74 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
function isEmptyFunction(node) {
if (!(0, _utils2.isFunction)(node)) {
return false;
}
return node.body.type === _utils.AST_NODE_TYPES.BlockStatement && !node.body.body.length;
}
function createTodoFixer(jestFnCall, fixer) {
if (jestFnCall.members.length) {
return (0, _utils2.replaceAccessorFixer)(fixer, jestFnCall.members[0], 'todo');
}
return fixer.replaceText(jestFnCall.head.node, `${jestFnCall.head.local}.todo`);
}
const isTargetedTestCase = jestFnCall => {
if (jestFnCall.members.some(s => (0, _utils2.getAccessorValue)(s) !== 'skip')) {
return false;
}
// todo: we should support this too (needs custom fixer)
if (jestFnCall.name.startsWith('x')) {
return false;
}
return !jestFnCall.name.startsWith('f');
};
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Suggest using `test.todo`',
recommended: false
},
messages: {
emptyTest: 'Prefer todo test case over empty test case',
unimplementedTest: 'Prefer todo test case over unimplemented test case'
},
fixable: 'code',
schema: [],
type: 'layout'
},
defaultOptions: [],
create(context) {
return {
CallExpression(node) {
const [title, callback] = node.arguments;
const jestFnCall = (0, _utils2.parseJestFnCall)(node, context);
if (!title || (jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'test' || !isTargetedTestCase(jestFnCall) || !(0, _utils2.isStringNode)(title)) {
return;
}
if (callback && isEmptyFunction(callback)) {
context.report({
messageId: 'emptyTest',
node,
fix: fixer => [fixer.removeRange([title.range[1], callback.range[1]]), createTodoFixer(jestFnCall, fixer)]
});
}
if ((0, _utils2.hasOnlyOneArgument)(node)) {
context.report({
messageId: 'unimplementedTest',
node,
fix: fixer => createTodoFixer(jestFnCall, fixer)
});
}
}
};
}
});

View File

@@ -0,0 +1,96 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
const isJestFnCall = (node, context) => {
var _getNodeName;
if ((0, _utils2.parseJestFnCall)(node, context)) {
return true;
}
return !!((_getNodeName = (0, _utils2.getNodeName)(node)) !== null && _getNodeName !== void 0 && _getNodeName.startsWith('jest.'));
};
const isNullOrUndefined = node => {
return node.type === _utils.AST_NODE_TYPES.Literal && node.value === null || (0, _utils2.isIdentifier)(node, 'undefined');
};
const shouldBeInHook = (node, context, allowedFunctionCalls = []) => {
switch (node.type) {
case _utils.AST_NODE_TYPES.ExpressionStatement:
return shouldBeInHook(node.expression, context, allowedFunctionCalls);
case _utils.AST_NODE_TYPES.CallExpression:
return !(isJestFnCall(node, context) || allowedFunctionCalls.includes((0, _utils2.getNodeName)(node)));
case _utils.AST_NODE_TYPES.VariableDeclaration:
{
if (node.kind === 'const') {
return false;
}
return node.declarations.some(({
init
}) => init !== null && !isNullOrUndefined(init));
}
default:
return false;
}
};
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Require setup and teardown code to be within a hook',
recommended: false
},
messages: {
useHook: 'This should be done within a hook'
},
type: 'suggestion',
schema: [{
type: 'object',
properties: {
allowedFunctionCalls: {
type: 'array',
items: {
type: 'string'
}
}
},
additionalProperties: false
}]
},
defaultOptions: [{
allowedFunctionCalls: []
}],
create(context) {
const {
allowedFunctionCalls
} = context.options[0] ?? {};
const checkBlockBody = body => {
for (const statement of body) {
if (shouldBeInHook(statement, context, allowedFunctionCalls)) {
context.report({
node: statement,
messageId: 'useHook'
});
}
}
};
return {
Program(program) {
checkBlockBody(program.body);
},
CallExpression(node) {
if (!(0, _utils2.isTypeOfJestFnCall)(node, context, ['describe']) || node.arguments.length < 2) {
return;
}
const [, testFn] = node.arguments;
if (!(0, _utils2.isFunction)(testFn) || testFn.body.type !== _utils.AST_NODE_TYPES.BlockStatement) {
return;
}
checkBlockBody(testFn.body.body);
}
};
}
});

View File

@@ -0,0 +1,47 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("./utils");
var _default = exports.default = (0, _utils.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Require a message for `toThrow()`',
recommended: false
},
messages: {
addErrorMessage: 'Add an error message to {{ matcherName }}()'
},
type: 'suggestion',
schema: []
},
defaultOptions: [],
create(context) {
return {
CallExpression(node) {
const jestFnCall = (0, _utils.parseJestFnCall)(node, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'expect') {
return;
}
const {
matcher
} = jestFnCall;
const matcherName = (0, _utils.getAccessorValue)(matcher);
if (jestFnCall.args.length === 0 && ['toThrow', 'toThrowError'].includes(matcherName) && !jestFnCall.modifiers.some(nod => (0, _utils.getAccessorValue)(nod) === 'not')) {
// Look for `toThrow` calls with no arguments.
context.report({
messageId: 'addErrorMessage',
data: {
matcherName
},
node: matcher
});
}
}
};
}
});

View File

@@ -0,0 +1,88 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("./utils");
const messages = {
tooManyDescribes: 'There should not be more than {{ max }} describe{{ s }} at the top level',
unexpectedTestCase: 'All test cases must be wrapped in a describe block.',
unexpectedHook: 'All hooks must be wrapped in a describe block.'
};
var _default = exports.default = (0, _utils.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Require test cases and hooks to be inside a `describe` block',
recommended: false
},
messages,
type: 'suggestion',
schema: [{
type: 'object',
properties: {
maxNumberOfTopLevelDescribes: {
type: 'number',
minimum: 1
}
},
additionalProperties: false
}]
},
defaultOptions: [{}],
create(context) {
const {
maxNumberOfTopLevelDescribes = Infinity
} = context.options[0] ?? {};
let numberOfTopLevelDescribeBlocks = 0;
let numberOfDescribeBlocks = 0;
return {
CallExpression(node) {
const jestFnCall = (0, _utils.parseJestFnCall)(node, context);
if (!jestFnCall) {
return;
}
if (jestFnCall.type === 'describe') {
numberOfDescribeBlocks++;
if (numberOfDescribeBlocks === 1) {
numberOfTopLevelDescribeBlocks++;
if (numberOfTopLevelDescribeBlocks > maxNumberOfTopLevelDescribes) {
context.report({
node,
messageId: 'tooManyDescribes',
data: {
max: maxNumberOfTopLevelDescribes,
s: maxNumberOfTopLevelDescribes === 1 ? '' : 's'
}
});
}
}
return;
}
if (numberOfDescribeBlocks === 0) {
if (jestFnCall.type === 'test') {
context.report({
node,
messageId: 'unexpectedTestCase'
});
return;
}
if (jestFnCall.type === 'hook') {
context.report({
node,
messageId: 'unexpectedHook'
});
return;
}
}
},
'CallExpression:exit'(node) {
if ((0, _utils.isTypeOfJestFnCall)(node, context, ['describe'])) {
numberOfDescribeBlocks--;
}
}
};
}
});

View File

@@ -0,0 +1,70 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
const toThrowMatchers = ['toThrow', 'toThrowError', 'toThrowErrorMatchingSnapshot', 'toThrowErrorMatchingInlineSnapshot'];
const baseRule = (() => {
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const TSESLintPlugin = require('@typescript-eslint/eslint-plugin');
return TSESLintPlugin.rules['unbound-method'];
} catch (e) {
const error = e;
if (error.code === 'MODULE_NOT_FOUND') {
return null;
}
throw error;
}
})();
const DEFAULT_MESSAGE = 'This rule requires `@typescript-eslint/eslint-plugin`';
var _default = exports.default = (0, _utils2.createRule)({
defaultOptions: [{
ignoreStatic: false
}],
...baseRule,
name: __filename,
meta: {
messages: {
unbound: DEFAULT_MESSAGE,
unboundWithoutThisAnnotation: DEFAULT_MESSAGE
},
schema: [],
type: 'problem',
...(baseRule === null || baseRule === void 0 ? void 0 : baseRule.meta),
docs: {
category: 'Best Practices',
description: 'Enforce unbound methods are called with their expected scope',
requiresTypeChecking: true,
...(baseRule === null || baseRule === void 0 ? void 0 : baseRule.meta.docs),
recommended: false
}
},
create(context) {
const baseSelectors = baseRule === null || baseRule === void 0 ? void 0 : baseRule.create(context);
if (!baseSelectors) {
return {};
}
return {
...baseSelectors,
MemberExpression(node) {
var _node$parent, _baseSelectors$Member;
if (((_node$parent = node.parent) === null || _node$parent === void 0 ? void 0 : _node$parent.type) === _utils.AST_NODE_TYPES.CallExpression) {
const jestFnCall = (0, _utils2.parseJestFnCall)((0, _utils2.findTopMostCallExpression)(node.parent), context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) === 'expect') {
const {
matcher
} = jestFnCall;
if (!toThrowMatchers.includes((0, _utils2.getAccessorValue)(matcher))) {
return;
}
}
}
(_baseSelectors$Member = baseSelectors.MemberExpression) === null || _baseSelectors$Member === void 0 || _baseSelectors$Member.call(baseSelectors, node);
}
};
}
});

View File

@@ -0,0 +1,127 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.isSupportedAccessor = exports.isStringNode = exports.isIdentifier = exports.getStringValue = exports.getAccessorValue = void 0;
var _utils = require("@typescript-eslint/utils");
/**
* A `Literal` with a `value` of type `string`.
*/
/**
* Checks if the given `node` is a `StringLiteral`.
*
* If a `value` is provided & the `node` is a `StringLiteral`,
* the `value` will be compared to that of the `StringLiteral`.
*
* @param {Node} node
* @param {V} [value]
*
* @return {node is StringLiteral<V>}
*
* @template V
*/
const isStringLiteral = (node, value) => node.type === _utils.AST_NODE_TYPES.Literal && typeof node.value === 'string' && (value === undefined || node.value === value);
/**
* Checks if the given `node` is a `TemplateLiteral`.
*
* Complex `TemplateLiteral`s are not considered specific, and so will return `false`.
*
* If a `value` is provided & the `node` is a `TemplateLiteral`,
* the `value` will be compared to that of the `TemplateLiteral`.
*
* @param {Node} node
* @param {V} [value]
*
* @return {node is TemplateLiteral<V>}
*
* @template V
*/
const isTemplateLiteral = (node, value) => node.type === _utils.AST_NODE_TYPES.TemplateLiteral && node.quasis.length === 1 && (
// bail out if not simple
value === undefined || node.quasis[0].value.raw === value);
/**
* Checks if the given `node` is a {@link StringNode}.
*
* @param {Node} node
* @param {V} [specifics]
*
* @return {node is StringNode}
*
* @template V
*/
const isStringNode = (node, specifics) => isStringLiteral(node, specifics) || isTemplateLiteral(node, specifics);
/**
* Gets the value of the given `StringNode`.
*
* If the `node` is a `TemplateLiteral`, the `raw` value is used;
* otherwise, `value` is returned instead.
*
* @param {StringNode<S>} node
*
* @return {S}
*
* @template S
*/
exports.isStringNode = isStringNode;
const getStringValue = node => isTemplateLiteral(node) ? node.quasis[0].value.raw : node.value;
/**
* An `Identifier` with a known `name` value - i.e `expect`.
*/
exports.getStringValue = getStringValue;
/**
* Checks if the given `node` is an `Identifier`.
*
* If a `name` is provided, & the `node` is an `Identifier`,
* the `name` will be compared to that of the `identifier`.
*
* @param {Node} node
* @param {V} [name]
*
* @return {node is KnownIdentifier<Name>}
*
* @template V
*/
const isIdentifier = (node, name) => node.type === _utils.AST_NODE_TYPES.Identifier && (name === undefined || node.name === name);
/**
* Checks if the given `node` is a "supported accessor".
*
* This means that it's a node can be used to access properties,
* and who's "value" can be statically determined.
*
* `MemberExpression` nodes most commonly contain accessors,
* but it's possible for other nodes to contain them.
*
* If a `value` is provided & the `node` is an `AccessorNode`,
* the `value` will be compared to that of the `AccessorNode`.
*
* Note that `value` here refers to the normalised value.
* The property that holds the value is not always called `name`.
*
* @param {Node} node
* @param {V} [value]
*
* @return {node is AccessorNode<V>}
*
* @template V
*/
exports.isIdentifier = isIdentifier;
const isSupportedAccessor = (node, value) => isIdentifier(node, value) || isStringNode(node, value);
/**
* Gets the value of the given `AccessorNode`,
* account for the different node types.
*
* @param {AccessorNode<S>} accessor
*
* @return {S}
*
* @template S
*/
exports.isSupportedAccessor = isSupportedAccessor;
const getAccessorValue = accessor => accessor.type === _utils.AST_NODE_TYPES.Identifier ? accessor.name : getStringValue(accessor);
exports.getAccessorValue = getAccessorValue;

View File

@@ -0,0 +1,24 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.detectJestVersion = void 0;
let cachedJestVersion = null;
const detectJestVersion = () => {
if (cachedJestVersion) {
return cachedJestVersion;
}
try {
const jestPath = require.resolve('jest/package.json');
const jestPackageJson =
// eslint-disable-next-line @typescript-eslint/no-require-imports
require(jestPath);
if (jestPackageJson.version) {
const [majorVersion] = jestPackageJson.version.split('.');
return cachedJestVersion = parseInt(majorVersion, 10);
}
} catch {}
throw new Error('Unable to detect Jest version - please ensure jest package is installed, or otherwise set version explicitly');
};
exports.detectJestVersion = detectJestVersion;

View File

@@ -0,0 +1,10 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.followTypeAssertionChain = void 0;
var _utils = require("@typescript-eslint/utils");
const isTypeCastExpression = node => node.type === _utils.AST_NODE_TYPES.TSAsExpression || node.type === _utils.AST_NODE_TYPES.TSTypeAssertion;
const followTypeAssertionChain = expression => isTypeCastExpression(expression) ? followTypeAssertionChain(expression.expression) : expression;
exports.followTypeAssertionChain = followTypeAssertionChain;

View File

@@ -0,0 +1,60 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _accessors = require("./accessors");
Object.keys(_accessors).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
if (key in exports && exports[key] === _accessors[key]) return;
Object.defineProperty(exports, key, {
enumerable: true,
get: function () {
return _accessors[key];
}
});
});
var _detectJestVersion = require("./detectJestVersion");
Object.keys(_detectJestVersion).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
if (key in exports && exports[key] === _detectJestVersion[key]) return;
Object.defineProperty(exports, key, {
enumerable: true,
get: function () {
return _detectJestVersion[key];
}
});
});
var _followTypeAssertionChain = require("./followTypeAssertionChain");
Object.keys(_followTypeAssertionChain).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
if (key in exports && exports[key] === _followTypeAssertionChain[key]) return;
Object.defineProperty(exports, key, {
enumerable: true,
get: function () {
return _followTypeAssertionChain[key];
}
});
});
var _misc = require("./misc");
Object.keys(_misc).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
if (key in exports && exports[key] === _misc[key]) return;
Object.defineProperty(exports, key, {
enumerable: true,
get: function () {
return _misc[key];
}
});
});
var _parseJestFnCall = require("./parseJestFnCall");
Object.keys(_parseJestFnCall).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
if (key in exports && exports[key] === _parseJestFnCall[key]) return;
Object.defineProperty(exports, key, {
enumerable: true,
get: function () {
return _parseJestFnCall[key];
}
});
});

201
node_modules/eslint-plugin-jest/lib/rules/utils/misc.js generated vendored Normal file
View File

@@ -0,0 +1,201 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.getFirstMatcherArg = exports.getFilename = exports.getDeclaredVariables = exports.getAncestors = exports.findTopMostCallExpression = exports.createRule = exports.TestCaseName = exports.ModifierName = exports.HookName = exports.EqualityMatcher = exports.DescribeAlias = void 0;
exports.getNodeName = getNodeName;
exports.replaceAccessorFixer = exports.removeExtraArgumentsFixer = exports.isFunction = exports.isBooleanLiteral = exports.hasOnlyOneArgument = exports.getTestCallExpressionsFromDeclaredVariables = exports.getSourceCode = exports.getScope = void 0;
var _path = require("path");
var _utils = require("@typescript-eslint/utils");
var _package = require("../../../package.json");
var _accessors = require("./accessors");
var _followTypeAssertionChain = require("./followTypeAssertionChain");
var _parseJestFnCall = require("./parseJestFnCall");
const REPO_URL = 'https://github.com/jest-community/eslint-plugin-jest';
const createRule = exports.createRule = _utils.ESLintUtils.RuleCreator(name => {
const ruleName = (0, _path.parse)(name).name;
return `${REPO_URL}/blob/v${_package.version}/docs/rules/${ruleName}.md`;
});
/**
* Represents a `MemberExpression` with a "known" `property`.
*/
/**
* Represents a `CallExpression` with a "known" `property` accessor.
*
* i.e `KnownCallExpression<'includes'>` represents `.includes()`.
*/
/**
* Represents a `MemberExpression` with a "known" `property`, that is called.
*
* This is `KnownCallExpression` from the perspective of the `MemberExpression` node.
*/
/**
* Represents a `CallExpression` with a single argument.
*/
/**
* Guards that the given `call` has only one `argument`.
*
* @param {CallExpression} call
*
* @return {call is CallExpressionWithSingleArgument}
*/
const hasOnlyOneArgument = call => call.arguments.length === 1;
exports.hasOnlyOneArgument = hasOnlyOneArgument;
let DescribeAlias = exports.DescribeAlias = /*#__PURE__*/function (DescribeAlias) {
DescribeAlias["describe"] = "describe";
DescribeAlias["fdescribe"] = "fdescribe";
DescribeAlias["xdescribe"] = "xdescribe";
return DescribeAlias;
}({});
let TestCaseName = exports.TestCaseName = /*#__PURE__*/function (TestCaseName) {
TestCaseName["fit"] = "fit";
TestCaseName["it"] = "it";
TestCaseName["test"] = "test";
TestCaseName["xit"] = "xit";
TestCaseName["xtest"] = "xtest";
return TestCaseName;
}({});
let HookName = exports.HookName = /*#__PURE__*/function (HookName) {
HookName["beforeAll"] = "beforeAll";
HookName["beforeEach"] = "beforeEach";
HookName["afterAll"] = "afterAll";
HookName["afterEach"] = "afterEach";
return HookName;
}({});
let ModifierName = exports.ModifierName = /*#__PURE__*/function (ModifierName) {
ModifierName["not"] = "not";
ModifierName["rejects"] = "rejects";
ModifierName["resolves"] = "resolves";
return ModifierName;
}({});
let EqualityMatcher = exports.EqualityMatcher = /*#__PURE__*/function (EqualityMatcher) {
EqualityMatcher["toBe"] = "toBe";
EqualityMatcher["toEqual"] = "toEqual";
EqualityMatcher["toStrictEqual"] = "toStrictEqual";
return EqualityMatcher;
}({});
const joinNames = (a, b) => a && b ? `${a}.${b}` : null;
function getNodeName(node) {
if ((0, _accessors.isSupportedAccessor)(node)) {
return (0, _accessors.getAccessorValue)(node);
}
switch (node.type) {
case _utils.AST_NODE_TYPES.TaggedTemplateExpression:
return getNodeName(node.tag);
case _utils.AST_NODE_TYPES.MemberExpression:
return joinNames(getNodeName(node.object), getNodeName(node.property));
case _utils.AST_NODE_TYPES.NewExpression:
case _utils.AST_NODE_TYPES.CallExpression:
return getNodeName(node.callee);
}
return null;
}
const isFunction = node => node.type === _utils.AST_NODE_TYPES.FunctionExpression || node.type === _utils.AST_NODE_TYPES.ArrowFunctionExpression;
exports.isFunction = isFunction;
const getTestCallExpressionsFromDeclaredVariables = (declaredVariables, context) => {
return declaredVariables.reduce((acc, {
references
}) => acc.concat(references.map(({
identifier
}) => identifier.parent).filter(node => (node === null || node === void 0 ? void 0 : node.type) === _utils.AST_NODE_TYPES.CallExpression && (0, _parseJestFnCall.isTypeOfJestFnCall)(node, context, ['test']))), []);
};
/**
* Replaces an accessor node with the given `text`, surrounding it in quotes if required.
*
* This ensures that fixes produce valid code when replacing both dot-based and
* bracket-based property accessors.
*/
exports.getTestCallExpressionsFromDeclaredVariables = getTestCallExpressionsFromDeclaredVariables;
const replaceAccessorFixer = (fixer, node, text) => {
return fixer.replaceText(node, node.type === _utils.AST_NODE_TYPES.Identifier ? text : `'${text}'`);
};
exports.replaceAccessorFixer = replaceAccessorFixer;
const removeExtraArgumentsFixer = (fixer, context, func, from) => {
const firstArg = func.arguments[from];
const lastArg = func.arguments[func.arguments.length - 1];
const sourceCode = getSourceCode(context);
let tokenAfterLastParam = sourceCode.getTokenAfter(lastArg);
if (tokenAfterLastParam.value === ',') {
tokenAfterLastParam = sourceCode.getTokenAfter(tokenAfterLastParam);
}
return fixer.removeRange([firstArg.range[0], tokenAfterLastParam.range[0]]);
};
exports.removeExtraArgumentsFixer = removeExtraArgumentsFixer;
const findTopMostCallExpression = node => {
let topMostCallExpression = node;
let {
parent
} = node;
while (parent) {
if (parent.type === _utils.AST_NODE_TYPES.CallExpression) {
topMostCallExpression = parent;
parent = parent.parent;
continue;
}
if (parent.type !== _utils.AST_NODE_TYPES.MemberExpression) {
break;
}
parent = parent.parent;
}
return topMostCallExpression;
};
exports.findTopMostCallExpression = findTopMostCallExpression;
const isBooleanLiteral = node => node.type === _utils.AST_NODE_TYPES.Literal && typeof node.value === 'boolean';
exports.isBooleanLiteral = isBooleanLiteral;
const getFirstMatcherArg = expectFnCall => {
const [firstArg] = expectFnCall.args;
if (firstArg.type === _utils.AST_NODE_TYPES.SpreadElement) {
return firstArg;
}
return (0, _followTypeAssertionChain.followTypeAssertionChain)(firstArg);
};
/* istanbul ignore next */
exports.getFirstMatcherArg = getFirstMatcherArg;
const getFilename = context => {
return 'filename' in context ? context.filename : context.getFilename();
};
/* istanbul ignore next */
exports.getFilename = getFilename;
const getSourceCode = context => {
return 'sourceCode' in context ? context.sourceCode : context.getSourceCode();
};
/* istanbul ignore next */
exports.getSourceCode = getSourceCode;
const getScope = (context, node) => {
const sourceCode = getSourceCode(context);
if ('getScope' in sourceCode) {
return sourceCode.getScope(node);
}
return context.getScope();
};
/* istanbul ignore next */
exports.getScope = getScope;
const getAncestors = (context, node) => {
const sourceCode = getSourceCode(context);
if ('getAncestors' in sourceCode) {
return sourceCode.getAncestors(node);
}
return context.getAncestors();
};
/* istanbul ignore next */
exports.getAncestors = getAncestors;
const getDeclaredVariables = (context, node) => {
const sourceCode = getSourceCode(context);
if ('getDeclaredVariables' in sourceCode) {
return sourceCode.getDeclaredVariables(node);
}
return context.getDeclaredVariables(node);
};
exports.getDeclaredVariables = getDeclaredVariables;

View File

@@ -0,0 +1,332 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.getNodeChain = getNodeChain;
exports.resolveScope = exports.parseJestFnCallWithReason = exports.parseJestFnCall = exports.isTypeOfJestFnCall = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("../utils");
const isTypeOfJestFnCall = (node, context, types) => {
const jestFnCall = parseJestFnCall(node, context);
return jestFnCall !== null && types.includes(jestFnCall.type);
};
exports.isTypeOfJestFnCall = isTypeOfJestFnCall;
const joinChains = (a, b) => a && b ? [...a, ...b] : null;
function getNodeChain(node) {
if ((0, _utils2.isSupportedAccessor)(node)) {
return [node];
}
switch (node.type) {
case _utils.AST_NODE_TYPES.TaggedTemplateExpression:
return getNodeChain(node.tag);
case _utils.AST_NODE_TYPES.MemberExpression:
return joinChains(getNodeChain(node.object), getNodeChain(node.property));
case _utils.AST_NODE_TYPES.CallExpression:
return getNodeChain(node.callee);
}
return null;
}
const determineJestFnType = name => {
if (name === 'expect') {
return 'expect';
}
if (name === 'jest') {
return 'jest';
}
if (_utils2.DescribeAlias.hasOwnProperty(name)) {
return 'describe';
}
if (_utils2.TestCaseName.hasOwnProperty(name)) {
return 'test';
}
/* istanbul ignore else */
if (_utils2.HookName.hasOwnProperty(name)) {
return 'hook';
}
/* istanbul ignore next */
return 'unknown';
};
const ValidJestFnCallChains = ['afterAll', 'afterEach', 'beforeAll', 'beforeEach', 'describe', 'describe.each', 'describe.only', 'describe.only.each', 'describe.skip', 'describe.skip.each', 'fdescribe', 'fdescribe.each', 'xdescribe', 'xdescribe.each', 'it', 'it.concurrent', 'it.concurrent.failing', 'it.concurrent.each', 'it.concurrent.failing.each', 'it.concurrent.failing.only.each', 'it.concurrent.failing.skip.each', 'it.concurrent.only.each', 'it.concurrent.skip.each', 'it.each', 'it.failing', 'it.failing.each', 'it.only', 'it.only.each', 'it.only.failing', 'it.only.failing.each', 'it.skip', 'it.skip.each', 'it.skip.failing', 'it.skip.failing.each', 'it.todo', 'fit', 'fit.each', 'fit.failing', 'fit.failing.each', 'xit', 'xit.each', 'xit.failing', 'xit.failing.each', 'test', 'test.concurrent', 'test.concurrent.failing', 'test.concurrent.each', 'test.concurrent.failing.each', 'test.concurrent.failing.only.each', 'test.concurrent.failing.skip.each', 'test.concurrent.only.each', 'test.concurrent.skip.each', 'test.each', 'test.failing', 'test.failing.each', 'test.only', 'test.only.each', 'test.only.failing', 'test.only.failing.each', 'test.skip', 'test.skip.each', 'test.skip.failing', 'test.skip.failing.each', 'test.todo', 'xtest', 'xtest.each', 'xtest.failing', 'xtest.failing.each'];
const resolvePossibleAliasedGlobal = (global, context) => {
var _context$settings$jes;
const globalAliases = ((_context$settings$jes = context.settings.jest) === null || _context$settings$jes === void 0 ? void 0 : _context$settings$jes.globalAliases) ?? {};
const alias = Object.entries(globalAliases).find(([, aliases]) => aliases.includes(global));
if (alias) {
return alias[0];
}
return null;
};
const parseJestFnCallCache = new WeakMap();
const parseJestFnCall = (node, context) => {
const jestFnCall = parseJestFnCallWithReason(node, context);
if (typeof jestFnCall === 'string') {
return null;
}
return jestFnCall;
};
exports.parseJestFnCall = parseJestFnCall;
const parseJestFnCallWithReason = (node, context) => {
let parsedJestFnCall = parseJestFnCallCache.get(node);
if (parsedJestFnCall) {
return parsedJestFnCall;
}
parsedJestFnCall = parseJestFnCallWithReasonInner(node, context);
parseJestFnCallCache.set(node, parsedJestFnCall);
return parsedJestFnCall;
};
exports.parseJestFnCallWithReason = parseJestFnCallWithReason;
const parseJestFnCallWithReasonInner = (node, context) => {
var _node$parent2, _node$parent3;
const chain = getNodeChain(node);
if (!(chain !== null && chain !== void 0 && chain.length)) {
return null;
}
const [first, ...rest] = chain;
const lastLink = (0, _utils2.getAccessorValue)(chain[chain.length - 1]);
// if we're an `each()`, ensure we're the outer CallExpression (i.e `.each()()`)
if (lastLink === 'each') {
if (node.callee.type !== _utils.AST_NODE_TYPES.CallExpression && node.callee.type !== _utils.AST_NODE_TYPES.TaggedTemplateExpression) {
return null;
}
}
if (node.callee.type === _utils.AST_NODE_TYPES.TaggedTemplateExpression && lastLink !== 'each') {
return null;
}
const resolved = resolveToJestFn(context, first);
// we're not a jest function
if (!resolved) {
return null;
}
const name = resolved.original ?? resolved.local;
const links = [name, ...rest.map(link => (0, _utils2.getAccessorValue)(link))];
if (name !== 'jest' && name !== 'expect' && !ValidJestFnCallChains.includes(links.join('.'))) {
return null;
}
const parsedJestFnCall = {
name,
head: {
...resolved,
node: first
},
// every member node must have a member expression as their parent
// in order to be part of the call chain we're parsing
members: rest
};
const type = determineJestFnType(name);
if (type === 'expect') {
const result = parseJestExpectCall(parsedJestFnCall);
// if the `expect` call chain is not valid, only report on the topmost node
// since all members in the chain are likely to get flagged for some reason
if (typeof result === 'string' && (0, _utils2.findTopMostCallExpression)(node) !== node) {
return null;
}
if (result === 'matcher-not-found') {
var _node$parent;
if (((_node$parent = node.parent) === null || _node$parent === void 0 ? void 0 : _node$parent.type) === _utils.AST_NODE_TYPES.MemberExpression) {
return 'matcher-not-called';
}
}
return result;
}
// check that every link in the chain except the last is a member expression
if (chain.slice(0, chain.length - 1).some(nod => {
var _nod$parent;
return ((_nod$parent = nod.parent) === null || _nod$parent === void 0 ? void 0 : _nod$parent.type) !== _utils.AST_NODE_TYPES.MemberExpression;
})) {
return null;
}
// ensure that we're at the "top" of the function call chain otherwise when
// parsing e.g. x().y.z(), we'll incorrectly find & parse "x()" even though
// the full chain is not a valid jest function call chain
if (((_node$parent2 = node.parent) === null || _node$parent2 === void 0 ? void 0 : _node$parent2.type) === _utils.AST_NODE_TYPES.CallExpression || ((_node$parent3 = node.parent) === null || _node$parent3 === void 0 ? void 0 : _node$parent3.type) === _utils.AST_NODE_TYPES.MemberExpression) {
return null;
}
return {
...parsedJestFnCall,
type
};
};
const findModifiersAndMatcher = members => {
const modifiers = [];
for (const member of members) {
var _member$parent, _member$parent$parent;
// check if the member is being called, which means it is the matcher
// (and also the end of the entire "expect" call chain)
if (((_member$parent = member.parent) === null || _member$parent === void 0 ? void 0 : _member$parent.type) === _utils.AST_NODE_TYPES.MemberExpression && ((_member$parent$parent = member.parent.parent) === null || _member$parent$parent === void 0 ? void 0 : _member$parent$parent.type) === _utils.AST_NODE_TYPES.CallExpression) {
return {
matcher: member,
args: member.parent.parent.arguments,
modifiers
};
}
// otherwise, it should be a modifier
const name = (0, _utils2.getAccessorValue)(member);
if (modifiers.length === 0) {
// the first modifier can be any of the three modifiers
if (!_utils2.ModifierName.hasOwnProperty(name)) {
return 'modifier-unknown';
}
} else if (modifiers.length === 1) {
// the second modifier can only be "not"
if (name !== _utils2.ModifierName.not) {
return 'modifier-unknown';
}
const firstModifier = (0, _utils2.getAccessorValue)(modifiers[0]);
// and the first modifier has to be either "resolves" or "rejects"
if (firstModifier !== _utils2.ModifierName.resolves && firstModifier !== _utils2.ModifierName.rejects) {
return 'modifier-unknown';
}
} else {
return 'modifier-unknown';
}
modifiers.push(member);
}
// this will only really happen if there are no members
return 'matcher-not-found';
};
const parseJestExpectCall = typelessParsedJestFnCall => {
const modifiersAndMatcher = findModifiersAndMatcher(typelessParsedJestFnCall.members);
if (typeof modifiersAndMatcher === 'string') {
return modifiersAndMatcher;
}
return {
...typelessParsedJestFnCall,
type: 'expect',
...modifiersAndMatcher
};
};
const describeImportDefAsImport = def => {
if (def.parent.type === _utils.AST_NODE_TYPES.TSImportEqualsDeclaration) {
return null;
}
if (def.node.type !== _utils.AST_NODE_TYPES.ImportSpecifier) {
return null;
}
// we only care about value imports
if (def.parent.importKind === 'type') {
return null;
}
return {
source: def.parent.source.value,
imported: def.node.imported.name,
local: def.node.local.name
};
};
/**
* Attempts to find the node that represents the import source for the
* given expression node, if it looks like it's an import.
*
* If no such node can be found (e.g. because the expression doesn't look
* like an import), then `null` is returned instead.
*/
const findImportSourceNode = node => {
if (node.type === _utils.AST_NODE_TYPES.AwaitExpression) {
if (node.argument.type === _utils.AST_NODE_TYPES.ImportExpression) {
return node.argument.source;
}
return null;
}
if (node.type === _utils.AST_NODE_TYPES.CallExpression && (0, _utils2.isIdentifier)(node.callee, 'require')) {
return node.arguments[0] ?? null;
}
return null;
};
const describeVariableDefAsImport = def => {
var _def$name$parent;
// make sure that we've actually being assigned a value
if (!def.node.init) {
return null;
}
const sourceNode = findImportSourceNode(def.node.init);
if (!sourceNode || !(0, _utils2.isStringNode)(sourceNode)) {
return null;
}
if (((_def$name$parent = def.name.parent) === null || _def$name$parent === void 0 ? void 0 : _def$name$parent.type) !== _utils.AST_NODE_TYPES.Property) {
return null;
}
if (!(0, _utils2.isSupportedAccessor)(def.name.parent.key)) {
return null;
}
return {
source: (0, _utils2.getStringValue)(sourceNode),
imported: (0, _utils2.getAccessorValue)(def.name.parent.key),
local: def.name.name
};
};
/**
* Attempts to describe a definition as an import if possible.
*
* If the definition is an import binding, it's described as you'd expect.
* If the definition is a variable, then we try and determine if it's either
* a dynamic `import()` or otherwise a call to `require()`.
*
* If it's neither of these, `null` is returned to indicate that the definition
* is not describable as an import of any kind.
*/
const describePossibleImportDef = def => {
if (def.type === 'Variable') {
return describeVariableDefAsImport(def);
}
if (def.type === 'ImportBinding') {
return describeImportDefAsImport(def);
}
return null;
};
const resolveScope = (scope, identifier) => {
let currentScope = scope;
while (currentScope !== null) {
const ref = currentScope.set.get(identifier);
if (ref && ref.defs.length > 0) {
const def = ref.defs[ref.defs.length - 1];
const importDetails = describePossibleImportDef(def);
if ((importDetails === null || importDetails === void 0 ? void 0 : importDetails.local) === identifier) {
return importDetails;
}
return 'local';
}
currentScope = currentScope.upper;
}
return null;
};
exports.resolveScope = resolveScope;
const resolveToJestFn = (context, accessor) => {
const identifier = (0, _utils2.getAccessorValue)(accessor);
const maybeImport = resolveScope((0, _utils2.getScope)(context, accessor), identifier);
// the identifier was found as a local variable or function declaration
// meaning it's not a function from jest
if (maybeImport === 'local') {
return null;
}
if (maybeImport) {
// the identifier is imported from @jest/globals,
// so return the original import name
if (maybeImport.source === '@jest/globals') {
return {
original: maybeImport.imported,
local: maybeImport.local,
type: 'import'
};
}
return null;
}
return {
original: resolvePossibleAliasedGlobal(identifier, context),
local: identifier,
type: 'global'
};
};

View File

@@ -0,0 +1,95 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
const paramsLocation = params => {
const [first] = params;
const last = params[params.length - 1];
return {
start: first.loc.start,
end: last.loc.end
};
};
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
type: 'problem',
docs: {
category: 'Possible Errors',
description: 'Enforce valid `describe()` callback',
recommended: 'error'
},
messages: {
nameAndCallback: 'Describe requires name and callback arguments',
secondArgumentMustBeFunction: 'Second argument must be function',
noAsyncDescribeCallback: 'No async describe callback',
unexpectedDescribeArgument: 'Unexpected argument(s) in describe callback',
unexpectedReturnInDescribe: 'Unexpected return statement in describe callback'
},
schema: []
},
defaultOptions: [],
create(context) {
return {
CallExpression(node) {
const jestFnCall = (0, _utils2.parseJestFnCall)(node, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'describe') {
return;
}
if (node.arguments.length < 1) {
return context.report({
messageId: 'nameAndCallback',
loc: node.loc
});
}
const [, callback] = node.arguments;
if (!callback) {
context.report({
messageId: 'nameAndCallback',
loc: paramsLocation(node.arguments)
});
return;
}
if (!(0, _utils2.isFunction)(callback)) {
context.report({
messageId: 'secondArgumentMustBeFunction',
loc: paramsLocation(node.arguments)
});
return;
}
if (callback.async) {
context.report({
messageId: 'noAsyncDescribeCallback',
node: callback
});
}
if (jestFnCall.members.every(s => (0, _utils2.getAccessorValue)(s) !== 'each') && callback.params.length) {
context.report({
messageId: 'unexpectedDescribeArgument',
loc: paramsLocation(callback.params)
});
}
if (callback.body.type === _utils.AST_NODE_TYPES.CallExpression) {
context.report({
messageId: 'unexpectedReturnInDescribe',
node: callback
});
}
if (callback.body.type === _utils.AST_NODE_TYPES.BlockStatement) {
callback.body.body.forEach(node => {
if (node.type === _utils.AST_NODE_TYPES.ReturnStatement) {
context.report({
messageId: 'unexpectedReturnInDescribe',
node
});
}
});
}
}
};
}
});

View File

@@ -0,0 +1,316 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
const isPromiseChainCall = node => {
if (node.type === _utils.AST_NODE_TYPES.CallExpression && node.callee.type === _utils.AST_NODE_TYPES.MemberExpression && (0, _utils2.isSupportedAccessor)(node.callee.property)) {
// promise methods should have at least 1 argument
if (node.arguments.length === 0) {
return false;
}
switch ((0, _utils2.getAccessorValue)(node.callee.property)) {
case 'then':
return node.arguments.length < 3;
case 'catch':
case 'finally':
return node.arguments.length < 2;
}
}
return false;
};
const isTestCaseCallWithCallbackArg = (node, context) => {
const jestCallFn = (0, _utils2.parseJestFnCall)(node, context);
if ((jestCallFn === null || jestCallFn === void 0 ? void 0 : jestCallFn.type) !== 'test') {
return false;
}
const isJestEach = jestCallFn.members.some(s => (0, _utils2.getAccessorValue)(s) === 'each');
if (isJestEach && node.callee.type !== _utils.AST_NODE_TYPES.TaggedTemplateExpression) {
// isJestEach but not a TaggedTemplateExpression, so this must be
// the `jest.each([])()` syntax which this rule doesn't support due
// to its complexity (see jest-community/eslint-plugin-jest#710)
// so we return true to trigger bailout
return true;
}
const [, callback] = node.arguments;
const callbackArgIndex = Number(isJestEach);
return callback && (0, _utils2.isFunction)(callback) && callback.params.length === 1 + callbackArgIndex;
};
const isPromiseMethodThatUsesValue = (node, identifier) => {
const {
name
} = identifier;
if (node.argument === null) {
return false;
}
if (node.argument.type === _utils.AST_NODE_TYPES.CallExpression && node.argument.arguments.length > 0) {
const nodeName = (0, _utils2.getNodeName)(node.argument);
if (['Promise.all', 'Promise.allSettled'].includes(nodeName)) {
const [firstArg] = node.argument.arguments;
if (firstArg.type === _utils.AST_NODE_TYPES.ArrayExpression && firstArg.elements.some(nod => nod && (0, _utils2.isIdentifier)(nod, name))) {
return true;
}
}
if (['Promise.resolve', 'Promise.reject'].includes(nodeName) && node.argument.arguments.length === 1) {
return (0, _utils2.isIdentifier)(node.argument.arguments[0], name);
}
}
return (0, _utils2.isIdentifier)(node.argument, name);
};
/**
* Attempts to determine if the runtime value represented by the given `identifier`
* is `await`ed within the given array of elements
*/
const isValueAwaitedInElements = (name, elements) => {
for (const element of elements) {
if ((element === null || element === void 0 ? void 0 : element.type) === _utils.AST_NODE_TYPES.AwaitExpression && (0, _utils2.isIdentifier)(element.argument, name)) {
return true;
}
if ((element === null || element === void 0 ? void 0 : element.type) === _utils.AST_NODE_TYPES.ArrayExpression && isValueAwaitedInElements(name, element.elements)) {
return true;
}
}
return false;
};
/**
* Attempts to determine if the runtime value represented by the given `identifier`
* is `await`ed as an argument along the given call expression
*/
const isValueAwaitedInArguments = (name, call) => {
let node = call;
while (node) {
if (node.type === _utils.AST_NODE_TYPES.CallExpression) {
if (isValueAwaitedInElements(name, node.arguments)) {
return true;
}
node = node.callee;
}
if (node.type !== _utils.AST_NODE_TYPES.MemberExpression) {
break;
}
node = node.object;
}
return false;
};
const getLeftMostCallExpression = call => {
let leftMostCallExpression = call;
let node = call;
while (node) {
if (node.type === _utils.AST_NODE_TYPES.CallExpression) {
leftMostCallExpression = node;
node = node.callee;
}
if (node.type !== _utils.AST_NODE_TYPES.MemberExpression) {
break;
}
node = node.object;
}
return leftMostCallExpression;
};
/**
* Attempts to determine if the runtime value represented by the given `identifier`
* is `await`ed or `return`ed within the given `body` of statements
*/
const isValueAwaitedOrReturned = (identifier, body, context) => {
const {
name
} = identifier;
for (const node of body) {
// skip all nodes that are before this identifier, because they'd probably
// be affecting a different runtime value (e.g. due to reassignment)
if (node.range[0] <= identifier.range[0]) {
continue;
}
if (node.type === _utils.AST_NODE_TYPES.ReturnStatement) {
return isPromiseMethodThatUsesValue(node, identifier);
}
if (node.type === _utils.AST_NODE_TYPES.ExpressionStatement) {
// it's possible that we're awaiting the value as an argument
if (node.expression.type === _utils.AST_NODE_TYPES.CallExpression) {
if (isValueAwaitedInArguments(name, node.expression)) {
return true;
}
const leftMostCall = getLeftMostCallExpression(node.expression);
const jestFnCall = (0, _utils2.parseJestFnCall)(node.expression, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) === 'expect' && leftMostCall.arguments.length > 0 && (0, _utils2.isIdentifier)(leftMostCall.arguments[0], name)) {
if (jestFnCall.members.some(m => {
const v = (0, _utils2.getAccessorValue)(m);
return v === _utils2.ModifierName.resolves || v === _utils2.ModifierName.rejects;
})) {
return true;
}
}
}
if (node.expression.type === _utils.AST_NODE_TYPES.AwaitExpression && isPromiseMethodThatUsesValue(node.expression, identifier)) {
return true;
}
// (re)assignment changes the runtime value, so if we've not found an
// await or return already we act as if we've reached the end of the body
if (node.expression.type === _utils.AST_NODE_TYPES.AssignmentExpression) {
var _getNodeName;
// unless we're assigning to the same identifier, in which case
// we might be chaining off the existing promise value
if ((0, _utils2.isIdentifier)(node.expression.left, name) && (_getNodeName = (0, _utils2.getNodeName)(node.expression.right)) !== null && _getNodeName !== void 0 && _getNodeName.startsWith(`${name}.`) && isPromiseChainCall(node.expression.right)) {
continue;
}
break;
}
}
if (node.type === _utils.AST_NODE_TYPES.BlockStatement && isValueAwaitedOrReturned(identifier, node.body, context)) {
return true;
}
}
return false;
};
const findFirstBlockBodyUp = node => {
let parent = node;
while (parent) {
if (parent.type === _utils.AST_NODE_TYPES.BlockStatement) {
return parent.body;
}
parent = parent.parent;
}
/* istanbul ignore next */
throw new Error(`Could not find BlockStatement - please file a github issue at https://github.com/jest-community/eslint-plugin-jest`);
};
const isDirectlyWithinTestCaseCall = (node, context) => {
let parent = node;
while (parent) {
if ((0, _utils2.isFunction)(parent)) {
var _parent;
parent = parent.parent;
return ((_parent = parent) === null || _parent === void 0 ? void 0 : _parent.type) === _utils.AST_NODE_TYPES.CallExpression && (0, _utils2.isTypeOfJestFnCall)(parent, context, ['test']);
}
parent = parent.parent;
}
return false;
};
const isVariableAwaitedOrReturned = (variable, context) => {
const body = findFirstBlockBodyUp(variable);
// it's pretty much impossible for us to track destructuring assignments,
// so we return true to bailout gracefully
if (!(0, _utils2.isIdentifier)(variable.id)) {
return true;
}
return isValueAwaitedOrReturned(variable.id, body, context);
};
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Require promises that have expectations in their chain to be valid',
recommended: 'error'
},
messages: {
expectInFloatingPromise: 'This promise should either be returned or awaited to ensure the expects in its chain are called'
},
type: 'suggestion',
schema: []
},
defaultOptions: [],
create(context) {
let inTestCaseWithDoneCallback = false;
// an array of booleans representing each promise chain we enter, with the
// boolean value representing if we think a given chain contains an expect
// in it's body.
//
// since we only care about the inner-most chain, we represent the state in
// reverse with the inner-most being the first item, as that makes it
// slightly less code to assign to by not needing to know the length
const chains = [];
return {
CallExpression(node) {
// there are too many ways that the done argument could be used with
// promises that contain expect that would make the promise safe for us
if (isTestCaseCallWithCallbackArg(node, context)) {
inTestCaseWithDoneCallback = true;
return;
}
// if this call expression is a promise chain, add it to the stack with
// value of "false", as we assume there are no expect calls initially
if (isPromiseChainCall(node)) {
chains.unshift(false);
return;
}
// if we're within a promise chain, and this call expression looks like
// an expect call, mark the deepest chain as having an expect call
if (chains.length > 0 && (0, _utils2.isTypeOfJestFnCall)(node, context, ['expect'])) {
chains[0] = true;
}
},
'CallExpression:exit'(node) {
// there are too many ways that the "done" argument could be used to
// make promises containing expects safe in a test for us to be able to
// accurately check, so we just bail out completely if it's present
if (inTestCaseWithDoneCallback) {
if ((0, _utils2.isTypeOfJestFnCall)(node, context, ['test'])) {
inTestCaseWithDoneCallback = false;
}
return;
}
if (!isPromiseChainCall(node)) {
return;
}
// since we're exiting this call expression (which is a promise chain)
// we remove it from the stack of chains, since we're unwinding
const hasExpectCall = chains.shift();
// if the promise chain we're exiting doesn't contain an expect,
// then we don't need to check it for anything
if (!hasExpectCall) {
return;
}
const {
parent
} = (0, _utils2.findTopMostCallExpression)(node);
// if we don't have a parent (which is technically impossible at runtime)
// or our parent is not directly within the test case, we stop checking
// because we're most likely in the body of a function being defined
// within the test, which we can't track
if (!parent || !isDirectlyWithinTestCaseCall(parent, context)) {
return;
}
switch (parent.type) {
case _utils.AST_NODE_TYPES.VariableDeclarator:
{
if (isVariableAwaitedOrReturned(parent, context)) {
return;
}
break;
}
case _utils.AST_NODE_TYPES.AssignmentExpression:
{
if (parent.left.type === _utils.AST_NODE_TYPES.Identifier && isValueAwaitedOrReturned(parent.left, findFirstBlockBodyUp(parent), context)) {
return;
}
break;
}
case _utils.AST_NODE_TYPES.ExpressionStatement:
break;
case _utils.AST_NODE_TYPES.ReturnStatement:
case _utils.AST_NODE_TYPES.AwaitExpression:
default:
return;
}
context.report({
messageId: 'expectInFloatingPromise',
node: parent
});
}
};
}
});

View File

@@ -0,0 +1,261 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
/*
* This implementation is ported from from eslint-plugin-jasmine.
* MIT license, Tom Vincent.
*/
/**
* Async assertions might be called in Promise
* methods like `Promise.x(expect1)` or `Promise.x([expect1, expect2])`.
* If that's the case, Promise node have to be awaited or returned.
*
* @Returns CallExpressionNode
*/
const getPromiseCallExpressionNode = node => {
if (node.type === _utils.AST_NODE_TYPES.ArrayExpression && node.parent && node.parent.type === _utils.AST_NODE_TYPES.CallExpression) {
node = node.parent;
}
if (node.type === _utils.AST_NODE_TYPES.CallExpression && node.callee.type === _utils.AST_NODE_TYPES.MemberExpression && (0, _utils2.isSupportedAccessor)(node.callee.object, 'Promise') && node.parent) {
return node;
}
return null;
};
const findPromiseCallExpressionNode = node => {
var _node$parent;
return (_node$parent = node.parent) !== null && _node$parent !== void 0 && _node$parent.parent && [_utils.AST_NODE_TYPES.CallExpression, _utils.AST_NODE_TYPES.ArrayExpression].includes(node.parent.type) ? getPromiseCallExpressionNode(node.parent) : null;
};
const getParentIfThenified = node => {
var _node$parent2;
const grandParentNode = (_node$parent2 = node.parent) === null || _node$parent2 === void 0 ? void 0 : _node$parent2.parent;
if (grandParentNode && grandParentNode.type === _utils.AST_NODE_TYPES.CallExpression && grandParentNode.callee.type === _utils.AST_NODE_TYPES.MemberExpression && (0, _utils2.isSupportedAccessor)(grandParentNode.callee.property) && ['then', 'catch'].includes((0, _utils2.getAccessorValue)(grandParentNode.callee.property)) && grandParentNode.parent) {
// Just in case `then`s are chained look one above.
return getParentIfThenified(grandParentNode);
}
return node;
};
const isAcceptableReturnNode = (node, allowReturn) => {
if (allowReturn && node.type === _utils.AST_NODE_TYPES.ReturnStatement) {
return true;
}
if (node.type === _utils.AST_NODE_TYPES.ConditionalExpression && node.parent) {
return isAcceptableReturnNode(node.parent, allowReturn);
}
return [_utils.AST_NODE_TYPES.ArrowFunctionExpression, _utils.AST_NODE_TYPES.AwaitExpression].includes(node.type);
};
const promiseArrayExceptionKey = ({
start,
end
}) => `${start.line}:${start.column}-${end.line}:${end.column}`;
const defaultAsyncMatchers = ['toReject', 'toResolve'];
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Enforce valid `expect()` usage',
recommended: 'error'
},
messages: {
tooManyArgs: 'Expect takes at most {{ amount }} argument{{ s }}',
notEnoughArgs: 'Expect requires at least {{ amount }} argument{{ s }}',
modifierUnknown: 'Expect has an unknown modifier',
matcherNotFound: 'Expect must have a corresponding matcher call',
matcherNotCalled: 'Matchers must be called to assert',
asyncMustBeAwaited: 'Async assertions must be awaited{{ orReturned }}',
promisesWithAsyncAssertionsMustBeAwaited: 'Promises which return async assertions must be awaited{{ orReturned }}'
},
type: 'suggestion',
schema: [{
type: 'object',
properties: {
alwaysAwait: {
type: 'boolean',
default: false
},
asyncMatchers: {
type: 'array',
items: {
type: 'string'
}
},
minArgs: {
type: 'number',
minimum: 1
},
maxArgs: {
type: 'number',
minimum: 1
}
},
additionalProperties: false
}]
},
defaultOptions: [{
alwaysAwait: false,
asyncMatchers: defaultAsyncMatchers,
minArgs: 1,
maxArgs: 1
}],
create(context, [{
alwaysAwait,
asyncMatchers = defaultAsyncMatchers,
minArgs = 1,
maxArgs = 1
}]) {
// Context state
const arrayExceptions = new Set();
const pushPromiseArrayException = loc => arrayExceptions.add(promiseArrayExceptionKey(loc));
/**
* Promise method that accepts an array of promises,
* (eg. Promise.all), will throw warnings for the each
* unawaited or non-returned promise. To avoid throwing
* multiple warnings, we check if there is a warning in
* the given location.
*/
const promiseArrayExceptionExists = loc => arrayExceptions.has(promiseArrayExceptionKey(loc));
const findTopMostMemberExpression = node => {
let topMostMemberExpression = node;
let {
parent
} = node;
while (parent) {
if (parent.type !== _utils.AST_NODE_TYPES.MemberExpression) {
break;
}
topMostMemberExpression = parent;
parent = parent.parent;
}
return topMostMemberExpression;
};
return {
CallExpression(node) {
const jestFnCall = (0, _utils2.parseJestFnCallWithReason)(node, context);
if (typeof jestFnCall === 'string') {
var _node$parent3;
const reportingNode = ((_node$parent3 = node.parent) === null || _node$parent3 === void 0 ? void 0 : _node$parent3.type) === _utils.AST_NODE_TYPES.MemberExpression ? findTopMostMemberExpression(node.parent).property : node;
if (jestFnCall === 'matcher-not-found') {
context.report({
messageId: 'matcherNotFound',
node: reportingNode
});
return;
}
if (jestFnCall === 'matcher-not-called') {
context.report({
messageId: (0, _utils2.isSupportedAccessor)(reportingNode) && _utils2.ModifierName.hasOwnProperty((0, _utils2.getAccessorValue)(reportingNode)) ? 'matcherNotFound' : 'matcherNotCalled',
node: reportingNode
});
}
if (jestFnCall === 'modifier-unknown') {
context.report({
messageId: 'modifierUnknown',
node: reportingNode
});
return;
}
return;
} else if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'expect') {
return;
}
const {
parent: expect
} = jestFnCall.head.node;
if ((expect === null || expect === void 0 ? void 0 : expect.type) !== _utils.AST_NODE_TYPES.CallExpression) {
return;
}
if (expect.arguments.length < minArgs) {
const expectLength = (0, _utils2.getAccessorValue)(jestFnCall.head.node).length;
const loc = {
start: {
column: expect.loc.start.column + expectLength,
line: expect.loc.start.line
},
end: {
column: expect.loc.start.column + expectLength + 1,
line: expect.loc.start.line
}
};
context.report({
messageId: 'notEnoughArgs',
data: {
amount: minArgs,
s: minArgs === 1 ? '' : 's'
},
node: expect,
loc
});
}
if (expect.arguments.length > maxArgs) {
const {
start
} = expect.arguments[maxArgs].loc;
const {
end
} = expect.arguments[expect.arguments.length - 1].loc;
const loc = {
start,
end: {
column: end.column - 1,
line: end.line
}
};
context.report({
messageId: 'tooManyArgs',
data: {
amount: maxArgs,
s: maxArgs === 1 ? '' : 's'
},
node: expect,
loc
});
}
const {
matcher
} = jestFnCall;
const parentNode = matcher.parent.parent;
const shouldBeAwaited = jestFnCall.modifiers.some(nod => (0, _utils2.getAccessorValue)(nod) !== 'not') || asyncMatchers.includes((0, _utils2.getAccessorValue)(matcher));
if (!(parentNode !== null && parentNode !== void 0 && parentNode.parent) || !shouldBeAwaited) {
return;
}
/**
* If parent node is an array expression, we'll report the warning,
* for the array object, not for each individual assertion.
*/
const isParentArrayExpression = parentNode.parent.type === _utils.AST_NODE_TYPES.ArrayExpression;
const orReturned = alwaysAwait ? '' : ' or returned';
/**
* An async assertion can be chained with `then` or `catch` statements.
* In that case our target CallExpression node is the one with
* the last `then` or `catch` statement.
*/
const targetNode = getParentIfThenified(parentNode);
const finalNode = findPromiseCallExpressionNode(targetNode) || targetNode;
if (finalNode.parent &&
// If node is not awaited or returned
!isAcceptableReturnNode(finalNode.parent, !alwaysAwait) &&
// if we didn't warn user already
!promiseArrayExceptionExists(finalNode.loc)) {
context.report({
loc: finalNode.loc,
data: {
orReturned
},
messageId: finalNode === targetNode ? 'asyncMustBeAwaited' : 'promisesWithAsyncAssertionsMustBeAwaited',
node
});
if (isParentArrayExpression) {
pushPromiseArrayException(finalNode.loc);
}
}
}
};
}
});

View File

@@ -0,0 +1,224 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("./utils");
const trimFXprefix = word => ['f', 'x'].includes(word.charAt(0)) ? word.substring(1) : word;
const doesBinaryExpressionContainStringNode = binaryExp => {
if ((0, _utils2.isStringNode)(binaryExp.right)) {
return true;
}
if (binaryExp.left.type === _utils.AST_NODE_TYPES.BinaryExpression) {
return doesBinaryExpressionContainStringNode(binaryExp.left);
}
return (0, _utils2.isStringNode)(binaryExp.left);
};
const quoteStringValue = node => node.type === _utils.AST_NODE_TYPES.TemplateLiteral ? `\`${node.quasis[0].value.raw}\`` : node.raw;
const compileMatcherPattern = matcherMaybeWithMessage => {
const [matcher, message] = Array.isArray(matcherMaybeWithMessage) ? matcherMaybeWithMessage : [matcherMaybeWithMessage];
return [new RegExp(matcher, 'u'), message];
};
const compileMatcherPatterns = matchers => {
if (typeof matchers === 'string' || Array.isArray(matchers)) {
const compiledMatcher = compileMatcherPattern(matchers);
return {
describe: compiledMatcher,
test: compiledMatcher,
it: compiledMatcher
};
}
return {
describe: matchers.describe ? compileMatcherPattern(matchers.describe) : null,
test: matchers.test ? compileMatcherPattern(matchers.test) : null,
it: matchers.it ? compileMatcherPattern(matchers.it) : null
};
};
const MatcherAndMessageSchema = {
type: 'array',
items: {
type: 'string'
},
minItems: 1,
maxItems: 2,
additionalItems: false
};
var _default = exports.default = (0, _utils2.createRule)({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Enforce valid titles',
recommended: 'error'
},
messages: {
titleMustBeString: 'Title must be a string',
emptyTitle: '{{ jestFunctionName }} should not have an empty title',
duplicatePrefix: 'should not have duplicate prefix',
accidentalSpace: 'should not have leading or trailing spaces',
disallowedWord: '"{{ word }}" is not allowed in test titles',
mustNotMatch: '{{ jestFunctionName }} should not match {{ pattern }}',
mustMatch: '{{ jestFunctionName }} should match {{ pattern }}',
mustNotMatchCustom: '{{ message }}',
mustMatchCustom: '{{ message }}'
},
type: 'suggestion',
schema: [{
type: 'object',
properties: {
ignoreSpaces: {
type: 'boolean',
default: false
},
ignoreTypeOfDescribeName: {
type: 'boolean',
default: false
},
ignoreTypeOfTestName: {
type: 'boolean',
default: false
},
disallowedWords: {
type: 'array',
items: {
type: 'string'
}
}
},
patternProperties: {
[/^must(?:Not)?Match$/u.source]: {
oneOf: [{
type: 'string'
}, MatcherAndMessageSchema, {
type: 'object',
propertyNames: {
enum: ['describe', 'test', 'it']
},
additionalProperties: {
oneOf: [{
type: 'string'
}, MatcherAndMessageSchema]
}
}]
}
},
additionalProperties: false
}],
fixable: 'code'
},
defaultOptions: [{
ignoreSpaces: false,
ignoreTypeOfDescribeName: false,
ignoreTypeOfTestName: false,
disallowedWords: []
}],
create(context, [{
ignoreSpaces,
ignoreTypeOfDescribeName,
ignoreTypeOfTestName,
disallowedWords = [],
mustNotMatch,
mustMatch
}]) {
const disallowedWordsRegexp = new RegExp(`\\b(${disallowedWords.join('|')})\\b`, 'iu');
const mustNotMatchPatterns = compileMatcherPatterns(mustNotMatch ?? {});
const mustMatchPatterns = compileMatcherPatterns(mustMatch ?? {});
return {
CallExpression(node) {
const jestFnCall = (0, _utils2.parseJestFnCall)(node, context);
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'describe' && (jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'test') {
return;
}
const [argument] = node.arguments;
if (!argument) {
return;
}
if (!(0, _utils2.isStringNode)(argument)) {
if (argument.type === _utils.AST_NODE_TYPES.BinaryExpression && doesBinaryExpressionContainStringNode(argument)) {
return;
}
if (!(jestFnCall.type === 'describe' && ignoreTypeOfDescribeName || jestFnCall.type === 'test' && ignoreTypeOfTestName) && argument.type !== _utils.AST_NODE_TYPES.TemplateLiteral) {
context.report({
messageId: 'titleMustBeString',
loc: argument.loc
});
}
return;
}
const title = (0, _utils2.getStringValue)(argument);
if (!title) {
context.report({
messageId: 'emptyTitle',
data: {
jestFunctionName: jestFnCall.type === 'describe' ? _utils2.DescribeAlias.describe : _utils2.TestCaseName.test
},
node
});
return;
}
if (disallowedWords.length > 0) {
const disallowedMatch = disallowedWordsRegexp.exec(title);
if (disallowedMatch) {
context.report({
data: {
word: disallowedMatch[1]
},
messageId: 'disallowedWord',
node: argument
});
return;
}
}
if (ignoreSpaces === false && title.trim().length !== title.length) {
context.report({
messageId: 'accidentalSpace',
node: argument,
fix: fixer => [fixer.replaceTextRange(argument.range, quoteStringValue(argument).replace(/^([`'"]) +?/u, '$1').replace(/ +?([`'"])$/u, '$1'))]
});
}
const unprefixedName = trimFXprefix(jestFnCall.name);
const [firstWord] = title.split(' ');
if (firstWord.toLowerCase() === unprefixedName) {
context.report({
messageId: 'duplicatePrefix',
node: argument,
fix: fixer => [fixer.replaceTextRange(argument.range, quoteStringValue(argument).replace(/^([`'"]).+? /u, '$1'))]
});
}
const jestFunctionName = unprefixedName;
const [mustNotMatchPattern, mustNotMatchMessage] = mustNotMatchPatterns[jestFunctionName] ?? [];
if (mustNotMatchPattern) {
if (mustNotMatchPattern.test(title)) {
context.report({
messageId: mustNotMatchMessage ? 'mustNotMatchCustom' : 'mustNotMatch',
node: argument,
data: {
jestFunctionName,
pattern: mustNotMatchPattern,
message: mustNotMatchMessage
}
});
return;
}
}
const [mustMatchPattern, mustMatchMessage] = mustMatchPatterns[jestFunctionName] ?? [];
if (mustMatchPattern) {
if (!mustMatchPattern.test(title)) {
context.report({
messageId: mustMatchMessage ? 'mustMatchCustom' : 'mustMatch',
node: argument,
data: {
jestFunctionName,
pattern: mustMatchPattern,
message: mustMatchMessage
}
});
return;
}
}
}
};
}
});