@ethlete/eslint-plugin 1.0.0-next.22 → 1.0.0-next.24

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 (61) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/package.json +4 -5
  3. package/src/configs/recommended.js +23 -10
  4. package/src/index.js +6 -1
  5. package/src/rules/angular-decorator-property-order.js +20 -20
  6. package/src/rules/class-constant-property.js +35 -14
  7. package/src/rules/class-member-order.js +77 -46
  8. package/src/rules/guard-return-newline.js +5 -4
  9. package/src/rules/inject-member-accessibility.js +22 -24
  10. package/src/rules/internals/angular-io.js +31 -14
  11. package/src/rules/internals/angular-member-visibility.js +16 -12
  12. package/src/rules/internals/angular-metadata-fix.js +22 -0
  13. package/src/rules/internals/import-resolution.js +191 -0
  14. package/src/rules/internals/member-accessibility-fix.js +26 -0
  15. package/src/rules/no-angular-router-api.js +23 -29
  16. package/src/rules/no-angular-seo-services.js +21 -16
  17. package/src/rules/no-cdk-import.js +7 -14
  18. package/src/rules/no-direct-dom-manipulation.js +17 -0
  19. package/src/rules/no-document-cookie.js +3 -0
  20. package/src/rules/no-effect-cleanup-return.js +15 -9
  21. package/src/rules/no-empty-angular-metadata-arrays.js +6 -36
  22. package/src/rules/no-enum.js +115 -0
  23. package/src/rules/no-inject-chain.js +3 -1
  24. package/src/rules/no-leading-underscore-class-member.js +33 -3
  25. package/src/rules/no-legacy-angular-decorators.js +17 -21
  26. package/src/rules/no-legacy-prepare-without-injector.js +42 -26
  27. package/src/rules/no-legacy-query-import.js +7 -16
  28. package/src/rules/no-locale-id.js +6 -18
  29. package/src/rules/no-member-alias.js +21 -0
  30. package/src/rules/no-native-html-input-name.js +1 -1
  31. package/src/rules/no-pipe-logic.js +11 -17
  32. package/src/rules/no-readonly-signal.js +13 -5
  33. package/src/rules/no-redundant-internal.js +21 -2
  34. package/src/rules/no-redundant-on-push-change-detection.js +32 -56
  35. package/src/rules/no-rxjs-in-effect.js +12 -8
  36. package/src/rules/no-screaming-case-local.js +1 -0
  37. package/src/rules/no-standalone-flag.js +7 -36
  38. package/src/rules/no-subscribe-in-pipe.js +3 -2
  39. package/src/rules/no-template-literal-before-inline-template.js +3 -14
  40. package/src/rules/no-trivial-return-type.js +2 -0
  41. package/src/rules/no-trivial-wrapper-method.js +13 -16
  42. package/src/rules/no-type-only-import.js +7 -2
  43. package/src/rules/no-typed-injected-element-ref.js +23 -19
  44. package/src/rules/no-unused-class-member.js +34 -14
  45. package/src/rules/prefer-clone-equal.js +8 -6
  46. package/src/rules/prefer-concise-angular-host-directives.js +23 -29
  47. package/src/rules/prefer-concise-angular-style-metadata.js +8 -16
  48. package/src/rules/prefer-element-dimensions.js +9 -10
  49. package/src/rules/prefer-linked-signal.js +7 -4
  50. package/src/rules/prefer-match-media.js +12 -16
  51. package/src/rules/prefer-present-tense-output.js +1 -1
  52. package/src/rules/prefer-rxjs-timer.js +3 -1
  53. package/src/rules/prefer-scroll-state.js +3 -67
  54. package/src/rules/prefer-static-boolean-properties.js +30 -0
  55. package/src/rules/prefer-viewport-size.js +1 -7
  56. package/src/rules/require-dollar-suffix.js +7 -4
  57. package/src/rules/require-form-submit.js +2 -28
  58. package/src/rules/require-on-push-change-detection.js +48 -36
  59. package/src/rules/require-view-encapsulation-none.js +48 -34
  60. package/src/rules/take-until-destroyed-last.js +64 -0
  61. package/src/rules/template-member-accessibility.js +12 -27
@@ -19,6 +19,8 @@
19
19
  * the types this rule leaves alone), enable `@typescript-eslint/no-deprecated` alongside it.
20
20
  */
21
21
 
22
+ const { MODULE_REFERENCE_SELECTOR, getModuleReference } = require('./internals/import-resolution');
23
+
22
24
  const QUERY_PACKAGE = '@ethlete/query';
23
25
  const DEFAULT_DOCS_BASE_URL = 'https://ethlete-sdk-docs.web.app';
24
26
 
@@ -48,7 +50,7 @@ const LEGACY_SYMBOLS = {
48
50
  queryStateErrorSignal: { to: '`query.error()`', docs: '/query/queries#the-query-object' },
49
51
  queryStateLoadingSignal: { to: '`query.loading()`', docs: '/query/queries#the-query-object' },
50
52
  validateWithV2Query: { to: '`validateWithQuery`', docs: '/query/errors#validating-against-the-server-as-the-user-types' }, // prettier-ignore
51
- provideQueryClientForDevtools: { to: '`provideQueryDevtools()` from `@ethlete/query-devtools`, which registers every client at once', docs: '/query-devtools/' }, // prettier-ignore
53
+ provideQueryClientForDevtools: { to: '`provideQueryDevtools()` from `@ethlete/query`, which registers every client at once', docs: '/query-devtools/' }, // prettier-ignore
52
54
  };
53
55
 
54
56
  /** The current-system counterparts of the names the legacy system had to give up its half of. */
@@ -60,13 +62,6 @@ const V2_SUCCESSORS = {
60
62
  V2BearerAuthProvider: { to: '`createBearerAuthProvider` plus the secure creator templates', docs: '/query/auth' },
61
63
  };
62
64
 
63
- /** @param {any} specifier */
64
- const importedName = (specifier) => {
65
- if (specifier.type !== 'ImportSpecifier') return null;
66
-
67
- return specifier.imported.name ?? specifier.imported.value ?? null;
68
- };
69
-
70
65
  /** @param {string} name */
71
66
  const isV2Symbol = (name) => /^(V2|AnyV2)[A-Z]/.test(name);
72
67
 
@@ -98,16 +93,12 @@ const noLegacyQueryImport = {
98
93
  const docsBaseUrl = (options.docsBaseUrl ?? DEFAULT_DOCS_BASE_URL).replace(/\/$/, '');
99
94
 
100
95
  return {
101
- ImportDeclaration(node) {
102
- const declaration = /** @type {any} */ (node);
103
-
104
- if (declaration.source.value !== QUERY_PACKAGE) return;
105
-
106
- for (const specifier of declaration.specifiers) {
107
- const name = importedName(specifier);
96
+ [MODULE_REFERENCE_SELECTOR](node) {
97
+ const reference = getModuleReference(node);
108
98
 
109
- if (!name) continue;
99
+ if (reference?.source !== QUERY_PACKAGE) return;
110
100
 
101
+ for (const { name, node: specifier } of reference.named) {
111
102
  const successor = V2_SUCCESSORS[name] ?? LEGACY_SYMBOLS[name];
112
103
 
113
104
  if (successor) {
@@ -4,18 +4,18 @@
4
4
  /**
5
5
  * Disallows injecting Angular's LOCALE_ID token directly.
6
6
  *
7
- * @ethlete/core provides injectLocale() which wraps LOCALE_ID in a signal
8
- * and exposes helper utilities for locale-aware formatting.
7
+ * @ethlete/core provides a signal-based locale store for application code.
9
8
  *
10
9
  * BAD:
11
10
  * inject(LOCALE_ID)
12
- * import { LOCALE_ID } from '@angular/core';
13
11
  *
14
12
  * GOOD:
15
13
  * import { injectLocale } from '@ethlete/core';
16
14
  * const locale = injectLocale();
17
15
  */
18
16
 
17
+ const { isImportedAs } = require('./internals/import-resolution');
18
+
19
19
  /** @type {import('eslint').Rule.RuleModule} */
20
20
  const noLocaleId = {
21
21
  meta: {
@@ -26,30 +26,18 @@ const noLocaleId = {
26
26
  },
27
27
  messages: {
28
28
  noLocaleId:
29
- "Do not inject 'LOCALE_ID' directly. Use 'injectLocale()' from '@ethlete/core' instead — it wraps LOCALE_ID and provides a clean, signal-compatible API.",
29
+ "Do not inject 'LOCALE_ID' directly. Use the signal-based 'injectLocale()' API from '@ethlete/core' for application locale state.",
30
30
  },
31
31
  schema: [],
32
32
  },
33
33
  create(context) {
34
34
  return {
35
35
  CallExpression(node) {
36
- const { callee } = node;
37
- if (callee.type !== 'Identifier' || callee.name !== 'inject') return;
38
- const arg = node.arguments[0];
39
- if (arg?.type === 'Identifier' && arg.name === 'LOCALE_ID') {
36
+ if (!isImportedAs(context.sourceCode, node.callee, 'inject')) return;
37
+ if (isImportedAs(context.sourceCode, node.arguments[0], 'LOCALE_ID')) {
40
38
  context.report({ node, messageId: 'noLocaleId' });
41
39
  }
42
40
  },
43
-
44
- ImportDeclaration(node) {
45
- if (node.source.value !== '@angular/core') return;
46
- for (const specifier of node.specifiers) {
47
- if (specifier.type === 'ImportSpecifier' && specifier.imported.name === 'LOCALE_ID') {
48
- context.report({ node, messageId: 'noLocaleId' });
49
- return;
50
- }
51
- }
52
- },
53
41
  };
54
42
  },
55
43
  };
@@ -1,6 +1,8 @@
1
1
  // @ts-check
2
2
  'use strict';
3
3
 
4
+ const { getAngularDecoratorName } = require('./internals/import-resolution');
5
+
4
6
  /**
5
7
  * Disallows class members that exist solely as aliases for a nested property or
6
8
  * zero-arg method of another class member accessed via `this`.
@@ -79,6 +81,18 @@ const getMethodAliasInfo = (node) => {
79
81
  return { sourceName: obj.property.name, propName: callee.property.name };
80
82
  };
81
83
 
84
+ const CONTRACT_METHOD_NAMES = new Set(['blur', 'focus', 'reset']);
85
+
86
+ /**
87
+ * @param {import('eslint').SourceCode} sourceCode
88
+ * @param {any} methodNode
89
+ */
90
+ const isInAngularClass = (sourceCode, methodNode) =>
91
+ methodNode.parent?.parent?.decorators?.some((/** @type {any} */ decorator) => {
92
+ const name = getAngularDecoratorName(sourceCode, decorator);
93
+ return name === 'Component' || name === 'Directive';
94
+ }) ?? false;
95
+
82
96
  /** @type {import('eslint').Rule.RuleModule} */
83
97
  const noMemberAlias = {
84
98
  meta: {
@@ -119,6 +133,10 @@ const noMemberAlias = {
119
133
 
120
134
  const sourceName = obj.property.name;
121
135
  const propName = value.property.name;
136
+ const sourceMember = node.parent?.body?.find(
137
+ (member) => !member.computed && member.key?.type === 'Identifier' && member.key.name === sourceName,
138
+ );
139
+ if (node.accessibility === 'private' && sourceMember?.accessibility === 'private') return;
122
140
 
123
141
  context.report({
124
142
  node,
@@ -133,6 +151,9 @@ const noMemberAlias = {
133
151
 
134
152
  const info = getMethodAliasInfo(node);
135
153
  if (!info) return;
154
+ const classNode = node.parent?.parent;
155
+ if (node.override || classNode?.superClass || classNode?.implements?.length) return;
156
+ if (isInAngularClass(context.sourceCode, node) && CONTRACT_METHOD_NAMES.has(aliasName)) return;
136
157
 
137
158
  // Only flag when names match — different names indicate an intentional
138
159
  // API rename, which is not a pure alias.
@@ -75,7 +75,7 @@ const rule = {
75
75
  create(context) {
76
76
  return {
77
77
  PropertyDefinition(node) {
78
- const io = getReactiveIo(node);
78
+ const io = getReactiveIo(context.sourceCode, node);
79
79
  if (!io) return;
80
80
  if (io.kind === 'output') return;
81
81
 
@@ -1,23 +1,16 @@
1
1
  // @ts-check
2
2
  'use strict';
3
3
 
4
+ const { getAngularDecoratorName } = require('./internals/import-resolution');
5
+
4
6
  /**
5
- * Checks whether a class node has a @Pipe decorator.
6
- * @param {import('eslint').Rule.Node} classNode
7
+ * @param {import('eslint').SourceCode} sourceCode
8
+ * @param {any} classNode
7
9
  */
8
- const hasPipeDecorator = (classNode) => {
9
- const decorators = /** @type {any} */ (classNode).decorators ?? [];
10
- return decorators.some((/** @type {any} */ dec) => {
11
- const expr = dec.expression;
12
- if (expr.type === 'CallExpression') {
13
- return expr.callee.type === 'Identifier' && expr.callee.name === 'Pipe';
14
- }
15
- if (expr.type === 'Identifier') {
16
- return expr.name === 'Pipe';
17
- }
18
- return false;
19
- });
20
- };
10
+ const hasPipeDecorator = (sourceCode, classNode) =>
11
+ (classNode.decorators ?? []).some(
12
+ (/** @type {any} */ decorator) => getAngularDecoratorName(sourceCode, decorator) === 'Pipe',
13
+ );
21
14
 
22
15
  /** @type {import('eslint').Rule.RuleModule} */
23
16
  const noPipeLogic = {
@@ -39,6 +32,7 @@ const noPipeLogic = {
39
32
  MethodDefinition(node) {
40
33
  const key = /** @type {any} */ (node).key;
41
34
  if (key.type !== 'Identifier' || key.name !== 'transform') return;
35
+ if (!node.value?.body || node.value.body.type === 'TSEmptyBodyFunctionExpression') return;
42
36
 
43
37
  const classBody = node.parent;
44
38
  if (!classBody || classBody.type !== 'ClassBody') return;
@@ -46,7 +40,7 @@ const noPipeLogic = {
46
40
  const classNode = classBody.parent;
47
41
  if (!classNode || (classNode.type !== 'ClassDeclaration' && classNode.type !== 'ClassExpression')) return;
48
42
 
49
- if (!hasPipeDecorator(classNode)) return;
43
+ if (!hasPipeDecorator(context.sourceCode, classNode)) return;
50
44
 
51
45
  context.report({ node, messageId: 'noLogicInTransform' });
52
46
  },
@@ -62,7 +56,7 @@ const noPipeLogic = {
62
56
  const classNode = classBody.parent;
63
57
  if (!classNode || (classNode.type !== 'ClassDeclaration' && classNode.type !== 'ClassExpression')) return;
64
58
 
65
- if (!hasPipeDecorator(classNode)) return;
59
+ if (!hasPipeDecorator(context.sourceCode, classNode)) return;
66
60
 
67
61
  const value = /** @type {any} */ (node).value;
68
62
  if (value && (value.type === 'ArrowFunctionExpression' || value.type === 'FunctionExpression')) {
@@ -50,6 +50,10 @@ const REACTIVE_APIS = new Set([
50
50
  'effect',
51
51
  'afterNextRender',
52
52
  'afterRender',
53
+ 'afterRenderEffect',
54
+ 'resource',
55
+ 'rxResource',
56
+ 'httpResource',
53
57
  // RxJS interop
54
58
  'toSignal',
55
59
  'toObservable',
@@ -83,7 +87,7 @@ const noReadonlySignal = {
83
87
  let apiName = null;
84
88
 
85
89
  // Simple call: signal(), input(), computed(), inject(), etc.
86
- if (callee.type === 'Identifier' && REACTIVE_APIS.has(callee.name)) {
90
+ if (callee.type === 'Identifier' && (REACTIVE_APIS.has(callee.name) || callee.name.startsWith('inject'))) {
87
91
  apiName = callee.name;
88
92
  }
89
93
  // Member call: input.required(), outputFromObservable.something(), etc.
@@ -101,13 +105,17 @@ const noReadonlySignal = {
101
105
  messageId: 'noReadonlySignal',
102
106
  data: { name: apiName },
103
107
  fix(fixer) {
104
- // Find and remove the "readonly" token
105
108
  const sourceCode = context.sourceCode;
106
109
  const tokens = sourceCode.getTokens(node);
107
- const readonlyToken = tokens.find((t) => t.value === 'readonly');
110
+ const decoratorEnd = node.decorators?.at(-1)?.range[1] ?? node.range[0];
111
+ const readonlyToken = tokens.find(
112
+ (token) =>
113
+ token.value === 'readonly' && token.range[0] >= decoratorEnd && token.range[1] <= node.key.range[0],
114
+ );
108
115
  if (!readonlyToken) return null;
109
- // Remove "readonly " (including the trailing space)
110
- return fixer.removeRange([readonlyToken.range[0], readonlyToken.range[1] + 1]);
116
+ const nextToken = sourceCode.getTokenAfter(readonlyToken);
117
+ const rangeEnd = nextToken ? nextToken.range[0] : readonlyToken.range[1];
118
+ return fixer.removeRange([readonlyToken.range[0], rangeEnd]);
111
119
  },
112
120
  });
113
121
  }
@@ -40,6 +40,25 @@ const getMemberKind = (node) => {
40
40
  return 'property';
41
41
  };
42
42
 
43
+ /**
44
+ * @param {import('eslint').SourceCode} sourceCode
45
+ * @param {import('eslint').Rule.Node} node
46
+ * @param {import('eslint').AST.Token} comment
47
+ */
48
+ const buildInternalTagFix = (sourceCode, node, comment) => {
49
+ const commentText = sourceCode.getText(comment);
50
+ const remainingText = commentText
51
+ .replace(/^\s*\/\*\*?\s*@internal\s*\*\/\s*$/u, '')
52
+ .replace(/^\s*\*\s*@internal\s*\r?\n/mu, '')
53
+ .replace(/@internal\b\s*/u, '');
54
+
55
+ if (remainingText.trim() === '') {
56
+ return (fixer) => fixer.removeRange([comment.range[0], node.range[0]]);
57
+ }
58
+
59
+ return (fixer) => fixer.replaceText(comment, remainingText);
60
+ };
61
+
43
62
  /** @type {import('eslint').Rule.RuleModule} */
44
63
  const noRedundantInternal = {
45
64
  meta: {
@@ -68,7 +87,7 @@ const noRedundantInternal = {
68
87
  return;
69
88
  }
70
89
 
71
- if (node.accessibility !== 'private' && node.accessibility !== 'protected') {
90
+ if (node.accessibility !== 'private') {
72
91
  return;
73
92
  }
74
93
 
@@ -80,7 +99,7 @@ const noRedundantInternal = {
80
99
  kind: getMemberKind(node),
81
100
  name: getMemberName(node),
82
101
  },
83
- fix: (fixer) => fixer.removeRange([internalComment.range[0], node.range[0]]),
102
+ fix: buildInternalTagFix(sourceCode, node, internalComment),
84
103
  });
85
104
  };
86
105
 
@@ -1,6 +1,10 @@
1
1
  // @ts-check
2
2
  'use strict';
3
3
 
4
+ const { getAngularDecoratorName, isImportedAs } = require('./internals/import-resolution');
5
+
6
+ const { getMetadataEntryRemovalRange } = require('./internals/angular-metadata-fix');
7
+
4
8
  /**
5
9
  * Disallow explicit `changeDetection: ChangeDetectionStrategy.OnPush` on
6
10
  * `@Component` decorators. Since Angular 22, OnPush is the default change
@@ -68,50 +72,15 @@ const getPropertyName = (key) => {
68
72
  };
69
73
 
70
74
  /**
71
- * @param {import('eslint').Rule.Node} node
72
- */
73
- const isComponentDecorator = (node) => {
74
- const decorator = /** @type {any} */ (node);
75
- if (decorator.type !== 'Decorator') return false;
76
-
77
- const expression = decorator.expression;
78
- if (expression.type === 'CallExpression') {
79
- return expression.callee.type === 'Identifier' && expression.callee.name === 'Component';
80
- }
81
-
82
- return expression.type === 'Identifier' && expression.name === 'Component';
83
- };
84
-
85
- /**
75
+ * @param {import('eslint').SourceCode} sourceCode
86
76
  * @param {any} value
87
77
  */
88
- const isOnPushValue = (value) =>
78
+ const isOnPushValue = (sourceCode, value) =>
89
79
  value.type === 'MemberExpression' &&
90
- value.object.type === 'Identifier' &&
91
- value.object.name === 'ChangeDetectionStrategy' &&
80
+ !value.computed &&
92
81
  value.property.type === 'Identifier' &&
93
- value.property.name === 'OnPush';
94
-
95
- /**
96
- * Range that removes a property together with the comma that joins it to its
97
- * siblings (leading comma when it is the last property, otherwise trailing).
98
- *
99
- * @param {import('eslint').SourceCode} sourceCode
100
- * @param {any} metadata
101
- * @param {any} property
102
- */
103
- const getPropertyRemovalRange = (sourceCode, metadata, property) => {
104
- const properties = metadata.properties.filter((entry) => entry.type === 'Property');
105
- const index = properties.indexOf(property);
106
- const openingBrace = sourceCode.getFirstToken(metadata);
107
- const closingBrace = sourceCode.getLastToken(metadata);
108
-
109
- if (!openingBrace || !closingBrace) return property.range;
110
- if (properties.length === 1) return [openingBrace.range[1], closingBrace.range[0]];
111
- if (index < properties.length - 1) return [property.range[0], properties[index + 1].range[0]];
112
-
113
- return [properties[index - 1].range[1], property.range[1]];
114
- };
82
+ value.property.name === 'OnPush' &&
83
+ isImportedAs(sourceCode, value.object, 'ChangeDetectionStrategy');
115
84
 
116
85
  /**
117
86
  * Range that removes a single named import specifier together with its comma.
@@ -126,6 +95,15 @@ const getSpecifierRemovalRange = (sourceCode, importNode, specifier) => {
126
95
 
127
96
  // Sole specifier of the whole import — drop the entire declaration.
128
97
  if (importNode.specifiers.length === 1) return importNode.range;
98
+ if (named.length === 1) {
99
+ const openingBrace = sourceCode.getTokenBefore(specifier, (token) => token.value === '{');
100
+ const closingBrace = sourceCode.getTokenAfter(specifier, (token) => token.value === '}');
101
+ const precedingComma = openingBrace ? sourceCode.getTokenBefore(openingBrace) : null;
102
+
103
+ if (openingBrace && closingBrace && precedingComma?.value === ',') {
104
+ return [precedingComma.range[0], closingBrace.range[1]];
105
+ }
106
+ }
129
107
  if (index < named.length - 1) return [specifier.range[0], named[index + 1].range[0]];
130
108
 
131
109
  return [named[index - 1].range[1], specifier.range[1]];
@@ -137,18 +115,10 @@ const getSpecifierRemovalRange = (sourceCode, importNode, specifier) => {
137
115
  * removing those properties leaves the import unused and safe to drop.
138
116
  *
139
117
  * @param {import('eslint').Scope.Reference} reference
118
+ * @param {Set<any>} removableIdentifiers
140
119
  */
141
- const isOnPushChangeDetectionReference = (reference) => {
142
- const identifier = /** @type {any} */ (reference.identifier);
143
- const member = identifier.parent;
144
- if (!member || member.type !== 'MemberExpression' || member.object !== identifier) return false;
145
- if (member.property.type !== 'Identifier' || member.property.name !== 'OnPush') return false;
146
-
147
- const property = member.parent;
148
- if (!property || property.type !== 'Property' || property.value !== member) return false;
149
-
150
- return getPropertyName(property.key) === 'changeDetection';
151
- };
120
+ const isOnPushChangeDetectionReference = (reference, removableIdentifiers) =>
121
+ removableIdentifiers.has(reference.identifier);
152
122
 
153
123
  /** @type {import('eslint').Rule.RuleModule} */
154
124
  const noRedundantOnPushChangeDetection = {
@@ -177,10 +147,11 @@ const noRedundantOnPushChangeDetection = {
177
147
  const sourceCode = context.sourceCode;
178
148
  /** @type {any} */
179
149
  let changeDetectionSpecifier = null;
150
+ const removableIdentifiers = new Set();
180
151
 
181
152
  return {
182
153
  Decorator(node) {
183
- if (!isComponentDecorator(node)) return;
154
+ if (getAngularDecoratorName(sourceCode, node) !== 'Component') return;
184
155
 
185
156
  const expression = /** @type {any} */ (node).expression;
186
157
  if (expression.type !== 'CallExpression' || expression.arguments.length === 0) return;
@@ -191,12 +162,13 @@ const noRedundantOnPushChangeDetection = {
191
162
  const property = metadata.properties.find(
192
163
  (entry) => entry.type === 'Property' && getPropertyName(entry.key) === 'changeDetection',
193
164
  );
194
- if (!property || !isOnPushValue(property.value)) return;
165
+ if (!property || !isOnPushValue(sourceCode, property.value)) return;
166
+ removableIdentifiers.add(property.value.object);
195
167
 
196
168
  context.report({
197
169
  node: property,
198
170
  messageId: 'redundant',
199
- fix: (fixer) => fixer.replaceTextRange(getPropertyRemovalRange(sourceCode, metadata, property), ''),
171
+ fix: (fixer) => fixer.replaceTextRange(getMetadataEntryRemovalRange(sourceCode, metadata, property), ''),
200
172
  });
201
173
  },
202
174
 
@@ -219,8 +191,12 @@ const noRedundantOnPushChangeDetection = {
219
191
  if (!changeDetectionSpecifier) return;
220
192
 
221
193
  const [variable] = sourceCode.getDeclaredVariables(changeDetectionSpecifier);
222
- if (!variable || variable.references.length === 0) return;
223
- if (!variable.references.every(isOnPushChangeDetectionReference)) return;
194
+ if (!variable) return;
195
+ if (
196
+ !variable.references.every((reference) => isOnPushChangeDetectionReference(reference, removableIdentifiers))
197
+ ) {
198
+ return;
199
+ }
224
200
 
225
201
  const importNode = changeDetectionSpecifier.parent;
226
202
  context.report({
@@ -1,6 +1,8 @@
1
1
  // @ts-check
2
2
  'use strict';
3
3
 
4
+ const { ANGULAR_CORE, getImportedName } = require('./internals/import-resolution');
5
+
4
6
  /**
5
7
  * Disallows calling .subscribe() inside Angular's effect() or computed() callbacks.
6
8
  *
@@ -22,6 +24,11 @@
22
24
  * .subscribe();
23
25
  */
24
26
 
27
+ /**
28
+ * @param {any} node
29
+ */
30
+ const isStringLiteral = (node) => node?.type === 'Literal' && typeof node.value === 'string';
31
+
25
32
  /** @type {import('eslint').Rule.RuleModule} */
26
33
  const noRxjsInEffect = {
27
34
  meta: {
@@ -50,13 +57,9 @@ const noRxjsInEffect = {
50
57
  if (current.type === 'ArrowFunctionExpression' || current.type === 'FunctionExpression') {
51
58
  crossedFunctionBoundary = true;
52
59
  }
53
- if (
54
- crossedFunctionBoundary &&
55
- current.type === 'CallExpression' &&
56
- current.callee.type === 'Identifier' &&
57
- (current.callee.name === 'effect' || current.callee.name === 'computed')
58
- ) {
59
- return current.callee.name;
60
+ if (crossedFunctionBoundary && current.type === 'CallExpression') {
61
+ const calleeName = getImportedName(context.sourceCode, current.callee, ANGULAR_CORE);
62
+ if (calleeName === 'effect' || calleeName === 'computed') return calleeName;
60
63
  }
61
64
  current = current.parent;
62
65
  }
@@ -69,7 +72,8 @@ const noRxjsInEffect = {
69
72
  if (
70
73
  callee.type === 'MemberExpression' &&
71
74
  callee.property.type === 'Identifier' &&
72
- callee.property.name === 'subscribe'
75
+ callee.property.name === 'subscribe' &&
76
+ !isStringLiteral(node.arguments[0])
73
77
  ) {
74
78
  const reactiveContext = getReactiveContext(node);
75
79
  if (reactiveContext) {
@@ -79,6 +79,7 @@ const noScreamingCaseLocal = {
79
79
 
80
80
  // Only flag inside function bodies
81
81
  if (!isInsideFunction(node)) return;
82
+ if (node.init?.type === 'ArrowFunctionExpression' || node.init?.type === 'FunctionExpression') return;
82
83
 
83
84
  context.report({
84
85
  node: node.id,
@@ -1,6 +1,10 @@
1
1
  // @ts-check
2
2
  'use strict';
3
3
 
4
+ const { getAngularDecoratorName } = require('./internals/import-resolution');
5
+
6
+ const { getMetadataEntryRemovalRange } = require('./internals/angular-metadata-fix');
7
+
4
8
  /** @typedef {'Component' | 'Directive' | 'Pipe'} TDecoratorName */
5
9
 
6
10
  /**
@@ -12,40 +16,6 @@ const getPropertyName = (key) => {
12
16
  return null;
13
17
  };
14
18
 
15
- /**
16
- * @param {import('eslint').Rule.Node} node
17
- * @returns {TDecoratorName | null}
18
- */
19
- const getDecoratorName = (node) => {
20
- const decorator = /** @type {any} */ (node);
21
- if (decorator.type !== 'Decorator') return null;
22
-
23
- const expression = decorator.expression;
24
- if (expression.type === 'CallExpression') {
25
- return expression.callee.type === 'Identifier' ? expression.callee.name : null;
26
- }
27
-
28
- return expression.type === 'Identifier' ? expression.name : null;
29
- };
30
-
31
- /**
32
- * @param {import('eslint').SourceCode} sourceCode
33
- * @param {any} metadata
34
- * @param {any} property
35
- */
36
- const getRemovalRange = (sourceCode, metadata, property) => {
37
- const properties = metadata.properties.filter((entry) => entry.type === 'Property');
38
- const propertyIndex = properties.indexOf(property);
39
- const openingBrace = sourceCode.getFirstToken(metadata);
40
- const closingBrace = sourceCode.getLastToken(metadata);
41
-
42
- if (!openingBrace || !closingBrace) return property.range;
43
- if (properties.length === 1) return [openingBrace.range[1], closingBrace.range[0]];
44
- if (propertyIndex < properties.length - 1) return [property.range[0], properties[propertyIndex + 1].range[0]];
45
-
46
- return [properties[propertyIndex - 1].range[1], property.range[1]];
47
- };
48
-
49
19
  /** @type {import('eslint').Rule.RuleModule} */
50
20
  const noStandaloneFlag = {
51
21
  meta: {
@@ -65,7 +35,7 @@ const noStandaloneFlag = {
65
35
 
66
36
  return {
67
37
  Decorator(node) {
68
- const decoratorName = getDecoratorName(node);
38
+ const decoratorName = getAngularDecoratorName(context.sourceCode, node);
69
39
  if (decoratorName !== 'Component' && decoratorName !== 'Directive' && decoratorName !== 'Pipe') return;
70
40
 
71
41
  const decorator = /** @type {any} */ (node);
@@ -78,12 +48,13 @@ const noStandaloneFlag = {
78
48
  for (const property of metadata.properties) {
79
49
  if (property.type !== 'Property') continue;
80
50
  if (getPropertyName(property.key) !== 'standalone') continue;
51
+ if (property.value.type !== 'Literal' || property.value.value !== true) continue;
81
52
 
82
53
  context.report({
83
54
  node: property,
84
55
  messageId: 'noStandalone',
85
56
  fix(fixer) {
86
- return fixer.replaceTextRange(getRemovalRange(sourceCode, metadata, property), '');
57
+ return fixer.replaceTextRange(getMetadataEntryRemovalRange(sourceCode, metadata, property), '');
87
58
  },
88
59
  });
89
60
  }
@@ -19,6 +19,8 @@
19
19
  * obs$.pipe(...operators).subscribe(); // ✅ subscribe on the result of pipe
20
20
  */
21
21
 
22
+ const { isImportedAs } = require('./internals/import-resolution');
23
+
22
24
  /** @type {import('eslint').Rule.RuleModule} */
23
25
  const noSubscribeInPipe = {
24
26
  meta: {
@@ -48,8 +50,7 @@ const noSubscribeInPipe = {
48
50
  // be flagged even if the Observable itself is passed into a .pipe() operator.
49
51
  if (
50
52
  current.type === 'NewExpression' &&
51
- current.callee.type === 'Identifier' &&
52
- current.callee.name === 'Observable'
53
+ isImportedAs(context.sourceCode, current.callee, 'Observable', 'rxjs')
53
54
  ) {
54
55
  return false;
55
56
  }
@@ -1,6 +1,8 @@
1
1
  // @ts-check
2
2
  'use strict';
3
3
 
4
+ const { getAngularDecoratorName } = require('./internals/import-resolution');
5
+
4
6
  const ts = require('typescript');
5
7
 
6
8
  /**
@@ -92,19 +94,6 @@ const getPropertyName = (key) => {
92
94
  return null;
93
95
  };
94
96
 
95
- /**
96
- * @param {any} node
97
- */
98
- const getDecoratorName = (node) => {
99
- const expression = node.expression;
100
- if (!expression) return null;
101
- if (expression.type === 'CallExpression') {
102
- return expression.callee.type === 'Identifier' ? expression.callee.name : null;
103
- }
104
-
105
- return expression.type === 'Identifier' ? expression.name : null;
106
- };
107
-
108
97
  /**
109
98
  * The inline template text, or null when the property is not a plain non-empty string literal.
110
99
  *
@@ -147,7 +136,7 @@ const noTemplateLiteralBeforeInlineTemplate = {
147
136
 
148
137
  /** @param {import('eslint').Rule.Node} node */
149
138
  Decorator(node) {
150
- const decoratorName = getDecoratorName(node);
139
+ const decoratorName = getAngularDecoratorName(context.sourceCode, node);
151
140
  if (decoratorName !== 'Component' && decoratorName !== 'Directive') return;
152
141
 
153
142
  const expression = /** @type {any} */ (node).expression;
@@ -173,6 +173,7 @@ const noTrivialReturnType = {
173
173
 
174
174
  const annotation = fn.returnType.typeAnnotation;
175
175
  if (!TRIVIAL_TYPES.has(annotation.type)) return;
176
+ if (fn.body.type !== 'BlockStatement') return;
176
177
  if (isSelfReferencing(fn)) return;
177
178
 
178
179
  const label = TRIVIAL_LABELS[annotation.type] ?? annotation.type;
@@ -190,6 +191,7 @@ const noTrivialReturnType = {
190
191
  return {
191
192
  ArrowFunctionExpression: checkReturnType,
192
193
  FunctionExpression: checkReturnType,
194
+ FunctionDeclaration: checkReturnType,
193
195
  };
194
196
  },
195
197
  };