@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
|
@@ -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
|
+
};
|