-
Notifications
You must be signed in to change notification settings - Fork 21
/
no-hooks-from-ancestor-modules.js
117 lines (101 loc) · 4.84 KB
/
no-hooks-from-ancestor-modules.js
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
114
115
116
117
/**
* @fileoverview disallow the use of hooks from ancestor modules
* @author Raymond Cohen
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const utils = require("../utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
const NESTABLE_HOOK_NAMES = new Set(["afterEach", "beforeEach"]);
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: "problem",
docs: {
description: "disallow the use of hooks from ancestor modules",
category: "Possible Errors",
recommended: false,
url: "https://github.com/platinumazure/eslint-plugin-qunit/blob/master/docs/rules/no-hooks-from-ancestor-modules.md"
},
fixable: null,
messages: {
"noHooksFromAncestorModules": "Do not call {{usedHooksIdentifierName}}.{{hookName}} from an ancestor module."
},
schema: []
},
create: function (context) {
const moduleStack = [];
//----------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------
function isInModuleCallbackBody(callExpressionNode) {
return callExpressionNode &&
callExpressionNode.parent &&
callExpressionNode.parent.type === "ExpressionStatement" &&
callExpressionNode.parent.parent &&
callExpressionNode.parent.parent.type === "BlockStatement" &&
callExpressionNode.parent.parent.parent &&
callExpressionNode.parent.parent.parent.type === "FunctionExpression" &&
callExpressionNode.parent.parent.parent.parent &&
callExpressionNode.parent.parent.parent.parent.type === "CallExpression" &&
utils.isModule(callExpressionNode.parent.parent.parent.parent.callee);
}
function isHookInvocation(node) {
return node.callee.type === "MemberExpression" &&
node.callee.object.type === "Identifier" &&
NESTABLE_HOOK_NAMES.has(node.callee.property.name) &&
isInModuleCallbackBody(node);
}
function getCallbackArg(args) {
// Callback can be either args[1] or args[2]
// https://api.qunitjs.com/QUnit/module/
return args.slice(1, 3).find(arg => arg.type === "FunctionExpression");
}
function getHooksIdentifierFromParams(params) {
// In TypeScript, `this` can be passed as the first function parameter to add a type to it,
// and we want to ignore that parameter since we're looking for the `hooks` variable.
return params.find(p => p.type === "Identifier" && p.name !== "this");
}
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
return {
"CallExpression": function (node) {
if (utils.isModule(node.callee)) {
const moduleStackInfo = {
callExpression: node,
description: node.arguments[0].value
};
const callback = getCallbackArg(node.arguments);
const hooksParam = callback ? getHooksIdentifierFromParams(callback.params) : null;
moduleStackInfo.hookIdentifierName = hooksParam ? hooksParam.name : null;
moduleStack.push(moduleStackInfo);
} else if (isHookInvocation(node)) {
const containingModuleInfo = moduleStack[moduleStack.length - 1];
const expectedHooksIdentifierName = containingModuleInfo.hookIdentifierName;
const usedHooksIdentifierName = node.callee.object.name;
const invokedMethodName = node.callee.property.name;
if (expectedHooksIdentifierName !== usedHooksIdentifierName) {
context.report({
node: node.callee,
messageId: "noHooksFromAncestorModules",
data: {
invokedMethodName,
usedHooksIdentifierName
}
});
}
}
},
"CallExpression:exit": function (node) {
if (utils.isModule(node.callee)) {
moduleStack.pop();
}
}
};
}
};