-
Notifications
You must be signed in to change notification settings - Fork 27
/
expect-expect.ts
113 lines (100 loc) · 3.13 KB
/
expect-expect.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'
import { createEslintRule, getNodeName, isSupportedAccessor } from '../utils'
import { parsePluginSettings } from '../utils/parsePluginSettings'
import { getTestCallExpressionsFromDeclaredVariables, isTypeOfVitestFnCall } from '../utils/parseVitestFnCall'
export const RULE_NAME = 'expect-expect'
export type MESSAGE_ID = 'noAssertions';
type Options = [
{
assertFunctionNames: string[],
additionalTestBlockFunctions: string[]
}
]
function matchesAssertFunctionName(
nodeName: string,
patterns: readonly string[]
): boolean {
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)
)
}
export default createEslintRule<Options, MESSAGE_ID>({
name: RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Enforce having expectation in test body',
recommended: 'strict'
},
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'], additionalTestBlockFunctions: [] }],
create(context, [{ assertFunctionNames = ['expect'], additionalTestBlockFunctions = [] }]) {
const unchecked: TSESTree.CallExpression[] = []
const settings = parsePluginSettings(context.settings)
if (settings.typecheck) assertFunctionNames.push('expectTypeOf')
function checkCallExpression(nodes: TSESTree.Node[]) {
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.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.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.getAncestors())
}
},
'Program:exit'() {
unchecked.forEach(node => {
context.report({
node: node.callee,
messageId: 'noAssertions'
})
})
}
}
}
})