You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
4659 lines
187 KiB
4659 lines
187 KiB
import { ESLintUtils, AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils'; |
|
import * as path from 'node:path'; |
|
import { isAbsolute, posix } from 'node:path'; |
|
import ts from 'typescript'; |
|
import { createRequire } from 'node:module'; |
|
|
|
const version = "1.1.6"; |
|
|
|
function createEslintRule(rule) { |
|
const createRule = ESLintUtils.RuleCreator( |
|
(ruleName) => `https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/${ruleName}.md` |
|
); |
|
return createRule(rule); |
|
} |
|
const joinNames = (a, b) => a && b ? `${a}.${b}` : null; |
|
const isFunction = (node) => node.type === AST_NODE_TYPES.FunctionExpression || node.type === AST_NODE_TYPES.ArrowFunctionExpression; |
|
function getNodeName(node) { |
|
if (isSupportedAccessor(node)) |
|
return getAccessorValue(node); |
|
switch (node.type) { |
|
case AST_NODE_TYPES.TaggedTemplateExpression: |
|
return getNodeName(node.tag); |
|
case AST_NODE_TYPES.MemberExpression: |
|
return joinNames(getNodeName(node.object), getNodeName(node.property)); |
|
case AST_NODE_TYPES.NewExpression: |
|
case AST_NODE_TYPES.CallExpression: |
|
return getNodeName(node.callee); |
|
} |
|
return null; |
|
} |
|
const isSupportedAccessor = (node, value) => { |
|
return isIdentifier(node, value) || isStringNode(node, value); |
|
}; |
|
const isIdentifier = (node, name) => { |
|
return node.type === AST_NODE_TYPES.Identifier && (name === void 0 || node.name === name); |
|
}; |
|
const isTemplateLiteral = (node, value) => { |
|
return node.type === AST_NODE_TYPES.TemplateLiteral && node.quasis.length === 1 && (value === void 0 || node.quasis[0].value.raw === value); |
|
}; |
|
const isStringLiteral = (node, value) => node.type === AST_NODE_TYPES.Literal && typeof node.value === "string" && (value === void 0 || node.value === value); |
|
const isStringNode = (node, specifics) => isStringLiteral(node, specifics) || isTemplateLiteral(node, specifics); |
|
const getAccessorValue = (accessor) => accessor.type === AST_NODE_TYPES.Identifier ? accessor.name : getStringValue(accessor); |
|
const getStringValue = (node) => node?.type === AST_NODE_TYPES.TemplateLiteral ? node.quasis[0].value.raw : node?.value; |
|
const replaceAccessorFixer = (fixer, node, text) => { |
|
return fixer.replaceText( |
|
node, |
|
node.type === AST_NODE_TYPES.Identifier ? text : `'${text}'` |
|
); |
|
}; |
|
const removeExtraArgumentsFixer = (fixer, context, func, from) => { |
|
const firstArg = func.arguments[from]; |
|
const lastArg = func.arguments[func.arguments.length - 1]; |
|
const { sourceCode } = context; |
|
let tokenAfterLastParam = sourceCode.getTokenAfter(lastArg); |
|
if (tokenAfterLastParam.value === ",") |
|
tokenAfterLastParam = sourceCode.getTokenAfter(tokenAfterLastParam); |
|
return fixer.removeRange([firstArg.range[0], tokenAfterLastParam.range[0]]); |
|
}; |
|
const isParsedInstanceOfMatcherCall = (expectFnCall, classArg) => { |
|
return getAccessorValue(expectFnCall.matcher) === "toBeInstanceOf" && expectFnCall.args.length === 1 && isSupportedAccessor(expectFnCall.args[0], classArg); |
|
}; |
|
|
|
var DescribeAlias = /* @__PURE__ */ ((DescribeAlias2) => { |
|
DescribeAlias2["describe"] = "describe"; |
|
DescribeAlias2["fdescribe"] = "fdescribe"; |
|
DescribeAlias2["xdescribe"] = "xdescribe"; |
|
return DescribeAlias2; |
|
})(DescribeAlias || {}); |
|
var TestCaseName = /* @__PURE__ */ ((TestCaseName2) => { |
|
TestCaseName2["fit"] = "fit"; |
|
TestCaseName2["it"] = "it"; |
|
TestCaseName2["test"] = "test"; |
|
TestCaseName2["xit"] = "xit"; |
|
TestCaseName2["xtest"] = "xtest"; |
|
TestCaseName2["bench"] = "bench"; |
|
return TestCaseName2; |
|
})(TestCaseName || {}); |
|
var HookName = /* @__PURE__ */ ((HookName2) => { |
|
HookName2["beforeAll"] = "beforeAll"; |
|
HookName2["beforeEach"] = "beforeEach"; |
|
HookName2["afterAll"] = "afterAll"; |
|
HookName2["afterEach"] = "afterEach"; |
|
return HookName2; |
|
})(HookName || {}); |
|
var ModifierName = /* @__PURE__ */ ((ModifierName2) => { |
|
ModifierName2["not"] = "not"; |
|
ModifierName2["rejects"] = "rejects"; |
|
ModifierName2["resolves"] = "resolves"; |
|
return ModifierName2; |
|
})(ModifierName || {}); |
|
var EqualityMatcher = /* @__PURE__ */ ((EqualityMatcher2) => { |
|
EqualityMatcher2["toBe"] = "toBe"; |
|
EqualityMatcher2["toEqual"] = "toEqual"; |
|
EqualityMatcher2["toStrictEqual"] = "toStrictEqual"; |
|
return EqualityMatcher2; |
|
})(EqualityMatcher || {}); |
|
|
|
const ValidVitestFnCallChains = /* @__PURE__ */ new Set(["beforeEach", "beforeAll", "afterEach", "afterAll", "it", "it.skip", "it.only", "it.concurrent", "it.sequential", "it.todo", "it.fails", "it.extend", "it.skipIf", "it.runIf", "it.each", "it.skip.only", "it.skip.concurrent", "it.skip.sequential", "it.skip.todo", "it.skip.fails", "it.only.skip", "it.only.concurrent", "it.only.sequential", "it.only.todo", "it.only.fails", "it.concurrent.skip", "it.concurrent.only", "it.concurrent.sequential", "it.concurrent.todo", "it.concurrent.fails", "it.sequential.skip", "it.sequential.only", "it.sequential.concurrent", "it.sequential.todo", "it.sequential.fails", "it.todo.skip", "it.todo.only", "it.todo.concurrent", "it.todo.sequential", "it.todo.fails", "it.fails.skip", "it.fails.only", "it.fails.concurrent", "it.fails.sequential", "it.fails.todo", "it.extend.skip", "it.extend.only", "it.extend.concurrent", "it.extend.sequential", "it.extend.todo", "it.extend.fails", "it.skipIf.skip", "it.skipIf.only", "it.skipIf.concurrent", "it.skipIf.sequential", "it.skipIf.todo", "it.skipIf.fails", "it.runIf.skip", "it.runIf.only", "it.runIf.concurrent", "it.runIf.sequential", "it.runIf.todo", "it.runIf.fails", "it.skip.each", "it.only.each", "it.concurrent.each", "it.sequential.each", "it.todo.each", "it.fails.each", "it.extend.skipIf", "it.extend.runIf", "it.extend.each", "it.skipIf.each", "it.runIf.each", "it.skip.only.concurrent", "it.skip.only.sequential", "it.skip.only.todo", "it.skip.only.fails", "it.skip.concurrent.only", "it.skip.concurrent.sequential", "it.skip.concurrent.todo", "it.skip.concurrent.fails", "it.skip.sequential.only", "it.skip.sequential.concurrent", "it.skip.sequential.todo", "it.skip.sequential.fails", "it.skip.todo.only", "it.skip.todo.concurrent", "it.skip.todo.sequential", "it.skip.todo.fails", "it.skip.fails.only", "it.skip.fails.concurrent", "it.skip.fails.sequential", "it.skip.fails.todo", "it.only.skip.concurrent", "it.only.skip.sequential", "it.only.skip.todo", "it.only.skip.fails", "it.only.concurrent.skip", "it.only.concurrent.sequential", "it.only.concurrent.todo", "it.only.concurrent.fails", "it.only.sequential.skip", "it.only.sequential.concurrent", "it.only.sequential.todo", "it.only.sequential.fails", "it.only.todo.skip", "it.only.todo.concurrent", "it.only.todo.sequential", "it.only.todo.fails", "it.only.fails.skip", "it.only.fails.concurrent", "it.only.fails.sequential", "it.only.fails.todo", "it.concurrent.skip.only", "it.concurrent.skip.sequential", "it.concurrent.skip.todo", "it.concurrent.skip.fails", "it.concurrent.only.skip", "it.concurrent.only.sequential", "it.concurrent.only.todo", "it.concurrent.only.fails", "it.concurrent.sequential.skip", "it.concurrent.sequential.only", "it.concurrent.sequential.todo", "it.concurrent.sequential.fails", "it.concurrent.todo.skip", "it.concurrent.todo.only", "it.concurrent.todo.sequential", "it.concurrent.todo.fails", "it.concurrent.fails.skip", "it.concurrent.fails.only", "it.concurrent.fails.sequential", "it.concurrent.fails.todo", "it.sequential.skip.only", "it.sequential.skip.concurrent", "it.sequential.skip.todo", "it.sequential.skip.fails", "it.sequential.only.skip", "it.sequential.only.concurrent", "it.sequential.only.todo", "it.sequential.only.fails", "it.sequential.concurrent.skip", "it.sequential.concurrent.only", "it.sequential.concurrent.todo", "it.sequential.concurrent.fails", "it.sequential.todo.skip", "it.sequential.todo.only", "it.sequential.todo.concurrent", "it.sequential.todo.fails", "it.sequential.fails.skip", "it.sequential.fails.only", "it.sequential.fails.concurrent", "it.sequential.fails.todo", "it.todo.skip.only", "it.todo.skip.concurrent", "it.todo.skip.sequential", "it.todo.skip.fails", "it.todo.only.skip", "it.todo.only.concurrent", "it.todo.only.sequential", "it.todo.only.fails", "it.todo.concurrent.skip", "it.todo.concurrent.only", "it.todo.concurrent.sequential", "it.todo.concurrent.fails", "it.todo.sequential.skip", "it.todo.sequential.only", "it.todo.sequential.concurrent", "it.todo.sequential.fails", "it.todo.fails.skip", "it.todo.fails.only", "it.todo.fails.concurrent", "it.todo.fails.sequential", "it.fails.skip.only", "it.fails.skip.concurrent", "it.fails.skip.sequential", "it.fails.skip.todo", "it.fails.only.skip", "it.fails.only.concurrent", "it.fails.only.sequential", "it.fails.only.todo", "it.fails.concurrent.skip", "it.fails.concurrent.only", "it.fails.concurrent.sequential", "it.fails.concurrent.todo", "it.fails.sequential.skip", "it.fails.sequential.only", "it.fails.sequential.concurrent", "it.fails.sequential.todo", "it.fails.todo.skip", "it.fails.todo.only", "it.fails.todo.concurrent", "it.fails.todo.sequential", "it.extend.skip.only", "it.extend.skip.concurrent", "it.extend.skip.sequential", "it.extend.skip.todo", "it.extend.skip.fails", "it.extend.only.skip", "it.extend.only.concurrent", "it.extend.only.sequential", "it.extend.only.todo", "it.extend.only.fails", "it.extend.concurrent.skip", "it.extend.concurrent.only", "it.extend.concurrent.sequential", "it.extend.concurrent.todo", "it.extend.concurrent.fails", "it.extend.sequential.skip", "it.extend.sequential.only", "it.extend.sequential.concurrent", "it.extend.sequential.todo", "it.extend.sequential.fails", "it.extend.todo.skip", "it.extend.todo.only", "it.extend.todo.concurrent", "it.extend.todo.sequential", "it.extend.todo.fails", "it.extend.fails.skip", "it.extend.fails.only", "it.extend.fails.concurrent", "it.extend.fails.sequential", "it.extend.fails.todo", "it.skipIf.skip.only", "it.skipIf.skip.concurrent", "it.skipIf.skip.sequential", "it.skipIf.skip.todo", "it.skipIf.skip.fails", "it.skipIf.only.skip", "it.skipIf.only.concurrent", "it.skipIf.only.sequential", "it.skipIf.only.todo", "it.skipIf.only.fails", "it.skipIf.concurrent.skip", "it.skipIf.concurrent.only", "it.skipIf.concurrent.sequential", "it.skipIf.concurrent.todo", "it.skipIf.concurrent.fails", "it.skipIf.sequential.skip", "it.skipIf.sequential.only", "it.skipIf.sequential.concurrent", "it.skipIf.sequential.todo", "it.skipIf.sequential.fails", "it.skipIf.todo.skip", "it.skipIf.todo.only", "it.skipIf.todo.concurrent", "it.skipIf.todo.sequential", "it.skipIf.todo.fails", "it.skipIf.fails.skip", "it.skipIf.fails.only", "it.skipIf.fails.concurrent", "it.skipIf.fails.sequential", "it.skipIf.fails.todo", "it.runIf.skip.only", "it.runIf.skip.concurrent", "it.runIf.skip.sequential", "it.runIf.skip.todo", "it.runIf.skip.fails", "it.runIf.only.skip", "it.runIf.only.concurrent", "it.runIf.only.sequential", "it.runIf.only.todo", "it.runIf.only.fails", "it.runIf.concurrent.skip", "it.runIf.concurrent.only", "it.runIf.concurrent.sequential", "it.runIf.concurrent.todo", "it.runIf.concurrent.fails", "it.runIf.sequential.skip", "it.runIf.sequential.only", "it.runIf.sequential.concurrent", "it.runIf.sequential.todo", "it.runIf.sequential.fails", "it.runIf.todo.skip", "it.runIf.todo.only", "it.runIf.todo.concurrent", "it.runIf.todo.sequential", "it.runIf.todo.fails", "it.runIf.fails.skip", "it.runIf.fails.only", "it.runIf.fails.concurrent", "it.runIf.fails.sequential", "it.runIf.fails.todo", "it.skip.only.each", "it.skip.concurrent.each", "it.skip.sequential.each", "it.skip.todo.each", "it.skip.fails.each", "it.only.skip.each", "it.only.concurrent.each", "it.only.sequential.each", "it.only.todo.each", "it.only.fails.each", "it.concurrent.skip.each", "it.concurrent.only.each", "it.concurrent.sequential.each", "it.concurrent.todo.each", "it.concurrent.fails.each", "it.sequential.skip.each", "it.sequential.only.each", "it.sequential.concurrent.each", "it.sequential.todo.each", "it.sequential.fails.each", "it.todo.skip.each", "it.todo.only.each", "it.todo.concurrent.each", "it.todo.sequential.each", "it.todo.fails.each", "it.fails.skip.each", "it.fails.only.each", "it.fails.concurrent.each", "it.fails.sequential.each", "it.fails.todo.each", "it.extend.skipIf.skip", "it.extend.skipIf.only", "it.extend.skipIf.concurrent", "it.extend.skipIf.sequential", "it.extend.skipIf.todo", "it.extend.skipIf.fails", "it.extend.runIf.skip", "it.extend.runIf.only", "it.extend.runIf.concurrent", "it.extend.runIf.sequential", "it.extend.runIf.todo", "it.extend.runIf.fails", "it.extend.skip.each", "it.extend.only.each", "it.extend.concurrent.each", "it.extend.sequential.each", "it.extend.todo.each", "it.extend.fails.each", "it.skipIf.skip.each", "it.skipIf.only.each", "it.skipIf.concurrent.each", "it.skipIf.sequential.each", "it.skipIf.todo.each", "it.skipIf.fails.each", "it.runIf.skip.each", "it.runIf.only.each", "it.runIf.concurrent.each", "it.runIf.sequential.each", "it.runIf.todo.each", "it.runIf.fails.each", "it.extend.skipIf.each", "it.extend.runIf.each", "test", "test.skip", "test.only", "test.concurrent", "test.sequential", "test.todo", "test.fails", "test.extend", "test.skipIf", "test.runIf", "test.each", "test.skip.only", "test.skip.concurrent", "test.skip.sequential", "test.skip.todo", "test.skip.fails", "test.only.skip", "test.only.concurrent", "test.only.sequential", "test.only.todo", "test.only.fails", "test.concurrent.skip", "test.concurrent.only", "test.concurrent.sequential", "test.concurrent.todo", "test.concurrent.fails", "test.sequential.skip", "test.sequential.only", "test.sequential.concurrent", "test.sequential.todo", "test.sequential.fails", "test.todo.skip", "test.todo.only", "test.todo.concurrent", "test.todo.sequential", "test.todo.fails", "test.fails.skip", "test.fails.only", "test.fails.concurrent", "test.fails.sequential", "test.fails.todo", "test.extend.skip", "test.extend.only", "test.extend.concurrent", "test.extend.sequential", "test.extend.todo", "test.extend.fails", "test.skipIf.skip", "test.skipIf.only", "test.skipIf.concurrent", "test.skipIf.sequential", "test.skipIf.todo", "test.skipIf.fails", "test.runIf.skip", "test.runIf.only", "test.runIf.concurrent", "test.runIf.sequential", "test.runIf.todo", "test.runIf.fails", "test.skip.each", "test.only.each", "test.concurrent.each", "test.sequential.each", "test.todo.each", "test.fails.each", "test.extend.skipIf", "test.extend.runIf", "test.extend.each", "test.skipIf.each", "test.runIf.each", "test.skip.only.concurrent", "test.skip.only.sequential", "test.skip.only.todo", "test.skip.only.fails", "test.skip.concurrent.only", "test.skip.concurrent.sequential", "test.skip.concurrent.todo", "test.skip.concurrent.fails", "test.skip.sequential.only", "test.skip.sequential.concurrent", "test.skip.sequential.todo", "test.skip.sequential.fails", "test.skip.todo.only", "test.skip.todo.concurrent", "test.skip.todo.sequential", "test.skip.todo.fails", "test.skip.fails.only", "test.skip.fails.concurrent", "test.skip.fails.sequential", "test.skip.fails.todo", "test.only.skip.concurrent", "test.only.skip.sequential", "test.only.skip.todo", "test.only.skip.fails", "test.only.concurrent.skip", "test.only.concurrent.sequential", "test.only.concurrent.todo", "test.only.concurrent.fails", "test.only.sequential.skip", "test.only.sequential.concurrent", "test.only.sequential.todo", "test.only.sequential.fails", "test.only.todo.skip", "test.only.todo.concurrent", "test.only.todo.sequential", "test.only.todo.fails", "test.only.fails.skip", "test.only.fails.concurrent", "test.only.fails.sequential", "test.only.fails.todo", "test.concurrent.skip.only", "test.concurrent.skip.sequential", "test.concurrent.skip.todo", "test.concurrent.skip.fails", "test.concurrent.only.skip", "test.concurrent.only.sequential", "test.concurrent.only.todo", "test.concurrent.only.fails", "test.concurrent.sequential.skip", "test.concurrent.sequential.only", "test.concurrent.sequential.todo", "test.concurrent.sequential.fails", "test.concurrent.todo.skip", "test.concurrent.todo.only", "test.concurrent.todo.sequential", "test.concurrent.todo.fails", "test.concurrent.fails.skip", "test.concurrent.fails.only", "test.concurrent.fails.sequential", "test.concurrent.fails.todo", "test.sequential.skip.only", "test.sequential.skip.concurrent", "test.sequential.skip.todo", "test.sequential.skip.fails", "test.sequential.only.skip", "test.sequential.only.concurrent", "test.sequential.only.todo", "test.sequential.only.fails", "test.sequential.concurrent.skip", "test.sequential.concurrent.only", "test.sequential.concurrent.todo", "test.sequential.concurrent.fails", "test.sequential.todo.skip", "test.sequential.todo.only", "test.sequential.todo.concurrent", "test.sequential.todo.fails", "test.sequential.fails.skip", "test.sequential.fails.only", "test.sequential.fails.concurrent", "test.sequential.fails.todo", "test.todo.skip.only", "test.todo.skip.concurrent", "test.todo.skip.sequential", "test.todo.skip.fails", "test.todo.only.skip", "test.todo.only.concurrent", "test.todo.only.sequential", "test.todo.only.fails", "test.todo.concurrent.skip", "test.todo.concurrent.only", "test.todo.concurrent.sequential", "test.todo.concurrent.fails", "test.todo.sequential.skip", "test.todo.sequential.only", "test.todo.sequential.concurrent", "test.todo.sequential.fails", "test.todo.fails.skip", "test.todo.fails.only", "test.todo.fails.concurrent", "test.todo.fails.sequential", "test.fails.skip.only", "test.fails.skip.concurrent", "test.fails.skip.sequential", "test.fails.skip.todo", "test.fails.only.skip", "test.fails.only.concurrent", "test.fails.only.sequential", "test.fails.only.todo", "test.fails.concurrent.skip", "test.fails.concurrent.only", "test.fails.concurrent.sequential", "test.fails.concurrent.todo", "test.fails.sequential.skip", "test.fails.sequential.only", "test.fails.sequential.concurrent", "test.fails.sequential.todo", "test.fails.todo.skip", "test.fails.todo.only", "test.fails.todo.concurrent", "test.fails.todo.sequential", "test.extend.skip.only", "test.extend.skip.concurrent", "test.extend.skip.sequential", "test.extend.skip.todo", "test.extend.skip.fails", "test.extend.only.skip", "test.extend.only.concurrent", "test.extend.only.sequential", "test.extend.only.todo", "test.extend.only.fails", "test.extend.concurrent.skip", "test.extend.concurrent.only", "test.extend.concurrent.sequential", "test.extend.concurrent.todo", "test.extend.concurrent.fails", "test.extend.sequential.skip", "test.extend.sequential.only", "test.extend.sequential.concurrent", "test.extend.sequential.todo", "test.extend.sequential.fails", "test.extend.todo.skip", "test.extend.todo.only", "test.extend.todo.concurrent", "test.extend.todo.sequential", "test.extend.todo.fails", "test.extend.fails.skip", "test.extend.fails.only", "test.extend.fails.concurrent", "test.extend.fails.sequential", "test.extend.fails.todo", "test.skipIf.skip.only", "test.skipIf.skip.concurrent", "test.skipIf.skip.sequential", "test.skipIf.skip.todo", "test.skipIf.skip.fails", "test.skipIf.only.skip", "test.skipIf.only.concurrent", "test.skipIf.only.sequential", "test.skipIf.only.todo", "test.skipIf.only.fails", "test.skipIf.concurrent.skip", "test.skipIf.concurrent.only", "test.skipIf.concurrent.sequential", "test.skipIf.concurrent.todo", "test.skipIf.concurrent.fails", "test.skipIf.sequential.skip", "test.skipIf.sequential.only", "test.skipIf.sequential.concurrent", "test.skipIf.sequential.todo", "test.skipIf.sequential.fails", "test.skipIf.todo.skip", "test.skipIf.todo.only", "test.skipIf.todo.concurrent", "test.skipIf.todo.sequential", "test.skipIf.todo.fails", "test.skipIf.fails.skip", "test.skipIf.fails.only", "test.skipIf.fails.concurrent", "test.skipIf.fails.sequential", "test.skipIf.fails.todo", "test.runIf.skip.only", "test.runIf.skip.concurrent", "test.runIf.skip.sequential", "test.runIf.skip.todo", "test.runIf.skip.fails", "test.runIf.only.skip", "test.runIf.only.concurrent", "test.runIf.only.sequential", "test.runIf.only.todo", "test.runIf.only.fails", "test.runIf.concurrent.skip", "test.runIf.concurrent.only", "test.runIf.concurrent.sequential", "test.runIf.concurrent.todo", "test.runIf.concurrent.fails", "test.runIf.sequential.skip", "test.runIf.sequential.only", "test.runIf.sequential.concurrent", "test.runIf.sequential.todo", "test.runIf.sequential.fails", "test.runIf.todo.skip", "test.runIf.todo.only", "test.runIf.todo.concurrent", "test.runIf.todo.sequential", "test.runIf.todo.fails", "test.runIf.fails.skip", "test.runIf.fails.only", "test.runIf.fails.concurrent", "test.runIf.fails.sequential", "test.runIf.fails.todo", "test.skip.only.each", "test.skip.concurrent.each", "test.skip.sequential.each", "test.skip.todo.each", "test.skip.fails.each", "test.only.skip.each", "test.only.concurrent.each", "test.only.sequential.each", "test.only.todo.each", "test.only.fails.each", "test.concurrent.skip.each", "test.concurrent.only.each", "test.concurrent.sequential.each", "test.concurrent.todo.each", "test.concurrent.fails.each", "test.sequential.skip.each", "test.sequential.only.each", "test.sequential.concurrent.each", "test.sequential.todo.each", "test.sequential.fails.each", "test.todo.skip.each", "test.todo.only.each", "test.todo.concurrent.each", "test.todo.sequential.each", "test.todo.fails.each", "test.fails.skip.each", "test.fails.only.each", "test.fails.concurrent.each", "test.fails.sequential.each", "test.fails.todo.each", "test.extend.skipIf.skip", "test.extend.skipIf.only", "test.extend.skipIf.concurrent", "test.extend.skipIf.sequential", "test.extend.skipIf.todo", "test.extend.skipIf.fails", "test.extend.runIf.skip", "test.extend.runIf.only", "test.extend.runIf.concurrent", "test.extend.runIf.sequential", "test.extend.runIf.todo", "test.extend.runIf.fails", "test.extend.skip.each", "test.extend.only.each", "test.extend.concurrent.each", "test.extend.sequential.each", "test.extend.todo.each", "test.extend.fails.each", "test.skipIf.skip.each", "test.skipIf.only.each", "test.skipIf.concurrent.each", "test.skipIf.sequential.each", "test.skipIf.todo.each", "test.skipIf.fails.each", "test.runIf.skip.each", "test.runIf.only.each", "test.runIf.concurrent.each", "test.runIf.sequential.each", "test.runIf.todo.each", "test.runIf.fails.each", "test.extend.skipIf.each", "test.extend.runIf.each", "bench", "bench.skip", "bench.only", "bench.todo", "bench.skipIf", "bench.runIf", "bench.skip.only", "bench.skip.todo", "bench.only.skip", "bench.only.todo", "bench.todo.skip", "bench.todo.only", "bench.skipIf.skip", "bench.skipIf.only", "bench.skipIf.todo", "bench.runIf.skip", "bench.runIf.only", "bench.runIf.todo", "bench.skip.only.todo", "bench.skip.todo.only", "bench.only.skip.todo", "bench.only.todo.skip", "bench.todo.skip.only", "bench.todo.only.skip", "bench.skipIf.skip.only", "bench.skipIf.skip.todo", "bench.skipIf.only.skip", "bench.skipIf.only.todo", "bench.skipIf.todo.skip", "bench.skipIf.todo.only", "bench.runIf.skip.only", "bench.runIf.skip.todo", "bench.runIf.only.skip", "bench.runIf.only.todo", "bench.runIf.todo.skip", "bench.runIf.todo.only", "describe", "describe.skip", "describe.only", "describe.concurrent", "describe.sequential", "describe.shuffle", "describe.todo", "describe.skipIf", "describe.runIf", "describe.each", "describe.skip.only", "describe.skip.concurrent", "describe.skip.sequential", "describe.skip.shuffle", "describe.skip.todo", "describe.only.skip", "describe.only.concurrent", "describe.only.sequential", "describe.only.shuffle", "describe.only.todo", "describe.concurrent.skip", "describe.concurrent.only", "describe.concurrent.sequential", "describe.concurrent.shuffle", "describe.concurrent.todo", "describe.sequential.skip", "describe.sequential.only", "describe.sequential.concurrent", "describe.sequential.shuffle", "describe.sequential.todo", "describe.shuffle.skip", "describe.shuffle.only", "describe.shuffle.concurrent", "describe.shuffle.sequential", "describe.shuffle.todo", "describe.todo.skip", "describe.todo.only", "describe.todo.concurrent", "describe.todo.sequential", "describe.todo.shuffle", "describe.skipIf.skip", "describe.skipIf.only", "describe.skipIf.concurrent", "describe.skipIf.sequential", "describe.skipIf.shuffle", "describe.skipIf.todo", "describe.runIf.skip", "describe.runIf.only", "describe.runIf.concurrent", "describe.runIf.sequential", "describe.runIf.shuffle", "describe.runIf.todo", "describe.skip.each", "describe.only.each", "describe.concurrent.each", "describe.sequential.each", "describe.shuffle.each", "describe.todo.each", "describe.skipIf.each", "describe.runIf.each", "describe.skip.only.concurrent", "describe.skip.only.sequential", "describe.skip.only.shuffle", "describe.skip.only.todo", "describe.skip.concurrent.only", "describe.skip.concurrent.sequential", "describe.skip.concurrent.shuffle", "describe.skip.concurrent.todo", "describe.skip.sequential.only", "describe.skip.sequential.concurrent", "describe.skip.sequential.shuffle", "describe.skip.sequential.todo", "describe.skip.shuffle.only", "describe.skip.shuffle.concurrent", "describe.skip.shuffle.sequential", "describe.skip.shuffle.todo", "describe.skip.todo.only", "describe.skip.todo.concurrent", "describe.skip.todo.sequential", "describe.skip.todo.shuffle", "describe.only.skip.concurrent", "describe.only.skip.sequential", "describe.only.skip.shuffle", "describe.only.skip.todo", "describe.only.concurrent.skip", "describe.only.concurrent.sequential", "describe.only.concurrent.shuffle", "describe.only.concurrent.todo", "describe.only.sequential.skip", "describe.only.sequential.concurrent", "describe.only.sequential.shuffle", "describe.only.sequential.todo", "describe.only.shuffle.skip", "describe.only.shuffle.concurrent", "describe.only.shuffle.sequential", "describe.only.shuffle.todo", "describe.only.todo.skip", "describe.only.todo.concurrent", "describe.only.todo.sequential", "describe.only.todo.shuffle", "describe.concurrent.skip.only", "describe.concurrent.skip.sequential", "describe.concurrent.skip.shuffle", "describe.concurrent.skip.todo", "describe.concurrent.only.skip", "describe.concurrent.only.sequential", "describe.concurrent.only.shuffle", "describe.concurrent.only.todo", "describe.concurrent.sequential.skip", "describe.concurrent.sequential.only", "describe.concurrent.sequential.shuffle", "describe.concurrent.sequential.todo", "describe.concurrent.shuffle.skip", "describe.concurrent.shuffle.only", "describe.concurrent.shuffle.sequential", "describe.concurrent.shuffle.todo", "describe.concurrent.todo.skip", "describe.concurrent.todo.only", "describe.concurrent.todo.sequential", "describe.concurrent.todo.shuffle", "describe.sequential.skip.only", "describe.sequential.skip.concurrent", "describe.sequential.skip.shuffle", "describe.sequential.skip.todo", "describe.sequential.only.skip", "describe.sequential.only.concurrent", "describe.sequential.only.shuffle", "describe.sequential.only.todo", "describe.sequential.concurrent.skip", "describe.sequential.concurrent.only", "describe.sequential.concurrent.shuffle", "describe.sequential.concurrent.todo", "describe.sequential.shuffle.skip", "describe.sequential.shuffle.only", "describe.sequential.shuffle.concurrent", "describe.sequential.shuffle.todo", "describe.sequential.todo.skip", "describe.sequential.todo.only", "describe.sequential.todo.concurrent", "describe.sequential.todo.shuffle", "describe.shuffle.skip.only", "describe.shuffle.skip.concurrent", "describe.shuffle.skip.sequential", "describe.shuffle.skip.todo", "describe.shuffle.only.skip", "describe.shuffle.only.concurrent", "describe.shuffle.only.sequential", "describe.shuffle.only.todo", "describe.shuffle.concurrent.skip", "describe.shuffle.concurrent.only", "describe.shuffle.concurrent.sequential", "describe.shuffle.concurrent.todo", "describe.shuffle.sequential.skip", "describe.shuffle.sequential.only", "describe.shuffle.sequential.concurrent", "describe.shuffle.sequential.todo", "describe.shuffle.todo.skip", "describe.shuffle.todo.only", "describe.shuffle.todo.concurrent", "describe.shuffle.todo.sequential", "describe.todo.skip.only", "describe.todo.skip.concurrent", "describe.todo.skip.sequential", "describe.todo.skip.shuffle", "describe.todo.only.skip", "describe.todo.only.concurrent", "describe.todo.only.sequential", "describe.todo.only.shuffle", "describe.todo.concurrent.skip", "describe.todo.concurrent.only", "describe.todo.concurrent.sequential", "describe.todo.concurrent.shuffle", "describe.todo.sequential.skip", "describe.todo.sequential.only", "describe.todo.sequential.concurrent", "describe.todo.sequential.shuffle", "describe.todo.shuffle.skip", "describe.todo.shuffle.only", "describe.todo.shuffle.concurrent", "describe.todo.shuffle.sequential", "describe.skipIf.skip.only", "describe.skipIf.skip.concurrent", "describe.skipIf.skip.sequential", "describe.skipIf.skip.shuffle", "describe.skipIf.skip.todo", "describe.skipIf.only.skip", "describe.skipIf.only.concurrent", "describe.skipIf.only.sequential", "describe.skipIf.only.shuffle", "describe.skipIf.only.todo", "describe.skipIf.concurrent.skip", "describe.skipIf.concurrent.only", "describe.skipIf.concurrent.sequential", "describe.skipIf.concurrent.shuffle", "describe.skipIf.concurrent.todo", "describe.skipIf.sequential.skip", "describe.skipIf.sequential.only", "describe.skipIf.sequential.concurrent", "describe.skipIf.sequential.shuffle", "describe.skipIf.sequential.todo", "describe.skipIf.shuffle.skip", "describe.skipIf.shuffle.only", "describe.skipIf.shuffle.concurrent", "describe.skipIf.shuffle.sequential", "describe.skipIf.shuffle.todo", "describe.skipIf.todo.skip", "describe.skipIf.todo.only", "describe.skipIf.todo.concurrent", "describe.skipIf.todo.sequential", "describe.skipIf.todo.shuffle", "describe.runIf.skip.only", "describe.runIf.skip.concurrent", "describe.runIf.skip.sequential", "describe.runIf.skip.shuffle", "describe.runIf.skip.todo", "describe.runIf.only.skip", "describe.runIf.only.concurrent", "describe.runIf.only.sequential", "describe.runIf.only.shuffle", "describe.runIf.only.todo", "describe.runIf.concurrent.skip", "describe.runIf.concurrent.only", "describe.runIf.concurrent.sequential", "describe.runIf.concurrent.shuffle", "describe.runIf.concurrent.todo", "describe.runIf.sequential.skip", "describe.runIf.sequential.only", "describe.runIf.sequential.concurrent", "describe.runIf.sequential.shuffle", "describe.runIf.sequential.todo", "describe.runIf.shuffle.skip", "describe.runIf.shuffle.only", "describe.runIf.shuffle.concurrent", "describe.runIf.shuffle.sequential", "describe.runIf.shuffle.todo", "describe.runIf.todo.skip", "describe.runIf.todo.only", "describe.runIf.todo.concurrent", "describe.runIf.todo.sequential", "describe.runIf.todo.shuffle", "describe.skip.only.each", "describe.skip.concurrent.each", "describe.skip.sequential.each", "describe.skip.shuffle.each", "describe.skip.todo.each", "describe.only.skip.each", "describe.only.concurrent.each", "describe.only.sequential.each", "describe.only.shuffle.each", "describe.only.todo.each", "describe.concurrent.skip.each", "describe.concurrent.only.each", "describe.concurrent.sequential.each", "describe.concurrent.shuffle.each", "describe.concurrent.todo.each", "describe.sequential.skip.each", "describe.sequential.only.each", "describe.sequential.concurrent.each", "describe.sequential.shuffle.each", "describe.sequential.todo.each", "describe.shuffle.skip.each", "describe.shuffle.only.each", "describe.shuffle.concurrent.each", "describe.shuffle.sequential.each", "describe.shuffle.todo.each", "describe.todo.skip.each", "describe.todo.only.each", "describe.todo.concurrent.each", "describe.todo.sequential.each", "describe.todo.shuffle.each", "describe.skipIf.skip.each", "describe.skipIf.only.each", "describe.skipIf.concurrent.each", "describe.skipIf.sequential.each", "describe.skipIf.shuffle.each", "describe.skipIf.todo.each", "describe.runIf.skip.each", "describe.runIf.only.each", "describe.runIf.concurrent.each", "describe.runIf.sequential.each", "describe.runIf.shuffle.each", "describe.runIf.todo.each", "suite", "suite.skip", "suite.only", "suite.concurrent", "suite.sequential", "suite.shuffle", "suite.todo", "suite.skipIf", "suite.runIf", "suite.each", "suite.skip.only", "suite.skip.concurrent", "suite.skip.sequential", "suite.skip.shuffle", "suite.skip.todo", "suite.only.skip", "suite.only.concurrent", "suite.only.sequential", "suite.only.shuffle", "suite.only.todo", "suite.concurrent.skip", "suite.concurrent.only", "suite.concurrent.sequential", "suite.concurrent.shuffle", "suite.concurrent.todo", "suite.sequential.skip", "suite.sequential.only", "suite.sequential.concurrent", "suite.sequential.shuffle", "suite.sequential.todo", "suite.shuffle.skip", "suite.shuffle.only", "suite.shuffle.concurrent", "suite.shuffle.sequential", "suite.shuffle.todo", "suite.todo.skip", "suite.todo.only", "suite.todo.concurrent", "suite.todo.sequential", "suite.todo.shuffle", "suite.skipIf.skip", "suite.skipIf.only", "suite.skipIf.concurrent", "suite.skipIf.sequential", "suite.skipIf.shuffle", "suite.skipIf.todo", "suite.runIf.skip", "suite.runIf.only", "suite.runIf.concurrent", "suite.runIf.sequential", "suite.runIf.shuffle", "suite.runIf.todo", "suite.skip.each", "suite.only.each", "suite.concurrent.each", "suite.sequential.each", "suite.shuffle.each", "suite.todo.each", "suite.skipIf.each", "suite.runIf.each", "suite.skip.only.concurrent", "suite.skip.only.sequential", "suite.skip.only.shuffle", "suite.skip.only.todo", "suite.skip.concurrent.only", "suite.skip.concurrent.sequential", "suite.skip.concurrent.shuffle", "suite.skip.concurrent.todo", "suite.skip.sequential.only", "suite.skip.sequential.concurrent", "suite.skip.sequential.shuffle", "suite.skip.sequential.todo", "suite.skip.shuffle.only", "suite.skip.shuffle.concurrent", "suite.skip.shuffle.sequential", "suite.skip.shuffle.todo", "suite.skip.todo.only", "suite.skip.todo.concurrent", "suite.skip.todo.sequential", "suite.skip.todo.shuffle", "suite.only.skip.concurrent", "suite.only.skip.sequential", "suite.only.skip.shuffle", "suite.only.skip.todo", "suite.only.concurrent.skip", "suite.only.concurrent.sequential", "suite.only.concurrent.shuffle", "suite.only.concurrent.todo", "suite.only.sequential.skip", "suite.only.sequential.concurrent", "suite.only.sequential.shuffle", "suite.only.sequential.todo", "suite.only.shuffle.skip", "suite.only.shuffle.concurrent", "suite.only.shuffle.sequential", "suite.only.shuffle.todo", "suite.only.todo.skip", "suite.only.todo.concurrent", "suite.only.todo.sequential", "suite.only.todo.shuffle", "suite.concurrent.skip.only", "suite.concurrent.skip.sequential", "suite.concurrent.skip.shuffle", "suite.concurrent.skip.todo", "suite.concurrent.only.skip", "suite.concurrent.only.sequential", "suite.concurrent.only.shuffle", "suite.concurrent.only.todo", "suite.concurrent.sequential.skip", "suite.concurrent.sequential.only", "suite.concurrent.sequential.shuffle", "suite.concurrent.sequential.todo", "suite.concurrent.shuffle.skip", "suite.concurrent.shuffle.only", "suite.concurrent.shuffle.sequential", "suite.concurrent.shuffle.todo", "suite.concurrent.todo.skip", "suite.concurrent.todo.only", "suite.concurrent.todo.sequential", "suite.concurrent.todo.shuffle", "suite.sequential.skip.only", "suite.sequential.skip.concurrent", "suite.sequential.skip.shuffle", "suite.sequential.skip.todo", "suite.sequential.only.skip", "suite.sequential.only.concurrent", "suite.sequential.only.shuffle", "suite.sequential.only.todo", "suite.sequential.concurrent.skip", "suite.sequential.concurrent.only", "suite.sequential.concurrent.shuffle", "suite.sequential.concurrent.todo", "suite.sequential.shuffle.skip", "suite.sequential.shuffle.only", "suite.sequential.shuffle.concurrent", "suite.sequential.shuffle.todo", "suite.sequential.todo.skip", "suite.sequential.todo.only", "suite.sequential.todo.concurrent", "suite.sequential.todo.shuffle", "suite.shuffle.skip.only", "suite.shuffle.skip.concurrent", "suite.shuffle.skip.sequential", "suite.shuffle.skip.todo", "suite.shuffle.only.skip", "suite.shuffle.only.concurrent", "suite.shuffle.only.sequential", "suite.shuffle.only.todo", "suite.shuffle.concurrent.skip", "suite.shuffle.concurrent.only", "suite.shuffle.concurrent.sequential", "suite.shuffle.concurrent.todo", "suite.shuffle.sequential.skip", "suite.shuffle.sequential.only", "suite.shuffle.sequential.concurrent", "suite.shuffle.sequential.todo", "suite.shuffle.todo.skip", "suite.shuffle.todo.only", "suite.shuffle.todo.concurrent", "suite.shuffle.todo.sequential", "suite.todo.skip.only", "suite.todo.skip.concurrent", "suite.todo.skip.sequential", "suite.todo.skip.shuffle", "suite.todo.only.skip", "suite.todo.only.concurrent", "suite.todo.only.sequential", "suite.todo.only.shuffle", "suite.todo.concurrent.skip", "suite.todo.concurrent.only", "suite.todo.concurrent.sequential", "suite.todo.concurrent.shuffle", "suite.todo.sequential.skip", "suite.todo.sequential.only", "suite.todo.sequential.concurrent", "suite.todo.sequential.shuffle", "suite.todo.shuffle.skip", "suite.todo.shuffle.only", "suite.todo.shuffle.concurrent", "suite.todo.shuffle.sequential", "suite.skipIf.skip.only", "suite.skipIf.skip.concurrent", "suite.skipIf.skip.sequential", "suite.skipIf.skip.shuffle", "suite.skipIf.skip.todo", "suite.skipIf.only.skip", "suite.skipIf.only.concurrent", "suite.skipIf.only.sequential", "suite.skipIf.only.shuffle", "suite.skipIf.only.todo", "suite.skipIf.concurrent.skip", "suite.skipIf.concurrent.only", "suite.skipIf.concurrent.sequential", "suite.skipIf.concurrent.shuffle", "suite.skipIf.concurrent.todo", "suite.skipIf.sequential.skip", "suite.skipIf.sequential.only", "suite.skipIf.sequential.concurrent", "suite.skipIf.sequential.shuffle", "suite.skipIf.sequential.todo", "suite.skipIf.shuffle.skip", "suite.skipIf.shuffle.only", "suite.skipIf.shuffle.concurrent", "suite.skipIf.shuffle.sequential", "suite.skipIf.shuffle.todo", "suite.skipIf.todo.skip", "suite.skipIf.todo.only", "suite.skipIf.todo.concurrent", "suite.skipIf.todo.sequential", "suite.skipIf.todo.shuffle", "suite.runIf.skip.only", "suite.runIf.skip.concurrent", "suite.runIf.skip.sequential", "suite.runIf.skip.shuffle", "suite.runIf.skip.todo", "suite.runIf.only.skip", "suite.runIf.only.concurrent", "suite.runIf.only.sequential", "suite.runIf.only.shuffle", "suite.runIf.only.todo", "suite.runIf.concurrent.skip", "suite.runIf.concurrent.only", "suite.runIf.concurrent.sequential", "suite.runIf.concurrent.shuffle", "suite.runIf.concurrent.todo", "suite.runIf.sequential.skip", "suite.runIf.sequential.only", "suite.runIf.sequential.concurrent", "suite.runIf.sequential.shuffle", "suite.runIf.sequential.todo", "suite.runIf.shuffle.skip", "suite.runIf.shuffle.only", "suite.runIf.shuffle.concurrent", "suite.runIf.shuffle.sequential", "suite.runIf.shuffle.todo", "suite.runIf.todo.skip", "suite.runIf.todo.only", "suite.runIf.todo.concurrent", "suite.runIf.todo.sequential", "suite.runIf.todo.shuffle", "suite.skip.only.each", "suite.skip.concurrent.each", "suite.skip.sequential.each", "suite.skip.shuffle.each", "suite.skip.todo.each", "suite.only.skip.each", "suite.only.concurrent.each", "suite.only.sequential.each", "suite.only.shuffle.each", "suite.only.todo.each", "suite.concurrent.skip.each", "suite.concurrent.only.each", "suite.concurrent.sequential.each", "suite.concurrent.shuffle.each", "suite.concurrent.todo.each", "suite.sequential.skip.each", "suite.sequential.only.each", "suite.sequential.concurrent.each", "suite.sequential.shuffle.each", "suite.sequential.todo.each", "suite.shuffle.skip.each", "suite.shuffle.only.each", "suite.shuffle.concurrent.each", "suite.shuffle.sequential.each", "suite.shuffle.todo.each", "suite.todo.skip.each", "suite.todo.only.each", "suite.todo.concurrent.each", "suite.todo.sequential.each", "suite.todo.shuffle.each", "suite.skipIf.skip.each", "suite.skipIf.only.each", "suite.skipIf.concurrent.each", "suite.skipIf.sequential.each", "suite.skipIf.shuffle.each", "suite.skipIf.todo.each", "suite.runIf.skip.each", "suite.runIf.only.each", "suite.runIf.concurrent.each", "suite.runIf.sequential.each", "suite.runIf.shuffle.each", "suite.runIf.todo.each", "xtest", "xtest.each", "xit", "xit.each", "fit", "xdescribe", "xdescribe.each", "fdescribe"]); |
|
|
|
const isTypeOfVitestFnCall = (node, context, types) => { |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
return vitestFnCall !== null && types.includes(vitestFnCall.type); |
|
}; |
|
const parseVitestFnCall = (node, context) => { |
|
const vitestFnCall = parseVitestFnCallWithReason(node, context); |
|
if (typeof vitestFnCall === "string") |
|
return null; |
|
return vitestFnCall; |
|
}; |
|
const parseVitestFnCallCache = /* @__PURE__ */ new WeakMap(); |
|
const parseVitestFnCallWithReason = (node, context) => { |
|
let parsedVitestFnCall = parseVitestFnCallCache.get(node); |
|
if (parsedVitestFnCall) |
|
return parsedVitestFnCall; |
|
parsedVitestFnCall = parseVitestFnCallWithReasonInner(node, context); |
|
parseVitestFnCallCache.set(node, parsedVitestFnCall); |
|
return parsedVitestFnCall; |
|
}; |
|
const determineVitestFnType = (name) => { |
|
if (name === "expect") |
|
return "expect"; |
|
if (name === "expectTypeOf") |
|
return "expectTypeOf"; |
|
if (name === "vi") |
|
return "vi"; |
|
if (DescribeAlias.hasOwnProperty(name)) |
|
return "describe"; |
|
if (TestCaseName.hasOwnProperty(name)) |
|
return "test"; |
|
if (HookName.hasOwnProperty(name)) |
|
return "hook"; |
|
return "unknown"; |
|
}; |
|
const findModifiersAndMatcher = (members) => { |
|
const modifiers = []; |
|
for (const member of members) { |
|
if (member.parent?.type === AST_NODE_TYPES.MemberExpression && member.parent.parent?.type === AST_NODE_TYPES.CallExpression) { |
|
return { |
|
matcher: member, |
|
args: member.parent.parent.arguments, |
|
modifiers |
|
}; |
|
} |
|
const name = getAccessorValue(member); |
|
if (modifiers.length === 0) { |
|
if (!ModifierName.hasOwnProperty(name)) |
|
return "modifier-unknown"; |
|
} else if (modifiers.length === 1) { |
|
if (name !== ModifierName.not) |
|
return "modifier-unknown"; |
|
const firstModifier = getAccessorValue(modifiers[0]); |
|
if (firstModifier !== ModifierName.resolves && firstModifier !== ModifierName.rejects) |
|
return "modifier-unknown"; |
|
} else { |
|
return "modifier-unknown"; |
|
} |
|
modifiers.push(member); |
|
} |
|
return "matcher-not-found"; |
|
}; |
|
const parseVitestExpectCall = (typelessParsedVitestFnCall, type) => { |
|
const modifiersMatcher = findModifiersAndMatcher(typelessParsedVitestFnCall.members); |
|
if (typeof modifiersMatcher === "string") |
|
return modifiersMatcher; |
|
return { |
|
...typelessParsedVitestFnCall, |
|
type, |
|
...modifiersMatcher |
|
}; |
|
}; |
|
const findTopMostCallExpression = (node) => { |
|
let topMostCallExpression = node; |
|
let { parent } = node; |
|
while (parent) { |
|
if (parent.type === AST_NODE_TYPES.CallExpression) { |
|
topMostCallExpression = parent; |
|
parent = parent.parent; |
|
continue; |
|
} |
|
if (parent.type !== AST_NODE_TYPES.MemberExpression) |
|
break; |
|
parent = parent.parent; |
|
} |
|
return topMostCallExpression; |
|
}; |
|
const parseVitestFnCallWithReasonInner = (node, context) => { |
|
const chain = getNodeChain(node); |
|
if (!chain?.length) |
|
return null; |
|
const [first, ...rest] = chain; |
|
const lastLink = getAccessorValue(chain[chain.length - 1]); |
|
if (lastLink === "each") { |
|
if (node.callee.type !== AST_NODE_TYPES.CallExpression && node.callee.type !== AST_NODE_TYPES.TaggedTemplateExpression) |
|
return null; |
|
} |
|
if (node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression && lastLink !== "each") |
|
return null; |
|
const resolved = resolveVitestFn(context, node, getAccessorValue(first)); |
|
if (!resolved) |
|
return null; |
|
const name = resolved.original ?? resolved.local; |
|
const links = [name, ...rest.map(getAccessorValue)]; |
|
if (name !== "vi" && name !== "expect" && name !== "expectTypeOf" && !ValidVitestFnCallChains.has(links.join("."))) |
|
return null; |
|
const parsedVitestFnCall = { |
|
name, |
|
head: { ...resolved, node: first }, |
|
members: rest |
|
}; |
|
const type = determineVitestFnType(name); |
|
if (type === "expect" || type === "expectTypeOf") { |
|
const result = parseVitestExpectCall(parsedVitestFnCall, type); |
|
if (typeof result === "string" && findTopMostCallExpression(node) !== node) |
|
return null; |
|
if (result === "matcher-not-found") { |
|
if (node.parent?.type === AST_NODE_TYPES.MemberExpression) |
|
return "matcher-not-called"; |
|
} |
|
return result; |
|
} |
|
if (chain.slice(0, chain.length - 1).some((node2) => node2.parent?.type !== AST_NODE_TYPES.MemberExpression)) |
|
return null; |
|
if (node.parent?.type === AST_NODE_TYPES.CallExpression || node.parent?.type === AST_NODE_TYPES.MemberExpression) |
|
return null; |
|
return { ...parsedVitestFnCall, type }; |
|
}; |
|
const joinChains = (a, b) => a && b ? [...a, ...b] : null; |
|
function getNodeChain(node) { |
|
if (isSupportedAccessor(node)) |
|
return [node]; |
|
switch (node.type) { |
|
case AST_NODE_TYPES.TaggedTemplateExpression: |
|
return getNodeChain(node.tag); |
|
case AST_NODE_TYPES.MemberExpression: |
|
return joinChains(getNodeChain(node.object), getNodeChain(node.property)); |
|
case AST_NODE_TYPES.CallExpression: |
|
return getNodeChain(node.callee); |
|
} |
|
return null; |
|
} |
|
const resolveVitestFn = (context, node, identifier) => { |
|
const scope = context.sourceCode.getScope ? context.sourceCode.getScope(node) : context.getScope(); |
|
const maybeImport = resolveScope(scope, identifier); |
|
if (maybeImport === "local") |
|
return null; |
|
if (maybeImport) { |
|
if (maybeImport.source === "vitest") { |
|
return { |
|
original: maybeImport.imported, |
|
local: maybeImport.local, |
|
type: "import" |
|
}; |
|
} |
|
return null; |
|
} |
|
return { |
|
original: resolvePossibleAliasedGlobal(identifier, context), |
|
local: identifier, |
|
type: "global" |
|
}; |
|
}; |
|
const resolvePossibleAliasedGlobal = (global, context) => { |
|
const globalAliases = context.settings.vitest?.globalAliases ?? {}; |
|
const alias = Object.entries(globalAliases).find(([_, aliases]) => aliases.includes(global)); |
|
if (alias) |
|
return alias[0]; |
|
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?.local === identifier) |
|
return importDetails; |
|
return "local"; |
|
} |
|
currentScope = currentScope.upper; |
|
} |
|
return null; |
|
}; |
|
const findImportSourceNode = (node) => { |
|
if (node.type === AST_NODE_TYPES.AwaitExpression) { |
|
if (node.argument.type === AST_NODE_TYPES.ImportExpression) |
|
return node.argument.source; |
|
return null; |
|
} |
|
if (node.type === AST_NODE_TYPES.CallExpression && isIdentifier(node.callee, "require")) |
|
return node.arguments[0] ?? null; |
|
return null; |
|
}; |
|
const describeImportDefAsImport = (def) => { |
|
if (def.parent.type === AST_NODE_TYPES.TSImportEqualsDeclaration) |
|
return null; |
|
if (def.node.type !== AST_NODE_TYPES.ImportSpecifier) |
|
return null; |
|
if (def.parent.importKind === "type") |
|
return null; |
|
return { |
|
source: def.parent.source.value, |
|
imported: def.node.imported.name, |
|
local: def.node.local.name |
|
}; |
|
}; |
|
const describePossibleImportDef = (def) => { |
|
if (def.type === "Variable") |
|
return describeVariableDefAsImport(def); |
|
if (def.type === "ImportBinding") |
|
return describeImportDefAsImport(def); |
|
return null; |
|
}; |
|
const describeVariableDefAsImport = (def) => { |
|
if (!def.node.init) |
|
return null; |
|
const sourceNode = findImportSourceNode(def.node.init); |
|
if (!sourceNode || !isStringNode(sourceNode)) |
|
return null; |
|
if (def.name.parent?.type !== AST_NODE_TYPES.Property) |
|
return null; |
|
if (!isSupportedAccessor(def.name.parent.key)) |
|
return null; |
|
return { |
|
source: getStringValue(sourceNode), |
|
imported: getAccessorValue(def.name.parent.key), |
|
local: def.name.name |
|
}; |
|
}; |
|
const getTestCallExpressionsFromDeclaredVariables = (declaredVariables, context) => { |
|
return declaredVariables.reduce( |
|
(acc, { references }) => acc.concat( |
|
references.map(({ identifier }) => identifier.parent).filter( |
|
(node) => node?.type === AST_NODE_TYPES.CallExpression && isTypeOfVitestFnCall(node, context, ["test"]) |
|
) |
|
), |
|
[] |
|
); |
|
}; |
|
const getFirstMatcherArg = (expectFnCall) => { |
|
const [firstArg] = expectFnCall.args; |
|
if (firstArg.type === AST_NODE_TYPES.SpreadElement) |
|
return firstArg; |
|
return followTypeAssertionChain$1(firstArg); |
|
}; |
|
const isTypeCastExpression$1 = (node) => node.type === AST_NODE_TYPES.TSAsExpression || node.type === AST_NODE_TYPES.TSTypeAssertion; |
|
const followTypeAssertionChain$1 = (expression) => isTypeCastExpression$1(expression) ? followTypeAssertionChain$1(expression.expression) : expression; |
|
|
|
const RULE_NAME$Y = "prefer-lowercase-title"; |
|
const hasStringAsFirstArgument = (node) => node.arguments[0] && isStringNode(node.arguments[0]); |
|
const populateIgnores = (ignore) => { |
|
const ignores = []; |
|
if (ignore.includes(DescribeAlias.describe)) |
|
ignores.push(...Object.keys(DescribeAlias)); |
|
if (ignore.includes(TestCaseName.test)) { |
|
ignores.push( |
|
...Object.keys(TestCaseName).filter((k) => k.endsWith(TestCaseName.test)) |
|
); |
|
} |
|
if (ignore.includes(TestCaseName.it)) { |
|
ignores.push( |
|
...Object.keys(TestCaseName).filter((k) => k.endsWith(TestCaseName.it)) |
|
); |
|
} |
|
return ignores; |
|
}; |
|
const lowerCaseTitle = createEslintRule({ |
|
name: RULE_NAME$Y, |
|
meta: { |
|
type: "problem", |
|
docs: { |
|
description: "enforce lowercase titles", |
|
recommended: false |
|
}, |
|
fixable: "code", |
|
messages: { |
|
lowerCaseTitle: "`{{ method }}`s should begin with lowercase", |
|
fullyLowerCaseTitle: "`{{ method }}`s should be lowercase" |
|
}, |
|
schema: [ |
|
{ |
|
type: "object", |
|
properties: { |
|
ignore: { |
|
type: "array", |
|
items: { |
|
type: "string", |
|
enum: [ |
|
DescribeAlias.describe, |
|
TestCaseName.test, |
|
TestCaseName.it |
|
] |
|
} |
|
}, |
|
allowedPrefixes: { |
|
type: "array", |
|
items: { type: "string" }, |
|
additionalItems: false |
|
}, |
|
ignoreTopLevelDescribe: { |
|
type: "boolean", |
|
default: false |
|
}, |
|
lowercaseFirstCharacterOnly: { |
|
type: "boolean", |
|
default: true |
|
} |
|
}, |
|
additionalProperties: false |
|
} |
|
] |
|
}, |
|
defaultOptions: [ |
|
{ ignore: [], allowedPrefixes: [], ignoreTopLevelDescribe: false, lowercaseFirstCharacterOnly: true } |
|
], |
|
create: (context, [{ ignore = [], allowedPrefixes = [], ignoreTopLevelDescribe = false, lowercaseFirstCharacterOnly = false }]) => { |
|
const ignores = populateIgnores(ignore); |
|
let numberOfDescribeBlocks = 0; |
|
return { |
|
CallExpression(node) { |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (!vitestFnCall || !hasStringAsFirstArgument) |
|
return; |
|
if (vitestFnCall?.type === "describe") { |
|
numberOfDescribeBlocks++; |
|
if (ignoreTopLevelDescribe && numberOfDescribeBlocks === 1) |
|
return; |
|
} else if (vitestFnCall?.type !== "test") { |
|
return; |
|
} |
|
const [firstArgument] = node.arguments; |
|
const description = getStringValue(firstArgument); |
|
if (typeof description !== "string") |
|
return; |
|
if (allowedPrefixes.some((prefix) => description.startsWith(prefix))) |
|
return; |
|
const firstCharacter = description.charAt(0); |
|
if (ignores.includes(vitestFnCall.name) || lowercaseFirstCharacterOnly && (!firstCharacter || firstCharacter === firstCharacter.toLowerCase()) || !lowercaseFirstCharacterOnly && description === description.toLowerCase()) |
|
return; |
|
context.report({ |
|
messageId: lowercaseFirstCharacterOnly ? "lowerCaseTitle" : "fullyLowerCaseTitle", |
|
node: node.arguments[0], |
|
data: { |
|
method: vitestFnCall.name |
|
}, |
|
fix: (fixer) => { |
|
const description2 = getStringValue(firstArgument); |
|
const rangeIgnoreQuotes = [ |
|
firstArgument.range[0] + 1, |
|
firstArgument.range[1] - 1 |
|
]; |
|
const newDescription = lowercaseFirstCharacterOnly ? description2.substring(0, 1).toLowerCase() + description2.substring(1) : description2.toLowerCase(); |
|
return [fixer.replaceTextRange(rangeIgnoreQuotes, newDescription)]; |
|
} |
|
}); |
|
}, |
|
"CallExpression:exit"(node) { |
|
if (isTypeOfVitestFnCall(node, context, ["describe"])) |
|
numberOfDescribeBlocks--; |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$X = "max-nested-describe"; |
|
const maxNestedDescribe = createEslintRule({ |
|
name: RULE_NAME$X, |
|
meta: { |
|
type: "problem", |
|
docs: { |
|
description: "require describe block to be less than set max value or default value", |
|
recommended: false |
|
}, |
|
schema: [ |
|
{ |
|
type: "object", |
|
properties: { |
|
max: { |
|
type: "number" |
|
} |
|
}, |
|
additionalProperties: false |
|
} |
|
], |
|
messages: { |
|
maxNestedDescribe: "Nested describe block should be less than set max value." |
|
} |
|
}, |
|
defaultOptions: [ |
|
{ |
|
max: 5 |
|
} |
|
], |
|
create(context, [{ max }]) { |
|
const stack = []; |
|
function pushStack(node) { |
|
if (node.parent?.type !== "CallExpression") |
|
return; |
|
if (node.parent.callee.type !== "Identifier" || node.parent.callee.name !== "describe") |
|
return; |
|
stack.push(0); |
|
if (stack.length > max) { |
|
context.report({ |
|
node: node.parent, |
|
messageId: "maxNestedDescribe" |
|
}); |
|
} |
|
} |
|
function popStack(node) { |
|
if (node.parent?.type !== "CallExpression") |
|
return; |
|
if (node.parent.callee.type !== "Identifier" || node.parent.callee.name !== "describe") |
|
return; |
|
stack.pop(); |
|
} |
|
return { |
|
"FunctionExpression": pushStack, |
|
"FunctionExpression:exit": popStack, |
|
"ArrowFunctionExpression": pushStack, |
|
"ArrowFunctionExpression:exit": popStack |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$W = "no-identical-title"; |
|
const newDescribeContext = () => ({ |
|
describeTitles: [], |
|
testTitles: [] |
|
}); |
|
const noIdenticalTitle = createEslintRule({ |
|
name: RULE_NAME$W, |
|
meta: { |
|
type: "problem", |
|
docs: { |
|
description: "disallow identical titles", |
|
recommended: false |
|
}, |
|
fixable: "code", |
|
schema: [], |
|
messages: { |
|
multipleTestTitle: "Test is used multiple times in the same describe(suite) block", |
|
multipleDescribeTitle: "Describe is used multiple times in the same describe(suite) block" |
|
} |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
const stack = [newDescribeContext()]; |
|
return { |
|
CallExpression(node) { |
|
const currentStack = stack[stack.length - 1]; |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (!vitestFnCall) |
|
return; |
|
if (vitestFnCall.name === "describe" || vitestFnCall.name === "suite") |
|
stack.push(newDescribeContext()); |
|
if (vitestFnCall.members.find((s) => isSupportedAccessor(s, "each"))) |
|
return; |
|
const [argument] = node.arguments; |
|
if (!argument || !isStringNode(argument)) |
|
return; |
|
const title = getStringValue(argument); |
|
if (vitestFnCall.type === "test") { |
|
if (currentStack?.testTitles.includes(title)) { |
|
context.report({ |
|
node, |
|
messageId: "multipleTestTitle" |
|
}); |
|
} |
|
currentStack?.testTitles.push(title); |
|
} |
|
if (vitestFnCall.type !== "describe") |
|
return; |
|
if (currentStack?.describeTitles.includes(title)) { |
|
context.report({ |
|
node, |
|
messageId: "multipleDescribeTitle" |
|
}); |
|
} |
|
currentStack?.describeTitles.push(title); |
|
}, |
|
"CallExpression:exit"(node) { |
|
if (isTypeOfVitestFnCall(node, context, ["describe"])) |
|
stack.pop(); |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$V = "no-focused-tests"; |
|
const isTestOrDescribe = (node) => { |
|
return node.type === "Identifier" && ["it", "test", "describe"].includes(node.name); |
|
}; |
|
const isOnly = (node) => { |
|
return node.type === "Identifier" && node.name === "only"; |
|
}; |
|
const noFocusedTests = createEslintRule({ |
|
name: RULE_NAME$V, |
|
meta: { |
|
type: "problem", |
|
docs: { |
|
description: "disallow focused tests", |
|
recommended: false |
|
}, |
|
fixable: "code", |
|
schema: [ |
|
{ |
|
type: "object", |
|
properties: { |
|
fixable: { |
|
type: "boolean", |
|
default: true |
|
} |
|
}, |
|
additionalProperties: false |
|
} |
|
], |
|
messages: { |
|
noFocusedTests: "Focused tests are not allowed." |
|
} |
|
}, |
|
defaultOptions: [{ fixable: true }], |
|
create: (context) => { |
|
const fixable = context.options[0]?.fixable; |
|
return { |
|
ExpressionStatement(node) { |
|
if (node.expression.type === "CallExpression") { |
|
const { callee } = node.expression; |
|
if (callee.type === "MemberExpression" && isTestOrDescribe(callee.object) && isOnly(callee.property)) { |
|
context.report({ |
|
node: callee.property, |
|
messageId: "noFocusedTests", |
|
fix: (fixer) => fixable ? fixer.removeRange([callee.property.range[0] - 1, callee.property.range[1]]) : null |
|
}); |
|
} |
|
if (callee.type === "TaggedTemplateExpression") { |
|
const tagCall = callee.tag.type === "MemberExpression" ? callee.tag.object : null; |
|
if (!tagCall) |
|
return; |
|
if (tagCall.type === "MemberExpression" && isTestOrDescribe(tagCall.object) && isOnly(tagCall.property)) { |
|
context.report({ |
|
node: tagCall.property, |
|
messageId: "noFocusedTests", |
|
fix: (fixer) => fixable ? fixer.removeRange([tagCall.property.range[0] - 1, tagCall.property.range[1]]) : null |
|
}); |
|
} |
|
} |
|
} |
|
}, |
|
CallExpression(node) { |
|
if (node.callee.type === "CallExpression") { |
|
const { callee } = node.callee; |
|
if (callee.type === "MemberExpression" && callee.object.type === "MemberExpression" && isTestOrDescribe(callee.object.object) && isOnly(callee.object.property) && callee.property.type === "Identifier" && callee.property.name === "each") { |
|
const onlyCallee = callee.object.property; |
|
context.report({ |
|
node: callee.object.property, |
|
messageId: "noFocusedTests", |
|
fix: (fixer) => fixable ? fixer.removeRange([ |
|
onlyCallee.range[0] - 1, |
|
onlyCallee.range[1] |
|
]) : null |
|
}); |
|
} |
|
} |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$U = "no-conditional-tests"; |
|
const noConditionalTest = createEslintRule({ |
|
name: RULE_NAME$U, |
|
meta: { |
|
type: "problem", |
|
docs: { |
|
description: "disallow conditional tests", |
|
recommended: false |
|
}, |
|
schema: [], |
|
messages: { |
|
noConditionalTests: "Avoid using if conditions in a test." |
|
} |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
return { |
|
Identifier: function(node) { |
|
if (["test", "it", "describe"].includes(node.name)) { |
|
if (node.parent?.parent?.parent?.parent?.type === "IfStatement") { |
|
context.report({ |
|
node, |
|
messageId: "noConditionalTests" |
|
}); |
|
} |
|
} |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const DEFAULTS = { |
|
typecheck: false |
|
}; |
|
function parsePluginSettings(settings) { |
|
const pluginSettings = typeof settings.vitest !== "object" || settings.vitest === null ? {} : settings.vitest; |
|
return { |
|
...DEFAULTS, |
|
...pluginSettings |
|
}; |
|
} |
|
|
|
const RULE_NAME$T = "expect-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) |
|
); |
|
} |
|
const expectExpect = createEslintRule({ |
|
name: RULE_NAME$T, |
|
meta: { |
|
type: "suggestion", |
|
docs: { |
|
description: "enforce having expectation in test body", |
|
recommended: false |
|
}, |
|
schema: [ |
|
{ |
|
type: "object", |
|
properties: { |
|
assertFunctionNames: { |
|
type: "array", |
|
items: [{ type: "string" }] |
|
}, |
|
additionalTestBlockFunctions: { |
|
type: "array", |
|
items: { type: "string" } |
|
} |
|
}, |
|
additionalProperties: false |
|
} |
|
], |
|
messages: { |
|
noAssertions: "Test has no assertions" |
|
} |
|
}, |
|
defaultOptions: [{ assertFunctionNames: ["expect", "assert"], additionalTestBlockFunctions: [] }], |
|
create(context, [{ assertFunctionNames = ["expect"], additionalTestBlockFunctions = [] }]) { |
|
const unchecked = []; |
|
const settings = parsePluginSettings(context.settings); |
|
if (settings.typecheck) |
|
assertFunctionNames.push("expectTypeOf", "assertType"); |
|
function checkCallExpression(nodes) { |
|
for (const node of nodes) { |
|
const index = node.type === AST_NODE_TYPES.CallExpression ? unchecked.indexOf(node) : -1; |
|
if (node.type === AST_NODE_TYPES.FunctionDeclaration) { |
|
const declaredVariables = context.sourceCode.getDeclaredVariables(node); |
|
const testCallExpressions = getTestCallExpressionsFromDeclaredVariables(declaredVariables, context); |
|
checkCallExpression(testCallExpressions); |
|
} |
|
if (index !== -1) { |
|
unchecked.splice(index, 1); |
|
break; |
|
} |
|
} |
|
} |
|
return { |
|
CallExpression(node) { |
|
if (node.callee.type === AST_NODE_TYPES.Identifier && node.callee.name === "bench") |
|
return; |
|
if (node?.callee?.type === AST_NODE_TYPES.MemberExpression && node.callee.property.type === AST_NODE_TYPES.Identifier && node.callee.property.name === "extend") |
|
return; |
|
if (node?.callee?.type === AST_NODE_TYPES.MemberExpression && node.callee.property.type === AST_NODE_TYPES.Identifier && node.callee.property.name === "skip") |
|
return; |
|
const name = getNodeName(node) ?? ""; |
|
if (isTypeOfVitestFnCall(node, context, ["test"]) || additionalTestBlockFunctions.includes(name)) { |
|
if (node.callee.type === AST_NODE_TYPES.MemberExpression && isSupportedAccessor(node.callee.property, "todo")) |
|
return; |
|
unchecked.push(node); |
|
} else if (matchesAssertFunctionName(name, assertFunctionNames)) { |
|
checkCallExpression(context.sourceCode.getAncestors(node)); |
|
} |
|
}, |
|
"Program:exit"() { |
|
unchecked.forEach((node) => { |
|
context.report({ |
|
node: node.callee, |
|
messageId: "noAssertions" |
|
}); |
|
}); |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$S = "consistent-test-it"; |
|
const buildFixer = (callee, nodeName, preferredTestKeyword) => (fixer) => [ |
|
fixer.replaceText( |
|
callee.type === AST_NODE_TYPES.MemberExpression ? callee.object : callee, |
|
getPreferredNodeName(nodeName, preferredTestKeyword) |
|
) |
|
]; |
|
function getPreferredNodeName(nodeName, preferredTestKeyword) { |
|
if (nodeName === TestCaseName.fit) |
|
return "test.only"; |
|
return nodeName.startsWith("f") || nodeName.startsWith("x") ? nodeName.charAt(0) + preferredTestKeyword : preferredTestKeyword; |
|
} |
|
function getOppositeTestKeyword(test) { |
|
if (test === TestCaseName.test) |
|
return TestCaseName.it; |
|
return TestCaseName.test; |
|
} |
|
const consistentTestIt = createEslintRule({ |
|
name: RULE_NAME$S, |
|
meta: { |
|
type: "suggestion", |
|
fixable: "code", |
|
docs: { |
|
description: "enforce using test or it but not both", |
|
recommended: false |
|
}, |
|
messages: { |
|
consistentMethod: "Prefer using {{ testFnKeyWork }} instead of {{ oppositeTestKeyword }}", |
|
consistentMethodWithinDescribe: "Prefer using {{ testKeywordWithinDescribe }} instead of {{ oppositeTestKeyword }} within describe" |
|
}, |
|
schema: [ |
|
{ |
|
type: "object", |
|
properties: { |
|
fn: { |
|
type: "string", |
|
enum: [TestCaseName.test, TestCaseName.it] |
|
}, |
|
withinDescribe: { |
|
type: "string", |
|
enum: [TestCaseName.test, TestCaseName.it] |
|
} |
|
}, |
|
additionalProperties: false |
|
} |
|
] |
|
}, |
|
defaultOptions: [{ fn: TestCaseName.test, withinDescribe: TestCaseName.it }], |
|
create(context) { |
|
const config = context.options[0] ?? {}; |
|
const testFnKeyWork = config.fn || TestCaseName.test; |
|
const testKeywordWithinDescribe = config?.withinDescribe || config?.fn || TestCaseName?.it; |
|
const testFnDisabled = testFnKeyWork === testKeywordWithinDescribe ? testFnKeyWork : void 0; |
|
let describeNestingLevel = 0; |
|
return { |
|
ImportDeclaration(node) { |
|
if (testFnDisabled == null) |
|
return; |
|
if (node.source.type !== "Literal" || node.source.value !== "vitest") |
|
return; |
|
const oppositeTestKeyword = getOppositeTestKeyword(testFnDisabled); |
|
for (const specifier of node.specifiers) { |
|
if (specifier.type !== "ImportSpecifier") |
|
continue; |
|
if (specifier.local.name !== specifier.imported.name) |
|
continue; |
|
if (specifier.local.name === oppositeTestKeyword) { |
|
context.report({ |
|
node: specifier, |
|
data: { testFnKeyWork, oppositeTestKeyword }, |
|
messageId: "consistentMethod", |
|
fix: (fixer) => fixer.replaceText( |
|
specifier.local, |
|
testFnDisabled |
|
) |
|
}); |
|
} |
|
} |
|
}, |
|
CallExpression(node) { |
|
if (node.callee.type === AST_NODE_TYPES.Identifier && node.callee.name === "bench") |
|
return; |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (!vitestFnCall) |
|
return; |
|
if (vitestFnCall.type === "describe") { |
|
describeNestingLevel++; |
|
return; |
|
} |
|
const funcNode = node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression ? node.callee.tag : node.callee.type === AST_NODE_TYPES.CallExpression ? node.callee.callee : node.callee; |
|
if (vitestFnCall.type === "test" && describeNestingLevel === 0 && !vitestFnCall.name.endsWith(testFnKeyWork)) { |
|
const oppositeTestKeyword = getOppositeTestKeyword(testFnKeyWork); |
|
context.report({ |
|
node: node.callee, |
|
data: { testFnKeyWork, oppositeTestKeyword }, |
|
messageId: "consistentMethod", |
|
fix: buildFixer(funcNode, vitestFnCall.name, testFnKeyWork) |
|
}); |
|
} else if (vitestFnCall.type === "test" && describeNestingLevel > 0 && !vitestFnCall.name.endsWith(testKeywordWithinDescribe)) { |
|
const oppositeTestKeyword = getOppositeTestKeyword(testKeywordWithinDescribe); |
|
context.report({ |
|
messageId: "consistentMethodWithinDescribe", |
|
node: node.callee, |
|
data: { testKeywordWithinDescribe, oppositeTestKeyword }, |
|
fix: buildFixer(funcNode, vitestFnCall.name, testKeywordWithinDescribe) |
|
}); |
|
} |
|
}, |
|
"CallExpression:exit"(node) { |
|
if (isTypeOfVitestFnCall(node, context, ["describe"])) |
|
describeNestingLevel--; |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$R = "prefer-to-be"; |
|
const isNullLiteral = (node) => node.type === AST_NODE_TYPES.Literal && node.value === null; |
|
const isNullEqualityMatcher = (expectFnCall) => isNullLiteral(getFirstMatcherArg(expectFnCall)); |
|
const isFirstArgumentIdentifier = (expectFnCall, name) => isIdentifier(getFirstMatcherArg(expectFnCall), name); |
|
const isFloat = (v) => Math.floor(v) !== Math.ceil(v); |
|
const shouldUseToBe = (expectFnCall) => { |
|
let firstArg = getFirstMatcherArg(expectFnCall); |
|
if (firstArg.type === AST_NODE_TYPES.Literal && typeof firstArg.value === "number" && isFloat(firstArg.value)) |
|
return false; |
|
if (firstArg.type === AST_NODE_TYPES.UnaryExpression && firstArg.operator === "-") |
|
firstArg = firstArg.argument; |
|
if (firstArg.type === AST_NODE_TYPES.Literal) { |
|
return !("regex" in firstArg); |
|
} |
|
return firstArg.type === AST_NODE_TYPES.TemplateLiteral; |
|
}; |
|
const reportPreferToBe = (context, whatToBe, expectFnCall, func, modifierNode) => { |
|
context.report({ |
|
messageId: `useToBe${whatToBe}`, |
|
fix(fixer) { |
|
const fixes = [ |
|
replaceAccessorFixer(fixer, expectFnCall.matcher, `toBe${whatToBe}`) |
|
]; |
|
if (expectFnCall.args?.length && whatToBe !== "") |
|
fixes.push(removeExtraArgumentsFixer(fixer, context, func, 0)); |
|
if (modifierNode) { |
|
fixes.push( |
|
fixer.removeRange([modifierNode.range[0] - 1, modifierNode.range[1]]) |
|
); |
|
} |
|
return fixes; |
|
}, |
|
node: expectFnCall.matcher |
|
}); |
|
}; |
|
const preferToBe = createEslintRule({ |
|
name: RULE_NAME$R, |
|
meta: { |
|
type: "suggestion", |
|
docs: { |
|
description: "enforce using toBe()", |
|
recommended: false |
|
}, |
|
schema: [], |
|
fixable: "code", |
|
messages: { |
|
useToBe: "Use `toBe` instead", |
|
useToBeUndefined: "Use `toBeUndefined()` instead", |
|
useToBeDefined: "Use `toBeDefined()` instead", |
|
useToBeNull: "Use `toBeNull()` instead", |
|
useToBeNaN: "Use `toBeNaN()` instead" |
|
} |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
return { |
|
CallExpression(node) { |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (vitestFnCall?.type !== "expect") |
|
return; |
|
const matcherName = getAccessorValue(vitestFnCall.matcher); |
|
const notModifier = vitestFnCall.modifiers.find((node2) => getAccessorValue(node2) === "not"); |
|
if (notModifier && ["toBeUndefined", "toBeDefined"].includes(matcherName)) { |
|
reportPreferToBe(context, matcherName === "toBeDefined" ? "Undefined" : "Defined", vitestFnCall, node, notModifier); |
|
return; |
|
} |
|
if (!EqualityMatcher.hasOwnProperty(matcherName) || vitestFnCall.args.length === 0) |
|
return; |
|
if (isNullEqualityMatcher(vitestFnCall)) { |
|
reportPreferToBe(context, "Null", vitestFnCall, node); |
|
return; |
|
} |
|
if (isFirstArgumentIdentifier(vitestFnCall, "undefined")) { |
|
const name = notModifier ? "Defined" : "Undefined"; |
|
reportPreferToBe(context, name, vitestFnCall, node); |
|
return; |
|
} |
|
if (isFirstArgumentIdentifier(vitestFnCall, "NaN")) { |
|
reportPreferToBe(context, "NaN", vitestFnCall, node); |
|
return; |
|
} |
|
if (shouldUseToBe(vitestFnCall) && matcherName !== EqualityMatcher.toBe) |
|
reportPreferToBe(context, "", vitestFnCall, node); |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$Q = "no-hooks"; |
|
const noHooks = createEslintRule({ |
|
name: RULE_NAME$Q, |
|
meta: { |
|
type: "suggestion", |
|
docs: { |
|
description: "disallow setup and teardown hooks", |
|
recommended: false |
|
}, |
|
schema: [{ |
|
type: "object", |
|
properties: { |
|
allow: { |
|
type: "array", |
|
//@ts-ignore |
|
contains: ["beforeAll", "beforeEach", "afterAll", "afterEach"] |
|
} |
|
}, |
|
additionalProperties: false |
|
}], |
|
messages: { |
|
unexpectedHook: "Unexpected '{{ hookName }}' hook" |
|
} |
|
}, |
|
defaultOptions: [{ allow: [] }], |
|
create(context, [{ allow = [] }]) { |
|
return { |
|
CallExpression(node) { |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (vitestFnCall?.type === "hook" && !allow.includes(vitestFnCall.name)) { |
|
context.report({ |
|
node, |
|
messageId: "unexpectedHook", |
|
data: { hookName: vitestFnCall.name } |
|
}); |
|
} |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$P = "no-restricted-vi-methods"; |
|
const noRestrictedViMethods = createEslintRule({ |
|
name: RULE_NAME$P, |
|
meta: { |
|
type: "suggestion", |
|
docs: { |
|
description: "disallow specific `vi.` methods", |
|
recommended: false |
|
}, |
|
schema: [{ |
|
type: "object", |
|
additionalProperties: { type: ["string", "null"] } |
|
}], |
|
messages: { |
|
restrictedViMethod: "Use of `{{ restriction }}` is disallowed", |
|
restrictedViMethodWithMessage: "{{ message }}" |
|
} |
|
}, |
|
defaultOptions: [{}], |
|
create(context, [restrictedMethods]) { |
|
return { |
|
CallExpression(node) { |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (vitestFnCall?.type !== "vi" || vitestFnCall.members.length === 0) |
|
return; |
|
const method = getAccessorValue(vitestFnCall.members[0]); |
|
if (method in restrictedMethods) { |
|
const message = restrictedMethods[method]; |
|
context.report({ |
|
messageId: message ? "restrictedViMethodWithMessage" : "restrictedViMethod", |
|
data: { message, restriction: method }, |
|
loc: { |
|
start: vitestFnCall.members[0].loc.start, |
|
end: vitestFnCall.members[vitestFnCall.members.length - 1].loc.end |
|
} |
|
}); |
|
} |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$O = "consistent-test-filename"; |
|
const defaultPattern = /.*\.test\.[tj]sx?$/; |
|
const defaultTestsPattern = /.*\.(test|spec)\.[tj]sx?$/; |
|
const consistentTestFilename = createEslintRule({ |
|
name: RULE_NAME$O, |
|
meta: { |
|
type: "problem", |
|
docs: { |
|
recommended: false, |
|
requiresTypeChecking: false, |
|
description: "require .spec test file pattern" |
|
}, |
|
messages: { |
|
consistentTestFilename: "use test file name pattern {{pattern}}" |
|
}, |
|
schema: [ |
|
{ |
|
type: "object", |
|
additionalProperties: false, |
|
properties: { |
|
pattern: { |
|
//@ts-ignore |
|
format: "regex", |
|
default: defaultPattern.source |
|
}, |
|
allTestPattern: { |
|
//@ts-ignore |
|
format: "regex", |
|
default: defaultTestsPattern.source |
|
} |
|
} |
|
} |
|
] |
|
}, |
|
defaultOptions: [{ pattern: defaultTestsPattern.source, allTestPattern: defaultTestsPattern.source }], |
|
create: (context) => { |
|
const config = context.options[0] ?? {}; |
|
const { pattern: patternRaw = defaultPattern, allTestPattern: allTestPatternRaw = defaultTestsPattern } = config; |
|
const pattern = typeof patternRaw === "string" ? new RegExp(patternRaw) : patternRaw; |
|
const testPattern = typeof allTestPatternRaw === "string" ? new RegExp(allTestPatternRaw) : allTestPatternRaw; |
|
const filename = path.basename(context.filename); |
|
if (!testPattern.test(filename)) |
|
return {}; |
|
return { |
|
Program: (p) => { |
|
if (!pattern.test(filename)) { |
|
context.report({ |
|
node: p, |
|
messageId: "consistentTestFilename", |
|
data: { |
|
pattern: pattern.source |
|
} |
|
}); |
|
} |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$N = "max-expects"; |
|
const maxExpect = createEslintRule({ |
|
name: RULE_NAME$N, |
|
meta: { |
|
docs: { |
|
requiresTypeChecking: false, |
|
recommended: false, |
|
description: "enforce a maximum number of expect per test" |
|
}, |
|
messages: { |
|
maxExpect: "Too many assertion calls ({{count}}). Maximum is {{max}}." |
|
}, |
|
type: "suggestion", |
|
schema: [ |
|
{ |
|
type: "object", |
|
properties: { |
|
max: { |
|
type: "number" |
|
} |
|
}, |
|
additionalProperties: false |
|
} |
|
] |
|
}, |
|
defaultOptions: [{ max: 5 }], |
|
create(context, [{ max }]) { |
|
let assertsCount = 0; |
|
const resetAssertCount = (node) => { |
|
const isFunctionTest = node.parent?.type !== AST_NODE_TYPES.CallExpression || isTypeOfVitestFnCall(node.parent, context, ["test"]); |
|
if (isFunctionTest) |
|
assertsCount = 0; |
|
}; |
|
return { |
|
"FunctionExpression": resetAssertCount, |
|
"FunctionExpression:exit": resetAssertCount, |
|
"ArrowFunctionExpression": resetAssertCount, |
|
"ArrowFunctionExpression:exit": resetAssertCount, |
|
CallExpression(node) { |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (vitestFnCall?.type !== "expect" || vitestFnCall.head.node.parent?.type === AST_NODE_TYPES.MemberExpression) |
|
return; |
|
assertsCount += 1; |
|
if (assertsCount > max) { |
|
context.report({ |
|
node, |
|
messageId: "maxExpect", |
|
data: { |
|
count: assertsCount, |
|
max |
|
} |
|
}); |
|
} |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$M = "no-alias-methods"; |
|
const noAliasMethod = createEslintRule({ |
|
name: RULE_NAME$M, |
|
meta: { |
|
docs: { |
|
description: "disallow alias methods", |
|
requiresTypeChecking: false, |
|
recommended: false |
|
}, |
|
messages: { |
|
noAliasMethods: "Replace {{ alias }}() with its canonical name {{ canonical }}()" |
|
}, |
|
type: "suggestion", |
|
fixable: "code", |
|
schema: [] |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
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 vitestFnCall = parseVitestFnCall(node, context); |
|
if (vitestFnCall?.type !== "expect") |
|
return; |
|
const { matcher } = vitestFnCall; |
|
const alias = getAccessorValue(matcher); |
|
if (alias in methodNames) { |
|
const canonical = methodNames[alias]; |
|
context.report({ |
|
messageId: "noAliasMethods", |
|
data: { alias, canonical }, |
|
node: matcher, |
|
fix: (fixer) => [replaceAccessorFixer(fixer, matcher, canonical)] |
|
}); |
|
} |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$L = "no-commented-out-tests"; |
|
function hasTests(node) { |
|
return /^\s*[xf]?(test|it|describe)(\.\w+|\[['"]\w+['"]\])?\s*\(/mu.test(node.value); |
|
} |
|
const noCommentedOutTests = createEslintRule({ |
|
name: RULE_NAME$L, |
|
meta: { |
|
docs: { |
|
description: "disallow commented out tests", |
|
requiresTypeChecking: false, |
|
recommended: false |
|
}, |
|
messages: { |
|
noCommentedOutTests: "Remove commented out tests. You may want to use `skip` or `only` instead." |
|
}, |
|
schema: [], |
|
type: "suggestion" |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
const { sourceCode } = context; |
|
function checkNodeForCommentedOutTests(node) { |
|
if (!hasTests(node)) |
|
return; |
|
context.report({ messageId: "noCommentedOutTests", node }); |
|
} |
|
return { |
|
Program() { |
|
const comments = sourceCode.getAllComments(); |
|
comments.forEach(checkNodeForCommentedOutTests); |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$K = "no-conditional-expect"; |
|
const isCatchCall = (node) => node.callee.type === AST_NODE_TYPES.MemberExpression && isSupportedAccessor(node.callee.property, "catch"); |
|
const noConditionalExpect = createEslintRule({ |
|
name: RULE_NAME$K, |
|
meta: { |
|
type: "problem", |
|
docs: { |
|
description: "disallow conditional expects", |
|
requiresTypeChecking: false, |
|
recommended: false |
|
}, |
|
messages: { |
|
noConditionalExpect: "Avoid calling `expect` inside conditional statements" |
|
}, |
|
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 = context.sourceCode.getDeclaredVariables(node); |
|
const testCallExpressions = getTestCallExpressionsFromDeclaredVariables(declaredVariables, context); |
|
if (testCallExpressions.length > 0) |
|
inTestCase = true; |
|
}, |
|
CallExpression(node) { |
|
const { type: vitestFnCallType } = parseVitestFnCall(node, context) ?? {}; |
|
if (vitestFnCallType === "test") |
|
inTestCase = true; |
|
if (isCatchCall(node)) |
|
inPromiseCatch = true; |
|
if (inTestCase && vitestFnCallType === "expect" && conditionalDepth > 0) { |
|
context.report({ |
|
messageId: "noConditionalExpect", |
|
node |
|
}); |
|
} |
|
if (inPromiseCatch && vitestFnCallType === "expect") { |
|
context.report({ |
|
messageId: "noConditionalExpect", |
|
node |
|
}); |
|
} |
|
}, |
|
"CallExpression:exit"(node) { |
|
if (isTypeOfVitestFnCall(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 |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$J = "no-import-node-test"; |
|
const noImportNodeTest = createEslintRule({ |
|
name: RULE_NAME$J, |
|
meta: { |
|
docs: { |
|
description: "disallow importing `node:test`", |
|
recommended: false |
|
}, |
|
type: "suggestion", |
|
messages: { |
|
noImportNodeTest: "Import from `vitest` instead of `node:test`" |
|
}, |
|
fixable: "code", |
|
schema: [] |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
return { |
|
ImportDeclaration(node) { |
|
if (node.source.value === "node:test") { |
|
context.report({ |
|
messageId: "noImportNodeTest", |
|
node, |
|
fix: (fixer) => fixer.replaceText( |
|
node.source, |
|
node.source.raw.replace("node:test", "vitest") |
|
) |
|
}); |
|
} |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$I = "no-conditional-in-test"; |
|
const noConditionalInTest = createEslintRule({ |
|
name: RULE_NAME$I, |
|
meta: { |
|
docs: { |
|
description: "disallow conditional tests", |
|
requiresTypeChecking: false, |
|
recommended: false |
|
}, |
|
messages: { |
|
noConditionalInTest: "Remove conditional tests" |
|
}, |
|
schema: [], |
|
type: "problem" |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
return { |
|
IfStatement(node) { |
|
if (node.parent?.parent?.parent?.type === "CallExpression" && isTypeOfVitestFnCall(node.parent?.parent?.parent, context, ["test", "it"])) { |
|
context.report({ |
|
messageId: "noConditionalInTest", |
|
node |
|
}); |
|
} |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$H = "no-disabled-tests"; |
|
const noDisabledTests = createEslintRule({ |
|
name: RULE_NAME$H, |
|
meta: { |
|
type: "suggestion", |
|
docs: { |
|
description: "disallow disabled tests", |
|
recommended: false |
|
}, |
|
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. If you want to skip a test suite temporarily, use .todo() instead.", |
|
disabledTest: "Disabled test. If you want to skip a test temporarily, use .todo() instead." |
|
}, |
|
schema: [] |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
let suiteDepth = 0; |
|
let testDepth = 0; |
|
return { |
|
CallExpression(node) { |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (!vitestFnCall) |
|
return; |
|
if (vitestFnCall.type === "describe") |
|
suiteDepth++; |
|
if (vitestFnCall.type === "test") { |
|
testDepth++; |
|
if (node.arguments.length < 2 && vitestFnCall.members.every((s) => getAccessorValue(s) === "skip")) { |
|
context.report({ |
|
messageId: "missingFunction", |
|
node |
|
}); |
|
} |
|
} |
|
const skipMember = vitestFnCall.members.find((s) => getAccessorValue(s) === "skip"); |
|
if (vitestFnCall.name.startsWith("x") || skipMember !== void 0) { |
|
context.report({ |
|
messageId: vitestFnCall.type === "describe" ? "disabledSuite" : "disabledTest", |
|
node: skipMember ?? vitestFnCall.head.node |
|
}); |
|
} |
|
}, |
|
"CallExpression:exit"(node) { |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (!vitestFnCall) |
|
return; |
|
if (vitestFnCall.type === "describe") |
|
suiteDepth--; |
|
if (vitestFnCall.type === "test") |
|
testDepth--; |
|
}, |
|
'CallExpression[callee.name="pending"]'(node) { |
|
const scope = context.sourceCode.getScope ? context.sourceCode.getScope(node) : context.getScope(); |
|
if (resolveScope(scope, "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 }); |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$G = "no-done-callback"; |
|
const findCallbackArg = (node, isVitestEach, context) => { |
|
if (isVitestEach) |
|
return node.arguments[1]; |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (vitestFnCall?.type === "hook" && node.arguments.length >= 1) |
|
return node.arguments[0]; |
|
if (vitestFnCall?.type === "test" && node.arguments.length >= 2) |
|
return node.arguments[1]; |
|
return null; |
|
}; |
|
const noDoneCallback = createEslintRule({ |
|
name: RULE_NAME$G, |
|
meta: { |
|
type: "suggestion", |
|
docs: { |
|
description: "disallow using a callback in asynchronous tests and hooks", |
|
recommended: false |
|
}, |
|
deprecated: true, |
|
schema: [], |
|
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 function" |
|
}, |
|
hasSuggestions: true |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
return { |
|
CallExpression(node) { |
|
const isVitestEach = /\.each$|\.concurrent$/.test(getNodeName(node.callee) ?? ""); |
|
if (isVitestEach && node.callee.type !== AST_NODE_TYPES.TaggedTemplateExpression) |
|
return; |
|
const isInsideConcurrentTestOrDescribe = context.sourceCode.getAncestors(node).some((ancestor) => { |
|
if (ancestor.type !== AST_NODE_TYPES.CallExpression) |
|
return false; |
|
const isNotInsideDescribeOrTest = !isTypeOfVitestFnCall(ancestor, context, ["describe", "test"]); |
|
if (isNotInsideDescribeOrTest) |
|
return false; |
|
const isTestRunningConcurrently = ancestor.callee.type === AST_NODE_TYPES.MemberExpression && isSupportedAccessor(ancestor.callee.property, "concurrent"); |
|
return isTestRunningConcurrently; |
|
}); |
|
if (isInsideConcurrentTestOrDescribe) |
|
return; |
|
const callback = findCallbackArg(node, isVitestEach, context); |
|
const callbackArgIndex = Number(isVitestEach); |
|
if (!callback || !isFunction(callback) || callback.params.length !== 1 + callbackArgIndex) |
|
return; |
|
const argument = callback.params[callbackArgIndex]; |
|
if (argument.type !== AST_NODE_TYPES.Identifier) { |
|
context.report({ |
|
node: argument, |
|
messageId: "noDoneCallback" |
|
}); |
|
return; |
|
} |
|
if (callback.async) { |
|
context.report({ |
|
node: argument, |
|
messageId: "useAwaitInsteadOfCallback" |
|
}); |
|
return; |
|
} |
|
context.report({ |
|
node, |
|
messageId: "noDoneCallback", |
|
suggest: [ |
|
{ |
|
messageId: "suggestWrappingInPromise", |
|
data: { callback: argument.name }, |
|
fix(fixer) { |
|
const { body, params } = callback; |
|
const { sourceCode } = 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?.value === ",") |
|
tokenAfterLastParam = sourceCode.getTokenAfter(tokenAfterLastParam); |
|
if (!firstBodyToken || !lastBodyToken || !tokenBeforeFirstParam || !tokenAfterLastParam) |
|
throw new Error(`Unexpected null when attempting to fix ${context.filename} - please file an issue at https://github/veritem/eslint-plugin-vitest`); |
|
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 === 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) |
|
]; |
|
} |
|
} |
|
] |
|
}); |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$F = "no-duplicate-hooks"; |
|
const noDuplicateHooks = createEslintRule({ |
|
name: RULE_NAME$F, |
|
meta: { |
|
docs: { |
|
recommended: false, |
|
description: "disallow duplicate hooks and teardown hooks", |
|
requiresTypeChecking: false |
|
}, |
|
messages: { |
|
noDuplicateHooks: "Duplicate {{hook}} in describe block." |
|
}, |
|
schema: [], |
|
type: "suggestion" |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
const hooksContexts = [{}]; |
|
return { |
|
CallExpression(node) { |
|
var _a; |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (vitestFnCall?.type === "describe") |
|
hooksContexts.push({}); |
|
if (vitestFnCall?.type !== "hook") |
|
return; |
|
const currentLayer = hooksContexts[hooksContexts.length - 1]; |
|
currentLayer[_a = vitestFnCall.name] || (currentLayer[_a] = 0); |
|
currentLayer[vitestFnCall.name] += 1; |
|
if (currentLayer[vitestFnCall.name] > 1) { |
|
context.report({ |
|
messageId: "noDuplicateHooks", |
|
data: { hook: vitestFnCall.name }, |
|
node |
|
}); |
|
} |
|
}, |
|
"CallExpression:exit"(node) { |
|
if (isTypeOfVitestFnCall(node, context, ["describe"])) |
|
hooksContexts.pop(); |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$E = "no-large-snapshots"; |
|
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(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 === AST_NODE_TYPES.ExpressionStatement && "left" in node.expression && node.expression.left.type === AST_NODE_TYPES.MemberExpression && isSupportedAccessor(node.expression.left.property)) { |
|
const fileName = context.filename; |
|
const allowedSnapshotsInFile = allowedSnapshots[fileName]; |
|
if (allowedSnapshotsInFile) { |
|
const snapshotName = 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({ |
|
node, |
|
messageId: lineLimit === 0 ? "noSnapShot" : "tooLongSnapShot", |
|
data: { |
|
lineCount, |
|
lineLimit |
|
} |
|
}); |
|
} |
|
}; |
|
const noLargeSnapshots = createEslintRule({ |
|
name: RULE_NAME$E, |
|
meta: { |
|
docs: { |
|
description: "disallow large snapshots", |
|
recommended: false |
|
}, |
|
messages: { |
|
noSnapShot: "`{{ lineCount }}`s should begin with lowercase", |
|
tooLongSnapShot: "Expected vitest 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.filename.endsWith(".snap")) { |
|
return { |
|
ExpressionStatement(node) { |
|
reportOnViolation(context, node, options); |
|
} |
|
}; |
|
} |
|
return { |
|
CallExpression(node) { |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (vitestFnCall?.type !== "expect") |
|
return; |
|
if ([ |
|
"toMatchInlineSnapshot", |
|
"toThrowErrorMatchingInlineSnapshot" |
|
].includes(getAccessorValue(vitestFnCall.matcher)) && vitestFnCall.args.length) { |
|
reportOnViolation(context, vitestFnCall.args[0], { |
|
...options, |
|
maxSize: options.inlineMaxSize ?? options.maxSize |
|
}); |
|
} |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$D = "no-interpolation-in-snapshots"; |
|
const nonInterpolationInSnapShots = createEslintRule({ |
|
name: RULE_NAME$D, |
|
meta: { |
|
type: "problem", |
|
docs: { |
|
description: "disallow string interpolation in snapshots", |
|
recommended: false |
|
}, |
|
fixable: "code", |
|
schema: [], |
|
messages: { |
|
noInterpolationInSnapshots: "Do not use string interpolation in snapshots" |
|
} |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
return { |
|
CallExpression(node) { |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (vitestFnCall?.type !== "expect") |
|
return; |
|
if ([ |
|
"toMatchInlineSnapshot", |
|
"toThrowErrorMatchingInlineSnapshot" |
|
].includes(getAccessorValue(vitestFnCall.matcher))) { |
|
vitestFnCall.args.forEach((argument) => { |
|
if (argument.type === AST_NODE_TYPES.TemplateLiteral && argument.expressions.length > 0) { |
|
context.report({ |
|
messageId: "noInterpolationInSnapshots", |
|
node: argument |
|
}); |
|
} |
|
}); |
|
} |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const mocksDirName = "__mocks__"; |
|
const isMockPath = (path) => path.split(posix.sep).includes(mocksDirName); |
|
const isMockImportLiteral = (expression) => isStringNode(expression) && isMockPath(getStringValue(expression)); |
|
const RULE_NAME$C = "no-mocks-import"; |
|
const noMocksImport = createEslintRule({ |
|
name: RULE_NAME$C, |
|
meta: { |
|
type: "problem", |
|
docs: { |
|
description: "disallow importing from __mocks__ directory", |
|
recommended: false |
|
}, |
|
messages: { |
|
noMocksImport: `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: "noMocksImport" }); |
|
}, |
|
'CallExpression[callee.name="require"]'(node) { |
|
const [args] = node.arguments; |
|
if (args && isMockImportLiteral(args)) |
|
context.report({ node: args, messageId: "noMocksImport" }); |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$B = "no-restricted-matchers"; |
|
const isChainRestricted = (chain, restriction) => { |
|
if (ModifierName.hasOwnProperty(restriction) || restriction.endsWith(".not")) |
|
return chain.startsWith(restriction); |
|
return chain === restriction; |
|
}; |
|
const noRestrictedMatchers = createEslintRule({ |
|
name: RULE_NAME$B, |
|
meta: { |
|
docs: { |
|
description: "disallow the use of certain matchers", |
|
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 vitestFnCall = parseVitestFnCall(node, context); |
|
if (vitestFnCall?.type !== "expect") |
|
return; |
|
const chain = vitestFnCall.members.map((node2) => getAccessorValue(node2)).join("."); |
|
for (const [restriction, message] of Object.entries(restrictedChains)) { |
|
if (isChainRestricted(chain, restriction)) { |
|
context.report({ |
|
messageId: message ? "restrictedChainWithMessage" : "restrictedChain", |
|
data: { message, restriction }, |
|
loc: { |
|
start: vitestFnCall.members[0].loc.start, |
|
end: vitestFnCall.members[vitestFnCall.members.length - 1].loc.end |
|
} |
|
}); |
|
break; |
|
} |
|
} |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$A = "no-standalone-expect"; |
|
const getBlockType = (statement, context) => { |
|
const func = statement.parent; |
|
if (!func) |
|
throw new Error("Unexpected block statement. If you feel like this is a bug report https://github.com/veritem/eslint-plugin-vitest/issues/new"); |
|
if (func.type === AST_NODE_TYPES.FunctionDeclaration) |
|
return "function"; |
|
if (isFunction(func) && func.parent) { |
|
const expr = func.parent; |
|
if (expr.type === AST_NODE_TYPES.VariableDeclarator) |
|
return "function"; |
|
if (expr.type === AST_NODE_TYPES.CallExpression && isTypeOfVitestFnCall(expr, context, ["describe"])) |
|
return "describe"; |
|
} |
|
return null; |
|
}; |
|
const noStandaloneExpect = createEslintRule({ |
|
name: RULE_NAME$A, |
|
meta: { |
|
docs: { |
|
description: "disallow using `expect` outside of `it` or `test` blocks", |
|
recommended: false |
|
}, |
|
type: "suggestion", |
|
messages: { |
|
noStandaloneExpect: "Expect must be called inside a test block" |
|
}, |
|
schema: [ |
|
{ |
|
properties: { |
|
additionaltestblockfunctions: { |
|
//@ts-ignore |
|
type: "array", |
|
//@ts-ignore |
|
items: { type: `string` } |
|
} |
|
}, |
|
additionalproperties: false |
|
} |
|
] |
|
}, |
|
defaultOptions: [{ additionalTestBlockFunctions: [] }], |
|
create(context, [{ additionalTestBlockFunctions = [] }]) { |
|
const callStack = []; |
|
const isCustomTestBlockFunction = (node) => additionalTestBlockFunctions.includes(getNodeName(node) || ""); |
|
return { |
|
CallExpression(node) { |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (vitestFnCall?.type === "expect") { |
|
if (vitestFnCall.head.node.parent?.type === AST_NODE_TYPES.MemberExpression && vitestFnCall.members.length === 1 && !["assertions", "hasAssertions"].includes( |
|
getAccessorValue(vitestFnCall.members[0]) |
|
)) |
|
return; |
|
const parent = callStack[callStack.length - 1]; |
|
if (!parent || parent === DescribeAlias.describe) |
|
context.report({ node, messageId: "noStandaloneExpect" }); |
|
return; |
|
} |
|
if (vitestFnCall?.type === "test" || isCustomTestBlockFunction(node)) |
|
callStack.push("test"); |
|
if (node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression) |
|
callStack.push("template"); |
|
}, |
|
"CallExpression:exit"(node) { |
|
const top = callStack[callStack.length - 1]; |
|
if (top === "test" && (isTypeOfVitestFnCall(node, context, ["test"]) || isCustomTestBlockFunction(node)) && node.callee.type !== AST_NODE_TYPES.MemberExpression || top === "template" && node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression) |
|
callStack.pop(); |
|
}, |
|
BlockStatement(statement) { |
|
const blockType = getBlockType(statement, context); |
|
if (blockType) |
|
callStack.push(blockType); |
|
}, |
|
"BlockStatement:exit"(statement) { |
|
const blockType = getBlockType(statement, context); |
|
if (blockType) |
|
callStack.pop(); |
|
}, |
|
ArrowFunctionExpression(node) { |
|
if (node.parent?.type !== AST_NODE_TYPES.CallExpression) |
|
callStack.push("arrow"); |
|
}, |
|
"ArrowFunctionExpression:exit"() { |
|
if (callStack[callStack.length - 1] === "arrow") |
|
callStack.pop(); |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$z = "no-test-prefixes"; |
|
const noTestPrefixes = createEslintRule({ |
|
name: RULE_NAME$z, |
|
meta: { |
|
docs: { |
|
description: "disallow using `test` as a prefix", |
|
recommended: false |
|
}, |
|
type: "suggestion", |
|
messages: { |
|
usePreferredName: 'Use "{{preferredNodeName}}" instead' |
|
}, |
|
fixable: "code", |
|
schema: [] |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
return { |
|
CallExpression(node) { |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (vitestFnCall?.type !== "describe" && vitestFnCall?.type !== "test") |
|
return; |
|
if (vitestFnCall.name[0] !== "f" && vitestFnCall.name[0] !== "x") |
|
return; |
|
const preferredNodeName = [ |
|
vitestFnCall.name.slice(1), |
|
vitestFnCall.name[0] === "f" ? "only" : "skip", |
|
...vitestFnCall.members.map((m) => getAccessorValue(m)) |
|
].join("."); |
|
const funcNode = node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression ? node.callee.tag : node.callee.type === AST_NODE_TYPES.CallExpression ? node.callee.callee : node.callee; |
|
context.report({ |
|
messageId: "usePreferredName", |
|
node: node.callee, |
|
data: { preferredNodeName }, |
|
fix: (fixer) => [fixer.replaceText(funcNode, preferredNodeName)] |
|
}); |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$y = "no-test-return-statement"; |
|
const getBody = (args) => { |
|
const [, secondArg] = args; |
|
if (secondArg && isFunction(secondArg) && secondArg.body.type === AST_NODE_TYPES.BlockStatement) |
|
return secondArg.body.body; |
|
return []; |
|
}; |
|
const noTestReturnStatement = createEslintRule({ |
|
name: RULE_NAME$y, |
|
meta: { |
|
type: "problem", |
|
docs: { |
|
description: "disallow return statements in tests", |
|
recommended: false |
|
}, |
|
schema: [], |
|
messages: { |
|
noTestReturnStatement: "Return statements are not allowed in tests" |
|
} |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
return { |
|
CallExpression(node) { |
|
if (!isTypeOfVitestFnCall(node, context, ["test"])) |
|
return; |
|
const body = getBody(node.arguments); |
|
const returnStmt = body.find((stmt) => stmt.type === AST_NODE_TYPES.ReturnStatement); |
|
if (!returnStmt) |
|
return; |
|
context.report({ messageId: "noTestReturnStatement", node: returnStmt }); |
|
}, |
|
FunctionDeclaration(node) { |
|
const declaredVariables = context.sourceCode.getDeclaredVariables(node); |
|
const testCallExpressions = getTestCallExpressionsFromDeclaredVariables(declaredVariables, context); |
|
if (testCallExpressions.length === 0) |
|
return; |
|
const returnStmt = node.body.body.find((stmt) => stmt.type === AST_NODE_TYPES.ReturnStatement); |
|
if (!returnStmt) |
|
return; |
|
context.report({ messageId: "noTestReturnStatement", node: returnStmt }); |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$x = "prefer-called-with"; |
|
const preferCalledWith = createEslintRule({ |
|
name: RULE_NAME$x, |
|
meta: { |
|
docs: { |
|
description: "enforce using `toBeCalledWith()` or `toHaveBeenCalledWith()`", |
|
recommended: false |
|
}, |
|
messages: { |
|
preferCalledWith: "Prefer {{ matcherName }}With(/* expected args */)" |
|
}, |
|
type: "suggestion", |
|
fixable: "code", |
|
schema: [] |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
return { |
|
CallExpression(node) { |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (vitestFnCall?.type !== "expect") |
|
return; |
|
if (vitestFnCall.modifiers.some( |
|
(node2) => getAccessorValue(node2) === "not" |
|
)) |
|
return; |
|
const { matcher } = vitestFnCall; |
|
const matcherName = getAccessorValue(matcher); |
|
if (["toBeCalled", "toHaveBeenCalled"].includes(matcherName)) { |
|
context.report({ |
|
data: { matcherName }, |
|
messageId: "preferCalledWith", |
|
node: matcher, |
|
fix: (fixer) => [fixer.replaceText(matcher, `${matcherName}With`)] |
|
}); |
|
} |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$w = "valid-title"; |
|
const trimFXPrefix = (word) => ["f", "x"].includes(word.charAt(0)) ? word.substring(1) : word; |
|
const quoteStringValue = (node) => node.type === AST_NODE_TYPES.TemplateLiteral ? `\`${node.quasis[0].value.raw}\`` : node.raw; |
|
const MatcherAndMessageSchema = { |
|
type: "array", |
|
items: { type: "string" }, |
|
minItems: 1, |
|
maxItems: 2, |
|
additionalItems: false |
|
}; |
|
const compileMatcherPattern = (matcherMaybeWithMessage) => { |
|
const [matcher, message] = Array.isArray(matcherMaybeWithMessage) ? matcherMaybeWithMessage : [matcherMaybeWithMessage]; |
|
return [new RegExp(matcher, "u"), message]; |
|
}; |
|
function isFunctionType(type) { |
|
const symbol = type.getSymbol(); |
|
if (!symbol) { |
|
return false; |
|
} |
|
return symbol.getDeclarations()?.some((declaration) => ts.isFunctionDeclaration(declaration) || ts.isMethodDeclaration(declaration) || ts.isFunctionExpression(declaration) || ts.isArrowFunction(declaration)) ?? false; |
|
} |
|
function isClassType(type) { |
|
const symbol = type.getSymbol(); |
|
if (!symbol) |
|
return false; |
|
return symbol.getDeclarations()?.some((declaration) => ts.isClassDeclaration(declaration) || ts.isClassExpression(declaration)) ?? false; |
|
} |
|
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 doesBinaryExpressionContainStringNode = (binaryExp) => { |
|
if (isStringNode(binaryExp.right)) |
|
return true; |
|
if (binaryExp.left.type === AST_NODE_TYPES.BinaryExpression) |
|
return doesBinaryExpressionContainStringNode(binaryExp.left); |
|
return isStringNode(binaryExp.left); |
|
}; |
|
const validTitle = createEslintRule({ |
|
name: RULE_NAME$w, |
|
meta: { |
|
docs: { |
|
description: "enforce valid titles", |
|
recommended: false |
|
}, |
|
messages: { |
|
titleMustBeString: "Test title must be a string, a function or class name", |
|
emptyTitle: "{{functionName}} 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 title', |
|
mustNotMatch: "{{functionName}} should not match {{pattern}}", |
|
mustMatch: "{{functionName}} should match {{pattern}}", |
|
mustNotMatchCustom: "{{message}}", |
|
mustMatchCustom: "{{message}}" |
|
}, |
|
type: "suggestion", |
|
schema: [ |
|
{ |
|
type: "object", |
|
properties: { |
|
ignoreTypeOfDescribeName: { |
|
type: "boolean", |
|
default: false |
|
}, |
|
allowArguments: { |
|
type: "boolean", |
|
default: false |
|
}, |
|
disallowedWords: { |
|
type: "array", |
|
items: { type: "string" } |
|
} |
|
}, |
|
patternProperties: { |
|
[/^must(?:Not)?Match$/u.source]: { |
|
oneOf: [ |
|
{ type: "string" }, |
|
MatcherAndMessageSchema, |
|
{ |
|
type: "object", |
|
//@ts-ignore |
|
propertyNames: { type: "string", enum: ["describe", "test", "it"] }, |
|
additionalProperties: { |
|
oneOf: [{ type: "string" }, MatcherAndMessageSchema] |
|
} |
|
} |
|
] |
|
} |
|
}, |
|
additionalProperties: false |
|
} |
|
], |
|
fixable: "code" |
|
}, |
|
defaultOptions: [{ ignoreTypeOfDescribeName: false, allowArguments: false, disallowedWords: [] }], |
|
create(context, [ |
|
{ |
|
ignoreTypeOfDescribeName, |
|
allowArguments, |
|
disallowedWords = [], |
|
mustNotMatch, |
|
mustMatch |
|
} |
|
]) { |
|
const disallowedWordsRegexp = new RegExp(`\\b(${disallowedWords.join("|")})\\b`, "iu"); |
|
const mustNotMatchPatterns = compileMatcherPatterns(mustNotMatch ?? {}); |
|
const mustMatchPatterns = compileMatcherPatterns(mustMatch ?? {}); |
|
const settings = parsePluginSettings(context.settings); |
|
return { |
|
CallExpression(node) { |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (vitestFnCall?.type !== "describe" && vitestFnCall?.type !== "test" && vitestFnCall?.type !== "it") |
|
return; |
|
const [argument] = node.arguments; |
|
if (settings.typecheck) { |
|
const services = ESLintUtils.getParserServices(context); |
|
const type = services.getTypeAtLocation(argument); |
|
if (isFunctionType(type) || isClassType(type)) |
|
return; |
|
} |
|
if (!argument || allowArguments && argument.type === AST_NODE_TYPES.Identifier) |
|
return; |
|
if (!isStringNode(argument)) { |
|
if (argument.type === AST_NODE_TYPES.BinaryExpression && doesBinaryExpressionContainStringNode(argument)) |
|
return; |
|
if (argument.type !== AST_NODE_TYPES.TemplateLiteral && !(ignoreTypeOfDescribeName && vitestFnCall.type === "describe")) { |
|
context.report({ |
|
messageId: "titleMustBeString", |
|
loc: argument.loc |
|
}); |
|
} |
|
return; |
|
} |
|
const title = getStringValue(argument); |
|
if (!title) { |
|
context.report({ |
|
messageId: "emptyTitle", |
|
data: { |
|
functionName: vitestFnCall.type === "describe" ? DescribeAlias.describe : TestCaseName.test |
|
}, |
|
node |
|
}); |
|
return; |
|
} |
|
if (disallowedWords.length > 0) { |
|
const disallowedMatch = disallowedWordsRegexp.exec(title); |
|
if (disallowedMatch) { |
|
context.report({ |
|
messageId: "disallowedWord", |
|
data: { |
|
word: disallowedMatch[1] |
|
}, |
|
node: argument |
|
}); |
|
return; |
|
} |
|
} |
|
if (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(vitestFnCall.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 vitestFnName = unPrefixedName; |
|
const [mustNotMatchPattern, mustNotMatchMessage] = mustNotMatchPatterns[vitestFnName] ?? []; |
|
if (mustNotMatchPattern) { |
|
if (mustNotMatchPattern.test(title)) { |
|
context.report({ |
|
messageId: mustNotMatchMessage ? "mustNotMatchCustom" : "mustNotMatch", |
|
node: argument, |
|
data: { |
|
functionName: vitestFnName, |
|
pattern: mustNotMatchPattern, |
|
message: mustNotMatchMessage |
|
} |
|
}); |
|
return; |
|
} |
|
} |
|
const [mustMatchPattern, mustMatchMessage] = mustMatchPatterns[vitestFnName] ?? []; |
|
if (mustMatchPattern) { |
|
if (!mustMatchPattern.test(title)) { |
|
context.report({ |
|
messageId: mustMatchMessage ? "mustMatchCustom" : "mustMatch", |
|
node: argument, |
|
data: { |
|
functionName: vitestFnName, |
|
pattern: mustMatchPattern, |
|
message: mustMatchMessage |
|
} |
|
}); |
|
} |
|
} |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$v = "valid-expect"; |
|
const defaultAsyncMatchers = ["toReject", "toResolve"]; |
|
const getPromiseCallExpressionNode = (node) => { |
|
if (node.type === AST_NODE_TYPES.ArrayExpression && node.parent && node.parent.type === AST_NODE_TYPES.CallExpression) |
|
node = node.parent; |
|
if (node.type === AST_NODE_TYPES.CallExpression && node.callee.type === AST_NODE_TYPES.MemberExpression && isSupportedAccessor(node.callee.object, "Promise") && node.parent) |
|
return node; |
|
return null; |
|
}; |
|
const promiseArrayExceptionKey = ({ start, end }) => `${start.line}:${start.column}-${end.line}:${end.column}`; |
|
function getParentIfThenified(node) { |
|
const grandParentNode = node.parent?.parent; |
|
if (grandParentNode && grandParentNode.type === AST_NODE_TYPES.CallExpression && grandParentNode.callee.type === AST_NODE_TYPES.MemberExpression && isSupportedAccessor(grandParentNode.callee.property) && ["then", "catch"].includes(getAccessorValue(grandParentNode.callee.property)) && grandParentNode.parent) |
|
return getParentIfThenified(grandParentNode); |
|
return node; |
|
} |
|
const findPromiseCallExpressionNode = (node) => node.parent?.parent && [AST_NODE_TYPES.CallExpression, AST_NODE_TYPES.ArrayExpression].includes( |
|
node.parent.type |
|
) ? getPromiseCallExpressionNode(node.parent) : null; |
|
const isAcceptableReturnNode = (node, allowReturn) => { |
|
if (allowReturn && node.type === AST_NODE_TYPES.ReturnStatement) |
|
return true; |
|
if (node.type === AST_NODE_TYPES.ConditionalExpression && node.parent) |
|
return isAcceptableReturnNode(node.parent, allowReturn); |
|
return [ |
|
AST_NODE_TYPES.ArrowFunctionExpression, |
|
AST_NODE_TYPES.AwaitExpression |
|
].includes(node.type); |
|
}; |
|
const validExpect = createEslintRule({ |
|
name: RULE_NAME$v, |
|
meta: { |
|
docs: { |
|
description: "enforce valid `expect()` usage", |
|
recommended: false |
|
}, |
|
messages: { |
|
tooManyArgs: "Expect takes most {{ amount}} argument{{s}}", |
|
notEnoughArgs: "Expect requires atleast {{ amount }} argument{{s}}", |
|
modifierUnknown: "Expect has 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 }]) => { |
|
const arrayExceptions = /* @__PURE__ */ new Set(); |
|
const pushPromiseArrayException = (loc) => arrayExceptions.add(promiseArrayExceptionKey(loc)); |
|
const promiseArrayExceptionExists = (loc) => arrayExceptions.has(promiseArrayExceptionKey(loc)); |
|
const findTopMostMemberExpression = (node) => { |
|
let topMostMemberExpression = node; |
|
let { parent } = node; |
|
while (parent) { |
|
if (parent.type !== AST_NODE_TYPES.MemberExpression) |
|
break; |
|
topMostMemberExpression = parent; |
|
parent = parent.parent; |
|
} |
|
return topMostMemberExpression; |
|
}; |
|
return { |
|
CallExpression(node) { |
|
const vitestFnCall = parseVitestFnCallWithReason(node, context); |
|
if (typeof vitestFnCall === "string") { |
|
const reportingNode = node.parent?.type === AST_NODE_TYPES.MemberExpression ? findTopMostMemberExpression(node.parent).property : node; |
|
if (vitestFnCall === "matcher-not-found") { |
|
context.report({ |
|
messageId: "matcherNotFound", |
|
node: reportingNode |
|
}); |
|
return; |
|
} |
|
if (vitestFnCall === "matcher-not-called") { |
|
context.report({ |
|
messageId: isSupportedAccessor(reportingNode) && ModifierName.hasOwnProperty(getAccessorValue(reportingNode)) ? "matcherNotFound" : "matcherNotCalled", |
|
node: reportingNode |
|
}); |
|
} |
|
if (vitestFnCall === "modifier-unknown") { |
|
context.report({ |
|
messageId: "modifierUnknown", |
|
node: reportingNode |
|
}); |
|
return; |
|
} |
|
return; |
|
} else if (vitestFnCall?.type !== "expect") { |
|
return; |
|
} |
|
const { parent: expect } = vitestFnCall.head.node; |
|
if (expect?.type !== AST_NODE_TYPES.CallExpression) |
|
return; |
|
if (expect.arguments.length < minArgs) { |
|
const expectLength = getAccessorValue(vitestFnCall.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) { |
|
if (expect.arguments.length === 2) { |
|
const isSecondArgString = expect.arguments[1].type === AST_NODE_TYPES.Literal && typeof expect.arguments[1].value === "string"; |
|
const isSecondArgTemplateLiteral = expect.arguments[1].type === AST_NODE_TYPES.TemplateLiteral; |
|
if (isSecondArgString || isSecondArgTemplateLiteral) { |
|
return; |
|
} |
|
} |
|
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 } = vitestFnCall; |
|
const parentNode = matcher.parent.parent; |
|
const shouldBeAwaited = vitestFnCall.modifiers.some((nod) => getAccessorValue(nod) !== "not") || asyncMatchers.includes(getAccessorValue(matcher)); |
|
if (!parentNode?.parent || !shouldBeAwaited) |
|
return; |
|
const isParentArrayExpression = parentNode.parent.type === AST_NODE_TYPES.ArrayExpression; |
|
const orReturned = alwaysAwait ? "" : " or returned"; |
|
const targetNode = getParentIfThenified(parentNode); |
|
const finalNode = findPromiseCallExpressionNode(targetNode) || targetNode; |
|
if (finalNode.parent && !isAcceptableReturnNode(finalNode.parent, !alwaysAwait) && !promiseArrayExceptionExists(finalNode.loc)) { |
|
context.report({ |
|
loc: finalNode.loc, |
|
data: { orReturned }, |
|
messageId: finalNode === targetNode ? "asyncMustBeAwaited" : "promisesWithAsyncAssertionsMustBeAwaited", |
|
node |
|
}); |
|
if (isParentArrayExpression) |
|
pushPromiseArrayException(finalNode.loc); |
|
} |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const isBooleanLiteral = (node) => node.type === AST_NODE_TYPES.Literal && typeof node.value === "boolean"; |
|
const isBooleanEqualityMatcher = (expectFnCall) => { |
|
const matcherName = getAccessorValue(expectFnCall.matcher); |
|
if (["toBeTruthy", "toBeFalsy"].includes(matcherName)) |
|
return true; |
|
if (expectFnCall.args.length !== 1) |
|
return false; |
|
const arg = getFirstMatcherArg(expectFnCall); |
|
return EqualityMatcher.hasOwnProperty(matcherName) && isBooleanLiteral(arg); |
|
}; |
|
const isInstanceOfBinaryExpression = (node, className) => node.type === AST_NODE_TYPES.BinaryExpression && node.operator === "instanceof" && isSupportedAccessor(node.right, className); |
|
const hasOnlyOneArgument = (call) => call.arguments.length === 1; |
|
|
|
const RULE_NAME$u = "prefer-to-be-object"; |
|
const preferToBeObject = createEslintRule({ |
|
name: RULE_NAME$u, |
|
meta: { |
|
type: "suggestion", |
|
docs: { |
|
description: "enforce using toBeObject()", |
|
recommended: false |
|
}, |
|
fixable: "code", |
|
messages: { |
|
preferToBeObject: "Prefer toBeObject() to test if a value is an object." |
|
}, |
|
schema: [] |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
return { |
|
CallExpression(node) { |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (vitestFnCall?.type !== "expectTypeOf") |
|
return; |
|
if (isParsedInstanceOfMatcherCall(vitestFnCall, "Object")) { |
|
context.report({ |
|
node: vitestFnCall.matcher, |
|
messageId: "preferToBeObject", |
|
fix: (fixer) => [ |
|
fixer.replaceTextRange( |
|
[ |
|
vitestFnCall.matcher.range[0], |
|
vitestFnCall.matcher.range[1] + "(Object)".length |
|
], |
|
"toBeObject()" |
|
) |
|
] |
|
}); |
|
return; |
|
} |
|
const { parent: expectTypeOf } = vitestFnCall.head.node; |
|
if (expectTypeOf?.type !== AST_NODE_TYPES.CallExpression) |
|
return; |
|
const [expectTypeOfArgs] = expectTypeOf.arguments; |
|
if (!expectTypeOfArgs || !isBooleanEqualityMatcher(vitestFnCall) || !isInstanceOfBinaryExpression(expectTypeOfArgs, "Object")) |
|
return; |
|
context.report({ |
|
node: vitestFnCall.matcher, |
|
messageId: "preferToBeObject", |
|
fix(fixer) { |
|
const fixes = [ |
|
fixer.replaceText(vitestFnCall.matcher, "toBeObject"), |
|
fixer.removeRange([expectTypeOfArgs.left.range[1], expectTypeOfArgs.range[1]]) |
|
]; |
|
let invertCondition = getAccessorValue(vitestFnCall.matcher) === "toBeFalsy"; |
|
if (vitestFnCall.args.length) { |
|
const [matcherArg] = vitestFnCall.args; |
|
fixes.push(fixer.remove(matcherArg)); |
|
invertCondition = matcherArg.type === AST_NODE_TYPES.Literal && followTypeAssertionChain$1(matcherArg).value === false; |
|
} |
|
if (invertCondition) { |
|
const notModifier = vitestFnCall.modifiers.find((node2) => getAccessorValue(node2) === "not"); |
|
fixes.push( |
|
notModifier ? fixer.removeRange([ |
|
notModifier.range[0] - 1, |
|
notModifier.range[1] |
|
]) : fixer.insertTextBefore(vitestFnCall.matcher, "not.") |
|
); |
|
} |
|
return fixes; |
|
} |
|
}); |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$t = "prefer-to-be-truthy"; |
|
const isTrueLiteral = (node) => node.type === AST_NODE_TYPES.Literal && node.value === true; |
|
const preferToBeTruthy = createEslintRule({ |
|
name: RULE_NAME$t, |
|
meta: { |
|
type: "suggestion", |
|
docs: { |
|
description: "enforce using `toBeTruthy`", |
|
recommended: false |
|
}, |
|
messages: { |
|
preferToBeTruthy: "Prefer using `toBeTruthy` to test value is `true`" |
|
}, |
|
fixable: "code", |
|
schema: [] |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
return { |
|
CallExpression(node) { |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (!(vitestFnCall?.type === "expect" || vitestFnCall?.type === "expectTypeOf")) |
|
return; |
|
if (vitestFnCall.args.length === 1 && isTrueLiteral(getFirstMatcherArg(vitestFnCall)) && EqualityMatcher.hasOwnProperty(getAccessorValue(vitestFnCall.matcher))) { |
|
context.report({ |
|
node: vitestFnCall.matcher, |
|
messageId: "preferToBeTruthy", |
|
fix: (fixer) => [ |
|
fixer.replaceText(vitestFnCall.matcher, "toBeTruthy"), |
|
fixer.remove(vitestFnCall.args[0]) |
|
] |
|
}); |
|
} |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$s = "prefer-to-be-falsy"; |
|
const isFalseLiteral = (node) => node.type === AST_NODE_TYPES.Literal && node.value === false; |
|
const preferToBeFalsy = createEslintRule({ |
|
name: RULE_NAME$s, |
|
meta: { |
|
type: "suggestion", |
|
docs: { |
|
description: "enforce using toBeFalsy()", |
|
recommended: false |
|
}, |
|
fixable: "code", |
|
schema: [], |
|
messages: { |
|
preferToBeFalsy: "Prefer using toBeFalsy()" |
|
} |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
return { |
|
CallExpression(node) { |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (!(vitestFnCall?.type === "expect" || vitestFnCall?.type === "expectTypeOf")) |
|
return; |
|
if (vitestFnCall.args.length === 1 && isFalseLiteral(getFirstMatcherArg(vitestFnCall)) && EqualityMatcher.hasOwnProperty(getAccessorValue(vitestFnCall.matcher))) { |
|
context.report({ |
|
node: vitestFnCall.matcher, |
|
messageId: "preferToBeFalsy", |
|
fix: (fixer) => [ |
|
fixer.replaceText(vitestFnCall.matcher, "toBeFalsy"), |
|
fixer.remove(vitestFnCall.args[0]) |
|
] |
|
}); |
|
} |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$r = "prefer-to-have-length"; |
|
const preferToHaveLength = createEslintRule({ |
|
name: RULE_NAME$r, |
|
meta: { |
|
type: "suggestion", |
|
docs: { |
|
description: "enforce using toHaveLength()", |
|
recommended: false |
|
}, |
|
fixable: "code", |
|
messages: { |
|
preferToHaveLength: "Prefer toHaveLength()" |
|
}, |
|
schema: [] |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
return { |
|
CallExpression(node) { |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (vitestFnCall?.type !== "expect") |
|
return; |
|
const { parent: expect } = vitestFnCall.head.node; |
|
if (expect?.type !== AST_NODE_TYPES.CallExpression) |
|
return; |
|
const [argument] = expect.arguments; |
|
const { matcher } = vitestFnCall; |
|
if (!EqualityMatcher.hasOwnProperty(getAccessorValue(matcher)) || argument?.type !== AST_NODE_TYPES.MemberExpression || !isSupportedAccessor(argument.property, "length")) |
|
return; |
|
context.report({ |
|
node: matcher, |
|
messageId: "preferToHaveLength", |
|
fix(fixer) { |
|
return [ |
|
fixer.removeRange([ |
|
argument.property.range[0] - 1, |
|
argument.range[1] |
|
]), |
|
fixer.replaceTextRange( |
|
[matcher.parent.object.range[1], matcher.parent.range[1]], |
|
".toHaveLength" |
|
) |
|
]; |
|
} |
|
}); |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$q = "prefer-equality-matcher"; |
|
const preferEqualityMatcher = createEslintRule({ |
|
name: RULE_NAME$q, |
|
meta: { |
|
type: "suggestion", |
|
docs: { |
|
description: "enforce using the built-in quality matchers", |
|
recommended: false |
|
}, |
|
messages: { |
|
useEqualityMatcher: "Prefer using one of the equality matchers instead", |
|
suggestEqualityMatcher: "Use `{{ equalityMatcher }}`" |
|
}, |
|
hasSuggestions: true, |
|
schema: [] |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
return { |
|
CallExpression(node) { |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (vitestFnCall?.type !== "expect" || vitestFnCall.args.length === 0) |
|
return; |
|
const { parent: expect } = vitestFnCall.head.node; |
|
if (expect?.type !== AST_NODE_TYPES.CallExpression) |
|
return; |
|
const { |
|
arguments: [comparison], |
|
range: [, expectCallEnd] |
|
} = expect; |
|
const { matcher } = vitestFnCall; |
|
const matcherArg = getFirstMatcherArg(vitestFnCall); |
|
if (comparison?.type !== AST_NODE_TYPES.BinaryExpression || comparison.operator !== "===" && comparison.operator !== "!==" || !EqualityMatcher.hasOwnProperty(getAccessorValue(matcher)) || !isBooleanLiteral(matcherArg)) |
|
return; |
|
const matcherValue = matcherArg.value; |
|
const [modifier] = vitestFnCall.modifiers; |
|
const hasNot = vitestFnCall.modifiers.some( |
|
(nod) => getAccessorValue(nod) === "not" |
|
); |
|
const addNotModifier = (comparison.operator === "!==" ? !matcherValue : matcherValue) === hasNot; |
|
const buildFixer = (equalityMatcher) => (fixer) => { |
|
const { sourceCode } = context; |
|
let modifierText = modifier && getAccessorValue(modifier) !== "not" ? `.${getAccessorValue(modifier)}` : ""; |
|
if (addNotModifier) |
|
modifierText += `.${ModifierName.not}`; |
|
return [ |
|
fixer.replaceText( |
|
comparison, |
|
sourceCode.getText(comparison.left) |
|
), |
|
fixer.replaceTextRange( |
|
[expectCallEnd, matcher.parent.range[1]], |
|
`${modifierText}.${equalityMatcher}` |
|
), |
|
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 |
|
}); |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$p = "prefer-strict-equal"; |
|
const preferStrictEqual = createEslintRule({ |
|
name: RULE_NAME$p, |
|
meta: { |
|
type: "suggestion", |
|
docs: { |
|
description: "enforce strict equal over equal", |
|
recommended: false |
|
}, |
|
messages: { |
|
useToStrictEqual: "Use `toStrictEqual()` instead", |
|
suggestReplaceWithStrictEqual: "Replace with `toStrictEqual()`" |
|
}, |
|
schema: [], |
|
hasSuggestions: true |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
return { |
|
CallExpression(node) { |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (vitestFnCall?.type !== "expect") |
|
return; |
|
const { matcher } = vitestFnCall; |
|
if (isSupportedAccessor(matcher, "toEqual")) { |
|
context.report({ |
|
messageId: "useToStrictEqual", |
|
node: matcher, |
|
suggest: [ |
|
{ |
|
messageId: "suggestReplaceWithStrictEqual", |
|
fix: (fixer) => [ |
|
replaceAccessorFixer(fixer, matcher, EqualityMatcher.toStrictEqual) |
|
] |
|
} |
|
] |
|
}); |
|
} |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$o = "prefer-expect-resolves"; |
|
const preferExpectResolves = createEslintRule({ |
|
name: RULE_NAME$o, |
|
meta: { |
|
type: "suggestion", |
|
docs: { |
|
description: "enforce using `expect().resolves` over `expect(await ...)` syntax", |
|
recommended: false |
|
}, |
|
fixable: "code", |
|
messages: { |
|
expectResolves: "Use `expect().resolves` instead" |
|
}, |
|
schema: [] |
|
}, |
|
defaultOptions: [], |
|
create: (context) => ({ |
|
CallExpression(node) { |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (vitestFnCall?.type !== "expect") |
|
return; |
|
const { parent } = vitestFnCall.head.node; |
|
if (parent?.type !== AST_NODE_TYPES.CallExpression) |
|
return; |
|
const [awaitNode] = parent.arguments; |
|
if (awaitNode?.type === 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") |
|
]; |
|
} |
|
}); |
|
} |
|
} |
|
}) |
|
}); |
|
|
|
const RULE_NAME$n = "prefer-each"; |
|
const preferEach = createEslintRule({ |
|
name: RULE_NAME$n, |
|
meta: { |
|
type: "suggestion", |
|
docs: { |
|
description: "enforce using `each` rather than manual loops", |
|
recommended: false |
|
}, |
|
schema: [], |
|
messages: { |
|
preferEach: "Prefer using `{{ fn }}.each` rather than a manual loop" |
|
} |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
const vitestFnCalls = []; |
|
let inTestCaseCall = false; |
|
const recommendFn = () => { |
|
if (vitestFnCalls.length === 1 && vitestFnCalls[0] === "test") |
|
return "it"; |
|
return "describe"; |
|
}; |
|
const enterForLoop = () => { |
|
if (vitestFnCalls.length === 0 || inTestCaseCall) |
|
return; |
|
vitestFnCalls.length = 0; |
|
}; |
|
const exitForLoop = (node) => { |
|
if (vitestFnCalls.length === 0 || inTestCaseCall) |
|
return; |
|
context.report({ |
|
node, |
|
messageId: "preferEach", |
|
data: { fn: recommendFn() } |
|
}); |
|
vitestFnCalls.length = 0; |
|
}; |
|
return { |
|
"ForStatement": enterForLoop, |
|
"ForStatement:exit": exitForLoop, |
|
"ForInStatement": enterForLoop, |
|
"ForInStatement:exit": exitForLoop, |
|
"ForOfStatement": enterForLoop, |
|
"ForOfStatement:exit": exitForLoop, |
|
CallExpression(node) { |
|
const { type: vitestFnCallType } = parseVitestFnCall(node, context) ?? {}; |
|
if (vitestFnCallType === "hook" || vitestFnCallType === "describe" || vitestFnCallType === "test") |
|
vitestFnCalls.push(vitestFnCallType); |
|
if (vitestFnCallType === "test") |
|
inTestCaseCall = true; |
|
}, |
|
"CallExpression:exit"(node) { |
|
const { type: vitestFnCallType } = parseVitestFnCall(node, context) ?? {}; |
|
if (vitestFnCallType === "test") |
|
inTestCaseCall = false; |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$m = "prefer-hooks-on-top"; |
|
const preferHooksOnTop = createEslintRule({ |
|
name: RULE_NAME$m, |
|
meta: { |
|
type: "suggestion", |
|
docs: { |
|
description: "enforce having hooks before any test cases", |
|
recommended: false |
|
}, |
|
messages: { |
|
noHookOnTop: "Hooks should come before test cases" |
|
}, |
|
schema: [] |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
const hooksContext = [false]; |
|
return { |
|
CallExpression(node) { |
|
if (isTypeOfVitestFnCall(node, context, ["test"])) |
|
hooksContext[hooksContext.length - 1] = true; |
|
if (hooksContext[hooksContext.length - 1] && isTypeOfVitestFnCall(node, context, ["hook"])) { |
|
context.report({ |
|
messageId: "noHookOnTop", |
|
node |
|
}); |
|
} |
|
hooksContext.push(false); |
|
}, |
|
"CallExpression:exit"() { |
|
hooksContext.pop(); |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$l = "prefer-hooks-in-order"; |
|
const HooksOrder = ["beforeAll", "beforeEach", "afterEach", "afterAll"]; |
|
const preferHooksInOrder = createEslintRule({ |
|
name: RULE_NAME$l, |
|
meta: { |
|
type: "suggestion", |
|
docs: { |
|
description: "enforce having hooks in consistent order", |
|
recommended: false |
|
}, |
|
messages: { |
|
reorderHooks: "`{{ currentHook }}` hooks should be before any `{{ previousHook }}` hooks" |
|
}, |
|
schema: [] |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
let previousHookIndex = -1; |
|
let inHook = false; |
|
return { |
|
CallExpression(node) { |
|
if (inHook) |
|
return; |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (vitestFnCall?.type !== "hook") { |
|
previousHookIndex = -1; |
|
return; |
|
} |
|
inHook = true; |
|
const currentHook = vitestFnCall.name; |
|
const currentHookIndex = HooksOrder.indexOf(currentHook); |
|
if (currentHookIndex < previousHookIndex) { |
|
context.report({ |
|
messageId: "reorderHooks", |
|
data: { |
|
previousHook: HooksOrder[previousHookIndex], |
|
currentHook |
|
}, |
|
node |
|
}); |
|
inHook = false; |
|
return; |
|
} |
|
previousHookIndex = currentHookIndex; |
|
}, |
|
"CallExpression:exit"(node) { |
|
if (isTypeOfVitestFnCall(node, context, ["hook"])) { |
|
inHook = false; |
|
return; |
|
} |
|
if (inHook) |
|
return; |
|
previousHookIndex = -1; |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$k = "prefer-mock-promise-shorthand"; |
|
const withOnce = (name, addOnce) => { |
|
return `${name}${addOnce ? "Once" : ""}`; |
|
}; |
|
const findSingleReturnArgumentNode = (fnNode) => { |
|
if (fnNode.body.type !== AST_NODE_TYPES.BlockStatement) |
|
return fnNode.body; |
|
if (fnNode.body.body[0]?.type === AST_NODE_TYPES.ReturnStatement) |
|
return fnNode.body.body[0].argument; |
|
return null; |
|
}; |
|
const preferMockPromiseShorthand = createEslintRule({ |
|
name: RULE_NAME$k, |
|
meta: { |
|
type: "suggestion", |
|
docs: { |
|
description: "enforce mock resolved/rejected shorthands for promises", |
|
recommended: false |
|
}, |
|
messages: { |
|
useMockShorthand: "Prefer {{ replacement }}" |
|
}, |
|
schema: [], |
|
fixable: "code" |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
const report = (property, isOnce, outerArgNode, innerArgNode = outerArgNode) => { |
|
if (innerArgNode?.type !== AST_NODE_TYPES.CallExpression) |
|
return; |
|
const argName = 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 } = context; |
|
if (innerArgNode.arguments.length > 1) |
|
return null; |
|
return [ |
|
fixer.replaceText(property, replacement), |
|
fixer.replaceText(outerArgNode, innerArgNode.arguments.length === 1 ? sourceCode.getText(innerArgNode.arguments[0]) : "undefined") |
|
]; |
|
} |
|
}); |
|
}; |
|
return { |
|
CallExpression(node) { |
|
if (node.callee.type !== AST_NODE_TYPES.MemberExpression || !isSupportedAccessor(node.callee.property) || node.arguments.length === 0) |
|
return; |
|
const mockFnName = 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 (!isFunction(arg) || arg.params.length !== 0) |
|
return; |
|
report( |
|
node.callee.property, |
|
isOnce, |
|
arg, |
|
findSingleReturnArgumentNode(arg) |
|
); |
|
} |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const require = createRequire(import.meta.url); |
|
const eslintRequire = createRequire(require.resolve("eslint")); |
|
eslintRequire.resolve("espree"); |
|
const STATEMENT_LIST_PARENTS = /* @__PURE__ */ new Set([ |
|
AST_NODE_TYPES.Program, |
|
AST_NODE_TYPES.BlockStatement, |
|
AST_NODE_TYPES.SwitchCase, |
|
AST_NODE_TYPES.SwitchStatement |
|
]); |
|
const isValidParent = (parentType) => { |
|
return STATEMENT_LIST_PARENTS.has(parentType); |
|
}; |
|
const isTokenASemicolon = (token) => token.value === ";" && token.type === AST_TOKEN_TYPES.Punctuator; |
|
const getActualLastToken = (sourceCode, node) => { |
|
const semiToken = sourceCode.getLastToken(node); |
|
const prevToken = sourceCode.getTokenBefore(semiToken); |
|
const nextToken = sourceCode.getTokenAfter(semiToken); |
|
const isSemicolonLessStyle = Boolean( |
|
prevToken && nextToken && prevToken.range[0] >= node.range[0] && isTokenASemicolon(semiToken) && semiToken.loc.start.line !== prevToken.loc.end.line && semiToken.loc.end.line === nextToken.loc.start.line |
|
); |
|
return isSemicolonLessStyle ? prevToken : semiToken; |
|
}; |
|
const getPaddingLineSequences = (prevNode, nextNode, sourceCode) => { |
|
const pairs = []; |
|
const includeComments = true; |
|
let prevToken = getActualLastToken(sourceCode, prevNode); |
|
if (nextNode.loc.start.line - prevNode.loc.end.line >= 2) { |
|
do { |
|
const token = sourceCode.getTokenAfter(prevToken, { includeComments }); |
|
if (token.loc.start.line - prevToken.loc.end.line >= 2) { |
|
pairs.push([prevToken, token]); |
|
} |
|
prevToken = token; |
|
} while (prevToken.range[0] < nextNode.range[0]); |
|
} |
|
return pairs; |
|
}; |
|
const areTokensOnSameLine = (left, right) => left.loc.end.line === right.loc.start.line; |
|
const isTypeCastExpression = (node) => node.type === AST_NODE_TYPES.TSAsExpression || node.type === AST_NODE_TYPES.TSTypeAssertion; |
|
const followTypeAssertionChain = (expression) => isTypeCastExpression(expression) ? followTypeAssertionChain(expression.expression) : expression; |
|
|
|
const RULE_NAME$j = "prefer-vi-mocked"; |
|
const mockTypes = ["Mock", "MockedFunction", "MockedClass", "MockedObject"]; |
|
const preferViMocked = createEslintRule({ |
|
name: RULE_NAME$j, |
|
meta: { |
|
type: "suggestion", |
|
docs: { |
|
description: "Prefer `vi.mocked()` over `fn as Mock`", |
|
requiresTypeChecking: true, |
|
recommended: false |
|
}, |
|
fixable: "code", |
|
messages: { |
|
useViMocked: "Prefer `vi.mocked()`" |
|
}, |
|
schema: [] |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
function check(node) { |
|
const { typeAnnotation } = node; |
|
if (typeAnnotation.type !== AST_NODE_TYPES.TSTypeReference) |
|
return; |
|
const { typeName } = typeAnnotation; |
|
if (typeName.type !== AST_NODE_TYPES.Identifier) |
|
return; |
|
if (!mockTypes.includes(typeName.name)) |
|
return; |
|
const fnName = context.sourceCode.text.slice( |
|
...followTypeAssertionChain(node.expression).range |
|
); |
|
context.report({ |
|
node, |
|
messageId: "useViMocked", |
|
fix(fixer) { |
|
return fixer.replaceText(node, `vi.mocked(${fnName})`); |
|
} |
|
}); |
|
} |
|
return { |
|
TSAsExpression(node) { |
|
if (node.parent.type === AST_NODE_TYPES.TSAsExpression) |
|
return; |
|
check(node); |
|
}, |
|
TSTypeAssertion(node) { |
|
check(node); |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$i = "prefer-snapshot-hint"; |
|
const snapshotMatchers = ["toMatchSnapshot", "toThrowErrorMatchingSnapshot"]; |
|
const snapshotMatcherNames = snapshotMatchers; |
|
const isSnapshotMatcherWithoutHint = (expectFnCall) => { |
|
if (expectFnCall.args.length === 0) |
|
return true; |
|
if (!isSupportedAccessor(expectFnCall.matcher, "toMatchSnapshot")) |
|
return expectFnCall.args.length !== 1; |
|
if (expectFnCall.args.length === 2) |
|
return false; |
|
const [arg] = expectFnCall.args; |
|
return !isStringNode(arg); |
|
}; |
|
const preferSnapshotHint = createEslintRule({ |
|
name: RULE_NAME$i, |
|
meta: { |
|
type: "suggestion", |
|
docs: { |
|
description: "enforce including a hint with external snapshots", |
|
recommended: false |
|
}, |
|
messages: { |
|
missingHint: "You should provide a hint for this snapshot" |
|
}, |
|
schema: [ |
|
{ |
|
type: "string", |
|
enum: ["always", "multi"] |
|
} |
|
] |
|
}, |
|
defaultOptions: ["multi"], |
|
create(context, [mode]) { |
|
const snapshotMatchers2 = []; |
|
let expressionDepth = 0; |
|
const depths = []; |
|
const reportSnapshotMatchersWithoutHints = () => { |
|
for (const snapshotMatcher of snapshotMatchers2) { |
|
if (isSnapshotMatcherWithoutHint(snapshotMatcher)) { |
|
context.report({ |
|
messageId: "missingHint", |
|
node: snapshotMatcher.matcher |
|
}); |
|
} |
|
} |
|
}; |
|
const enterExpression = () => { |
|
expressionDepth++; |
|
}; |
|
const exitExpression = () => { |
|
expressionDepth--; |
|
if (mode === "always") { |
|
reportSnapshotMatchersWithoutHints(); |
|
snapshotMatchers2.length = 0; |
|
} |
|
if (mode === "multi" && expressionDepth === 0) { |
|
if (snapshotMatchers2.length > 1) |
|
reportSnapshotMatchersWithoutHints(); |
|
snapshotMatchers2.length = 0; |
|
} |
|
}; |
|
return { |
|
"Program:exit"() { |
|
enterExpression(); |
|
exitExpression(); |
|
}, |
|
"FunctionExpression": enterExpression, |
|
"FunctionExpression:exit": exitExpression, |
|
"ArrowFunctionExpression": enterExpression, |
|
"ArrowFunctionExpression:exit": exitExpression, |
|
"CallExpression:exit"(node) { |
|
if (isTypeOfVitestFnCall(node, context, ["describe", "test"])) |
|
expressionDepth = depths.pop() ?? 0; |
|
}, |
|
CallExpression(node) { |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (vitestFnCall?.type !== "expect") { |
|
if (vitestFnCall?.type === "describe" || vitestFnCall?.type === "test") { |
|
depths.push(expressionDepth); |
|
expressionDepth = 0; |
|
} |
|
return; |
|
} |
|
const matcherName = getAccessorValue(vitestFnCall.matcher); |
|
if (!snapshotMatcherNames.includes(matcherName)) |
|
return; |
|
snapshotMatchers2.push(vitestFnCall); |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$h = "valid-describe-callback"; |
|
const paramsLocation = (params) => { |
|
const [first] = params; |
|
const last = params[params.length - 1]; |
|
return { |
|
start: first.loc.start, |
|
end: last.loc.end |
|
}; |
|
}; |
|
const validDescribeCallback = createEslintRule({ |
|
name: RULE_NAME$h, |
|
meta: { |
|
type: "problem", |
|
docs: { |
|
description: "enforce valid describe callback", |
|
recommended: false |
|
}, |
|
messages: { |
|
nameAndCallback: "Describe requires a name and callback arguments", |
|
secondArgumentMustBeFunction: "Second argument must be a function", |
|
unexpectedDescribeArgument: "Unexpected argument in describe callback", |
|
unexpectedReturnInDescribe: "Unexpected return statement in describe callback" |
|
}, |
|
schema: [] |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
return { |
|
CallExpression(node) { |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (vitestFnCall?.type !== "describe") |
|
return; |
|
if (vitestFnCall?.members[0]?.type === AST_NODE_TYPES.Identifier && vitestFnCall.members[0].name === "todo") |
|
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 (!isFunction(callback)) { |
|
context.report({ |
|
messageId: "secondArgumentMustBeFunction", |
|
loc: paramsLocation(node.arguments) |
|
}); |
|
return; |
|
} |
|
if (vitestFnCall.members.every((s) => getAccessorValue(s) !== "each") && callback.params.length) { |
|
context.report({ |
|
messageId: "unexpectedDescribeArgument", |
|
node: callback |
|
}); |
|
} |
|
if (callback.body.type === AST_NODE_TYPES.CallExpression) { |
|
context.report({ |
|
messageId: "unexpectedReturnInDescribe", |
|
node: callback |
|
}); |
|
} |
|
if (callback.body.type === AST_NODE_TYPES.BlockStatement) { |
|
callback.body.body.forEach((node2) => { |
|
if (node2.type === AST_NODE_TYPES.ReturnStatement) { |
|
context.report({ |
|
messageId: "unexpectedReturnInDescribe", |
|
node: node2 |
|
}); |
|
} |
|
}); |
|
} |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$g = "require-top-level-describe"; |
|
const requireTopLevelDescribe = createEslintRule({ |
|
name: RULE_NAME$g, |
|
meta: { |
|
docs: { |
|
description: "enforce that all tests are in a top-level describe", |
|
recommended: false |
|
}, |
|
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." |
|
}, |
|
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 vitestFnCall = parseVitestFnCall(node, context); |
|
if (!vitestFnCall) |
|
return; |
|
if (vitestFnCall.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 (vitestFnCall.type === "test") { |
|
context.report({ node, messageId: "unexpectedTestCase" }); |
|
return; |
|
} |
|
if (vitestFnCall.type === "hook") |
|
context.report({ node, messageId: "unexpectedHook" }); |
|
} |
|
}, |
|
"CallExpression:exit"(node) { |
|
if (isTypeOfVitestFnCall(node, context, ["describe"])) |
|
numberOfDescribeBlocks--; |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$f = "require-to-throw-message"; |
|
const requireToThrowMessage = createEslintRule({ |
|
name: RULE_NAME$f, |
|
meta: { |
|
type: "suggestion", |
|
docs: { |
|
description: "require toThrow() to be called with an error message", |
|
recommended: false |
|
}, |
|
schema: [], |
|
messages: { |
|
addErrorMessage: "Add an error message to {{ matcherName }}()" |
|
} |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
return { |
|
CallExpression(node) { |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (vitestFnCall?.type !== "expect") |
|
return; |
|
const { matcher } = vitestFnCall; |
|
const matcherName = getAccessorValue(matcher); |
|
if (vitestFnCall.args.length === 0 && ["toThrow", "toThrowError"].includes(matcherName) && !vitestFnCall.modifiers.some((nod) => getAccessorValue(nod) === "not")) { |
|
context.report({ |
|
messageId: "addErrorMessage", |
|
data: { matcherName }, |
|
node: matcher |
|
}); |
|
} |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$e = "require-hook"; |
|
const isVitestFnCall = (node, context) => { |
|
if (parseVitestFnCall(node, context)) |
|
return true; |
|
return !!getNodeName(node)?.startsWith("vi"); |
|
}; |
|
const isNullOrUndefined = (node) => { |
|
return node.type === AST_NODE_TYPES.Literal && node.value === null || isIdentifier(node, "undefined"); |
|
}; |
|
const shouldBeInHook = (node, context, allowedFunctionCalls = []) => { |
|
switch (node.type) { |
|
case AST_NODE_TYPES.ExpressionStatement: |
|
return shouldBeInHook(node.expression, context, allowedFunctionCalls); |
|
case AST_NODE_TYPES.CallExpression: |
|
return !(isVitestFnCall(node, context) || allowedFunctionCalls.includes(getNodeName(node))); |
|
case AST_NODE_TYPES.VariableDeclaration: { |
|
if (node.kind === "const") |
|
return false; |
|
return node.declarations.some( |
|
({ init }) => init !== null && !isNullOrUndefined(init) |
|
); |
|
} |
|
default: |
|
return false; |
|
} |
|
}; |
|
const requireHook = createEslintRule({ |
|
name: RULE_NAME$e, |
|
meta: { |
|
docs: { |
|
description: "require setup and teardown 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 (!isTypeOfVitestFnCall(node, context, ["describe"]) || node.arguments.length < 2) |
|
return; |
|
const [, testFn] = node.arguments; |
|
if (!isFunction(testFn) || testFn.body.type !== AST_NODE_TYPES.BlockStatement) |
|
return; |
|
checkBlockBody(testFn.body.body); |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$d = "require-local-test-context-for-concurrent-snapshots"; |
|
const requireLocalTestContextForConcurrentSnapshots = createEslintRule({ |
|
name: RULE_NAME$d, |
|
meta: { |
|
docs: { |
|
description: "require local Test Context for concurrent snapshot tests", |
|
recommended: false |
|
}, |
|
messages: { |
|
requireLocalTestContext: "Use local Test Context instead" |
|
}, |
|
type: "problem", |
|
schema: [] |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
return { |
|
CallExpression(node) { |
|
const isNotAnAssertion = !isTypeOfVitestFnCall(node, context, ["expect"]); |
|
if (isNotAnAssertion) |
|
return; |
|
const isNotASnapshotAssertion = ![ |
|
"toMatchSnapshot", |
|
"toMatchInlineSnapshot", |
|
"toMatchFileSnapshot", |
|
"toThrowErrorMatchingSnapshot", |
|
"toThrowErrorMatchingInlineSnapshot" |
|
//@ts-ignore |
|
].includes(node.callee?.property.name); |
|
if (isNotASnapshotAssertion) |
|
return; |
|
const isInsideSequentialDescribeOrTest = !context.sourceCode.getAncestors(node).some((ancestor) => { |
|
if (ancestor.type !== AST_NODE_TYPES.CallExpression) |
|
return false; |
|
const isNotInsideDescribeOrTest = !isTypeOfVitestFnCall(ancestor, context, ["describe", "test"]); |
|
if (isNotInsideDescribeOrTest) |
|
return false; |
|
const isTestRunningConcurrently = ancestor.callee.type === AST_NODE_TYPES.MemberExpression && isSupportedAccessor(ancestor.callee.property, "concurrent"); |
|
return isTestRunningConcurrently; |
|
}); |
|
if (isInsideSequentialDescribeOrTest) |
|
return; |
|
context.report({ |
|
node, |
|
messageId: "requireLocalTestContext" |
|
}); |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$c = "prefer-todo"; |
|
const isTargetedTestCase = (vitestFnCall) => { |
|
if (vitestFnCall.members.some((s) => getAccessorValue(s) !== "skip")) |
|
return false; |
|
if (vitestFnCall.name.startsWith("x")) |
|
return false; |
|
return !vitestFnCall.name.startsWith("f"); |
|
}; |
|
function isEmptyFunction(node) { |
|
if (!isFunction(node)) |
|
return false; |
|
return node.body.type === AST_NODE_TYPES.BlockStatement && !node.body.body.length; |
|
} |
|
function createTodoFixer(vitestFnCall, fixer) { |
|
if (vitestFnCall.members.length) |
|
return replaceAccessorFixer(fixer, vitestFnCall.members[0], "todo"); |
|
return fixer.replaceText(vitestFnCall.head.node, `${vitestFnCall.head.local}.todo`); |
|
} |
|
const preferTodo = createEslintRule({ |
|
name: RULE_NAME$c, |
|
meta: { |
|
type: "layout", |
|
docs: { |
|
description: "enforce 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: [] |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
return { |
|
CallExpression(node) { |
|
const [title, callback] = node.arguments; |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (!title || vitestFnCall?.type !== "test" || !isTargetedTestCase(vitestFnCall) || !isStringNode(title)) |
|
return; |
|
if (callback && isEmptyFunction(callback)) { |
|
context.report({ |
|
messageId: "emptyTest", |
|
node, |
|
fix: (fixer) => [ |
|
fixer.removeRange([title.range[1], callback.range[1]]), |
|
createTodoFixer(vitestFnCall, fixer) |
|
] |
|
}); |
|
} |
|
if (hasOnlyOneArgument(node)) { |
|
context.report({ |
|
messageId: "unimplementedTest", |
|
node, |
|
fix: (fixer) => createTodoFixer(vitestFnCall, fixer) |
|
}); |
|
} |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$b = "prefer-spy-on"; |
|
const findNodeObject = (node) => { |
|
if ("object" in node) |
|
return node.object; |
|
if (node.callee.type === AST_NODE_TYPES.MemberExpression) |
|
return node.callee.object; |
|
return null; |
|
}; |
|
const getVitestFnCall = (node) => { |
|
if (node.type !== AST_NODE_TYPES.CallExpression && node.type !== AST_NODE_TYPES.MemberExpression) |
|
return null; |
|
const obj = findNodeObject(node); |
|
if (!obj) |
|
return null; |
|
if (obj.type === AST_NODE_TYPES.Identifier) { |
|
return node.type === AST_NODE_TYPES.CallExpression && getNodeName(node.callee) === "vi.fn" ? node : null; |
|
} |
|
return getVitestFnCall(obj); |
|
}; |
|
const getAutoFixMockImplementation = (vitestFnCall, context) => { |
|
const hasMockImplementationAlready = vitestFnCall.parent?.type === AST_NODE_TYPES.MemberExpression && vitestFnCall.parent.property.type === AST_NODE_TYPES.Identifier && vitestFnCall.parent.property.name === "mockImplementation"; |
|
if (hasMockImplementationAlready) |
|
return ""; |
|
const [arg] = vitestFnCall.arguments; |
|
const argSource = arg && context.sourceCode.getText(arg); |
|
return argSource ? `.mockImplementation(${argSource})` : ".mockImplementation()"; |
|
}; |
|
const preferSpyOn = createEslintRule({ |
|
name: RULE_NAME$b, |
|
meta: { |
|
type: "suggestion", |
|
docs: { |
|
description: "enforce using `vi.spyOn`", |
|
recommended: false |
|
}, |
|
messages: { |
|
useViSpayOn: "Use `vi.spyOn` instead" |
|
}, |
|
fixable: "code", |
|
schema: [] |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
return { |
|
AssignmentExpression(node) { |
|
const { left, right } = node; |
|
if (left.type !== AST_NODE_TYPES.MemberExpression) |
|
return; |
|
const vitestFnCall = getVitestFnCall(right); |
|
if (!vitestFnCall) |
|
return; |
|
context.report({ |
|
node, |
|
messageId: "useViSpayOn", |
|
fix(fixer) { |
|
const lefPropQuote = left.property.type === AST_NODE_TYPES.Identifier && !left.computed ? "'" : ""; |
|
const mockImplementation = getAutoFixMockImplementation(vitestFnCall, context); |
|
return [ |
|
fixer.insertTextBefore(left, "vi.spyOn("), |
|
fixer.replaceTextRange( |
|
[left.object.range[1], left.property.range[0]], |
|
`, ${lefPropQuote}` |
|
), |
|
fixer.replaceTextRange( |
|
[left.property.range[1], vitestFnCall.range[1]], |
|
`${lefPropQuote})${mockImplementation}` |
|
) |
|
]; |
|
} |
|
}); |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$a = "prefer-comparison-matcher"; |
|
const isString = (node) => { |
|
return isStringNode(node) || node?.type === 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; |
|
}; |
|
const preferComparisonMatcher = createEslintRule({ |
|
name: RULE_NAME$a, |
|
meta: { |
|
type: "suggestion", |
|
docs: { |
|
description: "enforce using the built-in comparison matchers", |
|
recommended: false |
|
}, |
|
schema: [], |
|
fixable: "code", |
|
messages: { |
|
useToBeComparison: "Prefer using `{{ preferredMatcher }}` instead" |
|
} |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
return { |
|
CallExpression(node) { |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (vitestFnCall?.type !== "expect" || vitestFnCall.args.length === 0) |
|
return; |
|
const { parent: expect } = vitestFnCall.head.node; |
|
if (expect?.type !== AST_NODE_TYPES.CallExpression) |
|
return; |
|
const { |
|
arguments: [comparison], |
|
range: [, expectCallEnd] |
|
} = expect; |
|
const { matcher } = vitestFnCall; |
|
const matcherArg = getFirstMatcherArg(vitestFnCall); |
|
if (comparison?.type !== AST_NODE_TYPES.BinaryExpression || isComparingToString(comparison) || !EqualityMatcher.hasOwnProperty(getAccessorValue(matcher)) || !isBooleanLiteral(matcherArg)) |
|
return; |
|
const [modifier] = vitestFnCall.modifiers; |
|
const hasNot = vitestFnCall.modifiers.some((nod) => getAccessorValue(nod) === "not"); |
|
const preferredMatcher = determineMatcher(comparison.operator, matcherArg.value === hasNot); |
|
if (!preferredMatcher) |
|
return; |
|
context.report({ |
|
fix(fixer) { |
|
const { sourceCode } = context; |
|
const modifierText = modifier && getAccessorValue(modifier) !== "not" ? `.${getAccessorValue(modifier)}` : ""; |
|
return [ |
|
fixer.replaceText( |
|
comparison, |
|
sourceCode.getText(comparison.left) |
|
), |
|
fixer.replaceTextRange( |
|
[expectCallEnd, matcher.parent.range[1]], |
|
`${modifierText}.${preferredMatcher}` |
|
), |
|
fixer.replaceText( |
|
matcherArg, |
|
sourceCode.getText(comparison.right) |
|
) |
|
]; |
|
}, |
|
messageId: "useToBeComparison", |
|
data: { preferredMatcher }, |
|
node: matcher |
|
}); |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$9 = "prefer-to-contain"; |
|
const isFixableIncludesCallExpression = (node) => node.type === AST_NODE_TYPES.CallExpression && node.callee.type === AST_NODE_TYPES.MemberExpression && isSupportedAccessor(node.callee.property, "includes") && hasOnlyOneArgument(node) && node.arguments[0].type !== AST_NODE_TYPES.SpreadElement; |
|
const preferToContain = createEslintRule({ |
|
name: RULE_NAME$9, |
|
meta: { |
|
docs: { |
|
description: "enforce using toContain()", |
|
recommended: false |
|
}, |
|
messages: { |
|
useToContain: "Use toContain() instead" |
|
}, |
|
fixable: "code", |
|
type: "suggestion", |
|
schema: [] |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
return { |
|
CallExpression(node) { |
|
const vitestFnCall = parseVitestFnCall(node, context); |
|
if (vitestFnCall?.type !== "expect" || vitestFnCall.args.length === 0) |
|
return; |
|
const { parent: expect } = vitestFnCall.head.node; |
|
if (expect?.type !== AST_NODE_TYPES.CallExpression) |
|
return; |
|
const { |
|
arguments: [includesCall], |
|
range: [, expectCallEnd] |
|
} = expect; |
|
const { matcher } = vitestFnCall; |
|
const matcherArg = getFirstMatcherArg(vitestFnCall); |
|
if (!includesCall || matcherArg.type === AST_NODE_TYPES.SpreadElement || !EqualityMatcher.hasOwnProperty(getAccessorValue(matcher)) || !isBooleanLiteral(matcherArg) || !isFixableIncludesCallExpression(includesCall)) |
|
return; |
|
const hasNot = vitestFnCall.modifiers.some((nod) => getAccessorValue(nod) === "not"); |
|
context.report({ |
|
fix(fixer) { |
|
const { sourceCode } = context; |
|
const addNotModifier = matcherArg.value === hasNot; |
|
return [ |
|
fixer.removeRange([ |
|
includesCall.callee.property.range[0] - 1, |
|
includesCall.range[1] |
|
]), |
|
fixer.replaceTextRange( |
|
[expectCallEnd, matcher.parent.range[1]], |
|
addNotModifier ? `.${ModifierName.not}.toContain` : ".toContain" |
|
), |
|
fixer.replaceText( |
|
vitestFnCall.args[0], |
|
sourceCode.getText(includesCall.arguments[0]) |
|
) |
|
]; |
|
}, |
|
messageId: "useToContain", |
|
node: matcher |
|
}); |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
const RULE_NAME$8 = "prefer-expect-assertions"; |
|
const isFirstStatement = (node) => { |
|
let parent = node; |
|
while (parent) { |
|
if (parent.parent?.type === AST_NODE_TYPES.BlockStatement) |
|
return parent.parent.body[0] === parent; |
|
if (parent.parent?.type === AST_NODE_TYPES.ArrowFunctionExpression) |
|
return true; |
|
parent = parent.parent; |
|
} |
|
throw new Error("Could not find parent block statement"); |
|
}; |
|
const suggestRemovingExtraArguments = (context, func, from) => ({ |
|
messageId: "suggestRemovingExtraArguments", |
|
fix: (fixer) => removeExtraArgumentsFixer(fixer, context, func, from) |
|
}); |
|
const preferExpectAssertions = createEslintRule({ |
|
name: "prefer-expect-assertions", |
|
meta: { |
|
docs: { |
|
description: "enforce using expect assertions instead of callbacks", |
|
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, |
|
onlyFunctionsWithExpectInCallback: false, |
|
onlyFunctionsWithExpectInLoop: false |
|
} |
|
], |
|
create(context, [options]) { |
|
let expressionDepth = 0; |
|
let hasExpectInCallBack = false; |
|
let hasExpectInLoop = false; |
|
let hasExpectAssertAsFirstStatement = false; |
|
let inTestCaseCall = false; |
|
let inForLoop = false; |
|
const shouldCheckFunction = (testFunction) => { |
|
if (!options.onlyFunctionsWithAsyncKeyword && !options.onlyFunctionsWithExpectInCallback && !options.onlyFunctionsWithExpectInLoop) |
|
return true; |
|
if (options.onlyFunctionsWithAsyncKeyword) { |
|
if (testFunction.async) |
|
return true; |
|
} |
|
if (options.onlyFunctionsWithExpectInCallback) { |
|
if (hasExpectInCallBack) |
|
return true; |
|
} |
|
if (options.onlyFunctionsWithExpectInLoop) { |
|
if (hasExpectInLoop) |
|
return true; |
|
} |
|
return false; |
|
}; |
|
function checkExpectHasAssertions(expectFnCall, func) { |
|
if (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 suggestions = []; |
|
if (expectFnCall.args.length) { |
|
loc = expectFnCall.args[1].loc; |
|
suggestions.push(suggestRemovingExtraArguments(context, func, 1)); |
|
} |
|
context.report({ |
|
messageId: "assertionsRequiresOneArgument", |
|
suggest: suggestions, |
|
loc |
|
}); |
|
return; |
|
} |
|
const [arg] = expectFnCall.args; |
|
if (arg.type === 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 vitestFnCall = parseVitestFnCall(node, context); |
|
if (vitestFnCall?.type === "test") { |
|
inTestCaseCall = true; |
|
return; |
|
} |
|
if (vitestFnCall?.type === "expect" && inTestCaseCall) { |
|
if (expressionDepth === 1 && isFirstStatement(node) && vitestFnCall.head.node.parent?.type === AST_NODE_TYPES.MemberExpression && vitestFnCall.members.length === 1 && ["assertions", "hasAssertions"].includes(getAccessorValue(vitestFnCall.members[0]))) { |
|
checkExpectHasAssertions(vitestFnCall, node); |
|
hasExpectAssertAsFirstStatement = true; |
|
} |
|
if (inForLoop) |
|
hasExpectInLoop = true; |
|
if (expressionDepth > 1) |
|
hasExpectInCallBack = true; |
|
} |
|
}, |
|
"CallExpression:exit"(node) { |
|
if (!isTypeOfVitestFnCall(node, context, ["test"])) |
|
return; |
|
inTestCaseCall = false; |
|
if (node.arguments.length < 2) |
|
return; |
|
const [, secondArg] = node.arguments; |
|
if (secondArg?.type === AST_NODE_TYPES.ArrowFunctionExpression && secondArg.params.length) { |
|
if (secondArg?.params[0].type === AST_NODE_TYPES.ObjectPattern) { |
|
if (secondArg.params[0].properties[0].type === AST_NODE_TYPES.Property && secondArg.params[0].properties[0].key.type === AST_NODE_TYPES.Identifier && secondArg.params[0].properties[0].key.name === "expect") |
|
return; |
|
} |
|
} |
|
if (!isFunction(secondArg) || !shouldCheckFunction(secondArg)) |
|
return; |
|
hasExpectInLoop = false; |
|
hasExpectInCallBack = false; |
|
if (hasExpectAssertAsFirstStatement) { |
|
hasExpectAssertAsFirstStatement = false; |
|
return; |
|
} |
|
const suggestions = []; |
|
if (secondArg.body.type === 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( |
|
[secondArg.body.range[0] + 1, secondArg.body.range[1]], |
|
text |
|
) |
|
})) |
|
}); |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
var PaddingType = /* @__PURE__ */ ((PaddingType2) => { |
|
PaddingType2[PaddingType2["Any"] = 0] = "Any"; |
|
PaddingType2[PaddingType2["Always"] = 1] = "Always"; |
|
return PaddingType2; |
|
})(PaddingType || {}); |
|
var StatementType = /* @__PURE__ */ ((StatementType2) => { |
|
StatementType2[StatementType2["Any"] = 0] = "Any"; |
|
StatementType2[StatementType2["AfterAllToken"] = 1] = "AfterAllToken"; |
|
StatementType2[StatementType2["AfterEachToken"] = 2] = "AfterEachToken"; |
|
StatementType2[StatementType2["BeforeAllToken"] = 3] = "BeforeAllToken"; |
|
StatementType2[StatementType2["BeforeEachToken"] = 4] = "BeforeEachToken"; |
|
StatementType2[StatementType2["DescribeToken"] = 5] = "DescribeToken"; |
|
StatementType2[StatementType2["ExpectToken"] = 6] = "ExpectToken"; |
|
StatementType2[StatementType2["FdescribeToken"] = 7] = "FdescribeToken"; |
|
StatementType2[StatementType2["FitToken"] = 8] = "FitToken"; |
|
StatementType2[StatementType2["ItToken"] = 9] = "ItToken"; |
|
StatementType2[StatementType2["TestToken"] = 10] = "TestToken"; |
|
StatementType2[StatementType2["XdescribeToken"] = 11] = "XdescribeToken"; |
|
StatementType2[StatementType2["XitToken"] = 12] = "XitToken"; |
|
StatementType2[StatementType2["XtestToken"] = 13] = "XtestToken"; |
|
return StatementType2; |
|
})(StatementType || {}); |
|
const paddingAlwaysTester = (prevNode, nextNode, paddingContext) => { |
|
const { sourceCode, ruleContext } = paddingContext; |
|
const paddingLines = getPaddingLineSequences(prevNode, nextNode, sourceCode); |
|
if (paddingLines.length > 0) |
|
return; |
|
ruleContext.report({ |
|
node: nextNode, |
|
messageId: "missingPadding", |
|
fix(fixer) { |
|
let prevToken = getActualLastToken(sourceCode, prevNode); |
|
const nextToken = sourceCode.getFirstTokenBetween(prevToken, nextNode, { |
|
includeComments: true, |
|
/** |
|
* Skip the trailing comments of the previous node. |
|
* This inserts a blank line after the last trailing comment. |
|
* |
|
* For example: |
|
* |
|
* foo(); // trailing comment. |
|
* // comment. |
|
* bar(); |
|
* |
|
* Get fixed to: |
|
* |
|
* foo(); // trailing comment. |
|
* |
|
* // comment. |
|
* bar(); |
|
*/ |
|
filter(token) { |
|
if (areTokensOnSameLine(prevToken, token)) { |
|
prevToken = token; |
|
return false; |
|
} |
|
return true; |
|
} |
|
}) || nextNode; |
|
const insertText = areTokensOnSameLine(prevToken, nextToken) ? "\n\n" : "\n"; |
|
return fixer.insertTextAfter(prevToken, insertText); |
|
} |
|
}); |
|
}; |
|
const paddingTesters = { |
|
[0 /* Any */]: () => true, |
|
[1 /* Always */]: paddingAlwaysTester |
|
}; |
|
const createScopeInfo = () => { |
|
let scope = null; |
|
return { |
|
get prevNode() { |
|
return scope.prevNode; |
|
}, |
|
set prevNode(node) { |
|
scope.prevNode = node; |
|
}, |
|
enter() { |
|
scope = { upper: scope, prevNode: null }; |
|
}, |
|
exit() { |
|
scope = scope.upper; |
|
} |
|
}; |
|
}; |
|
const createTokenTester = (tokenName) => { |
|
return (node, sourceCode) => { |
|
let activeNode = node; |
|
if (activeNode.type === AST_NODE_TYPES.ExpressionStatement) { |
|
if (activeNode.expression.type === AST_NODE_TYPES.AwaitExpression) { |
|
activeNode = activeNode.expression.argument; |
|
} |
|
const token = sourceCode.getFirstToken(activeNode); |
|
return token?.type === AST_TOKEN_TYPES.Identifier && token.value === tokenName; |
|
} |
|
return false; |
|
}; |
|
}; |
|
const statementTesters = { |
|
[0 /* Any */]: () => true, |
|
[1 /* AfterAllToken */]: createTokenTester("afterAll"), |
|
[2 /* AfterEachToken */]: createTokenTester("afterEach"), |
|
[3 /* BeforeAllToken */]: createTokenTester("beforeAll"), |
|
[4 /* BeforeEachToken */]: createTokenTester("beforeEach"), |
|
[5 /* DescribeToken */]: createTokenTester("describe"), |
|
[6 /* ExpectToken */]: createTokenTester("expect"), |
|
[7 /* FdescribeToken */]: createTokenTester("fdescribe"), |
|
[8 /* FitToken */]: createTokenTester("fit"), |
|
[9 /* ItToken */]: createTokenTester("it"), |
|
[10 /* TestToken */]: createTokenTester("test"), |
|
[11 /* XdescribeToken */]: createTokenTester("xdescribe"), |
|
[12 /* XitToken */]: createTokenTester("xit"), |
|
[13 /* XtestToken */]: createTokenTester("xtest") |
|
}; |
|
const nodeMatchesType = (node, statementType, paddingContext) => { |
|
let innerStatementNode = node; |
|
const { sourceCode } = paddingContext; |
|
while (innerStatementNode.type === AST_NODE_TYPES.LabeledStatement) { |
|
innerStatementNode = innerStatementNode.body; |
|
} |
|
if (Array.isArray(statementType)) { |
|
return statementType.some( |
|
(type) => nodeMatchesType(innerStatementNode, type, paddingContext) |
|
); |
|
} |
|
return statementTesters[statementType](innerStatementNode, sourceCode); |
|
}; |
|
const testPadding = (prevNode, nextNode, paddingContext) => { |
|
const { configs } = paddingContext; |
|
const testType = (type) => paddingTesters[type](prevNode, nextNode, paddingContext); |
|
for (let i = configs.length - 1; i >= 0; --i) { |
|
const { prevStatementType: prevType, nextStatementType: nextType, paddingType } = configs[i]; |
|
if (nodeMatchesType(prevNode, prevType, paddingContext) && nodeMatchesType(nextNode, nextType, paddingContext)) { |
|
return testType(paddingType); |
|
} |
|
} |
|
return testType(0 /* Any */); |
|
}; |
|
const verifyNode = (node, paddingContext) => { |
|
const { scopeInfo } = paddingContext; |
|
if (!isValidParent(node?.parent.type)) |
|
return; |
|
if (scopeInfo.prevNode) { |
|
testPadding(scopeInfo.prevNode, node, paddingContext); |
|
} |
|
scopeInfo.prevNode = node; |
|
}; |
|
const createPaddingRule = (name, description, configs, deprecated = false) => { |
|
return createEslintRule({ |
|
name, |
|
meta: { |
|
docs: { description }, |
|
fixable: "whitespace", |
|
deprecated, |
|
messages: { |
|
missingPadding: "expect blank line before this statement" |
|
}, |
|
schema: [], |
|
type: "suggestion" |
|
}, |
|
defaultOptions: [], |
|
create(context) { |
|
const paddingContext = { |
|
ruleContext: context, |
|
sourceCode: context.sourceCode ?? context.getSourceCode(), |
|
scopeInfo: createScopeInfo(), |
|
configs |
|
}; |
|
const { scopeInfo } = paddingContext; |
|
return { |
|
Program: scopeInfo.enter, |
|
"Program:exit": scopeInfo.exit, |
|
BlockStatement: scopeInfo.enter, |
|
"BlockStatement:exit": scopeInfo.exit, |
|
SwitchStatement: scopeInfo.enter, |
|
"SwitchStatement:exit": scopeInfo.exit, |
|
":statement": (node) => verifyNode(node, paddingContext), |
|
SwitchCase(node) { |
|
verifyNode(node, paddingContext); |
|
scopeInfo.enter(); |
|
}, |
|
"SwitchCase:exit": scopeInfo.exit |
|
}; |
|
} |
|
}); |
|
}; |
|
|
|
const RULE_NAME$7 = "padding-around-after-all-blocks"; |
|
const config$6 = [ |
|
{ |
|
paddingType: PaddingType.Always, |
|
prevStatementType: StatementType.Any, |
|
nextStatementType: StatementType.AfterAllToken |
|
}, |
|
{ |
|
paddingType: PaddingType.Always, |
|
prevStatementType: StatementType.AfterAllToken, |
|
nextStatementType: StatementType.Any |
|
} |
|
]; |
|
const paddingAroundAfterAllBlocks = createPaddingRule(RULE_NAME$7, "Enforce padding around `afterAll` blocks", config$6); |
|
|
|
const RULE_NAME$6 = "padding-around-after-each-blocks"; |
|
const config$5 = [ |
|
{ |
|
paddingType: PaddingType.Always, |
|
prevStatementType: StatementType.Any, |
|
nextStatementType: StatementType.AfterEachToken |
|
}, |
|
{ |
|
paddingType: PaddingType.Always, |
|
prevStatementType: StatementType.AfterEachToken, |
|
nextStatementType: StatementType.Any |
|
} |
|
]; |
|
const paddingAroundAfterEachBlocks = createPaddingRule(RULE_NAME$6, "Enforce padding around `afterEach` blocks", config$5); |
|
|
|
const RULE_NAME$5 = "padding-around-before-all-blocks"; |
|
const config$4 = [ |
|
{ |
|
paddingType: PaddingType.Always, |
|
prevStatementType: StatementType.Any, |
|
nextStatementType: StatementType.BeforeAllToken |
|
}, |
|
{ |
|
paddingType: PaddingType.Always, |
|
prevStatementType: StatementType.BeforeAllToken, |
|
nextStatementType: StatementType.Any |
|
} |
|
]; |
|
const paddingAroundBeforeAllBlocks = createPaddingRule(RULE_NAME$5, "Enforce padding around `beforeAll` blocks", config$4); |
|
|
|
const RULE_NAME$4 = "padding-around-before-each-blocks"; |
|
const config$3 = [ |
|
{ |
|
paddingType: PaddingType.Always, |
|
prevStatementType: StatementType.Any, |
|
nextStatementType: StatementType.BeforeEachToken |
|
}, |
|
{ |
|
paddingType: PaddingType.Always, |
|
prevStatementType: StatementType.BeforeEachToken, |
|
nextStatementType: StatementType.Any |
|
} |
|
]; |
|
const paddingAroundBeforeEachBlocks = createPaddingRule( |
|
RULE_NAME$4, |
|
"Enforce padding around `beforeEach` blocks", |
|
config$3 |
|
); |
|
|
|
const RULE_NAME$3 = "padding-around-describe-blocks"; |
|
const config$2 = [ |
|
{ |
|
paddingType: PaddingType.Always, |
|
prevStatementType: StatementType.Any, |
|
nextStatementType: [ |
|
StatementType.DescribeToken, |
|
StatementType.FdescribeToken, |
|
StatementType.XdescribeToken |
|
] |
|
}, |
|
{ |
|
paddingType: PaddingType.Always, |
|
prevStatementType: [ |
|
StatementType.DescribeToken, |
|
StatementType.FdescribeToken, |
|
StatementType.XdescribeToken |
|
], |
|
nextStatementType: StatementType.Any |
|
} |
|
]; |
|
const paddingAroundDescribeBlocks = createPaddingRule( |
|
RULE_NAME$3, |
|
"Enforce padding around `describe` blocks", |
|
config$2 |
|
); |
|
|
|
const RULE_NAME$2 = "padding-around-expect-groups"; |
|
const config$1 = [ |
|
{ |
|
paddingType: PaddingType.Always, |
|
prevStatementType: StatementType.Any, |
|
nextStatementType: StatementType.ExpectToken |
|
}, |
|
{ |
|
paddingType: PaddingType.Always, |
|
prevStatementType: StatementType.ExpectToken, |
|
nextStatementType: StatementType.Any |
|
}, |
|
{ |
|
paddingType: PaddingType.Any, |
|
prevStatementType: StatementType.ExpectToken, |
|
nextStatementType: StatementType.ExpectToken |
|
} |
|
]; |
|
const paddingAroundExpectGroups = createPaddingRule( |
|
RULE_NAME$2, |
|
"Enforce padding around `expect` groups", |
|
config$1 |
|
); |
|
|
|
const RULE_NAME$1 = "padding-around-test-blocks"; |
|
const config = [ |
|
{ |
|
paddingType: PaddingType.Always, |
|
prevStatementType: StatementType.Any, |
|
nextStatementType: [ |
|
StatementType.TestToken, |
|
StatementType.ItToken, |
|
StatementType.FitToken, |
|
StatementType.XitToken, |
|
StatementType.XtestToken |
|
] |
|
}, |
|
{ |
|
paddingType: PaddingType.Always, |
|
prevStatementType: [ |
|
StatementType.TestToken, |
|
StatementType.ItToken, |
|
StatementType.FitToken, |
|
StatementType.XitToken, |
|
StatementType.XtestToken |
|
], |
|
nextStatementType: StatementType.Any |
|
} |
|
]; |
|
const paddingAroundTestBlocks = createPaddingRule( |
|
RULE_NAME$1, |
|
"Enforce padding around afterAll blocks", |
|
config |
|
); |
|
|
|
const RULE_NAME = "padding-around-all"; |
|
const paddingAroundAll = createPaddingRule( |
|
RULE_NAME, |
|
"Enforce padding around vitest functions", |
|
[ |
|
...config$6, |
|
...config$5, |
|
...config$4, |
|
...config$3, |
|
...config$2, |
|
...config$1, |
|
...config |
|
] |
|
); |
|
|
|
const createConfig = (rules) => Object.keys(rules).reduce((acc, ruleName) => { |
|
return { |
|
...acc, |
|
[`vitest/${ruleName}`]: rules[ruleName] |
|
}; |
|
}, {}); |
|
const createConfigLegacy = (rules) => ({ |
|
plugins: ["@vitest"], |
|
rules: Object.keys(rules).reduce((acc, ruleName) => { |
|
return { |
|
...acc, |
|
[`@vitest/${ruleName}`]: rules[ruleName] |
|
}; |
|
}, {}) |
|
}); |
|
const allRules = { |
|
[RULE_NAME$Y]: "warn", |
|
[RULE_NAME$X]: "warn", |
|
[RULE_NAME$V]: "warn", |
|
[RULE_NAME$U]: "warn", |
|
[RULE_NAME$S]: "warn", |
|
[RULE_NAME$Q]: "warn", |
|
[RULE_NAME$P]: "warn", |
|
[RULE_NAME$O]: "warn", |
|
[RULE_NAME$N]: "warn", |
|
[RULE_NAME$M]: "warn", |
|
[RULE_NAME$K]: "warn", |
|
[RULE_NAME$I]: "warn", |
|
[RULE_NAME$H]: "warn", |
|
[RULE_NAME$G]: "warn", |
|
[RULE_NAME$F]: "warn", |
|
[RULE_NAME$E]: "warn", |
|
[RULE_NAME$D]: "warn", |
|
[RULE_NAME$C]: "warn", |
|
[RULE_NAME$B]: "warn", |
|
[RULE_NAME$A]: "warn", |
|
[RULE_NAME$z]: "warn", |
|
[RULE_NAME$y]: "warn", |
|
[RULE_NAME$x]: "warn", |
|
[RULE_NAME$s]: "warn", |
|
[RULE_NAME$u]: "warn", |
|
[RULE_NAME$t]: "warn", |
|
[RULE_NAME$r]: "warn", |
|
[RULE_NAME$q]: "warn", |
|
[RULE_NAME$p]: "warn", |
|
[RULE_NAME$o]: "warn", |
|
[RULE_NAME$n]: "warn", |
|
[RULE_NAME$m]: "warn", |
|
[RULE_NAME$l]: "warn", |
|
[RULE_NAME$k]: "warn", |
|
[RULE_NAME$j]: "warn", |
|
[RULE_NAME$i]: "warn", |
|
[RULE_NAME$g]: "warn", |
|
[RULE_NAME$f]: "warn", |
|
[RULE_NAME$e]: "warn", |
|
[RULE_NAME$c]: "warn", |
|
[RULE_NAME$b]: "warn", |
|
[RULE_NAME$a]: "warn", |
|
[RULE_NAME$9]: "warn", |
|
[RULE_NAME$8]: "warn", |
|
[RULE_NAME$R]: "warn", |
|
[RULE_NAME$7]: "warn", |
|
[RULE_NAME$6]: "warn", |
|
[RULE_NAME]: "warn", |
|
[RULE_NAME$5]: "warn", |
|
[RULE_NAME$4]: "warn", |
|
[RULE_NAME$3]: "warn", |
|
[RULE_NAME$2]: "warn", |
|
[RULE_NAME$1]: "warn" |
|
}; |
|
const recommended = { |
|
[RULE_NAME$T]: "error", |
|
[RULE_NAME$W]: "error", |
|
[RULE_NAME$L]: "error", |
|
[RULE_NAME$w]: "error", |
|
[RULE_NAME$v]: "error", |
|
[RULE_NAME$h]: "error", |
|
[RULE_NAME$d]: "error", |
|
[RULE_NAME$J]: "error" |
|
}; |
|
const plugin = { |
|
meta: { |
|
name: "vitest", |
|
version |
|
}, |
|
rules: { |
|
[RULE_NAME$Y]: lowerCaseTitle, |
|
[RULE_NAME$X]: maxNestedDescribe, |
|
[RULE_NAME$W]: noIdenticalTitle, |
|
[RULE_NAME$V]: noFocusedTests, |
|
[RULE_NAME$U]: noConditionalTest, |
|
[RULE_NAME$T]: expectExpect, |
|
[RULE_NAME$S]: consistentTestIt, |
|
[RULE_NAME$R]: preferToBe, |
|
[RULE_NAME$Q]: noHooks, |
|
[RULE_NAME$P]: noRestrictedViMethods, |
|
[RULE_NAME$O]: consistentTestFilename, |
|
[RULE_NAME$N]: maxExpect, |
|
[RULE_NAME$M]: noAliasMethod, |
|
[RULE_NAME$L]: noCommentedOutTests, |
|
[RULE_NAME$K]: noConditionalExpect, |
|
[RULE_NAME$I]: noConditionalInTest, |
|
[RULE_NAME$H]: noDisabledTests, |
|
[RULE_NAME$G]: noDoneCallback, |
|
[RULE_NAME$F]: noDuplicateHooks, |
|
[RULE_NAME$E]: noLargeSnapshots, |
|
[RULE_NAME$D]: nonInterpolationInSnapShots, |
|
[RULE_NAME$C]: noMocksImport, |
|
[RULE_NAME$B]: noRestrictedMatchers, |
|
[RULE_NAME$A]: noStandaloneExpect, |
|
[RULE_NAME$z]: noTestPrefixes, |
|
[RULE_NAME$y]: noTestReturnStatement, |
|
[RULE_NAME$J]: noImportNodeTest, |
|
[RULE_NAME$x]: preferCalledWith, |
|
[RULE_NAME$w]: validTitle, |
|
[RULE_NAME$v]: validExpect, |
|
[RULE_NAME$s]: preferToBeFalsy, |
|
[RULE_NAME$u]: preferToBeObject, |
|
[RULE_NAME$t]: preferToBeTruthy, |
|
[RULE_NAME$r]: preferToHaveLength, |
|
[RULE_NAME$q]: preferEqualityMatcher, |
|
[RULE_NAME$p]: preferStrictEqual, |
|
[RULE_NAME$o]: preferExpectResolves, |
|
[RULE_NAME$n]: preferEach, |
|
[RULE_NAME$m]: preferHooksOnTop, |
|
[RULE_NAME$l]: preferHooksInOrder, |
|
[RULE_NAME$d]: requireLocalTestContextForConcurrentSnapshots, |
|
[RULE_NAME$k]: preferMockPromiseShorthand, |
|
[RULE_NAME$j]: preferViMocked, |
|
[RULE_NAME$i]: preferSnapshotHint, |
|
[RULE_NAME$h]: validDescribeCallback, |
|
[RULE_NAME$g]: requireTopLevelDescribe, |
|
[RULE_NAME$f]: requireToThrowMessage, |
|
[RULE_NAME$e]: requireHook, |
|
[RULE_NAME$c]: preferTodo, |
|
[RULE_NAME$b]: preferSpyOn, |
|
[RULE_NAME$a]: preferComparisonMatcher, |
|
[RULE_NAME$9]: preferToContain, |
|
[RULE_NAME$8]: preferExpectAssertions, |
|
[RULE_NAME$7]: paddingAroundAfterAllBlocks, |
|
[RULE_NAME$6]: paddingAroundAfterEachBlocks, |
|
[RULE_NAME]: paddingAroundAll, |
|
[RULE_NAME$5]: paddingAroundBeforeAllBlocks, |
|
[RULE_NAME$4]: paddingAroundBeforeEachBlocks, |
|
[RULE_NAME$3]: paddingAroundDescribeBlocks, |
|
[RULE_NAME$2]: paddingAroundExpectGroups, |
|
[RULE_NAME$1]: paddingAroundTestBlocks |
|
}, |
|
configs: { |
|
"legacy-recommended": createConfigLegacy(recommended), |
|
"legacy-all": createConfigLegacy(allRules), |
|
"recommended": { |
|
plugins: { |
|
get vitest() { |
|
return plugin; |
|
} |
|
}, |
|
rules: createConfig(recommended) |
|
}, |
|
"all": { |
|
plugins: { |
|
get vitest() { |
|
return plugin; |
|
} |
|
}, |
|
rules: createConfig(allRules) |
|
}, |
|
"env": { |
|
languageOptions: { |
|
globals: { |
|
suite: "writable", |
|
test: "writable", |
|
describe: "writable", |
|
it: "writable", |
|
expect: "writable", |
|
assert: "writable", |
|
vitest: "writable", |
|
vi: "writable", |
|
beforeAll: "writable", |
|
afterAll: "writable", |
|
beforeEach: "writable", |
|
afterEach: "writable" |
|
} |
|
} |
|
} |
|
}, |
|
environments: { |
|
env: { |
|
globals: { |
|
suite: true, |
|
test: true, |
|
describe: true, |
|
it: true, |
|
expect: true, |
|
assert: true, |
|
vitest: true, |
|
vi: true, |
|
beforeAll: true, |
|
afterAll: true, |
|
beforeEach: true, |
|
afterEach: true |
|
} |
|
} |
|
} |
|
}; |
|
|
|
export { plugin as default };
|
|
|