@ethlete/eslint-plugin 1.0.0-next.23 → 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 (48) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +2 -2
  3. package/src/configs/recommended.js +3 -0
  4. package/src/index.js +2 -0
  5. package/src/rules/angular-decorator-property-order.js +20 -20
  6. package/src/rules/class-constant-property.js +8 -14
  7. package/src/rules/class-member-order.js +77 -46
  8. package/src/rules/inject-member-accessibility.js +5 -3
  9. package/src/rules/internals/angular-io.js +12 -13
  10. package/src/rules/internals/angular-member-visibility.js +11 -7
  11. package/src/rules/internals/import-resolution.js +191 -0
  12. package/src/rules/no-angular-router-api.js +23 -29
  13. package/src/rules/no-angular-seo-services.js +14 -9
  14. package/src/rules/no-cdk-import.js +7 -14
  15. package/src/rules/no-document-cookie.js +3 -0
  16. package/src/rules/no-effect-cleanup-return.js +7 -3
  17. package/src/rules/no-empty-angular-metadata-arrays.js +3 -17
  18. package/src/rules/no-inject-chain.js +3 -1
  19. package/src/rules/no-leading-underscore-class-member.js +26 -3
  20. package/src/rules/no-legacy-angular-decorators.js +9 -21
  21. package/src/rules/no-legacy-query-import.js +6 -15
  22. package/src/rules/no-locale-id.js +4 -4
  23. package/src/rules/no-member-alias.js +11 -7
  24. package/src/rules/no-native-html-input-name.js +1 -1
  25. package/src/rules/no-pipe-logic.js +10 -17
  26. package/src/rules/no-redundant-on-push-change-detection.js +9 -23
  27. package/src/rules/no-rxjs-in-effect.js +12 -8
  28. package/src/rules/no-screaming-case-local.js +1 -0
  29. package/src/rules/no-standalone-flag.js +3 -17
  30. package/src/rules/no-subscribe-in-pipe.js +3 -2
  31. package/src/rules/no-template-literal-before-inline-template.js +3 -14
  32. package/src/rules/no-trivial-return-type.js +1 -0
  33. package/src/rules/no-trivial-wrapper-method.js +10 -16
  34. package/src/rules/no-typed-injected-element-ref.js +14 -17
  35. package/src/rules/no-unused-class-member.js +6 -9
  36. package/src/rules/prefer-clone-equal.js +8 -6
  37. package/src/rules/prefer-concise-angular-host-directives.js +23 -29
  38. package/src/rules/prefer-concise-angular-style-metadata.js +8 -16
  39. package/src/rules/prefer-element-dimensions.js +9 -10
  40. package/src/rules/prefer-linked-signal.js +7 -4
  41. package/src/rules/prefer-match-media.js +10 -14
  42. package/src/rules/prefer-present-tense-output.js +1 -1
  43. package/src/rules/prefer-rxjs-timer.js +3 -1
  44. package/src/rules/require-dollar-suffix.js +7 -4
  45. package/src/rules/require-on-push-change-detection.js +17 -25
  46. package/src/rules/require-view-encapsulation-none.js +17 -25
  47. package/src/rules/take-until-destroyed-last.js +64 -0
  48. package/src/rules/template-member-accessibility.js +8 -6
@@ -14,6 +14,8 @@
14
14
  * const locale = injectLocale();
15
15
  */
16
16
 
17
+ const { isImportedAs } = require('./internals/import-resolution');
18
+
17
19
  /** @type {import('eslint').Rule.RuleModule} */
18
20
  const noLocaleId = {
19
21
  meta: {
@@ -31,10 +33,8 @@ const noLocaleId = {
31
33
  create(context) {
32
34
  return {
33
35
  CallExpression(node) {
34
- const { callee } = node;
35
- if (callee.type !== 'Identifier' || callee.name !== 'inject') return;
36
- const arg = node.arguments[0];
37
- 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')) {
38
38
  context.report({ node, messageId: 'noLocaleId' });
39
39
  }
40
40
  },
@@ -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`.
@@ -81,12 +83,14 @@ const getMethodAliasInfo = (node) => {
81
83
 
82
84
  const CONTRACT_METHOD_NAMES = new Set(['blur', 'focus', 'reset']);
83
85
 
84
- /** @param {any} methodNode */
85
- const isInAngularClass = (methodNode) =>
86
- methodNode.parent?.parent?.decorators?.some((decorator) => {
87
- const expression = decorator.expression;
88
- const callee = expression.type === 'CallExpression' ? expression.callee : expression;
89
- return callee.type === 'Identifier' && (callee.name === 'Component' || callee.name === 'Directive');
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';
90
94
  }) ?? false;
91
95
 
92
96
  /** @type {import('eslint').Rule.RuleModule} */
@@ -149,7 +153,7 @@ const noMemberAlias = {
149
153
  if (!info) return;
150
154
  const classNode = node.parent?.parent;
151
155
  if (node.override || classNode?.superClass || classNode?.implements?.length) return;
152
- if (isInAngularClass(node) && CONTRACT_METHOD_NAMES.has(aliasName)) return;
156
+ if (isInAngularClass(context.sourceCode, node) && CONTRACT_METHOD_NAMES.has(aliasName)) return;
153
157
 
154
158
  // Only flag when names match — different names indicate an intentional
155
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 = {
@@ -47,7 +40,7 @@ const noPipeLogic = {
47
40
  const classNode = classBody.parent;
48
41
  if (!classNode || (classNode.type !== 'ClassDeclaration' && classNode.type !== 'ClassExpression')) return;
49
42
 
50
- if (!hasPipeDecorator(classNode)) return;
43
+ if (!hasPipeDecorator(context.sourceCode, classNode)) return;
51
44
 
52
45
  context.report({ node, messageId: 'noLogicInTransform' });
53
46
  },
@@ -63,7 +56,7 @@ const noPipeLogic = {
63
56
  const classNode = classBody.parent;
64
57
  if (!classNode || (classNode.type !== 'ClassDeclaration' && classNode.type !== 'ClassExpression')) return;
65
58
 
66
- if (!hasPipeDecorator(classNode)) return;
59
+ if (!hasPipeDecorator(context.sourceCode, classNode)) return;
67
60
 
68
61
  const value = /** @type {any} */ (node).value;
69
62
  if (value && (value.type === 'ArrowFunctionExpression' || value.type === 'FunctionExpression')) {
@@ -1,6 +1,8 @@
1
1
  // @ts-check
2
2
  'use strict';
3
3
 
4
+ const { getAngularDecoratorName, isImportedAs } = require('./internals/import-resolution');
5
+
4
6
  const { getMetadataEntryRemovalRange } = require('./internals/angular-metadata-fix');
5
7
 
6
8
  /**
@@ -70,29 +72,15 @@ const getPropertyName = (key) => {
70
72
  };
71
73
 
72
74
  /**
73
- * @param {import('eslint').Rule.Node} node
74
- */
75
- const isComponentDecorator = (node) => {
76
- const decorator = /** @type {any} */ (node);
77
- if (decorator.type !== 'Decorator') return false;
78
-
79
- const expression = decorator.expression;
80
- if (expression.type === 'CallExpression') {
81
- return expression.callee.type === 'Identifier' && expression.callee.name === 'Component';
82
- }
83
-
84
- return expression.type === 'Identifier' && expression.name === 'Component';
85
- };
86
-
87
- /**
75
+ * @param {import('eslint').SourceCode} sourceCode
88
76
  * @param {any} value
89
77
  */
90
- const isOnPushValue = (value, localName) =>
78
+ const isOnPushValue = (sourceCode, value) =>
91
79
  value.type === 'MemberExpression' &&
92
- value.object.type === 'Identifier' &&
93
- value.object.name === localName &&
80
+ !value.computed &&
94
81
  value.property.type === 'Identifier' &&
95
- value.property.name === 'OnPush';
82
+ value.property.name === 'OnPush' &&
83
+ isImportedAs(sourceCode, value.object, 'ChangeDetectionStrategy');
96
84
 
97
85
  /**
98
86
  * Range that removes a single named import specifier together with its comma.
@@ -159,12 +147,11 @@ const noRedundantOnPushChangeDetection = {
159
147
  const sourceCode = context.sourceCode;
160
148
  /** @type {any} */
161
149
  let changeDetectionSpecifier = null;
162
- let changeDetectionLocalName = 'ChangeDetectionStrategy';
163
150
  const removableIdentifiers = new Set();
164
151
 
165
152
  return {
166
153
  Decorator(node) {
167
- if (!isComponentDecorator(node)) return;
154
+ if (getAngularDecoratorName(sourceCode, node) !== 'Component') return;
168
155
 
169
156
  const expression = /** @type {any} */ (node).expression;
170
157
  if (expression.type !== 'CallExpression' || expression.arguments.length === 0) return;
@@ -175,7 +162,7 @@ const noRedundantOnPushChangeDetection = {
175
162
  const property = metadata.properties.find(
176
163
  (entry) => entry.type === 'Property' && getPropertyName(entry.key) === 'changeDetection',
177
164
  );
178
- if (!property || !isOnPushValue(property.value, changeDetectionLocalName)) return;
165
+ if (!property || !isOnPushValue(sourceCode, property.value)) return;
179
166
  removableIdentifiers.add(property.value.object);
180
167
 
181
168
  context.report({
@@ -195,7 +182,6 @@ const noRedundantOnPushChangeDetection = {
195
182
  specifier.imported.name === 'ChangeDetectionStrategy'
196
183
  ) {
197
184
  changeDetectionSpecifier = specifier;
198
- changeDetectionLocalName = specifier.local.name;
199
185
  return;
200
186
  }
201
187
  }
@@ -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,8 @@
1
1
  // @ts-check
2
2
  'use strict';
3
3
 
4
+ const { getAngularDecoratorName } = require('./internals/import-resolution');
5
+
4
6
  const { getMetadataEntryRemovalRange } = require('./internals/angular-metadata-fix');
5
7
 
6
8
  /** @typedef {'Component' | 'Directive' | 'Pipe'} TDecoratorName */
@@ -14,22 +16,6 @@ const getPropertyName = (key) => {
14
16
  return null;
15
17
  };
16
18
 
17
- /**
18
- * @param {import('eslint').Rule.Node} node
19
- * @returns {TDecoratorName | null}
20
- */
21
- const getDecoratorName = (node) => {
22
- const decorator = /** @type {any} */ (node);
23
- if (decorator.type !== 'Decorator') return null;
24
-
25
- const expression = decorator.expression;
26
- if (expression.type === 'CallExpression') {
27
- return expression.callee.type === 'Identifier' ? expression.callee.name : null;
28
- }
29
-
30
- return expression.type === 'Identifier' ? expression.name : null;
31
- };
32
-
33
19
  /** @type {import('eslint').Rule.RuleModule} */
34
20
  const noStandaloneFlag = {
35
21
  meta: {
@@ -49,7 +35,7 @@ const noStandaloneFlag = {
49
35
 
50
36
  return {
51
37
  Decorator(node) {
52
- const decoratorName = getDecoratorName(node);
38
+ const decoratorName = getAngularDecoratorName(context.sourceCode, node);
53
39
  if (decoratorName !== 'Component' && decoratorName !== 'Directive' && decoratorName !== 'Pipe') return;
54
40
 
55
41
  const decorator = /** @type {any} */ (node);
@@ -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;
@@ -191,6 +191,7 @@ const noTrivialReturnType = {
191
191
  return {
192
192
  ArrowFunctionExpression: checkReturnType,
193
193
  FunctionExpression: checkReturnType,
194
+ FunctionDeclaration: checkReturnType,
194
195
  };
195
196
  },
196
197
  };
@@ -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 trivial wrapper methods that do nothing but forward all their
6
8
  * arguments to another call.
@@ -34,22 +36,14 @@ const CONTRACT_METHOD_NAMES = new Set(['focus', 'blur', 'reset']);
34
36
  const isSimpleParam = (param) => param.type === 'Identifier';
35
37
 
36
38
  /**
37
- * Whether the class holding this method is an Angular component or directive.
38
- * @param {import('eslint').Rule.Node} methodNode MethodDefinition node
39
+ * @param {import('eslint').SourceCode} sourceCode
40
+ * @param {any} methodNode
39
41
  */
40
- const isInAngularClass = (methodNode) => {
41
- const classNode = methodNode.parent?.parent;
42
- const decorators = classNode && 'decorators' in classNode ? classNode.decorators : null;
43
-
44
- if (!decorators) return false;
45
-
46
- return decorators.some((decorator) => {
47
- const expression = decorator.expression;
48
- const callee = expression.type === 'CallExpression' ? expression.callee : expression;
49
-
50
- return callee.type === 'Identifier' && (callee.name === 'Component' || callee.name === 'Directive');
51
- });
52
- };
42
+ const isInAngularClass = (sourceCode, methodNode) =>
43
+ methodNode.parent?.parent?.decorators?.some((/** @type {any} */ decorator) => {
44
+ const name = getAngularDecoratorName(sourceCode, decorator);
45
+ return name === 'Component' || name === 'Directive';
46
+ }) ?? false;
53
47
 
54
48
  /**
55
49
  * Returns the single CallExpression in the method body when the method is a
@@ -127,7 +121,7 @@ const noTrivialWrapperMethod = {
127
121
  const classNode = node.parent?.parent;
128
122
  if (node.override || classNode?.superClass || classNode?.implements?.length) return;
129
123
  if (node.static && methodName === 'ngTemplateContextGuard') return;
130
- if (CONTRACT_METHOD_NAMES.has(methodName) && isInAngularClass(node)) return;
124
+ if (CONTRACT_METHOD_NAMES.has(methodName) && isInAngularClass(context.sourceCode, node)) return;
131
125
 
132
126
  const callee = callExpr.callee;
133
127
  const targetName =
@@ -1,6 +1,8 @@
1
1
  // @ts-check
2
2
  'use strict';
3
3
 
4
+ const { isImportedAs } = require('./internals/import-resolution');
5
+
4
6
  /** @type {import('eslint').Rule.RuleModule} */
5
7
  const noTypedInjectedElementRef = {
6
8
  meta: {
@@ -20,30 +22,25 @@ const noTypedInjectedElementRef = {
20
22
  create(context) {
21
23
  return {
22
24
  CallExpression(node) {
23
- if (node.callee.type !== 'Identifier' || node.callee.name !== 'inject') {
24
- return;
25
- }
25
+ const src = context.sourceCode;
26
+ if (!isImportedAs(src, node.callee, 'inject')) return;
26
27
 
27
28
  const firstArg = node.arguments[0];
28
29
  if (!firstArg) return;
29
30
 
30
- const isPlainElementRef = firstArg.type === 'Identifier' && firstArg.name === 'ElementRef';
31
+ const isPlainElementRef = isImportedAs(src, firstArg, 'ElementRef');
31
32
  const isInstantiatedElementRef =
32
- firstArg.type === 'TSInstantiationExpression' &&
33
- firstArg.expression &&
34
- firstArg.expression.type === 'Identifier' &&
35
- firstArg.expression.name === 'ElementRef';
33
+ firstArg.type === 'TSInstantiationExpression' && isImportedAs(src, firstArg.expression, 'ElementRef');
36
34
 
37
35
  if (!isPlainElementRef && !isInstantiatedElementRef) return;
38
36
 
39
- const src = context.sourceCode;
37
+ const elementRef = src.getText(isPlainElementRef ? firstArg : firstArg.expression);
40
38
  const existingTypeParams = node.typeParameters || node.typeArguments;
41
39
  const injectType = existingTypeParams?.params?.[0];
42
40
  const hasTypedElementRef =
43
41
  existingTypeParams?.params?.length === 1 &&
44
42
  injectType?.type === 'TSTypeReference' &&
45
- injectType.typeName?.type === 'Identifier' &&
46
- injectType.typeName.name === 'ElementRef' &&
43
+ src.getText(injectType.typeName) === elementRef &&
47
44
  (injectType.typeArguments || injectType.typeParameters)?.params?.length > 0;
48
45
 
49
46
  if (isPlainElementRef && !hasTypedElementRef) {
@@ -53,8 +50,8 @@ const noTypedInjectedElementRef = {
53
50
  messageId: 'missingGeneric',
54
51
  fix(fixer) {
55
52
  return existingTypeParams
56
- ? fixer.replaceText(existingTypeParams, '<ElementRef<HTMLElement>>')
57
- : fixer.insertTextAfter(node.callee, '<ElementRef<HTMLElement>>');
53
+ ? fixer.replaceText(existingTypeParams, `<${elementRef}<HTMLElement>>`)
54
+ : fixer.insertTextAfter(node.callee, `<${elementRef}<HTMLElement>>`);
58
55
  },
59
56
  });
60
57
  return;
@@ -71,9 +68,9 @@ const noTypedInjectedElementRef = {
71
68
  messageId: 'missingGeneric',
72
69
  fix(fixer) {
73
70
  const typeFix = existingTypeParams
74
- ? fixer.replaceText(existingTypeParams, `<ElementRef${tokenTypeParamText}>`)
75
- : fixer.insertTextAfter(node.callee, `<ElementRef${tokenTypeParamText}>`);
76
- return [typeFix, fixer.replaceText(firstArg, 'ElementRef')];
71
+ ? fixer.replaceText(existingTypeParams, `<${elementRef}${tokenTypeParamText}>`)
72
+ : fixer.insertTextAfter(node.callee, `<${elementRef}${tokenTypeParamText}>`);
73
+ return [typeFix, fixer.replaceText(firstArg, elementRef)];
77
74
  },
78
75
  });
79
76
  } else {
@@ -82,7 +79,7 @@ const noTypedInjectedElementRef = {
82
79
  node,
83
80
  messageId: 'missingGeneric',
84
81
  fix(fixer) {
85
- return fixer.replaceText(firstArg, 'ElementRef');
82
+ return fixer.replaceText(firstArg, elementRef);
86
83
  },
87
84
  });
88
85
  }
@@ -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 are declared but never read within the class body.
6
8
  *
@@ -42,22 +44,17 @@ const extractIdentifiers = (str) => str.match(/\b[a-zA-Z_$][a-zA-Z0-9_$]*\b/g) ?
42
44
 
43
45
  /**
44
46
  * Returns info about the Angular decorators applied to a class node.
47
+ * @param {import('eslint').SourceCode} sourceCode
45
48
  * @param {any} classNode
46
49
  * @returns {{ isComponent: boolean, isAngularClass: boolean }}
47
50
  */
48
- const getDecoratorInfo = (classNode) => {
51
+ const getDecoratorInfo = (sourceCode, classNode) => {
49
52
  const decorators = classNode.decorators ?? [];
50
53
  let isComponent = false;
51
54
  let isAngularClass = false;
52
55
 
53
56
  for (const dec of decorators) {
54
- const expr = dec.expression;
55
- const name =
56
- expr?.type === 'CallExpression' && expr.callee?.type === 'Identifier'
57
- ? expr.callee.name
58
- : expr?.type === 'Identifier'
59
- ? expr.name
60
- : null;
57
+ const name = getAngularDecoratorName(sourceCode, dec);
61
58
 
62
59
  if (name && ANGULAR_DECORATORS.has(name)) {
63
60
  isAngularClass = true;
@@ -167,7 +164,7 @@ const noUnusedClassMember = {
167
164
  return {
168
165
  ClassBody(node) {
169
166
  const classNode = node.parent;
170
- const { isComponent, isAngularClass } = getDecoratorInfo(classNode);
167
+ const { isComponent, isAngularClass } = getDecoratorInfo(context.sourceCode, classNode);
171
168
  const hostBound = getHostBoundMembers(classNode);
172
169
  classStack.push({
173
170
  classBody: node,
@@ -25,6 +25,8 @@
25
25
  * import { clone, equal } from '@ethlete/core';
26
26
  */
27
27
 
28
+ const { MODULE_REFERENCE_SELECTOR, getModuleReference, isGlobalReference } = require('./internals/import-resolution');
29
+
28
30
  /** @type {import('eslint').Rule.RuleModule} */
29
31
  const preferCloneEqual = {
30
32
  meta: {
@@ -70,21 +72,21 @@ const preferCloneEqual = {
70
72
  }
71
73
 
72
74
  // ── structuredClone(expr) ─────────────────────────────────────────────
73
- if (callee.type === 'Identifier' && callee.name === 'structuredClone') {
75
+ if (isGlobalReference(context.sourceCode, callee) && callee.name === 'structuredClone') {
74
76
  context.report({ node, messageId: 'preferClone', data: { method: 'structuredClone()' } });
75
77
  }
76
78
  },
77
79
 
78
- ImportDeclaration(node) {
79
- const src = node.source.value;
80
+ [MODULE_REFERENCE_SELECTOR](node) {
81
+ const reference = getModuleReference(node);
82
+ if (!reference) return;
83
+ const src = reference.source;
80
84
 
81
85
  // ── lodash cloneDeep / isEqual ────────────────────────────────────────
82
86
  // import { cloneDeep } from 'lodash' / 'lodash-es'
83
87
  // import { isEqual } from 'lodash' / 'lodash-es'
84
88
  if (src === 'lodash' || src === 'lodash-es') {
85
- for (const specifier of node.specifiers) {
86
- if (specifier.type !== 'ImportSpecifier') continue;
87
- const name = specifier.imported.name;
89
+ for (const { name } of reference.named) {
88
90
  if (name === 'cloneDeep') {
89
91
  context.report({ node, messageId: 'preferClone', data: { method: 'lodash cloneDeep' } });
90
92
  } else if (name === 'isEqual') {