@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/migrations.json +12 -0
  3. package/package.json +13 -3
  4. package/src/configs/recommended.js +33 -11
  5. package/src/generators/generators.json +10 -0
  6. package/src/generators/restore-module-augmentation-interfaces/migration.js +50 -0
  7. package/src/generators/restore-module-augmentation-interfaces/restore-module-augmentation-interfaces.js +103 -0
  8. package/src/generators/restore-module-augmentation-interfaces/schema.json +14 -0
  9. package/src/index.js +10 -2
  10. package/src/rules/angular-decorator-property-order.js +20 -20
  11. package/src/rules/class-constant-property.js +8 -14
  12. package/src/rules/class-member-order.js +77 -46
  13. package/src/rules/consistent-type-definitions.js +60 -0
  14. package/src/rules/inject-member-accessibility.js +5 -3
  15. package/src/rules/internals/angular-io.js +12 -13
  16. package/src/rules/internals/angular-member-visibility.js +11 -7
  17. package/src/rules/internals/import-resolution.js +191 -0
  18. package/src/rules/no-angular-router-api.js +23 -29
  19. package/src/rules/no-angular-seo-services.js +14 -9
  20. package/src/rules/no-cdk-import.js +7 -14
  21. package/src/rules/no-csp-unsafe.js +362 -0
  22. package/src/rules/no-document-cookie.js +3 -0
  23. package/src/rules/no-effect-cleanup-return.js +7 -3
  24. package/src/rules/no-empty-angular-metadata-arrays.js +3 -17
  25. package/src/rules/no-inject-chain.js +3 -1
  26. package/src/rules/no-leading-underscore-class-member.js +26 -3
  27. package/src/rules/no-legacy-angular-decorators.js +9 -21
  28. package/src/rules/no-legacy-query-import.js +6 -15
  29. package/src/rules/no-locale-id.js +4 -4
  30. package/src/rules/no-member-alias.js +11 -7
  31. package/src/rules/no-native-html-input-name.js +1 -1
  32. package/src/rules/no-pipe-logic.js +10 -17
  33. package/src/rules/no-redundant-on-push-change-detection.js +9 -23
  34. package/src/rules/no-rxjs-in-effect.js +12 -8
  35. package/src/rules/no-screaming-case-local.js +1 -0
  36. package/src/rules/no-standalone-flag.js +3 -17
  37. package/src/rules/no-subscribe-in-pipe.js +3 -2
  38. package/src/rules/no-template-literal-before-inline-template.js +3 -14
  39. package/src/rules/no-trivial-return-type.js +1 -0
  40. package/src/rules/no-trivial-wrapper-method.js +10 -16
  41. package/src/rules/no-typed-injected-element-ref.js +14 -17
  42. package/src/rules/no-unused-class-member.js +6 -9
  43. package/src/rules/prefer-clone-equal.js +8 -6
  44. package/src/rules/prefer-concise-angular-host-directives.js +23 -29
  45. package/src/rules/prefer-concise-angular-style-metadata.js +8 -16
  46. package/src/rules/prefer-element-dimensions.js +9 -10
  47. package/src/rules/prefer-linked-signal.js +42 -41
  48. package/src/rules/prefer-match-media.js +10 -14
  49. package/src/rules/prefer-present-tense-output.js +1 -1
  50. package/src/rules/prefer-rxjs-timer.js +3 -1
  51. package/src/rules/require-dollar-suffix.js +7 -4
  52. package/src/rules/require-on-push-change-detection.js +17 -25
  53. package/src/rules/require-view-encapsulation-none.js +17 -25
  54. package/src/rules/take-until-destroyed-last.js +64 -0
  55. 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 = 'changeDetection: ChangeDetectionStrategy.OnPush';
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
- if (valueNode) {
248
- fixes.push((fixer) => fixer.replaceText(valueNode, 'ChangeDetectionStrategy.OnPush'));
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 (!isComponentDecorator(node)) return;
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.type === 'Identifier' &&
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 = 'encapsulation: ViewEncapsulation.None';
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
- if (valueNode) {
201
- fixes.push((fixer) => fixer.replaceText(valueNode, 'ViewEncapsulation.None'));
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 (!isComponentDecorator(node)) return;
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.type === 'Identifier' &&
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 node.value.callee.type === 'Identifier' && node.value.callee.name === 'inject';
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;