@dineroregnskab/eslint-plugin-custom-rules 4.3.0 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +9 -6
- package/rules/no-feature-toggle-without-await.js +69 -94
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ module.exports = {
|
|
|
63
63
|
### Publish & install new rule locally
|
|
64
64
|
|
|
65
65
|
1. Log in to npm using `npm login`
|
|
66
|
-
2. run
|
|
66
|
+
2. run:
|
|
67
67
|
|
|
68
68
|
```bash
|
|
69
69
|
# {version_type}: The type of version increment (patch, minor, major)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dineroregnskab/eslint-plugin-custom-rules",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.0",
|
|
4
4
|
"description": "ESLint plugin with custom rules for Dinero Regnskab",
|
|
5
5
|
"main": "eslint-plugin-custom-rules.js",
|
|
6
6
|
"scripts": {
|
|
@@ -10,20 +10,23 @@
|
|
|
10
10
|
"author": "",
|
|
11
11
|
"license": "ISC",
|
|
12
12
|
"devDependencies": {
|
|
13
|
+
"@angular-eslint/template-parser": "^19.2.1",
|
|
13
14
|
"@typescript-eslint/parser": "^8.26.1",
|
|
14
15
|
"eslint": "^9.22.0",
|
|
15
16
|
"eslint-config-prettier": "^10.1.1",
|
|
16
17
|
"eslint-plugin-prettier": "^5.2.3",
|
|
17
|
-
"prettier": "^3.5.3"
|
|
18
|
-
"@angular-eslint/template-parser": "^19.2.1"
|
|
18
|
+
"prettier": "^3.5.3"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
|
-
"eslint": ">=
|
|
21
|
+
"@angular-eslint/template-parser": ">=17",
|
|
22
22
|
"@typescript-eslint/parser": ">=7",
|
|
23
|
-
"
|
|
23
|
+
"eslint": ">=8.0.0"
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"**/*",
|
|
27
27
|
"!example/**/*"
|
|
28
|
-
]
|
|
28
|
+
],
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@dineroregnskab/eslint-plugin-custom-rules": "^4.0.1"
|
|
31
|
+
}
|
|
29
32
|
}
|
|
@@ -3,127 +3,107 @@ module.exports = {
|
|
|
3
3
|
type: 'suggestion',
|
|
4
4
|
docs: {
|
|
5
5
|
description:
|
|
6
|
-
'Warn when
|
|
6
|
+
'Warn when an it() block is missing awaitFeatureReady while createUserWithOrganization uses featureToggles without awaiting',
|
|
7
7
|
category: 'Best Practices',
|
|
8
8
|
recommended: true,
|
|
9
9
|
},
|
|
10
10
|
messages: {
|
|
11
11
|
missingAwaitFeatureReady:
|
|
12
|
-
'
|
|
12
|
+
'This it() block is missing cy.awaitFeatureReady() / cy.awaitFeaturesReady(), and createUserWithOrganization uses featureToggles without awaiting.',
|
|
13
13
|
},
|
|
14
14
|
},
|
|
15
15
|
|
|
16
16
|
create(context) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
function findNearestDescribe(node) {
|
|
21
|
-
let current = node && node.parent;
|
|
22
|
-
while (current) {
|
|
23
|
-
if (
|
|
24
|
-
current.type === 'CallExpression' &&
|
|
25
|
-
current.callee &&
|
|
26
|
-
(current.callee.name === 'describe' || current.callee.name === 'context')
|
|
27
|
-
) {
|
|
28
|
-
return current;
|
|
29
|
-
}
|
|
30
|
-
current = current.parent;
|
|
31
|
-
}
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
17
|
+
let hasFeatureToggles = false;
|
|
18
|
+
let beforeEachHasAwait = false;
|
|
34
19
|
|
|
35
20
|
return {
|
|
36
21
|
'CallExpression[callee.name="beforeEach"]'(node) {
|
|
37
|
-
const describeNode = findNearestDescribe(node);
|
|
38
|
-
if (!describeNode) return;
|
|
39
|
-
|
|
40
22
|
const callback = node.arguments[0];
|
|
41
23
|
if (!callback) return;
|
|
42
24
|
|
|
43
|
-
let hasFeatureToggles = false;
|
|
44
25
|
walkNode(callback, (child) => {
|
|
45
26
|
if (
|
|
46
27
|
child.type === 'CallExpression' &&
|
|
47
|
-
child.callee &&
|
|
48
|
-
child
|
|
28
|
+
child.callee?.name === 'createUserWithOrganization' &&
|
|
29
|
+
hasFeatureTogglesArg(child)
|
|
49
30
|
) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
prop.key &&
|
|
56
|
-
(prop.key.name || prop.key.value)
|
|
57
|
-
) {
|
|
58
|
-
const keyName = prop.key.name || prop.key.value;
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
keyName === 'featureToggles' &&
|
|
62
|
-
prop.value.type === 'ArrayExpression' &&
|
|
63
|
-
prop.value.elements.length > 0
|
|
64
|
-
) {
|
|
65
|
-
hasFeatureToggles = true;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
});
|
|
31
|
+
hasFeatureToggles = true;
|
|
32
|
+
|
|
33
|
+
const chainRoot = findChainRoot(child);
|
|
34
|
+
if (hasAwaitFeatureReady(chainRoot)) {
|
|
35
|
+
beforeEachHasAwait = true;
|
|
69
36
|
}
|
|
70
37
|
}
|
|
71
38
|
});
|
|
72
|
-
|
|
73
|
-
if (hasFeatureToggles) {
|
|
74
|
-
describeFeatureMap.set(describeNode, true);
|
|
75
|
-
}
|
|
76
39
|
},
|
|
77
40
|
|
|
78
41
|
'CallExpression[callee.name="it"]'(node) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
let hasFeatureTogglesInAnyAncestor = false;
|
|
82
|
-
while (current) {
|
|
83
|
-
if (
|
|
84
|
-
current.type === 'CallExpression' &&
|
|
85
|
-
current.callee &&
|
|
86
|
-
(current.callee.name === 'describe' || current.callee.name === 'context')
|
|
87
|
-
) {
|
|
88
|
-
if (describeFeatureMap.get(current)) {
|
|
89
|
-
hasFeatureTogglesInAnyAncestor = true;
|
|
90
|
-
break;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
current = current.parent;
|
|
94
|
-
}
|
|
95
|
-
if (!hasFeatureTogglesInAnyAncestor) return;
|
|
42
|
+
if (!hasFeatureToggles) return;
|
|
43
|
+
if (beforeEachHasAwait) return;
|
|
96
44
|
|
|
97
|
-
const callback = node.arguments[1]
|
|
45
|
+
const callback = node.arguments[1];
|
|
98
46
|
if (!callback) return;
|
|
99
47
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
child.type === 'CallExpression' &&
|
|
104
|
-
child.callee &&
|
|
105
|
-
child.callee.type === 'MemberExpression' &&
|
|
106
|
-
child.callee.object &&
|
|
107
|
-
child.callee.object.name === 'cy' &&
|
|
108
|
-
child.callee.property &&
|
|
109
|
-
(child.callee.property.name === 'awaitFeatureReady' ||
|
|
110
|
-
child.callee.property.name === 'awaitFeaturesReady')
|
|
111
|
-
) {
|
|
112
|
-
hasAwaitFeatureReady = true;
|
|
113
|
-
}
|
|
114
|
-
});
|
|
48
|
+
const itHasAwait = hasAwaitFeatureReady(callback);
|
|
49
|
+
if (itHasAwait) return;
|
|
115
50
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
});
|
|
121
|
-
}
|
|
51
|
+
context.report({
|
|
52
|
+
node,
|
|
53
|
+
messageId: 'missingAwaitFeatureReady',
|
|
54
|
+
});
|
|
122
55
|
},
|
|
123
56
|
};
|
|
124
57
|
},
|
|
125
58
|
};
|
|
126
59
|
|
|
60
|
+
/* ================= HELPERS ================= */
|
|
61
|
+
|
|
62
|
+
function hasFeatureTogglesArg(callExpression) {
|
|
63
|
+
const arg = callExpression.arguments[0];
|
|
64
|
+
if (!arg || arg.type !== 'ObjectExpression') return false;
|
|
65
|
+
|
|
66
|
+
return arg.properties.some((prop) => {
|
|
67
|
+
const key = prop.key?.name || prop.key?.value;
|
|
68
|
+
return (
|
|
69
|
+
key === 'featureToggles' &&
|
|
70
|
+
prop.value.type === 'ArrayExpression' &&
|
|
71
|
+
prop.value.elements.length > 0
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function hasAwaitFeatureReady(node) {
|
|
77
|
+
let found = false;
|
|
78
|
+
|
|
79
|
+
walkNode(node, (n) => {
|
|
80
|
+
if (
|
|
81
|
+
n.type === 'CallExpression' &&
|
|
82
|
+
n.callee?.type === 'MemberExpression' &&
|
|
83
|
+
n.callee.object?.name === 'cy' &&
|
|
84
|
+
(n.callee.property?.name === 'awaitFeatureReady' ||
|
|
85
|
+
n.callee.property?.name === 'awaitFeaturesReady')
|
|
86
|
+
) {
|
|
87
|
+
found = true;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return found;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function findChainRoot(node) {
|
|
95
|
+
let current = node;
|
|
96
|
+
|
|
97
|
+
while (
|
|
98
|
+
current.parent?.type === 'MemberExpression' &&
|
|
99
|
+
current.parent.parent?.type === 'CallExpression'
|
|
100
|
+
) {
|
|
101
|
+
current = current.parent.parent;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return current;
|
|
105
|
+
}
|
|
106
|
+
|
|
127
107
|
function walkNode(node, callback) {
|
|
128
108
|
if (!node) return;
|
|
129
109
|
|
|
@@ -133,14 +113,9 @@ function walkNode(node, callback) {
|
|
|
133
113
|
if (key === 'parent' || key === 'loc' || key === 'range') continue;
|
|
134
114
|
|
|
135
115
|
const child = node[key];
|
|
136
|
-
|
|
137
116
|
if (Array.isArray(child)) {
|
|
138
|
-
child.forEach((
|
|
139
|
-
|
|
140
|
-
walkNode(item, callback);
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
} else if (child && typeof child === 'object' && child.type) {
|
|
117
|
+
child.forEach((c) => c?.type && walkNode(c, callback));
|
|
118
|
+
} else if (child?.type) {
|
|
144
119
|
walkNode(child, callback);
|
|
145
120
|
}
|
|
146
121
|
}
|