@dineroregnskab/eslint-plugin-custom-rules 4.2.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 +67 -77
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,112 +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
|
-
let
|
|
17
|
+
let hasFeatureToggles = false;
|
|
18
|
+
let beforeEachHasAwait = false;
|
|
18
19
|
|
|
19
20
|
return {
|
|
20
|
-
'CallExpression[callee.name="describe"], CallExpression[callee.name="context"]'() {
|
|
21
|
-
describeStack.push({
|
|
22
|
-
beforeEachHasFeatureToggles: false,
|
|
23
|
-
});
|
|
24
|
-
},
|
|
25
|
-
|
|
26
21
|
'CallExpression[callee.name="beforeEach"]'(node) {
|
|
27
|
-
if (describeStack.length === 0) return;
|
|
28
|
-
|
|
29
|
-
const currentLevel = describeStack[describeStack.length - 1];
|
|
30
22
|
const callback = node.arguments[0];
|
|
31
23
|
if (!callback) return;
|
|
32
24
|
|
|
33
|
-
let hasFeatureToggles = false;
|
|
34
25
|
walkNode(callback, (child) => {
|
|
35
26
|
if (
|
|
36
27
|
child.type === 'CallExpression' &&
|
|
37
|
-
child.callee &&
|
|
38
|
-
child
|
|
28
|
+
child.callee?.name === 'createUserWithOrganization' &&
|
|
29
|
+
hasFeatureTogglesArg(child)
|
|
39
30
|
) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
prop.key &&
|
|
46
|
-
(prop.key.name || prop.key.value)
|
|
47
|
-
) {
|
|
48
|
-
const keyName = prop.key.name || prop.key.value;
|
|
49
|
-
|
|
50
|
-
if (
|
|
51
|
-
keyName === 'featureToggles' &&
|
|
52
|
-
prop.value.type === 'ArrayExpression' &&
|
|
53
|
-
prop.value.elements.length > 0
|
|
54
|
-
) {
|
|
55
|
-
hasFeatureToggles = true;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
});
|
|
31
|
+
hasFeatureToggles = true;
|
|
32
|
+
|
|
33
|
+
const chainRoot = findChainRoot(child);
|
|
34
|
+
if (hasAwaitFeatureReady(chainRoot)) {
|
|
35
|
+
beforeEachHasAwait = true;
|
|
59
36
|
}
|
|
60
37
|
}
|
|
61
38
|
});
|
|
62
|
-
|
|
63
|
-
if (hasFeatureToggles) {
|
|
64
|
-
currentLevel.beforeEachHasFeatureToggles = true;
|
|
65
|
-
}
|
|
66
39
|
},
|
|
67
40
|
|
|
68
41
|
'CallExpression[callee.name="it"]'(node) {
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
const hasFeatureTogglesInAnyAncestor = describeStack.some(
|
|
72
|
-
(level) => level.beforeEachHasFeatureToggles
|
|
73
|
-
);
|
|
74
|
-
if (!hasFeatureTogglesInAnyAncestor) return;
|
|
75
|
-
|
|
42
|
+
if (!hasFeatureToggles) return;
|
|
43
|
+
if (beforeEachHasAwait) return;
|
|
76
44
|
|
|
77
45
|
const callback = node.arguments[1];
|
|
78
46
|
if (!callback) return;
|
|
79
47
|
|
|
80
|
-
|
|
48
|
+
const itHasAwait = hasAwaitFeatureReady(callback);
|
|
49
|
+
if (itHasAwait) return;
|
|
81
50
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
child.callee &&
|
|
86
|
-
child.callee.type === 'MemberExpression' &&
|
|
87
|
-
child.callee.object &&
|
|
88
|
-
child.callee.object.name === 'cy' &&
|
|
89
|
-
child.callee.property &&
|
|
90
|
-
(child.callee.property.name === 'awaitFeatureReady' ||
|
|
91
|
-
child.callee.property.name === 'awaitFeaturesReady')
|
|
92
|
-
) {
|
|
93
|
-
hasAwaitFeatureReady = true;
|
|
94
|
-
}
|
|
51
|
+
context.report({
|
|
52
|
+
node,
|
|
53
|
+
messageId: 'missingAwaitFeatureReady',
|
|
95
54
|
});
|
|
96
|
-
|
|
97
|
-
if (!hasAwaitFeatureReady) {
|
|
98
|
-
context.report({
|
|
99
|
-
node: node,
|
|
100
|
-
messageId: 'missingAwaitFeatureReady',
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
},
|
|
104
|
-
|
|
105
|
-
'CallExpression[callee.name="describe"]:exit, CallExpression[callee.name="context"]:exit'() {
|
|
106
|
-
describeStack.pop();
|
|
107
55
|
},
|
|
108
56
|
};
|
|
109
57
|
},
|
|
110
58
|
};
|
|
111
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
|
+
|
|
112
107
|
function walkNode(node, callback) {
|
|
113
108
|
if (!node) return;
|
|
114
109
|
|
|
@@ -118,14 +113,9 @@ function walkNode(node, callback) {
|
|
|
118
113
|
if (key === 'parent' || key === 'loc' || key === 'range') continue;
|
|
119
114
|
|
|
120
115
|
const child = node[key];
|
|
121
|
-
|
|
122
116
|
if (Array.isArray(child)) {
|
|
123
|
-
child.forEach((
|
|
124
|
-
|
|
125
|
-
walkNode(item, callback);
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
} else if (child && typeof child === 'object' && child.type) {
|
|
117
|
+
child.forEach((c) => c?.type && walkNode(c, callback));
|
|
118
|
+
} else if (child?.type) {
|
|
129
119
|
walkNode(child, callback);
|
|
130
120
|
}
|
|
131
121
|
}
|