@ethlete/eslint-plugin 1.0.0-next.25 → 1.0.0-next.26

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 (54) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +2 -1
  3. package/src/configs/recommended.js +7 -16
  4. package/src/rules/angular-decorator-property-order.js +24 -7
  5. package/src/rules/class-constant-property.js +1 -1
  6. package/src/rules/class-member-order.js +60 -8
  7. package/src/rules/enforce-routing-view-naming.js +34 -23
  8. package/src/rules/guard-return-newline.js +4 -7
  9. package/src/rules/internals/angular-member-visibility.js +24 -2
  10. package/src/rules/internals/angular-metadata-fix.js +231 -1
  11. package/src/rules/internals/angular-version.js +50 -0
  12. package/src/rules/internals/implemented-contract-members.js +26 -11
  13. package/src/rules/no-angular-router-api.js +86 -33
  14. package/src/rules/no-angular-seo-services.js +1 -0
  15. package/src/rules/no-async-await.js +5 -1
  16. package/src/rules/no-cdk-import.js +32 -7
  17. package/src/rules/no-direct-dom-manipulation.js +50 -28
  18. package/src/rules/no-document-cookie.js +51 -8
  19. package/src/rules/no-dom-query.js +5 -2
  20. package/src/rules/no-empty-angular-metadata-arrays.js +2 -0
  21. package/src/rules/no-empty-newlines-between-imports.js +4 -1
  22. package/src/rules/no-impure-top-level-provider.js +42 -7
  23. package/src/rules/no-leading-underscore-class-member.js +27 -2
  24. package/src/rules/no-legacy-angular-decorators.js +11 -6
  25. package/src/rules/no-legacy-prepare-without-injector.js +57 -12
  26. package/src/rules/no-legacy-query-import.js +61 -16
  27. package/src/rules/no-member-alias.js +0 -5
  28. package/src/rules/no-native-observers.js +89 -24
  29. package/src/rules/no-pipe-logic.js +1 -2
  30. package/src/rules/no-readonly-signal.js +1 -5
  31. package/src/rules/no-redundant-internal.js +5 -2
  32. package/src/rules/no-redundant-on-push-change-detection.js +3 -48
  33. package/src/rules/no-rxjs-in-effect.js +7 -5
  34. package/src/rules/no-screaming-case-local.js +3 -5
  35. package/src/rules/no-standalone-flag.js +1 -0
  36. package/src/rules/no-subscribe-with-body.js +30 -1
  37. package/src/rules/no-template-literal-before-inline-template.js +6 -0
  38. package/src/rules/no-trivial-return-type.js +18 -1
  39. package/src/rules/no-unused-class-member.js +1 -1
  40. package/src/rules/no-window-location.js +60 -22
  41. package/src/rules/prefer-clone-equal.js +61 -8
  42. package/src/rules/prefer-concise-angular-host-directives.js +39 -11
  43. package/src/rules/prefer-concise-angular-style-metadata.js +10 -2
  44. package/src/rules/prefer-element-dimensions.js +264 -11
  45. package/src/rules/prefer-match-media.js +17 -3
  46. package/src/rules/prefer-rxjs-timer.js +26 -4
  47. package/src/rules/prefer-scroll-state.js +0 -4
  48. package/src/rules/prefer-static-boolean-properties.js +1 -0
  49. package/src/rules/prefer-viewport-size.js +153 -16
  50. package/src/rules/require-dollar-suffix.js +11 -3
  51. package/src/rules/require-form-submit.js +4 -1
  52. package/src/rules/require-on-push-change-detection.js +7 -259
  53. package/src/rules/require-view-encapsulation-none.js +6 -214
  54. package/src/rules/template-member-accessibility.js +12 -4
@@ -92,6 +92,56 @@ const noLegacyQueryImport = {
92
92
  const options = context.options[0] ?? {};
93
93
  const docsBaseUrl = (options.docsBaseUrl ?? DEFAULT_DOCS_BASE_URL).replace(/\/$/, '');
94
94
 
95
+ /**
96
+ * @param {string} name
97
+ * @param {any} node
98
+ */
99
+ const reportName = (name, node) => {
100
+ const successor =
101
+ (Object.hasOwn(V2_SUCCESSORS, name) ? V2_SUCCESSORS[name] : null) ??
102
+ (Object.hasOwn(LEGACY_SYMBOLS, name) ? LEGACY_SYMBOLS[name] : null);
103
+
104
+ if (successor) {
105
+ context.report({
106
+ node,
107
+ messageId: 'successor',
108
+ data: { name, to: successor.to, docs: `${docsBaseUrl}${successor.docs}` },
109
+ });
110
+
111
+ return;
112
+ }
113
+
114
+ if (!isV2Symbol(name)) return;
115
+
116
+ context.report({
117
+ node,
118
+ messageId: 'legacySystem',
119
+ data: { name, docs: `${docsBaseUrl}/query/migrating-from-v2` },
120
+ });
121
+ };
122
+
123
+ /**
124
+ * @param {any} identifier
125
+ * @returns {{ name: string, node: any } | null}
126
+ */
127
+ const getNamespaceMember = (identifier) => {
128
+ const parent = identifier.parent;
129
+
130
+ if (parent?.type === 'MemberExpression' && parent.object === identifier) {
131
+ if (!parent.computed && parent.property.type === 'Identifier')
132
+ return { name: parent.property.name, node: parent };
133
+ if (parent.property.type === 'Literal' && typeof parent.property.value === 'string') {
134
+ return { name: parent.property.value, node: parent };
135
+ }
136
+ }
137
+
138
+ if (parent?.type === 'TSQualifiedName' && parent.left === identifier) {
139
+ return { name: parent.right.name, node: parent };
140
+ }
141
+
142
+ return null;
143
+ };
144
+
95
145
  return {
96
146
  [MODULE_REFERENCE_SELECTOR](node) {
97
147
  const reference = getModuleReference(node);
@@ -99,25 +149,20 @@ const noLegacyQueryImport = {
99
149
  if (reference?.source !== QUERY_PACKAGE) return;
100
150
 
101
151
  for (const { name, node: specifier } of reference.named) {
102
- const successor = V2_SUCCESSORS[name] ?? LEGACY_SYMBOLS[name];
103
-
104
- if (successor) {
105
- context.report({
106
- node: specifier,
107
- messageId: 'successor',
108
- data: { name, to: successor.to, docs: `${docsBaseUrl}${successor.docs}` },
109
- });
152
+ reportName(name, specifier);
153
+ }
110
154
 
111
- continue;
112
- }
155
+ if (node.type !== 'ImportDeclaration') return;
113
156
 
114
- if (!isV2Symbol(name)) continue;
157
+ for (const specifier of node.specifiers) {
158
+ if (specifier.type !== 'ImportNamespaceSpecifier') continue;
115
159
 
116
- context.report({
117
- node: specifier,
118
- messageId: 'legacySystem',
119
- data: { name, docs: `${docsBaseUrl}/query/migrating-from-v2` },
120
- });
160
+ for (const variable of context.sourceCode.getDeclaredVariables(specifier)) {
161
+ for (const variableReference of variable.references) {
162
+ const member = getNamespaceMember(variableReference.identifier);
163
+ if (member) reportName(member.name, member.node);
164
+ }
165
+ }
121
166
  }
122
167
  },
123
168
  };
@@ -63,11 +63,9 @@ const getMethodAliasInfo = (node) => {
63
63
  }
64
64
  if (!callExpr || callExpr.type !== 'CallExpression') return null;
65
65
 
66
- // Must have zero arguments
67
66
  if (callExpr.arguments.length !== 0) return null;
68
67
 
69
68
  const callee = callExpr.callee;
70
- // callee must be: this.something.method
71
69
  if (callee.type !== 'MemberExpression') return null;
72
70
  if (callee.computed) return null;
73
71
  if (callee.property.type !== 'Identifier') return null;
@@ -115,14 +113,11 @@ const noMemberAlias = {
115
113
  const value = node.value;
116
114
  if (!value) return;
117
115
 
118
- // Must be: this.something.property
119
116
  if (value.type !== 'MemberExpression') return;
120
- // Outer property must be a plain identifier (not computed)
121
117
  if (value.computed) return;
122
118
  if (value.property.type !== 'Identifier') return;
123
119
 
124
120
  const obj = value.object;
125
- // Object must itself be: this.something
126
121
  if (obj.type !== 'MemberExpression') return;
127
122
  if (obj.computed) return;
128
123
  if (obj.object.type !== 'ThisExpression') return;
@@ -31,6 +31,8 @@
31
31
  * signalHostElementDimensions()
32
32
  */
33
33
 
34
+ const { isGlobalReference } = require('./internals/import-resolution');
35
+
34
36
  /**
35
37
  * Maps Observer constructor names to their @ethlete/core signal equivalents.
36
38
  * @type {Record<string, { alternatives: string[], from: string } | null>}
@@ -51,6 +53,8 @@ const OBSERVER_ALTERNATIVES = {
51
53
  PerformanceObserver: null,
52
54
  };
53
55
 
56
+ const GLOBAL_OBJECTS = new Set(['window', 'globalThis', 'self']);
57
+
54
58
  /** @type {import('eslint').Rule.RuleModule} */
55
59
  const noNativeObservers = {
56
60
  meta: {
@@ -68,35 +72,96 @@ const noNativeObservers = {
68
72
  schema: [],
69
73
  },
70
74
  create(context) {
71
- return {
72
- NewExpression(node) {
73
- if (node.callee.type !== 'Identifier') return;
75
+ const { sourceCode } = context;
76
+
77
+ /**
78
+ * @param {any} node
79
+ * @param {Set<any>} seen
80
+ * @returns {string | null}
81
+ */
82
+ const resolveObserverName = (node, seen = new Set()) => {
83
+ if (!node || seen.has(node)) return null;
84
+ seen.add(node);
85
+
86
+ if (node.type === 'TSNonNullExpression' || node.type === 'TSAsExpression') {
87
+ return resolveObserverName(node.expression, seen);
88
+ }
74
89
 
75
- const name = node.callee.name;
90
+ if (node.type === 'MemberExpression') {
91
+ if (node.computed || node.property.type !== 'Identifier') return null;
92
+ if (!Object.hasOwn(OBSERVER_ALTERNATIVES, node.property.name)) return null;
76
93
 
94
+ return node.object.type === 'Identifier' &&
95
+ GLOBAL_OBJECTS.has(node.object.name) &&
96
+ isGlobalReference(sourceCode, node.object)
97
+ ? node.property.name
98
+ : null;
99
+ }
100
+
101
+ if (node.type !== 'Identifier') return null;
102
+
103
+ if (isGlobalReference(sourceCode, node)) {
77
104
  // hasOwn, not `in`: `new constructor()` / `new toString()` would otherwise match an
78
105
  // inherited Object.prototype key and read a function where an entry is expected
79
- if (!Object.hasOwn(OBSERVER_ALTERNATIVES, name)) return;
80
-
81
- const info = OBSERVER_ALTERNATIVES[name];
82
-
83
- if (info) {
84
- context.report({
85
- node,
86
- messageId: 'useSignalUtil',
87
- data: {
88
- observer: name,
89
- alternatives: info.alternatives.join(' or '),
90
- from: info.from,
91
- },
92
- });
93
- } else {
94
- context.report({
95
- node,
96
- messageId: 'avoidObserver',
97
- data: { observer: name },
98
- });
106
+ return Object.hasOwn(OBSERVER_ALTERNATIVES, node.name) ? node.name : null;
107
+ }
108
+
109
+ /** @type {import('eslint').Scope.Scope | null} */
110
+ let scope = sourceCode.getScope(node);
111
+
112
+ while (scope) {
113
+ const variable = scope.set.get(node.name);
114
+
115
+ if (variable) {
116
+ const definition = /** @type {any} */ (variable.defs[0]);
117
+
118
+ return definition?.type === 'Variable' && definition.parent.kind === 'const'
119
+ ? resolveObserverName(definition.node.init, seen)
120
+ : null;
99
121
  }
122
+
123
+ scope = scope.upper;
124
+ }
125
+
126
+ return null;
127
+ };
128
+
129
+ /**
130
+ * @param {any} node
131
+ * @param {any} callee
132
+ */
133
+ const check = (node, callee) => {
134
+ const name = resolveObserverName(callee);
135
+
136
+ if (!name) return;
137
+
138
+ const info = OBSERVER_ALTERNATIVES[name];
139
+
140
+ if (info) {
141
+ context.report({
142
+ node,
143
+ messageId: 'useSignalUtil',
144
+ data: {
145
+ observer: name,
146
+ alternatives: info.alternatives.join(' or '),
147
+ from: info.from,
148
+ },
149
+ });
150
+ } else {
151
+ context.report({
152
+ node,
153
+ messageId: 'avoidObserver',
154
+ data: { observer: name },
155
+ });
156
+ }
157
+ };
158
+
159
+ return {
160
+ NewExpression(node) {
161
+ check(node, node.callee);
162
+ },
163
+ 'ClassDeclaration, ClassExpression'(node) {
164
+ if (node.superClass) check(node.superClass, node.superClass);
100
165
  },
101
166
  };
102
167
  },
@@ -19,6 +19,7 @@ const noPipeLogic = {
19
19
  docs: {
20
20
  description:
21
21
  'Pipes must not contain logic in the transform method. Extract to a utility function and assign it: `transform = myUtil;`',
22
+ recommended: true,
22
23
  },
23
24
  messages: {
24
25
  noLogicInTransform:
@@ -28,7 +29,6 @@ const noPipeLogic = {
28
29
  },
29
30
  create(context) {
30
31
  return {
31
- // Case 1: transform() { ... } — method with a body
32
32
  MethodDefinition(node) {
33
33
  const key = /** @type {any} */ (node).key;
34
34
  if (key.type !== 'Identifier' || key.name !== 'transform') return;
@@ -45,7 +45,6 @@ const noPipeLogic = {
45
45
  context.report({ node, messageId: 'noLogicInTransform' });
46
46
  },
47
47
 
48
- // Case 2: transform = () => { ... } or transform = function() { ... }
49
48
  PropertyDefinition(node) {
50
49
  const key = /** @type {any} */ (node).key;
51
50
  if (key.type !== 'Identifier' || key.name !== 'transform') return;
@@ -77,7 +77,6 @@ const noReadonlySignal = {
77
77
  create(context) {
78
78
  return {
79
79
  PropertyDefinition(node) {
80
- // Only flag when readonly is set
81
80
  if (!node.readonly) return;
82
81
 
83
82
  const init = node.value;
@@ -86,12 +85,9 @@ const noReadonlySignal = {
86
85
  const { callee } = init;
87
86
  let apiName = null;
88
87
 
89
- // Simple call: signal(), input(), computed(), inject(), etc.
90
88
  if (callee.type === 'Identifier' && (REACTIVE_APIS.has(callee.name) || callee.name.startsWith('inject'))) {
91
89
  apiName = callee.name;
92
- }
93
- // Member call: input.required(), outputFromObservable.something(), etc.
94
- else if (
90
+ } else if (
95
91
  callee.type === 'MemberExpression' &&
96
92
  callee.object.type === 'Identifier' &&
97
93
  REACTIVE_APIS.has(callee.object.name)
@@ -28,6 +28,7 @@ const getAttachedInternalComment = (sourceCode, node) => {
28
28
  const getMemberName = (node) => {
29
29
  if (node.key?.type === 'Identifier') return node.key.name;
30
30
  if (node.key?.type === 'Literal' && typeof node.key.value === 'string') return node.key.value;
31
+ if (node.key?.type === 'PrivateIdentifier') return `#${node.key.name}`;
31
32
 
32
33
  return 'member';
33
34
  };
@@ -87,7 +88,9 @@ const noRedundantInternal = {
87
88
  return;
88
89
  }
89
90
 
90
- if (node.accessibility !== 'private') {
91
+ const isPrivateIdentifier = node.key?.type === 'PrivateIdentifier';
92
+
93
+ if (node.accessibility !== 'private' && !isPrivateIdentifier) {
91
94
  return;
92
95
  }
93
96
 
@@ -95,7 +98,7 @@ const noRedundantInternal = {
95
98
  node: internalComment,
96
99
  messageId: 'redundantInternal',
97
100
  data: {
98
- accessibility: node.accessibility,
101
+ accessibility: 'private',
99
102
  kind: getMemberKind(node),
100
103
  name: getMemberName(node),
101
104
  },
@@ -3,7 +3,8 @@
3
3
 
4
4
  const { getAngularDecoratorName, isImportedAs } = require('./internals/import-resolution');
5
5
 
6
- const { getMetadataEntryRemovalRange } = require('./internals/angular-metadata-fix');
6
+ const { getMetadataEntryRemovalRange, getPropertyName } = require('./internals/angular-metadata-fix');
7
+ const { getAngularMajor } = require('./internals/angular-version');
7
8
 
8
9
  /**
9
10
  * Disallow explicit `changeDetection: ChangeDetectionStrategy.OnPush` on
@@ -24,53 +25,6 @@ const { getMetadataEntryRemovalRange } = require('./internals/angular-metadata-f
24
25
 
25
26
  const REQUIRED_MAJOR = 22;
26
27
 
27
- /** @type {number | null | undefined} */
28
- let cachedAngularMajor;
29
-
30
- /**
31
- * Detect the workspace's installed Angular major version. Cached across files.
32
- *
33
- * @returns {number | null} major version, or null when it cannot be determined
34
- */
35
- const detectAngularMajor = () => {
36
- if (cachedAngularMajor !== undefined) return cachedAngularMajor;
37
-
38
- try {
39
- const pkg = require('@angular/core/package.json');
40
- const major = Number.parseInt(String(pkg.version).split('.')[0], 10);
41
- cachedAngularMajor = Number.isNaN(major) ? null : major;
42
- } catch {
43
- cachedAngularMajor = null;
44
- }
45
-
46
- return cachedAngularMajor;
47
- };
48
-
49
- /**
50
- * Resolve the Angular major version to gate on. An explicit
51
- * `settings.ethlete.angularMajor` wins over auto-detection — handy for
52
- * pinning behaviour and for tests.
53
- *
54
- * @param {import('eslint').Rule.RuleContext} context
55
- * @returns {number | null}
56
- */
57
- const getAngularMajor = (context) => {
58
- const settings = /** @type {any} */ (context.settings);
59
- const override = settings?.ethlete?.angularMajor;
60
- if (typeof override === 'number') return override;
61
-
62
- return detectAngularMajor();
63
- };
64
-
65
- /**
66
- * @param {import('estree').Property['key']} key
67
- */
68
- const getPropertyName = (key) => {
69
- if (key.type === 'Identifier') return key.name;
70
- if (key.type === 'Literal' && typeof key.value === 'string') return key.value;
71
- return null;
72
- };
73
-
74
28
  /**
75
29
  * @param {import('eslint').SourceCode} sourceCode
76
30
  * @param {any} value
@@ -127,6 +81,7 @@ const noRedundantOnPushChangeDetection = {
127
81
  docs: {
128
82
  description:
129
83
  'Disallow redundant `changeDetection: ChangeDetectionStrategy.OnPush`. OnPush is the default since Angular 22.',
84
+ recommended: true,
130
85
  },
131
86
  fixable: 'code',
132
87
  schema: [],
@@ -4,7 +4,7 @@
4
4
  const { ANGULAR_CORE, getImportedName } = require('./internals/import-resolution');
5
5
 
6
6
  /**
7
- * Disallows calling .subscribe() inside Angular's effect() or computed() callbacks.
7
+ * Disallows calling .subscribe() inside Angular's effect(), computed(), afterRenderEffect() or linkedSignal() callbacks.
8
8
  *
9
9
  * This creates unmanaged subscriptions and causes memory leaks because neither
10
10
  * effect() nor computed() provides a lifecycle-tied cleanup mechanism for subscriptions.
@@ -24,6 +24,8 @@ const { ANGULAR_CORE, getImportedName } = require('./internals/import-resolution
24
24
  * .subscribe();
25
25
  */
26
26
 
27
+ const REACTIVE_CONTEXTS = new Set(['effect', 'computed', 'afterRenderEffect', 'linkedSignal']);
28
+
27
29
  /**
28
30
  * @param {any} node
29
31
  */
@@ -34,7 +36,8 @@ const noRxjsInEffect = {
34
36
  meta: {
35
37
  type: 'problem',
36
38
  docs: {
37
- description: 'Disallow .subscribe() inside effect() or computed() callbacks.',
39
+ description:
40
+ 'Disallow .subscribe() inside effect(), computed(), afterRenderEffect() or linkedSignal() callbacks.',
38
41
  recommended: true,
39
42
  },
40
43
  messages: {
@@ -45,8 +48,7 @@ const noRxjsInEffect = {
45
48
  },
46
49
  create(context) {
47
50
  /**
48
- * Returns the name of the wrapping reactive context ('effect' | 'computed') if
49
- * the given node is inside one, or null otherwise.
51
+ * Returns the name of the wrapping reactive context if the given node is inside one, or null otherwise.
50
52
  * @param {import('eslint').Rule.Node} node
51
53
  * @returns {string | null}
52
54
  */
@@ -59,7 +61,7 @@ const noRxjsInEffect = {
59
61
  }
60
62
  if (crossedFunctionBoundary && current.type === 'CallExpression') {
61
63
  const calleeName = getImportedName(context.sourceCode, current.callee, ANGULAR_CORE);
62
- if (calleeName === 'effect' || calleeName === 'computed') return calleeName;
64
+ if (calleeName && REACTIVE_CONTEXTS.has(calleeName)) return calleeName;
63
65
  }
64
66
  current = current.parent;
65
67
  }
@@ -2,7 +2,8 @@
2
2
  'use strict';
3
3
 
4
4
  /**
5
- * Disallows SCREAMING_CASE variable names inside function bodies.
5
+ * Disallows SCREAMING_CASE variable names inside function bodies, and SCREAMING_CASE names
6
+ * for function-valued variables at any scope.
6
7
  *
7
8
  * At module level, SCREAMING_CASE is fine for true constants. Inside functions,
8
9
  * all local variables must be camelCase — there is no meaningful distinction
@@ -56,7 +57,7 @@ const noScreamingCaseLocal = {
56
57
  type: 'suggestion',
57
58
  docs: {
58
59
  description:
59
- 'Disallow SCREAMING_CASE variable names inside function bodies. Use camelCase for all local variables.',
60
+ 'Disallow SCREAMING_CASE variable names inside function bodies, and SCREAMING_CASE names for function-valued variables at any scope.',
60
61
  recommended: true,
61
62
  },
62
63
  messages: {
@@ -69,15 +70,12 @@ const noScreamingCaseLocal = {
69
70
  create(context) {
70
71
  return {
71
72
  VariableDeclarator(node) {
72
- // Only flag simple identifier bindings (not destructuring)
73
73
  if (node.id.type !== 'Identifier') return;
74
74
 
75
75
  const { name } = node.id;
76
76
 
77
- // Must match the SCREAMING_CASE pattern
78
77
  if (!SCREAMING_CASE_RE.test(name)) return;
79
78
 
80
- // Only flag inside function bodies
81
79
  if (!isInsideFunction(node)) return;
82
80
  if (node.init?.type === 'ArrowFunctionExpression' || node.init?.type === 'FunctionExpression') return;
83
81
 
@@ -23,6 +23,7 @@ const noStandaloneFlag = {
23
23
  docs: {
24
24
  description:
25
25
  'Disallow standalone metadata on Angular decorators because standalone is the default and should be omitted.',
26
+ recommended: true,
26
27
  },
27
28
  fixable: 'code',
28
29
  schema: [],
@@ -20,6 +20,7 @@ const rule = {
20
20
  type: 'suggestion',
21
21
  docs: {
22
22
  description: 'Require subscribe() to be called with an empty body. Move side effects into tap() inside the pipe.',
23
+ recommended: true,
23
24
  },
24
25
  schema: [],
25
26
  messages: {
@@ -44,6 +45,26 @@ const rule = {
44
45
  return false;
45
46
  };
46
47
 
48
+ /** @param {any} value */
49
+ const isHandlerReference = (value) => {
50
+ if (value.type === 'CallExpression') {
51
+ return (
52
+ value.callee.type === 'MemberExpression' &&
53
+ !value.callee.computed &&
54
+ value.callee.property.type === 'Identifier' &&
55
+ value.callee.property.name === 'bind'
56
+ );
57
+ }
58
+
59
+ return (
60
+ value.type === 'MemberExpression' &&
61
+ value.object.type === 'ThisExpression' &&
62
+ !value.computed &&
63
+ value.property.type === 'Identifier' &&
64
+ !value.property.name.endsWith('$')
65
+ );
66
+ };
67
+
47
68
  return {
48
69
  CallExpression(node) {
49
70
  const callee = /** @type {any} */ (node.callee);
@@ -75,12 +96,20 @@ const rule = {
75
96
  continue;
76
97
  }
77
98
 
99
+ if (isHandlerReference(arg)) {
100
+ context.report({ node, messageId: 'noSubscribeBody' });
101
+ return;
102
+ }
103
+
78
104
  // Observer object: .subscribe({ next: (res) => ..., error: ..., complete: ... })
79
105
  if (arg.type === 'ObjectExpression') {
80
106
  for (const prop of arg.properties) {
81
107
  if (prop.type !== 'Property') continue;
82
108
  const val = /** @type {any} */ (prop.value);
83
- if ((val.type === 'ArrowFunctionExpression' || val.type === 'FunctionExpression') && hasBody(val)) {
109
+ if (
110
+ ((val.type === 'ArrowFunctionExpression' || val.type === 'FunctionExpression') && hasBody(val)) ||
111
+ isHandlerReference(val)
112
+ ) {
84
113
  context.report({ node, messageId: 'noSubscribeBody' });
85
114
  return;
86
115
  }
@@ -112,6 +112,7 @@ const noTemplateLiteralBeforeInlineTemplate = {
112
112
  docs: {
113
113
  description:
114
114
  'Disallow a substituted template literal above an inline component template, which silently disables Angular language service completions for the rest of the file.',
115
+ recommended: true,
115
116
  },
116
117
  schema: [],
117
118
  messages: {
@@ -134,6 +135,11 @@ const noTemplateLiteralBeforeInlineTemplate = {
134
135
  if (node.expressions.length > 0) interpolatedLiteralStarts.push(node.range[0]);
135
136
  },
136
137
 
138
+ /** @param {any} node */
139
+ TSTemplateLiteralType(node) {
140
+ if (node.types.length > 0) interpolatedLiteralStarts.push(node.range[0]);
141
+ },
142
+
137
143
  /** @param {import('eslint').Rule.Node} node */
138
144
  Decorator(node) {
139
145
  const decoratorName = getAngularDecoratorName(context.sourceCode, node);
@@ -108,6 +108,23 @@ const noTrivialReturnType = {
108
108
  return names;
109
109
  };
110
110
 
111
+ /**
112
+ * Whether an identifier is a property name (`a.name`, `{ name: 1 }`) rather than a reference.
113
+ * @param {any} identifier
114
+ */
115
+ const isNonReference = (identifier) => {
116
+ const parent = identifier.parent;
117
+
118
+ if (!parent || parent.computed) return false;
119
+ if (parent.type === 'MemberExpression') return parent.property === identifier;
120
+
121
+ return (
122
+ (parent.type === 'Property' || parent.type === 'PropertyDefinition' || parent.type === 'MethodDefinition') &&
123
+ parent.key === identifier &&
124
+ !parent.shorthand
125
+ );
126
+ };
127
+
111
128
  /**
112
129
  * Whether the function body references the function itself (recursion) — there the
113
130
  * annotation is required, since TypeScript cannot infer a self-dependent return type.
@@ -135,7 +152,7 @@ const noTrivialReturnType = {
135
152
  if (typeof current.type !== 'string') continue;
136
153
 
137
154
  for (const { kind, name } of names) {
138
- if (kind === 'name' && current.type === 'Identifier' && current.name === name) {
155
+ if (kind === 'name' && current.type === 'Identifier' && current.name === name && !isNonReference(current)) {
139
156
  return true;
140
157
  }
141
158
 
@@ -139,7 +139,6 @@ const noUnusedClassMember = {
139
139
  const registerMember = (node) => {
140
140
  const frame = classStack[classStack.length - 1];
141
141
  if (!frame) return;
142
- // Only register members that belong directly to this class body
143
142
  if (node.parent !== frame.classBody) return;
144
143
 
145
144
  if (node.static) return;
@@ -189,6 +188,7 @@ const noUnusedClassMember = {
189
188
 
190
189
  PropertyDefinition: registerMember,
191
190
  MethodDefinition: registerMember,
191
+ AccessorProperty: registerMember,
192
192
 
193
193
  MemberExpression(node) {
194
194
  if (node.object?.type !== 'ThisExpression') return;