@dineroregnskab/eslint-plugin-custom-rules 4.7.0 → 4.9.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/CLAUDE.md ADDED
File without changes
@@ -12,6 +12,8 @@ const rules = {
12
12
  'enum-comparison-reminder': require('./rules/enum-comparison-reminder'),
13
13
  'enum-lowercase': require('./rules/enum-lowercase'),
14
14
  'no-feature-toggle-without-await': require('./rules/no-feature-toggle-without-await'),
15
+ 'no-window-location-redirect': require('./rules/no-window-location-redirect'),
16
+ 'no-actions-subscribe-in-component': require('./rules/no-actions-subscribe-in-component'),
15
17
  };
16
18
 
17
19
  console.log('Custom ESLint rules loaded:', Object.keys(rules)); // Debug log
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dineroregnskab/eslint-plugin-custom-rules",
3
- "version": "4.7.0",
3
+ "version": "4.9.0",
4
4
  "description": "ESLint plugin with custom rules for Dinero Regnskab",
5
5
  "main": "eslint-plugin-custom-rules.js",
6
6
  "scripts": {
@@ -0,0 +1,183 @@
1
+ module.exports = {
2
+ meta: {
3
+ type: 'problem',
4
+ docs: {
5
+ description:
6
+ 'Disallow subscribing to the NgRx `Actions` stream inside Angular components. Components should be driven by state via selectors (Action -> Reducer -> State -> Selector -> Component), not by listening to action events directly.',
7
+ },
8
+ schema: [],
9
+ messages: {
10
+ noActionsSubscribeInComponent:
11
+ 'Do not subscribe to the NgRx `Actions` stream inside an Angular component. Drive the component from state via a selector instead (Action -> Reducer -> State -> Selector -> Component).',
12
+ },
13
+ },
14
+
15
+ create(context) {
16
+ // Stack matching the class nesting we are currently inside. For a class
17
+ // decorated with `@Component`, the entry is a Set of property/parameter
18
+ // names that hold an NgRx `Actions` instance. For any other class the
19
+ // entry is `null`, so we can ignore it cheaply without losing the
20
+ // nesting depth.
21
+ const componentStack = [];
22
+
23
+ function isComponentDecorator(decorator) {
24
+ const expression = decorator && decorator.expression;
25
+ return (
26
+ expression &&
27
+ expression.type === 'CallExpression' &&
28
+ expression.callee.type === 'Identifier' &&
29
+ expression.callee.name === 'Component'
30
+ );
31
+ }
32
+
33
+ function classHasComponentDecorator(classNode) {
34
+ const decorators = classNode.decorators || [];
35
+ return decorators.some(isComponentDecorator);
36
+ }
37
+
38
+ function isActionsTypeAnnotation(typeAnnotation) {
39
+ if (!typeAnnotation) {
40
+ return false;
41
+ }
42
+ const inner = typeAnnotation.typeAnnotation;
43
+ return (
44
+ inner &&
45
+ inner.type === 'TSTypeReference' &&
46
+ inner.typeName &&
47
+ inner.typeName.type === 'Identifier' &&
48
+ inner.typeName.name === 'Actions'
49
+ );
50
+ }
51
+
52
+ function isInjectActionsCall(node) {
53
+ return (
54
+ node &&
55
+ node.type === 'CallExpression' &&
56
+ node.callee.type === 'Identifier' &&
57
+ node.callee.name === 'inject' &&
58
+ node.arguments.length === 1 &&
59
+ node.arguments[0].type === 'Identifier' &&
60
+ node.arguments[0].name === 'Actions'
61
+ );
62
+ }
63
+
64
+ function getMemberName(member) {
65
+ const key = member && member.key;
66
+ if (!key) {
67
+ return null;
68
+ }
69
+ if (
70
+ key.type === 'Identifier' ||
71
+ key.type === 'PrivateIdentifier'
72
+ ) {
73
+ return key.name;
74
+ }
75
+ return null;
76
+ }
77
+
78
+ function collectActionsFields(classNode, scope) {
79
+ for (const member of classNode.body.body) {
80
+ if (member.type === 'PropertyDefinition') {
81
+ const name = getMemberName(member);
82
+ if (!name) {
83
+ continue;
84
+ }
85
+ if (
86
+ isActionsTypeAnnotation(member.typeAnnotation) ||
87
+ isInjectActionsCall(member.value)
88
+ ) {
89
+ scope.add(name);
90
+ }
91
+ } else if (
92
+ member.type === 'MethodDefinition' &&
93
+ member.kind === 'constructor'
94
+ ) {
95
+ const params = (member.value && member.value.params) || [];
96
+ for (const rawParam of params) {
97
+ const param =
98
+ rawParam.type === 'TSParameterProperty'
99
+ ? rawParam.parameter
100
+ : rawParam;
101
+ if (
102
+ param.type === 'Identifier' &&
103
+ isActionsTypeAnnotation(param.typeAnnotation)
104
+ ) {
105
+ scope.add(param.name);
106
+ }
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ function findThisActionsName(node, scope) {
113
+ // Walk down the left side of a chain like
114
+ // `this.actions$.pipe(...).subscribe(...)` to find a `this.X`
115
+ // where `X` is a known Actions field.
116
+ let current = node;
117
+ while (current) {
118
+ if (
119
+ current.type === 'MemberExpression' &&
120
+ current.object.type === 'ThisExpression' &&
121
+ current.property.type === 'Identifier' &&
122
+ scope.has(current.property.name)
123
+ ) {
124
+ return current.property.name;
125
+ }
126
+ if (current.type === 'MemberExpression') {
127
+ current = current.object;
128
+ } else if (current.type === 'CallExpression') {
129
+ current = current.callee;
130
+ } else {
131
+ return null;
132
+ }
133
+ }
134
+ return null;
135
+ }
136
+
137
+ function enterClass(node) {
138
+ if (classHasComponentDecorator(node)) {
139
+ const scope = new Set();
140
+ collectActionsFields(node, scope);
141
+ componentStack.push(scope);
142
+ } else {
143
+ componentStack.push(null);
144
+ }
145
+ }
146
+
147
+ function exitClass() {
148
+ componentStack.pop();
149
+ }
150
+
151
+ return {
152
+ ClassDeclaration: enterClass,
153
+ 'ClassDeclaration:exit': exitClass,
154
+ ClassExpression: enterClass,
155
+ 'ClassExpression:exit': exitClass,
156
+
157
+ CallExpression(node) {
158
+ if (componentStack.length === 0) {
159
+ return;
160
+ }
161
+ const scope = componentStack[componentStack.length - 1];
162
+ if (!scope || scope.size === 0) {
163
+ return;
164
+ }
165
+
166
+ if (
167
+ node.callee.type !== 'MemberExpression' ||
168
+ node.callee.property.type !== 'Identifier' ||
169
+ node.callee.property.name !== 'subscribe'
170
+ ) {
171
+ return;
172
+ }
173
+
174
+ if (findThisActionsName(node.callee.object, scope)) {
175
+ context.report({
176
+ node,
177
+ messageId: 'noActionsSubscribeInComponent',
178
+ });
179
+ }
180
+ },
181
+ };
182
+ },
183
+ };
@@ -0,0 +1,97 @@
1
+ module.exports = {
2
+ meta: {
3
+ type: 'problem',
4
+ docs: {
5
+ description:
6
+ 'Disallow using window.location.href or window.location.replace for redirects.',
7
+ },
8
+ schema: [],
9
+ messages: {
10
+ useRedirectToSafeUrl:
11
+ 'use redirectToSafeUrl function for safe redirect',
12
+ },
13
+ },
14
+ create(context) {
15
+ function unwrapChain(node) {
16
+ return node && node.type === 'ChainExpression'
17
+ ? node.expression
18
+ : node;
19
+ }
20
+
21
+ function isIdentifier(node, name) {
22
+ return node && node.type === 'Identifier' && node.name === name;
23
+ }
24
+
25
+ function isPropertyName(node, name) {
26
+ if (!node) {
27
+ return false;
28
+ }
29
+
30
+ if (node.type === 'Identifier') {
31
+ return node.name === name;
32
+ }
33
+
34
+ return node.type === 'Literal' && node.value === name;
35
+ }
36
+
37
+ function isLocationObject(node) {
38
+ const unwrapped = unwrapChain(node);
39
+
40
+ if (!unwrapped) {
41
+ return false;
42
+ }
43
+
44
+ if (isIdentifier(unwrapped, 'location')) {
45
+ return true;
46
+ }
47
+
48
+ return (
49
+ unwrapped.type === 'MemberExpression' &&
50
+ !unwrapped.computed &&
51
+ isIdentifier(unwrapped.object, 'window') &&
52
+ isPropertyName(unwrapped.property, 'location')
53
+ );
54
+ }
55
+
56
+ function isWindowLocationHref(node) {
57
+ const unwrapped = unwrapChain(node);
58
+
59
+ return (
60
+ unwrapped &&
61
+ unwrapped.type === 'MemberExpression' &&
62
+ isLocationObject(unwrapped.object) &&
63
+ isPropertyName(unwrapped.property, 'href')
64
+ );
65
+ }
66
+
67
+ function isWindowLocationReplaceCall(node) {
68
+ const callee = unwrapChain(node.callee);
69
+
70
+ return (
71
+ callee &&
72
+ callee.type === 'MemberExpression' &&
73
+ isLocationObject(callee.object) &&
74
+ isPropertyName(callee.property, 'replace')
75
+ );
76
+ }
77
+
78
+ return {
79
+ MemberExpression(node) {
80
+ if (isWindowLocationHref(node)) {
81
+ context.report({
82
+ node,
83
+ messageId: 'useRedirectToSafeUrl',
84
+ });
85
+ }
86
+ },
87
+ CallExpression(node) {
88
+ if (isWindowLocationReplaceCall(node)) {
89
+ context.report({
90
+ node,
91
+ messageId: 'useRedirectToSafeUrl',
92
+ });
93
+ }
94
+ },
95
+ };
96
+ },
97
+ };