@ethlete/eslint-plugin 1.0.0-next.25 → 1.0.0-next.26
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/CHANGELOG.md +18 -0
- package/package.json +2 -1
- package/src/configs/recommended.js +7 -16
- package/src/rules/angular-decorator-property-order.js +24 -7
- package/src/rules/class-constant-property.js +1 -1
- package/src/rules/class-member-order.js +60 -8
- package/src/rules/enforce-routing-view-naming.js +34 -23
- package/src/rules/guard-return-newline.js +4 -7
- package/src/rules/internals/angular-member-visibility.js +24 -2
- package/src/rules/internals/angular-metadata-fix.js +231 -1
- package/src/rules/internals/angular-version.js +50 -0
- package/src/rules/internals/implemented-contract-members.js +26 -11
- package/src/rules/no-angular-router-api.js +86 -33
- package/src/rules/no-angular-seo-services.js +1 -0
- package/src/rules/no-async-await.js +5 -1
- package/src/rules/no-cdk-import.js +32 -7
- package/src/rules/no-direct-dom-manipulation.js +50 -28
- package/src/rules/no-document-cookie.js +51 -8
- package/src/rules/no-dom-query.js +5 -2
- package/src/rules/no-empty-angular-metadata-arrays.js +2 -0
- package/src/rules/no-empty-newlines-between-imports.js +4 -1
- package/src/rules/no-impure-top-level-provider.js +42 -7
- package/src/rules/no-leading-underscore-class-member.js +27 -2
- package/src/rules/no-legacy-angular-decorators.js +11 -6
- package/src/rules/no-legacy-prepare-without-injector.js +57 -12
- package/src/rules/no-legacy-query-import.js +61 -16
- package/src/rules/no-member-alias.js +0 -5
- package/src/rules/no-native-observers.js +89 -24
- package/src/rules/no-pipe-logic.js +1 -2
- package/src/rules/no-readonly-signal.js +1 -5
- package/src/rules/no-redundant-internal.js +5 -2
- package/src/rules/no-redundant-on-push-change-detection.js +3 -48
- package/src/rules/no-rxjs-in-effect.js +7 -5
- package/src/rules/no-screaming-case-local.js +3 -5
- package/src/rules/no-standalone-flag.js +1 -0
- package/src/rules/no-subscribe-with-body.js +30 -1
- package/src/rules/no-template-literal-before-inline-template.js +6 -0
- package/src/rules/no-trivial-return-type.js +18 -1
- package/src/rules/no-unused-class-member.js +1 -1
- package/src/rules/no-window-location.js +60 -22
- package/src/rules/prefer-clone-equal.js +61 -8
- package/src/rules/prefer-concise-angular-host-directives.js +39 -11
- package/src/rules/prefer-concise-angular-style-metadata.js +10 -2
- package/src/rules/prefer-element-dimensions.js +264 -11
- package/src/rules/prefer-match-media.js +17 -3
- package/src/rules/prefer-rxjs-timer.js +26 -4
- package/src/rules/prefer-scroll-state.js +0 -4
- package/src/rules/prefer-static-boolean-properties.js +1 -0
- package/src/rules/prefer-viewport-size.js +153 -16
- package/src/rules/require-dollar-suffix.js +11 -3
- package/src/rules/require-form-submit.js +4 -1
- package/src/rules/require-on-push-change-detection.js +7 -259
- package/src/rules/require-view-encapsulation-none.js +6 -214
- package/src/rules/template-member-accessibility.js +12 -4
|
@@ -37,7 +37,12 @@
|
|
|
37
37
|
* // … etc. All from @ethlete/core.
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
|
-
const {
|
|
40
|
+
const {
|
|
41
|
+
MODULE_REFERENCE_SELECTOR,
|
|
42
|
+
getModuleReference,
|
|
43
|
+
isGlobalReference,
|
|
44
|
+
isImportedAs,
|
|
45
|
+
} = require('./internals/import-resolution');
|
|
41
46
|
|
|
42
47
|
const CDK_LAYOUT = '@angular/cdk/layout';
|
|
43
48
|
|
|
@@ -62,7 +67,15 @@ const preferMatchMedia = {
|
|
|
62
67
|
CallExpression(node) {
|
|
63
68
|
const { callee } = node;
|
|
64
69
|
|
|
65
|
-
|
|
70
|
+
if (
|
|
71
|
+
callee.type === 'Identifier' &&
|
|
72
|
+
callee.name === 'matchMedia' &&
|
|
73
|
+
isGlobalReference(context.sourceCode, callee)
|
|
74
|
+
) {
|
|
75
|
+
context.report({ node, messageId: 'preferMatchMedia' });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
66
79
|
if (
|
|
67
80
|
callee.type === 'MemberExpression' &&
|
|
68
81
|
callee.property.type === 'Identifier' &&
|
|
@@ -74,9 +87,10 @@ const preferMatchMedia = {
|
|
|
74
87
|
return;
|
|
75
88
|
}
|
|
76
89
|
|
|
77
|
-
// ── inject(BreakpointObserver) ────────────────────────────────────────
|
|
78
90
|
if (
|
|
79
91
|
isImportedAs(context.sourceCode, callee, 'inject') &&
|
|
92
|
+
node.arguments[0]?.type === 'Identifier' &&
|
|
93
|
+
isGlobalReference(context.sourceCode, node.arguments[0]) &&
|
|
80
94
|
isImportedAs(context.sourceCode, node.arguments[0], 'BreakpointObserver', CDK_LAYOUT)
|
|
81
95
|
) {
|
|
82
96
|
context.report({ node, messageId: 'noBreakpointObserver' });
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
|
|
22
22
|
const { isGlobalReference } = require('./internals/import-resolution');
|
|
23
23
|
|
|
24
|
+
const GLOBAL_OBJECTS = new Set(['window', 'globalThis', 'self']);
|
|
25
|
+
const WINDOW_MEMBERS = new Set(['win', 'window', '_win', '_window']);
|
|
26
|
+
|
|
24
27
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
25
28
|
const preferRxjsTimer = {
|
|
26
29
|
meta: {
|
|
@@ -44,13 +47,33 @@ const preferRxjsTimer = {
|
|
|
44
47
|
schema: [],
|
|
45
48
|
},
|
|
46
49
|
create(context) {
|
|
50
|
+
const getTimerName = (callee) => {
|
|
51
|
+
if (callee.type === 'Identifier') {
|
|
52
|
+
return isGlobalReference(context.sourceCode, callee) ? callee.name : null;
|
|
53
|
+
}
|
|
54
|
+
if (callee.type !== 'MemberExpression' || callee.computed || callee.property.type !== 'Identifier') return null;
|
|
55
|
+
|
|
56
|
+
const { object } = callee;
|
|
57
|
+
const isWindow =
|
|
58
|
+
(object.type === 'Identifier' &&
|
|
59
|
+
GLOBAL_OBJECTS.has(object.name) &&
|
|
60
|
+
isGlobalReference(context.sourceCode, object)) ||
|
|
61
|
+
(object.type === 'MemberExpression' &&
|
|
62
|
+
object.object.type === 'ThisExpression' &&
|
|
63
|
+
!object.computed &&
|
|
64
|
+
object.property.type === 'Identifier' &&
|
|
65
|
+
WINDOW_MEMBERS.has(object.property.name));
|
|
66
|
+
|
|
67
|
+
return isWindow ? callee.property.name : null;
|
|
68
|
+
};
|
|
69
|
+
|
|
47
70
|
return {
|
|
48
71
|
CallExpression(node) {
|
|
49
72
|
const { callee } = node;
|
|
50
73
|
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
const
|
|
74
|
+
const timerName = getTimerName(callee);
|
|
75
|
+
if (timerName) {
|
|
76
|
+
const name = timerName;
|
|
54
77
|
if (name === 'setTimeout') {
|
|
55
78
|
context.report({ node, messageId: 'preferTimer' });
|
|
56
79
|
return;
|
|
@@ -65,7 +88,6 @@ const preferRxjsTimer = {
|
|
|
65
88
|
}
|
|
66
89
|
}
|
|
67
90
|
|
|
68
|
-
// ── Member calls: .addEventListener / .removeEventListener ────────────
|
|
69
91
|
if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier') {
|
|
70
92
|
const methodName = callee.property.name;
|
|
71
93
|
if (methodName === 'addEventListener') {
|
|
@@ -41,7 +41,6 @@ const preferScrollState = {
|
|
|
41
41
|
const { callee } = node;
|
|
42
42
|
const args = node.arguments;
|
|
43
43
|
|
|
44
|
-
// ── el.addEventListener('scroll', fn) / window.addEventListener('scroll', fn) ──
|
|
45
44
|
if (
|
|
46
45
|
callee.type === 'MemberExpression' &&
|
|
47
46
|
callee.property.type === 'Identifier' &&
|
|
@@ -53,7 +52,6 @@ const preferScrollState = {
|
|
|
53
52
|
return;
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
// ── fromEvent(el, 'scroll') ──────────────────────────────────────────
|
|
57
55
|
if (
|
|
58
56
|
callee.type === 'Identifier' &&
|
|
59
57
|
callee.name === 'fromEvent' &&
|
|
@@ -64,7 +62,6 @@ const preferScrollState = {
|
|
|
64
62
|
return;
|
|
65
63
|
}
|
|
66
64
|
|
|
67
|
-
// ── renderer.listen(el, 'scroll', fn) ───────────────────────────────
|
|
68
65
|
if (
|
|
69
66
|
callee.type === 'MemberExpression' &&
|
|
70
67
|
callee.property.type === 'Identifier' &&
|
|
@@ -77,7 +74,6 @@ const preferScrollState = {
|
|
|
77
74
|
},
|
|
78
75
|
|
|
79
76
|
AssignmentExpression(node) {
|
|
80
|
-
// ── el.onscroll = fn ────────────────────────────────────────────────
|
|
81
77
|
const { left } = node;
|
|
82
78
|
if (
|
|
83
79
|
left.type === 'MemberExpression' &&
|
|
@@ -57,6 +57,7 @@ const preferStaticBooleanProperties = {
|
|
|
57
57
|
docs: {
|
|
58
58
|
description:
|
|
59
59
|
'Prefer a static attribute over a property binding for static boolean values when the input uses a booleanAttribute transform.',
|
|
60
|
+
recommended: true,
|
|
60
61
|
},
|
|
61
62
|
hasSuggestions: true,
|
|
62
63
|
schema: [],
|
|
@@ -20,8 +20,37 @@
|
|
|
20
20
|
* const h = viewport().height;
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
const { isGlobalReference } = require('./internals/import-resolution');
|
|
24
|
+
|
|
24
25
|
const VIEWPORT_PROPERTIES = new Set(['innerWidth', 'innerHeight']);
|
|
26
|
+
const GLOBAL_OBJECTS = new Set(['window', 'globalThis', 'self']);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {any} node
|
|
30
|
+
* @returns {string | null}
|
|
31
|
+
*/
|
|
32
|
+
const getPropertyName = (node) => {
|
|
33
|
+
if (!node.computed && node.property.type === 'Identifier') return node.property.name;
|
|
34
|
+
if (node.computed && node.property.type === 'Literal' && typeof node.property.value === 'string') {
|
|
35
|
+
return node.property.value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {any} node
|
|
43
|
+
*/
|
|
44
|
+
const getThisClassBody = (node) => {
|
|
45
|
+
let current = node.parent;
|
|
46
|
+
|
|
47
|
+
while (current) {
|
|
48
|
+
if (current.type === 'ClassBody') return current;
|
|
49
|
+
current = current.parent;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return null;
|
|
53
|
+
};
|
|
25
54
|
|
|
26
55
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
27
56
|
const preferViewportSize = {
|
|
@@ -38,30 +67,138 @@ const preferViewportSize = {
|
|
|
38
67
|
schema: [],
|
|
39
68
|
},
|
|
40
69
|
create(context) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
70
|
+
const { sourceCode } = context;
|
|
71
|
+
/** @type {WeakMap<any, Set<string>>} */
|
|
72
|
+
const classWindowMembers = new WeakMap();
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {any} classBody
|
|
76
|
+
* @returns {Set<string>}
|
|
77
|
+
*/
|
|
78
|
+
const getClassWindowMembers = (classBody) => {
|
|
79
|
+
const cached = classWindowMembers.get(classBody);
|
|
80
|
+
if (cached) return cached;
|
|
81
|
+
|
|
82
|
+
/** @type {Set<string>} */
|
|
83
|
+
const members = new Set();
|
|
84
|
+
classWindowMembers.set(classBody, members);
|
|
85
|
+
|
|
86
|
+
/** @param {any} node */
|
|
87
|
+
const visit = (node) => {
|
|
88
|
+
if (!node || typeof node.type !== 'string') return;
|
|
89
|
+
if (node !== classBody && node.type === 'ClassBody') return;
|
|
45
90
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
91
|
+
if (
|
|
92
|
+
node.type === 'PropertyDefinition' &&
|
|
93
|
+
!node.computed &&
|
|
94
|
+
node.key.type === 'Identifier' &&
|
|
95
|
+
isWindowLike(node.value)
|
|
96
|
+
) {
|
|
97
|
+
members.add(node.key.name);
|
|
98
|
+
}
|
|
49
99
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
100
|
+
if (
|
|
101
|
+
node.type === 'AssignmentExpression' &&
|
|
102
|
+
node.left.type === 'MemberExpression' &&
|
|
103
|
+
node.left.object.type === 'ThisExpression' &&
|
|
104
|
+
!node.left.computed &&
|
|
105
|
+
node.left.property.type === 'Identifier' &&
|
|
106
|
+
isWindowLike(node.right)
|
|
107
|
+
) {
|
|
108
|
+
members.add(node.left.property.name);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const key of Object.keys(node)) {
|
|
112
|
+
if (key === 'parent') continue;
|
|
113
|
+
|
|
114
|
+
const value = node[key];
|
|
115
|
+
if (Array.isArray(value)) value.forEach(visit);
|
|
116
|
+
else if (value && typeof value.type === 'string') visit(value);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
visit(classBody);
|
|
121
|
+
|
|
122
|
+
return members;
|
|
123
|
+
};
|
|
56
124
|
|
|
57
|
-
|
|
125
|
+
/**
|
|
126
|
+
* @param {any} node
|
|
127
|
+
* @param {Set<any>} seen
|
|
128
|
+
* @returns {boolean}
|
|
129
|
+
*/
|
|
130
|
+
const isWindowLike = (node, seen = new Set()) => {
|
|
131
|
+
if (!node || seen.has(node)) return false;
|
|
132
|
+
seen.add(node);
|
|
133
|
+
|
|
134
|
+
if (node.type === 'ChainExpression' || node.type === 'TSNonNullExpression' || node.type === 'TSAsExpression') {
|
|
135
|
+
return isWindowLike(node.expression, seen);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (node.type === 'Identifier') {
|
|
139
|
+
if (isGlobalReference(sourceCode, node)) return GLOBAL_OBJECTS.has(node.name);
|
|
140
|
+
|
|
141
|
+
/** @type {import('eslint').Scope.Scope | null} */
|
|
142
|
+
let scope = sourceCode.getScope(node);
|
|
143
|
+
|
|
144
|
+
while (scope) {
|
|
145
|
+
const variable = scope.set.get(node.name);
|
|
146
|
+
|
|
147
|
+
if (variable) {
|
|
148
|
+
const definition = /** @type {any} */ (variable.defs[0]);
|
|
149
|
+
|
|
150
|
+
return definition?.type === 'Variable' && isWindowLike(definition.node.init, seen);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
scope = scope.upper;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (node.type !== 'MemberExpression') return false;
|
|
160
|
+
|
|
161
|
+
const name = getPropertyName(node);
|
|
162
|
+
if (name === 'defaultView') return true;
|
|
163
|
+
if (name && GLOBAL_OBJECTS.has(name) && isWindowLike(node.object, seen)) return true;
|
|
164
|
+
if (node.object.type !== 'ThisExpression' || !name) return false;
|
|
165
|
+
|
|
166
|
+
const classBody = getThisClassBody(node);
|
|
167
|
+
|
|
168
|
+
return !!classBody && getClassWindowMembers(classBody).has(name);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
MemberExpression(node) {
|
|
173
|
+
const prop = getPropertyName(node);
|
|
174
|
+
if (!prop || !VIEWPORT_PROPERTIES.has(prop)) return;
|
|
175
|
+
if (!isWindowLike(node.object)) return;
|
|
58
176
|
|
|
59
177
|
context.report({
|
|
60
178
|
node,
|
|
61
179
|
messageId: 'preferViewportSize',
|
|
62
|
-
data: { prop
|
|
180
|
+
data: { prop },
|
|
63
181
|
});
|
|
64
182
|
},
|
|
183
|
+
'Program:exit'(program) {
|
|
184
|
+
const globalScope = sourceCode.getScope(program);
|
|
185
|
+
const declaredGlobalReferences = [...VIEWPORT_PROPERTIES].flatMap((name) => {
|
|
186
|
+
const variable = globalScope.set.get(name);
|
|
187
|
+
|
|
188
|
+
return variable && variable.defs.length === 0 ? variable.references : [];
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
for (const reference of [...globalScope.through, ...declaredGlobalReferences]) {
|
|
192
|
+
const { identifier } = reference;
|
|
193
|
+
if (!VIEWPORT_PROPERTIES.has(identifier.name)) continue;
|
|
194
|
+
|
|
195
|
+
context.report({
|
|
196
|
+
node: identifier,
|
|
197
|
+
messageId: 'preferViewportSize',
|
|
198
|
+
data: { prop: identifier.name },
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
},
|
|
65
202
|
};
|
|
66
203
|
},
|
|
67
204
|
};
|
|
@@ -12,7 +12,8 @@ const { getImportedName } = require('./internals/import-resolution');
|
|
|
12
12
|
* - `new Subject / BehaviorSubject / ReplaySubject / AsyncSubject / Observable()`
|
|
13
13
|
* - a call to a known RxJS creator: from, of, interval, timer, fromEvent,
|
|
14
14
|
* fromEventPattern, combineLatest, merge, concat, forkJoin, race, zip,
|
|
15
|
-
* defer, iif, throwError, using
|
|
15
|
+
* defer, iif, throwError, using
|
|
16
|
+
* - `toObservable(...)` or `.asObservable()`
|
|
16
17
|
*
|
|
17
18
|
* ❌ const myData = someObs.pipe(map(...));
|
|
18
19
|
* ❌ const clicks = fromEvent(el, 'click');
|
|
@@ -40,7 +41,6 @@ const RXJS_CREATORS = new Set([
|
|
|
40
41
|
'iif',
|
|
41
42
|
'throwError',
|
|
42
43
|
'using',
|
|
43
|
-
'partition',
|
|
44
44
|
]);
|
|
45
45
|
|
|
46
46
|
const RXJS_SUBJECT_CTORS = new Set(['Subject', 'BehaviorSubject', 'ReplaySubject', 'AsyncSubject', 'Observable']);
|
|
@@ -58,7 +58,7 @@ const isObservableInit = (sourceCode, node) => {
|
|
|
58
58
|
node.type === 'CallExpression' &&
|
|
59
59
|
node.callee.type === 'MemberExpression' &&
|
|
60
60
|
node.callee.property.type === 'Identifier' &&
|
|
61
|
-
node.callee.property.name === 'pipe'
|
|
61
|
+
(node.callee.property.name === 'pipe' || node.callee.property.name === 'asObservable')
|
|
62
62
|
) {
|
|
63
63
|
return true;
|
|
64
64
|
}
|
|
@@ -68,6 +68,13 @@ const isObservableInit = (sourceCode, node) => {
|
|
|
68
68
|
return true;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
if (
|
|
72
|
+
node.type === 'CallExpression' &&
|
|
73
|
+
getImportedName(sourceCode, node.callee, '@angular/core/rxjs-interop') === 'toObservable'
|
|
74
|
+
) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
71
78
|
// new Subject(), new BehaviorSubject(...), etc.
|
|
72
79
|
if (node.type === 'NewExpression' && RXJS_SUBJECT_CTORS.has(getImportedName(sourceCode, node.callee, 'rxjs') ?? '')) {
|
|
73
80
|
return true;
|
|
@@ -82,6 +89,7 @@ const rule = {
|
|
|
82
89
|
type: 'suggestion',
|
|
83
90
|
docs: {
|
|
84
91
|
description: 'Require observable variables and class properties to use the $ suffix.',
|
|
92
|
+
recommended: true,
|
|
85
93
|
},
|
|
86
94
|
schema: [],
|
|
87
95
|
messages: {
|
|
@@ -28,7 +28,9 @@ const handlesSubmit = (node) =>
|
|
|
28
28
|
|
|
29
29
|
/** @param {any} node */
|
|
30
30
|
const submitsNatively = (node) =>
|
|
31
|
-
hasBinding(node, 'action') ||
|
|
31
|
+
hasBinding(node, 'action') ||
|
|
32
|
+
hasBinding(node, 'ngNoForm') ||
|
|
33
|
+
attribute(node, 'method')?.value?.trim().toLowerCase() === 'dialog';
|
|
32
34
|
|
|
33
35
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
34
36
|
const requireFormSubmit = {
|
|
@@ -36,6 +38,7 @@ const requireFormSubmit = {
|
|
|
36
38
|
type: 'problem',
|
|
37
39
|
docs: {
|
|
38
40
|
description: 'Require a `<form>` to handle its own submission.',
|
|
41
|
+
recommended: true,
|
|
39
42
|
},
|
|
40
43
|
schema: [],
|
|
41
44
|
messages: {
|