@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 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.2.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,112 +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
- let describeStack = [];
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.callee.name === 'createUserWithOrganization'
28
+ child.callee?.name === 'createUserWithOrganization' &&
29
+ hasFeatureTogglesArg(child)
39
30
  ) {
40
- const arg = child.arguments[0];
41
- if (arg && arg.type === 'ObjectExpression') {
42
- arg.properties.forEach((prop) => {
43
- if (
44
- prop.type === 'Property' &&
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 (describeStack.length === 0) return;
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
- let hasAwaitFeatureReady = false;
48
+ const itHasAwait = hasAwaitFeatureReady(callback);
49
+ if (itHasAwait) return;
81
50
 
82
- walkNode(callback, (child) => {
83
- if (
84
- child.type === 'CallExpression' &&
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((item) => {
124
- if (item && typeof item === 'object' && item.type) {
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
  }