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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/migrations.json +12 -0
  3. package/package.json +13 -3
  4. package/src/configs/recommended.js +33 -11
  5. package/src/generators/generators.json +10 -0
  6. package/src/generators/restore-module-augmentation-interfaces/migration.js +50 -0
  7. package/src/generators/restore-module-augmentation-interfaces/restore-module-augmentation-interfaces.js +103 -0
  8. package/src/generators/restore-module-augmentation-interfaces/schema.json +14 -0
  9. package/src/index.js +10 -2
  10. package/src/rules/angular-decorator-property-order.js +20 -20
  11. package/src/rules/class-constant-property.js +8 -14
  12. package/src/rules/class-member-order.js +77 -46
  13. package/src/rules/consistent-type-definitions.js +60 -0
  14. package/src/rules/inject-member-accessibility.js +5 -3
  15. package/src/rules/internals/angular-io.js +12 -13
  16. package/src/rules/internals/angular-member-visibility.js +11 -7
  17. package/src/rules/internals/import-resolution.js +191 -0
  18. package/src/rules/no-angular-router-api.js +23 -29
  19. package/src/rules/no-angular-seo-services.js +14 -9
  20. package/src/rules/no-cdk-import.js +7 -14
  21. package/src/rules/no-csp-unsafe.js +362 -0
  22. package/src/rules/no-document-cookie.js +3 -0
  23. package/src/rules/no-effect-cleanup-return.js +7 -3
  24. package/src/rules/no-empty-angular-metadata-arrays.js +3 -17
  25. package/src/rules/no-inject-chain.js +3 -1
  26. package/src/rules/no-leading-underscore-class-member.js +26 -3
  27. package/src/rules/no-legacy-angular-decorators.js +9 -21
  28. package/src/rules/no-legacy-query-import.js +6 -15
  29. package/src/rules/no-locale-id.js +4 -4
  30. package/src/rules/no-member-alias.js +11 -7
  31. package/src/rules/no-native-html-input-name.js +1 -1
  32. package/src/rules/no-pipe-logic.js +10 -17
  33. package/src/rules/no-redundant-on-push-change-detection.js +9 -23
  34. package/src/rules/no-rxjs-in-effect.js +12 -8
  35. package/src/rules/no-screaming-case-local.js +1 -0
  36. package/src/rules/no-standalone-flag.js +3 -17
  37. package/src/rules/no-subscribe-in-pipe.js +3 -2
  38. package/src/rules/no-template-literal-before-inline-template.js +3 -14
  39. package/src/rules/no-trivial-return-type.js +1 -0
  40. package/src/rules/no-trivial-wrapper-method.js +10 -16
  41. package/src/rules/no-typed-injected-element-ref.js +14 -17
  42. package/src/rules/no-unused-class-member.js +6 -9
  43. package/src/rules/prefer-clone-equal.js +8 -6
  44. package/src/rules/prefer-concise-angular-host-directives.js +23 -29
  45. package/src/rules/prefer-concise-angular-style-metadata.js +8 -16
  46. package/src/rules/prefer-element-dimensions.js +9 -10
  47. package/src/rules/prefer-linked-signal.js +42 -41
  48. package/src/rules/prefer-match-media.js +10 -14
  49. package/src/rules/prefer-present-tense-output.js +1 -1
  50. package/src/rules/prefer-rxjs-timer.js +3 -1
  51. package/src/rules/require-dollar-suffix.js +7 -4
  52. package/src/rules/require-on-push-change-detection.js +17 -25
  53. package/src/rules/require-view-encapsulation-none.js +17 -25
  54. package/src/rules/take-until-destroyed-last.js +64 -0
  55. package/src/rules/template-member-accessibility.js +8 -6
@@ -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) {
@@ -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 =