@benjavicente/lint-angular 0.0.1 → 0.0.2

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 +16 -10
  2. package/dist/index.mjs +1643 -230
  3. package/package.json +11 -11
package/dist/index.mjs CHANGED
@@ -23,6 +23,168 @@ 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 addAngularCoreDecoratorImport(specifier, decoratorNames, imports) {
175
+ if (specifier.type === "ImportSpecifier") {
176
+ const importedName = getPropertyName(specifier.imported);
177
+ if (importedName && decoratorNames.has(importedName)) imports.decoratorLocalNames.add(specifier.local.name);
178
+ }
179
+ if (specifier.type === "ImportNamespaceSpecifier") imports.angularNamespaces.add(specifier.local.name);
180
+ }
181
+ function isAngularCoreDecorator(context, decorator, imports) {
182
+ if (!decorator) return false;
183
+ const expression = decorator.expression ?? decorator;
184
+ const callee = expression?.type === "CallExpression" ? expression.callee : expression;
185
+ if (callee?.type === "Identifier") return imports.decoratorLocalNames.has(callee.name) && !isShadowedIdentifier(context, callee);
186
+ return callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && imports.angularNamespaces.has(callee.object.name) && !isShadowedIdentifier(context, callee.object) && imports.decoratorNames.has(getPropertyName(callee.property) ?? "");
187
+ }
26
188
  //#endregion
27
189
  //#region src/rules/class-member-order/index.ts
28
190
  const ANGULAR_CLASS_DECORATOR_NAMES$1 = new Set([
@@ -45,38 +207,141 @@ const ORDER_LABELS = [
45
207
  "outputs",
46
208
  "everything else"
47
209
  ];
48
- function hasAngularClassDecorator(classNode) {
210
+ function hasAngularClassDecorator(context, classNode, imports) {
49
211
  if (!classNode || !Array.isArray(classNode.decorators)) return false;
50
- return classNode.decorators.some((decorator) => ANGULAR_CLASS_DECORATOR_NAMES$1.has(getDecoratorName(decorator) ?? ""));
212
+ return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, imports));
51
213
  }
52
- function getCallName(node) {
53
- if (!node || node.type !== "CallExpression") return null;
214
+ function isApiCall(context, node, localNames, angularNamespaces, apiNames) {
215
+ if (!node || node.type !== "CallExpression") return false;
54
216
  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;
217
+ if (callee?.type === "Identifier") return localNames.has(callee.name) && !isShadowedIdentifier(context, callee);
218
+ if (callee?.type !== "MemberExpression") return false;
219
+ const supportsRequiredApi = [...INPUT_MODEL_CALL_NAMES].some((name) => apiNames.has(name));
220
+ if (callee.object?.type === "Identifier") {
221
+ if (getPropertyName(callee.property) === "required") return supportsRequiredApi && localNames.has(callee.object.name) && !isShadowedIdentifier(context, callee.object);
222
+ return angularNamespaces.has(callee.object.name) && !isShadowedIdentifier(context, callee.object) && apiNames.has(getPropertyName(callee.property) ?? "");
223
+ }
224
+ if (getPropertyName(callee.property) === "required" && callee.object?.type === "MemberExpression" && callee.object.object?.type === "Identifier") return supportsRequiredApi && angularNamespaces.has(callee.object.object.name) && !isShadowedIdentifier(context, callee.object.object) && apiNames.has(getPropertyName(callee.object.property) ?? "");
225
+ return false;
61
226
  }
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";
227
+ function hasDecorator(context, element, localNames, angularNamespaces, decoratorNames) {
228
+ return Array.isArray(element.decorators) ? element.decorators.some((decorator) => {
229
+ const expression = decorator.expression ?? decorator;
230
+ const callee = expression?.type === "CallExpression" ? expression.callee : expression;
231
+ if (callee?.type === "Identifier") return localNames.has(callee.name) && !isShadowedIdentifier(context, callee);
232
+ return callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && angularNamespaces.has(callee.object.name) && !isShadowedIdentifier(context, callee.object) && decoratorNames.has(getPropertyName(callee.property) ?? "");
233
+ }) : false;
66
234
  }
67
- function classifyMember(element) {
235
+ function classifyMember(context, element, imports) {
68
236
  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;
237
+ if (isApiCall(context, element.value, imports.injectLocalNames, imports.angularNamespaces, new Set(["inject"]))) return 0;
238
+ if (isApiCall(context, element.value, imports.inputModelLocalNames, imports.angularNamespaces, INPUT_MODEL_CALL_NAMES)) return 1;
239
+ if (hasDecorator(context, element, imports.inputModelDecoratorLocalNames, imports.angularNamespaces, new Set(["Input"]))) return 1;
240
+ if (isApiCall(context, element.value, imports.outputLocalNames, imports.angularNamespaces, OUTPUT_CALL_NAMES)) return 2;
241
+ if (hasDecorator(context, element, imports.outputDecoratorLocalNames, imports.angularNamespaces, new Set(["Output"]))) return 2;
75
242
  return 3;
76
243
  }
77
244
  if (element.type === "MethodDefinition") return 3;
78
245
  return 3;
79
246
  }
247
+ function getMemberName$1(node) {
248
+ if (!node) return null;
249
+ if (node.type === "Identifier" || node.type === "PrivateIdentifier") return node.name;
250
+ return null;
251
+ }
252
+ function collectThisMemberReferences(node, references = /* @__PURE__ */ new Set()) {
253
+ if (!node) return references;
254
+ if (Array.isArray(node)) {
255
+ for (const item of node) collectThisMemberReferences(item, references);
256
+ return references;
257
+ }
258
+ if (node.type === "MemberExpression" && node.object?.type === "ThisExpression") {
259
+ const propertyName = getPropertyName(node.property);
260
+ if (propertyName) references.add(propertyName);
261
+ }
262
+ for (const [key, value] of Object.entries(node)) {
263
+ if (key === "parent") continue;
264
+ if (!value || typeof value !== "object") continue;
265
+ collectThisMemberReferences(value, references);
266
+ }
267
+ return references;
268
+ }
269
+ function applyDependencyGroups(classifiedMembers) {
270
+ const membersByName = /* @__PURE__ */ new Map();
271
+ for (const member of classifiedMembers) if (member.name) membersByName.set(member.name, member);
272
+ let changed = true;
273
+ while (changed) {
274
+ changed = false;
275
+ for (const member of classifiedMembers) for (const dependencyName of member.dependencies) {
276
+ const dependency = membersByName.get(dependencyName);
277
+ if (!dependency) continue;
278
+ if (dependency.effectiveGroup <= member.effectiveGroup) continue;
279
+ dependency.effectiveGroup = member.effectiveGroup;
280
+ changed = true;
281
+ }
282
+ }
283
+ }
284
+ function dependsOn(member, dependency, membersByName, seen = /* @__PURE__ */ new Set()) {
285
+ if (!dependency.name) return false;
286
+ if (member.dependencies.has(dependency.name)) return true;
287
+ if (seen.has(member)) return false;
288
+ seen.add(member);
289
+ for (const dependencyName of member.dependencies) {
290
+ const next = membersByName.get(dependencyName);
291
+ if (next && dependsOn(next, dependency, membersByName, seen)) return true;
292
+ }
293
+ return false;
294
+ }
295
+ function getSortedMembers(classifiedMembers) {
296
+ const membersByName = /* @__PURE__ */ new Map();
297
+ for (const member of classifiedMembers) if (member.name) membersByName.set(member.name, member);
298
+ return classifiedMembers.toSorted((left, right) => {
299
+ if (dependsOn(left, right, membersByName)) return 1;
300
+ if (dependsOn(right, left, membersByName)) return -1;
301
+ return left.effectiveGroup - right.effectiveGroup;
302
+ });
303
+ }
304
+ function hasUnresolvedPrioritizedDependency(member, membersByName) {
305
+ if (member.group === 3) return false;
306
+ for (const dependencyName of member.dependencies) if (!membersByName.has(dependencyName)) return true;
307
+ return false;
308
+ }
309
+ function getSortedMembersFix(context, classBody, classifiedMembers) {
310
+ if (!classifiedMembers.length) return void 0;
311
+ if ((classBody.body ?? []).length !== classifiedMembers.length) return void 0;
312
+ const sortableRanges = [];
313
+ const membersByName = /* @__PURE__ */ new Map();
314
+ for (const member of classifiedMembers) if (member.name) membersByName.set(member.name, member);
315
+ for (const member of classifiedMembers) {
316
+ const { element } = member;
317
+ if (!CLASS_FIELD_TYPES$1.has(element.type)) return void 0;
318
+ if (element.type === "AccessorProperty") return void 0;
319
+ if (element.computed) return void 0;
320
+ if (Array.isArray(element.decorators) && element.decorators.length > 0) return void 0;
321
+ if (hasUnresolvedPrioritizedDependency(member, membersByName)) return;
322
+ const range = getRange(element);
323
+ if (!range) return void 0;
324
+ if (!context.sourceCode.text.slice(range[0], range[1]).trimEnd().endsWith(";")) return;
325
+ sortableRanges.push(range);
326
+ }
327
+ const sortedMembers = getSortedMembers(classifiedMembers);
328
+ if (sortedMembers.every((member, index) => member.element === classifiedMembers[index]?.element)) return void 0;
329
+ return (fixer) => {
330
+ const sourceText = context.sourceCode.text;
331
+ const sortedTexts = sortedMembers.map(({ element }) => {
332
+ const range = getRange(element);
333
+ return range ? sourceText.slice(range[0], range[1]) : "";
334
+ });
335
+ let output = "";
336
+ for (let index = 0; index < sortableRanges.length; index += 1) {
337
+ const [, end] = sortableRanges[index];
338
+ const nextStart = sortableRanges[index + 1]?.[0];
339
+ output += sortedTexts[index];
340
+ output += sourceText.slice(end, nextStart ?? end);
341
+ }
342
+ return fixer.replaceTextRange([sortableRanges[0][0], sortableRanges.at(-1)[1]], output);
343
+ };
344
+ }
80
345
  const classMemberOrder = defineRule({
81
346
  meta: {
82
347
  type: "suggestion",
@@ -84,30 +349,88 @@ const classMemberOrder = defineRule({
84
349
  description: "Require Angular class members to be ordered as inject fields, inputs/models, outputs, then everything else.",
85
350
  recommended: true
86
351
  },
352
+ fixable: "code",
87
353
  schema: [],
88
354
  messages: { outOfOrder: "Angular class member should be ordered before {{previousGroup}} and with {{expectedGroup}}." }
89
355
  },
90
356
  createOnce(context) {
91
- return { ClassBody(node) {
92
- if (!hasAngularClassDecorator(node.parent)) return;
93
- let highestSeen = null;
94
- for (const element of node.body ?? []) {
95
- const group = classifyMember(element);
96
- 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
- }
357
+ const imports = {
358
+ decoratorNames: ANGULAR_CLASS_DECORATOR_NAMES$1,
359
+ decoratorLocalNames: /* @__PURE__ */ new Set(),
360
+ injectLocalNames: /* @__PURE__ */ new Set(),
361
+ inputModelLocalNames: /* @__PURE__ */ new Set(),
362
+ inputModelDecoratorLocalNames: /* @__PURE__ */ new Set(),
363
+ outputLocalNames: /* @__PURE__ */ new Set(),
364
+ outputDecoratorLocalNames: /* @__PURE__ */ new Set(),
365
+ angularNamespaces: /* @__PURE__ */ new Set()
366
+ };
367
+ return {
368
+ before() {
369
+ imports.decoratorLocalNames.clear();
370
+ imports.injectLocalNames.clear();
371
+ imports.inputModelLocalNames.clear();
372
+ imports.inputModelDecoratorLocalNames.clear();
373
+ imports.outputLocalNames.clear();
374
+ imports.outputDecoratorLocalNames.clear();
375
+ imports.angularNamespaces.clear();
376
+ },
377
+ ImportDeclaration(node) {
378
+ if (node.source?.value !== "@angular/core") return;
379
+ for (const specifier of node.specifiers ?? []) {
380
+ addAngularCoreDecoratorImport(specifier, ANGULAR_CLASS_DECORATOR_NAMES$1, imports);
381
+ if (specifier.type === "ImportSpecifier") {
382
+ const importedName = getPropertyName(specifier.imported);
383
+ if (importedName === "inject") imports.injectLocalNames.add(specifier.local.name);
384
+ if (importedName && INPUT_MODEL_CALL_NAMES.has(importedName)) imports.inputModelLocalNames.add(specifier.local.name);
385
+ if (importedName === "Input") imports.inputModelDecoratorLocalNames.add(specifier.local.name);
386
+ if (importedName && OUTPUT_CALL_NAMES.has(importedName)) imports.outputLocalNames.add(specifier.local.name);
387
+ if (importedName === "Output") imports.outputDecoratorLocalNames.add(specifier.local.name);
388
+ }
389
+ if (specifier.type === "ImportNamespaceSpecifier") imports.angularNamespaces.add(specifier.local.name);
390
+ }
391
+ },
392
+ ClassBody(node) {
393
+ const classBody = node;
394
+ if (!hasAngularClassDecorator(context, classBody.parent, imports)) return;
395
+ let highestSeen = null;
396
+ const classifiedMembers = [];
397
+ const outOfOrderReports = [];
398
+ for (const element of classBody.body ?? []) {
399
+ const group = classifyMember(context, element, imports);
400
+ if (group === null) continue;
401
+ classifiedMembers.push({
402
+ element,
403
+ group,
404
+ effectiveGroup: group,
405
+ name: getMemberName$1(element.key),
406
+ dependencies: collectThisMemberReferences(element.value)
105
407
  });
106
- continue;
107
408
  }
108
- highestSeen = group;
409
+ applyDependencyGroups(classifiedMembers);
410
+ for (const { element, effectiveGroup } of classifiedMembers) {
411
+ if (highestSeen !== null && effectiveGroup < highestSeen) {
412
+ outOfOrderReports.push({
413
+ element,
414
+ effectiveGroup,
415
+ previousEffectiveGroup: highestSeen
416
+ });
417
+ continue;
418
+ }
419
+ highestSeen = effectiveGroup;
420
+ }
421
+ if (!outOfOrderReports.length) return;
422
+ const fix = getSortedMembersFix(context, classBody, classifiedMembers);
423
+ for (const [index, report] of outOfOrderReports.entries()) context.report({
424
+ node: report.element,
425
+ messageId: "outOfOrder",
426
+ data: {
427
+ expectedGroup: ORDER_LABELS[report.effectiveGroup],
428
+ previousGroup: ORDER_LABELS[report.previousEffectiveGroup]
429
+ },
430
+ fix: index === 0 ? fix : void 0
431
+ });
109
432
  }
110
- } };
433
+ };
111
434
  }
112
435
  });
113
436
  //#endregion
@@ -115,24 +438,24 @@ const classMemberOrder = defineRule({
115
438
  const DEFAULT_DISALLOW_INJECT_INJECTOR = true;
116
439
  const DEFAULT_DISALLOW_RUN_IN_INJECTION_CONTEXT = true;
117
440
  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;
441
+ function isAngularNamespaceMember(context, node, namespaces, memberName) {
442
+ return node?.type === "MemberExpression" && node.object?.type === "Identifier" && namespaces.has(node.object.name) && !isShadowedIdentifier(context, node.object) && getPropertyName(node.property) === memberName;
120
443
  }
121
- function isInjectCall(callNode, injectNames, angularNamespaces) {
444
+ function isInjectCall(context, callNode, injectNames, angularNamespaces) {
122
445
  const callee = callNode.callee;
123
- return callee?.type === "Identifier" && injectNames.has(callee.name) || isAngularNamespaceMember(callee, angularNamespaces, "inject");
446
+ return callee?.type === "Identifier" && injectNames.has(callee.name) && !isShadowedIdentifier(context, callee) || isAngularNamespaceMember(context, callee, angularNamespaces, "inject");
124
447
  }
125
- function isInjectorReference(node, injectorNames, angularNamespaces) {
126
- return node?.type === "Identifier" && injectorNames.has(node.name) || isAngularNamespaceMember(node, angularNamespaces, "Injector");
448
+ function isInjectorReference(context, node, injectorNames, angularNamespaces) {
449
+ return node?.type === "Identifier" && injectorNames.has(node.name) && !isShadowedIdentifier(context, node) || isAngularNamespaceMember(context, node, angularNamespaces, "Injector");
127
450
  }
128
- function isDisallowedInjectInjector(callNode, injectNames, injectorNames, angularNamespaces) {
129
- if (!isInjectCall(callNode, injectNames, angularNamespaces)) return false;
130
- return isInjectorReference(callNode.arguments?.[0], injectorNames, angularNamespaces);
451
+ function isDisallowedInjectInjector(context, callNode, injectNames, injectorNames, angularNamespaces) {
452
+ if (!isInjectCall(context, callNode, injectNames, angularNamespaces)) return false;
453
+ return isInjectorReference(context, callNode.arguments?.[0], injectorNames, angularNamespaces);
131
454
  }
132
- function isDisallowedRunInInjectionContext(callNode, runInInjectionContextNames, runInContextNames, angularNamespaces) {
455
+ function isDisallowedRunInInjectionContext(context, callNode, runInInjectionContextNames, runInContextNames, angularNamespaces) {
133
456
  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) ?? "");
457
+ if (callee?.type === "Identifier") return (runInInjectionContextNames.has(callee.name) || runInContextNames.has(callee.name)) && !isShadowedIdentifier(context, callee);
458
+ if (callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && angularNamespaces.has(callee.object.name) && !isShadowedIdentifier(context, callee.object)) return RUN_IN_INJECTION_CONTEXT_NAMES.has(getPropertyName(callee.property) ?? "");
136
459
  return false;
137
460
  }
138
461
  const avoidExplicitInjectionContext = defineRule({
@@ -193,11 +516,11 @@ const avoidExplicitInjectionContext = defineRule({
193
516
  const options = context.options[0] ?? {};
194
517
  const disallowInjectInjector = options.disallowInjectInjector ?? DEFAULT_DISALLOW_INJECT_INJECTOR;
195
518
  const disallowRunInInjectionContext = options.disallowRunInInjectionContext ?? DEFAULT_DISALLOW_RUN_IN_INJECTION_CONTEXT;
196
- if (disallowInjectInjector && isDisallowedInjectInjector(callNode, injectNames, injectorNames, angularNamespaces)) context.report({
519
+ if (disallowInjectInjector && isDisallowedInjectInjector(context, callNode, injectNames, injectorNames, angularNamespaces)) context.report({
197
520
  node: callNode.callee,
198
521
  messageId: "avoidInjectInjector"
199
522
  });
200
- if (disallowRunInInjectionContext && isDisallowedRunInInjectionContext(callNode, runInInjectionContextNames, runInContextNames, angularNamespaces)) context.report({
523
+ if (disallowRunInInjectionContext && isDisallowedRunInInjectionContext(context, callNode, runInInjectionContextNames, runInContextNames, angularNamespaces)) context.report({
201
524
  node: callNode.callee,
202
525
  messageId: "avoidRunInInjectionContext"
203
526
  });
@@ -206,13 +529,391 @@ const avoidExplicitInjectionContext = defineRule({
206
529
  }
207
530
  });
208
531
  //#endregion
532
+ //#region src/rules/avoid-explicit-subscription-management/index.ts
533
+ const TARGET_DECORATORS$2 = new Set([
534
+ "Component",
535
+ "Directive",
536
+ "Injectable"
537
+ ]);
538
+ const FIELD_NODE_TYPES$2 = new Set([
539
+ "AccessorProperty",
540
+ "FieldDefinition",
541
+ "PropertyDefinition"
542
+ ]);
543
+ const RXJS_SUBSCRIBABLE_NAMES = new Set([
544
+ "AsyncSubject",
545
+ "BehaviorSubject",
546
+ "Observable",
547
+ "ReplaySubject",
548
+ "Subject"
549
+ ]);
550
+ function hasTargetDecorator$2(context, classNode, decoratorImports) {
551
+ if (!classNode || !Array.isArray(classNode.decorators)) return false;
552
+ return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, decoratorImports));
553
+ }
554
+ function hasSubscriptionTypeReference(context, node, subscriptionLocalNames, rxjsNamespaces) {
555
+ if (!node) return false;
556
+ if (node.type === "TSTypeReference") {
557
+ const typeName = getTypeName(node.typeName);
558
+ if (node.typeName?.type === "Identifier" && typeName && subscriptionLocalNames.has(typeName) && !isShadowedIdentifier(context, node.typeName)) return true;
559
+ if (typeName?.includes(".")) {
560
+ const [namespaceName, memberName] = typeName.split(".");
561
+ return memberName === "Subscription" && rxjsNamespaces.has(namespaceName);
562
+ }
563
+ }
564
+ for (const [key, value] of Object.entries(node)) {
565
+ if (key === "parent") continue;
566
+ if (!value || typeof value !== "object") continue;
567
+ if (Array.isArray(value)) {
568
+ if (value.some((child) => hasSubscriptionTypeReference(context, child, subscriptionLocalNames, rxjsNamespaces))) return true;
569
+ continue;
570
+ }
571
+ if (hasSubscriptionTypeReference(context, value, subscriptionLocalNames, rxjsNamespaces)) return true;
572
+ }
573
+ return false;
574
+ }
575
+ function isSubscriptionConstructor(context, node, subscriptionLocalNames, rxjsNamespaces) {
576
+ const expression = unwrapExpression(node);
577
+ if (expression?.type !== "NewExpression") return false;
578
+ const callee = unwrapExpression(expression.callee);
579
+ if (callee?.type === "Identifier") return subscriptionLocalNames.has(callee.name) && !isShadowedIdentifier(context, callee);
580
+ if (callee?.type !== "MemberExpression") return false;
581
+ return callee.object?.type === "Identifier" && rxjsNamespaces.has(callee.object.name) && !isShadowedIdentifier(context, callee.object) && getPropertyName(callee.property) === "Subscription";
582
+ }
583
+ function getReferenceName(node) {
584
+ const expression = unwrapExpression(node);
585
+ if (!expression) return null;
586
+ if (expression.type === "Identifier") return expression.name;
587
+ if (expression.type !== "MemberExpression") return null;
588
+ if (expression.object?.type !== "ThisExpression") return null;
589
+ return getPropertyName(expression.property);
590
+ }
591
+ function isObservableReferenceName(name) {
592
+ return name.length > 1 && name.endsWith("$");
593
+ }
594
+ function addTrackedReference(references, name, binding) {
595
+ const bindings = references.get(name) ?? /* @__PURE__ */ new Set();
596
+ if (binding) bindings.add(binding);
597
+ references.set(name, bindings);
598
+ }
599
+ function hasTrackedReferenceName(references, name) {
600
+ return references.has(name);
601
+ }
602
+ function isTrackedReferenceExpression(context, node, references) {
603
+ const expression = unwrapExpression(node);
604
+ if (!expression) return false;
605
+ const referenceName = getReferenceName(expression);
606
+ if (!referenceName) return false;
607
+ const bindings = references.get(referenceName);
608
+ if (!bindings) return false;
609
+ if (expression.type === "MemberExpression" && expression.object?.type === "ThisExpression") return true;
610
+ if (expression.type !== "Identifier") return false;
611
+ const nearestBinding = findNearestBindingIdentifier(context, expression);
612
+ return !!nearestBinding && bindings.has(nearestBinding);
613
+ }
614
+ function getDeclaredName(node) {
615
+ if (!node) return null;
616
+ if (node.type === "Identifier") return node.name;
617
+ if (node.type === "PrivateIdentifier") return node.name;
618
+ return null;
619
+ }
620
+ function hasRxjsSubscribableTypeReference(context, node, rxjsSubscribableLocalNames, rxjsNamespaces) {
621
+ if (!node) return false;
622
+ if (node.type === "TSTypeReference") {
623
+ const typeName = getTypeName(node.typeName);
624
+ if (node.typeName?.type === "Identifier" && typeName && rxjsSubscribableLocalNames.has(typeName) && !isShadowedIdentifier(context, node.typeName)) return true;
625
+ if (typeName?.includes(".")) {
626
+ const [namespaceName, memberName] = typeName.split(".");
627
+ return RXJS_SUBSCRIBABLE_NAMES.has(memberName) && rxjsNamespaces.has(namespaceName);
628
+ }
629
+ }
630
+ for (const [key, value] of Object.entries(node)) {
631
+ if (key === "parent") continue;
632
+ if (!value || typeof value !== "object") continue;
633
+ if (Array.isArray(value)) {
634
+ if (value.some((child) => hasRxjsSubscribableTypeReference(context, child, rxjsSubscribableLocalNames, rxjsNamespaces))) return true;
635
+ continue;
636
+ }
637
+ if (hasRxjsSubscribableTypeReference(context, value, rxjsSubscribableLocalNames, rxjsNamespaces)) return true;
638
+ }
639
+ return false;
640
+ }
641
+ function isRxjsSubscribableConstructor(context, node, rxjsSubscribableLocalNames, rxjsNamespaces) {
642
+ const expression = unwrapExpression(node);
643
+ if (expression?.type !== "NewExpression") return false;
644
+ const callee = unwrapExpression(expression.callee);
645
+ if (callee?.type === "Identifier") return rxjsSubscribableLocalNames.has(callee.name) && !isShadowedIdentifier(context, callee);
646
+ if (callee?.type !== "MemberExpression") return false;
647
+ return callee.object?.type === "Identifier" && rxjsNamespaces.has(callee.object.name) && !isShadowedIdentifier(context, callee.object) && RXJS_SUBSCRIBABLE_NAMES.has(getPropertyName(callee.property) ?? "");
648
+ }
649
+ function isTakeUntilDestroyedCall(node, takeUntilDestroyedLocalNames, interopNamespaces) {
650
+ const callNode = unwrapExpression(node);
651
+ if (callNode?.type !== "CallExpression") return false;
652
+ const callee = unwrapExpression(callNode.callee);
653
+ if (callee?.type === "Identifier") return takeUntilDestroyedLocalNames.has(callee.name);
654
+ if (callee?.type !== "MemberExpression") return false;
655
+ return callee.object?.type === "Identifier" && interopNamespaces.has(callee.object.name) && getPropertyName(callee.property) === "takeUntilDestroyed";
656
+ }
657
+ function isTakeUntilDestroyedSubscribe(node, takeUntilDestroyedLocalNames, interopNamespaces) {
658
+ const callNode = unwrapExpression(node);
659
+ if (callNode?.type !== "CallExpression") return false;
660
+ const callee = unwrapExpression(callNode.callee);
661
+ if (callee?.type !== "MemberExpression") return false;
662
+ if (getPropertyName(callee.property) !== "subscribe") return false;
663
+ const source = unwrapExpression(callee.object);
664
+ if (source?.type !== "CallExpression") return false;
665
+ const sourceCallee = unwrapExpression(source.callee);
666
+ if (sourceCallee?.type !== "MemberExpression") return false;
667
+ if (getPropertyName(sourceCallee.property) !== "pipe") return false;
668
+ return (source.arguments ?? []).some((argument) => isTakeUntilDestroyedCall(argument, takeUntilDestroyedLocalNames, interopNamespaces));
669
+ }
670
+ function isKnownRxjsSubscribableExpression(context, node, rxjsSubscribableReferences, rxjsSubscribableLocalNames, rxjsNamespaces) {
671
+ const expression = unwrapExpression(node);
672
+ if (!expression) return false;
673
+ const referenceName = getReferenceName(expression);
674
+ if (referenceName && isObservableReferenceName(referenceName)) return true;
675
+ if (isTrackedReferenceExpression(context, expression, rxjsSubscribableReferences)) return true;
676
+ if (isRxjsSubscribableConstructor(context, expression, rxjsSubscribableLocalNames, rxjsNamespaces)) return true;
677
+ if (expression.type !== "CallExpression") return false;
678
+ const callee = unwrapExpression(expression.callee);
679
+ if (callee?.type !== "MemberExpression") return false;
680
+ const methodName = getPropertyName(callee.property);
681
+ if (methodName !== "asObservable" && methodName !== "pipe") return false;
682
+ return isKnownRxjsSubscribableExpression(context, callee.object, rxjsSubscribableReferences, rxjsSubscribableLocalNames, rxjsNamespaces);
683
+ }
684
+ function isUnmanagedSubscribeCall(context, node, rxjsSubscribableReferences, rxjsSubscribableLocalNames, rxjsNamespaces, takeUntilDestroyedLocalNames, interopNamespaces) {
685
+ const callNode = unwrapExpression(node);
686
+ if (callNode?.type !== "CallExpression") return false;
687
+ const callee = unwrapExpression(callNode.callee);
688
+ if (callee?.type !== "MemberExpression") return false;
689
+ if (getPropertyName(callee.property) !== "subscribe") return false;
690
+ return isKnownRxjsSubscribableExpression(context, callee.object, rxjsSubscribableReferences, rxjsSubscribableLocalNames, rxjsNamespaces) && !isTakeUntilDestroyedSubscribe(node, takeUntilDestroyedLocalNames, interopNamespaces);
691
+ }
692
+ function walkNode$1(node, containingClass, visitor) {
693
+ if (!node) return;
694
+ if (Array.isArray(node)) {
695
+ for (const child of node) walkNode$1(child, containingClass, visitor);
696
+ return;
697
+ }
698
+ if (node !== containingClass && (node.type === "ClassDeclaration" || node.type === "ClassExpression")) return;
699
+ if (visitor(node) === false) return;
700
+ for (const [key, value] of Object.entries(node)) {
701
+ if (key === "parent") continue;
702
+ if (!value || typeof value !== "object") continue;
703
+ walkNode$1(value, containingClass, visitor);
704
+ }
705
+ }
706
+ function containsUnmanagedSubscribeCall(context, node, containingClass, rxjsSubscribableReferences, rxjsSubscribableLocalNames, rxjsNamespaces, takeUntilDestroyedLocalNames, interopNamespaces) {
707
+ let found = false;
708
+ walkNode$1(node, containingClass, (current) => {
709
+ if (isUnmanagedSubscribeCall(context, current, rxjsSubscribableReferences, rxjsSubscribableLocalNames, rxjsNamespaces, takeUntilDestroyedLocalNames, interopNamespaces)) {
710
+ found = true;
711
+ return false;
712
+ }
713
+ });
714
+ return found;
715
+ }
716
+ function collectRxjsSubscribableReferences(context, classBody, classNode, rxjsSubscribableLocalNames, rxjsNamespaces) {
717
+ const references = /* @__PURE__ */ new Map();
718
+ let changed = true;
719
+ while (changed) {
720
+ changed = false;
721
+ walkNode$1(classBody, classNode, (node) => {
722
+ if (FIELD_NODE_TYPES$2.has(node.type)) {
723
+ const name = getDeclaredName(node.key);
724
+ if (!name || hasTrackedReferenceName(references, name)) return;
725
+ if (isObservableReferenceName(name) || hasRxjsSubscribableTypeReference(context, node.typeAnnotation?.typeAnnotation, rxjsSubscribableLocalNames, rxjsNamespaces) || isRxjsSubscribableConstructor(context, node.value, rxjsSubscribableLocalNames, rxjsNamespaces) || isKnownRxjsSubscribableExpression(context, node.value, references, rxjsSubscribableLocalNames, rxjsNamespaces)) {
726
+ addTrackedReference(references, name, node.key);
727
+ changed = true;
728
+ }
729
+ return;
730
+ }
731
+ if (node.type === "VariableDeclarator" && node.id?.type === "Identifier") {
732
+ if (hasTrackedReferenceName(references, node.id.name)) return;
733
+ 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)) {
734
+ addTrackedReference(references, node.id.name, node.id);
735
+ changed = true;
736
+ }
737
+ return;
738
+ }
739
+ if (node.type === "AssignmentExpression") {
740
+ const name = getReferenceName(node.left);
741
+ if (!name || hasTrackedReferenceName(references, name)) return;
742
+ if (isObservableReferenceName(name) || isRxjsSubscribableConstructor(context, node.right, rxjsSubscribableLocalNames, rxjsNamespaces) || isKnownRxjsSubscribableExpression(context, node.right, references, rxjsSubscribableLocalNames, rxjsNamespaces)) {
743
+ addTrackedReference(references, name, findNearestBindingIdentifier(context, node.left));
744
+ changed = true;
745
+ }
746
+ }
747
+ });
748
+ }
749
+ return references;
750
+ }
751
+ function collectSubscriptionReferences(context, classBody, classNode, subscriptionLocalNames, rxjsNamespaces, rxjsSubscribableReferences, rxjsSubscribableLocalNames, takeUntilDestroyedLocalNames, interopNamespaces) {
752
+ const references = /* @__PURE__ */ new Map();
753
+ walkNode$1(classBody, classNode, (node) => {
754
+ if (FIELD_NODE_TYPES$2.has(node.type)) {
755
+ const name = getDeclaredName(node.key);
756
+ if (!name) return;
757
+ if (hasSubscriptionTypeReference(context, node.typeAnnotation?.typeAnnotation, subscriptionLocalNames, rxjsNamespaces) || isSubscriptionConstructor(context, node.value, subscriptionLocalNames, rxjsNamespaces)) addTrackedReference(references, name, node.key);
758
+ return;
759
+ }
760
+ if (node.type === "VariableDeclarator" && node.id?.type === "Identifier") {
761
+ 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);
762
+ return;
763
+ }
764
+ if (node.type === "AssignmentExpression") {
765
+ const name = getReferenceName(node.left);
766
+ if (!name) return;
767
+ if (isSubscriptionConstructor(context, node.right, subscriptionLocalNames, rxjsNamespaces) || isUnmanagedSubscribeCall(context, node.right, rxjsSubscribableReferences, rxjsSubscribableLocalNames, rxjsNamespaces, takeUntilDestroyedLocalNames, interopNamespaces)) addTrackedReference(references, name, findNearestBindingIdentifier(context, node.left));
768
+ }
769
+ });
770
+ return references;
771
+ }
772
+ function isSubscriptionAddCall(context, node, containingClass, subscriptionReferences, rxjsSubscribableReferences, rxjsSubscribableLocalNames, rxjsNamespaces, takeUntilDestroyedLocalNames, interopNamespaces) {
773
+ const callNode = unwrapExpression(node);
774
+ if (callNode?.type !== "CallExpression") return false;
775
+ const callee = unwrapExpression(callNode.callee);
776
+ if (callee?.type !== "MemberExpression") return false;
777
+ if (getPropertyName(callee.property) !== "add") return false;
778
+ if (isTrackedReferenceExpression(context, callee.object, subscriptionReferences)) return true;
779
+ return (callNode.arguments ?? []).some((argument) => isTrackedReferenceExpression(context, argument, subscriptionReferences) || containsUnmanagedSubscribeCall(context, argument, containingClass, rxjsSubscribableReferences, rxjsSubscribableLocalNames, rxjsNamespaces, takeUntilDestroyedLocalNames, interopNamespaces));
780
+ }
781
+ function isSubscriptionUnsubscribeCall(context, node, subscriptionReferences) {
782
+ const callNode = unwrapExpression(node);
783
+ if (callNode?.type !== "CallExpression") return false;
784
+ const callee = unwrapExpression(callNode.callee);
785
+ if (callee?.type !== "MemberExpression") return false;
786
+ if (getPropertyName(callee.property) !== "unsubscribe") return false;
787
+ return isTrackedReferenceExpression(context, callee.object, subscriptionReferences);
788
+ }
789
+ const avoidExplicitSubscriptionManagement = defineRule({
790
+ meta: {
791
+ type: "suggestion",
792
+ docs: {
793
+ description: "Avoid manual RxJS Subscription lifecycle management in Angular components, directives, and services.",
794
+ recommended: true
795
+ },
796
+ schema: [],
797
+ messages: {
798
+ explicitSubscriptionType: "Avoid storing RxJS Subscription references in Angular classes. Prefer takeUntilDestroyed, toSignal, or firstValueFrom.",
799
+ explicitSubscriptionConstructor: "Avoid creating RxJS Subscription instances in Angular classes. Prefer takeUntilDestroyed, toSignal, or firstValueFrom.",
800
+ explicitSubscribe: "Avoid subscribe() calls that require manual lifecycle management. Prefer takeUntilDestroyed, toSignal, or firstValueFrom.",
801
+ explicitSubscriptionAdd: "Avoid adding subscriptions to a Subscription container. Prefer takeUntilDestroyed, toSignal, or firstValueFrom.",
802
+ explicitUnsubscribe: "Avoid manual unsubscribe() calls in Angular classes. Prefer takeUntilDestroyed, toSignal, or firstValueFrom."
803
+ }
804
+ },
805
+ createOnce(context) {
806
+ const subscriptionLocalNames = /* @__PURE__ */ new Set();
807
+ const rxjsSubscribableLocalNames = /* @__PURE__ */ new Set();
808
+ const rxjsNamespaces = /* @__PURE__ */ new Set();
809
+ const takeUntilDestroyedLocalNames = /* @__PURE__ */ new Set();
810
+ const interopNamespaces = /* @__PURE__ */ new Set();
811
+ const decoratorImports = {
812
+ decoratorNames: TARGET_DECORATORS$2,
813
+ decoratorLocalNames: /* @__PURE__ */ new Set(),
814
+ angularNamespaces: /* @__PURE__ */ new Set()
815
+ };
816
+ return {
817
+ before() {
818
+ subscriptionLocalNames.clear();
819
+ rxjsSubscribableLocalNames.clear();
820
+ rxjsNamespaces.clear();
821
+ takeUntilDestroyedLocalNames.clear();
822
+ interopNamespaces.clear();
823
+ decoratorImports.decoratorLocalNames.clear();
824
+ decoratorImports.angularNamespaces.clear();
825
+ },
826
+ ImportDeclaration(node) {
827
+ const source = node.source?.value;
828
+ if (source === "@angular/core") for (const specifier of node.specifiers ?? []) addAngularCoreDecoratorImport(specifier, TARGET_DECORATORS$2, decoratorImports);
829
+ if (source === "rxjs") for (const specifier of node.specifiers ?? []) {
830
+ if (specifier.type === "ImportSpecifier") {
831
+ const importedName = getPropertyName(specifier.imported);
832
+ if (importedName === "Subscription") subscriptionLocalNames.add(specifier.local.name);
833
+ if (RXJS_SUBSCRIBABLE_NAMES.has(importedName ?? "")) rxjsSubscribableLocalNames.add(specifier.local.name);
834
+ continue;
835
+ }
836
+ if (specifier.type === "ImportNamespaceSpecifier") rxjsNamespaces.add(specifier.local.name);
837
+ }
838
+ if (source === "@angular/core/rxjs-interop") for (const specifier of node.specifiers ?? []) {
839
+ if (specifier.type === "ImportSpecifier") {
840
+ if (getPropertyName(specifier.imported) === "takeUntilDestroyed") takeUntilDestroyedLocalNames.add(specifier.local.name);
841
+ continue;
842
+ }
843
+ if (specifier.type === "ImportNamespaceSpecifier") interopNamespaces.add(specifier.local.name);
844
+ }
845
+ },
846
+ ClassBody(node) {
847
+ const classBody = node;
848
+ const classNode = classBody.parent;
849
+ if (!classNode || !hasTargetDecorator$2(context, classNode, decoratorImports)) return;
850
+ const rxjsSubscribableReferences = collectRxjsSubscribableReferences(context, classBody, classNode, rxjsSubscribableLocalNames, rxjsNamespaces);
851
+ const subscriptionReferences = collectSubscriptionReferences(context, classBody, classNode, subscriptionLocalNames, rxjsNamespaces, rxjsSubscribableReferences, rxjsSubscribableLocalNames, takeUntilDestroyedLocalNames, interopNamespaces);
852
+ walkNode$1(classBody, classNode, (current) => {
853
+ if (FIELD_NODE_TYPES$2.has(current.type)) {
854
+ if (hasSubscriptionTypeReference(context, current.typeAnnotation?.typeAnnotation, subscriptionLocalNames, rxjsNamespaces)) context.report({
855
+ node: current.key ?? current,
856
+ messageId: "explicitSubscriptionType"
857
+ });
858
+ return;
859
+ }
860
+ if (current.type === "VariableDeclarator" && current.id?.type === "Identifier" && hasSubscriptionTypeReference(context, current.id.typeAnnotation?.typeAnnotation, subscriptionLocalNames, rxjsNamespaces)) {
861
+ context.report({
862
+ node: current.id,
863
+ messageId: "explicitSubscriptionType"
864
+ });
865
+ return;
866
+ }
867
+ if (isSubscriptionConstructor(context, current, subscriptionLocalNames, rxjsNamespaces)) {
868
+ context.report({
869
+ node: current,
870
+ messageId: "explicitSubscriptionConstructor"
871
+ });
872
+ return false;
873
+ }
874
+ if (isSubscriptionAddCall(context, current, classNode, subscriptionReferences, rxjsSubscribableReferences, rxjsSubscribableLocalNames, rxjsNamespaces, takeUntilDestroyedLocalNames, interopNamespaces)) {
875
+ context.report({
876
+ node: current,
877
+ messageId: "explicitSubscriptionAdd"
878
+ });
879
+ return false;
880
+ }
881
+ if (isSubscriptionUnsubscribeCall(context, current, subscriptionReferences)) {
882
+ context.report({
883
+ node: current,
884
+ messageId: "explicitUnsubscribe"
885
+ });
886
+ return false;
887
+ }
888
+ if (isUnmanagedSubscribeCall(context, current, rxjsSubscribableReferences, rxjsSubscribableLocalNames, rxjsNamespaces, takeUntilDestroyedLocalNames, interopNamespaces)) {
889
+ context.report({
890
+ node: current,
891
+ messageId: "explicitSubscribe"
892
+ });
893
+ return false;
894
+ }
895
+ });
896
+ }
897
+ };
898
+ }
899
+ });
900
+ //#endregion
209
901
  //#region src/rules/avoid-ng-modules/index.ts
210
902
  const DEFAULT_ALLOW_FOR_GROUPING = true;
211
903
  const DEFAULT_ALLOW_FOR_PROVIDING = false;
212
904
  const DEFAULT_ALLOW_FOR_ROUTING = false;
213
- function getNgModuleMetadata(node) {
214
- if (node.type !== "Decorator") return null;
215
- if (getDecoratorName(node) !== "NgModule") return null;
905
+ function isNgModuleDecorator(context, node, importBindings, namespaceImportBindings) {
906
+ if (node.type !== "Decorator") return false;
907
+ const expression = node.expression ?? node;
908
+ const callee = expression?.type === "CallExpression" ? expression.callee : expression;
909
+ if (callee?.type === "Identifier") {
910
+ const binding = importBindings.get(callee.name);
911
+ return !!binding && binding.source === "@angular/core" && binding.importedName === "NgModule" && !isShadowedIdentifier(context, callee);
912
+ }
913
+ return callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && namespaceImportBindings.get(callee.object.name) === "@angular/core" && !isShadowedIdentifier(context, callee.object) && getPropertyName(callee.property) === "NgModule";
914
+ }
915
+ function getNgModuleMetadata(context, node, importBindings, namespaceImportBindings) {
916
+ if (!isNgModuleDecorator(context, node, importBindings, namespaceImportBindings)) return null;
216
917
  const expression = node.expression;
217
918
  if (expression?.type !== "CallExpression") return null;
218
919
  const metadata = expression.arguments?.[0];
@@ -226,8 +927,19 @@ function getArrayElementsFromProperty(metadata, propertyName) {
226
927
  function isMemberCall(node, methodName) {
227
928
  return node.type === "CallExpression" && getPropertyName(node.callee?.property) === methodName;
228
929
  }
229
- function isRouterModuleForChild(node) {
230
- return isMemberCall(node, "forChild") && node.callee?.type === "MemberExpression" && getPropertyName(node.callee.object) === "RouterModule";
930
+ function isModuleCallFromImport(node, expectedSource, expectedModuleName, importBindings, namespaceImportBindings) {
931
+ if (node.type !== "CallExpression") return false;
932
+ const callee = node.callee;
933
+ if (callee?.type !== "MemberExpression") return false;
934
+ const moduleRef = callee.object;
935
+ if (moduleRef?.type === "Identifier") {
936
+ const binding = importBindings.get(moduleRef.name);
937
+ return !!binding && binding.source === expectedSource && binding.importedName === expectedModuleName;
938
+ }
939
+ if (moduleRef?.type !== "MemberExpression") return false;
940
+ if (moduleRef.object?.type !== "Identifier") return false;
941
+ if (namespaceImportBindings.get(moduleRef.object.name) !== expectedSource) return false;
942
+ return getPropertyName(moduleRef.property) === expectedModuleName;
231
943
  }
232
944
  function getNgModuleClassNode(decoratorNode) {
233
945
  const parent = decoratorNode.parent;
@@ -264,48 +976,342 @@ const avoidNgModules = defineRule({
264
976
  }
265
977
  }],
266
978
  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')."
979
+ avoidModuleImportsExports: "NgModule imports/exports are legacy composition. Prefer using standalone things directly.",
980
+ avoidForRoot: "Avoid module.forRoot(...). Prefer provideX(...) functions to register injection providers.",
981
+ avoidRouterForRoot: "Avoid RouterModule.forRoot(routes). Prefer provideRouter(routes). See https://angular.dev/guide/routing/define-routes",
982
+ avoidRouterForChild: "Avoid RouterModule.forChild(routes). Prefer provideRouter(...) and lazy route entries such as loadComponent: () => import('./components/auth/login-page').",
983
+ avoidNgrxStoreForRoot: "Avoid StoreModule.forRoot(...). Prefer provideStore(...) with standalone providers. See https://ngrx.io/guide/store",
984
+ avoidNgrxEffectsForRoot: "Avoid EffectsModule.forRoot(...). Prefer provideEffects(...) with standalone providers. See https://ngrx.io/guide/effects",
985
+ avoidNgrxStoreForFeature: "Avoid StoreModule.forFeature(...). Prefer provideState(...) with standalone providers. See https://ngrx.io/guide/store",
986
+ avoidNgrxEffectsForFeature: "Avoid EffectsModule.forFeature(...). Prefer provideEffects(...) scoped to feature providers. See https://ngrx.io/guide/effects",
987
+ avoidNgrxStoreDevtoolsInstrument: "Avoid StoreDevtoolsModule.instrument(...). Prefer provideStoreDevtools(...) with standalone providers. See https://ngrx.io/guide/store-devtools",
988
+ avoidNgrxRouterStoreForRoot: "Avoid StoreRouterConnectingModule.forRoot(...). Prefer provideRouterStore(...) with standalone providers. See https://ngrx.io/guide/router-store"
989
+ }
990
+ },
991
+ createOnce(context) {
992
+ const importBindings = /* @__PURE__ */ new Map();
993
+ const namespaceImportBindings = /* @__PURE__ */ new Map();
994
+ return {
995
+ before() {
996
+ importBindings.clear();
997
+ namespaceImportBindings.clear();
998
+ },
999
+ ImportDeclaration(node) {
1000
+ const source = node.source?.value;
1001
+ if (typeof source !== "string") return;
1002
+ for (const specifier of node.specifiers ?? []) {
1003
+ if (specifier.type === "ImportSpecifier") {
1004
+ const importedName = getPropertyName(specifier.imported);
1005
+ if (!importedName) continue;
1006
+ importBindings.set(specifier.local.name, {
1007
+ source,
1008
+ importedName
1009
+ });
1010
+ continue;
1011
+ }
1012
+ if (specifier.type === "ImportNamespaceSpecifier") namespaceImportBindings.set(specifier.local.name, source);
1013
+ }
1014
+ },
1015
+ Decorator(node) {
1016
+ const options = context.options[0] ?? {};
1017
+ const allowForGrouping = options.allowForGrouping ?? DEFAULT_ALLOW_FOR_GROUPING;
1018
+ const allowForProviding = options.allowForProviding ?? DEFAULT_ALLOW_FOR_PROVIDING;
1019
+ const allowForRouting = options.allowForRouting ?? DEFAULT_ALLOW_FOR_ROUTING;
1020
+ const metadata = getNgModuleMetadata(context, node, importBindings, namespaceImportBindings);
1021
+ if (!metadata) return;
1022
+ const importsElements = getArrayElementsFromProperty(metadata, "imports");
1023
+ const exportsElements = getArrayElementsFromProperty(metadata, "exports");
1024
+ if (!allowForGrouping && (importsElements.length > 0 || exportsElements.length > 0)) context.report({
1025
+ node,
1026
+ messageId: "avoidModuleImportsExports"
1027
+ });
1028
+ const importCalls = importsElements.filter((element) => element.type === "CallExpression");
1029
+ if (!allowForProviding) {
1030
+ for (const call of importCalls) {
1031
+ const methodName = getPropertyName(call.callee?.property);
1032
+ if (methodName === "forRoot" && isModuleCallFromImport(call, "@angular/router", "RouterModule", importBindings, namespaceImportBindings) && !allowForRouting) {
1033
+ context.report({
1034
+ node: call,
1035
+ messageId: "avoidRouterForRoot"
1036
+ });
1037
+ continue;
1038
+ }
1039
+ if (methodName === "forRoot" && isModuleCallFromImport(call, "@ngrx/store", "StoreModule", importBindings, namespaceImportBindings)) {
1040
+ context.report({
1041
+ node: call,
1042
+ messageId: "avoidNgrxStoreForRoot"
1043
+ });
1044
+ continue;
1045
+ }
1046
+ if (methodName === "forRoot" && isModuleCallFromImport(call, "@ngrx/effects", "EffectsModule", importBindings, namespaceImportBindings)) {
1047
+ context.report({
1048
+ node: call,
1049
+ messageId: "avoidNgrxEffectsForRoot"
1050
+ });
1051
+ continue;
1052
+ }
1053
+ if (methodName === "forFeature" && isModuleCallFromImport(call, "@ngrx/store", "StoreModule", importBindings, namespaceImportBindings)) {
1054
+ context.report({
1055
+ node: call,
1056
+ messageId: "avoidNgrxStoreForFeature"
1057
+ });
1058
+ continue;
1059
+ }
1060
+ if (methodName === "forFeature" && isModuleCallFromImport(call, "@ngrx/effects", "EffectsModule", importBindings, namespaceImportBindings)) {
1061
+ context.report({
1062
+ node: call,
1063
+ messageId: "avoidNgrxEffectsForFeature"
1064
+ });
1065
+ continue;
1066
+ }
1067
+ if (methodName === "instrument" && isModuleCallFromImport(call, "@ngrx/store-devtools", "StoreDevtoolsModule", importBindings, namespaceImportBindings)) {
1068
+ context.report({
1069
+ node: call,
1070
+ messageId: "avoidNgrxStoreDevtoolsInstrument"
1071
+ });
1072
+ continue;
1073
+ }
1074
+ if (methodName === "forRoot" && isModuleCallFromImport(call, "@ngrx/router-store", "StoreRouterConnectingModule", importBindings, namespaceImportBindings)) {
1075
+ context.report({
1076
+ node: call,
1077
+ messageId: "avoidNgrxRouterStoreForRoot"
1078
+ });
1079
+ continue;
1080
+ }
1081
+ if (methodName === "forRoot" && isModuleCallFromImport(call, "@angular/router", "RouterModule", importBindings, namespaceImportBindings) && allowForRouting) continue;
1082
+ if (!isMemberCall(call, "forRoot")) continue;
1083
+ context.report({
1084
+ node: call,
1085
+ messageId: "avoidForRoot"
1086
+ });
1087
+ }
1088
+ const staticForRootMethod = getStaticForRootMethod(getNgModuleClassNode(node));
1089
+ if (staticForRootMethod) context.report({
1090
+ node: staticForRootMethod.key ?? staticForRootMethod,
1091
+ messageId: "avoidForRoot"
1092
+ });
1093
+ }
1094
+ if (!allowForRouting) for (const call of importCalls) {
1095
+ if (!isMemberCall(call, "forChild")) continue;
1096
+ if (!isModuleCallFromImport(call, "@angular/router", "RouterModule", importBindings, namespaceImportBindings)) continue;
1097
+ context.report({
1098
+ node: call,
1099
+ messageId: "avoidRouterForChild"
1100
+ });
1101
+ }
1102
+ }
1103
+ };
1104
+ }
1105
+ });
1106
+ //#endregion
1107
+ //#region src/rules/avoid-rxjs-state-in-component/index.ts
1108
+ const TARGET_DECORATORS$1 = new Set(["Component", "Directive"]);
1109
+ const SUBJECT_NAMES = new Set([
1110
+ "BehaviorSubject",
1111
+ "ReplaySubject",
1112
+ "Subject"
1113
+ ]);
1114
+ const FIELD_NODE_TYPES$1 = new Set([
1115
+ "AccessorProperty",
1116
+ "FieldDefinition",
1117
+ "PropertyDefinition"
1118
+ ]);
1119
+ function hasTargetDecorator$1(context, classNode, decoratorImports) {
1120
+ if (!classNode || !Array.isArray(classNode.decorators)) return false;
1121
+ return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, decoratorImports));
1122
+ }
1123
+ function getImportedSubjectKind(localName, subjectLocalNames) {
1124
+ return subjectLocalNames.get(localName) ?? null;
1125
+ }
1126
+ function getSubjectKindFromType(context, node, subjectLocalNames, rxjsNamespaces) {
1127
+ if (!node) return null;
1128
+ if (node.type !== "TSTypeReference") return null;
1129
+ const typeName = getTypeName(node.typeName);
1130
+ if (!typeName) return null;
1131
+ const localKind = getImportedSubjectKind(typeName, subjectLocalNames);
1132
+ if (localKind && node.typeName?.type === "Identifier" && !isShadowedIdentifier(context, node.typeName)) return localKind;
1133
+ if (!typeName.includes(".")) return null;
1134
+ const [namespaceName, memberName] = typeName.split(".");
1135
+ if (!rxjsNamespaces.has(namespaceName)) return null;
1136
+ if (node.typeName?.type === "TSQualifiedName" && node.typeName.left?.type === "Identifier" && isShadowedIdentifier(context, node.typeName.left)) return null;
1137
+ return SUBJECT_NAMES.has(memberName) ? memberName : null;
1138
+ }
1139
+ function getSubjectKindFromConstructor(context, node, subjectLocalNames, rxjsNamespaces) {
1140
+ const expression = unwrapExpression(node);
1141
+ if (expression?.type !== "NewExpression") return null;
1142
+ const callee = unwrapExpression(expression.callee);
1143
+ if (callee?.type === "Identifier") {
1144
+ if (isShadowedIdentifier(context, callee)) return null;
1145
+ return getImportedSubjectKind(callee.name, subjectLocalNames);
1146
+ }
1147
+ if (callee?.type !== "MemberExpression") return null;
1148
+ if (callee.object?.type !== "Identifier" || !rxjsNamespaces.has(callee.object.name)) return null;
1149
+ if (isShadowedIdentifier(context, callee.object)) return null;
1150
+ const memberName = getPropertyName(callee.property);
1151
+ return SUBJECT_NAMES.has(memberName) ? memberName : null;
1152
+ }
1153
+ function getMemberName(node) {
1154
+ const expression = unwrapExpression(node);
1155
+ if (!expression) return null;
1156
+ if (expression.type === "Identifier") return expression.name;
1157
+ if (expression.type === "PrivateIdentifier") return expression.name;
1158
+ return null;
1159
+ }
1160
+ function getThisFieldName(node) {
1161
+ const expression = unwrapExpression(node);
1162
+ if (expression?.type !== "MemberExpression") return null;
1163
+ if (expression.object?.type !== "ThisExpression") return null;
1164
+ return getPropertyName(expression.property);
1165
+ }
1166
+ function isCallOnThisField(node, fields) {
1167
+ const callNode = unwrapExpression(node);
1168
+ if (callNode?.type !== "CallExpression") return null;
1169
+ const callee = unwrapExpression(callNode.callee);
1170
+ if (callee?.type !== "MemberExpression") return null;
1171
+ const fieldName = getThisFieldName(callee.object);
1172
+ const methodName = getPropertyName(callee.property);
1173
+ if (!fieldName || !methodName || !fields.has(fieldName)) return null;
1174
+ return {
1175
+ fieldName,
1176
+ methodName,
1177
+ argumentCount: callNode.arguments?.length ?? 0
1178
+ };
1179
+ }
1180
+ function isNgOnDestroyMethod(member) {
1181
+ return member.type === "MethodDefinition" && getPropertyName(member.key) === "ngOnDestroy" && !!member.value;
1182
+ }
1183
+ function walkNode(node, visitor) {
1184
+ if (!node) return;
1185
+ if (Array.isArray(node)) {
1186
+ for (const child of node) walkNode(child, visitor);
1187
+ return;
1188
+ }
1189
+ visitor(node);
1190
+ for (const [key, value] of Object.entries(node)) {
1191
+ if (key === "parent") continue;
1192
+ if (!value || typeof value !== "object") continue;
1193
+ walkNode(value, visitor);
1194
+ }
1195
+ }
1196
+ function getUsage(usages, fieldName) {
1197
+ const existing = usages.get(fieldName);
1198
+ if (existing) return existing;
1199
+ const next = {
1200
+ hasAsObservable: false,
1201
+ hasNextWithValue: false,
1202
+ hasNgOnDestroyComplete: false,
1203
+ hasNgOnDestroyNext: false
1204
+ };
1205
+ usages.set(fieldName, next);
1206
+ return next;
1207
+ }
1208
+ function collectSubjectFields(context, classBody, subjectLocalNames, rxjsNamespaces) {
1209
+ const fields = /* @__PURE__ */ new Map();
1210
+ for (const member of classBody.body ?? []) {
1211
+ if (!FIELD_NODE_TYPES$1.has(member.type)) continue;
1212
+ const fieldName = getMemberName(member.key);
1213
+ if (!fieldName) continue;
1214
+ const typeNode = member.typeAnnotation?.typeAnnotation;
1215
+ const kind = getSubjectKindFromConstructor(context, member.value, subjectLocalNames, rxjsNamespaces) ?? getSubjectKindFromType(context, typeNode, subjectLocalNames, rxjsNamespaces);
1216
+ if (!kind) continue;
1217
+ fields.set(fieldName, {
1218
+ key: member.key ?? member,
1219
+ kind
1220
+ });
1221
+ }
1222
+ return fields;
1223
+ }
1224
+ function collectFieldUsages(classBody, fields) {
1225
+ const usages = /* @__PURE__ */ new Map();
1226
+ const ngOnDestroyMethods = /* @__PURE__ */ new Set();
1227
+ for (const member of classBody.body ?? []) if (isNgOnDestroyMethod(member)) ngOnDestroyMethods.add(member.value);
1228
+ walkNode(classBody, (node) => {
1229
+ const call = isCallOnThisField(node, fields);
1230
+ if (!call) return;
1231
+ const usage = getUsage(usages, call.fieldName);
1232
+ if (call.methodName === "asObservable") usage.hasAsObservable = true;
1233
+ if (call.methodName === "next" && call.argumentCount > 0) usage.hasNextWithValue = true;
1234
+ const isInsideNgOnDestroy = ngOnDestroyMethods.has(node.parent) || ngOnDestroyMethods.has(node);
1235
+ let current = node.parent;
1236
+ while (!isInsideNgOnDestroy && current) {
1237
+ if (ngOnDestroyMethods.has(current)) break;
1238
+ current = current.parent;
1239
+ }
1240
+ if (!current && !ngOnDestroyMethods.has(node.parent) && !ngOnDestroyMethods.has(node)) return;
1241
+ if (call.methodName === "next") usage.hasNgOnDestroyNext = true;
1242
+ if (call.methodName === "complete") usage.hasNgOnDestroyComplete = true;
1243
+ });
1244
+ return usages;
1245
+ }
1246
+ function isDestroySubjectUsage(usage) {
1247
+ return !!usage?.hasNgOnDestroyNext && !!usage.hasNgOnDestroyComplete;
1248
+ }
1249
+ const avoidRxjsStateInComponent = defineRule({
1250
+ meta: {
1251
+ type: "suggestion",
1252
+ docs: {
1253
+ description: "Avoid using RxJS Subject classes for component or directive state and destruction lifecycle.",
1254
+ recommended: true
1255
+ },
1256
+ schema: [],
1257
+ messages: {
1258
+ avoidRxjsState: "Avoid using RxJS {{kind}} for component/directive state. Prefer signal(), computed(), or linkedSignal().",
1259
+ avoidDestroySubject: "Avoid destroy Subject lifecycle management. Prefer takeUntilDestroyed() from @angular/core/rxjs-interop."
270
1260
  }
271
1261
  },
272
1262
  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;
1263
+ const subjectLocalNames = /* @__PURE__ */ new Map();
1264
+ const rxjsNamespaces = /* @__PURE__ */ new Set();
1265
+ const decoratorImports = {
1266
+ decoratorNames: TARGET_DECORATORS$1,
1267
+ decoratorLocalNames: /* @__PURE__ */ new Set(),
1268
+ angularNamespaces: /* @__PURE__ */ new Set()
1269
+ };
1270
+ return {
1271
+ before() {
1272
+ subjectLocalNames.clear();
1273
+ rxjsNamespaces.clear();
1274
+ decoratorImports.decoratorLocalNames.clear();
1275
+ decoratorImports.angularNamespaces.clear();
1276
+ },
1277
+ ImportDeclaration(node) {
1278
+ if (node.source?.value === "@angular/core") {
1279
+ for (const specifier of node.specifiers ?? []) addAngularCoreDecoratorImport(specifier, TARGET_DECORATORS$1, decoratorImports);
1280
+ return;
1281
+ }
1282
+ if (node.source?.value !== "rxjs") return;
1283
+ for (const specifier of node.specifiers ?? []) {
1284
+ if (specifier.type === "ImportSpecifier") {
1285
+ const importedName = getPropertyName(specifier.imported);
1286
+ if (SUBJECT_NAMES.has(importedName)) subjectLocalNames.set(specifier.local.name, importedName);
1287
+ continue;
1288
+ }
1289
+ if (specifier.type === "ImportNamespaceSpecifier") rxjsNamespaces.add(specifier.local.name);
1290
+ }
1291
+ },
1292
+ ClassBody(node) {
1293
+ const classBody = node;
1294
+ const classNode = classBody.parent;
1295
+ if (!hasTargetDecorator$1(context, classNode, decoratorImports)) return;
1296
+ const fields = collectSubjectFields(context, classBody, subjectLocalNames, rxjsNamespaces);
1297
+ const usages = collectFieldUsages(classBody, fields);
1298
+ for (const [fieldName, field] of fields) {
1299
+ const usage = usages.get(fieldName);
1300
+ if (field.kind === "Subject" && isDestroySubjectUsage(usage)) {
1301
+ context.report({
1302
+ node: field.key,
1303
+ messageId: "avoidDestroySubject"
1304
+ });
1305
+ continue;
1306
+ }
290
1307
  context.report({
291
- node: call,
292
- messageId: "avoidForRoot"
1308
+ node: field.key,
1309
+ messageId: "avoidRxjsState",
1310
+ data: { kind: field.kind }
293
1311
  });
294
1312
  }
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
- context.report({
304
- node: call,
305
- messageId: "avoidRouterForChild"
306
- });
307
1313
  }
308
- } };
1314
+ };
309
1315
  }
310
1316
  });
311
1317
  //#endregion
@@ -323,38 +1329,49 @@ const KNOWN_SIGNAL_CREATION_FUNCTIONS = new Set([
323
1329
  const LINKED_SIGNAL_CREATOR_NAME = "linkedSignal";
324
1330
  const COMPUTED_CREATOR_NAME = "computed";
325
1331
  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;
1332
+ function isAngularCoreNamespaceMember(context, node, angularNamespaces, memberName) {
1333
+ return node?.type === "MemberExpression" && node.object?.type === "Identifier" && angularNamespaces.has(node.object.name) && !isShadowedIdentifier(context, node.object) && getPropertyName(node.property) === memberName;
328
1334
  }
329
- function isSignalCreatorCall(node, signalCreatorNames, angularNamespaces) {
1335
+ function isSignalCreatorCall(context, node, signalCreatorNames, angularNamespaces) {
330
1336
  if (node?.type !== "CallExpression") return false;
331
1337
  const callee = node.callee;
332
- return callee?.type === "Identifier" && signalCreatorNames.has(callee.name) || [...KNOWN_SIGNAL_CREATION_FUNCTIONS].some((name) => isAngularCoreNamespaceMember(callee, angularNamespaces, name));
1338
+ return callee?.type === "Identifier" && signalCreatorNames.has(callee.name) && !isShadowedIdentifier(context, callee) || [...KNOWN_SIGNAL_CREATION_FUNCTIONS].some((name) => isAngularCoreNamespaceMember(context, callee, angularNamespaces, name));
333
1339
  }
334
- function isEffectCall(node, effectNames, angularNamespaces) {
1340
+ function isEffectCall(context, node, effectNames, angularNamespaces) {
335
1341
  const callee = node.callee;
336
- return callee?.type === "Identifier" && effectNames.has(callee.name) || isAngularCoreNamespaceMember(callee, angularNamespaces, "effect");
1342
+ return callee?.type === "Identifier" && effectNames.has(callee.name) && !isShadowedIdentifier(context, callee) || isAngularCoreNamespaceMember(context, callee, angularNamespaces, "effect");
337
1343
  }
338
- function isReactiveCreatorCall(node, creatorNames, angularNamespaces, angularMemberName) {
1344
+ function isReactiveCreatorCall(context, node, creatorNames, angularNamespaces, angularMemberName) {
339
1345
  const callee = node.callee;
340
- return callee?.type === "Identifier" && creatorNames.has(callee.name) || isAngularCoreNamespaceMember(callee, angularNamespaces, angularMemberName);
1346
+ return callee?.type === "Identifier" && creatorNames.has(callee.name) && !isShadowedIdentifier(context, callee) || isAngularCoreNamespaceMember(context, callee, angularNamespaces, angularMemberName);
341
1347
  }
342
- function isKnownSignalObject(objectNode, signalVariables, classSignalProperties) {
1348
+ function isKnownSignalObject(context, objectNode, signalVariableBindings, classSignalProperties) {
343
1349
  if (!objectNode) return false;
344
- if (objectNode.type === "Identifier") return signalVariables.has(objectNode.name);
1350
+ if (objectNode.type === "Identifier") {
1351
+ const trackedBindings = signalVariableBindings.get(objectNode.name);
1352
+ if (!trackedBindings) return false;
1353
+ const nearestBinding = findNearestBindingIdentifier(context, objectNode);
1354
+ return !!nearestBinding && trackedBindings.has(nearestBinding);
1355
+ }
345
1356
  return objectNode.type === "MemberExpression" && objectNode.object?.type === "ThisExpression" && objectNode.property?.type === "Identifier" && classSignalProperties.has(objectNode.property.name);
346
1357
  }
347
- function visitNodes$1(node, visitor) {
1358
+ const FUNCTION_NODE_TYPES = new Set([
1359
+ "ArrowFunctionExpression",
1360
+ "FunctionExpression",
1361
+ "FunctionDeclaration"
1362
+ ]);
1363
+ function visitNodes(node, visitor, skipFunctionBodies = false) {
348
1364
  if (!node) return;
349
1365
  if (Array.isArray(node)) {
350
- for (const item of node) visitNodes$1(item, visitor);
1366
+ for (const item of node) visitNodes(item, visitor, skipFunctionBodies);
351
1367
  return;
352
1368
  }
1369
+ if (skipFunctionBodies && FUNCTION_NODE_TYPES.has(node.type)) return;
353
1370
  visitor(node);
354
1371
  for (const [key, value] of Object.entries(node)) {
355
1372
  if (key === "parent") continue;
356
1373
  if (!value || typeof value !== "object") continue;
357
- visitNodes$1(value, visitor);
1374
+ visitNodes(value, visitor, skipFunctionBodies);
358
1375
  }
359
1376
  }
360
1377
  const avoidWritingSignalsInReactiveContext = defineRule({
@@ -386,7 +1403,7 @@ const avoidWritingSignalsInReactiveContext = defineRule({
386
1403
  const linkedSignalNames = /* @__PURE__ */ new Set();
387
1404
  const signalCreatorNames = /* @__PURE__ */ new Set();
388
1405
  const angularNamespaces = /* @__PURE__ */ new Set();
389
- const signalVariables = /* @__PURE__ */ new Set();
1406
+ const signalVariableBindings = /* @__PURE__ */ new Map();
390
1407
  const classSignalProperties = /* @__PURE__ */ new Set();
391
1408
  return {
392
1409
  before() {
@@ -395,7 +1412,7 @@ const avoidWritingSignalsInReactiveContext = defineRule({
395
1412
  linkedSignalNames.clear();
396
1413
  signalCreatorNames.clear();
397
1414
  angularNamespaces.clear();
398
- signalVariables.clear();
1415
+ signalVariableBindings.clear();
399
1416
  classSignalProperties.clear();
400
1417
  },
401
1418
  ImportDeclaration(node) {
@@ -414,13 +1431,15 @@ const avoidWritingSignalsInReactiveContext = defineRule({
414
1431
  VariableDeclarator(node) {
415
1432
  const declarator = node;
416
1433
  if (declarator.id?.type !== "Identifier") return;
417
- if (!isSignalCreatorCall(declarator.init, signalCreatorNames, angularNamespaces)) return;
418
- signalVariables.add(declarator.id.name);
1434
+ if (!isSignalCreatorCall(context, declarator.init, signalCreatorNames, angularNamespaces)) return;
1435
+ const bindings = signalVariableBindings.get(declarator.id.name) ?? /* @__PURE__ */ new Set();
1436
+ bindings.add(declarator.id);
1437
+ signalVariableBindings.set(declarator.id.name, bindings);
419
1438
  },
420
1439
  "PropertyDefinition, FieldDefinition, AccessorProperty"(node) {
421
1440
  const property = node;
422
1441
  if (property.key?.type !== "Identifier") return;
423
- if (!isSignalCreatorCall(property.value, signalCreatorNames, angularNamespaces)) return;
1442
+ if (!isSignalCreatorCall(context, property.value, signalCreatorNames, angularNamespaces)) return;
424
1443
  classSignalProperties.add(property.key.name);
425
1444
  },
426
1445
  CallExpression(node) {
@@ -429,21 +1448,21 @@ const avoidWritingSignalsInReactiveContext = defineRule({
429
1448
  const allowComputedAndLinkedSignals = options.allowComputedAndLinkedSignals ?? false;
430
1449
  const callNode = node;
431
1450
  const callbackCandidates = [];
432
- if (!allowEffects && isEffectCall(callNode, effectNames, angularNamespaces)) {
1451
+ if (!allowEffects && isEffectCall(context, callNode, effectNames, angularNamespaces)) {
433
1452
  const callback = callNode.arguments?.[0];
434
1453
  if (callback?.type === "ArrowFunctionExpression" || callback?.type === "FunctionExpression") callbackCandidates.push({
435
1454
  callback,
436
1455
  contextName: "effect()"
437
1456
  });
438
1457
  }
439
- if (!allowComputedAndLinkedSignals && isReactiveCreatorCall(callNode, computedNames, angularNamespaces, COMPUTED_CREATOR_NAME)) {
1458
+ if (!allowComputedAndLinkedSignals && isReactiveCreatorCall(context, callNode, computedNames, angularNamespaces, COMPUTED_CREATOR_NAME)) {
440
1459
  const callback = callNode.arguments?.[0];
441
1460
  if (callback?.type === "ArrowFunctionExpression" || callback?.type === "FunctionExpression") callbackCandidates.push({
442
1461
  callback,
443
1462
  contextName: "computed()"
444
1463
  });
445
1464
  }
446
- if (!allowComputedAndLinkedSignals && isReactiveCreatorCall(callNode, linkedSignalNames, angularNamespaces, LINKED_SIGNAL_CREATOR_NAME)) for (const argumentNode of callNode.arguments ?? []) {
1465
+ if (!allowComputedAndLinkedSignals && isReactiveCreatorCall(context, callNode, linkedSignalNames, angularNamespaces, LINKED_SIGNAL_CREATOR_NAME)) for (const argumentNode of callNode.arguments ?? []) {
447
1466
  const argument = argumentNode;
448
1467
  if (argument.type === "ArrowFunctionExpression" || argument.type === "FunctionExpression") {
449
1468
  callbackCandidates.push({
@@ -464,27 +1483,27 @@ const avoidWritingSignalsInReactiveContext = defineRule({
464
1483
  });
465
1484
  }
466
1485
  }
467
- for (const { callback, contextName } of callbackCandidates) visitNodes$1(callback.body, (current) => {
1486
+ for (const { callback, contextName } of callbackCandidates) visitNodes(callback.body, (current) => {
468
1487
  if (current.type !== "CallExpression") return;
469
1488
  const callee = current.callee;
470
1489
  if (callee?.type !== "MemberExpression") return;
471
1490
  const methodName = getPropertyName(callee.property);
472
1491
  if (!methodName || !SIGNAL_WRITE_METHODS.has(methodName)) return;
473
- if (!isKnownSignalObject(callee.object, signalVariables, classSignalProperties)) return;
1492
+ if (!isKnownSignalObject(context, callee.object, signalVariableBindings, classSignalProperties)) return;
474
1493
  context.report({
475
1494
  node: callee.property ?? callee,
476
1495
  messageId: "avoidSignalWriteInReactiveContext",
477
1496
  data: { contextName }
478
1497
  });
479
- });
1498
+ }, true);
480
1499
  },
481
- "Program:exit"() {
1500
+ after() {
482
1501
  effectNames.clear();
483
1502
  computedNames.clear();
484
1503
  linkedSignalNames.clear();
485
1504
  signalCreatorNames.clear();
486
1505
  angularNamespaces.clear();
487
- signalVariables.clear();
1506
+ signalVariableBindings.clear();
488
1507
  classSignalProperties.clear();
489
1508
  }
490
1509
  };
@@ -492,6 +1511,7 @@ const avoidWritingSignalsInReactiveContext = defineRule({
492
1511
  });
493
1512
  //#endregion
494
1513
  //#region src/rules/component-class-matches-filename/index.ts
1514
+ const COMPONENT_DECORATORS$1 = new Set(["Component"]);
495
1515
  function toPascalCase(raw) {
496
1516
  return raw.split(/[-_\s.]+/u).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
497
1517
  }
@@ -514,20 +1534,33 @@ const componentClassMatchesFilename = defineRule({
514
1534
  messages: { classNameMismatch: "Component class name should be '{{expectedName}}' to match the filename '{{filename}}'." }
515
1535
  },
516
1536
  createOnce(context) {
517
- const classDeclarations = [];
1537
+ const componentClasses = [];
1538
+ const decoratorImports = {
1539
+ decoratorNames: COMPONENT_DECORATORS$1,
1540
+ decoratorLocalNames: /* @__PURE__ */ new Set(),
1541
+ angularNamespaces: /* @__PURE__ */ new Set()
1542
+ };
518
1543
  return {
519
1544
  before() {
520
- classDeclarations.length = 0;
1545
+ componentClasses.length = 0;
1546
+ decoratorImports.decoratorLocalNames.clear();
1547
+ decoratorImports.angularNamespaces.clear();
1548
+ },
1549
+ ImportDeclaration(node) {
1550
+ if (node.source?.value !== "@angular/core") return;
1551
+ for (const specifier of node.specifiers ?? []) addAngularCoreDecoratorImport(specifier, COMPONENT_DECORATORS$1, decoratorImports);
521
1552
  },
522
1553
  ClassDeclaration(node) {
523
- classDeclarations.push(node);
1554
+ const classNode = node;
1555
+ if (!Array.isArray(classNode.decorators)) return;
1556
+ if (classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, decoratorImports))) componentClasses.push(classNode);
524
1557
  },
525
1558
  after() {
526
1559
  const filename = context.filename ?? "";
527
1560
  const expectedName = getExpectedComponentClassName(filename);
528
1561
  if (!expectedName) return;
529
1562
  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];
1563
+ const classNode = componentClasses.find((candidate) => candidate.parent?.type === "ExportNamedDeclaration" || candidate.parent?.type === "ExportDefaultDeclaration") ?? null ?? componentClasses[0];
531
1564
  if (!classNode?.id?.name) return;
532
1565
  if (classNode.id.name === expectedName) return;
533
1566
  context.report({
@@ -543,13 +1576,158 @@ const componentClassMatchesFilename = defineRule({
543
1576
  }
544
1577
  });
545
1578
  //#endregion
1579
+ //#region src/rules/component-resource-filenames/index.ts
1580
+ const COMPONENT_DECORATORS = new Set(["Component"]);
1581
+ const STYLE_EXTENSIONS = new Set([
1582
+ "css",
1583
+ "less",
1584
+ "sass",
1585
+ "scss"
1586
+ ]);
1587
+ function getExpectedStem(filename) {
1588
+ const base = filename.split(/[/\\]/u).at(-1) ?? "";
1589
+ if (!base.endsWith(".ts")) return null;
1590
+ return base.slice(0, -3);
1591
+ }
1592
+ function normalizeResourcePath(value) {
1593
+ return value.startsWith("./") ? value.slice(2) : value;
1594
+ }
1595
+ function getResourceBasename(value) {
1596
+ return normalizeResourcePath(value).split(/[/\\]/u).at(-1) ?? value;
1597
+ }
1598
+ function getExtension(value) {
1599
+ const basename = getResourceBasename(value);
1600
+ const index = basename.lastIndexOf(".");
1601
+ return index === -1 ? null : basename.slice(index + 1);
1602
+ }
1603
+ function getStaticString(node) {
1604
+ if (!node) return null;
1605
+ if (node.type === "Literal" || node.type === "StringLiteral") {
1606
+ if (typeof node.value !== "string") return null;
1607
+ return { value: node.value };
1608
+ }
1609
+ if (node.type === "TemplateLiteral" && node.expressions?.length === 0) return { value: node.quasis?.[0]?.value?.cooked ?? node.quasis?.[0]?.value?.raw ?? "" };
1610
+ return null;
1611
+ }
1612
+ function getComponentMetadata(context, node, decoratorImports) {
1613
+ if (node.type !== "Decorator") return null;
1614
+ if (!isAngularCoreDecorator(context, node, decoratorImports)) return null;
1615
+ const expression = node.expression;
1616
+ if (expression?.type !== "CallExpression") return null;
1617
+ const metadata = expression.arguments?.[0];
1618
+ return metadata?.type === "ObjectExpression" ? metadata : null;
1619
+ }
1620
+ const componentResourceFilenames = defineRule({
1621
+ meta: {
1622
+ type: "suggestion",
1623
+ docs: {
1624
+ description: "Require Angular component templateUrl and styleUrl resource filenames to match the component TypeScript filename.",
1625
+ recommended: true
1626
+ },
1627
+ schema: [],
1628
+ messages: {
1629
+ templateUrlMismatch: "Component templateUrl should be '{{expected}}' to match this component filename.",
1630
+ styleUrlMismatch: "Component style URL should be '{{expected}}' to match this component filename.",
1631
+ unsupportedStyleExtension: "Component style URL should use one of: .css, .less, .sass, or .scss."
1632
+ }
1633
+ },
1634
+ createOnce(context) {
1635
+ const decoratorImports = {
1636
+ decoratorNames: COMPONENT_DECORATORS,
1637
+ decoratorLocalNames: /* @__PURE__ */ new Set(),
1638
+ angularNamespaces: /* @__PURE__ */ new Set()
1639
+ };
1640
+ function reportTemplateUrl(valueNode) {
1641
+ const expectedStem = getExpectedStem(context.filename ?? "");
1642
+ if (!expectedStem) return;
1643
+ const staticString = getStaticString(valueNode);
1644
+ if (!staticString) return;
1645
+ const expected = `./${expectedStem}.html`;
1646
+ if (normalizeResourcePath(staticString.value) === `${expectedStem}.html`) return;
1647
+ context.report({
1648
+ node: valueNode,
1649
+ messageId: "templateUrlMismatch",
1650
+ data: { expected }
1651
+ });
1652
+ }
1653
+ function reportStyleUrl(valueNode) {
1654
+ const expectedStem = getExpectedStem(context.filename ?? "");
1655
+ if (!expectedStem) return;
1656
+ const staticString = getStaticString(valueNode);
1657
+ if (!staticString) return;
1658
+ const extension = getExtension(staticString.value);
1659
+ if (!extension || !STYLE_EXTENSIONS.has(extension)) {
1660
+ context.report({
1661
+ node: valueNode,
1662
+ messageId: "unsupportedStyleExtension"
1663
+ });
1664
+ return;
1665
+ }
1666
+ const expected = `./${expectedStem}.${extension}`;
1667
+ if (normalizeResourcePath(staticString.value) === `${expectedStem}.${extension}`) return;
1668
+ context.report({
1669
+ node: valueNode,
1670
+ messageId: "styleUrlMismatch",
1671
+ data: { expected }
1672
+ });
1673
+ }
1674
+ return {
1675
+ before() {
1676
+ decoratorImports.decoratorLocalNames.clear();
1677
+ decoratorImports.angularNamespaces.clear();
1678
+ },
1679
+ ImportDeclaration(node) {
1680
+ if (node.source?.value !== "@angular/core") return;
1681
+ for (const specifier of node.specifiers ?? []) addAngularCoreDecoratorImport(specifier, COMPONENT_DECORATORS, decoratorImports);
1682
+ },
1683
+ Decorator(node) {
1684
+ const metadata = getComponentMetadata(context, node, decoratorImports);
1685
+ if (!metadata) return;
1686
+ for (const property of metadata.properties ?? []) {
1687
+ if (property.type !== "Property" || property.computed) continue;
1688
+ const propertyName = getPropertyName(property.key);
1689
+ if (propertyName === "templateUrl") {
1690
+ reportTemplateUrl(property.value);
1691
+ continue;
1692
+ }
1693
+ if (propertyName === "styleUrl") {
1694
+ reportStyleUrl(property.value);
1695
+ continue;
1696
+ }
1697
+ }
1698
+ }
1699
+ };
1700
+ }
1701
+ });
1702
+ //#endregion
546
1703
  //#region src/rules/prefer-load-component-over-load-children/index.ts
547
- function isRoutesTypeAnnotation(node) {
1704
+ function isImportedTypeName(typeNode, localNames, routerNamespaces) {
1705
+ const typeName = getTypeName(typeNode);
1706
+ if (!typeName) return false;
1707
+ if (!typeName.includes(".")) return localNames.has(typeName);
1708
+ const [namespaceName, memberName] = typeName.split(".");
1709
+ return routerNamespaces.has(namespaceName) && localNames.has(memberName);
1710
+ }
1711
+ function getTypeParameterNodes(typeNode) {
1712
+ return typeNode.typeParameters?.params ?? typeNode.typeArguments?.params ?? [];
1713
+ }
1714
+ function isRouteType(typeNode, imports) {
1715
+ return typeNode?.type === "TSTypeReference" && isImportedTypeName(typeNode.typeName, imports.routeTypeLocalNames, imports.routerNamespaces);
1716
+ }
1717
+ function isRouteArrayType(typeNode, imports) {
1718
+ if (!typeNode) return false;
1719
+ if (typeNode.type === "TSTypeReference" && isImportedTypeName(typeNode.typeName, imports.routesTypeLocalNames, imports.routerNamespaces)) return true;
1720
+ if (typeNode.type === "TSArrayType") return isRouteType(typeNode.elementType, imports);
1721
+ if (typeNode.type !== "TSTypeReference") return false;
1722
+ const typeName = getTypeName(typeNode.typeName);
1723
+ if (typeName !== "Array" && typeName !== "ReadonlyArray") return false;
1724
+ const [elementType] = getTypeParameterNodes(typeNode);
1725
+ return isRouteType(elementType, imports);
1726
+ }
1727
+ function isRoutesTypeAnnotation(node, imports) {
548
1728
  const typeAnnotation = node?.typeAnnotation;
549
1729
  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";
1730
+ return isRouteArrayType(typeAnnotation.typeAnnotation, imports);
553
1731
  }
554
1732
  function isExportedConstDeclarator(node) {
555
1733
  if (node.type !== "VariableDeclarator") return false;
@@ -557,19 +1735,6 @@ function isExportedConstDeclarator(node) {
557
1735
  if (declaration?.type !== "VariableDeclaration" || declaration.kind !== "const") return false;
558
1736
  return declaration.parent?.type === "ExportNamedDeclaration";
559
1737
  }
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
1738
  const preferLoadComponentOverLoadChildren = defineRule({
574
1739
  meta: {
575
1740
  type: "problem",
@@ -581,21 +1746,58 @@ const preferLoadComponentOverLoadChildren = defineRule({
581
1746
  messages: { avoidLoadChildren: "Avoid loadChildren in Routes arrays; prefer loadComponent for lazy-loading standalone route components." }
582
1747
  },
583
1748
  createOnce(context) {
584
- return { VariableDeclarator(node) {
585
- const declarator = node;
586
- if (!isExportedConstDeclarator(declarator)) return;
587
- if (declarator.id?.type !== "Identifier") return;
588
- if (!isRoutesTypeAnnotation(declarator.id)) return;
589
- 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
- });
598
- } };
1749
+ const imports = {
1750
+ routeTypeLocalNames: /* @__PURE__ */ new Set(),
1751
+ routesTypeLocalNames: /* @__PURE__ */ new Set(),
1752
+ routerNamespaces: /* @__PURE__ */ new Set()
1753
+ };
1754
+ function reportLoadChildrenInRouteObject(routeObject) {
1755
+ for (const property of routeObject.properties ?? []) {
1756
+ if (property.type !== "Property" || property.computed) continue;
1757
+ const propertyName = getPropertyName(property.key);
1758
+ if (propertyName === "loadChildren") {
1759
+ context.report({
1760
+ node: property.key ?? property,
1761
+ messageId: "avoidLoadChildren"
1762
+ });
1763
+ continue;
1764
+ }
1765
+ if (propertyName === "children" && property.value?.type === "ArrayExpression") reportLoadChildrenInRouteArray(property.value);
1766
+ }
1767
+ }
1768
+ function reportLoadChildrenInRouteArray(routeArray) {
1769
+ for (const element of routeArray.elements ?? []) if (element?.type === "ObjectExpression") reportLoadChildrenInRouteObject(element);
1770
+ }
1771
+ return {
1772
+ before() {
1773
+ imports.routeTypeLocalNames.clear();
1774
+ imports.routesTypeLocalNames.clear();
1775
+ imports.routerNamespaces.clear();
1776
+ },
1777
+ ImportDeclaration(node) {
1778
+ if (node.source?.value !== "@angular/router") return;
1779
+ for (const specifier of node.specifiers ?? []) {
1780
+ if (specifier.type === "ImportSpecifier") {
1781
+ const importedName = getPropertyName(specifier.imported);
1782
+ if (importedName === "Route") imports.routeTypeLocalNames.add(specifier.local.name);
1783
+ if (importedName === "Routes") imports.routesTypeLocalNames.add(specifier.local.name);
1784
+ }
1785
+ if (specifier.type === "ImportNamespaceSpecifier") {
1786
+ imports.routerNamespaces.add(specifier.local.name);
1787
+ imports.routeTypeLocalNames.add("Route");
1788
+ imports.routesTypeLocalNames.add("Routes");
1789
+ }
1790
+ }
1791
+ },
1792
+ VariableDeclarator(node) {
1793
+ const declarator = node;
1794
+ if (!isExportedConstDeclarator(declarator)) return;
1795
+ if (declarator.id?.type !== "Identifier") return;
1796
+ if (!isRoutesTypeAnnotation(declarator.id, imports)) return;
1797
+ if (declarator.init?.type !== "ArrayExpression") return;
1798
+ reportLoadChildrenInRouteArray(declarator.init);
1799
+ }
1800
+ };
599
1801
  }
600
1802
  });
601
1803
  //#endregion
@@ -604,6 +1806,7 @@ function getPrivateTarget(element) {
604
1806
  if (element.type !== "PropertyDefinition" && element.type !== "FieldDefinition" && element.type !== "AccessorProperty" && element.type !== "MethodDefinition") return null;
605
1807
  if (element.accessibility !== "private") return null;
606
1808
  if (element.computed) return null;
1809
+ if (Array.isArray(element.decorators) && element.decorators.length > 0) return null;
607
1810
  if (element.type === "MethodDefinition" && element.kind === "constructor") return null;
608
1811
  const name = getPropertyName(element.key);
609
1812
  if (!name || element.key?.type !== "Identifier") return null;
@@ -757,10 +1960,10 @@ const preferPrivateElements = defineRule({
757
1960
  });
758
1961
  //#endregion
759
1962
  //#region src/rules/prefer-style-url/index.ts
760
- function isComponentDecoratorCall(node) {
1963
+ function isComponentDecoratorCall(context, node, componentLocalNames, angularNamespaces) {
761
1964
  const callee = node.callee;
762
- if (callee?.type === "Identifier") return callee.name === "Component";
763
- return callee?.type === "MemberExpression" && getPropertyName(callee.property) === "Component";
1965
+ if (callee?.type === "Identifier") return componentLocalNames.has(callee.name) && !isShadowedIdentifier(context, callee);
1966
+ return callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && angularNamespaces.has(callee.object.name) && !isShadowedIdentifier(context, callee.object) && getPropertyName(callee.property) === "Component";
764
1967
  }
765
1968
  function isSingleStyleFileNode(node) {
766
1969
  if (!node) return false;
@@ -779,38 +1982,203 @@ const preferStyleUrl = defineRule({
779
1982
  messages: { preferStyleUrl: "Use styleUrl instead of styleUrls when a component has one style file." }
780
1983
  },
781
1984
  createOnce(context) {
782
- return { CallExpression(node) {
783
- const call = node;
784
- if (!isComponentDecoratorCall(call)) return;
785
- const metadata = call.arguments?.[0];
786
- if (metadata?.type !== "ObjectExpression") return;
787
- for (const property of metadata.properties ?? []) {
788
- if (property.type !== "Property") continue;
789
- if (property.computed) continue;
790
- if (getPropertyName(property.key) !== "styleUrls") continue;
791
- if (property.value?.type !== "ArrayExpression") continue;
792
- const elements = property.value.elements?.filter(Boolean) ?? [];
793
- if (elements.length !== 1) continue;
794
- const styleFile = elements[0];
795
- if (!isSingleStyleFileNode(styleFile)) continue;
796
- const keyRange = getRange(property.key);
797
- const valueRange = getRange(property.value);
798
- const styleFileRange = getRange(styleFile);
799
- context.report({
800
- node: property.key,
801
- messageId: "preferStyleUrl",
802
- fix: keyRange && valueRange && styleFileRange ? (fixer) => [fixer.replaceTextRange(keyRange, "styleUrl"), fixer.replaceTextRange(valueRange, context.sourceCode.text.slice(styleFileRange[0], styleFileRange[1]))] : void 0
803
- });
1985
+ const componentLocalNames = /* @__PURE__ */ new Set();
1986
+ const angularNamespaces = /* @__PURE__ */ new Set();
1987
+ return {
1988
+ before() {
1989
+ componentLocalNames.clear();
1990
+ angularNamespaces.clear();
1991
+ },
1992
+ ImportDeclaration(node) {
1993
+ if (node.source?.value !== "@angular/core") return;
1994
+ for (const specifier of node.specifiers ?? []) {
1995
+ if (specifier.type === "ImportSpecifier") {
1996
+ if (getPropertyName(specifier.imported) === "Component") componentLocalNames.add(specifier.local.name);
1997
+ }
1998
+ if (specifier.type === "ImportNamespaceSpecifier") angularNamespaces.add(specifier.local.name);
1999
+ }
2000
+ },
2001
+ CallExpression(node) {
2002
+ const call = node;
2003
+ if (!isComponentDecoratorCall(context, call, componentLocalNames, angularNamespaces)) return;
2004
+ const metadata = call.arguments?.[0];
2005
+ if (metadata?.type !== "ObjectExpression") return;
2006
+ for (const property of metadata.properties ?? []) {
2007
+ if (property.type !== "Property") continue;
2008
+ if (property.computed) continue;
2009
+ if (getPropertyName(property.key) !== "styleUrls") continue;
2010
+ if (property.value?.type !== "ArrayExpression") continue;
2011
+ const elements = property.value.elements?.filter(Boolean) ?? [];
2012
+ if (elements.length !== 1) continue;
2013
+ const styleFile = elements[0];
2014
+ if (!isSingleStyleFileNode(styleFile)) continue;
2015
+ const keyRange = getRange(property.key);
2016
+ const valueRange = getRange(property.value);
2017
+ const styleFileRange = getRange(styleFile);
2018
+ context.report({
2019
+ node: property.key,
2020
+ messageId: "preferStyleUrl",
2021
+ fix: keyRange && valueRange && styleFileRange ? (fixer) => [fixer.replaceTextRange(keyRange, "styleUrl"), fixer.replaceTextRange(valueRange, context.sourceCode.text.slice(styleFileRange[0], styleFileRange[1]))] : void 0
2022
+ });
2023
+ }
804
2024
  }
805
- } };
2025
+ };
2026
+ }
2027
+ });
2028
+ //#endregion
2029
+ //#region src/rules/public-component-interface/index.ts
2030
+ const TARGET_DECORATORS = new Set(["Component", "Directive"]);
2031
+ const INPUT_MODEL_APIS = new Set(["input", "model"]);
2032
+ const OUTPUT_APIS = new Set(["output", "outputFromObservable"]);
2033
+ const INJECT_APIS = new Set(["inject"]);
2034
+ const FIELD_NODE_TYPES = new Set([
2035
+ "AccessorProperty",
2036
+ "FieldDefinition",
2037
+ "PropertyDefinition"
2038
+ ]);
2039
+ function hasTargetDecorator(context, classNode, decoratorImports) {
2040
+ if (!classNode || !Array.isArray(classNode.decorators)) return false;
2041
+ return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, decoratorImports));
2042
+ }
2043
+ function isApiCallFromTrackedImports(context, node, importedApiLocalNames, angularNamespaces, apiNames) {
2044
+ if (!node || node.type !== "CallExpression") return false;
2045
+ const callee = node.callee;
2046
+ if (callee?.type === "Identifier") return importedApiLocalNames.has(callee.name) && !isShadowedIdentifier(context, callee);
2047
+ if (callee?.type !== "MemberExpression") return false;
2048
+ const supportsRequiredApi = [...INPUT_MODEL_APIS].some((name) => apiNames.has(name));
2049
+ if (getPropertyName(callee.property) === "required" && callee.object?.type === "Identifier") return supportsRequiredApi && importedApiLocalNames.has(callee.object.name) && !isShadowedIdentifier(context, callee.object);
2050
+ if (callee.object?.type === "Identifier" && angularNamespaces.has(callee.object.name)) {
2051
+ const namespaceApiName = getPropertyName(callee.property);
2052
+ return !!namespaceApiName && apiNames.has(namespaceApiName) && !isShadowedIdentifier(context, callee.object);
2053
+ }
2054
+ if (getPropertyName(callee.property) === "required" && callee.object?.type === "MemberExpression" && callee.object.object?.type === "Identifier" && angularNamespaces.has(callee.object.object.name)) {
2055
+ const namespaceApiName = getPropertyName(callee.object.property);
2056
+ return supportsRequiredApi && !!namespaceApiName && apiNames.has(namespaceApiName) && !isShadowedIdentifier(context, callee.object.object);
2057
+ }
2058
+ return false;
2059
+ }
2060
+ function isNonPublicMember(memberNode) {
2061
+ if (memberNode.key?.type === "PrivateIdentifier") return true;
2062
+ return memberNode.accessibility === "private" || memberNode.accessibility === "protected";
2063
+ }
2064
+ function isPublicMember(memberNode) {
2065
+ if (memberNode.key?.type === "PrivateIdentifier") return false;
2066
+ return memberNode.accessibility !== "private" && memberNode.accessibility !== "protected";
2067
+ }
2068
+ function getAccessibilityModifierRange(context, memberNode) {
2069
+ const memberRange = getRange(memberNode);
2070
+ const keyRange = getRange(memberNode.key);
2071
+ if (!memberRange || !keyRange) return null;
2072
+ const prefixText = context.sourceCode.text.slice(memberRange[0], keyRange[0]);
2073
+ const match = /\b(private|protected|public)\b/u.exec(prefixText);
2074
+ if (!match) return null;
2075
+ return [memberRange[0] + match.index, memberRange[0] + match.index + match[0].length];
2076
+ }
2077
+ function getVisibilityFix(context, memberNode, targetVisibility) {
2078
+ if (memberNode.key?.type === "PrivateIdentifier") return void 0;
2079
+ const keyRange = getRange(memberNode.key);
2080
+ if (!keyRange) return void 0;
2081
+ const accessibilityRange = getAccessibilityModifierRange(context, memberNode);
2082
+ if (accessibilityRange) return (fixer) => fixer.replaceTextRange(accessibilityRange, targetVisibility);
2083
+ return (fixer) => fixer.insertTextBeforeRange([keyRange[0], keyRange[0]], `${targetVisibility} `);
2084
+ }
2085
+ const publicComponentInterface = defineRule({
2086
+ meta: {
2087
+ type: "problem",
2088
+ docs: {
2089
+ description: "Require component/directive signal interface members (input/model/output APIs) to be public, and it's dependencies to be non-public.",
2090
+ recommended: true
2091
+ },
2092
+ fixable: "code",
2093
+ schema: [],
2094
+ messages: {
2095
+ nonPublicInputModel: "Input/model member {{name}} must be public so the component/directive interface is externally accessible.",
2096
+ nonPublicOutput: "Output member {{name}} must be public so the component/directive interface is externally accessible.",
2097
+ publicInjectMember: "Injected member {{name}} should not be public; prefer protected (template access) or private/#private."
2098
+ }
2099
+ },
2100
+ createOnce(context) {
2101
+ const inputModelLocalNames = /* @__PURE__ */ new Set();
2102
+ const outputLocalNames = /* @__PURE__ */ new Set();
2103
+ const injectLocalNames = /* @__PURE__ */ new Set();
2104
+ const angularNamespaces = /* @__PURE__ */ new Set();
2105
+ const decoratorImports = {
2106
+ decoratorNames: TARGET_DECORATORS,
2107
+ decoratorLocalNames: /* @__PURE__ */ new Set(),
2108
+ angularNamespaces
2109
+ };
2110
+ return {
2111
+ before() {
2112
+ inputModelLocalNames.clear();
2113
+ outputLocalNames.clear();
2114
+ injectLocalNames.clear();
2115
+ angularNamespaces.clear();
2116
+ decoratorImports.decoratorLocalNames.clear();
2117
+ },
2118
+ ImportDeclaration(node) {
2119
+ if (node.source?.value !== "@angular/core") return;
2120
+ for (const specifier of node.specifiers ?? []) {
2121
+ addAngularCoreDecoratorImport(specifier, TARGET_DECORATORS, decoratorImports);
2122
+ if (specifier.type === "ImportSpecifier") {
2123
+ const importedName = getPropertyName(specifier.imported);
2124
+ if (!importedName) continue;
2125
+ if (INPUT_MODEL_APIS.has(importedName)) inputModelLocalNames.add(specifier.local.name);
2126
+ if (OUTPUT_APIS.has(importedName)) outputLocalNames.add(specifier.local.name);
2127
+ if (INJECT_APIS.has(importedName)) injectLocalNames.add(specifier.local.name);
2128
+ continue;
2129
+ }
2130
+ if (specifier.type === "ImportNamespaceSpecifier") angularNamespaces.add(specifier.local.name);
2131
+ }
2132
+ },
2133
+ ClassBody(node) {
2134
+ const classNode = node.parent;
2135
+ if (!hasTargetDecorator(context, classNode, decoratorImports)) return;
2136
+ for (const member of node.body ?? []) {
2137
+ if (!FIELD_NODE_TYPES.has(member.type)) continue;
2138
+ const isInputModelMember = isApiCallFromTrackedImports(context, member.value, inputModelLocalNames, angularNamespaces, INPUT_MODEL_APIS);
2139
+ const isOutputMember = isApiCallFromTrackedImports(context, member.value, outputLocalNames, angularNamespaces, OUTPUT_APIS);
2140
+ if (isInputModelMember && isNonPublicMember(member)) {
2141
+ context.report({
2142
+ node: member.key ?? member,
2143
+ messageId: "nonPublicInputModel",
2144
+ data: { name: getPropertyName(member.key) ?? "member" },
2145
+ fix: getVisibilityFix(context, member, "public")
2146
+ });
2147
+ continue;
2148
+ }
2149
+ if (isOutputMember && isNonPublicMember(member)) {
2150
+ context.report({
2151
+ node: member.key ?? member,
2152
+ messageId: "nonPublicOutput",
2153
+ data: { name: getPropertyName(member.key) ?? "member" },
2154
+ fix: getVisibilityFix(context, member, "public")
2155
+ });
2156
+ continue;
2157
+ }
2158
+ if (isPublicMember(member) && isApiCallFromTrackedImports(context, member.value, injectLocalNames, angularNamespaces, INJECT_APIS)) context.report({
2159
+ node: member.key ?? member,
2160
+ messageId: "publicInjectMember",
2161
+ data: { name: getPropertyName(member.key) ?? "member" },
2162
+ fix: getVisibilityFix(context, member, "protected")
2163
+ });
2164
+ }
2165
+ },
2166
+ "Program:exit"() {
2167
+ inputModelLocalNames.clear();
2168
+ outputLocalNames.clear();
2169
+ injectLocalNames.clear();
2170
+ angularNamespaces.clear();
2171
+ }
2172
+ };
806
2173
  }
807
2174
  });
808
2175
  //#endregion
809
2176
  //#region src/rules/restrict-injectable-provided-in/index.ts
810
2177
  const ALLOWED_PROVIDED_IN_VALUES = new Set(["root", "platform"]);
811
- function getInjectableMetadata(node) {
2178
+ const INJECTABLE_DECORATORS = new Set(["Injectable"]);
2179
+ function getInjectableMetadata(context, node, decoratorImports) {
812
2180
  if (node.type !== "Decorator") return null;
813
- if (getDecoratorName(node) !== "Injectable") return null;
2181
+ if (!isAngularCoreDecorator(context, node, decoratorImports)) return null;
814
2182
  const expression = node.expression;
815
2183
  if (expression?.type !== "CallExpression") return null;
816
2184
  const metadata = expression.arguments?.[0];
@@ -832,23 +2200,38 @@ const restrictInjectableProvidedIn = defineRule({
832
2200
  recommended: true
833
2201
  },
834
2202
  schema: [],
835
- messages: { disallowedProvidedIn: "@Injectable providedIn should be 'root' or 'platform', not {{actual}}." }
2203
+ messages: { disallowedProvidedIn: "@Injectable providedIn should be the literal 'root' or 'platform', not {{actual}}." }
836
2204
  },
837
2205
  createOnce(context) {
838
- return { Decorator(node) {
839
- const metadata = getInjectableMetadata(node);
840
- if (!metadata) return;
841
- const providedInProperty = getProvidedInProperty(metadata);
842
- if (!providedInProperty) return;
843
- const value = providedInProperty.value;
844
- if (isAllowedProvidedInValue(value)) return;
845
- const actual = context.sourceCode.text.slice(value?.range?.[0] ?? providedInProperty.range?.[0] ?? 0, value?.range?.[1] ?? providedInProperty.range?.[1] ?? 0);
846
- context.report({
847
- node: value ?? providedInProperty,
848
- messageId: "disallowedProvidedIn",
849
- data: { actual: actual || "this value" }
850
- });
851
- } };
2206
+ const decoratorImports = {
2207
+ decoratorNames: INJECTABLE_DECORATORS,
2208
+ decoratorLocalNames: /* @__PURE__ */ new Set(),
2209
+ angularNamespaces: /* @__PURE__ */ new Set()
2210
+ };
2211
+ return {
2212
+ before() {
2213
+ decoratorImports.decoratorLocalNames.clear();
2214
+ decoratorImports.angularNamespaces.clear();
2215
+ },
2216
+ ImportDeclaration(node) {
2217
+ if (node.source?.value !== "@angular/core") return;
2218
+ for (const specifier of node.specifiers ?? []) addAngularCoreDecoratorImport(specifier, INJECTABLE_DECORATORS, decoratorImports);
2219
+ },
2220
+ Decorator(node) {
2221
+ const metadata = getInjectableMetadata(context, node, decoratorImports);
2222
+ if (!metadata) return;
2223
+ const providedInProperty = getProvidedInProperty(metadata);
2224
+ if (!providedInProperty) return;
2225
+ const value = providedInProperty.value;
2226
+ if (isAllowedProvidedInValue(value)) return;
2227
+ const actual = context.sourceCode.text.slice(value?.range?.[0] ?? providedInProperty.range?.[0] ?? 0, value?.range?.[1] ?? providedInProperty.range?.[1] ?? 0);
2228
+ context.report({
2229
+ node: value ?? providedInProperty,
2230
+ messageId: "disallowedProvidedIn",
2231
+ data: { actual: actual || "this value" }
2232
+ });
2233
+ }
2234
+ };
852
2235
  }
853
2236
  });
854
2237
  //#endregion
@@ -878,6 +2261,37 @@ const ANGULAR_CLASS_DECORATOR_NAMES = new Set([
878
2261
  "NgModule"
879
2262
  ]);
880
2263
  const INJECTION_CONTEXT_RUNNER_NAMES = new Set(["runInInjectionContext", "runInContext"]);
2264
+ const KNOWN_INJECTION_CONTEXT_API_IMPORTS = [
2265
+ {
2266
+ from: "@angular/core",
2267
+ imports: [
2268
+ "afterEveryRender",
2269
+ "afterNextRender",
2270
+ "afterRender",
2271
+ "afterRenderEffect",
2272
+ "assertInInjectionContext",
2273
+ "effect",
2274
+ "inject",
2275
+ "resource"
2276
+ ]
2277
+ },
2278
+ {
2279
+ from: "@angular/core/rxjs-interop",
2280
+ imports: [
2281
+ "rxResource",
2282
+ "toObservable",
2283
+ "toSignal"
2284
+ ]
2285
+ },
2286
+ {
2287
+ from: "@angular/common/http",
2288
+ imports: ["httpResource"]
2289
+ },
2290
+ {
2291
+ from: "@angular/forms/signals",
2292
+ imports: ["form"]
2293
+ }
2294
+ ];
881
2295
  const INJECTION_CONTEXT_FUNCTION_TYPE_NAMES = new Set([
882
2296
  "CanActivateFn",
883
2297
  "CanActivateChildFn",
@@ -898,17 +2312,6 @@ const CLASS_FIELD_TYPES = new Set([
898
2312
  "FieldDefinition",
899
2313
  "PropertyDefinition"
900
2314
  ]);
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
2315
  function isFunction(node) {
913
2316
  return !!node && FUNCTION_TYPES.has(node.type);
914
2317
  }
@@ -975,11 +2378,6 @@ function isTypedInjectionContextFunction(functionNode) {
975
2378
  const unqualifiedTypeName = typeName.includes(".") ? typeName.split(".").at(-1) : typeName;
976
2379
  return INJECTION_CONTEXT_FUNCTION_TYPE_NAMES.has(unqualifiedTypeName ?? typeName);
977
2380
  }
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
2381
  function hasAwaitBeforeNodeInFunction(functionNode, node) {
984
2382
  const nodeStart = getNodeStart(node);
985
2383
  if (nodeStart === null) return false;
@@ -1027,16 +2425,27 @@ function isAllowedInjectionContext(context, node, allowedFunctionNames, injectFu
1027
2425
  const functionName = getFunctionName(nearestFunction);
1028
2426
  return functionName ? allowedFunctionNames.has(functionName) || injectFunctionPrefixes.some((prefix) => functionName.startsWith(prefix)) || injectFunctionSuffixes.some((suffix) => functionName.endsWith(suffix)) : false;
1029
2427
  }
1030
- function isAngularInjectCall(node, injectNames, angularNamespaces, checkUnimportedInject) {
2428
+ function getKnownInjectionContextApiImports(source) {
2429
+ const apiImports = KNOWN_INJECTION_CONTEXT_API_IMPORTS.find((entry) => entry.from === source);
2430
+ return apiImports ? new Set(apiImports.imports) : null;
2431
+ }
2432
+ function isKnownInjectionContextApiCall(context, node, injectionContextApiLocalNames, injectionContextApiNamespaceMembers, checkUnimportedInject) {
1031
2433
  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";
2434
+ if (callee?.type === "Identifier") return injectionContextApiLocalNames.has(callee.name) && !isShadowedIdentifier(context, callee) || checkUnimportedInject && callee.name === "inject" && !isShadowedIdentifier(context, callee);
2435
+ if (callee?.type !== "MemberExpression") return false;
2436
+ if (callee.object?.type === "Identifier") {
2437
+ if (injectionContextApiLocalNames.has(callee.object.name) && !isShadowedIdentifier(context, callee.object)) return true;
2438
+ return !!injectionContextApiNamespaceMembers.get(callee.object.name)?.has(getPropertyName(callee.property) ?? "") && !isShadowedIdentifier(context, callee.object);
2439
+ }
2440
+ 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);
2441
+ return false;
1034
2442
  }
1035
- function isInjectLikeHelperCall(node, injectNames, injectFunctionPrefixes, injectFunctionSuffixes, runsInInjectionContextFunctionNames) {
2443
+ function isInjectLikeHelperCall(context, node, injectionContextApiLocalNames, injectFunctionPrefixes, injectFunctionSuffixes, runsInInjectionContextFunctionNames) {
1036
2444
  const callee = node.callee;
1037
2445
  if (callee?.type !== "Identifier") return false;
2446
+ if (isShadowedIdentifier(context, callee)) return false;
1038
2447
  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)));
2448
+ return !injectionContextApiLocalNames.has(callee.name) && callee.name !== "inject" && (injectFunctionPrefixes.some((prefix) => callee.name.startsWith(prefix)) || injectFunctionSuffixes.some((suffix) => callee.name.endsWith(suffix)));
1040
2449
  }
1041
2450
  //#endregion
1042
2451
  //#region src/index.ts
@@ -1044,19 +2453,23 @@ const plugin = eslintCompatPlugin({
1044
2453
  meta: { name: "@benjavicente/lint-angular" },
1045
2454
  rules: {
1046
2455
  "avoid-explicit-injection-context": avoidExplicitInjectionContext,
2456
+ "avoid-explicit-subscription-management": avoidExplicitSubscriptionManagement,
1047
2457
  "avoid-ng-modules": avoidNgModules,
2458
+ "avoid-rxjs-state-in-component": avoidRxjsStateInComponent,
1048
2459
  "avoid-writing-signals-in-reactive-context": avoidWritingSignalsInReactiveContext,
1049
2460
  "class-member-order": classMemberOrder,
1050
2461
  "component-class-matches-filename": componentClassMatchesFilename,
2462
+ "component-resource-filenames": componentResourceFilenames,
1051
2463
  "prefer-load-component-over-load-children": preferLoadComponentOverLoadChildren,
1052
2464
  "prefer-private-elements": preferPrivateElements,
1053
2465
  "prefer-style-url": preferStyleUrl,
2466
+ "public-component-interface": publicComponentInterface,
1054
2467
  "restrict-injectable-provided-in": restrictInjectableProvidedIn,
1055
2468
  "rules-of-inject": defineRule({
1056
2469
  meta: {
1057
2470
  type: "problem",
1058
2471
  docs: {
1059
- description: "Require Angular inject() calls to appear only in known injection contexts.",
2472
+ description: "Require Angular APIs that depend on injection context to appear only in known injection contexts.",
1060
2473
  recommended: true
1061
2474
  },
1062
2475
  schema: [{
@@ -1100,17 +2513,17 @@ const plugin = eslintCompatPlugin({
1100
2513
  }
1101
2514
  }
1102
2515
  }],
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." }
2516
+ 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
2517
  },
1105
2518
  createOnce(context) {
1106
- const injectNames = /* @__PURE__ */ new Set();
1107
- const angularNamespaces = /* @__PURE__ */ new Set();
2519
+ const injectionContextApiLocalNames = /* @__PURE__ */ new Set();
2520
+ const injectionContextApiNamespaceMembers = /* @__PURE__ */ new Map();
1108
2521
  const runsInInjectionContextFunctionNames = /* @__PURE__ */ new Set();
1109
2522
  let runsInInjectionContextRules = [];
1110
2523
  return {
1111
2524
  before() {
1112
- injectNames.clear();
1113
- angularNamespaces.clear();
2525
+ injectionContextApiLocalNames.clear();
2526
+ injectionContextApiNamespaceMembers.clear();
1114
2527
  runsInInjectionContextFunctionNames.clear();
1115
2528
  runsInInjectionContextRules = (context.options[0] ?? {}).runsInInjectionContext ?? DEFAULT_RUNS_IN_INJECTION_CONTEXT;
1116
2529
  },
@@ -1121,10 +2534,10 @@ const plugin = eslintCompatPlugin({
1121
2534
  if (specifier.type === "ImportSpecifier" && (matchingRule.imports === "all" || matchingRule.imports.includes(getPropertyName(specifier.imported) ?? ""))) runsInInjectionContextFunctionNames.add(specifier.local.name);
1122
2535
  if ((specifier.type === "ImportDefaultSpecifier" || specifier.type === "ImportNamespaceSpecifier") && matchingRule.imports === "all") runsInInjectionContextFunctionNames.add(specifier.local.name);
1123
2536
  }
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);
2537
+ const knownApiImports = typeof source === "string" ? getKnownInjectionContextApiImports(source) : null;
2538
+ if (knownApiImports) for (const specifier of node.specifiers ?? []) {
2539
+ if (specifier.type === "ImportSpecifier" && knownApiImports.has(getPropertyName(specifier.imported) ?? "")) injectionContextApiLocalNames.add(specifier.local.name);
2540
+ if (specifier.type === "ImportNamespaceSpecifier") injectionContextApiNamespaceMembers.set(specifier.local.name, knownApiImports);
1128
2541
  }
1129
2542
  },
1130
2543
  CallExpression(node) {
@@ -1134,14 +2547,14 @@ const plugin = eslintCompatPlugin({
1134
2547
  const injectFunctionPrefixes = options.injectFunctionPrefixes ?? DEFAULT_INJECT_FUNCTION_PREFIXES;
1135
2548
  const injectFunctionSuffixes = options.injectFunctionSuffixes ?? DEFAULT_INJECT_FUNCTION_SUFFIXES;
1136
2549
  const inAllowedContext = isAllowedInjectionContext(context, node, allowedFunctionNames, injectFunctionPrefixes, injectFunctionSuffixes);
1137
- if (isInjectLikeHelperCall(node, injectNames, injectFunctionPrefixes, injectFunctionSuffixes, runsInInjectionContextFunctionNames) && !inAllowedContext) {
2550
+ if (isInjectLikeHelperCall(context, node, injectionContextApiLocalNames, injectFunctionPrefixes, injectFunctionSuffixes, runsInInjectionContextFunctionNames) && !inAllowedContext) {
1138
2551
  context.report({
1139
2552
  node: node.callee,
1140
2553
  messageId: "disallowedInject"
1141
2554
  });
1142
2555
  return;
1143
2556
  }
1144
- if (!isAngularInjectCall(node, injectNames, angularNamespaces, checkUnimportedInject)) return;
2557
+ if (!isKnownInjectionContextApiCall(context, node, injectionContextApiLocalNames, injectionContextApiNamespaceMembers, checkUnimportedInject)) return;
1145
2558
  if (inAllowedContext) return;
1146
2559
  context.report({
1147
2560
  node: node.callee,