@benjavicente/lint-angular 0.0.1 → 0.0.3

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 (3) hide show
  1. package/README.md +20 -13
  2. package/dist/index.mjs +1554 -258
  3. package/package.json +4 -4
package/dist/index.mjs CHANGED
@@ -23,6 +23,194 @@ function getRange(node) {
23
23
  if (typeof node.start === "number" && typeof node.end === "number") return [node.start, node.end];
24
24
  return null;
25
25
  }
26
+ function getNodeStart(node) {
27
+ if (typeof node.start === "number") return node.start;
28
+ if (Array.isArray(node.range) && typeof node.range[0] === "number") return node.range[0];
29
+ return null;
30
+ }
31
+ function getTypeName(node) {
32
+ if (!node) return null;
33
+ if (node.type === "Identifier") return node.name;
34
+ if (node.type === "TSTypeReference") return getTypeName(node.typeName);
35
+ if (node.type === "TSQualifiedName") {
36
+ const left = getTypeName(node.left);
37
+ const right = getTypeName(node.right);
38
+ return left && right ? `${left}.${right}` : right ?? left;
39
+ }
40
+ return null;
41
+ }
42
+ const TRANSPARENT_EXPRESSION_NODE_TYPES = new Set([
43
+ "ChainExpression",
44
+ "ParenthesizedExpression",
45
+ "TSAsExpression",
46
+ "TSInstantiationExpression",
47
+ "TSNonNullExpression",
48
+ "TSSatisfiesExpression",
49
+ "TSTypeAssertion"
50
+ ]);
51
+ function unwrapExpression(node) {
52
+ let current = node;
53
+ while (current && TRANSPARENT_EXPRESSION_NODE_TYPES.has(current.type)) current = current.expression ?? current.argument ?? current.object ?? current.callee ?? current.typeAnnotation ?? null;
54
+ return current ?? null;
55
+ }
56
+ //#endregion
57
+ //#region src/utilities/scope.ts
58
+ const FUNCTION_TYPES$1 = new Set([
59
+ "ArrowFunctionExpression",
60
+ "FunctionDeclaration",
61
+ "FunctionExpression"
62
+ ]);
63
+ function isFunction$1(node) {
64
+ return !!node && FUNCTION_TYPES$1.has(node.type);
65
+ }
66
+ function addBindingIdentifierNodes(node, identifiers) {
67
+ if (!node) return;
68
+ if (node.type === "Identifier") {
69
+ identifiers.push(node);
70
+ return;
71
+ }
72
+ if (node.type === "AssignmentPattern") {
73
+ addBindingIdentifierNodes(node.left, identifiers);
74
+ return;
75
+ }
76
+ if (node.type === "RestElement" || node.type === "TSParameterProperty") {
77
+ addBindingIdentifierNodes(node.argument ?? node.parameter, identifiers);
78
+ return;
79
+ }
80
+ if (node.type === "ArrayPattern") {
81
+ for (const element of node.elements ?? []) addBindingIdentifierNodes(element, identifiers);
82
+ return;
83
+ }
84
+ if (node.type === "ObjectPattern") for (const property of node.properties ?? []) if (property.type === "Property") addBindingIdentifierNodes(property.value, identifiers);
85
+ else addBindingIdentifierNodes(property.argument, identifiers);
86
+ }
87
+ function getBindingIdentifierNodes(node) {
88
+ const identifiers = [];
89
+ addBindingIdentifierNodes(node, identifiers);
90
+ return identifiers;
91
+ }
92
+ function getBindingNames(node) {
93
+ return new Set(getBindingIdentifierNodes(node).map((identifier) => identifier.name));
94
+ }
95
+ function hasBindingName(node, name) {
96
+ return getBindingNames(node).has(name);
97
+ }
98
+ function getMatchingBindingIdentifier(node, name) {
99
+ return getBindingIdentifierNodes(node).find((identifier) => identifier.name === name) ?? null;
100
+ }
101
+ function isBeforeReference(node, reference) {
102
+ const nodeStart = getNodeStart(node);
103
+ const referenceStart = getNodeStart(reference);
104
+ return nodeStart !== null && referenceStart !== null && nodeStart < referenceStart;
105
+ }
106
+ function hasDeclarationBeforeReference(node, name, reference) {
107
+ if (!node) return false;
108
+ if (Array.isArray(node)) return node.some((child) => hasDeclarationBeforeReference(child, name, reference));
109
+ if (!isBeforeReference(node, reference)) return false;
110
+ if (node.type === "VariableDeclarator") return hasBindingName(node.id, name);
111
+ if ((node.type === "FunctionDeclaration" || node.type === "ClassDeclaration") && node.id?.name === name) return true;
112
+ if (node !== reference && isFunction$1(node)) return false;
113
+ if (node !== reference && (node.type === "ClassDeclaration" || node.type === "ClassExpression")) return false;
114
+ for (const [key, value] of Object.entries(node)) {
115
+ if (key === "parent") continue;
116
+ if (!value || typeof value !== "object") continue;
117
+ if (hasDeclarationBeforeReference(value, name, reference)) return true;
118
+ }
119
+ return false;
120
+ }
121
+ function findDeclarationBeforeReference(node, name, reference) {
122
+ if (!node) return null;
123
+ if (Array.isArray(node)) {
124
+ for (const child of node) {
125
+ const declaration = findDeclarationBeforeReference(child, name, reference);
126
+ if (declaration) return declaration;
127
+ }
128
+ return null;
129
+ }
130
+ if (!isBeforeReference(node, reference)) return null;
131
+ if (node.type === "VariableDeclarator") return getMatchingBindingIdentifier(node.id, name);
132
+ if ((node.type === "FunctionDeclaration" || node.type === "ClassDeclaration") && node.id?.name === name) return node.id;
133
+ if (node !== reference && isFunction$1(node)) return null;
134
+ if (node !== reference && (node.type === "ClassDeclaration" || node.type === "ClassExpression")) return null;
135
+ for (const [key, value] of Object.entries(node)) {
136
+ if (key === "parent") continue;
137
+ if (!value || typeof value !== "object") continue;
138
+ const declaration = findDeclarationBeforeReference(value, name, reference);
139
+ if (declaration) return declaration;
140
+ }
141
+ return null;
142
+ }
143
+ function findNearestBindingIdentifier(context, node) {
144
+ if (node?.type !== "Identifier") return null;
145
+ const ancestors = context.sourceCode.getAncestors(node);
146
+ for (const ancestor of ancestors.toReversed()) {
147
+ if (isFunction$1(ancestor)) for (const param of ancestor.params ?? []) {
148
+ const binding = getMatchingBindingIdentifier(param, node.name);
149
+ if (binding) return binding;
150
+ }
151
+ if (ancestor.type === "CatchClause") {
152
+ const binding = getMatchingBindingIdentifier(ancestor.param, node.name);
153
+ if (binding) return binding;
154
+ }
155
+ if (ancestor.type === "BlockStatement" || ancestor.type === "Program") {
156
+ const binding = findDeclarationBeforeReference(ancestor.body, node.name, node);
157
+ if (binding) return binding;
158
+ }
159
+ }
160
+ return null;
161
+ }
162
+ function isShadowedIdentifier(context, node) {
163
+ if (node?.type !== "Identifier") return false;
164
+ const ancestors = context.sourceCode.getAncestors(node);
165
+ for (const ancestor of ancestors.toReversed()) {
166
+ if (isFunction$1(ancestor) && (ancestor.params ?? []).some((param) => hasBindingName(param, node.name))) return true;
167
+ if (ancestor.type === "CatchClause" && hasBindingName(ancestor.param, node.name)) return true;
168
+ if ((ancestor.type === "BlockStatement" || ancestor.type === "Program") && hasDeclarationBeforeReference(ancestor.body, node.name, node)) return true;
169
+ }
170
+ return false;
171
+ }
172
+ //#endregion
173
+ //#region src/utilities/angular.ts
174
+ function getProgramNode(context, node) {
175
+ if (node.type === "Program") return node;
176
+ return context.sourceCode.getAncestors(node).find((ancestor) => ancestor.type === "Program") ?? null;
177
+ }
178
+ function getImportedName(context, node, source) {
179
+ if (node?.type !== "Identifier") return null;
180
+ if (isShadowedIdentifier(context, node)) return null;
181
+ const program = getProgramNode(context, node);
182
+ for (const statement of program?.body ?? []) {
183
+ if (statement.type !== "ImportDeclaration" || statement.source?.value !== source) continue;
184
+ for (const specifier of statement.specifiers ?? []) {
185
+ if (specifier.type !== "ImportSpecifier" || specifier.local.name !== node.name) continue;
186
+ return getPropertyName(specifier.imported);
187
+ }
188
+ }
189
+ return null;
190
+ }
191
+ function isNamespaceImport(context, node, source) {
192
+ if (node?.type !== "Identifier") return false;
193
+ if (isShadowedIdentifier(context, node)) return false;
194
+ const program = getProgramNode(context, node);
195
+ for (const statement of program?.body ?? []) {
196
+ if (statement.type !== "ImportDeclaration" || statement.source?.value !== source) continue;
197
+ if ((statement.specifiers ?? []).some((specifier) => specifier.type === "ImportNamespaceSpecifier" && specifier.local.name === node.name)) return true;
198
+ }
199
+ return false;
200
+ }
201
+ function isImportedReference(context, node, source, importedNames) {
202
+ const importedName = getImportedName(context, node, source);
203
+ return !!importedName && importedNames.has(importedName);
204
+ }
205
+ function isImportedNamespaceMember(context, node, source, memberNames) {
206
+ return node?.type === "MemberExpression" && node.object?.type === "Identifier" && isNamespaceImport(context, node.object, source) && memberNames.has(getPropertyName(node.property) ?? "");
207
+ }
208
+ function isAngularCoreDecorator(context, decorator, decoratorNames) {
209
+ if (!decorator) return false;
210
+ const expression = decorator.expression ?? decorator;
211
+ const callee = expression?.type === "CallExpression" ? expression.callee : expression;
212
+ return isImportedReference(context, callee, "@angular/core", decoratorNames) || isImportedNamespaceMember(context, callee, "@angular/core", decoratorNames);
213
+ }
26
214
  //#endregion
27
215
  //#region src/rules/class-member-order/index.ts
28
216
  const ANGULAR_CLASS_DECORATOR_NAMES$1 = new Set([
@@ -45,38 +233,136 @@ const ORDER_LABELS = [
45
233
  "outputs",
46
234
  "everything else"
47
235
  ];
48
- function hasAngularClassDecorator(classNode) {
236
+ function hasAngularClassDecorator(context, classNode) {
49
237
  if (!classNode || !Array.isArray(classNode.decorators)) return false;
50
- return classNode.decorators.some((decorator) => ANGULAR_CLASS_DECORATOR_NAMES$1.has(getDecoratorName(decorator) ?? ""));
238
+ return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, ANGULAR_CLASS_DECORATOR_NAMES$1));
51
239
  }
52
- function getCallName(node) {
53
- if (!node || node.type !== "CallExpression") return null;
240
+ function isApiCall(context, node, apiNames) {
241
+ if (!node || node.type !== "CallExpression") return false;
54
242
  const callee = node.callee;
55
- if (callee?.type === "Identifier") return callee.name;
56
- if (callee?.type === "MemberExpression") return getPropertyName(callee.property);
57
- return null;
58
- }
59
- function hasDecorator(element, names) {
60
- return Array.isArray(element.decorators) ? element.decorators.some((decorator) => names.has(getDecoratorName(decorator) ?? "")) : false;
243
+ if (isImportedReference(context, callee, "@angular/core", apiNames)) return true;
244
+ if (callee?.type !== "MemberExpression") return false;
245
+ const supportsRequiredApi = [...INPUT_MODEL_CALL_NAMES].some((name) => apiNames.has(name));
246
+ if (callee.object?.type === "Identifier") {
247
+ if (getPropertyName(callee.property) === "required") return supportsRequiredApi && isImportedReference(context, callee.object, "@angular/core", apiNames);
248
+ return isImportedNamespaceMember(context, callee, "@angular/core", apiNames);
249
+ }
250
+ if (getPropertyName(callee.property) === "required" && callee.object?.type === "MemberExpression" && callee.object.object?.type === "Identifier") return supportsRequiredApi && isImportedNamespaceMember(context, callee.object, "@angular/core", apiNames) && apiNames.has(getPropertyName(callee.object.property) ?? "");
251
+ return false;
61
252
  }
62
- function isDirectInjectCall(value) {
63
- if (!value || value.type !== "CallExpression") return false;
64
- const callee = value.callee;
65
- return callee?.type === "Identifier" && callee.name === "inject" || callee?.type === "MemberExpression" && getPropertyName(callee.property) === "inject";
253
+ function hasDecorator(context, element, decoratorNames) {
254
+ return Array.isArray(element.decorators) ? element.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, decoratorNames)) : false;
66
255
  }
67
- function classifyMember(element) {
256
+ function classifyMember(context, element) {
68
257
  if (CLASS_FIELD_TYPES$1.has(element.type)) {
69
- const callName = getCallName(element.value);
70
- if (isDirectInjectCall(element.value)) return 0;
71
- if (callName && INPUT_MODEL_CALL_NAMES.has(callName)) return 1;
72
- if (hasDecorator(element, new Set(["Input"]))) return 1;
73
- if (callName && OUTPUT_CALL_NAMES.has(callName)) return 2;
74
- if (hasDecorator(element, new Set(["Output"]))) return 2;
258
+ if (isApiCall(context, element.value, new Set(["inject"]))) return 0;
259
+ if (isApiCall(context, element.value, INPUT_MODEL_CALL_NAMES)) return 1;
260
+ if (hasDecorator(context, element, new Set(["Input"]))) return 1;
261
+ if (isApiCall(context, element.value, OUTPUT_CALL_NAMES)) return 2;
262
+ if (hasDecorator(context, element, new Set(["Output"]))) return 2;
75
263
  return 3;
76
264
  }
77
265
  if (element.type === "MethodDefinition") return 3;
78
266
  return 3;
79
267
  }
268
+ function getMemberName$1(node) {
269
+ if (!node) return null;
270
+ if (node.type === "Identifier" || node.type === "PrivateIdentifier") return node.name;
271
+ return null;
272
+ }
273
+ function collectThisMemberReferences(node, references = /* @__PURE__ */ new Set()) {
274
+ if (!node) return references;
275
+ if (Array.isArray(node)) {
276
+ for (const item of node) collectThisMemberReferences(item, references);
277
+ return references;
278
+ }
279
+ if (node.type === "MemberExpression" && node.object?.type === "ThisExpression") {
280
+ const propertyName = getPropertyName(node.property);
281
+ if (propertyName) references.add(propertyName);
282
+ }
283
+ for (const [key, value] of Object.entries(node)) {
284
+ if (key === "parent") continue;
285
+ if (!value || typeof value !== "object") continue;
286
+ collectThisMemberReferences(value, references);
287
+ }
288
+ return references;
289
+ }
290
+ function applyDependencyGroups(classifiedMembers) {
291
+ const membersByName = /* @__PURE__ */ new Map();
292
+ for (const member of classifiedMembers) if (member.name) membersByName.set(member.name, member);
293
+ let changed = true;
294
+ while (changed) {
295
+ changed = false;
296
+ for (const member of classifiedMembers) for (const dependencyName of member.dependencies) {
297
+ const dependency = membersByName.get(dependencyName);
298
+ if (!dependency) continue;
299
+ if (dependency.effectiveGroup <= member.effectiveGroup) continue;
300
+ dependency.effectiveGroup = member.effectiveGroup;
301
+ changed = true;
302
+ }
303
+ }
304
+ }
305
+ function dependsOn(member, dependency, membersByName, seen = /* @__PURE__ */ new Set()) {
306
+ if (!dependency.name) return false;
307
+ if (member.dependencies.has(dependency.name)) return true;
308
+ if (seen.has(member)) return false;
309
+ seen.add(member);
310
+ for (const dependencyName of member.dependencies) {
311
+ const next = membersByName.get(dependencyName);
312
+ if (next && dependsOn(next, dependency, membersByName, seen)) return true;
313
+ }
314
+ return false;
315
+ }
316
+ function getSortedMembers(classifiedMembers) {
317
+ const membersByName = /* @__PURE__ */ new Map();
318
+ for (const member of classifiedMembers) if (member.name) membersByName.set(member.name, member);
319
+ return classifiedMembers.toSorted((left, right) => {
320
+ if (dependsOn(left, right, membersByName)) return 1;
321
+ if (dependsOn(right, left, membersByName)) return -1;
322
+ return left.effectiveGroup - right.effectiveGroup;
323
+ });
324
+ }
325
+ function hasUnresolvedPrioritizedDependency(member, membersByName) {
326
+ if (member.group === 3) return false;
327
+ for (const dependencyName of member.dependencies) if (!membersByName.has(dependencyName)) return true;
328
+ return false;
329
+ }
330
+ function getSortedMembersFix(context, classBody, classifiedMembers) {
331
+ if (!classifiedMembers.length) return void 0;
332
+ if ((classBody.body ?? []).length !== classifiedMembers.length) return void 0;
333
+ const sortableRanges = [];
334
+ const membersByName = /* @__PURE__ */ new Map();
335
+ for (const member of classifiedMembers) if (member.name) membersByName.set(member.name, member);
336
+ for (const member of classifiedMembers) {
337
+ const { element } = member;
338
+ if (!CLASS_FIELD_TYPES$1.has(element.type)) return void 0;
339
+ if (element.type === "AccessorProperty") return void 0;
340
+ if (element.computed) return void 0;
341
+ if (Array.isArray(element.decorators) && element.decorators.length > 0) return void 0;
342
+ if (hasUnresolvedPrioritizedDependency(member, membersByName)) return;
343
+ const range = getRange(element);
344
+ if (!range) return void 0;
345
+ if (!context.sourceCode.text.slice(range[0], range[1]).trimEnd().endsWith(";")) return;
346
+ sortableRanges.push(range);
347
+ }
348
+ const sortedMembers = getSortedMembers(classifiedMembers);
349
+ if (sortedMembers.every((member, index) => member.element === classifiedMembers[index]?.element)) return void 0;
350
+ return (fixer) => {
351
+ const sourceText = context.sourceCode.text;
352
+ const sortedTexts = sortedMembers.map(({ element }) => {
353
+ const range = getRange(element);
354
+ return range ? sourceText.slice(range[0], range[1]) : "";
355
+ });
356
+ let output = "";
357
+ for (let index = 0; index < sortableRanges.length; index += 1) {
358
+ const [, end] = sortableRanges[index];
359
+ const nextStart = sortableRanges[index + 1]?.[0];
360
+ output += sortedTexts[index];
361
+ output += sourceText.slice(end, nextStart ?? end);
362
+ }
363
+ return fixer.replaceTextRange([sortableRanges[0][0], sortableRanges.at(-1)[1]], output);
364
+ };
365
+ }
80
366
  const classMemberOrder = defineRule({
81
367
  meta: {
82
368
  type: "suggestion",
@@ -84,29 +370,51 @@ const classMemberOrder = defineRule({
84
370
  description: "Require Angular class members to be ordered as inject fields, inputs/models, outputs, then everything else.",
85
371
  recommended: true
86
372
  },
373
+ fixable: "code",
87
374
  schema: [],
88
375
  messages: { outOfOrder: "Angular class member should be ordered before {{previousGroup}} and with {{expectedGroup}}." }
89
376
  },
90
377
  createOnce(context) {
91
378
  return { ClassBody(node) {
92
- if (!hasAngularClassDecorator(node.parent)) return;
379
+ const classBody = node;
380
+ if (!hasAngularClassDecorator(context, classBody.parent)) return;
93
381
  let highestSeen = null;
94
- for (const element of node.body ?? []) {
95
- const group = classifyMember(element);
382
+ const classifiedMembers = [];
383
+ const outOfOrderReports = [];
384
+ for (const element of classBody.body ?? []) {
385
+ const group = classifyMember(context, element);
96
386
  if (group === null) continue;
97
- if (highestSeen !== null && group < highestSeen) {
98
- context.report({
99
- node: element,
100
- messageId: "outOfOrder",
101
- data: {
102
- expectedGroup: ORDER_LABELS[group],
103
- previousGroup: ORDER_LABELS[highestSeen]
104
- }
387
+ classifiedMembers.push({
388
+ element,
389
+ group,
390
+ effectiveGroup: group,
391
+ name: getMemberName$1(element.key),
392
+ dependencies: collectThisMemberReferences(element.value)
393
+ });
394
+ }
395
+ applyDependencyGroups(classifiedMembers);
396
+ for (const { element, effectiveGroup } of classifiedMembers) {
397
+ if (highestSeen !== null && effectiveGroup < highestSeen) {
398
+ outOfOrderReports.push({
399
+ element,
400
+ effectiveGroup,
401
+ previousEffectiveGroup: highestSeen
105
402
  });
106
403
  continue;
107
404
  }
108
- highestSeen = group;
405
+ highestSeen = effectiveGroup;
109
406
  }
407
+ if (!outOfOrderReports.length) return;
408
+ const fix = getSortedMembersFix(context, classBody, classifiedMembers);
409
+ for (const [index, report] of outOfOrderReports.entries()) context.report({
410
+ node: report.element,
411
+ messageId: "outOfOrder",
412
+ data: {
413
+ expectedGroup: ORDER_LABELS[report.effectiveGroup],
414
+ previousGroup: ORDER_LABELS[report.previousEffectiveGroup]
415
+ },
416
+ fix: index === 0 ? fix : void 0
417
+ });
110
418
  } };
111
419
  }
112
420
  });
@@ -115,25 +423,22 @@ const classMemberOrder = defineRule({
115
423
  const DEFAULT_DISALLOW_INJECT_INJECTOR = true;
116
424
  const DEFAULT_DISALLOW_RUN_IN_INJECTION_CONTEXT = true;
117
425
  const RUN_IN_INJECTION_CONTEXT_NAMES = new Set(["runInInjectionContext", "runInContext"]);
118
- function isAngularNamespaceMember(node, namespaces, memberName) {
119
- return node?.type === "MemberExpression" && node.object?.type === "Identifier" && namespaces.has(node.object.name) && getPropertyName(node.property) === memberName;
120
- }
121
- function isInjectCall(callNode, injectNames, angularNamespaces) {
426
+ const INJECT_NAMES = new Set(["inject"]);
427
+ const INJECTOR_NAMES = new Set(["Injector"]);
428
+ function isInjectCall(context, callNode) {
122
429
  const callee = callNode.callee;
123
- return callee?.type === "Identifier" && injectNames.has(callee.name) || isAngularNamespaceMember(callee, angularNamespaces, "inject");
430
+ return isImportedReference(context, callee, "@angular/core", INJECT_NAMES) || isImportedNamespaceMember(context, callee, "@angular/core", INJECT_NAMES);
124
431
  }
125
- function isInjectorReference(node, injectorNames, angularNamespaces) {
126
- return node?.type === "Identifier" && injectorNames.has(node.name) || isAngularNamespaceMember(node, angularNamespaces, "Injector");
432
+ function isInjectorReference(context, node) {
433
+ return isImportedReference(context, node, "@angular/core", INJECTOR_NAMES) || isImportedNamespaceMember(context, node, "@angular/core", INJECTOR_NAMES);
127
434
  }
128
- function isDisallowedInjectInjector(callNode, injectNames, injectorNames, angularNamespaces) {
129
- if (!isInjectCall(callNode, injectNames, angularNamespaces)) return false;
130
- return isInjectorReference(callNode.arguments?.[0], injectorNames, angularNamespaces);
435
+ function isDisallowedInjectInjector(context, callNode) {
436
+ if (!isInjectCall(context, callNode)) return false;
437
+ return isInjectorReference(context, callNode.arguments?.[0]);
131
438
  }
132
- function isDisallowedRunInInjectionContext(callNode, runInInjectionContextNames, runInContextNames, angularNamespaces) {
439
+ function isDisallowedRunInInjectionContext(context, callNode) {
133
440
  const callee = callNode.callee;
134
- if (callee?.type === "Identifier") return runInInjectionContextNames.has(callee.name) || runInContextNames.has(callee.name);
135
- if (callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && angularNamespaces.has(callee.object.name)) return RUN_IN_INJECTION_CONTEXT_NAMES.has(getPropertyName(callee.property) ?? "");
136
- return false;
441
+ return isImportedReference(context, callee, "@angular/core", RUN_IN_INJECTION_CONTEXT_NAMES) || isImportedNamespaceMember(context, callee, "@angular/core", RUN_IN_INJECTION_CONTEXT_NAMES);
137
442
  }
138
443
  const avoidExplicitInjectionContext = defineRule({
139
444
  meta: {
@@ -162,44 +467,378 @@ const avoidExplicitInjectionContext = defineRule({
162
467
  }
163
468
  },
164
469
  createOnce(context) {
165
- const injectNames = /* @__PURE__ */ new Set();
166
- const injectorNames = /* @__PURE__ */ new Set();
167
- const runInInjectionContextNames = /* @__PURE__ */ new Set();
168
- const runInContextNames = /* @__PURE__ */ new Set();
169
- const angularNamespaces = /* @__PURE__ */ new Set();
470
+ return { CallExpression(node) {
471
+ const callNode = node;
472
+ const options = context.options[0] ?? {};
473
+ const disallowInjectInjector = options.disallowInjectInjector ?? DEFAULT_DISALLOW_INJECT_INJECTOR;
474
+ const disallowRunInInjectionContext = options.disallowRunInInjectionContext ?? DEFAULT_DISALLOW_RUN_IN_INJECTION_CONTEXT;
475
+ if (disallowInjectInjector && isDisallowedInjectInjector(context, callNode)) context.report({
476
+ node: callNode.callee,
477
+ messageId: "avoidInjectInjector"
478
+ });
479
+ if (disallowRunInInjectionContext && isDisallowedRunInInjectionContext(context, callNode)) context.report({
480
+ node: callNode.callee,
481
+ messageId: "avoidRunInInjectionContext"
482
+ });
483
+ } };
484
+ }
485
+ });
486
+ //#endregion
487
+ //#region src/rules/avoid-explicit-subscription-management/index.ts
488
+ const TARGET_DECORATORS$2 = new Set([
489
+ "Component",
490
+ "Directive",
491
+ "Injectable"
492
+ ]);
493
+ const FIELD_NODE_TYPES$2 = new Set([
494
+ "AccessorProperty",
495
+ "FieldDefinition",
496
+ "PropertyDefinition"
497
+ ]);
498
+ const RXJS_SUBSCRIBABLE_NAMES = new Set([
499
+ "AsyncSubject",
500
+ "BehaviorSubject",
501
+ "Observable",
502
+ "ReplaySubject",
503
+ "Subject"
504
+ ]);
505
+ function hasTargetDecorator$3(context, classNode) {
506
+ if (!classNode || !Array.isArray(classNode.decorators)) return false;
507
+ return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS$2));
508
+ }
509
+ function hasSubscriptionTypeReference(context, node, subscriptionLocalNames, rxjsNamespaces) {
510
+ if (!node) return false;
511
+ if (node.type === "TSTypeReference") {
512
+ const typeName = getTypeName(node.typeName);
513
+ if (node.typeName?.type === "Identifier" && typeName && subscriptionLocalNames.has(typeName) && !isShadowedIdentifier(context, node.typeName)) return true;
514
+ if (typeName?.includes(".")) {
515
+ const [namespaceName, memberName] = typeName.split(".");
516
+ return memberName === "Subscription" && rxjsNamespaces.has(namespaceName);
517
+ }
518
+ }
519
+ for (const [key, value] of Object.entries(node)) {
520
+ if (key === "parent") continue;
521
+ if (!value || typeof value !== "object") continue;
522
+ if (Array.isArray(value)) {
523
+ if (value.some((child) => hasSubscriptionTypeReference(context, child, subscriptionLocalNames, rxjsNamespaces))) return true;
524
+ continue;
525
+ }
526
+ if (hasSubscriptionTypeReference(context, value, subscriptionLocalNames, rxjsNamespaces)) return true;
527
+ }
528
+ return false;
529
+ }
530
+ function isSubscriptionConstructor(context, node, subscriptionLocalNames, rxjsNamespaces) {
531
+ const expression = unwrapExpression(node);
532
+ if (expression?.type !== "NewExpression") return false;
533
+ const callee = unwrapExpression(expression.callee);
534
+ if (callee?.type === "Identifier") return subscriptionLocalNames.has(callee.name) && !isShadowedIdentifier(context, callee);
535
+ if (callee?.type !== "MemberExpression") return false;
536
+ return callee.object?.type === "Identifier" && rxjsNamespaces.has(callee.object.name) && !isShadowedIdentifier(context, callee.object) && getPropertyName(callee.property) === "Subscription";
537
+ }
538
+ function getReferenceName(node) {
539
+ const expression = unwrapExpression(node);
540
+ if (!expression) return null;
541
+ if (expression.type === "Identifier") return expression.name;
542
+ if (expression.type !== "MemberExpression") return null;
543
+ if (expression.object?.type !== "ThisExpression") return null;
544
+ return getPropertyName(expression.property);
545
+ }
546
+ function isObservableReferenceName(name) {
547
+ return name.length > 1 && name.endsWith("$");
548
+ }
549
+ function addTrackedReference(references, name, binding) {
550
+ const bindings = references.get(name) ?? /* @__PURE__ */ new Set();
551
+ if (binding) bindings.add(binding);
552
+ references.set(name, bindings);
553
+ }
554
+ function hasTrackedReferenceName(references, name) {
555
+ return references.has(name);
556
+ }
557
+ function isTrackedReferenceExpression(context, node, references) {
558
+ const expression = unwrapExpression(node);
559
+ if (!expression) return false;
560
+ const referenceName = getReferenceName(expression);
561
+ if (!referenceName) return false;
562
+ const bindings = references.get(referenceName);
563
+ if (!bindings) return false;
564
+ if (expression.type === "MemberExpression" && expression.object?.type === "ThisExpression") return true;
565
+ if (expression.type !== "Identifier") return false;
566
+ const nearestBinding = findNearestBindingIdentifier(context, expression);
567
+ return !!nearestBinding && bindings.has(nearestBinding);
568
+ }
569
+ function getDeclaredName(node) {
570
+ if (!node) return null;
571
+ if (node.type === "Identifier") return node.name;
572
+ if (node.type === "PrivateIdentifier") return node.name;
573
+ return null;
574
+ }
575
+ function hasRxjsSubscribableTypeReference(context, node, rxjsSubscribableLocalNames, rxjsNamespaces) {
576
+ if (!node) return false;
577
+ if (node.type === "TSTypeReference") {
578
+ const typeName = getTypeName(node.typeName);
579
+ if (node.typeName?.type === "Identifier" && typeName && rxjsSubscribableLocalNames.has(typeName) && !isShadowedIdentifier(context, node.typeName)) return true;
580
+ if (typeName?.includes(".")) {
581
+ const [namespaceName, memberName] = typeName.split(".");
582
+ return RXJS_SUBSCRIBABLE_NAMES.has(memberName) && rxjsNamespaces.has(namespaceName);
583
+ }
584
+ }
585
+ for (const [key, value] of Object.entries(node)) {
586
+ if (key === "parent") continue;
587
+ if (!value || typeof value !== "object") continue;
588
+ if (Array.isArray(value)) {
589
+ if (value.some((child) => hasRxjsSubscribableTypeReference(context, child, rxjsSubscribableLocalNames, rxjsNamespaces))) return true;
590
+ continue;
591
+ }
592
+ if (hasRxjsSubscribableTypeReference(context, value, rxjsSubscribableLocalNames, rxjsNamespaces)) return true;
593
+ }
594
+ return false;
595
+ }
596
+ function isRxjsSubscribableConstructor(context, node, rxjsSubscribableLocalNames, rxjsNamespaces) {
597
+ const expression = unwrapExpression(node);
598
+ if (expression?.type !== "NewExpression") return false;
599
+ const callee = unwrapExpression(expression.callee);
600
+ if (callee?.type === "Identifier") return rxjsSubscribableLocalNames.has(callee.name) && !isShadowedIdentifier(context, callee);
601
+ if (callee?.type !== "MemberExpression") return false;
602
+ return callee.object?.type === "Identifier" && rxjsNamespaces.has(callee.object.name) && !isShadowedIdentifier(context, callee.object) && RXJS_SUBSCRIBABLE_NAMES.has(getPropertyName(callee.property) ?? "");
603
+ }
604
+ function isTakeUntilDestroyedCall(node, takeUntilDestroyedLocalNames, interopNamespaces) {
605
+ const callNode = unwrapExpression(node);
606
+ if (callNode?.type !== "CallExpression") return false;
607
+ const callee = unwrapExpression(callNode.callee);
608
+ if (callee?.type === "Identifier") return takeUntilDestroyedLocalNames.has(callee.name);
609
+ if (callee?.type !== "MemberExpression") return false;
610
+ return callee.object?.type === "Identifier" && interopNamespaces.has(callee.object.name) && getPropertyName(callee.property) === "takeUntilDestroyed";
611
+ }
612
+ function isTakeUntilDestroyedSubscribe(node, takeUntilDestroyedLocalNames, interopNamespaces) {
613
+ const callNode = unwrapExpression(node);
614
+ if (callNode?.type !== "CallExpression") return false;
615
+ const callee = unwrapExpression(callNode.callee);
616
+ if (callee?.type !== "MemberExpression") return false;
617
+ if (getPropertyName(callee.property) !== "subscribe") return false;
618
+ const source = unwrapExpression(callee.object);
619
+ if (source?.type !== "CallExpression") return false;
620
+ const sourceCallee = unwrapExpression(source.callee);
621
+ if (sourceCallee?.type !== "MemberExpression") return false;
622
+ if (getPropertyName(sourceCallee.property) !== "pipe") return false;
623
+ return (source.arguments ?? []).some((argument) => isTakeUntilDestroyedCall(argument, takeUntilDestroyedLocalNames, interopNamespaces));
624
+ }
625
+ function isKnownRxjsSubscribableExpression(context, node, rxjsSubscribableReferences, rxjsSubscribableLocalNames, rxjsNamespaces) {
626
+ const expression = unwrapExpression(node);
627
+ if (!expression) return false;
628
+ const referenceName = getReferenceName(expression);
629
+ if (referenceName && isObservableReferenceName(referenceName)) return true;
630
+ if (isTrackedReferenceExpression(context, expression, rxjsSubscribableReferences)) return true;
631
+ if (isRxjsSubscribableConstructor(context, expression, rxjsSubscribableLocalNames, rxjsNamespaces)) return true;
632
+ if (expression.type !== "CallExpression") return false;
633
+ const callee = unwrapExpression(expression.callee);
634
+ if (callee?.type !== "MemberExpression") return false;
635
+ const methodName = getPropertyName(callee.property);
636
+ if (methodName !== "asObservable" && methodName !== "pipe") return false;
637
+ return isKnownRxjsSubscribableExpression(context, callee.object, rxjsSubscribableReferences, rxjsSubscribableLocalNames, rxjsNamespaces);
638
+ }
639
+ function isUnmanagedSubscribeCall(context, node, rxjsSubscribableReferences, rxjsSubscribableLocalNames, rxjsNamespaces, takeUntilDestroyedLocalNames, interopNamespaces) {
640
+ const callNode = unwrapExpression(node);
641
+ if (callNode?.type !== "CallExpression") return false;
642
+ const callee = unwrapExpression(callNode.callee);
643
+ if (callee?.type !== "MemberExpression") return false;
644
+ if (getPropertyName(callee.property) !== "subscribe") return false;
645
+ return isKnownRxjsSubscribableExpression(context, callee.object, rxjsSubscribableReferences, rxjsSubscribableLocalNames, rxjsNamespaces) && !isTakeUntilDestroyedSubscribe(node, takeUntilDestroyedLocalNames, interopNamespaces);
646
+ }
647
+ function walkNode$1(node, containingClass, visitor) {
648
+ if (!node) return;
649
+ if (Array.isArray(node)) {
650
+ for (const child of node) walkNode$1(child, containingClass, visitor);
651
+ return;
652
+ }
653
+ if (node !== containingClass && (node.type === "ClassDeclaration" || node.type === "ClassExpression")) return;
654
+ if (visitor(node) === false) return;
655
+ for (const [key, value] of Object.entries(node)) {
656
+ if (key === "parent") continue;
657
+ if (!value || typeof value !== "object") continue;
658
+ walkNode$1(value, containingClass, visitor);
659
+ }
660
+ }
661
+ function containsUnmanagedSubscribeCall(context, node, containingClass, rxjsSubscribableReferences, rxjsSubscribableLocalNames, rxjsNamespaces, takeUntilDestroyedLocalNames, interopNamespaces) {
662
+ let found = false;
663
+ walkNode$1(node, containingClass, (current) => {
664
+ if (isUnmanagedSubscribeCall(context, current, rxjsSubscribableReferences, rxjsSubscribableLocalNames, rxjsNamespaces, takeUntilDestroyedLocalNames, interopNamespaces)) {
665
+ found = true;
666
+ return false;
667
+ }
668
+ });
669
+ return found;
670
+ }
671
+ function collectRxjsSubscribableReferences(context, classBody, classNode, rxjsSubscribableLocalNames, rxjsNamespaces) {
672
+ const references = /* @__PURE__ */ new Map();
673
+ let changed = true;
674
+ while (changed) {
675
+ changed = false;
676
+ walkNode$1(classBody, classNode, (node) => {
677
+ if (FIELD_NODE_TYPES$2.has(node.type)) {
678
+ const name = getDeclaredName(node.key);
679
+ if (!name || hasTrackedReferenceName(references, name)) return;
680
+ if (isObservableReferenceName(name) || hasRxjsSubscribableTypeReference(context, node.typeAnnotation?.typeAnnotation, rxjsSubscribableLocalNames, rxjsNamespaces) || isRxjsSubscribableConstructor(context, node.value, rxjsSubscribableLocalNames, rxjsNamespaces) || isKnownRxjsSubscribableExpression(context, node.value, references, rxjsSubscribableLocalNames, rxjsNamespaces)) {
681
+ addTrackedReference(references, name, node.key);
682
+ changed = true;
683
+ }
684
+ return;
685
+ }
686
+ if (node.type === "VariableDeclarator" && node.id?.type === "Identifier") {
687
+ if (hasTrackedReferenceName(references, node.id.name)) return;
688
+ if (isObservableReferenceName(node.id.name) || hasRxjsSubscribableTypeReference(context, node.id.typeAnnotation?.typeAnnotation, rxjsSubscribableLocalNames, rxjsNamespaces) || isRxjsSubscribableConstructor(context, node.init, rxjsSubscribableLocalNames, rxjsNamespaces) || isKnownRxjsSubscribableExpression(context, node.init, references, rxjsSubscribableLocalNames, rxjsNamespaces)) {
689
+ addTrackedReference(references, node.id.name, node.id);
690
+ changed = true;
691
+ }
692
+ return;
693
+ }
694
+ if (node.type === "AssignmentExpression") {
695
+ const name = getReferenceName(node.left);
696
+ if (!name || hasTrackedReferenceName(references, name)) return;
697
+ if (isObservableReferenceName(name) || isRxjsSubscribableConstructor(context, node.right, rxjsSubscribableLocalNames, rxjsNamespaces) || isKnownRxjsSubscribableExpression(context, node.right, references, rxjsSubscribableLocalNames, rxjsNamespaces)) {
698
+ addTrackedReference(references, name, findNearestBindingIdentifier(context, node.left));
699
+ changed = true;
700
+ }
701
+ }
702
+ });
703
+ }
704
+ return references;
705
+ }
706
+ function collectSubscriptionReferences(context, classBody, classNode, subscriptionLocalNames, rxjsNamespaces, rxjsSubscribableReferences, rxjsSubscribableLocalNames, takeUntilDestroyedLocalNames, interopNamespaces) {
707
+ const references = /* @__PURE__ */ new Map();
708
+ walkNode$1(classBody, classNode, (node) => {
709
+ if (FIELD_NODE_TYPES$2.has(node.type)) {
710
+ const name = getDeclaredName(node.key);
711
+ if (!name) return;
712
+ if (hasSubscriptionTypeReference(context, node.typeAnnotation?.typeAnnotation, subscriptionLocalNames, rxjsNamespaces) || isSubscriptionConstructor(context, node.value, subscriptionLocalNames, rxjsNamespaces)) addTrackedReference(references, name, node.key);
713
+ return;
714
+ }
715
+ if (node.type === "VariableDeclarator" && node.id?.type === "Identifier") {
716
+ if (hasSubscriptionTypeReference(context, node.id.typeAnnotation?.typeAnnotation, subscriptionLocalNames, rxjsNamespaces) || isSubscriptionConstructor(context, node.init, subscriptionLocalNames, rxjsNamespaces) || isUnmanagedSubscribeCall(context, node.init, rxjsSubscribableReferences, rxjsSubscribableLocalNames, rxjsNamespaces, takeUntilDestroyedLocalNames, interopNamespaces)) addTrackedReference(references, node.id.name, node.id);
717
+ return;
718
+ }
719
+ if (node.type === "AssignmentExpression") {
720
+ const name = getReferenceName(node.left);
721
+ if (!name) return;
722
+ if (isSubscriptionConstructor(context, node.right, subscriptionLocalNames, rxjsNamespaces) || isUnmanagedSubscribeCall(context, node.right, rxjsSubscribableReferences, rxjsSubscribableLocalNames, rxjsNamespaces, takeUntilDestroyedLocalNames, interopNamespaces)) addTrackedReference(references, name, findNearestBindingIdentifier(context, node.left));
723
+ }
724
+ });
725
+ return references;
726
+ }
727
+ function isSubscriptionAddCall(context, node, containingClass, subscriptionReferences, rxjsSubscribableReferences, rxjsSubscribableLocalNames, rxjsNamespaces, takeUntilDestroyedLocalNames, interopNamespaces) {
728
+ const callNode = unwrapExpression(node);
729
+ if (callNode?.type !== "CallExpression") return false;
730
+ const callee = unwrapExpression(callNode.callee);
731
+ if (callee?.type !== "MemberExpression") return false;
732
+ if (getPropertyName(callee.property) !== "add") return false;
733
+ if (isTrackedReferenceExpression(context, callee.object, subscriptionReferences)) return true;
734
+ return (callNode.arguments ?? []).some((argument) => isTrackedReferenceExpression(context, argument, subscriptionReferences) || containsUnmanagedSubscribeCall(context, argument, containingClass, rxjsSubscribableReferences, rxjsSubscribableLocalNames, rxjsNamespaces, takeUntilDestroyedLocalNames, interopNamespaces));
735
+ }
736
+ function isSubscriptionUnsubscribeCall(context, node, subscriptionReferences) {
737
+ const callNode = unwrapExpression(node);
738
+ if (callNode?.type !== "CallExpression") return false;
739
+ const callee = unwrapExpression(callNode.callee);
740
+ if (callee?.type !== "MemberExpression") return false;
741
+ if (getPropertyName(callee.property) !== "unsubscribe") return false;
742
+ return isTrackedReferenceExpression(context, callee.object, subscriptionReferences);
743
+ }
744
+ const avoidExplicitSubscriptionManagement = defineRule({
745
+ meta: {
746
+ type: "suggestion",
747
+ docs: {
748
+ description: "Avoid manual RxJS Subscription lifecycle management in Angular components, directives, and services.",
749
+ recommended: true
750
+ },
751
+ schema: [],
752
+ messages: {
753
+ explicitSubscriptionType: "Avoid storing RxJS Subscription references in Angular classes. Prefer takeUntilDestroyed, toSignal, or firstValueFrom.",
754
+ explicitSubscriptionConstructor: "Avoid creating RxJS Subscription instances in Angular classes. Prefer takeUntilDestroyed, toSignal, or firstValueFrom.",
755
+ explicitSubscribe: "Avoid subscribe() calls that require manual lifecycle management. Prefer takeUntilDestroyed, toSignal, or firstValueFrom.",
756
+ explicitSubscriptionAdd: "Avoid adding subscriptions to a Subscription container. Prefer takeUntilDestroyed, toSignal, or firstValueFrom.",
757
+ explicitUnsubscribe: "Avoid manual unsubscribe() calls in Angular classes. Prefer takeUntilDestroyed, toSignal, or firstValueFrom."
758
+ }
759
+ },
760
+ createOnce(context) {
761
+ const subscriptionLocalNames = /* @__PURE__ */ new Set();
762
+ const rxjsSubscribableLocalNames = /* @__PURE__ */ new Set();
763
+ const rxjsNamespaces = /* @__PURE__ */ new Set();
764
+ const takeUntilDestroyedLocalNames = /* @__PURE__ */ new Set();
765
+ const interopNamespaces = /* @__PURE__ */ new Set();
170
766
  return {
171
767
  before() {
172
- injectNames.clear();
173
- injectorNames.clear();
174
- runInInjectionContextNames.clear();
175
- runInContextNames.clear();
176
- angularNamespaces.clear();
768
+ subscriptionLocalNames.clear();
769
+ rxjsSubscribableLocalNames.clear();
770
+ rxjsNamespaces.clear();
771
+ takeUntilDestroyedLocalNames.clear();
772
+ interopNamespaces.clear();
177
773
  },
178
774
  ImportDeclaration(node) {
179
- if (node.source?.value !== "@angular/core") return;
180
- for (const specifier of node.specifiers ?? []) {
775
+ const source = node.source?.value;
776
+ if (source === "rxjs") for (const specifier of node.specifiers ?? []) {
181
777
  if (specifier.type === "ImportSpecifier") {
182
778
  const importedName = getPropertyName(specifier.imported);
183
- if (importedName === "inject") injectNames.add(specifier.local.name);
184
- if (importedName === "Injector") injectorNames.add(specifier.local.name);
185
- if (importedName === "runInInjectionContext") runInInjectionContextNames.add(specifier.local.name);
186
- if (importedName === "runInContext") runInContextNames.add(specifier.local.name);
779
+ if (importedName === "Subscription") subscriptionLocalNames.add(specifier.local.name);
780
+ if (RXJS_SUBSCRIBABLE_NAMES.has(importedName ?? "")) rxjsSubscribableLocalNames.add(specifier.local.name);
781
+ continue;
782
+ }
783
+ if (specifier.type === "ImportNamespaceSpecifier") rxjsNamespaces.add(specifier.local.name);
784
+ }
785
+ if (source === "@angular/core/rxjs-interop") for (const specifier of node.specifiers ?? []) {
786
+ if (specifier.type === "ImportSpecifier") {
787
+ if (getPropertyName(specifier.imported) === "takeUntilDestroyed") takeUntilDestroyedLocalNames.add(specifier.local.name);
788
+ continue;
187
789
  }
188
- if (specifier.type === "ImportNamespaceSpecifier") angularNamespaces.add(specifier.local.name);
790
+ if (specifier.type === "ImportNamespaceSpecifier") interopNamespaces.add(specifier.local.name);
189
791
  }
190
792
  },
191
- CallExpression(node) {
192
- const callNode = node;
193
- const options = context.options[0] ?? {};
194
- const disallowInjectInjector = options.disallowInjectInjector ?? DEFAULT_DISALLOW_INJECT_INJECTOR;
195
- const disallowRunInInjectionContext = options.disallowRunInInjectionContext ?? DEFAULT_DISALLOW_RUN_IN_INJECTION_CONTEXT;
196
- if (disallowInjectInjector && isDisallowedInjectInjector(callNode, injectNames, injectorNames, angularNamespaces)) context.report({
197
- node: callNode.callee,
198
- messageId: "avoidInjectInjector"
199
- });
200
- if (disallowRunInInjectionContext && isDisallowedRunInInjectionContext(callNode, runInInjectionContextNames, runInContextNames, angularNamespaces)) context.report({
201
- node: callNode.callee,
202
- messageId: "avoidRunInInjectionContext"
793
+ ClassBody(node) {
794
+ const classBody = node;
795
+ const classNode = classBody.parent;
796
+ if (!classNode || !hasTargetDecorator$3(context, classNode)) return;
797
+ const rxjsSubscribableReferences = collectRxjsSubscribableReferences(context, classBody, classNode, rxjsSubscribableLocalNames, rxjsNamespaces);
798
+ const subscriptionReferences = collectSubscriptionReferences(context, classBody, classNode, subscriptionLocalNames, rxjsNamespaces, rxjsSubscribableReferences, rxjsSubscribableLocalNames, takeUntilDestroyedLocalNames, interopNamespaces);
799
+ walkNode$1(classBody, classNode, (current) => {
800
+ if (FIELD_NODE_TYPES$2.has(current.type)) {
801
+ if (hasSubscriptionTypeReference(context, current.typeAnnotation?.typeAnnotation, subscriptionLocalNames, rxjsNamespaces)) context.report({
802
+ node: current.key ?? current,
803
+ messageId: "explicitSubscriptionType"
804
+ });
805
+ return;
806
+ }
807
+ if (current.type === "VariableDeclarator" && current.id?.type === "Identifier" && hasSubscriptionTypeReference(context, current.id.typeAnnotation?.typeAnnotation, subscriptionLocalNames, rxjsNamespaces)) {
808
+ context.report({
809
+ node: current.id,
810
+ messageId: "explicitSubscriptionType"
811
+ });
812
+ return;
813
+ }
814
+ if (isSubscriptionConstructor(context, current, subscriptionLocalNames, rxjsNamespaces)) {
815
+ context.report({
816
+ node: current,
817
+ messageId: "explicitSubscriptionConstructor"
818
+ });
819
+ return false;
820
+ }
821
+ if (isSubscriptionAddCall(context, current, classNode, subscriptionReferences, rxjsSubscribableReferences, rxjsSubscribableLocalNames, rxjsNamespaces, takeUntilDestroyedLocalNames, interopNamespaces)) {
822
+ context.report({
823
+ node: current,
824
+ messageId: "explicitSubscriptionAdd"
825
+ });
826
+ return false;
827
+ }
828
+ if (isSubscriptionUnsubscribeCall(context, current, subscriptionReferences)) {
829
+ context.report({
830
+ node: current,
831
+ messageId: "explicitUnsubscribe"
832
+ });
833
+ return false;
834
+ }
835
+ if (isUnmanagedSubscribeCall(context, current, rxjsSubscribableReferences, rxjsSubscribableLocalNames, rxjsNamespaces, takeUntilDestroyedLocalNames, interopNamespaces)) {
836
+ context.report({
837
+ node: current,
838
+ messageId: "explicitSubscribe"
839
+ });
840
+ return false;
841
+ }
203
842
  });
204
843
  }
205
844
  };
@@ -210,9 +849,15 @@ const avoidExplicitInjectionContext = defineRule({
210
849
  const DEFAULT_ALLOW_FOR_GROUPING = true;
211
850
  const DEFAULT_ALLOW_FOR_PROVIDING = false;
212
851
  const DEFAULT_ALLOW_FOR_ROUTING = false;
213
- function getNgModuleMetadata(node) {
214
- if (node.type !== "Decorator") return null;
215
- if (getDecoratorName(node) !== "NgModule") return null;
852
+ function isNgModuleDecorator(context, node) {
853
+ if (node.type !== "Decorator") return false;
854
+ const expression = node.expression ?? node;
855
+ const callee = expression?.type === "CallExpression" ? expression.callee : expression;
856
+ if (callee?.type === "Identifier") return getImportedName(context, callee, "@angular/core") === "NgModule";
857
+ return callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && isNamespaceImport(context, callee.object, "@angular/core") && getPropertyName(callee.property) === "NgModule";
858
+ }
859
+ function getNgModuleMetadata(context, node) {
860
+ if (!isNgModuleDecorator(context, node)) return null;
216
861
  const expression = node.expression;
217
862
  if (expression?.type !== "CallExpression") return null;
218
863
  const metadata = expression.arguments?.[0];
@@ -226,8 +871,16 @@ function getArrayElementsFromProperty(metadata, propertyName) {
226
871
  function isMemberCall(node, methodName) {
227
872
  return node.type === "CallExpression" && getPropertyName(node.callee?.property) === methodName;
228
873
  }
229
- function isRouterModuleForChild(node) {
230
- return isMemberCall(node, "forChild") && node.callee?.type === "MemberExpression" && getPropertyName(node.callee.object) === "RouterModule";
874
+ function isModuleCallFromImport(context, node, expectedSource, expectedModuleName) {
875
+ if (node.type !== "CallExpression") return false;
876
+ const callee = node.callee;
877
+ if (callee?.type !== "MemberExpression") return false;
878
+ const moduleRef = callee.object;
879
+ if (moduleRef?.type === "Identifier") return getImportedName(context, moduleRef, expectedSource) === expectedModuleName;
880
+ if (moduleRef?.type !== "MemberExpression") return false;
881
+ if (moduleRef.object?.type !== "Identifier") return false;
882
+ if (!isNamespaceImport(context, moduleRef.object, expectedSource)) return false;
883
+ return getPropertyName(moduleRef.property) === expectedModuleName;
231
884
  }
232
885
  function getNgModuleClassNode(decoratorNode) {
233
886
  const parent = decoratorNode.parent;
@@ -264,45 +917,279 @@ const avoidNgModules = defineRule({
264
917
  }
265
918
  }],
266
919
  messages: {
267
- avoidModuleImportsExports: "NgModule imports/exports are legacy composition. Prefer using standalone things directly.",
268
- avoidForRoot: "Avoid module.forRoot(...). Prefer provideX(...) functions to register injection providers.",
269
- avoidRouterForChild: "Avoid RouterModule.forChild(routes). Prefer provideRouter(...) and lazy route entries such as loadComponent: () => import('./components/auth/login-page')."
920
+ avoidModuleImportsExports: "NgModule imports/exports are legacy composition. Prefer using standalone things directly.",
921
+ avoidForRoot: "Avoid module.forRoot(...). Prefer provideX(...) functions to register injection providers.",
922
+ avoidRouterForRoot: "Avoid RouterModule.forRoot(routes). Prefer provideRouter(routes). See https://angular.dev/guide/routing/define-routes",
923
+ avoidRouterForChild: "Avoid RouterModule.forChild(routes). Prefer provideRouter(...) and lazy route entries such as loadComponent: () => import('./components/auth/login-page').",
924
+ avoidNgrxStoreForRoot: "Avoid StoreModule.forRoot(...). Prefer provideStore(...) with standalone providers. See https://ngrx.io/guide/store",
925
+ avoidNgrxEffectsForRoot: "Avoid EffectsModule.forRoot(...). Prefer provideEffects(...) with standalone providers. See https://ngrx.io/guide/effects",
926
+ avoidNgrxStoreForFeature: "Avoid StoreModule.forFeature(...). Prefer provideState(...) with standalone providers. See https://ngrx.io/guide/store",
927
+ avoidNgrxEffectsForFeature: "Avoid EffectsModule.forFeature(...). Prefer provideEffects(...) scoped to feature providers. See https://ngrx.io/guide/effects",
928
+ avoidNgrxStoreDevtoolsInstrument: "Avoid StoreDevtoolsModule.instrument(...). Prefer provideStoreDevtools(...) with standalone providers. See https://ngrx.io/guide/store-devtools",
929
+ avoidNgrxRouterStoreForRoot: "Avoid StoreRouterConnectingModule.forRoot(...). Prefer provideRouterStore(...) with standalone providers. See https://ngrx.io/guide/router-store"
930
+ }
931
+ },
932
+ createOnce(context) {
933
+ return { Decorator(node) {
934
+ const options = context.options[0] ?? {};
935
+ const allowForGrouping = options.allowForGrouping ?? DEFAULT_ALLOW_FOR_GROUPING;
936
+ const allowForProviding = options.allowForProviding ?? DEFAULT_ALLOW_FOR_PROVIDING;
937
+ const allowForRouting = options.allowForRouting ?? DEFAULT_ALLOW_FOR_ROUTING;
938
+ const metadata = getNgModuleMetadata(context, node);
939
+ if (!metadata) return;
940
+ const importsElements = getArrayElementsFromProperty(metadata, "imports");
941
+ const exportsElements = getArrayElementsFromProperty(metadata, "exports");
942
+ if (!allowForGrouping && (importsElements.length > 0 || exportsElements.length > 0)) context.report({
943
+ node,
944
+ messageId: "avoidModuleImportsExports"
945
+ });
946
+ const importCalls = importsElements.filter((element) => element.type === "CallExpression");
947
+ if (!allowForProviding) {
948
+ for (const call of importCalls) {
949
+ const methodName = getPropertyName(call.callee?.property);
950
+ if (methodName === "forRoot" && isModuleCallFromImport(context, call, "@angular/router", "RouterModule") && !allowForRouting) {
951
+ context.report({
952
+ node: call,
953
+ messageId: "avoidRouterForRoot"
954
+ });
955
+ continue;
956
+ }
957
+ if (methodName === "forRoot" && isModuleCallFromImport(context, call, "@ngrx/store", "StoreModule")) {
958
+ context.report({
959
+ node: call,
960
+ messageId: "avoidNgrxStoreForRoot"
961
+ });
962
+ continue;
963
+ }
964
+ if (methodName === "forRoot" && isModuleCallFromImport(context, call, "@ngrx/effects", "EffectsModule")) {
965
+ context.report({
966
+ node: call,
967
+ messageId: "avoidNgrxEffectsForRoot"
968
+ });
969
+ continue;
970
+ }
971
+ if (methodName === "forFeature" && isModuleCallFromImport(context, call, "@ngrx/store", "StoreModule")) {
972
+ context.report({
973
+ node: call,
974
+ messageId: "avoidNgrxStoreForFeature"
975
+ });
976
+ continue;
977
+ }
978
+ if (methodName === "forFeature" && isModuleCallFromImport(context, call, "@ngrx/effects", "EffectsModule")) {
979
+ context.report({
980
+ node: call,
981
+ messageId: "avoidNgrxEffectsForFeature"
982
+ });
983
+ continue;
984
+ }
985
+ if (methodName === "instrument" && isModuleCallFromImport(context, call, "@ngrx/store-devtools", "StoreDevtoolsModule")) {
986
+ context.report({
987
+ node: call,
988
+ messageId: "avoidNgrxStoreDevtoolsInstrument"
989
+ });
990
+ continue;
991
+ }
992
+ if (methodName === "forRoot" && isModuleCallFromImport(context, call, "@ngrx/router-store", "StoreRouterConnectingModule")) {
993
+ context.report({
994
+ node: call,
995
+ messageId: "avoidNgrxRouterStoreForRoot"
996
+ });
997
+ continue;
998
+ }
999
+ if (methodName === "forRoot" && isModuleCallFromImport(context, call, "@angular/router", "RouterModule") && allowForRouting) continue;
1000
+ if (!isMemberCall(call, "forRoot")) continue;
1001
+ context.report({
1002
+ node: call,
1003
+ messageId: "avoidForRoot"
1004
+ });
1005
+ }
1006
+ const staticForRootMethod = getStaticForRootMethod(getNgModuleClassNode(node));
1007
+ if (staticForRootMethod) context.report({
1008
+ node: staticForRootMethod.key ?? staticForRootMethod,
1009
+ messageId: "avoidForRoot"
1010
+ });
1011
+ }
1012
+ if (!allowForRouting) for (const call of importCalls) {
1013
+ if (!isMemberCall(call, "forChild")) continue;
1014
+ if (!isModuleCallFromImport(context, call, "@angular/router", "RouterModule")) continue;
1015
+ context.report({
1016
+ node: call,
1017
+ messageId: "avoidRouterForChild"
1018
+ });
1019
+ }
1020
+ } };
1021
+ }
1022
+ });
1023
+ //#endregion
1024
+ //#region src/rules/avoid-rxjs-state-in-component/index.ts
1025
+ const TARGET_DECORATORS$1 = new Set(["Component", "Directive"]);
1026
+ const SUBJECT_NAMES = new Set([
1027
+ "BehaviorSubject",
1028
+ "ReplaySubject",
1029
+ "Subject"
1030
+ ]);
1031
+ const FIELD_NODE_TYPES$1 = new Set([
1032
+ "AccessorProperty",
1033
+ "FieldDefinition",
1034
+ "PropertyDefinition"
1035
+ ]);
1036
+ function hasTargetDecorator$2(context, classNode) {
1037
+ if (!classNode || !Array.isArray(classNode.decorators)) return false;
1038
+ return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS$1));
1039
+ }
1040
+ function getImportedSubjectKind(context, node) {
1041
+ const importedName = getImportedName(context, node, "rxjs");
1042
+ return SUBJECT_NAMES.has(importedName) ? importedName : null;
1043
+ }
1044
+ function getSubjectKindFromType(context, node) {
1045
+ if (!node) return null;
1046
+ if (node.type !== "TSTypeReference") return null;
1047
+ if (node.typeName?.type === "Identifier") return getImportedSubjectKind(context, node.typeName);
1048
+ if (node.typeName?.type === "TSQualifiedName" && node.typeName.left?.type === "Identifier") {
1049
+ const memberName = getPropertyName(node.typeName.right);
1050
+ return isNamespaceImport(context, node.typeName.left, "rxjs") && SUBJECT_NAMES.has(memberName) ? memberName : null;
1051
+ }
1052
+ return null;
1053
+ }
1054
+ function getSubjectKindFromConstructor(context, node) {
1055
+ const expression = unwrapExpression(node);
1056
+ if (expression?.type !== "NewExpression") return null;
1057
+ const callee = unwrapExpression(expression.callee);
1058
+ if (callee?.type === "Identifier") return getImportedSubjectKind(context, callee);
1059
+ if (callee?.type !== "MemberExpression") return null;
1060
+ if (callee.object?.type !== "Identifier" || !isNamespaceImport(context, callee.object, "rxjs")) return null;
1061
+ const memberName = getPropertyName(callee.property);
1062
+ return SUBJECT_NAMES.has(memberName) ? memberName : null;
1063
+ }
1064
+ function getMemberName(node) {
1065
+ const expression = unwrapExpression(node);
1066
+ if (!expression) return null;
1067
+ if (expression.type === "Identifier") return expression.name;
1068
+ if (expression.type === "PrivateIdentifier") return expression.name;
1069
+ return null;
1070
+ }
1071
+ function getThisFieldName(node) {
1072
+ const expression = unwrapExpression(node);
1073
+ if (expression?.type !== "MemberExpression") return null;
1074
+ if (expression.object?.type !== "ThisExpression") return null;
1075
+ return getPropertyName(expression.property);
1076
+ }
1077
+ function isCallOnThisField(node, fields) {
1078
+ const callNode = unwrapExpression(node);
1079
+ if (callNode?.type !== "CallExpression") return null;
1080
+ const callee = unwrapExpression(callNode.callee);
1081
+ if (callee?.type !== "MemberExpression") return null;
1082
+ const fieldName = getThisFieldName(callee.object);
1083
+ const methodName = getPropertyName(callee.property);
1084
+ if (!fieldName || !methodName || !fields.has(fieldName)) return null;
1085
+ return {
1086
+ fieldName,
1087
+ methodName,
1088
+ argumentCount: callNode.arguments?.length ?? 0
1089
+ };
1090
+ }
1091
+ function isNgOnDestroyMethod(member) {
1092
+ return member.type === "MethodDefinition" && getPropertyName(member.key) === "ngOnDestroy" && !!member.value;
1093
+ }
1094
+ function walkNode(node, visitor) {
1095
+ if (!node) return;
1096
+ if (Array.isArray(node)) {
1097
+ for (const child of node) walkNode(child, visitor);
1098
+ return;
1099
+ }
1100
+ visitor(node);
1101
+ for (const [key, value] of Object.entries(node)) {
1102
+ if (key === "parent") continue;
1103
+ if (!value || typeof value !== "object") continue;
1104
+ walkNode(value, visitor);
1105
+ }
1106
+ }
1107
+ function getUsage(usages, fieldName) {
1108
+ const existing = usages.get(fieldName);
1109
+ if (existing) return existing;
1110
+ const next = {
1111
+ hasAsObservable: false,
1112
+ hasNextWithValue: false,
1113
+ hasNgOnDestroyComplete: false,
1114
+ hasNgOnDestroyNext: false
1115
+ };
1116
+ usages.set(fieldName, next);
1117
+ return next;
1118
+ }
1119
+ function collectSubjectFields(context, classBody) {
1120
+ const fields = /* @__PURE__ */ new Map();
1121
+ for (const member of classBody.body ?? []) {
1122
+ if (!FIELD_NODE_TYPES$1.has(member.type)) continue;
1123
+ const fieldName = getMemberName(member.key);
1124
+ if (!fieldName) continue;
1125
+ const typeNode = member.typeAnnotation?.typeAnnotation;
1126
+ const kind = getSubjectKindFromConstructor(context, member.value) ?? getSubjectKindFromType(context, typeNode);
1127
+ if (!kind) continue;
1128
+ fields.set(fieldName, {
1129
+ key: member.key ?? member,
1130
+ kind
1131
+ });
1132
+ }
1133
+ return fields;
1134
+ }
1135
+ function collectFieldUsages(classBody, fields) {
1136
+ const usages = /* @__PURE__ */ new Map();
1137
+ const ngOnDestroyMethods = /* @__PURE__ */ new Set();
1138
+ for (const member of classBody.body ?? []) if (isNgOnDestroyMethod(member)) ngOnDestroyMethods.add(member.value);
1139
+ walkNode(classBody, (node) => {
1140
+ const call = isCallOnThisField(node, fields);
1141
+ if (!call) return;
1142
+ const usage = getUsage(usages, call.fieldName);
1143
+ if (call.methodName === "asObservable") usage.hasAsObservable = true;
1144
+ if (call.methodName === "next" && call.argumentCount > 0) usage.hasNextWithValue = true;
1145
+ const isInsideNgOnDestroy = ngOnDestroyMethods.has(node.parent) || ngOnDestroyMethods.has(node);
1146
+ let current = node.parent;
1147
+ while (!isInsideNgOnDestroy && current) {
1148
+ if (ngOnDestroyMethods.has(current)) break;
1149
+ current = current.parent;
1150
+ }
1151
+ if (!current && !ngOnDestroyMethods.has(node.parent) && !ngOnDestroyMethods.has(node)) return;
1152
+ if (call.methodName === "next") usage.hasNgOnDestroyNext = true;
1153
+ if (call.methodName === "complete") usage.hasNgOnDestroyComplete = true;
1154
+ });
1155
+ return usages;
1156
+ }
1157
+ function isDestroySubjectUsage(usage) {
1158
+ return !!usage?.hasNgOnDestroyNext && !!usage.hasNgOnDestroyComplete;
1159
+ }
1160
+ const avoidRxjsStateInComponent = defineRule({
1161
+ meta: {
1162
+ type: "suggestion",
1163
+ docs: {
1164
+ description: "Avoid using RxJS Subject classes for component or directive state and destruction lifecycle.",
1165
+ recommended: true
1166
+ },
1167
+ schema: [],
1168
+ messages: {
1169
+ avoidRxjsState: "Avoid using RxJS {{kind}} for component/directive state. Prefer signal(), computed(), or linkedSignal().",
1170
+ avoidDestroySubject: "Avoid destroy Subject lifecycle management. Prefer takeUntilDestroyed() from @angular/core/rxjs-interop."
270
1171
  }
271
1172
  },
272
1173
  createOnce(context) {
273
- return { Decorator(node) {
274
- const options = context.options[0] ?? {};
275
- const allowForGrouping = options.allowForGrouping ?? DEFAULT_ALLOW_FOR_GROUPING;
276
- const allowForProviding = options.allowForProviding ?? DEFAULT_ALLOW_FOR_PROVIDING;
277
- const allowForRouting = options.allowForRouting ?? DEFAULT_ALLOW_FOR_ROUTING;
278
- const metadata = getNgModuleMetadata(node);
279
- if (!metadata) return;
280
- const importsElements = getArrayElementsFromProperty(metadata, "imports");
281
- const exportsElements = getArrayElementsFromProperty(metadata, "exports");
282
- if (!allowForGrouping && (importsElements.length > 0 || exportsElements.length > 0)) context.report({
283
- node,
284
- messageId: "avoidModuleImportsExports"
285
- });
286
- const importCalls = importsElements.filter((element) => element.type === "CallExpression");
287
- if (!allowForProviding) {
288
- for (const call of importCalls) {
289
- if (!isMemberCall(call, "forRoot")) continue;
1174
+ return { ClassBody(node) {
1175
+ const classBody = node;
1176
+ const classNode = classBody.parent;
1177
+ if (!hasTargetDecorator$2(context, classNode)) return;
1178
+ const fields = collectSubjectFields(context, classBody);
1179
+ const usages = collectFieldUsages(classBody, fields);
1180
+ for (const [fieldName, field] of fields) {
1181
+ const usage = usages.get(fieldName);
1182
+ if (field.kind === "Subject" && isDestroySubjectUsage(usage)) {
290
1183
  context.report({
291
- node: call,
292
- messageId: "avoidForRoot"
1184
+ node: field.key,
1185
+ messageId: "avoidDestroySubject"
293
1186
  });
1187
+ continue;
294
1188
  }
295
- const staticForRootMethod = getStaticForRootMethod(getNgModuleClassNode(node));
296
- if (staticForRootMethod) context.report({
297
- node: staticForRootMethod.key ?? staticForRootMethod,
298
- messageId: "avoidForRoot"
299
- });
300
- }
301
- if (!allowForRouting) for (const call of importCalls) {
302
- if (!isRouterModuleForChild(call)) continue;
303
1189
  context.report({
304
- node: call,
305
- messageId: "avoidRouterForChild"
1190
+ node: field.key,
1191
+ messageId: "avoidRxjsState",
1192
+ data: { kind: field.kind }
306
1193
  });
307
1194
  }
308
1195
  } };
@@ -322,39 +1209,49 @@ const KNOWN_SIGNAL_CREATION_FUNCTIONS = new Set([
322
1209
  ]);
323
1210
  const LINKED_SIGNAL_CREATOR_NAME = "linkedSignal";
324
1211
  const COMPUTED_CREATOR_NAME = "computed";
325
- const EFFECT_CREATOR_NAME = "effect";
326
- function isAngularCoreNamespaceMember(node, angularNamespaces, memberName) {
327
- return node?.type === "MemberExpression" && node.object?.type === "Identifier" && angularNamespaces.has(node.object.name) && getPropertyName(node.property) === memberName;
328
- }
329
- function isSignalCreatorCall(node, signalCreatorNames, angularNamespaces) {
1212
+ const EFFECT_CREATOR_NAMES = new Set(["effect"]);
1213
+ const COMPUTED_CREATOR_NAMES = new Set([COMPUTED_CREATOR_NAME]);
1214
+ const LINKED_SIGNAL_CREATOR_NAMES = new Set([LINKED_SIGNAL_CREATOR_NAME]);
1215
+ function isSignalCreatorCall(context, node) {
330
1216
  if (node?.type !== "CallExpression") return false;
331
1217
  const callee = node.callee;
332
- return callee?.type === "Identifier" && signalCreatorNames.has(callee.name) || [...KNOWN_SIGNAL_CREATION_FUNCTIONS].some((name) => isAngularCoreNamespaceMember(callee, angularNamespaces, name));
1218
+ return isImportedReference(context, callee, "@angular/core", KNOWN_SIGNAL_CREATION_FUNCTIONS) || isImportedNamespaceMember(context, callee, "@angular/core", KNOWN_SIGNAL_CREATION_FUNCTIONS);
333
1219
  }
334
- function isEffectCall(node, effectNames, angularNamespaces) {
1220
+ function isEffectCall(context, node) {
335
1221
  const callee = node.callee;
336
- return callee?.type === "Identifier" && effectNames.has(callee.name) || isAngularCoreNamespaceMember(callee, angularNamespaces, "effect");
1222
+ return isImportedReference(context, callee, "@angular/core", EFFECT_CREATOR_NAMES) || isImportedNamespaceMember(context, callee, "@angular/core", EFFECT_CREATOR_NAMES);
337
1223
  }
338
- function isReactiveCreatorCall(node, creatorNames, angularNamespaces, angularMemberName) {
1224
+ function isReactiveCreatorCall(context, node, creatorNames) {
339
1225
  const callee = node.callee;
340
- return callee?.type === "Identifier" && creatorNames.has(callee.name) || isAngularCoreNamespaceMember(callee, angularNamespaces, angularMemberName);
1226
+ return isImportedReference(context, callee, "@angular/core", creatorNames) || isImportedNamespaceMember(context, callee, "@angular/core", creatorNames);
341
1227
  }
342
- function isKnownSignalObject(objectNode, signalVariables, classSignalProperties) {
1228
+ function isKnownSignalObject(context, objectNode, signalVariableBindings, classSignalProperties) {
343
1229
  if (!objectNode) return false;
344
- if (objectNode.type === "Identifier") return signalVariables.has(objectNode.name);
1230
+ if (objectNode.type === "Identifier") {
1231
+ const trackedBindings = signalVariableBindings.get(objectNode.name);
1232
+ if (!trackedBindings) return false;
1233
+ const nearestBinding = findNearestBindingIdentifier(context, objectNode);
1234
+ return !!nearestBinding && trackedBindings.has(nearestBinding);
1235
+ }
345
1236
  return objectNode.type === "MemberExpression" && objectNode.object?.type === "ThisExpression" && objectNode.property?.type === "Identifier" && classSignalProperties.has(objectNode.property.name);
346
1237
  }
347
- function visitNodes$1(node, visitor) {
1238
+ const FUNCTION_NODE_TYPES = new Set([
1239
+ "ArrowFunctionExpression",
1240
+ "FunctionExpression",
1241
+ "FunctionDeclaration"
1242
+ ]);
1243
+ function visitNodes(node, visitor, skipFunctionBodies = false) {
348
1244
  if (!node) return;
349
1245
  if (Array.isArray(node)) {
350
- for (const item of node) visitNodes$1(item, visitor);
1246
+ for (const item of node) visitNodes(item, visitor, skipFunctionBodies);
351
1247
  return;
352
1248
  }
1249
+ if (skipFunctionBodies && FUNCTION_NODE_TYPES.has(node.type)) return;
353
1250
  visitor(node);
354
1251
  for (const [key, value] of Object.entries(node)) {
355
1252
  if (key === "parent") continue;
356
1253
  if (!value || typeof value !== "object") continue;
357
- visitNodes$1(value, visitor);
1254
+ visitNodes(value, visitor, skipFunctionBodies);
358
1255
  }
359
1256
  }
360
1257
  const avoidWritingSignalsInReactiveContext = defineRule({
@@ -381,46 +1278,25 @@ const avoidWritingSignalsInReactiveContext = defineRule({
381
1278
  messages: { avoidSignalWriteInReactiveContext: "Avoid setting signal values inside {{contextName}}; move writes outside reactive derivations and effects." }
382
1279
  },
383
1280
  createOnce(context) {
384
- const effectNames = /* @__PURE__ */ new Set();
385
- const computedNames = /* @__PURE__ */ new Set();
386
- const linkedSignalNames = /* @__PURE__ */ new Set();
387
- const signalCreatorNames = /* @__PURE__ */ new Set();
388
- const angularNamespaces = /* @__PURE__ */ new Set();
389
- const signalVariables = /* @__PURE__ */ new Set();
1281
+ const signalVariableBindings = /* @__PURE__ */ new Map();
390
1282
  const classSignalProperties = /* @__PURE__ */ new Set();
391
1283
  return {
392
1284
  before() {
393
- effectNames.clear();
394
- computedNames.clear();
395
- linkedSignalNames.clear();
396
- signalCreatorNames.clear();
397
- angularNamespaces.clear();
398
- signalVariables.clear();
1285
+ signalVariableBindings.clear();
399
1286
  classSignalProperties.clear();
400
1287
  },
401
- ImportDeclaration(node) {
402
- if (node.source?.value !== "@angular/core") return;
403
- for (const specifier of node.specifiers ?? []) {
404
- if (specifier.type === "ImportSpecifier") {
405
- const importedName = getPropertyName(specifier.imported);
406
- if (importedName === EFFECT_CREATOR_NAME) effectNames.add(specifier.local.name);
407
- if (importedName === COMPUTED_CREATOR_NAME) computedNames.add(specifier.local.name);
408
- if (importedName === LINKED_SIGNAL_CREATOR_NAME) linkedSignalNames.add(specifier.local.name);
409
- if (importedName && KNOWN_SIGNAL_CREATION_FUNCTIONS.has(importedName)) signalCreatorNames.add(specifier.local.name);
410
- }
411
- if (specifier.type === "ImportNamespaceSpecifier") angularNamespaces.add(specifier.local.name);
412
- }
413
- },
414
1288
  VariableDeclarator(node) {
415
1289
  const declarator = node;
416
1290
  if (declarator.id?.type !== "Identifier") return;
417
- if (!isSignalCreatorCall(declarator.init, signalCreatorNames, angularNamespaces)) return;
418
- signalVariables.add(declarator.id.name);
1291
+ if (!isSignalCreatorCall(context, declarator.init)) return;
1292
+ const bindings = signalVariableBindings.get(declarator.id.name) ?? /* @__PURE__ */ new Set();
1293
+ bindings.add(declarator.id);
1294
+ signalVariableBindings.set(declarator.id.name, bindings);
419
1295
  },
420
1296
  "PropertyDefinition, FieldDefinition, AccessorProperty"(node) {
421
1297
  const property = node;
422
1298
  if (property.key?.type !== "Identifier") return;
423
- if (!isSignalCreatorCall(property.value, signalCreatorNames, angularNamespaces)) return;
1299
+ if (!isSignalCreatorCall(context, property.value)) return;
424
1300
  classSignalProperties.add(property.key.name);
425
1301
  },
426
1302
  CallExpression(node) {
@@ -429,21 +1305,21 @@ const avoidWritingSignalsInReactiveContext = defineRule({
429
1305
  const allowComputedAndLinkedSignals = options.allowComputedAndLinkedSignals ?? false;
430
1306
  const callNode = node;
431
1307
  const callbackCandidates = [];
432
- if (!allowEffects && isEffectCall(callNode, effectNames, angularNamespaces)) {
1308
+ if (!allowEffects && isEffectCall(context, callNode)) {
433
1309
  const callback = callNode.arguments?.[0];
434
1310
  if (callback?.type === "ArrowFunctionExpression" || callback?.type === "FunctionExpression") callbackCandidates.push({
435
1311
  callback,
436
1312
  contextName: "effect()"
437
1313
  });
438
1314
  }
439
- if (!allowComputedAndLinkedSignals && isReactiveCreatorCall(callNode, computedNames, angularNamespaces, COMPUTED_CREATOR_NAME)) {
1315
+ if (!allowComputedAndLinkedSignals && isReactiveCreatorCall(context, callNode, COMPUTED_CREATOR_NAMES)) {
440
1316
  const callback = callNode.arguments?.[0];
441
1317
  if (callback?.type === "ArrowFunctionExpression" || callback?.type === "FunctionExpression") callbackCandidates.push({
442
1318
  callback,
443
1319
  contextName: "computed()"
444
1320
  });
445
1321
  }
446
- if (!allowComputedAndLinkedSignals && isReactiveCreatorCall(callNode, linkedSignalNames, angularNamespaces, LINKED_SIGNAL_CREATOR_NAME)) for (const argumentNode of callNode.arguments ?? []) {
1322
+ if (!allowComputedAndLinkedSignals && isReactiveCreatorCall(context, callNode, LINKED_SIGNAL_CREATOR_NAMES)) for (const argumentNode of callNode.arguments ?? []) {
447
1323
  const argument = argumentNode;
448
1324
  if (argument.type === "ArrowFunctionExpression" || argument.type === "FunctionExpression") {
449
1325
  callbackCandidates.push({
@@ -464,77 +1340,167 @@ const avoidWritingSignalsInReactiveContext = defineRule({
464
1340
  });
465
1341
  }
466
1342
  }
467
- for (const { callback, contextName } of callbackCandidates) visitNodes$1(callback.body, (current) => {
1343
+ for (const { callback, contextName } of callbackCandidates) visitNodes(callback.body, (current) => {
468
1344
  if (current.type !== "CallExpression") return;
469
1345
  const callee = current.callee;
470
1346
  if (callee?.type !== "MemberExpression") return;
471
1347
  const methodName = getPropertyName(callee.property);
472
1348
  if (!methodName || !SIGNAL_WRITE_METHODS.has(methodName)) return;
473
- if (!isKnownSignalObject(callee.object, signalVariables, classSignalProperties)) return;
1349
+ if (!isKnownSignalObject(context, callee.object, signalVariableBindings, classSignalProperties)) return;
474
1350
  context.report({
475
1351
  node: callee.property ?? callee,
476
1352
  messageId: "avoidSignalWriteInReactiveContext",
477
1353
  data: { contextName }
478
1354
  });
479
- });
1355
+ }, true);
480
1356
  },
481
- "Program:exit"() {
482
- effectNames.clear();
483
- computedNames.clear();
484
- linkedSignalNames.clear();
485
- signalCreatorNames.clear();
486
- angularNamespaces.clear();
487
- signalVariables.clear();
1357
+ after() {
1358
+ signalVariableBindings.clear();
488
1359
  classSignalProperties.clear();
489
1360
  }
490
1361
  };
491
1362
  }
492
1363
  });
493
1364
  //#endregion
494
- //#region src/rules/component-class-matches-filename/index.ts
1365
+ //#region src/rules/class-matches-filename/index.ts
1366
+ const CLASS_FILENAME_MATCHERS = [
1367
+ {
1368
+ pattern: /^(.*)\.component\.ts$/u,
1369
+ decoratorNames: ["Component"]
1370
+ },
1371
+ {
1372
+ pattern: /^(.*)\.directive\.ts$/u,
1373
+ decoratorNames: ["Directive"]
1374
+ },
1375
+ {
1376
+ pattern: /^(.*)\.service\.ts$/u,
1377
+ decoratorNames: ["Service", "Injectable"]
1378
+ }
1379
+ ];
495
1380
  function toPascalCase(raw) {
496
1381
  return raw.split(/[-_\s.]+/u).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
497
1382
  }
498
- function getExpectedComponentClassName(filename) {
1383
+ function getClassKind(suffix) {
1384
+ return suffix.toLowerCase();
1385
+ }
1386
+ function getExpectedClassNames(filename, ignoreClassSuffix) {
499
1387
  const base = filename.split(/[/\\]/u).at(-1) ?? "";
500
- const match = /^(.*)\.component\.ts$/u.exec(base);
501
- if (!match) return null;
502
- const stem = match[1];
503
- const pascal = toPascalCase(stem);
504
- return pascal ? `${pascal}Component` : null;
1388
+ for (const { decoratorNames, pattern } of CLASS_FILENAME_MATCHERS) {
1389
+ const match = pattern.exec(base);
1390
+ if (!match) continue;
1391
+ const stem = match[1];
1392
+ const pascal = toPascalCase(stem);
1393
+ if (!pascal) return null;
1394
+ const suffix = decoratorNames[0];
1395
+ return ignoreClassSuffix ? [`${pascal}${suffix}`, pascal] : [`${pascal}${suffix}`];
1396
+ }
1397
+ return null;
1398
+ }
1399
+ function getFileMatcher(filename) {
1400
+ const base = filename.split(/[/\\]/u).at(-1) ?? "";
1401
+ return CLASS_FILENAME_MATCHERS.find(({ pattern }) => pattern.test(base)) ?? null;
505
1402
  }
506
- const componentClassMatchesFilename = defineRule({
1403
+ function getIgnoreClassSuffixOption(options, kind) {
1404
+ const ignoreClassSuffix = options.ignoreClassSuffix ?? false;
1405
+ if (typeof ignoreClassSuffix === "boolean") return ignoreClassSuffix;
1406
+ return ignoreClassSuffix[kind] ?? false;
1407
+ }
1408
+ function getFileKind(matcher) {
1409
+ return getClassKind(matcher.decoratorNames[0]);
1410
+ }
1411
+ function formatExpectedNames(expectedNames) {
1412
+ return expectedNames.join("' or '");
1413
+ }
1414
+ function getNodeName(node) {
1415
+ return node.id?.name ?? null;
1416
+ }
1417
+ function getBaseFilename$1(filename) {
1418
+ return filename.split(/[/\\]/u).at(-1) ?? filename;
1419
+ }
1420
+ function hasTargetDecorator$1(context, classNode, decoratorNames) {
1421
+ if (!Array.isArray(classNode.decorators)) return false;
1422
+ return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, decoratorNames));
1423
+ }
1424
+ function getDecoratedClassCountDescription(count) {
1425
+ return count === 0 ? "no decorated classes" : `${count} decorated classes`;
1426
+ }
1427
+ const classMatchesFilename = defineRule({
507
1428
  meta: {
508
1429
  type: "suggestion",
509
1430
  docs: {
510
- description: "Require the component class name to match its *.component.ts filename.",
1431
+ description: "Require Angular component, directive, and service class names to match their filenames.",
511
1432
  recommended: true
512
1433
  },
513
- schema: [],
514
- messages: { classNameMismatch: "Component class name should be '{{expectedName}}' to match the filename '{{filename}}'." }
1434
+ schema: [{
1435
+ type: "object",
1436
+ additionalProperties: false,
1437
+ properties: { ignoreClassSuffix: {
1438
+ anyOf: [{ type: "boolean" }, {
1439
+ type: "object",
1440
+ additionalProperties: false,
1441
+ properties: {
1442
+ component: { type: "boolean" },
1443
+ directive: { type: "boolean" },
1444
+ service: { type: "boolean" }
1445
+ }
1446
+ }],
1447
+ default: false
1448
+ } }
1449
+ }],
1450
+ messages: {
1451
+ classNameMismatch: "Angular class name should be '{{expectedName}}' to match the filename '{{filename}}'.",
1452
+ decoratedClassCount: "Angular filename '{{filename}}' should contain exactly one matching decorated class, but found {{actual}}."
1453
+ }
515
1454
  },
516
1455
  createOnce(context) {
517
- const classDeclarations = [];
1456
+ let programNode = null;
1457
+ let fileMatcher = null;
1458
+ let fileDecoratorNames = /* @__PURE__ */ new Set();
1459
+ const decoratedClasses = [];
518
1460
  return {
519
1461
  before() {
520
- classDeclarations.length = 0;
1462
+ programNode = null;
1463
+ decoratedClasses.length = 0;
1464
+ fileMatcher = getFileMatcher(context.filename ?? "");
1465
+ if (!fileMatcher) return false;
1466
+ fileDecoratorNames = new Set(fileMatcher.decoratorNames);
1467
+ },
1468
+ Program(node) {
1469
+ programNode = node;
521
1470
  },
522
1471
  ClassDeclaration(node) {
523
- classDeclarations.push(node);
1472
+ if (!fileMatcher) return;
1473
+ const classNode = node;
1474
+ if (hasTargetDecorator$1(context, classNode, fileDecoratorNames)) decoratedClasses.push(classNode);
524
1475
  },
525
1476
  after() {
526
1477
  const filename = context.filename ?? "";
527
- const expectedName = getExpectedComponentClassName(filename);
528
- if (!expectedName) return;
529
- const baseFilename = filename.split(/[/\\]/u).at(-1) ?? filename;
530
- const classNode = classDeclarations.find((candidate) => candidate.parent?.type === "ExportNamedDeclaration" || candidate.parent?.type === "ExportDefaultDeclaration") ?? null ?? classDeclarations[0];
531
- if (!classNode?.id?.name) return;
532
- if (classNode.id.name === expectedName) return;
1478
+ if (!fileMatcher) return;
1479
+ const baseFilename = getBaseFilename$1(filename);
1480
+ if (decoratedClasses.length !== 1) {
1481
+ const reportNode = decoratedClasses[0]?.id ?? decoratedClasses[0] ?? programNode;
1482
+ if (!reportNode) return;
1483
+ context.report({
1484
+ node: reportNode,
1485
+ messageId: "decoratedClassCount",
1486
+ data: {
1487
+ actual: getDecoratedClassCountDescription(decoratedClasses.length),
1488
+ filename: baseFilename
1489
+ }
1490
+ });
1491
+ return;
1492
+ }
1493
+ const expectedNames = getExpectedClassNames(filename, getIgnoreClassSuffixOption(context.options[0] ?? {}, getFileKind(fileMatcher)));
1494
+ if (!expectedNames) return;
1495
+ const classNode = decoratedClasses[0];
1496
+ const className = getNodeName(classNode);
1497
+ if (!className) return;
1498
+ if (expectedNames.includes(className)) return;
533
1499
  context.report({
534
1500
  node: classNode.id,
535
1501
  messageId: "classNameMismatch",
536
1502
  data: {
537
- expectedName,
1503
+ expectedName: formatExpectedNames(expectedNames),
538
1504
  filename: baseFilename
539
1505
  }
540
1506
  });
@@ -543,13 +1509,208 @@ const componentClassMatchesFilename = defineRule({
543
1509
  }
544
1510
  });
545
1511
  //#endregion
1512
+ //#region src/rules/component-resource-filenames/index.ts
1513
+ const COMPONENT_DECORATORS$1 = new Set(["Component"]);
1514
+ const STYLE_EXTENSIONS = new Set([
1515
+ "css",
1516
+ "less",
1517
+ "sass",
1518
+ "scss"
1519
+ ]);
1520
+ function getExpectedStem(filename) {
1521
+ const base = filename.split(/[/\\]/u).at(-1) ?? "";
1522
+ if (!base.endsWith(".ts")) return null;
1523
+ return base.slice(0, -3);
1524
+ }
1525
+ function normalizeResourcePath(value) {
1526
+ return value.startsWith("./") ? value.slice(2) : value;
1527
+ }
1528
+ function getResourceBasename(value) {
1529
+ return normalizeResourcePath(value).split(/[/\\]/u).at(-1) ?? value;
1530
+ }
1531
+ function getExtension(value) {
1532
+ const basename = getResourceBasename(value);
1533
+ const index = basename.lastIndexOf(".");
1534
+ return index === -1 ? null : basename.slice(index + 1);
1535
+ }
1536
+ function getStaticString(node) {
1537
+ if (!node) return null;
1538
+ if (node.type === "Literal" || node.type === "StringLiteral") {
1539
+ if (typeof node.value !== "string") return null;
1540
+ return { value: node.value };
1541
+ }
1542
+ if (node.type === "TemplateLiteral" && node.expressions?.length === 0) return { value: node.quasis?.[0]?.value?.cooked ?? node.quasis?.[0]?.value?.raw ?? "" };
1543
+ return null;
1544
+ }
1545
+ function getComponentMetadata(context, node) {
1546
+ if (node.type !== "Decorator") return null;
1547
+ if (!isAngularCoreDecorator(context, node, COMPONENT_DECORATORS$1)) return null;
1548
+ const expression = node.expression;
1549
+ if (expression?.type !== "CallExpression") return null;
1550
+ const metadata = expression.arguments?.[0];
1551
+ return metadata?.type === "ObjectExpression" ? metadata : null;
1552
+ }
1553
+ const componentResourceFilenames = defineRule({
1554
+ meta: {
1555
+ type: "suggestion",
1556
+ docs: {
1557
+ description: "Require Angular component templateUrl and styleUrl resource filenames to match the component TypeScript filename.",
1558
+ recommended: true
1559
+ },
1560
+ schema: [],
1561
+ messages: {
1562
+ templateUrlMismatch: "Component templateUrl should be '{{expected}}' to match this component filename.",
1563
+ styleUrlMismatch: "Component style URL should be '{{expected}}' to match this component filename.",
1564
+ unsupportedStyleExtension: "Component style URL should use one of: .css, .less, .sass, or .scss."
1565
+ }
1566
+ },
1567
+ createOnce(context) {
1568
+ let expectedStem = null;
1569
+ function reportTemplateUrl(valueNode) {
1570
+ if (!expectedStem) return;
1571
+ const staticString = getStaticString(valueNode);
1572
+ if (!staticString) return;
1573
+ const expected = `./${expectedStem}.html`;
1574
+ if (normalizeResourcePath(staticString.value) === `${expectedStem}.html`) return;
1575
+ context.report({
1576
+ node: valueNode,
1577
+ messageId: "templateUrlMismatch",
1578
+ data: { expected }
1579
+ });
1580
+ }
1581
+ function reportStyleUrl(valueNode) {
1582
+ if (!expectedStem) return;
1583
+ const staticString = getStaticString(valueNode);
1584
+ if (!staticString) return;
1585
+ const extension = getExtension(staticString.value);
1586
+ if (!extension || !STYLE_EXTENSIONS.has(extension)) {
1587
+ context.report({
1588
+ node: valueNode,
1589
+ messageId: "unsupportedStyleExtension"
1590
+ });
1591
+ return;
1592
+ }
1593
+ const expected = `./${expectedStem}.${extension}`;
1594
+ if (normalizeResourcePath(staticString.value) === `${expectedStem}.${extension}`) return;
1595
+ context.report({
1596
+ node: valueNode,
1597
+ messageId: "styleUrlMismatch",
1598
+ data: { expected }
1599
+ });
1600
+ }
1601
+ return {
1602
+ before() {
1603
+ expectedStem = getExpectedStem(context.filename ?? "");
1604
+ if (!expectedStem) return false;
1605
+ },
1606
+ Decorator(node) {
1607
+ const metadata = getComponentMetadata(context, node);
1608
+ if (!metadata) return;
1609
+ for (const property of metadata.properties ?? []) {
1610
+ if (property.type !== "Property" || property.computed) continue;
1611
+ const propertyName = getPropertyName(property.key);
1612
+ if (propertyName === "templateUrl") {
1613
+ reportTemplateUrl(property.value);
1614
+ continue;
1615
+ }
1616
+ if (propertyName === "styleUrl") {
1617
+ reportStyleUrl(property.value);
1618
+ continue;
1619
+ }
1620
+ }
1621
+ }
1622
+ };
1623
+ }
1624
+ });
1625
+ //#endregion
1626
+ //#region src/rules/decorator-filename-suffix/index.ts
1627
+ const FILENAME_SUFFIX_CONVENTIONS = [
1628
+ {
1629
+ kind: "component",
1630
+ suffix: ".component.ts",
1631
+ decoratorNames: new Set(["Component"])
1632
+ },
1633
+ {
1634
+ kind: "directive",
1635
+ suffix: ".directive.ts",
1636
+ decoratorNames: new Set(["Directive"])
1637
+ },
1638
+ {
1639
+ kind: "service",
1640
+ suffix: ".service.ts",
1641
+ decoratorNames: new Set(["Service", "Injectable"])
1642
+ }
1643
+ ];
1644
+ function getBaseFilename(filename) {
1645
+ return filename.split(/[/\\]/u).at(-1) ?? filename;
1646
+ }
1647
+ function isTypeScriptFile(filename) {
1648
+ return getBaseFilename(filename).endsWith(".ts");
1649
+ }
1650
+ function getMatchingConvention(context, decorator) {
1651
+ return FILENAME_SUFFIX_CONVENTIONS.find((convention) => isAngularCoreDecorator(context, decorator, convention.decoratorNames)) ?? null;
1652
+ }
1653
+ const decoratorFilenameSuffix = defineRule({
1654
+ meta: {
1655
+ type: "suggestion",
1656
+ docs: {
1657
+ description: "Require Angular component, directive, and service decorators to be declared in files with matching suffixes.",
1658
+ recommended: true
1659
+ },
1660
+ schema: [],
1661
+ messages: { filenameSuffix: "Angular {{kind}} decorator should be declared in a file ending with '{{suffix}}'." }
1662
+ },
1663
+ createOnce(context) {
1664
+ return {
1665
+ before() {
1666
+ if (!isTypeScriptFile(context.filename ?? "")) return false;
1667
+ },
1668
+ Decorator(node) {
1669
+ const convention = getMatchingConvention(context, node);
1670
+ if (!convention) return;
1671
+ if (getBaseFilename(context.filename ?? "").endsWith(convention.suffix)) return;
1672
+ context.report({
1673
+ node,
1674
+ messageId: "filenameSuffix",
1675
+ data: {
1676
+ kind: convention.kind,
1677
+ suffix: convention.suffix
1678
+ }
1679
+ });
1680
+ }
1681
+ };
1682
+ }
1683
+ });
1684
+ //#endregion
546
1685
  //#region src/rules/prefer-load-component-over-load-children/index.ts
547
- function isRoutesTypeAnnotation(node) {
1686
+ const ROUTE_TYPE_NAMES = new Set(["Route"]);
1687
+ const ROUTES_TYPE_NAMES = new Set(["Routes"]);
1688
+ function isImportedTypeName(context, typeNode, importedNames) {
1689
+ if (typeNode?.type === "Identifier") {
1690
+ const importedName = getImportedName(context, typeNode, "@angular/router");
1691
+ return !!importedName && importedNames.has(importedName);
1692
+ }
1693
+ return typeNode?.type === "TSQualifiedName" && typeNode.left?.type === "Identifier" && isNamespaceImport(context, typeNode.left, "@angular/router") && importedNames.has(getPropertyName(typeNode.right) ?? "");
1694
+ }
1695
+ function getTypeParameterNodes(typeNode) {
1696
+ return typeNode.typeParameters?.params ?? typeNode.typeArguments?.params ?? [];
1697
+ }
1698
+ function isRouteType(context, typeNode) {
1699
+ return typeNode?.type === "TSTypeReference" && isImportedTypeName(context, typeNode.typeName, ROUTE_TYPE_NAMES);
1700
+ }
1701
+ function isRouteArrayType(context, typeNode) {
1702
+ if (!typeNode) return false;
1703
+ if (typeNode.type === "TSTypeReference" && isImportedTypeName(context, typeNode.typeName, ROUTES_TYPE_NAMES)) return true;
1704
+ if (typeNode.type === "TSArrayType") return isRouteType(context, typeNode.elementType);
1705
+ if (typeNode.type !== "TSTypeReference") return false;
1706
+ if (getPropertyName(typeNode.typeName) !== "Array" && getPropertyName(typeNode.typeName) !== "ReadonlyArray") return false;
1707
+ const [elementType] = getTypeParameterNodes(typeNode);
1708
+ return isRouteType(context, elementType);
1709
+ }
1710
+ function isRoutesTypeAnnotation(context, node) {
548
1711
  const typeAnnotation = node?.typeAnnotation;
549
1712
  if (!typeAnnotation || typeAnnotation.type !== "TSTypeAnnotation") return false;
550
- const annotation = typeAnnotation.typeAnnotation;
551
- if (!annotation || annotation.type !== "TSTypeReference") return false;
552
- return getPropertyName(annotation.typeName) === "Routes";
1713
+ return isRouteArrayType(context, typeAnnotation.typeAnnotation);
553
1714
  }
554
1715
  function isExportedConstDeclarator(node) {
555
1716
  if (node.type !== "VariableDeclarator") return false;
@@ -557,19 +1718,6 @@ function isExportedConstDeclarator(node) {
557
1718
  if (declaration?.type !== "VariableDeclaration" || declaration.kind !== "const") return false;
558
1719
  return declaration.parent?.type === "ExportNamedDeclaration";
559
1720
  }
560
- function visitNodes(node, visitor) {
561
- if (!node) return;
562
- if (Array.isArray(node)) {
563
- for (const item of node) visitNodes(item, visitor);
564
- return;
565
- }
566
- visitor(node);
567
- for (const [key, value] of Object.entries(node)) {
568
- if (key === "parent") continue;
569
- if (!value || typeof value !== "object") continue;
570
- visitNodes(value, visitor);
571
- }
572
- }
573
1721
  const preferLoadComponentOverLoadChildren = defineRule({
574
1722
  meta: {
575
1723
  type: "problem",
@@ -581,20 +1729,30 @@ const preferLoadComponentOverLoadChildren = defineRule({
581
1729
  messages: { avoidLoadChildren: "Avoid loadChildren in Routes arrays; prefer loadComponent for lazy-loading standalone route components." }
582
1730
  },
583
1731
  createOnce(context) {
1732
+ function reportLoadChildrenInRouteObject(routeObject) {
1733
+ for (const property of routeObject.properties ?? []) {
1734
+ if (property.type !== "Property" || property.computed) continue;
1735
+ const propertyName = getPropertyName(property.key);
1736
+ if (propertyName === "loadChildren") {
1737
+ context.report({
1738
+ node: property.key ?? property,
1739
+ messageId: "avoidLoadChildren"
1740
+ });
1741
+ continue;
1742
+ }
1743
+ if (propertyName === "children" && property.value?.type === "ArrayExpression") reportLoadChildrenInRouteArray(property.value);
1744
+ }
1745
+ }
1746
+ function reportLoadChildrenInRouteArray(routeArray) {
1747
+ for (const element of routeArray.elements ?? []) if (element?.type === "ObjectExpression") reportLoadChildrenInRouteObject(element);
1748
+ }
584
1749
  return { VariableDeclarator(node) {
585
1750
  const declarator = node;
586
1751
  if (!isExportedConstDeclarator(declarator)) return;
587
1752
  if (declarator.id?.type !== "Identifier") return;
588
- if (!isRoutesTypeAnnotation(declarator.id)) return;
1753
+ if (!isRoutesTypeAnnotation(context, declarator.id)) return;
589
1754
  if (declarator.init?.type !== "ArrayExpression") return;
590
- visitNodes(declarator.init.elements, (current) => {
591
- if (current.type !== "Property" || current.computed) return;
592
- if (getPropertyName(current.key) !== "loadChildren") return;
593
- context.report({
594
- node: current.key ?? current,
595
- messageId: "avoidLoadChildren"
596
- });
597
- });
1755
+ reportLoadChildrenInRouteArray(declarator.init);
598
1756
  } };
599
1757
  }
600
1758
  });
@@ -604,6 +1762,7 @@ function getPrivateTarget(element) {
604
1762
  if (element.type !== "PropertyDefinition" && element.type !== "FieldDefinition" && element.type !== "AccessorProperty" && element.type !== "MethodDefinition") return null;
605
1763
  if (element.accessibility !== "private") return null;
606
1764
  if (element.computed) return null;
1765
+ if (Array.isArray(element.decorators) && element.decorators.length > 0) return null;
607
1766
  if (element.type === "MethodDefinition" && element.kind === "constructor") return null;
608
1767
  const name = getPropertyName(element.key);
609
1768
  if (!name || element.key?.type !== "Identifier") return null;
@@ -757,10 +1916,10 @@ const preferPrivateElements = defineRule({
757
1916
  });
758
1917
  //#endregion
759
1918
  //#region src/rules/prefer-style-url/index.ts
760
- function isComponentDecoratorCall(node) {
1919
+ const COMPONENT_DECORATORS = new Set(["Component"]);
1920
+ function isComponentDecoratorCall(context, node) {
761
1921
  const callee = node.callee;
762
- if (callee?.type === "Identifier") return callee.name === "Component";
763
- return callee?.type === "MemberExpression" && getPropertyName(callee.property) === "Component";
1922
+ return isImportedReference(context, callee, "@angular/core", COMPONENT_DECORATORS) || isImportedNamespaceMember(context, callee, "@angular/core", COMPONENT_DECORATORS);
764
1923
  }
765
1924
  function isSingleStyleFileNode(node) {
766
1925
  if (!node) return false;
@@ -781,7 +1940,7 @@ const preferStyleUrl = defineRule({
781
1940
  createOnce(context) {
782
1941
  return { CallExpression(node) {
783
1942
  const call = node;
784
- if (!isComponentDecoratorCall(call)) return;
1943
+ if (!isComponentDecoratorCall(context, call)) return;
785
1944
  const metadata = call.arguments?.[0];
786
1945
  if (metadata?.type !== "ObjectExpression") return;
787
1946
  for (const property of metadata.properties ?? []) {
@@ -806,11 +1965,117 @@ const preferStyleUrl = defineRule({
806
1965
  }
807
1966
  });
808
1967
  //#endregion
1968
+ //#region src/rules/public-component-interface/index.ts
1969
+ const TARGET_DECORATORS = new Set(["Component", "Directive"]);
1970
+ const INPUT_MODEL_APIS = new Set(["input", "model"]);
1971
+ const OUTPUT_APIS = new Set(["output", "outputFromObservable"]);
1972
+ const INJECT_APIS = new Set(["inject"]);
1973
+ const FIELD_NODE_TYPES = new Set([
1974
+ "AccessorProperty",
1975
+ "FieldDefinition",
1976
+ "PropertyDefinition"
1977
+ ]);
1978
+ function hasTargetDecorator(context, classNode) {
1979
+ if (!classNode || !Array.isArray(classNode.decorators)) return false;
1980
+ return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS));
1981
+ }
1982
+ function isApiCallFromAngularCore(context, node, apiNames) {
1983
+ if (!node || node.type !== "CallExpression") return false;
1984
+ const callee = node.callee;
1985
+ if (isImportedReference(context, callee, "@angular/core", apiNames)) return true;
1986
+ if (callee?.type !== "MemberExpression") return false;
1987
+ const supportsRequiredApi = [...INPUT_MODEL_APIS].some((name) => apiNames.has(name));
1988
+ if (getPropertyName(callee.property) === "required" && callee.object?.type === "Identifier") return supportsRequiredApi && isImportedReference(context, callee.object, "@angular/core", apiNames);
1989
+ if (isImportedNamespaceMember(context, callee, "@angular/core", apiNames)) return true;
1990
+ if (getPropertyName(callee.property) === "required" && callee.object?.type === "MemberExpression" && callee.object.object?.type === "Identifier") {
1991
+ const namespaceApiName = getPropertyName(callee.object.property);
1992
+ return supportsRequiredApi && !!namespaceApiName && apiNames.has(namespaceApiName) && isImportedNamespaceMember(context, callee.object, "@angular/core", apiNames);
1993
+ }
1994
+ return false;
1995
+ }
1996
+ function isNonPublicMember(memberNode) {
1997
+ if (memberNode.key?.type === "PrivateIdentifier") return true;
1998
+ return memberNode.accessibility === "private" || memberNode.accessibility === "protected";
1999
+ }
2000
+ function isPublicMember(memberNode) {
2001
+ if (memberNode.key?.type === "PrivateIdentifier") return false;
2002
+ return memberNode.accessibility !== "private" && memberNode.accessibility !== "protected";
2003
+ }
2004
+ function getAccessibilityModifierRange(context, memberNode) {
2005
+ const memberRange = getRange(memberNode);
2006
+ const keyRange = getRange(memberNode.key);
2007
+ if (!memberRange || !keyRange) return null;
2008
+ const prefixText = context.sourceCode.text.slice(memberRange[0], keyRange[0]);
2009
+ const match = /\b(private|protected|public)\b/u.exec(prefixText);
2010
+ if (!match) return null;
2011
+ return [memberRange[0] + match.index, memberRange[0] + match.index + match[0].length];
2012
+ }
2013
+ function getVisibilityFix(context, memberNode, targetVisibility) {
2014
+ if (memberNode.key?.type === "PrivateIdentifier") return void 0;
2015
+ const keyRange = getRange(memberNode.key);
2016
+ if (!keyRange) return void 0;
2017
+ const accessibilityRange = getAccessibilityModifierRange(context, memberNode);
2018
+ if (accessibilityRange) return (fixer) => fixer.replaceTextRange(accessibilityRange, targetVisibility);
2019
+ return (fixer) => fixer.insertTextBeforeRange([keyRange[0], keyRange[0]], `${targetVisibility} `);
2020
+ }
2021
+ const publicComponentInterface = defineRule({
2022
+ meta: {
2023
+ type: "problem",
2024
+ docs: {
2025
+ description: "Require component/directive signal interface members (input/model/output APIs) to be public, and it's dependencies to be non-public.",
2026
+ recommended: true
2027
+ },
2028
+ fixable: "code",
2029
+ schema: [],
2030
+ messages: {
2031
+ nonPublicInputModel: "Input/model member {{name}} must be public so the component/directive interface is externally accessible.",
2032
+ nonPublicOutput: "Output member {{name}} must be public so the component/directive interface is externally accessible.",
2033
+ publicInjectMember: "Injected member {{name}} should not be public; prefer protected (template access) or private/#private."
2034
+ }
2035
+ },
2036
+ createOnce(context) {
2037
+ return { ClassBody(node) {
2038
+ const classNode = node.parent;
2039
+ if (!hasTargetDecorator(context, classNode)) return;
2040
+ for (const member of node.body ?? []) {
2041
+ if (!FIELD_NODE_TYPES.has(member.type)) continue;
2042
+ const isInputModelMember = isApiCallFromAngularCore(context, member.value, INPUT_MODEL_APIS);
2043
+ const isOutputMember = isApiCallFromAngularCore(context, member.value, OUTPUT_APIS);
2044
+ if (isInputModelMember && isNonPublicMember(member)) {
2045
+ context.report({
2046
+ node: member.key ?? member,
2047
+ messageId: "nonPublicInputModel",
2048
+ data: { name: getPropertyName(member.key) ?? "member" },
2049
+ fix: getVisibilityFix(context, member, "public")
2050
+ });
2051
+ continue;
2052
+ }
2053
+ if (isOutputMember && isNonPublicMember(member)) {
2054
+ context.report({
2055
+ node: member.key ?? member,
2056
+ messageId: "nonPublicOutput",
2057
+ data: { name: getPropertyName(member.key) ?? "member" },
2058
+ fix: getVisibilityFix(context, member, "public")
2059
+ });
2060
+ continue;
2061
+ }
2062
+ if (isPublicMember(member) && isApiCallFromAngularCore(context, member.value, INJECT_APIS)) context.report({
2063
+ node: member.key ?? member,
2064
+ messageId: "publicInjectMember",
2065
+ data: { name: getPropertyName(member.key) ?? "member" },
2066
+ fix: getVisibilityFix(context, member, "protected")
2067
+ });
2068
+ }
2069
+ } };
2070
+ }
2071
+ });
2072
+ //#endregion
809
2073
  //#region src/rules/restrict-injectable-provided-in/index.ts
810
2074
  const ALLOWED_PROVIDED_IN_VALUES = new Set(["root", "platform"]);
811
- function getInjectableMetadata(node) {
2075
+ const INJECTABLE_DECORATORS = new Set(["Injectable"]);
2076
+ function getInjectableMetadata(context, node) {
812
2077
  if (node.type !== "Decorator") return null;
813
- if (getDecoratorName(node) !== "Injectable") return null;
2078
+ if (!isAngularCoreDecorator(context, node, INJECTABLE_DECORATORS)) return null;
814
2079
  const expression = node.expression;
815
2080
  if (expression?.type !== "CallExpression") return null;
816
2081
  const metadata = expression.arguments?.[0];
@@ -832,11 +2097,11 @@ const restrictInjectableProvidedIn = defineRule({
832
2097
  recommended: true
833
2098
  },
834
2099
  schema: [],
835
- messages: { disallowedProvidedIn: "@Injectable providedIn should be 'root' or 'platform', not {{actual}}." }
2100
+ messages: { disallowedProvidedIn: "@Injectable providedIn should be the literal 'root' or 'platform', not {{actual}}." }
836
2101
  },
837
2102
  createOnce(context) {
838
2103
  return { Decorator(node) {
839
- const metadata = getInjectableMetadata(node);
2104
+ const metadata = getInjectableMetadata(context, node);
840
2105
  if (!metadata) return;
841
2106
  const providedInProperty = getProvidedInProperty(metadata);
842
2107
  if (!providedInProperty) return;
@@ -878,6 +2143,37 @@ const ANGULAR_CLASS_DECORATOR_NAMES = new Set([
878
2143
  "NgModule"
879
2144
  ]);
880
2145
  const INJECTION_CONTEXT_RUNNER_NAMES = new Set(["runInInjectionContext", "runInContext"]);
2146
+ const KNOWN_INJECTION_CONTEXT_API_IMPORTS = [
2147
+ {
2148
+ from: "@angular/core",
2149
+ imports: [
2150
+ "afterEveryRender",
2151
+ "afterNextRender",
2152
+ "afterRender",
2153
+ "afterRenderEffect",
2154
+ "assertInInjectionContext",
2155
+ "effect",
2156
+ "inject",
2157
+ "resource"
2158
+ ]
2159
+ },
2160
+ {
2161
+ from: "@angular/core/rxjs-interop",
2162
+ imports: [
2163
+ "rxResource",
2164
+ "toObservable",
2165
+ "toSignal"
2166
+ ]
2167
+ },
2168
+ {
2169
+ from: "@angular/common/http",
2170
+ imports: ["httpResource"]
2171
+ },
2172
+ {
2173
+ from: "@angular/forms/signals",
2174
+ imports: ["form"]
2175
+ }
2176
+ ];
881
2177
  const INJECTION_CONTEXT_FUNCTION_TYPE_NAMES = new Set([
882
2178
  "CanActivateFn",
883
2179
  "CanActivateChildFn",
@@ -898,17 +2194,6 @@ const CLASS_FIELD_TYPES = new Set([
898
2194
  "FieldDefinition",
899
2195
  "PropertyDefinition"
900
2196
  ]);
901
- function getTypeName(node) {
902
- if (!node) return null;
903
- if (node.type === "Identifier") return node.name;
904
- if (node.type === "TSTypeReference") return getTypeName(node.typeName);
905
- if (node.type === "TSQualifiedName") {
906
- const left = getTypeName(node.left);
907
- const right = getTypeName(node.right);
908
- return left && right ? `${left}.${right}` : right ?? left;
909
- }
910
- return null;
911
- }
912
2197
  function isFunction(node) {
913
2198
  return !!node && FUNCTION_TYPES.has(node.type);
914
2199
  }
@@ -975,11 +2260,6 @@ function isTypedInjectionContextFunction(functionNode) {
975
2260
  const unqualifiedTypeName = typeName.includes(".") ? typeName.split(".").at(-1) : typeName;
976
2261
  return INJECTION_CONTEXT_FUNCTION_TYPE_NAMES.has(unqualifiedTypeName ?? typeName);
977
2262
  }
978
- function getNodeStart(node) {
979
- if (typeof node.start === "number") return node.start;
980
- if (Array.isArray(node.range) && typeof node.range[0] === "number") return node.range[0];
981
- return null;
982
- }
983
2263
  function hasAwaitBeforeNodeInFunction(functionNode, node) {
984
2264
  const nodeStart = getNodeStart(node);
985
2265
  if (nodeStart === null) return false;
@@ -1027,16 +2307,27 @@ function isAllowedInjectionContext(context, node, allowedFunctionNames, injectFu
1027
2307
  const functionName = getFunctionName(nearestFunction);
1028
2308
  return functionName ? allowedFunctionNames.has(functionName) || injectFunctionPrefixes.some((prefix) => functionName.startsWith(prefix)) || injectFunctionSuffixes.some((suffix) => functionName.endsWith(suffix)) : false;
1029
2309
  }
1030
- function isAngularInjectCall(node, injectNames, angularNamespaces, checkUnimportedInject) {
2310
+ function getKnownInjectionContextApiImports(source) {
2311
+ const apiImports = KNOWN_INJECTION_CONTEXT_API_IMPORTS.find((entry) => entry.from === source);
2312
+ return apiImports ? new Set(apiImports.imports) : null;
2313
+ }
2314
+ function isKnownInjectionContextApiCall(context, node, injectionContextApiLocalNames, injectionContextApiNamespaceMembers, checkUnimportedInject) {
1031
2315
  const callee = node.callee;
1032
- if (callee?.type === "Identifier") return injectNames.has(callee.name) || checkUnimportedInject && callee.name === "inject";
1033
- return callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && angularNamespaces.has(callee.object.name) && getPropertyName(callee.property) === "inject";
2316
+ if (callee?.type === "Identifier") return injectionContextApiLocalNames.has(callee.name) && !isShadowedIdentifier(context, callee) || checkUnimportedInject && callee.name === "inject" && !isShadowedIdentifier(context, callee);
2317
+ if (callee?.type !== "MemberExpression") return false;
2318
+ if (callee.object?.type === "Identifier") {
2319
+ if (injectionContextApiLocalNames.has(callee.object.name) && !isShadowedIdentifier(context, callee.object)) return true;
2320
+ return !!injectionContextApiNamespaceMembers.get(callee.object.name)?.has(getPropertyName(callee.property) ?? "") && !isShadowedIdentifier(context, callee.object);
2321
+ }
2322
+ if (callee.object?.type === "MemberExpression" && callee.object.object?.type === "Identifier") return !!injectionContextApiNamespaceMembers.get(callee.object.object.name)?.has(getPropertyName(callee.object.property) ?? "") && !isShadowedIdentifier(context, callee.object.object);
2323
+ return false;
1034
2324
  }
1035
- function isInjectLikeHelperCall(node, injectNames, injectFunctionPrefixes, injectFunctionSuffixes, runsInInjectionContextFunctionNames) {
2325
+ function isInjectLikeHelperCall(context, node, injectionContextApiLocalNames, injectFunctionPrefixes, injectFunctionSuffixes, runsInInjectionContextFunctionNames) {
1036
2326
  const callee = node.callee;
1037
2327
  if (callee?.type !== "Identifier") return false;
2328
+ if (isShadowedIdentifier(context, callee)) return false;
1038
2329
  if (runsInInjectionContextFunctionNames.has(callee.name)) return true;
1039
- return !injectNames.has(callee.name) && callee.name !== "inject" && (injectFunctionPrefixes.some((prefix) => callee.name.startsWith(prefix)) || injectFunctionSuffixes.some((suffix) => callee.name.endsWith(suffix)));
2330
+ return !injectionContextApiLocalNames.has(callee.name) && callee.name !== "inject" && (injectFunctionPrefixes.some((prefix) => callee.name.startsWith(prefix)) || injectFunctionSuffixes.some((suffix) => callee.name.endsWith(suffix)));
1040
2331
  }
1041
2332
  //#endregion
1042
2333
  //#region src/index.ts
@@ -1044,19 +2335,24 @@ const plugin = eslintCompatPlugin({
1044
2335
  meta: { name: "@benjavicente/lint-angular" },
1045
2336
  rules: {
1046
2337
  "avoid-explicit-injection-context": avoidExplicitInjectionContext,
2338
+ "avoid-explicit-subscription-management": avoidExplicitSubscriptionManagement,
1047
2339
  "avoid-ng-modules": avoidNgModules,
2340
+ "avoid-rxjs-state-in-component": avoidRxjsStateInComponent,
1048
2341
  "avoid-writing-signals-in-reactive-context": avoidWritingSignalsInReactiveContext,
1049
2342
  "class-member-order": classMemberOrder,
1050
- "component-class-matches-filename": componentClassMatchesFilename,
2343
+ "class-matches-filename": classMatchesFilename,
2344
+ "component-resource-filenames": componentResourceFilenames,
2345
+ "decorator-filename-suffix": decoratorFilenameSuffix,
1051
2346
  "prefer-load-component-over-load-children": preferLoadComponentOverLoadChildren,
1052
2347
  "prefer-private-elements": preferPrivateElements,
1053
2348
  "prefer-style-url": preferStyleUrl,
2349
+ "public-component-interface": publicComponentInterface,
1054
2350
  "restrict-injectable-provided-in": restrictInjectableProvidedIn,
1055
2351
  "rules-of-inject": defineRule({
1056
2352
  meta: {
1057
2353
  type: "problem",
1058
2354
  docs: {
1059
- description: "Require Angular inject() calls to appear only in known injection contexts.",
2355
+ description: "Require Angular APIs that depend on injection context to appear only in known injection contexts.",
1060
2356
  recommended: true
1061
2357
  },
1062
2358
  schema: [{
@@ -1100,17 +2396,17 @@ const plugin = eslintCompatPlugin({
1100
2396
  }
1101
2397
  }
1102
2398
  }],
1103
- messages: { disallowedInject: "Angular inject() must be called from an injection context: a class field initializer or constructor in an Angular-decorated class, provider factory, InjectionToken factory, runInInjectionContext/runInContext callback, Angular route callback property (for example loadComponent/canActivate), an inject* or *Guard function, or configured allowed function." }
2399
+ messages: { disallowedInject: "Angular APIs that depend on injection context must be called from an injection context: a class field initializer or constructor in an Angular-decorated class, provider factory, InjectionToken factory, runInInjectionContext/runInContext callback, Angular route callback property (for example loadComponent/canActivate), an inject* or *Guard function, or configured allowed function." }
1104
2400
  },
1105
2401
  createOnce(context) {
1106
- const injectNames = /* @__PURE__ */ new Set();
1107
- const angularNamespaces = /* @__PURE__ */ new Set();
2402
+ const injectionContextApiLocalNames = /* @__PURE__ */ new Set();
2403
+ const injectionContextApiNamespaceMembers = /* @__PURE__ */ new Map();
1108
2404
  const runsInInjectionContextFunctionNames = /* @__PURE__ */ new Set();
1109
2405
  let runsInInjectionContextRules = [];
1110
2406
  return {
1111
2407
  before() {
1112
- injectNames.clear();
1113
- angularNamespaces.clear();
2408
+ injectionContextApiLocalNames.clear();
2409
+ injectionContextApiNamespaceMembers.clear();
1114
2410
  runsInInjectionContextFunctionNames.clear();
1115
2411
  runsInInjectionContextRules = (context.options[0] ?? {}).runsInInjectionContext ?? DEFAULT_RUNS_IN_INJECTION_CONTEXT;
1116
2412
  },
@@ -1121,10 +2417,10 @@ const plugin = eslintCompatPlugin({
1121
2417
  if (specifier.type === "ImportSpecifier" && (matchingRule.imports === "all" || matchingRule.imports.includes(getPropertyName(specifier.imported) ?? ""))) runsInInjectionContextFunctionNames.add(specifier.local.name);
1122
2418
  if ((specifier.type === "ImportDefaultSpecifier" || specifier.type === "ImportNamespaceSpecifier") && matchingRule.imports === "all") runsInInjectionContextFunctionNames.add(specifier.local.name);
1123
2419
  }
1124
- if (source !== "@angular/core") return;
1125
- for (const specifier of node.specifiers ?? []) {
1126
- if (specifier.type === "ImportSpecifier" && getPropertyName(specifier.imported) === "inject") injectNames.add(specifier.local.name);
1127
- if (specifier.type === "ImportNamespaceSpecifier") angularNamespaces.add(specifier.local.name);
2420
+ const knownApiImports = typeof source === "string" ? getKnownInjectionContextApiImports(source) : null;
2421
+ if (knownApiImports) for (const specifier of node.specifiers ?? []) {
2422
+ if (specifier.type === "ImportSpecifier" && knownApiImports.has(getPropertyName(specifier.imported) ?? "")) injectionContextApiLocalNames.add(specifier.local.name);
2423
+ if (specifier.type === "ImportNamespaceSpecifier") injectionContextApiNamespaceMembers.set(specifier.local.name, knownApiImports);
1128
2424
  }
1129
2425
  },
1130
2426
  CallExpression(node) {
@@ -1134,14 +2430,14 @@ const plugin = eslintCompatPlugin({
1134
2430
  const injectFunctionPrefixes = options.injectFunctionPrefixes ?? DEFAULT_INJECT_FUNCTION_PREFIXES;
1135
2431
  const injectFunctionSuffixes = options.injectFunctionSuffixes ?? DEFAULT_INJECT_FUNCTION_SUFFIXES;
1136
2432
  const inAllowedContext = isAllowedInjectionContext(context, node, allowedFunctionNames, injectFunctionPrefixes, injectFunctionSuffixes);
1137
- if (isInjectLikeHelperCall(node, injectNames, injectFunctionPrefixes, injectFunctionSuffixes, runsInInjectionContextFunctionNames) && !inAllowedContext) {
2433
+ if (isInjectLikeHelperCall(context, node, injectionContextApiLocalNames, injectFunctionPrefixes, injectFunctionSuffixes, runsInInjectionContextFunctionNames) && !inAllowedContext) {
1138
2434
  context.report({
1139
2435
  node: node.callee,
1140
2436
  messageId: "disallowedInject"
1141
2437
  });
1142
2438
  return;
1143
2439
  }
1144
- if (!isAngularInjectCall(node, injectNames, angularNamespaces, checkUnimportedInject)) return;
2440
+ if (!isKnownInjectionContextApiCall(context, node, injectionContextApiLocalNames, injectionContextApiNamespaceMembers, checkUnimportedInject)) return;
1145
2441
  if (inAllowedContext) return;
1146
2442
  context.report({
1147
2443
  node: node.callee,