@ethlete/eslint-plugin 1.0.0-next.23 → 1.0.0-next.25
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 +26 -0
- package/migrations.json +12 -0
- package/package.json +13 -3
- package/src/configs/recommended.js +33 -11
- package/src/generators/generators.json +10 -0
- package/src/generators/restore-module-augmentation-interfaces/migration.js +50 -0
- package/src/generators/restore-module-augmentation-interfaces/restore-module-augmentation-interfaces.js +103 -0
- package/src/generators/restore-module-augmentation-interfaces/schema.json +14 -0
- package/src/index.js +10 -2
- package/src/rules/angular-decorator-property-order.js +20 -20
- package/src/rules/class-constant-property.js +8 -14
- package/src/rules/class-member-order.js +77 -46
- package/src/rules/consistent-type-definitions.js +60 -0
- package/src/rules/inject-member-accessibility.js +5 -3
- package/src/rules/internals/angular-io.js +12 -13
- package/src/rules/internals/angular-member-visibility.js +11 -7
- package/src/rules/internals/import-resolution.js +191 -0
- package/src/rules/no-angular-router-api.js +23 -29
- package/src/rules/no-angular-seo-services.js +14 -9
- package/src/rules/no-cdk-import.js +7 -14
- package/src/rules/no-csp-unsafe.js +362 -0
- package/src/rules/no-document-cookie.js +3 -0
- package/src/rules/no-effect-cleanup-return.js +7 -3
- package/src/rules/no-empty-angular-metadata-arrays.js +3 -17
- package/src/rules/no-inject-chain.js +3 -1
- package/src/rules/no-leading-underscore-class-member.js +26 -3
- package/src/rules/no-legacy-angular-decorators.js +9 -21
- package/src/rules/no-legacy-query-import.js +6 -15
- package/src/rules/no-locale-id.js +4 -4
- package/src/rules/no-member-alias.js +11 -7
- package/src/rules/no-native-html-input-name.js +1 -1
- package/src/rules/no-pipe-logic.js +10 -17
- package/src/rules/no-redundant-on-push-change-detection.js +9 -23
- package/src/rules/no-rxjs-in-effect.js +12 -8
- package/src/rules/no-screaming-case-local.js +1 -0
- package/src/rules/no-standalone-flag.js +3 -17
- package/src/rules/no-subscribe-in-pipe.js +3 -2
- package/src/rules/no-template-literal-before-inline-template.js +3 -14
- package/src/rules/no-trivial-return-type.js +1 -0
- package/src/rules/no-trivial-wrapper-method.js +10 -16
- package/src/rules/no-typed-injected-element-ref.js +14 -17
- package/src/rules/no-unused-class-member.js +6 -9
- package/src/rules/prefer-clone-equal.js +8 -6
- package/src/rules/prefer-concise-angular-host-directives.js +23 -29
- package/src/rules/prefer-concise-angular-style-metadata.js +8 -16
- package/src/rules/prefer-element-dimensions.js +9 -10
- package/src/rules/prefer-linked-signal.js +42 -41
- package/src/rules/prefer-match-media.js +10 -14
- package/src/rules/prefer-present-tense-output.js +1 -1
- package/src/rules/prefer-rxjs-timer.js +3 -1
- package/src/rules/require-dollar-suffix.js +7 -4
- package/src/rules/require-on-push-change-detection.js +17 -25
- package/src/rules/require-view-encapsulation-none.js +17 -25
- package/src/rules/take-until-destroyed-last.js +64 -0
- package/src/rules/template-member-accessibility.js +8 -6
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const {
|
|
5
|
+
ANGULAR_CORE,
|
|
6
|
+
getAngularDecoratorName,
|
|
7
|
+
getImportLocalName,
|
|
8
|
+
isImportedAs,
|
|
9
|
+
resolveIdentifier,
|
|
10
|
+
} = require('./internals/import-resolution');
|
|
11
|
+
|
|
4
12
|
const COMPONENT_ORDER = ['selector', 'template', 'styleUrl', 'encapsulation', 'changeDetection'];
|
|
5
13
|
|
|
6
14
|
// OnPush became the default change detection strategy in Angular 22, so from
|
|
@@ -187,7 +195,7 @@ const getChangeDetectionInsertionTarget = (metadata) => {
|
|
|
187
195
|
* @param {any} metadata
|
|
188
196
|
*/
|
|
189
197
|
const buildMetadataFix = (sourceCode, metadata) => {
|
|
190
|
-
const changeDetectionText =
|
|
198
|
+
const changeDetectionText = `changeDetection: ${getImportLocalName(sourceCode, ANGULAR_CORE, 'ChangeDetectionStrategy') ?? 'ChangeDetectionStrategy'}.OnPush`;
|
|
191
199
|
const properties = metadata.properties;
|
|
192
200
|
const insertionTarget = getChangeDetectionInsertionTarget(metadata);
|
|
193
201
|
const isMultiline = Boolean(metadata.loc && metadata.loc.start.line !== metadata.loc.end.line);
|
|
@@ -236,6 +244,10 @@ const buildMetadataFix = (sourceCode, metadata) => {
|
|
|
236
244
|
* @param {any} valueNode
|
|
237
245
|
*/
|
|
238
246
|
const buildFix = (sourceCode, metadata, valueNode) => {
|
|
247
|
+
const valueFix = valueNode && ((fixer) => fixer.replaceText(valueNode.property, 'OnPush'));
|
|
248
|
+
if (valueNode && valueNode.object.type !== 'Identifier') return valueFix;
|
|
249
|
+
if (valueNode && resolveIdentifier(sourceCode, valueNode.object)?.source) return valueFix;
|
|
250
|
+
|
|
239
251
|
const fixes = [];
|
|
240
252
|
const angularCoreImport = findAngularCoreImport(sourceCode);
|
|
241
253
|
const importFix = angularCoreImport
|
|
@@ -244,12 +256,8 @@ const buildFix = (sourceCode, metadata, valueNode) => {
|
|
|
244
256
|
|
|
245
257
|
if (importFix) fixes.push(importFix);
|
|
246
258
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
} else {
|
|
250
|
-
const metadataFix = buildMetadataFix(sourceCode, metadata);
|
|
251
|
-
if (metadataFix) fixes.push(metadataFix);
|
|
252
|
-
}
|
|
259
|
+
const metadataFix = valueFix ?? buildMetadataFix(sourceCode, metadata);
|
|
260
|
+
if (metadataFix) fixes.push(metadataFix);
|
|
253
261
|
|
|
254
262
|
return (fixer) => fixes.map((applyFix) => applyFix(fixer));
|
|
255
263
|
};
|
|
@@ -278,24 +286,9 @@ const requireOnPushChangeDetection = {
|
|
|
278
286
|
|
|
279
287
|
const sourceCode = context.sourceCode;
|
|
280
288
|
|
|
281
|
-
/**
|
|
282
|
-
* @param {import('eslint').Rule.Node} node
|
|
283
|
-
*/
|
|
284
|
-
const isComponentDecorator = (node) => {
|
|
285
|
-
const decorator = /** @type {any} */ (node);
|
|
286
|
-
if (decorator.type !== 'Decorator') return false;
|
|
287
|
-
|
|
288
|
-
const expression = decorator.expression;
|
|
289
|
-
if (expression.type === 'CallExpression') {
|
|
290
|
-
return expression.callee.type === 'Identifier' && expression.callee.name === 'Component';
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
return expression.type === 'Identifier' && expression.name === 'Component';
|
|
294
|
-
};
|
|
295
|
-
|
|
296
289
|
return {
|
|
297
290
|
Decorator(node) {
|
|
298
|
-
if (
|
|
291
|
+
if (getAngularDecoratorName(sourceCode, node) !== 'Component') return;
|
|
299
292
|
|
|
300
293
|
const decorator = /** @type {any} */ (node);
|
|
301
294
|
const expression = decorator.expression;
|
|
@@ -320,8 +313,7 @@ const requireOnPushChangeDetection = {
|
|
|
320
313
|
const value = changeDetectionProp.value;
|
|
321
314
|
if (
|
|
322
315
|
value.type === 'MemberExpression' &&
|
|
323
|
-
value.object
|
|
324
|
-
value.object.name === 'ChangeDetectionStrategy' &&
|
|
316
|
+
isImportedAs(sourceCode, value.object, 'ChangeDetectionStrategy') &&
|
|
325
317
|
value.property.type === 'Identifier' &&
|
|
326
318
|
value.property.name !== 'OnPush'
|
|
327
319
|
) {
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const {
|
|
5
|
+
ANGULAR_CORE,
|
|
6
|
+
getAngularDecoratorName,
|
|
7
|
+
getImportLocalName,
|
|
8
|
+
isImportedAs,
|
|
9
|
+
resolveIdentifier,
|
|
10
|
+
} = require('./internals/import-resolution');
|
|
11
|
+
|
|
4
12
|
const COMPONENT_ORDER = ['selector', 'template', 'styleUrl', 'encapsulation', 'changeDetection'];
|
|
5
13
|
|
|
6
14
|
/**
|
|
@@ -141,7 +149,7 @@ const getEncapsulationInsertionTarget = (metadata) => {
|
|
|
141
149
|
* @param {any} metadata
|
|
142
150
|
*/
|
|
143
151
|
const buildMetadataFix = (sourceCode, metadata) => {
|
|
144
|
-
const encText =
|
|
152
|
+
const encText = `encapsulation: ${getImportLocalName(sourceCode, ANGULAR_CORE, 'ViewEncapsulation') ?? 'ViewEncapsulation'}.None`;
|
|
145
153
|
const properties = metadata.properties;
|
|
146
154
|
const insertionTarget = getEncapsulationInsertionTarget(metadata);
|
|
147
155
|
const isMultiline = Boolean(metadata.loc && metadata.loc.start.line !== metadata.loc.end.line);
|
|
@@ -189,6 +197,10 @@ const buildMetadataFix = (sourceCode, metadata) => {
|
|
|
189
197
|
* @param {any} valueNode
|
|
190
198
|
*/
|
|
191
199
|
const buildFix = (sourceCode, metadata, valueNode) => {
|
|
200
|
+
const valueFix = valueNode && ((fixer) => fixer.replaceText(valueNode.property, 'None'));
|
|
201
|
+
if (valueNode && valueNode.object.type !== 'Identifier') return valueFix;
|
|
202
|
+
if (valueNode && resolveIdentifier(sourceCode, valueNode.object)?.source) return valueFix;
|
|
203
|
+
|
|
192
204
|
const fixes = [];
|
|
193
205
|
const angularCoreImport = findAngularCoreImport(sourceCode);
|
|
194
206
|
const importFix = angularCoreImport
|
|
@@ -197,12 +209,8 @@ const buildFix = (sourceCode, metadata, valueNode) => {
|
|
|
197
209
|
|
|
198
210
|
if (importFix) fixes.push(importFix);
|
|
199
211
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
} else {
|
|
203
|
-
const metadataFix = buildMetadataFix(sourceCode, metadata);
|
|
204
|
-
if (metadataFix) fixes.push(metadataFix);
|
|
205
|
-
}
|
|
212
|
+
const metadataFix = valueFix ?? buildMetadataFix(sourceCode, metadata);
|
|
213
|
+
if (metadataFix) fixes.push(metadataFix);
|
|
206
214
|
|
|
207
215
|
return (fixer) => fixes.map((applyFix) => applyFix(fixer));
|
|
208
216
|
};
|
|
@@ -224,24 +232,9 @@ const requireViewEncapsulationNone = {
|
|
|
224
232
|
create(context) {
|
|
225
233
|
const sourceCode = context.sourceCode;
|
|
226
234
|
|
|
227
|
-
/**
|
|
228
|
-
* @param {import('eslint').Rule.Node} node
|
|
229
|
-
*/
|
|
230
|
-
const isComponentDecorator = (node) => {
|
|
231
|
-
const decorator = /** @type {any} */ (node);
|
|
232
|
-
if (decorator.type !== 'Decorator') return false;
|
|
233
|
-
|
|
234
|
-
const expression = decorator.expression;
|
|
235
|
-
if (expression.type === 'CallExpression') {
|
|
236
|
-
return expression.callee.type === 'Identifier' && expression.callee.name === 'Component';
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
return expression.type === 'Identifier' && expression.name === 'Component';
|
|
240
|
-
};
|
|
241
|
-
|
|
242
235
|
return {
|
|
243
236
|
Decorator(node) {
|
|
244
|
-
if (
|
|
237
|
+
if (getAngularDecoratorName(sourceCode, node) !== 'Component') return;
|
|
245
238
|
|
|
246
239
|
const decorator = /** @type {any} */ (node);
|
|
247
240
|
const expression = decorator.expression;
|
|
@@ -266,8 +259,7 @@ const requireViewEncapsulationNone = {
|
|
|
266
259
|
const value = encProp.value;
|
|
267
260
|
if (
|
|
268
261
|
value.type === 'MemberExpression' &&
|
|
269
|
-
value.object
|
|
270
|
-
value.object.name === 'ViewEncapsulation' &&
|
|
262
|
+
isImportedAs(sourceCode, value.object, 'ViewEncapsulation') &&
|
|
271
263
|
value.property.type === 'Identifier' &&
|
|
272
264
|
value.property.name !== 'None'
|
|
273
265
|
) {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { isImportedAs } = require('./internals/import-resolution');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Requires takeUntilDestroyed() to be the last operator in a .pipe().
|
|
8
|
+
*
|
|
9
|
+
* BAD:
|
|
10
|
+
* obs$.pipe(
|
|
11
|
+
* takeUntilDestroyed(),
|
|
12
|
+
* switchMap(() => fromEvent(el, 'scroll')), // ❌ the inner subscription outlives the component
|
|
13
|
+
* ).subscribe();
|
|
14
|
+
*
|
|
15
|
+
* GOOD:
|
|
16
|
+
* obs$.pipe(
|
|
17
|
+
* switchMap(() => fromEvent(el, 'scroll')),
|
|
18
|
+
* takeUntilDestroyed(), // ✅ tears down everything above it
|
|
19
|
+
* ).subscribe();
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
23
|
+
const takeUntilDestroyedLast = {
|
|
24
|
+
meta: {
|
|
25
|
+
type: 'problem',
|
|
26
|
+
docs: {
|
|
27
|
+
description: 'Require takeUntilDestroyed() to be the last operator in a .pipe().',
|
|
28
|
+
recommended: true,
|
|
29
|
+
},
|
|
30
|
+
messages: {
|
|
31
|
+
takeUntilDestroyedLast:
|
|
32
|
+
'takeUntilDestroyed() must be the last operator in the pipe. An operator after it (switchMap, shareReplay, …) can keep the subscription alive after destroy.',
|
|
33
|
+
},
|
|
34
|
+
schema: [],
|
|
35
|
+
},
|
|
36
|
+
create(context) {
|
|
37
|
+
/** @param {import('estree').Node} node */
|
|
38
|
+
const isTakeUntilDestroyedCall = (node) =>
|
|
39
|
+
node.type === 'CallExpression' &&
|
|
40
|
+
isImportedAs(context.sourceCode, node.callee, 'takeUntilDestroyed', '@angular/core/rxjs-interop');
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
CallExpression(node) {
|
|
44
|
+
const { callee } = node;
|
|
45
|
+
if (
|
|
46
|
+
callee.type !== 'MemberExpression' ||
|
|
47
|
+
callee.property.type !== 'Identifier' ||
|
|
48
|
+
callee.property.name !== 'pipe'
|
|
49
|
+
) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const lastIndex = node.arguments.length - 1;
|
|
54
|
+
node.arguments.forEach((arg, index) => {
|
|
55
|
+
if (index < lastIndex && isTakeUntilDestroyedCall(arg)) {
|
|
56
|
+
context.report({ node: arg, messageId: 'takeUntilDestroyedLast' });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
module.exports = takeUntilDestroyedLast;
|
|
@@ -7,15 +7,17 @@ const {
|
|
|
7
7
|
isReferencedFromTemplateOrHost,
|
|
8
8
|
} = require('./internals/angular-member-visibility');
|
|
9
9
|
const { getImplementedContractMemberNames } = require('./internals/implemented-contract-members');
|
|
10
|
+
const { isImportedAs } = require('./internals/import-resolution');
|
|
10
11
|
const { buildAccessibilityFix } = require('./internals/member-accessibility-fix');
|
|
11
12
|
|
|
12
13
|
/**
|
|
14
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
13
15
|
* @param {any} node
|
|
14
16
|
*/
|
|
15
|
-
const isInjectCall = (node) => {
|
|
17
|
+
const isInjectCall = (sourceCode, node) => {
|
|
16
18
|
if (!node || node.type !== 'PropertyDefinition' || !node.value) return false;
|
|
17
19
|
if (node.value.type !== 'CallExpression') return false;
|
|
18
|
-
return
|
|
20
|
+
return isImportedAs(sourceCode, node.value.callee, 'inject');
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
/**
|
|
@@ -91,11 +93,11 @@ const templateMemberAccessibility = {
|
|
|
91
93
|
*/
|
|
92
94
|
const checkMember = (node) => {
|
|
93
95
|
if (!isSupportedMember(node)) return;
|
|
94
|
-
if (node.type === 'PropertyDefinition' && isInjectCall(node)) return;
|
|
96
|
+
if (node.type === 'PropertyDefinition' && isInjectCall(context.sourceCode, node)) return;
|
|
95
97
|
|
|
96
98
|
const classNode = node.parent && node.parent.type === 'ClassBody' ? node.parent.parent : null;
|
|
97
99
|
if (!classNode || (classNode.type !== 'ClassDeclaration' && classNode.type !== 'ClassExpression')) return;
|
|
98
|
-
if (!getAngularMetadata(classNode)) return;
|
|
100
|
+
if (!getAngularMetadata(context.sourceCode, classNode)) return;
|
|
99
101
|
|
|
100
102
|
const memberName = getMemberName(node);
|
|
101
103
|
if (!memberName) return;
|
|
@@ -148,11 +150,11 @@ const templateMemberAccessibility = {
|
|
|
148
150
|
*/
|
|
149
151
|
const checkImplicitPublicMember = (node) => {
|
|
150
152
|
if (!isSupportedMember(node)) return;
|
|
151
|
-
if (node.type === 'PropertyDefinition' && isInjectCall(node)) return;
|
|
153
|
+
if (node.type === 'PropertyDefinition' && isInjectCall(context.sourceCode, node)) return;
|
|
152
154
|
|
|
153
155
|
const classNode = node.parent && node.parent.type === 'ClassBody' ? node.parent.parent : null;
|
|
154
156
|
if (!classNode || (classNode.type !== 'ClassDeclaration' && classNode.type !== 'ClassExpression')) return;
|
|
155
|
-
if (!getAngularMetadata(classNode)) return;
|
|
157
|
+
if (!getAngularMetadata(context.sourceCode, classNode)) return;
|
|
156
158
|
|
|
157
159
|
const memberName = getMemberName(node);
|
|
158
160
|
if (!memberName) return;
|