@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 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 (in ./Packages/JavaScript/eslint-plugin-custom-rules/):
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.0",
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": ">=8.0.0",
21
+ "@angular-eslint/template-parser": ">=17",
22
22
  "@typescript-eslint/parser": ">=7",
23
- "@angular-eslint/template-parser": ">=17"
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 createUserWithOrganization uses featureToggles in beforeEach but awaitFeatureReady is not used in the it() block',
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
- 'Feature toggles detected in beforeEach via createUserWithOrganization, but cy.awaitFeatureReady() not found in this it() block.',
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
- // Map each describe/context CallExpression node -> whether its beforeEach contains feature toggles
18
- const describeFeatureMap = new WeakMap();
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.callee.name === 'createUserWithOrganization'
28
+ child.callee?.name === 'createUserWithOrganization' &&
29
+ hasFeatureTogglesArg(child)
49
30
  ) {
50
- const arg = child.arguments[0];
51
- if (arg && arg.type === 'ObjectExpression') {
52
- arg.properties.forEach((prop) => {
53
- if (
54
- prop.type === 'Property' &&
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
- // Walk ancestors to see if any describe/context has feature toggles
80
- let current = node.parent;
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] || node.arguments[0];
45
+ const callback = node.arguments[1];
98
46
  if (!callback) return;
99
47
 
100
- let hasAwaitFeatureReady = false;
101
- walkNode(callback, (child) => {
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
- if (!hasAwaitFeatureReady) {
117
- context.report({
118
- node: node,
119
- messageId: 'missingAwaitFeatureReady',
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((item) => {
139
- if (item && typeof item === 'object' && item.type) {
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
  }