@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
@@ -0,0 +1,191 @@
1
+ // @ts-check
2
+ 'use strict';
3
+
4
+ const ANGULAR_CORE = '@angular/core';
5
+
6
+ /**
7
+ * @param {import('eslint').SourceCode} sourceCode
8
+ * @param {any} identifier
9
+ */
10
+ const findVariable = (sourceCode, identifier) => {
11
+ /** @type {import('eslint').Scope.Scope | null} */
12
+ let scope = sourceCode.getScope(identifier);
13
+
14
+ while (scope) {
15
+ const variable = scope.set.get(identifier.name);
16
+ if (variable) return variable;
17
+ scope = scope.upper;
18
+ }
19
+
20
+ return null;
21
+ };
22
+
23
+ /**
24
+ * @param {any} specifier
25
+ */
26
+ const getSpecifierImportedName = (specifier) => {
27
+ if (specifier.type === 'ImportDefaultSpecifier') return 'default';
28
+ if (specifier.type === 'ImportNamespaceSpecifier') return '*';
29
+
30
+ return specifier.imported.name ?? specifier.imported.value;
31
+ };
32
+
33
+ /**
34
+ * What an identifier is bound to: `{ source, name }` for an import (`name` is the exported
35
+ * name, `'*'` for a namespace), `{ source: null, name }` for an undeclared global, and `null`
36
+ * for a local binding.
37
+ *
38
+ * @param {import('eslint').SourceCode} sourceCode
39
+ * @param {any} identifier
40
+ * @returns {{ source: string | null; name: string } | null}
41
+ */
42
+ const resolveIdentifier = (sourceCode, identifier) => {
43
+ const variable = findVariable(sourceCode, identifier);
44
+ if (!variable || variable.defs.length === 0) return { source: null, name: identifier.name };
45
+
46
+ const definition = /** @type {any} */ (variable.defs[0]);
47
+ if (definition.type !== 'ImportBinding') return null;
48
+
49
+ return { source: definition.parent.source.value, name: getSpecifierImportedName(definition.node) };
50
+ };
51
+
52
+ /**
53
+ * The exported name `node` refers to when it is imported from one of `sources`, directly, under
54
+ * an alias, or as `namespace.name`. An undeclared identifier falls back to its own name, so code
55
+ * without an import statement still matches; any other binding yields `null`.
56
+ *
57
+ * @param {import('eslint').SourceCode} sourceCode
58
+ * @param {any} node
59
+ * @param {string | readonly string[]} sources
60
+ * @returns {string | null}
61
+ */
62
+ const getImportedName = (sourceCode, node, sources) => {
63
+ const sourceList = typeof sources === 'string' ? [sources] : sources;
64
+
65
+ if (node?.type === 'Identifier') {
66
+ const resolved = resolveIdentifier(sourceCode, node);
67
+ if (!resolved) return null;
68
+ if (resolved.source === null) return resolved.name;
69
+
70
+ return sourceList.includes(resolved.source) && resolved.name !== '*' ? resolved.name : null;
71
+ }
72
+
73
+ if (
74
+ node?.type === 'MemberExpression' &&
75
+ !node.computed &&
76
+ node.object.type === 'Identifier' &&
77
+ node.property.type === 'Identifier'
78
+ ) {
79
+ const resolved = resolveIdentifier(sourceCode, node.object);
80
+ if (!resolved || resolved.name !== '*' || resolved.source === null) return null;
81
+
82
+ return sourceList.includes(resolved.source) ? node.property.name : null;
83
+ }
84
+
85
+ return null;
86
+ };
87
+
88
+ /**
89
+ * @param {import('eslint').SourceCode} sourceCode
90
+ * @param {any} node
91
+ * @param {string} name
92
+ * @param {string | readonly string[]} [sources]
93
+ */
94
+ const isImportedAs = (sourceCode, node, name, sources = ANGULAR_CORE) =>
95
+ getImportedName(sourceCode, node, sources) === name;
96
+
97
+ /**
98
+ * The `@angular/core` name of a decorator such as `@Component(...)`, `@Cmp(...)` or
99
+ * `@ng.Component(...)`.
100
+ *
101
+ * @param {import('eslint').SourceCode} sourceCode
102
+ * @param {any} decorator
103
+ */
104
+ const getAngularDecoratorName = (sourceCode, decorator) => {
105
+ if (decorator?.type !== 'Decorator') return null;
106
+
107
+ const expression = decorator.expression;
108
+ const target = expression.type === 'CallExpression' ? expression.callee : expression;
109
+
110
+ return getImportedName(sourceCode, target, ANGULAR_CORE);
111
+ };
112
+
113
+ /**
114
+ * The local name of a value import `{ importedName as local } from source`, or `null` when the
115
+ * file does not import it as a value.
116
+ *
117
+ * @param {import('eslint').SourceCode} sourceCode
118
+ * @param {string} source
119
+ * @param {string} importedName
120
+ */
121
+ const getImportLocalName = (sourceCode, source, importedName) => {
122
+ for (const node of sourceCode.ast.body) {
123
+ if (node.type !== 'ImportDeclaration' || node.source.value !== source) continue;
124
+ if (/** @type {any} */ (node).importKind === 'type') continue;
125
+
126
+ for (const specifier of node.specifiers) {
127
+ if (specifier.type !== 'ImportSpecifier' || /** @type {any} */ (specifier).importKind === 'type') continue;
128
+ if (getSpecifierImportedName(specifier) === importedName) return specifier.local.name;
129
+ }
130
+ }
131
+
132
+ return null;
133
+ };
134
+
135
+ /**
136
+ * Whether `identifier` refers to a global such as `document` or `setTimeout`, rather than a local,
137
+ * parameter or import of the same name.
138
+ *
139
+ * @param {import('eslint').SourceCode} sourceCode
140
+ * @param {any} identifier
141
+ */
142
+ const isGlobalReference = (sourceCode, identifier) =>
143
+ identifier?.type === 'Identifier' && resolveIdentifier(sourceCode, identifier)?.source === null;
144
+
145
+ const MODULE_REFERENCE_SELECTOR =
146
+ 'ImportDeclaration, ExportNamedDeclaration[source], ExportAllDeclaration, ImportExpression';
147
+
148
+ /**
149
+ * The module an import, re-export or dynamic `import('…')` references, with the names it takes by
150
+ * name. `export *`, a namespace or default import, and a dynamic import have no named entries.
151
+ *
152
+ * @param {any} node a node matched by `MODULE_REFERENCE_SELECTOR`
153
+ * @returns {{ source: string; named: { name: string; node: any }[] } | null}
154
+ */
155
+ const getModuleReference = (node) => {
156
+ const sourceNode = node.source;
157
+ let source = null;
158
+ if (sourceNode?.type === 'Literal' && typeof sourceNode.value === 'string') source = sourceNode.value;
159
+ if (sourceNode?.type === 'TemplateLiteral' && sourceNode.expressions.length === 0) {
160
+ source = sourceNode.quasis[0].value.cooked;
161
+ }
162
+ if (source === null) return null;
163
+
164
+ /** @type {{ name: string; node: any }[]} */
165
+ const named = [];
166
+ if (node.type === 'ImportDeclaration') {
167
+ for (const specifier of node.specifiers) {
168
+ if (specifier.type !== 'ImportSpecifier') continue;
169
+ named.push({ name: getSpecifierImportedName(specifier), node: specifier });
170
+ }
171
+ }
172
+ if (node.type === 'ExportNamedDeclaration') {
173
+ for (const specifier of node.specifiers) {
174
+ named.push({ name: specifier.local.name ?? specifier.local.value, node: specifier });
175
+ }
176
+ }
177
+
178
+ return { source, named };
179
+ };
180
+
181
+ module.exports = {
182
+ ANGULAR_CORE,
183
+ MODULE_REFERENCE_SELECTOR,
184
+ getAngularDecoratorName,
185
+ getImportedName,
186
+ getImportLocalName,
187
+ getModuleReference,
188
+ isGlobalReference,
189
+ isImportedAs,
190
+ resolveIdentifier,
191
+ };
@@ -35,6 +35,10 @@
35
35
  * injectQueryParamChanges(), injectPathParamChanges()
36
36
  */
37
37
 
38
+ const { MODULE_REFERENCE_SELECTOR, getModuleReference, isImportedAs } = require('./internals/import-resolution');
39
+
40
+ const ANGULAR_ROUTER = '@angular/router';
41
+
38
42
  /**
39
43
  * Router properties that expose route state and must not be read directly.
40
44
  * @type {Map<string, string>}
@@ -66,6 +70,17 @@ const noAngularRouterApi = {
66
70
  schema: [],
67
71
  },
68
72
  create(context) {
73
+ const sourceCode = context.sourceCode;
74
+
75
+ /**
76
+ * @param {any} call
77
+ * @param {string} token
78
+ */
79
+ const isInjectOf = (call, token) =>
80
+ call?.type === 'CallExpression' &&
81
+ isImportedAs(sourceCode, call.callee, 'inject') &&
82
+ isImportedAs(sourceCode, call.arguments[0], token, ANGULAR_ROUTER);
83
+
69
84
  /**
70
85
  * Names of variables/properties assigned from inject(Router).
71
86
  * We track identifiers whose initializer is inject(Router) so we can
@@ -78,28 +93,14 @@ const noAngularRouterApi = {
78
93
  // ── Track inject(Router) bindings ──────────────────────────────────────
79
94
  // const router = inject(Router);
80
95
  VariableDeclarator(node) {
81
- if (
82
- node.init?.type === 'CallExpression' &&
83
- node.init.callee.type === 'Identifier' &&
84
- node.init.callee.name === 'inject' &&
85
- node.init.arguments[0]?.type === 'Identifier' &&
86
- node.init.arguments[0].name === 'Router' &&
87
- node.id.type === 'Identifier'
88
- ) {
96
+ if (isInjectOf(node.init, 'Router') && node.id.type === 'Identifier') {
89
97
  routerBindings.add(node.id.name);
90
98
  }
91
99
  },
92
100
 
93
101
  // ── Track class property assignments: router = inject(Router) ──────────
94
102
  PropertyDefinition(node) {
95
- if (
96
- node.value?.type === 'CallExpression' &&
97
- node.value.callee.type === 'Identifier' &&
98
- node.value.callee.name === 'inject' &&
99
- node.value.arguments[0]?.type === 'Identifier' &&
100
- node.value.arguments[0].name === 'Router' &&
101
- node.key.type === 'Identifier'
102
- ) {
103
+ if (isInjectOf(node.value, 'Router') && node.key.type === 'Identifier') {
103
104
  routerBindings.add(node.key.name);
104
105
  }
105
106
  },
@@ -130,24 +131,17 @@ const noAngularRouterApi = {
130
131
  },
131
132
 
132
133
  // ── ActivatedRoute import ───────────────────────────────────────────────
133
- ImportDeclaration(node) {
134
- if (node.source.value !== '@angular/router') return;
135
- for (const specifier of node.specifiers) {
136
- if (specifier.type !== 'ImportSpecifier') continue;
137
- if (specifier.imported.name === 'ActivatedRoute') {
138
- context.report({ node, messageId: 'noActivatedRoute' });
139
- return;
140
- }
134
+ [MODULE_REFERENCE_SELECTOR](node) {
135
+ const reference = getModuleReference(node);
136
+ if (reference?.source !== ANGULAR_ROUTER) return;
137
+ if (reference.named.some((entry) => entry.name === 'ActivatedRoute')) {
138
+ context.report({ node, messageId: 'noActivatedRoute' });
141
139
  }
142
140
  },
143
141
 
144
142
  // ── inject(ActivatedRoute) ──────────────────────────────────────────────
145
143
  CallExpression(node) {
146
- const { callee } = node;
147
- if (callee.type !== 'Identifier' || callee.name !== 'inject') return;
148
- const arg = node.arguments[0];
149
- if (arg?.type !== 'Identifier') return;
150
- if (arg.name === 'ActivatedRoute') {
144
+ if (isInjectOf(node, 'ActivatedRoute')) {
151
145
  context.report({ node, messageId: 'noActivatedRoute' });
152
146
  }
153
147
  },
@@ -1,6 +1,13 @@
1
1
  // @ts-check
2
2
  'use strict';
3
3
 
4
+ const {
5
+ MODULE_REFERENCE_SELECTOR,
6
+ getImportedName,
7
+ getModuleReference,
8
+ isImportedAs,
9
+ } = require('./internals/import-resolution');
10
+
4
11
  /**
5
12
  * Disallows injecting Angular's `Title` and `Meta` services from
6
13
  * `@angular/platform-browser` and importing them from that module.
@@ -48,11 +55,11 @@ const rule = {
48
55
  */
49
56
  const injectTokenName = (node) => {
50
57
  if (node.type !== 'CallExpression') return null;
51
- if (node.callee.type !== 'Identifier' || node.callee.name !== 'inject') return null;
58
+ if (!isImportedAs(context.sourceCode, node.callee, 'inject')) return null;
52
59
  const firstArg = node.arguments[0];
53
60
  if (!firstArg) return null;
54
- if (firstArg.type === 'Identifier') return firstArg.name;
55
- return null;
61
+
62
+ return getImportedName(context.sourceCode, firstArg, '@angular/platform-browser');
56
63
  };
57
64
 
58
65
  return {
@@ -64,13 +71,11 @@ const rule = {
64
71
  },
65
72
 
66
73
  // Detect: import { Title, Meta } from '@angular/platform-browser'
67
- ImportDeclaration(node) {
68
- const decl = /** @type {any} */ (node);
69
- if (decl.source.value !== '@angular/platform-browser') return;
74
+ [MODULE_REFERENCE_SELECTOR](node) {
75
+ const reference = getModuleReference(node);
76
+ if (reference?.source !== '@angular/platform-browser') return;
70
77
 
71
- for (const specifier of decl.specifiers) {
72
- if (specifier.type !== 'ImportSpecifier') continue;
73
- const imported = specifier.imported.name ?? specifier.imported.value;
78
+ for (const { name: imported, node: specifier } of reference.named) {
74
79
  if (imported === 'Title') context.report({ node: specifier, messageId: 'noImportTitle' });
75
80
  if (imported === 'Meta') context.report({ node: specifier, messageId: 'noImportMeta' });
76
81
  }
@@ -4,6 +4,7 @@
4
4
  const { createRequire } = require('node:module');
5
5
  const path = require('node:path');
6
6
  const fs = require('node:fs');
7
+ const { MODULE_REFERENCE_SELECTOR, getModuleReference } = require('./internals/import-resolution');
7
8
 
8
9
  /**
9
10
  * Disallows importing from `@ethlete/cdk`, the maintenance-mode predecessor of `@ethlete/components`,
@@ -52,13 +53,6 @@ const loadMigrationMap = (mapPath, cwd) => {
52
53
  return map;
53
54
  };
54
55
 
55
- /** @param {any} specifier */
56
- const importedName = (specifier) => {
57
- if (specifier.type !== 'ImportSpecifier') return null;
58
-
59
- return specifier.imported.name ?? specifier.imported.value ?? null;
60
- };
61
-
62
56
  /** @type {import('eslint').Rule.RuleModule} */
63
57
  const noCdkImport = {
64
58
  meta: {
@@ -95,13 +89,13 @@ const noCdkImport = {
95
89
  const entryFor = (name) => map?.[name] ?? null;
96
90
 
97
91
  return {
98
- ImportDeclaration(node) {
99
- const declaration = /** @type {any} */ (node);
100
- const source = declaration.source.value;
92
+ [MODULE_REFERENCE_SELECTOR](node) {
93
+ const reference = getModuleReference(node);
94
+ const source = reference?.source;
101
95
 
102
- if (typeof source !== 'string' || (source !== CDK_PACKAGE && !source.startsWith(`${CDK_PACKAGE}/`))) return;
96
+ if (!reference || (source !== CDK_PACKAGE && !source?.startsWith(`${CDK_PACKAGE}/`))) return;
103
97
 
104
- const named = declaration.specifiers.filter(/** @param {any} s */ (s) => importedName(s));
98
+ const { named } = reference;
105
99
 
106
100
  if (!named.length) {
107
101
  context.report({ node, messageId: 'module', data: { cdk: CDK_PACKAGE, docs: migrationDocs } });
@@ -109,8 +103,7 @@ const noCdkImport = {
109
103
  return;
110
104
  }
111
105
 
112
- for (const specifier of named) {
113
- const name = /** @type {string} */ (importedName(specifier));
106
+ for (const { name, node: specifier } of named) {
114
107
  const entry = entryFor(name);
115
108
 
116
109
  if (!entry) {
@@ -21,6 +21,8 @@
21
21
  * deleteCookie('name');
22
22
  */
23
23
 
24
+ const { isGlobalReference } = require('./internals/import-resolution');
25
+
24
26
  /** @type {import('eslint').Rule.RuleModule} */
25
27
  const noDocumentCookie = {
26
28
  meta: {
@@ -42,6 +44,7 @@ const noDocumentCookie = {
42
44
  if (
43
45
  node.object.type === 'Identifier' &&
44
46
  node.object.name === 'document' &&
47
+ isGlobalReference(context.sourceCode, node.object) &&
45
48
  node.property.type === 'Identifier' &&
46
49
  node.property.name === 'cookie'
47
50
  ) {
@@ -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 returning a cleanup function from an `effect()` callback.
6
8
  *
@@ -115,7 +117,9 @@ const noEffectCleanupReturn = {
115
117
  CallExpression(node) {
116
118
  const { callee } = node;
117
119
 
118
- if (callee.type !== 'Identifier' || !EFFECT_CALLEES.has(callee.name)) {
120
+ const calleeName = getImportedName(context.sourceCode, callee, ANGULAR_CORE);
121
+
122
+ if (!calleeName || !EFFECT_CALLEES.has(calleeName)) {
119
123
  return;
120
124
  }
121
125
 
@@ -130,7 +134,7 @@ const noEffectCleanupReturn = {
130
134
  context.report({
131
135
  node: callback.body,
132
136
  messageId: 'returnedCleanup',
133
- data: { callee: callee.name },
137
+ data: { callee: calleeName },
134
138
  });
135
139
 
136
140
  return;
@@ -163,7 +167,7 @@ const noEffectCleanupReturn = {
163
167
  context.report({
164
168
  node: statement.argument,
165
169
  messageId: 'returnedCleanup',
166
- data: { callee: callee.name },
170
+ data: { callee: calleeName },
167
171
  fix:
168
172
  fixable && openParen
169
173
  ? (fixer) => [
@@ -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'} 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
  const MESSAGE_IDS = {
34
20
  hostDirectives: 'noEmptyHostDirectives',
35
21
  imports: 'noEmptyImports',
@@ -55,7 +41,7 @@ const noEmptyAngularMetadataArrays = {
55
41
  return {
56
42
  /** @param {import('eslint').Rule.Node} node */
57
43
  Decorator(node) {
58
- const decoratorName = getDecoratorName(node);
44
+ const decoratorName = getAngularDecoratorName(context.sourceCode, node);
59
45
  if (decoratorName !== 'Component' && decoratorName !== 'Directive') return;
60
46
 
61
47
  const decorator = /** @type {any} */ (node);
@@ -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 noInjectChain = {
6
8
  meta: {
@@ -19,7 +21,7 @@ const noInjectChain = {
19
21
  return {
20
22
  MemberExpression(node) {
21
23
  const obj = node.object;
22
- if (obj.type === 'CallExpression' && obj.callee.type === 'Identifier' && obj.callee.name === 'inject') {
24
+ if (obj.type === 'CallExpression' && isImportedAs(context.sourceCode, obj.callee, 'inject')) {
23
25
  // Allow inject(X).method() — immediately-invoked patterns like
24
26
  // inject(DestroyRef).onDestroy(...) are intentional Angular idioms.
25
27
  const parent = node.parent;
@@ -16,6 +16,16 @@ const getDirectMemberName = (node) => {
16
16
  return node.key?.type === 'Identifier' ? node.key.name : null;
17
17
  };
18
18
 
19
+ /**
20
+ * @param {any} key
21
+ * @param {boolean} computed
22
+ */
23
+ const getReferencedName = (key, computed) => {
24
+ if (!computed && key?.type === 'Identifier') return key.name;
25
+ if (key?.type === 'Literal' && typeof key.value === 'string') return key.value;
26
+ return null;
27
+ };
28
+
19
29
  /** @type {import('eslint').Rule.RuleModule} */
20
30
  const noLeadingUnderscoreClassMember = {
21
31
  meta: {
@@ -36,6 +46,7 @@ const noLeadingUnderscoreClassMember = {
36
46
  * declaredNames: Set<string>,
37
47
  * membersByName: Map<string, any[]>,
38
48
  * memberReads: any[],
49
+ * untrackedNames: Set<string>,
39
50
  * }>}
40
51
  */
41
52
  const classStack = [];
@@ -66,6 +77,7 @@ const noLeadingUnderscoreClassMember = {
66
77
  declaredNames: new Set(),
67
78
  membersByName: new Map(),
68
79
  memberReads: [],
80
+ untrackedNames: new Set(),
69
81
  });
70
82
  },
71
83
 
@@ -85,6 +97,7 @@ const noLeadingUnderscoreClassMember = {
85
97
  newName.length > 0 &&
86
98
  !frame.declaredNames.has(newName) &&
87
99
  targetNameCounts.get(newName) === 1 &&
100
+ !frame.untrackedNames.has(oldName) &&
88
101
  memberNodes.every((memberNode) => memberNode.accessibility === 'private');
89
102
 
90
103
  for (const memberNode of memberNodes) {
@@ -115,10 +128,20 @@ const noLeadingUnderscoreClassMember = {
115
128
  MemberExpression(node) {
116
129
  const frame = classStack[classStack.length - 1];
117
130
  if (!frame) return;
118
- if (node.object?.type !== 'ThisExpression' || node.computed) return;
119
- if (node.property?.type !== 'Identifier') return;
120
131
 
121
- frame.memberReads.push(node);
132
+ if (node.object?.type === 'ThisExpression' && !node.computed && node.property?.type === 'Identifier') {
133
+ frame.memberReads.push(node);
134
+ return;
135
+ }
136
+
137
+ const name = getReferencedName(node.property, node.computed);
138
+ if (name) frame.untrackedNames.add(name);
139
+ },
140
+
141
+ 'ObjectPattern > Property'(node) {
142
+ const frame = classStack[classStack.length - 1];
143
+ const name = getReferencedName(node.key, node.computed);
144
+ if (frame && name) frame.untrackedNames.add(name);
122
145
  },
123
146
  };
124
147
  },
@@ -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 legacy Angular decorators in favour of their modern signal-based
6
8
  * equivalents and the `host: {}` shorthand.
@@ -25,20 +27,6 @@
25
27
  * → ✅ host: { '[class.active]': 'isActive', '(click)': 'onClick()' }
26
28
  */
27
29
 
28
- /**
29
- * Returns the identifier name of a decorator.
30
- * Works for both `@Foo` (Identifier) and `@Foo(...)` (CallExpression).
31
- *
32
- * @param {any} decorator
33
- * @returns {string | null}
34
- */
35
- const getDecoratorName = (decorator) => {
36
- const expr = decorator.expression;
37
- if (expr.type === 'Identifier') return expr.name;
38
- if (expr.type === 'CallExpression' && expr.callee.type === 'Identifier') return expr.callee.name;
39
- return null;
40
- };
41
-
42
30
  /**
43
31
  * Returns the static string key of a class member, or null for computed keys.
44
32
  *
@@ -72,11 +60,12 @@ const getIndent = (sourceCode, node) => {
72
60
  };
73
61
 
74
62
  /**
63
+ * @param {import('eslint').SourceCode} sourceCode
75
64
  * @param {any} classNode
76
65
  */
77
- const getAngularMetadata = (classNode) => {
66
+ const getAngularMetadata = (sourceCode, classNode) => {
78
67
  for (const decorator of classNode.decorators ?? []) {
79
- const decoratorName = getDecoratorName(decorator);
68
+ const decoratorName = getAngularDecoratorName(sourceCode, decorator);
80
69
  if (decoratorName !== 'Component' && decoratorName !== 'Directive') continue;
81
70
 
82
71
  const expression = decorator.expression;
@@ -106,7 +95,7 @@ const findObjectProperty = (objectExpression, propertyName) =>
106
95
  * @param {string} entryText
107
96
  */
108
97
  const buildObjectTextWithAppendedProperty = (sourceCode, objectExpression, entryText) => {
109
- const properties = objectExpression.properties.filter((property) => property.type === 'Property');
98
+ const properties = objectExpression.properties;
110
99
  const isMultiline = Boolean(
111
100
  objectExpression.loc && objectExpression.loc.start.line !== objectExpression.loc.end.line,
112
101
  );
@@ -138,8 +127,7 @@ ${closingIndent}}`;
138
127
  * @param {any} objectExpression
139
128
  */
140
129
  const hasTrailingComma = (sourceCode, objectExpression) => {
141
- const properties = objectExpression.properties.filter((property) => property.type === 'Property');
142
- const lastProperty = properties[properties.length - 1];
130
+ const lastProperty = objectExpression.properties.at(-1);
143
131
  if (!lastProperty) return false;
144
132
 
145
133
  const tokenAfter = sourceCode.getTokenAfter(lastProperty);
@@ -158,7 +146,7 @@ const buildHostBindingFix = (sourceCode, decoratorNode, memberNode, bindingKey,
158
146
  const classNode = classBody?.parent;
159
147
  if (!classNode) return null;
160
148
 
161
- const metadata = getAngularMetadata(classNode);
149
+ const metadata = getAngularMetadata(sourceCode, classNode);
162
150
  if (!metadata) return null;
163
151
 
164
152
  const hostProperty = findObjectProperty(metadata, 'host');
@@ -312,7 +300,7 @@ const rule = {
312
300
 
313
301
  Decorator(node) {
314
302
  const anyNode = /** @type {any} */ (node);
315
- const decoratorName = getDecoratorName(anyNode);
303
+ const decoratorName = getAngularDecoratorName(context.sourceCode, anyNode);
316
304
  if (!decoratorName) return;
317
305
 
318
306
  // Only act on decorators attached to class members, not to the class itself
@@ -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
 
@@ -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) {