-
Notifications
You must be signed in to change notification settings - Fork 176
/
rule.ts
113 lines (105 loc) · 3.59 KB
/
rule.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
/*
* SonarQube JavaScript Plugin
* Copyright (C) 2011-2024 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// https://sonarsource.github.io/rspec/#/rspec/S2699/javascript
import { Rule, SourceCode } from 'eslint';
import * as estree from 'estree';
import { childrenOf } from '../../linter';
import { Chai, isFunctionCall, Mocha, resolveFunction, Sinon, Vitest } from '../helpers';
/**
* We assume that the user is using a single assertion library per file,
* this is why we are not saving if an assertion has been performed for
* libX and the imported library was libY.
*/
export const rule: Rule.RuleModule = {
create(context: Rule.RuleContext) {
const visitedNodes: Set<estree.Node> = new Set();
const potentialIssues: Rule.ReportDescriptor[] = [];
return {
'CallExpression:exit': (node: estree.Node) => {
const testCase = Mocha.extractTestCase(node);
if (testCase !== null) {
checkAssertions(testCase, context, potentialIssues, visitedNodes);
}
},
'Program:exit': () => {
if (Chai.isImported(context) || Sinon.isImported(context) || Vitest.isImported(context)) {
potentialIssues.forEach(issue => {
context.report(issue);
});
}
},
};
},
};
function checkAssertions(
testCase: Mocha.TestCase,
context: Rule.RuleContext,
potentialIssues: Rule.ReportDescriptor[],
visitedNodes: Set<estree.Node>,
) {
const { node, callback } = testCase;
const visitor = new TestCaseAssertionVisitor(context);
visitor.visit(context, callback.body, visitedNodes);
if (visitor.missingAssertions()) {
potentialIssues.push({ node, message: 'Add at least one assertion to this test case.' });
}
}
class TestCaseAssertionVisitor {
private readonly visitorKeys: SourceCode.VisitorKeys;
private hasAssertions: boolean;
constructor(private readonly context: Rule.RuleContext) {
this.visitorKeys = context.sourceCode.visitorKeys;
this.hasAssertions = false;
}
visit(context: Rule.RuleContext, node: estree.Node, visitedNodes: Set<estree.Node>) {
if (visitedNodes.has(node)) {
return;
}
visitedNodes.add(node);
if (this.hasAssertions) {
return;
}
if (
Chai.isAssertion(context, node) ||
Sinon.isAssertion(context, node) ||
Vitest.isAssertion(context, node)
) {
this.hasAssertions = true;
return;
}
if (isFunctionCall(node)) {
const { callee } = node;
if (callee.name === 'expect') {
this.hasAssertions = true;
return;
}
const functionDef = resolveFunction(this.context, callee);
if (functionDef) {
this.visit(context, functionDef.body, visitedNodes);
}
}
for (const child of childrenOf(node, this.visitorKeys)) {
this.visit(context, child, visitedNodes);
}
}
missingAssertions() {
return !this.hasAssertions;
}
}