@benjavicente/lint-angular 0.0.2 → 0.0.4

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 +23 -17
  2. package/dist/index.mjs +776 -656
  3. package/package.json +5 -5
package/dist/index.mjs CHANGED
@@ -55,13 +55,13 @@ function unwrapExpression(node) {
55
55
  }
56
56
  //#endregion
57
57
  //#region src/utilities/scope.ts
58
- const FUNCTION_TYPES$1 = new Set([
58
+ const FUNCTION_TYPES$2 = new Set([
59
59
  "ArrowFunctionExpression",
60
60
  "FunctionDeclaration",
61
61
  "FunctionExpression"
62
62
  ]);
63
63
  function isFunction$1(node) {
64
- return !!node && FUNCTION_TYPES$1.has(node.type);
64
+ return !!node && FUNCTION_TYPES$2.has(node.type);
65
65
  }
66
66
  function addBindingIdentifierNodes(node, identifiers) {
67
67
  if (!node) return;
@@ -171,19 +171,45 @@ function isShadowedIdentifier(context, node) {
171
171
  }
172
172
  //#endregion
173
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);
174
+ function getProgramNode(context, node) {
175
+ if (node.type === "Program") return node;
176
+ return context.sourceCode.getAncestors(node).find((ancestor) => ancestor.type === "Program") ?? null;
177
+ }
178
+ function getImportedName(context, node, source) {
179
+ if (node?.type !== "Identifier") return null;
180
+ if (isShadowedIdentifier(context, node)) return null;
181
+ const program = getProgramNode(context, node);
182
+ for (const statement of program?.body ?? []) {
183
+ if (statement.type !== "ImportDeclaration" || statement.source?.value !== source) continue;
184
+ for (const specifier of statement.specifiers ?? []) {
185
+ if (specifier.type !== "ImportSpecifier" || specifier.local.name !== node.name) continue;
186
+ return getPropertyName(specifier.imported);
187
+ }
188
+ }
189
+ return null;
190
+ }
191
+ function isNamespaceImport(context, node, source) {
192
+ if (node?.type !== "Identifier") return false;
193
+ if (isShadowedIdentifier(context, node)) return false;
194
+ const program = getProgramNode(context, node);
195
+ for (const statement of program?.body ?? []) {
196
+ if (statement.type !== "ImportDeclaration" || statement.source?.value !== source) continue;
197
+ if ((statement.specifiers ?? []).some((specifier) => specifier.type === "ImportNamespaceSpecifier" && specifier.local.name === node.name)) return true;
178
198
  }
179
- if (specifier.type === "ImportNamespaceSpecifier") imports.angularNamespaces.add(specifier.local.name);
199
+ return false;
200
+ }
201
+ function isImportedReference(context, node, source, importedNames) {
202
+ const importedName = getImportedName(context, node, source);
203
+ return !!importedName && importedNames.has(importedName);
204
+ }
205
+ function isImportedNamespaceMember(context, node, source, memberNames) {
206
+ return node?.type === "MemberExpression" && node.object?.type === "Identifier" && isNamespaceImport(context, node.object, source) && memberNames.has(getPropertyName(node.property) ?? "");
180
207
  }
181
- function isAngularCoreDecorator(context, decorator, imports) {
208
+ function isAngularCoreDecorator(context, decorator, decoratorNames) {
182
209
  if (!decorator) return false;
183
210
  const expression = decorator.expression ?? decorator;
184
211
  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) ?? "");
212
+ return isImportedReference(context, callee, "@angular/core", decoratorNames) || isImportedNamespaceMember(context, callee, "@angular/core", decoratorNames);
187
213
  }
188
214
  //#endregion
189
215
  //#region src/rules/class-member-order/index.ts
@@ -196,7 +222,7 @@ const ANGULAR_CLASS_DECORATOR_NAMES$1 = new Set([
196
222
  ]);
197
223
  const INPUT_MODEL_CALL_NAMES = new Set(["input", "model"]);
198
224
  const OUTPUT_CALL_NAMES = new Set(["output", "outputFromObservable"]);
199
- const CLASS_FIELD_TYPES$1 = new Set([
225
+ const CLASS_FIELD_TYPES$2 = new Set([
200
226
  "AccessorProperty",
201
227
  "FieldDefinition",
202
228
  "PropertyDefinition"
@@ -207,38 +233,33 @@ const ORDER_LABELS = [
207
233
  "outputs",
208
234
  "everything else"
209
235
  ];
210
- function hasAngularClassDecorator(context, classNode, imports) {
236
+ function hasAngularClassDecorator(context, classNode) {
211
237
  if (!classNode || !Array.isArray(classNode.decorators)) return false;
212
- return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, imports));
238
+ return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, ANGULAR_CLASS_DECORATOR_NAMES$1));
213
239
  }
214
- function isApiCall(context, node, localNames, angularNamespaces, apiNames) {
240
+ function isApiCall(context, node, apiNames) {
215
241
  if (!node || node.type !== "CallExpression") return false;
216
242
  const callee = node.callee;
217
- if (callee?.type === "Identifier") return localNames.has(callee.name) && !isShadowedIdentifier(context, callee);
243
+ if (isImportedReference(context, callee, "@angular/core", apiNames)) return true;
218
244
  if (callee?.type !== "MemberExpression") return false;
219
245
  const supportsRequiredApi = [...INPUT_MODEL_CALL_NAMES].some((name) => apiNames.has(name));
220
246
  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) ?? "");
247
+ if (getPropertyName(callee.property) === "required") return supportsRequiredApi && isImportedReference(context, callee.object, "@angular/core", apiNames);
248
+ return isImportedNamespaceMember(context, callee, "@angular/core", apiNames);
223
249
  }
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) ?? "");
250
+ if (getPropertyName(callee.property) === "required" && callee.object?.type === "MemberExpression" && callee.object.object?.type === "Identifier") return supportsRequiredApi && isImportedNamespaceMember(context, callee.object, "@angular/core", apiNames) && apiNames.has(getPropertyName(callee.object.property) ?? "");
225
251
  return false;
226
252
  }
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;
234
- }
235
- function classifyMember(context, element, imports) {
236
- if (CLASS_FIELD_TYPES$1.has(element.type)) {
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;
253
+ function hasDecorator(context, element, decoratorNames) {
254
+ return Array.isArray(element.decorators) ? element.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, decoratorNames)) : false;
255
+ }
256
+ function classifyMember(context, element) {
257
+ if (CLASS_FIELD_TYPES$2.has(element.type)) {
258
+ if (isApiCall(context, element.value, new Set(["inject"]))) return 0;
259
+ if (isApiCall(context, element.value, INPUT_MODEL_CALL_NAMES)) return 1;
260
+ if (hasDecorator(context, element, new Set(["Input"]))) return 1;
261
+ if (isApiCall(context, element.value, OUTPUT_CALL_NAMES)) return 2;
262
+ if (hasDecorator(context, element, new Set(["Output"]))) return 2;
242
263
  return 3;
243
264
  }
244
265
  if (element.type === "MethodDefinition") return 3;
@@ -314,7 +335,7 @@ function getSortedMembersFix(context, classBody, classifiedMembers) {
314
335
  for (const member of classifiedMembers) if (member.name) membersByName.set(member.name, member);
315
336
  for (const member of classifiedMembers) {
316
337
  const { element } = member;
317
- if (!CLASS_FIELD_TYPES$1.has(element.type)) return void 0;
338
+ if (!CLASS_FIELD_TYPES$2.has(element.type)) return void 0;
318
339
  if (element.type === "AccessorProperty") return void 0;
319
340
  if (element.computed) return void 0;
320
341
  if (Array.isArray(element.decorators) && element.decorators.length > 0) return void 0;
@@ -354,83 +375,47 @@ const classMemberOrder = defineRule({
354
375
  messages: { outOfOrder: "Angular class member should be ordered before {{previousGroup}} and with {{expectedGroup}}." }
355
376
  },
356
377
  createOnce(context) {
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({
378
+ return { ClassBody(node) {
379
+ const classBody = node;
380
+ if (!hasAngularClassDecorator(context, classBody.parent)) return;
381
+ let highestSeen = null;
382
+ const classifiedMembers = [];
383
+ const outOfOrderReports = [];
384
+ for (const element of classBody.body ?? []) {
385
+ const group = classifyMember(context, element);
386
+ if (group === null) continue;
387
+ classifiedMembers.push({
388
+ element,
389
+ group,
390
+ effectiveGroup: group,
391
+ name: getMemberName$1(element.key),
392
+ dependencies: collectThisMemberReferences(element.value)
393
+ });
394
+ }
395
+ applyDependencyGroups(classifiedMembers);
396
+ for (const { element, effectiveGroup } of classifiedMembers) {
397
+ if (highestSeen !== null && effectiveGroup < highestSeen) {
398
+ outOfOrderReports.push({
402
399
  element,
403
- group,
404
- effectiveGroup: group,
405
- name: getMemberName$1(element.key),
406
- dependencies: collectThisMemberReferences(element.value)
400
+ effectiveGroup,
401
+ previousEffectiveGroup: highestSeen
407
402
  });
403
+ continue;
408
404
  }
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
- });
405
+ highestSeen = effectiveGroup;
432
406
  }
433
- };
407
+ if (!outOfOrderReports.length) return;
408
+ const fix = getSortedMembersFix(context, classBody, classifiedMembers);
409
+ for (const [index, report] of outOfOrderReports.entries()) context.report({
410
+ node: report.element,
411
+ messageId: "outOfOrder",
412
+ data: {
413
+ expectedGroup: ORDER_LABELS[report.effectiveGroup],
414
+ previousGroup: ORDER_LABELS[report.previousEffectiveGroup]
415
+ },
416
+ fix: index === 0 ? fix : void 0
417
+ });
418
+ } };
434
419
  }
435
420
  });
436
421
  //#endregion
@@ -438,25 +423,22 @@ const classMemberOrder = defineRule({
438
423
  const DEFAULT_DISALLOW_INJECT_INJECTOR = true;
439
424
  const DEFAULT_DISALLOW_RUN_IN_INJECTION_CONTEXT = true;
440
425
  const RUN_IN_INJECTION_CONTEXT_NAMES = new Set(["runInInjectionContext", "runInContext"]);
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;
443
- }
444
- function isInjectCall(context, callNode, injectNames, angularNamespaces) {
426
+ const INJECT_NAMES = new Set(["inject"]);
427
+ const INJECTOR_NAMES = new Set(["Injector"]);
428
+ function isInjectCall(context, callNode) {
445
429
  const callee = callNode.callee;
446
- return callee?.type === "Identifier" && injectNames.has(callee.name) && !isShadowedIdentifier(context, callee) || isAngularNamespaceMember(context, callee, angularNamespaces, "inject");
430
+ return isImportedReference(context, callee, "@angular/core", INJECT_NAMES) || isImportedNamespaceMember(context, callee, "@angular/core", INJECT_NAMES);
447
431
  }
448
- function isInjectorReference(context, node, injectorNames, angularNamespaces) {
449
- return node?.type === "Identifier" && injectorNames.has(node.name) && !isShadowedIdentifier(context, node) || isAngularNamespaceMember(context, node, angularNamespaces, "Injector");
432
+ function isInjectorReference(context, node) {
433
+ return isImportedReference(context, node, "@angular/core", INJECTOR_NAMES) || isImportedNamespaceMember(context, node, "@angular/core", INJECTOR_NAMES);
450
434
  }
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);
435
+ function isDisallowedInjectInjector(context, callNode) {
436
+ if (!isInjectCall(context, callNode)) return false;
437
+ return isInjectorReference(context, callNode.arguments?.[0]);
454
438
  }
455
- function isDisallowedRunInInjectionContext(context, callNode, runInInjectionContextNames, runInContextNames, angularNamespaces) {
439
+ function isDisallowedRunInInjectionContext(context, callNode) {
456
440
  const callee = callNode.callee;
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) ?? "");
459
- return false;
441
+ return isImportedReference(context, callee, "@angular/core", RUN_IN_INJECTION_CONTEXT_NAMES) || isImportedNamespaceMember(context, callee, "@angular/core", RUN_IN_INJECTION_CONTEXT_NAMES);
460
442
  }
461
443
  const avoidExplicitInjectionContext = defineRule({
462
444
  meta: {
@@ -485,52 +467,25 @@ const avoidExplicitInjectionContext = defineRule({
485
467
  }
486
468
  },
487
469
  createOnce(context) {
488
- const injectNames = /* @__PURE__ */ new Set();
489
- const injectorNames = /* @__PURE__ */ new Set();
490
- const runInInjectionContextNames = /* @__PURE__ */ new Set();
491
- const runInContextNames = /* @__PURE__ */ new Set();
492
- const angularNamespaces = /* @__PURE__ */ new Set();
493
- return {
494
- before() {
495
- injectNames.clear();
496
- injectorNames.clear();
497
- runInInjectionContextNames.clear();
498
- runInContextNames.clear();
499
- angularNamespaces.clear();
500
- },
501
- ImportDeclaration(node) {
502
- if (node.source?.value !== "@angular/core") return;
503
- for (const specifier of node.specifiers ?? []) {
504
- if (specifier.type === "ImportSpecifier") {
505
- const importedName = getPropertyName(specifier.imported);
506
- if (importedName === "inject") injectNames.add(specifier.local.name);
507
- if (importedName === "Injector") injectorNames.add(specifier.local.name);
508
- if (importedName === "runInInjectionContext") runInInjectionContextNames.add(specifier.local.name);
509
- if (importedName === "runInContext") runInContextNames.add(specifier.local.name);
510
- }
511
- if (specifier.type === "ImportNamespaceSpecifier") angularNamespaces.add(specifier.local.name);
512
- }
513
- },
514
- CallExpression(node) {
515
- const callNode = node;
516
- const options = context.options[0] ?? {};
517
- const disallowInjectInjector = options.disallowInjectInjector ?? DEFAULT_DISALLOW_INJECT_INJECTOR;
518
- const disallowRunInInjectionContext = options.disallowRunInInjectionContext ?? DEFAULT_DISALLOW_RUN_IN_INJECTION_CONTEXT;
519
- if (disallowInjectInjector && isDisallowedInjectInjector(context, callNode, injectNames, injectorNames, angularNamespaces)) context.report({
520
- node: callNode.callee,
521
- messageId: "avoidInjectInjector"
522
- });
523
- if (disallowRunInInjectionContext && isDisallowedRunInInjectionContext(context, callNode, runInInjectionContextNames, runInContextNames, angularNamespaces)) context.report({
524
- node: callNode.callee,
525
- messageId: "avoidRunInInjectionContext"
526
- });
527
- }
528
- };
470
+ return { CallExpression(node) {
471
+ const callNode = node;
472
+ const options = context.options[0] ?? {};
473
+ const disallowInjectInjector = options.disallowInjectInjector ?? DEFAULT_DISALLOW_INJECT_INJECTOR;
474
+ const disallowRunInInjectionContext = options.disallowRunInInjectionContext ?? DEFAULT_DISALLOW_RUN_IN_INJECTION_CONTEXT;
475
+ if (disallowInjectInjector && isDisallowedInjectInjector(context, callNode)) context.report({
476
+ node: callNode.callee,
477
+ messageId: "avoidInjectInjector"
478
+ });
479
+ if (disallowRunInInjectionContext && isDisallowedRunInInjectionContext(context, callNode)) context.report({
480
+ node: callNode.callee,
481
+ messageId: "avoidRunInInjectionContext"
482
+ });
483
+ } };
529
484
  }
530
485
  });
531
486
  //#endregion
532
487
  //#region src/rules/avoid-explicit-subscription-management/index.ts
533
- const TARGET_DECORATORS$2 = new Set([
488
+ const TARGET_DECORATORS$4 = new Set([
534
489
  "Component",
535
490
  "Directive",
536
491
  "Injectable"
@@ -547,9 +502,9 @@ const RXJS_SUBSCRIBABLE_NAMES = new Set([
547
502
  "ReplaySubject",
548
503
  "Subject"
549
504
  ]);
550
- function hasTargetDecorator$2(context, classNode, decoratorImports) {
505
+ function hasTargetDecorator$4(context, classNode) {
551
506
  if (!classNode || !Array.isArray(classNode.decorators)) return false;
552
- return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, decoratorImports));
507
+ return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS$4));
553
508
  }
554
509
  function hasSubscriptionTypeReference(context, node, subscriptionLocalNames, rxjsNamespaces) {
555
510
  if (!node) return false;
@@ -808,11 +763,6 @@ const avoidExplicitSubscriptionManagement = defineRule({
808
763
  const rxjsNamespaces = /* @__PURE__ */ new Set();
809
764
  const takeUntilDestroyedLocalNames = /* @__PURE__ */ new Set();
810
765
  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
766
  return {
817
767
  before() {
818
768
  subscriptionLocalNames.clear();
@@ -820,12 +770,9 @@ const avoidExplicitSubscriptionManagement = defineRule({
820
770
  rxjsNamespaces.clear();
821
771
  takeUntilDestroyedLocalNames.clear();
822
772
  interopNamespaces.clear();
823
- decoratorImports.decoratorLocalNames.clear();
824
- decoratorImports.angularNamespaces.clear();
825
773
  },
826
774
  ImportDeclaration(node) {
827
775
  const source = node.source?.value;
828
- if (source === "@angular/core") for (const specifier of node.specifiers ?? []) addAngularCoreDecoratorImport(specifier, TARGET_DECORATORS$2, decoratorImports);
829
776
  if (source === "rxjs") for (const specifier of node.specifiers ?? []) {
830
777
  if (specifier.type === "ImportSpecifier") {
831
778
  const importedName = getPropertyName(specifier.imported);
@@ -846,7 +793,7 @@ const avoidExplicitSubscriptionManagement = defineRule({
846
793
  ClassBody(node) {
847
794
  const classBody = node;
848
795
  const classNode = classBody.parent;
849
- if (!classNode || !hasTargetDecorator$2(context, classNode, decoratorImports)) return;
796
+ if (!classNode || !hasTargetDecorator$4(context, classNode)) return;
850
797
  const rxjsSubscribableReferences = collectRxjsSubscribableReferences(context, classBody, classNode, rxjsSubscribableLocalNames, rxjsNamespaces);
851
798
  const subscriptionReferences = collectSubscriptionReferences(context, classBody, classNode, subscriptionLocalNames, rxjsNamespaces, rxjsSubscribableReferences, rxjsSubscribableLocalNames, takeUntilDestroyedLocalNames, interopNamespaces);
852
799
  walkNode$1(classBody, classNode, (current) => {
@@ -898,22 +845,59 @@ const avoidExplicitSubscriptionManagement = defineRule({
898
845
  }
899
846
  });
900
847
  //#endregion
848
+ //#region src/rules/avoid-inappropriate-intimacy/index.ts
849
+ const TARGET_DECORATORS$3 = new Set([
850
+ "Component",
851
+ "Directive",
852
+ "Injectable",
853
+ "Service"
854
+ ]);
855
+ function getAngularClassKind(context, classNode) {
856
+ if (!classNode || !Array.isArray(classNode.decorators)) return null;
857
+ if (classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, new Set(["Component"])))) return "component";
858
+ if (classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, new Set(["Directive"])))) return "directive";
859
+ if (classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS$3))) return "service";
860
+ return null;
861
+ }
862
+ const avoidInappropriateIntimacy = defineRule({
863
+ meta: {
864
+ type: "problem",
865
+ docs: {
866
+ description: "Disallow passing Angular component, directive, and service instances as function arguments.",
867
+ recommended: true
868
+ },
869
+ schema: [],
870
+ messages: { avoidThisArgument: "Avoid passing this {{kind}} instance as an argument. Pass the specific values or callbacks the callee needs." }
871
+ },
872
+ createOnce(context) {
873
+ return { CallExpression(node) {
874
+ const callNode = node;
875
+ const thisArguments = (callNode.arguments ?? []).filter((argument) => argument.type === "ThisExpression");
876
+ if (!thisArguments.length) return;
877
+ const kind = getAngularClassKind(context, context.sourceCode.getAncestors(callNode).findLast((ancestor) => ancestor.type === "ClassDeclaration" || ancestor.type === "ClassExpression"));
878
+ if (!kind) return;
879
+ for (const argument of thisArguments) context.report({
880
+ node: argument,
881
+ messageId: "avoidThisArgument",
882
+ data: { kind }
883
+ });
884
+ } };
885
+ }
886
+ });
887
+ //#endregion
901
888
  //#region src/rules/avoid-ng-modules/index.ts
902
889
  const DEFAULT_ALLOW_FOR_GROUPING = true;
903
890
  const DEFAULT_ALLOW_FOR_PROVIDING = false;
904
891
  const DEFAULT_ALLOW_FOR_ROUTING = false;
905
- function isNgModuleDecorator(context, node, importBindings, namespaceImportBindings) {
892
+ function isNgModuleDecorator(context, node) {
906
893
  if (node.type !== "Decorator") return false;
907
894
  const expression = node.expression ?? node;
908
895
  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";
896
+ if (callee?.type === "Identifier") return getImportedName(context, callee, "@angular/core") === "NgModule";
897
+ return callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && isNamespaceImport(context, callee.object, "@angular/core") && getPropertyName(callee.property) === "NgModule";
914
898
  }
915
- function getNgModuleMetadata(context, node, importBindings, namespaceImportBindings) {
916
- if (!isNgModuleDecorator(context, node, importBindings, namespaceImportBindings)) return null;
899
+ function getNgModuleMetadata(context, node) {
900
+ if (!isNgModuleDecorator(context, node)) return null;
917
901
  const expression = node.expression;
918
902
  if (expression?.type !== "CallExpression") return null;
919
903
  const metadata = expression.arguments?.[0];
@@ -927,18 +911,15 @@ function getArrayElementsFromProperty(metadata, propertyName) {
927
911
  function isMemberCall(node, methodName) {
928
912
  return node.type === "CallExpression" && getPropertyName(node.callee?.property) === methodName;
929
913
  }
930
- function isModuleCallFromImport(node, expectedSource, expectedModuleName, importBindings, namespaceImportBindings) {
914
+ function isModuleCallFromImport(context, node, expectedSource, expectedModuleName) {
931
915
  if (node.type !== "CallExpression") return false;
932
916
  const callee = node.callee;
933
917
  if (callee?.type !== "MemberExpression") return false;
934
918
  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
- }
919
+ if (moduleRef?.type === "Identifier") return getImportedName(context, moduleRef, expectedSource) === expectedModuleName;
939
920
  if (moduleRef?.type !== "MemberExpression") return false;
940
921
  if (moduleRef.object?.type !== "Identifier") return false;
941
- if (namespaceImportBindings.get(moduleRef.object.name) !== expectedSource) return false;
922
+ if (!isNamespaceImport(context, moduleRef.object, expectedSource)) return false;
942
923
  return getPropertyName(moduleRef.property) === expectedModuleName;
943
924
  }
944
925
  function getNgModuleClassNode(decoratorNode) {
@@ -989,123 +970,99 @@ const avoidNgModules = defineRule({
989
970
  }
990
971
  },
991
972
  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
973
+ return { Decorator(node) {
974
+ const options = context.options[0] ?? {};
975
+ const allowForGrouping = options.allowForGrouping ?? DEFAULT_ALLOW_FOR_GROUPING;
976
+ const allowForProviding = options.allowForProviding ?? DEFAULT_ALLOW_FOR_PROVIDING;
977
+ const allowForRouting = options.allowForRouting ?? DEFAULT_ALLOW_FOR_ROUTING;
978
+ const metadata = getNgModuleMetadata(context, node);
979
+ if (!metadata) return;
980
+ const importsElements = getArrayElementsFromProperty(metadata, "imports");
981
+ const exportsElements = getArrayElementsFromProperty(metadata, "exports");
982
+ if (!allowForGrouping && (importsElements.length > 0 || exportsElements.length > 0)) context.report({
983
+ node,
984
+ messageId: "avoidModuleImportsExports"
985
+ });
986
+ const importCalls = importsElements.filter((element) => element.type === "CallExpression");
987
+ if (!allowForProviding) {
988
+ for (const call of importCalls) {
989
+ const methodName = getPropertyName(call.callee?.property);
990
+ if (methodName === "forRoot" && isModuleCallFromImport(context, call, "@angular/router", "RouterModule") && !allowForRouting) {
991
+ context.report({
992
+ node: call,
993
+ messageId: "avoidRouterForRoot"
1009
994
  });
1010
995
  continue;
1011
996
  }
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;
997
+ if (methodName === "forRoot" && isModuleCallFromImport(context, call, "@ngrx/store", "StoreModule")) {
1083
998
  context.report({
1084
999
  node: call,
1085
- messageId: "avoidForRoot"
1000
+ messageId: "avoidNgrxStoreForRoot"
1086
1001
  });
1002
+ continue;
1087
1003
  }
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;
1004
+ if (methodName === "forRoot" && isModuleCallFromImport(context, call, "@ngrx/effects", "EffectsModule")) {
1005
+ context.report({
1006
+ node: call,
1007
+ messageId: "avoidNgrxEffectsForRoot"
1008
+ });
1009
+ continue;
1010
+ }
1011
+ if (methodName === "forFeature" && isModuleCallFromImport(context, call, "@ngrx/store", "StoreModule")) {
1012
+ context.report({
1013
+ node: call,
1014
+ messageId: "avoidNgrxStoreForFeature"
1015
+ });
1016
+ continue;
1017
+ }
1018
+ if (methodName === "forFeature" && isModuleCallFromImport(context, call, "@ngrx/effects", "EffectsModule")) {
1019
+ context.report({
1020
+ node: call,
1021
+ messageId: "avoidNgrxEffectsForFeature"
1022
+ });
1023
+ continue;
1024
+ }
1025
+ if (methodName === "instrument" && isModuleCallFromImport(context, call, "@ngrx/store-devtools", "StoreDevtoolsModule")) {
1026
+ context.report({
1027
+ node: call,
1028
+ messageId: "avoidNgrxStoreDevtoolsInstrument"
1029
+ });
1030
+ continue;
1031
+ }
1032
+ if (methodName === "forRoot" && isModuleCallFromImport(context, call, "@ngrx/router-store", "StoreRouterConnectingModule")) {
1033
+ context.report({
1034
+ node: call,
1035
+ messageId: "avoidNgrxRouterStoreForRoot"
1036
+ });
1037
+ continue;
1038
+ }
1039
+ if (methodName === "forRoot" && isModuleCallFromImport(context, call, "@angular/router", "RouterModule") && allowForRouting) continue;
1040
+ if (!isMemberCall(call, "forRoot")) continue;
1097
1041
  context.report({
1098
1042
  node: call,
1099
- messageId: "avoidRouterForChild"
1043
+ messageId: "avoidForRoot"
1100
1044
  });
1101
1045
  }
1046
+ const staticForRootMethod = getStaticForRootMethod(getNgModuleClassNode(node));
1047
+ if (staticForRootMethod) context.report({
1048
+ node: staticForRootMethod.key ?? staticForRootMethod,
1049
+ messageId: "avoidForRoot"
1050
+ });
1102
1051
  }
1103
- };
1052
+ if (!allowForRouting) for (const call of importCalls) {
1053
+ if (!isMemberCall(call, "forChild")) continue;
1054
+ if (!isModuleCallFromImport(context, call, "@angular/router", "RouterModule")) continue;
1055
+ context.report({
1056
+ node: call,
1057
+ messageId: "avoidRouterForChild"
1058
+ });
1059
+ }
1060
+ } };
1104
1061
  }
1105
1062
  });
1106
1063
  //#endregion
1107
1064
  //#region src/rules/avoid-rxjs-state-in-component/index.ts
1108
- const TARGET_DECORATORS$1 = new Set(["Component", "Directive"]);
1065
+ const TARGET_DECORATORS$2 = new Set(["Component", "Directive"]);
1109
1066
  const SUBJECT_NAMES = new Set([
1110
1067
  "BehaviorSubject",
1111
1068
  "ReplaySubject",
@@ -1116,37 +1073,31 @@ const FIELD_NODE_TYPES$1 = new Set([
1116
1073
  "FieldDefinition",
1117
1074
  "PropertyDefinition"
1118
1075
  ]);
1119
- function hasTargetDecorator$1(context, classNode, decoratorImports) {
1076
+ function hasTargetDecorator$3(context, classNode) {
1120
1077
  if (!classNode || !Array.isArray(classNode.decorators)) return false;
1121
- return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, decoratorImports));
1078
+ return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS$2));
1122
1079
  }
1123
- function getImportedSubjectKind(localName, subjectLocalNames) {
1124
- return subjectLocalNames.get(localName) ?? null;
1080
+ function getImportedSubjectKind(context, node) {
1081
+ const importedName = getImportedName(context, node, "rxjs");
1082
+ return SUBJECT_NAMES.has(importedName) ? importedName : null;
1125
1083
  }
1126
- function getSubjectKindFromType(context, node, subjectLocalNames, rxjsNamespaces) {
1084
+ function getSubjectKindFromType(context, node) {
1127
1085
  if (!node) return null;
1128
1086
  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;
1087
+ if (node.typeName?.type === "Identifier") return getImportedSubjectKind(context, node.typeName);
1088
+ if (node.typeName?.type === "TSQualifiedName" && node.typeName.left?.type === "Identifier") {
1089
+ const memberName = getPropertyName(node.typeName.right);
1090
+ return isNamespaceImport(context, node.typeName.left, "rxjs") && SUBJECT_NAMES.has(memberName) ? memberName : null;
1091
+ }
1092
+ return null;
1138
1093
  }
1139
- function getSubjectKindFromConstructor(context, node, subjectLocalNames, rxjsNamespaces) {
1094
+ function getSubjectKindFromConstructor(context, node) {
1140
1095
  const expression = unwrapExpression(node);
1141
1096
  if (expression?.type !== "NewExpression") return null;
1142
1097
  const callee = unwrapExpression(expression.callee);
1143
- if (callee?.type === "Identifier") {
1144
- if (isShadowedIdentifier(context, callee)) return null;
1145
- return getImportedSubjectKind(callee.name, subjectLocalNames);
1146
- }
1098
+ if (callee?.type === "Identifier") return getImportedSubjectKind(context, callee);
1147
1099
  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;
1100
+ if (callee.object?.type !== "Identifier" || !isNamespaceImport(context, callee.object, "rxjs")) return null;
1150
1101
  const memberName = getPropertyName(callee.property);
1151
1102
  return SUBJECT_NAMES.has(memberName) ? memberName : null;
1152
1103
  }
@@ -1205,14 +1156,14 @@ function getUsage(usages, fieldName) {
1205
1156
  usages.set(fieldName, next);
1206
1157
  return next;
1207
1158
  }
1208
- function collectSubjectFields(context, classBody, subjectLocalNames, rxjsNamespaces) {
1159
+ function collectSubjectFields(context, classBody) {
1209
1160
  const fields = /* @__PURE__ */ new Map();
1210
1161
  for (const member of classBody.body ?? []) {
1211
1162
  if (!FIELD_NODE_TYPES$1.has(member.type)) continue;
1212
1163
  const fieldName = getMemberName(member.key);
1213
1164
  if (!fieldName) continue;
1214
1165
  const typeNode = member.typeAnnotation?.typeAnnotation;
1215
- const kind = getSubjectKindFromConstructor(context, member.value, subjectLocalNames, rxjsNamespaces) ?? getSubjectKindFromType(context, typeNode, subjectLocalNames, rxjsNamespaces);
1166
+ const kind = getSubjectKindFromConstructor(context, member.value) ?? getSubjectKindFromType(context, typeNode);
1216
1167
  if (!kind) continue;
1217
1168
  fields.set(fieldName, {
1218
1169
  key: member.key ?? member,
@@ -1260,58 +1211,28 @@ const avoidRxjsStateInComponent = defineRule({
1260
1211
  }
1261
1212
  },
1262
1213
  createOnce(context) {
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
- }
1214
+ return { ClassBody(node) {
1215
+ const classBody = node;
1216
+ const classNode = classBody.parent;
1217
+ if (!hasTargetDecorator$3(context, classNode)) return;
1218
+ const fields = collectSubjectFields(context, classBody);
1219
+ const usages = collectFieldUsages(classBody, fields);
1220
+ for (const [fieldName, field] of fields) {
1221
+ const usage = usages.get(fieldName);
1222
+ if (field.kind === "Subject" && isDestroySubjectUsage(usage)) {
1307
1223
  context.report({
1308
1224
  node: field.key,
1309
- messageId: "avoidRxjsState",
1310
- data: { kind: field.kind }
1225
+ messageId: "avoidDestroySubject"
1311
1226
  });
1227
+ continue;
1312
1228
  }
1229
+ context.report({
1230
+ node: field.key,
1231
+ messageId: "avoidRxjsState",
1232
+ data: { kind: field.kind }
1233
+ });
1313
1234
  }
1314
- };
1235
+ } };
1315
1236
  }
1316
1237
  });
1317
1238
  //#endregion
@@ -1328,22 +1249,21 @@ const KNOWN_SIGNAL_CREATION_FUNCTIONS = new Set([
1328
1249
  ]);
1329
1250
  const LINKED_SIGNAL_CREATOR_NAME = "linkedSignal";
1330
1251
  const COMPUTED_CREATOR_NAME = "computed";
1331
- const EFFECT_CREATOR_NAME = "effect";
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;
1334
- }
1335
- function isSignalCreatorCall(context, node, signalCreatorNames, angularNamespaces) {
1252
+ const EFFECT_CREATOR_NAMES = new Set(["effect"]);
1253
+ const COMPUTED_CREATOR_NAMES = new Set([COMPUTED_CREATOR_NAME]);
1254
+ const LINKED_SIGNAL_CREATOR_NAMES = new Set([LINKED_SIGNAL_CREATOR_NAME]);
1255
+ function isSignalCreatorCall(context, node) {
1336
1256
  if (node?.type !== "CallExpression") return false;
1337
1257
  const callee = node.callee;
1338
- return callee?.type === "Identifier" && signalCreatorNames.has(callee.name) && !isShadowedIdentifier(context, callee) || [...KNOWN_SIGNAL_CREATION_FUNCTIONS].some((name) => isAngularCoreNamespaceMember(context, callee, angularNamespaces, name));
1258
+ return isImportedReference(context, callee, "@angular/core", KNOWN_SIGNAL_CREATION_FUNCTIONS) || isImportedNamespaceMember(context, callee, "@angular/core", KNOWN_SIGNAL_CREATION_FUNCTIONS);
1339
1259
  }
1340
- function isEffectCall(context, node, effectNames, angularNamespaces) {
1260
+ function isEffectCall(context, node) {
1341
1261
  const callee = node.callee;
1342
- return callee?.type === "Identifier" && effectNames.has(callee.name) && !isShadowedIdentifier(context, callee) || isAngularCoreNamespaceMember(context, callee, angularNamespaces, "effect");
1262
+ return isImportedReference(context, callee, "@angular/core", EFFECT_CREATOR_NAMES) || isImportedNamespaceMember(context, callee, "@angular/core", EFFECT_CREATOR_NAMES);
1343
1263
  }
1344
- function isReactiveCreatorCall(context, node, creatorNames, angularNamespaces, angularMemberName) {
1264
+ function isReactiveCreatorCall(context, node, creatorNames) {
1345
1265
  const callee = node.callee;
1346
- return callee?.type === "Identifier" && creatorNames.has(callee.name) && !isShadowedIdentifier(context, callee) || isAngularCoreNamespaceMember(context, callee, angularNamespaces, angularMemberName);
1266
+ return isImportedReference(context, callee, "@angular/core", creatorNames) || isImportedNamespaceMember(context, callee, "@angular/core", creatorNames);
1347
1267
  }
1348
1268
  function isKnownSignalObject(context, objectNode, signalVariableBindings, classSignalProperties) {
1349
1269
  if (!objectNode) return false;
@@ -1398,40 +1318,17 @@ const avoidWritingSignalsInReactiveContext = defineRule({
1398
1318
  messages: { avoidSignalWriteInReactiveContext: "Avoid setting signal values inside {{contextName}}; move writes outside reactive derivations and effects." }
1399
1319
  },
1400
1320
  createOnce(context) {
1401
- const effectNames = /* @__PURE__ */ new Set();
1402
- const computedNames = /* @__PURE__ */ new Set();
1403
- const linkedSignalNames = /* @__PURE__ */ new Set();
1404
- const signalCreatorNames = /* @__PURE__ */ new Set();
1405
- const angularNamespaces = /* @__PURE__ */ new Set();
1406
1321
  const signalVariableBindings = /* @__PURE__ */ new Map();
1407
1322
  const classSignalProperties = /* @__PURE__ */ new Set();
1408
1323
  return {
1409
1324
  before() {
1410
- effectNames.clear();
1411
- computedNames.clear();
1412
- linkedSignalNames.clear();
1413
- signalCreatorNames.clear();
1414
- angularNamespaces.clear();
1415
1325
  signalVariableBindings.clear();
1416
1326
  classSignalProperties.clear();
1417
1327
  },
1418
- ImportDeclaration(node) {
1419
- if (node.source?.value !== "@angular/core") return;
1420
- for (const specifier of node.specifiers ?? []) {
1421
- if (specifier.type === "ImportSpecifier") {
1422
- const importedName = getPropertyName(specifier.imported);
1423
- if (importedName === EFFECT_CREATOR_NAME) effectNames.add(specifier.local.name);
1424
- if (importedName === COMPUTED_CREATOR_NAME) computedNames.add(specifier.local.name);
1425
- if (importedName === LINKED_SIGNAL_CREATOR_NAME) linkedSignalNames.add(specifier.local.name);
1426
- if (importedName && KNOWN_SIGNAL_CREATION_FUNCTIONS.has(importedName)) signalCreatorNames.add(specifier.local.name);
1427
- }
1428
- if (specifier.type === "ImportNamespaceSpecifier") angularNamespaces.add(specifier.local.name);
1429
- }
1430
- },
1431
1328
  VariableDeclarator(node) {
1432
1329
  const declarator = node;
1433
1330
  if (declarator.id?.type !== "Identifier") return;
1434
- if (!isSignalCreatorCall(context, declarator.init, signalCreatorNames, angularNamespaces)) return;
1331
+ if (!isSignalCreatorCall(context, declarator.init)) return;
1435
1332
  const bindings = signalVariableBindings.get(declarator.id.name) ?? /* @__PURE__ */ new Set();
1436
1333
  bindings.add(declarator.id);
1437
1334
  signalVariableBindings.set(declarator.id.name, bindings);
@@ -1439,7 +1336,7 @@ const avoidWritingSignalsInReactiveContext = defineRule({
1439
1336
  "PropertyDefinition, FieldDefinition, AccessorProperty"(node) {
1440
1337
  const property = node;
1441
1338
  if (property.key?.type !== "Identifier") return;
1442
- if (!isSignalCreatorCall(context, property.value, signalCreatorNames, angularNamespaces)) return;
1339
+ if (!isSignalCreatorCall(context, property.value)) return;
1443
1340
  classSignalProperties.add(property.key.name);
1444
1341
  },
1445
1342
  CallExpression(node) {
@@ -1448,21 +1345,21 @@ const avoidWritingSignalsInReactiveContext = defineRule({
1448
1345
  const allowComputedAndLinkedSignals = options.allowComputedAndLinkedSignals ?? false;
1449
1346
  const callNode = node;
1450
1347
  const callbackCandidates = [];
1451
- if (!allowEffects && isEffectCall(context, callNode, effectNames, angularNamespaces)) {
1348
+ if (!allowEffects && isEffectCall(context, callNode)) {
1452
1349
  const callback = callNode.arguments?.[0];
1453
1350
  if (callback?.type === "ArrowFunctionExpression" || callback?.type === "FunctionExpression") callbackCandidates.push({
1454
1351
  callback,
1455
1352
  contextName: "effect()"
1456
1353
  });
1457
1354
  }
1458
- if (!allowComputedAndLinkedSignals && isReactiveCreatorCall(context, callNode, computedNames, angularNamespaces, COMPUTED_CREATOR_NAME)) {
1355
+ if (!allowComputedAndLinkedSignals && isReactiveCreatorCall(context, callNode, COMPUTED_CREATOR_NAMES)) {
1459
1356
  const callback = callNode.arguments?.[0];
1460
1357
  if (callback?.type === "ArrowFunctionExpression" || callback?.type === "FunctionExpression") callbackCandidates.push({
1461
1358
  callback,
1462
1359
  contextName: "computed()"
1463
1360
  });
1464
1361
  }
1465
- if (!allowComputedAndLinkedSignals && isReactiveCreatorCall(context, callNode, linkedSignalNames, angularNamespaces, LINKED_SIGNAL_CREATOR_NAME)) for (const argumentNode of callNode.arguments ?? []) {
1362
+ if (!allowComputedAndLinkedSignals && isReactiveCreatorCall(context, callNode, LINKED_SIGNAL_CREATOR_NAMES)) for (const argumentNode of callNode.arguments ?? []) {
1466
1363
  const argument = argumentNode;
1467
1364
  if (argument.type === "ArrowFunctionExpression" || argument.type === "FunctionExpression") {
1468
1365
  callbackCandidates.push({
@@ -1498,11 +1395,6 @@ const avoidWritingSignalsInReactiveContext = defineRule({
1498
1395
  }, true);
1499
1396
  },
1500
1397
  after() {
1501
- effectNames.clear();
1502
- computedNames.clear();
1503
- linkedSignalNames.clear();
1504
- signalCreatorNames.clear();
1505
- angularNamespaces.clear();
1506
1398
  signalVariableBindings.clear();
1507
1399
  classSignalProperties.clear();
1508
1400
  }
@@ -1510,64 +1402,145 @@ const avoidWritingSignalsInReactiveContext = defineRule({
1510
1402
  }
1511
1403
  });
1512
1404
  //#endregion
1513
- //#region src/rules/component-class-matches-filename/index.ts
1514
- const COMPONENT_DECORATORS$1 = new Set(["Component"]);
1405
+ //#region src/rules/class-matches-filename/index.ts
1406
+ const CLASS_FILENAME_MATCHERS = [
1407
+ {
1408
+ pattern: /^(.*)\.component\.ts$/u,
1409
+ decoratorNames: ["Component"]
1410
+ },
1411
+ {
1412
+ pattern: /^(.*)\.directive\.ts$/u,
1413
+ decoratorNames: ["Directive"]
1414
+ },
1415
+ {
1416
+ pattern: /^(.*)\.service\.ts$/u,
1417
+ decoratorNames: ["Service", "Injectable"]
1418
+ }
1419
+ ];
1515
1420
  function toPascalCase(raw) {
1516
1421
  return raw.split(/[-_\s.]+/u).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
1517
1422
  }
1518
- function getExpectedComponentClassName(filename) {
1423
+ function getClassKind(suffix) {
1424
+ return suffix.toLowerCase();
1425
+ }
1426
+ function getExpectedClassNames(filename, ignoreClassSuffix) {
1519
1427
  const base = filename.split(/[/\\]/u).at(-1) ?? "";
1520
- const match = /^(.*)\.component\.ts$/u.exec(base);
1521
- if (!match) return null;
1522
- const stem = match[1];
1523
- const pascal = toPascalCase(stem);
1524
- return pascal ? `${pascal}Component` : null;
1428
+ for (const { decoratorNames, pattern } of CLASS_FILENAME_MATCHERS) {
1429
+ const match = pattern.exec(base);
1430
+ if (!match) continue;
1431
+ const stem = match[1];
1432
+ const pascal = toPascalCase(stem);
1433
+ if (!pascal) return null;
1434
+ const suffix = decoratorNames[0];
1435
+ return ignoreClassSuffix ? [`${pascal}${suffix}`, pascal] : [`${pascal}${suffix}`];
1436
+ }
1437
+ return null;
1438
+ }
1439
+ function getFileMatcher(filename) {
1440
+ const base = filename.split(/[/\\]/u).at(-1) ?? "";
1441
+ return CLASS_FILENAME_MATCHERS.find(({ pattern }) => pattern.test(base)) ?? null;
1442
+ }
1443
+ function getIgnoreClassSuffixOption(options, kind) {
1444
+ const ignoreClassSuffix = options.ignoreClassSuffix ?? false;
1445
+ if (typeof ignoreClassSuffix === "boolean") return ignoreClassSuffix;
1446
+ return ignoreClassSuffix[kind] ?? false;
1447
+ }
1448
+ function getFileKind(matcher) {
1449
+ return getClassKind(matcher.decoratorNames[0]);
1525
1450
  }
1526
- const componentClassMatchesFilename = defineRule({
1451
+ function formatExpectedNames(expectedNames) {
1452
+ return expectedNames.join("' or '");
1453
+ }
1454
+ function getNodeName(node) {
1455
+ return node.id?.name ?? null;
1456
+ }
1457
+ function getBaseFilename$1(filename) {
1458
+ return filename.split(/[/\\]/u).at(-1) ?? filename;
1459
+ }
1460
+ function hasTargetDecorator$2(context, classNode, decoratorNames) {
1461
+ if (!Array.isArray(classNode.decorators)) return false;
1462
+ return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, decoratorNames));
1463
+ }
1464
+ function getDecoratedClassCountDescription(count) {
1465
+ return count === 0 ? "no decorated classes" : `${count} decorated classes`;
1466
+ }
1467
+ const classMatchesFilename = defineRule({
1527
1468
  meta: {
1528
1469
  type: "suggestion",
1529
1470
  docs: {
1530
- description: "Require the component class name to match its *.component.ts filename.",
1471
+ description: "Require Angular component, directive, and service class names to match their filenames.",
1531
1472
  recommended: true
1532
1473
  },
1533
- schema: [],
1534
- messages: { classNameMismatch: "Component class name should be '{{expectedName}}' to match the filename '{{filename}}'." }
1474
+ schema: [{
1475
+ type: "object",
1476
+ additionalProperties: false,
1477
+ properties: { ignoreClassSuffix: {
1478
+ anyOf: [{ type: "boolean" }, {
1479
+ type: "object",
1480
+ additionalProperties: false,
1481
+ properties: {
1482
+ component: { type: "boolean" },
1483
+ directive: { type: "boolean" },
1484
+ service: { type: "boolean" }
1485
+ }
1486
+ }],
1487
+ default: false
1488
+ } }
1489
+ }],
1490
+ messages: {
1491
+ classNameMismatch: "Angular class name should be '{{expectedName}}' to match the filename '{{filename}}'.",
1492
+ decoratedClassCount: "Angular filename '{{filename}}' should contain exactly one matching decorated class, but found {{actual}}."
1493
+ }
1535
1494
  },
1536
1495
  createOnce(context) {
1537
- const componentClasses = [];
1538
- const decoratorImports = {
1539
- decoratorNames: COMPONENT_DECORATORS$1,
1540
- decoratorLocalNames: /* @__PURE__ */ new Set(),
1541
- angularNamespaces: /* @__PURE__ */ new Set()
1542
- };
1496
+ let programNode = null;
1497
+ let fileMatcher = null;
1498
+ let fileDecoratorNames = /* @__PURE__ */ new Set();
1499
+ const decoratedClasses = [];
1543
1500
  return {
1544
1501
  before() {
1545
- componentClasses.length = 0;
1546
- decoratorImports.decoratorLocalNames.clear();
1547
- decoratorImports.angularNamespaces.clear();
1502
+ programNode = null;
1503
+ decoratedClasses.length = 0;
1504
+ fileMatcher = getFileMatcher(context.filename ?? "");
1505
+ if (!fileMatcher) return false;
1506
+ fileDecoratorNames = new Set(fileMatcher.decoratorNames);
1548
1507
  },
1549
- ImportDeclaration(node) {
1550
- if (node.source?.value !== "@angular/core") return;
1551
- for (const specifier of node.specifiers ?? []) addAngularCoreDecoratorImport(specifier, COMPONENT_DECORATORS$1, decoratorImports);
1508
+ Program(node) {
1509
+ programNode = node;
1552
1510
  },
1553
1511
  ClassDeclaration(node) {
1512
+ if (!fileMatcher) return;
1554
1513
  const classNode = node;
1555
- if (!Array.isArray(classNode.decorators)) return;
1556
- if (classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, decoratorImports))) componentClasses.push(classNode);
1514
+ if (hasTargetDecorator$2(context, classNode, fileDecoratorNames)) decoratedClasses.push(classNode);
1557
1515
  },
1558
1516
  after() {
1559
1517
  const filename = context.filename ?? "";
1560
- const expectedName = getExpectedComponentClassName(filename);
1561
- if (!expectedName) return;
1562
- const baseFilename = filename.split(/[/\\]/u).at(-1) ?? filename;
1563
- const classNode = componentClasses.find((candidate) => candidate.parent?.type === "ExportNamedDeclaration" || candidate.parent?.type === "ExportDefaultDeclaration") ?? null ?? componentClasses[0];
1564
- if (!classNode?.id?.name) return;
1565
- if (classNode.id.name === expectedName) return;
1518
+ if (!fileMatcher) return;
1519
+ const baseFilename = getBaseFilename$1(filename);
1520
+ if (decoratedClasses.length !== 1) {
1521
+ const reportNode = decoratedClasses[0]?.id ?? decoratedClasses[0] ?? programNode;
1522
+ if (!reportNode) return;
1523
+ context.report({
1524
+ node: reportNode,
1525
+ messageId: "decoratedClassCount",
1526
+ data: {
1527
+ actual: getDecoratedClassCountDescription(decoratedClasses.length),
1528
+ filename: baseFilename
1529
+ }
1530
+ });
1531
+ return;
1532
+ }
1533
+ const expectedNames = getExpectedClassNames(filename, getIgnoreClassSuffixOption(context.options[0] ?? {}, getFileKind(fileMatcher)));
1534
+ if (!expectedNames) return;
1535
+ const classNode = decoratedClasses[0];
1536
+ const className = getNodeName(classNode);
1537
+ if (!className) return;
1538
+ if (expectedNames.includes(className)) return;
1566
1539
  context.report({
1567
1540
  node: classNode.id,
1568
1541
  messageId: "classNameMismatch",
1569
1542
  data: {
1570
- expectedName,
1543
+ expectedName: formatExpectedNames(expectedNames),
1571
1544
  filename: baseFilename
1572
1545
  }
1573
1546
  });
@@ -1577,7 +1550,7 @@ const componentClassMatchesFilename = defineRule({
1577
1550
  });
1578
1551
  //#endregion
1579
1552
  //#region src/rules/component-resource-filenames/index.ts
1580
- const COMPONENT_DECORATORS = new Set(["Component"]);
1553
+ const COMPONENT_DECORATORS$1 = new Set(["Component"]);
1581
1554
  const STYLE_EXTENSIONS = new Set([
1582
1555
  "css",
1583
1556
  "less",
@@ -1609,9 +1582,9 @@ function getStaticString(node) {
1609
1582
  if (node.type === "TemplateLiteral" && node.expressions?.length === 0) return { value: node.quasis?.[0]?.value?.cooked ?? node.quasis?.[0]?.value?.raw ?? "" };
1610
1583
  return null;
1611
1584
  }
1612
- function getComponentMetadata(context, node, decoratorImports) {
1585
+ function getComponentMetadata(context, node) {
1613
1586
  if (node.type !== "Decorator") return null;
1614
- if (!isAngularCoreDecorator(context, node, decoratorImports)) return null;
1587
+ if (!isAngularCoreDecorator(context, node, COMPONENT_DECORATORS$1)) return null;
1615
1588
  const expression = node.expression;
1616
1589
  if (expression?.type !== "CallExpression") return null;
1617
1590
  const metadata = expression.arguments?.[0];
@@ -1632,13 +1605,8 @@ const componentResourceFilenames = defineRule({
1632
1605
  }
1633
1606
  },
1634
1607
  createOnce(context) {
1635
- const decoratorImports = {
1636
- decoratorNames: COMPONENT_DECORATORS,
1637
- decoratorLocalNames: /* @__PURE__ */ new Set(),
1638
- angularNamespaces: /* @__PURE__ */ new Set()
1639
- };
1608
+ let expectedStem = null;
1640
1609
  function reportTemplateUrl(valueNode) {
1641
- const expectedStem = getExpectedStem(context.filename ?? "");
1642
1610
  if (!expectedStem) return;
1643
1611
  const staticString = getStaticString(valueNode);
1644
1612
  if (!staticString) return;
@@ -1651,7 +1619,6 @@ const componentResourceFilenames = defineRule({
1651
1619
  });
1652
1620
  }
1653
1621
  function reportStyleUrl(valueNode) {
1654
- const expectedStem = getExpectedStem(context.filename ?? "");
1655
1622
  if (!expectedStem) return;
1656
1623
  const staticString = getStaticString(valueNode);
1657
1624
  if (!staticString) return;
@@ -1673,15 +1640,11 @@ const componentResourceFilenames = defineRule({
1673
1640
  }
1674
1641
  return {
1675
1642
  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);
1643
+ expectedStem = getExpectedStem(context.filename ?? "");
1644
+ if (!expectedStem) return false;
1682
1645
  },
1683
1646
  Decorator(node) {
1684
- const metadata = getComponentMetadata(context, node, decoratorImports);
1647
+ const metadata = getComponentMetadata(context, node);
1685
1648
  if (!metadata) return;
1686
1649
  for (const property of metadata.properties ?? []) {
1687
1650
  if (property.type !== "Property" || property.computed) continue;
@@ -1700,34 +1663,286 @@ const componentResourceFilenames = defineRule({
1700
1663
  }
1701
1664
  });
1702
1665
  //#endregion
1666
+ //#region src/rules/decorator-filename-suffix/index.ts
1667
+ const FILENAME_SUFFIX_CONVENTIONS = [
1668
+ {
1669
+ kind: "component",
1670
+ suffix: ".component.ts",
1671
+ decoratorNames: new Set(["Component"])
1672
+ },
1673
+ {
1674
+ kind: "directive",
1675
+ suffix: ".directive.ts",
1676
+ decoratorNames: new Set(["Directive"])
1677
+ },
1678
+ {
1679
+ kind: "service",
1680
+ suffix: ".service.ts",
1681
+ decoratorNames: new Set(["Service", "Injectable"])
1682
+ }
1683
+ ];
1684
+ function getBaseFilename(filename) {
1685
+ return filename.split(/[/\\]/u).at(-1) ?? filename;
1686
+ }
1687
+ function isTypeScriptFile(filename) {
1688
+ return getBaseFilename(filename).endsWith(".ts");
1689
+ }
1690
+ function getMatchingConvention(context, decorator) {
1691
+ return FILENAME_SUFFIX_CONVENTIONS.find((convention) => isAngularCoreDecorator(context, decorator, convention.decoratorNames)) ?? null;
1692
+ }
1693
+ const decoratorFilenameSuffix = defineRule({
1694
+ meta: {
1695
+ type: "suggestion",
1696
+ docs: {
1697
+ description: "Require Angular component, directive, and service decorators to be declared in files with matching suffixes.",
1698
+ recommended: true
1699
+ },
1700
+ schema: [],
1701
+ messages: { filenameSuffix: "Angular {{kind}} decorator should be declared in a file ending with '{{suffix}}'." }
1702
+ },
1703
+ createOnce(context) {
1704
+ return {
1705
+ before() {
1706
+ if (!isTypeScriptFile(context.filename ?? "")) return false;
1707
+ },
1708
+ Decorator(node) {
1709
+ const convention = getMatchingConvention(context, node);
1710
+ if (!convention) return;
1711
+ if (getBaseFilename(context.filename ?? "").endsWith(convention.suffix)) return;
1712
+ context.report({
1713
+ node,
1714
+ messageId: "filenameSuffix",
1715
+ data: {
1716
+ kind: convention.kind,
1717
+ suffix: convention.suffix
1718
+ }
1719
+ });
1720
+ }
1721
+ };
1722
+ }
1723
+ });
1724
+ //#endregion
1725
+ //#region src/rules/injects-tanstack-query-only-in-component-body/index.ts
1726
+ const TARGET_DECORATORS$1 = new Set(["Component", "Directive"]);
1727
+ const TANSTACK_QUERY_APIS = new Set(["injectQuery", "injectMutation"]);
1728
+ const TANSTACK_QUERY_SOURCES = new Set(["@tanstack/angular-query", "@benjavicente/angular-query"]);
1729
+ const CLASS_FIELD_TYPES$1 = new Set([
1730
+ "AccessorProperty",
1731
+ "FieldDefinition",
1732
+ "PropertyDefinition"
1733
+ ]);
1734
+ const FUNCTION_TYPES$1 = new Set([
1735
+ "ArrowFunctionExpression",
1736
+ "FunctionDeclaration",
1737
+ "FunctionExpression"
1738
+ ]);
1739
+ function hasTargetDecorator$1(context, classNode) {
1740
+ if (!classNode || !Array.isArray(classNode.decorators)) return false;
1741
+ return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS$1));
1742
+ }
1743
+ function isTanstackQueryInjectCall(context, callNode) {
1744
+ return [...TANSTACK_QUERY_SOURCES].some((source) => isImportedReference(context, callNode.callee, source, TANSTACK_QUERY_APIS));
1745
+ }
1746
+ function isDirectComponentOrDirectiveFieldInitializer(context, callNode) {
1747
+ const ancestors = context.sourceCode.getAncestors(callNode);
1748
+ const classField = ancestors.findLast((ancestor) => CLASS_FIELD_TYPES$1.has(ancestor.type));
1749
+ if (!classField || unwrapExpression(classField.value) !== callNode) return false;
1750
+ const classBodyIndex = ancestors.findLastIndex((ancestor) => ancestor.type === "ClassBody");
1751
+ if (classBodyIndex === -1) return false;
1752
+ const classBody = ancestors[classBodyIndex];
1753
+ if (ancestors.slice(classBodyIndex + 1).some((ancestor) => FUNCTION_TYPES$1.has(ancestor.type))) return false;
1754
+ return hasTargetDecorator$1(context, classBody?.parent);
1755
+ }
1756
+ const injectsTanstackQueryOnlyInComponentBody = defineRule({
1757
+ meta: {
1758
+ type: "problem",
1759
+ docs: {
1760
+ description: "Require Angular TanStack Query injectQuery/injectMutation calls to be direct component/directive class fields.",
1761
+ recommended: true
1762
+ },
1763
+ schema: [],
1764
+ messages: { onlyInComponentBody: "Call {{name}} only as a direct class field initializer in an Angular component or directive." }
1765
+ },
1766
+ createOnce(context) {
1767
+ return { CallExpression(node) {
1768
+ const callNode = node;
1769
+ if (!isTanstackQueryInjectCall(context, callNode)) return;
1770
+ if (isDirectComponentOrDirectiveFieldInitializer(context, callNode)) return;
1771
+ context.report({
1772
+ node: callNode.callee ?? callNode,
1773
+ messageId: "onlyInComponentBody",
1774
+ data: { name: callNode.callee?.name ?? "this TanStack Query inject API" }
1775
+ });
1776
+ } };
1777
+ }
1778
+ });
1779
+ //#endregion
1780
+ //#region src/rules/no-resource-api/index.ts
1781
+ const RESOURCE_APIS = new Set(["resource"]);
1782
+ const RXJS_RESOURCE_APIS = new Set(["rxResource"]);
1783
+ const HTTP_RESOURCE_APIS = new Set(["httpResource"]);
1784
+ function isResourceApiCall(context, callNode) {
1785
+ const callee = callNode.callee;
1786
+ if (isImportedReference(context, callee, "@angular/core", RESOURCE_APIS)) return true;
1787
+ if (isImportedNamespaceMember(context, callee, "@angular/core", RESOURCE_APIS)) return true;
1788
+ if (isImportedReference(context, callee, "@angular/core/rxjs-interop", RXJS_RESOURCE_APIS)) return true;
1789
+ if (isImportedNamespaceMember(context, callee, "@angular/core/rxjs-interop", RXJS_RESOURCE_APIS)) return true;
1790
+ if (isImportedReference(context, callee, "@angular/common/http", HTTP_RESOURCE_APIS)) return true;
1791
+ if (isImportedNamespaceMember(context, callee, "@angular/common/http", HTTP_RESOURCE_APIS)) return true;
1792
+ if (callee?.type === "MemberExpression" && callee.object?.type === "MemberExpression" && callee.object.object?.type === "Identifier" && isNamespaceImport(context, callee.object.object, "@angular/common/http") && HTTP_RESOURCE_APIS.has(getPropertyName(callee.object.property) ?? "")) return true;
1793
+ return callee?.type === "MemberExpression" && isImportedReference(context, callee.object, "@angular/common/http", HTTP_RESOURCE_APIS);
1794
+ }
1795
+ const noResourceApi = defineRule({
1796
+ meta: {
1797
+ type: "suggestion",
1798
+ docs: {
1799
+ description: "Disallow Angular resource APIs in favor of a dedicated server-state library.",
1800
+ recommended: true
1801
+ },
1802
+ schema: [],
1803
+ messages: { noResourceApi: "Avoid Angular resource APIs for server state. Prefer a dedicated server-state helper such as TanStack Query." }
1804
+ },
1805
+ createOnce(context) {
1806
+ return { CallExpression(node) {
1807
+ const callNode = node;
1808
+ if (!isResourceApiCall(context, callNode)) return;
1809
+ context.report({
1810
+ node: callNode.callee ?? callNode,
1811
+ messageId: "noResourceApi"
1812
+ });
1813
+ } };
1814
+ }
1815
+ });
1816
+ //#endregion
1817
+ //#region src/rules/no-route-resolvers/index.ts
1818
+ const ROUTE_TYPE_NAMES$1 = new Set(["Route"]);
1819
+ const ROUTES_TYPE_NAMES$1 = new Set(["Routes"]);
1820
+ function isImportedTypeName$1(context, typeNode, importedNames) {
1821
+ if (typeNode?.type === "Identifier") {
1822
+ const importedName = getImportedName(context, typeNode, "@angular/router");
1823
+ return !!importedName && importedNames.has(importedName);
1824
+ }
1825
+ return typeNode?.type === "TSQualifiedName" && typeNode.left?.type === "Identifier" && isNamespaceImport(context, typeNode.left, "@angular/router") && importedNames.has(getPropertyName(typeNode.right) ?? "");
1826
+ }
1827
+ function getTypeParameterNodes$1(typeNode) {
1828
+ return typeNode.typeParameters?.params ?? typeNode.typeArguments?.params ?? [];
1829
+ }
1830
+ function isRouteType$1(context, typeNode) {
1831
+ return typeNode?.type === "TSTypeReference" && isImportedTypeName$1(context, typeNode.typeName, ROUTE_TYPE_NAMES$1);
1832
+ }
1833
+ function isRouteArrayType$1(context, typeNode) {
1834
+ if (!typeNode) return false;
1835
+ if (typeNode.type === "TSTypeReference" && isImportedTypeName$1(context, typeNode.typeName, ROUTES_TYPE_NAMES$1)) return true;
1836
+ if (typeNode.type === "TSArrayType") return isRouteType$1(context, typeNode.elementType);
1837
+ if (typeNode.type !== "TSTypeReference") return false;
1838
+ if (getPropertyName(typeNode.typeName) !== "Array" && getPropertyName(typeNode.typeName) !== "ReadonlyArray") return false;
1839
+ const [elementType] = getTypeParameterNodes$1(typeNode);
1840
+ return isRouteType$1(context, elementType);
1841
+ }
1842
+ function getTypeAnnotation(node) {
1843
+ const typeAnnotation = node?.typeAnnotation;
1844
+ return typeAnnotation?.type === "TSTypeAnnotation" ? typeAnnotation.typeAnnotation : null;
1845
+ }
1846
+ const noRouteResolvers = defineRule({
1847
+ meta: {
1848
+ type: "suggestion",
1849
+ docs: {
1850
+ description: "Disallow Angular route resolvers in typed Route/Routes declarations.",
1851
+ recommended: true
1852
+ },
1853
+ schema: [],
1854
+ messages: { noRouteResolvers: "Avoid Angular route resolvers for data loading. Prefer component-level loading with signals or server-state helpers." }
1855
+ },
1856
+ createOnce(context) {
1857
+ function reportResolveInRouteObject(routeObject) {
1858
+ for (const property of routeObject.properties ?? []) {
1859
+ if (property.type !== "Property" || property.computed) continue;
1860
+ const propertyName = getPropertyName(property.key);
1861
+ if (propertyName === "resolve") {
1862
+ context.report({
1863
+ node: property.key ?? property,
1864
+ messageId: "noRouteResolvers"
1865
+ });
1866
+ continue;
1867
+ }
1868
+ if (propertyName === "children" && property.value?.type === "ArrayExpression") reportResolveInRouteArray(property.value);
1869
+ }
1870
+ }
1871
+ function reportResolveInRouteArray(routeArray) {
1872
+ for (const element of routeArray.elements ?? []) if (element?.type === "ObjectExpression") reportResolveInRouteObject(element);
1873
+ }
1874
+ return { VariableDeclarator(node) {
1875
+ const declarator = node;
1876
+ const typeNode = getTypeAnnotation(declarator.id);
1877
+ if (isRouteArrayType$1(context, typeNode) && declarator.init?.type === "ArrayExpression") {
1878
+ reportResolveInRouteArray(declarator.init);
1879
+ return;
1880
+ }
1881
+ if (isRouteType$1(context, typeNode) && declarator.init?.type === "ObjectExpression") reportResolveInRouteObject(declarator.init);
1882
+ } };
1883
+ }
1884
+ });
1885
+ //#endregion
1886
+ //#region src/rules/no-ui-inheritance/index.ts
1887
+ const UI_DECORATORS = new Set(["Component", "Directive"]);
1888
+ function hasUiDecorator(context, classNode) {
1889
+ if (!Array.isArray(classNode.decorators)) return false;
1890
+ return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, UI_DECORATORS));
1891
+ }
1892
+ const noUiInheritance = defineRule({
1893
+ meta: {
1894
+ type: "suggestion",
1895
+ docs: {
1896
+ description: "Disallow inheritance for Angular components and directives.",
1897
+ recommended: true
1898
+ },
1899
+ schema: [],
1900
+ messages: { noUiInheritance: "Avoid inheritance for Angular {{kind}} classes. Prefer composition with services or inject* helpers." }
1901
+ },
1902
+ createOnce(context) {
1903
+ return { "ClassDeclaration, ClassExpression"(node) {
1904
+ const classNode = node;
1905
+ if (!classNode.superClass) return;
1906
+ if (!hasUiDecorator(context, classNode)) return;
1907
+ const kind = classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, new Set(["Component"]))) ? "component" : "directive";
1908
+ context.report({
1909
+ node: classNode.superClass,
1910
+ messageId: "noUiInheritance",
1911
+ data: { kind }
1912
+ });
1913
+ } };
1914
+ }
1915
+ });
1916
+ //#endregion
1703
1917
  //#region src/rules/prefer-load-component-over-load-children/index.ts
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);
1918
+ const ROUTE_TYPE_NAMES = new Set(["Route"]);
1919
+ const ROUTES_TYPE_NAMES = new Set(["Routes"]);
1920
+ function isImportedTypeName(context, typeNode, importedNames) {
1921
+ if (typeNode?.type === "Identifier") {
1922
+ const importedName = getImportedName(context, typeNode, "@angular/router");
1923
+ return !!importedName && importedNames.has(importedName);
1924
+ }
1925
+ return typeNode?.type === "TSQualifiedName" && typeNode.left?.type === "Identifier" && isNamespaceImport(context, typeNode.left, "@angular/router") && importedNames.has(getPropertyName(typeNode.right) ?? "");
1710
1926
  }
1711
1927
  function getTypeParameterNodes(typeNode) {
1712
1928
  return typeNode.typeParameters?.params ?? typeNode.typeArguments?.params ?? [];
1713
1929
  }
1714
- function isRouteType(typeNode, imports) {
1715
- return typeNode?.type === "TSTypeReference" && isImportedTypeName(typeNode.typeName, imports.routeTypeLocalNames, imports.routerNamespaces);
1930
+ function isRouteType(context, typeNode) {
1931
+ return typeNode?.type === "TSTypeReference" && isImportedTypeName(context, typeNode.typeName, ROUTE_TYPE_NAMES);
1716
1932
  }
1717
- function isRouteArrayType(typeNode, imports) {
1933
+ function isRouteArrayType(context, typeNode) {
1718
1934
  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);
1935
+ if (typeNode.type === "TSTypeReference" && isImportedTypeName(context, typeNode.typeName, ROUTES_TYPE_NAMES)) return true;
1936
+ if (typeNode.type === "TSArrayType") return isRouteType(context, typeNode.elementType);
1721
1937
  if (typeNode.type !== "TSTypeReference") return false;
1722
- const typeName = getTypeName(typeNode.typeName);
1723
- if (typeName !== "Array" && typeName !== "ReadonlyArray") return false;
1938
+ if (getPropertyName(typeNode.typeName) !== "Array" && getPropertyName(typeNode.typeName) !== "ReadonlyArray") return false;
1724
1939
  const [elementType] = getTypeParameterNodes(typeNode);
1725
- return isRouteType(elementType, imports);
1940
+ return isRouteType(context, elementType);
1726
1941
  }
1727
- function isRoutesTypeAnnotation(node, imports) {
1942
+ function isRoutesTypeAnnotation(context, node) {
1728
1943
  const typeAnnotation = node?.typeAnnotation;
1729
1944
  if (!typeAnnotation || typeAnnotation.type !== "TSTypeAnnotation") return false;
1730
- return isRouteArrayType(typeAnnotation.typeAnnotation, imports);
1945
+ return isRouteArrayType(context, typeAnnotation.typeAnnotation);
1731
1946
  }
1732
1947
  function isExportedConstDeclarator(node) {
1733
1948
  if (node.type !== "VariableDeclarator") return false;
@@ -1746,11 +1961,6 @@ const preferLoadComponentOverLoadChildren = defineRule({
1746
1961
  messages: { avoidLoadChildren: "Avoid loadChildren in Routes arrays; prefer loadComponent for lazy-loading standalone route components." }
1747
1962
  },
1748
1963
  createOnce(context) {
1749
- const imports = {
1750
- routeTypeLocalNames: /* @__PURE__ */ new Set(),
1751
- routesTypeLocalNames: /* @__PURE__ */ new Set(),
1752
- routerNamespaces: /* @__PURE__ */ new Set()
1753
- };
1754
1964
  function reportLoadChildrenInRouteObject(routeObject) {
1755
1965
  for (const property of routeObject.properties ?? []) {
1756
1966
  if (property.type !== "Property" || property.computed) continue;
@@ -1768,36 +1978,14 @@ const preferLoadComponentOverLoadChildren = defineRule({
1768
1978
  function reportLoadChildrenInRouteArray(routeArray) {
1769
1979
  for (const element of routeArray.elements ?? []) if (element?.type === "ObjectExpression") reportLoadChildrenInRouteObject(element);
1770
1980
  }
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
- };
1981
+ return { VariableDeclarator(node) {
1982
+ const declarator = node;
1983
+ if (!isExportedConstDeclarator(declarator)) return;
1984
+ if (declarator.id?.type !== "Identifier") return;
1985
+ if (!isRoutesTypeAnnotation(context, declarator.id)) return;
1986
+ if (declarator.init?.type !== "ArrayExpression") return;
1987
+ reportLoadChildrenInRouteArray(declarator.init);
1988
+ } };
1801
1989
  }
1802
1990
  });
1803
1991
  //#endregion
@@ -1960,10 +2148,10 @@ const preferPrivateElements = defineRule({
1960
2148
  });
1961
2149
  //#endregion
1962
2150
  //#region src/rules/prefer-style-url/index.ts
1963
- function isComponentDecoratorCall(context, node, componentLocalNames, angularNamespaces) {
2151
+ const COMPONENT_DECORATORS = new Set(["Component"]);
2152
+ function isComponentDecoratorCall(context, node) {
1964
2153
  const callee = node.callee;
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";
2154
+ return isImportedReference(context, callee, "@angular/core", COMPONENT_DECORATORS) || isImportedNamespaceMember(context, callee, "@angular/core", COMPONENT_DECORATORS);
1967
2155
  }
1968
2156
  function isSingleStyleFileNode(node) {
1969
2157
  if (!node) return false;
@@ -1982,47 +2170,30 @@ const preferStyleUrl = defineRule({
1982
2170
  messages: { preferStyleUrl: "Use styleUrl instead of styleUrls when a component has one style file." }
1983
2171
  },
1984
2172
  createOnce(context) {
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
- }
2173
+ return { CallExpression(node) {
2174
+ const call = node;
2175
+ if (!isComponentDecoratorCall(context, call)) return;
2176
+ const metadata = call.arguments?.[0];
2177
+ if (metadata?.type !== "ObjectExpression") return;
2178
+ for (const property of metadata.properties ?? []) {
2179
+ if (property.type !== "Property") continue;
2180
+ if (property.computed) continue;
2181
+ if (getPropertyName(property.key) !== "styleUrls") continue;
2182
+ if (property.value?.type !== "ArrayExpression") continue;
2183
+ const elements = property.value.elements?.filter(Boolean) ?? [];
2184
+ if (elements.length !== 1) continue;
2185
+ const styleFile = elements[0];
2186
+ if (!isSingleStyleFileNode(styleFile)) continue;
2187
+ const keyRange = getRange(property.key);
2188
+ const valueRange = getRange(property.value);
2189
+ const styleFileRange = getRange(styleFile);
2190
+ context.report({
2191
+ node: property.key,
2192
+ messageId: "preferStyleUrl",
2193
+ fix: keyRange && valueRange && styleFileRange ? (fixer) => [fixer.replaceTextRange(keyRange, "styleUrl"), fixer.replaceTextRange(valueRange, context.sourceCode.text.slice(styleFileRange[0], styleFileRange[1]))] : void 0
2194
+ });
2024
2195
  }
2025
- };
2196
+ } };
2026
2197
  }
2027
2198
  });
2028
2199
  //#endregion
@@ -2036,24 +2207,21 @@ const FIELD_NODE_TYPES = new Set([
2036
2207
  "FieldDefinition",
2037
2208
  "PropertyDefinition"
2038
2209
  ]);
2039
- function hasTargetDecorator(context, classNode, decoratorImports) {
2210
+ function hasTargetDecorator(context, classNode) {
2040
2211
  if (!classNode || !Array.isArray(classNode.decorators)) return false;
2041
- return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, decoratorImports));
2212
+ return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS));
2042
2213
  }
2043
- function isApiCallFromTrackedImports(context, node, importedApiLocalNames, angularNamespaces, apiNames) {
2214
+ function isApiCallFromAngularCore(context, node, apiNames) {
2044
2215
  if (!node || node.type !== "CallExpression") return false;
2045
2216
  const callee = node.callee;
2046
- if (callee?.type === "Identifier") return importedApiLocalNames.has(callee.name) && !isShadowedIdentifier(context, callee);
2217
+ if (isImportedReference(context, callee, "@angular/core", apiNames)) return true;
2047
2218
  if (callee?.type !== "MemberExpression") return false;
2048
2219
  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)) {
2220
+ if (getPropertyName(callee.property) === "required" && callee.object?.type === "Identifier") return supportsRequiredApi && isImportedReference(context, callee.object, "@angular/core", apiNames);
2221
+ if (isImportedNamespaceMember(context, callee, "@angular/core", apiNames)) return true;
2222
+ if (getPropertyName(callee.property) === "required" && callee.object?.type === "MemberExpression" && callee.object.object?.type === "Identifier") {
2055
2223
  const namespaceApiName = getPropertyName(callee.object.property);
2056
- return supportsRequiredApi && !!namespaceApiName && apiNames.has(namespaceApiName) && !isShadowedIdentifier(context, callee.object.object);
2224
+ return supportsRequiredApi && !!namespaceApiName && apiNames.has(namespaceApiName) && isImportedNamespaceMember(context, callee.object, "@angular/core", apiNames);
2057
2225
  }
2058
2226
  return false;
2059
2227
  }
@@ -2098,87 +2266,48 @@ const publicComponentInterface = defineRule({
2098
2266
  }
2099
2267
  },
2100
2268
  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);
2269
+ return { ClassBody(node) {
2270
+ const classNode = node.parent;
2271
+ if (!hasTargetDecorator(context, classNode)) return;
2272
+ for (const member of node.body ?? []) {
2273
+ if (!FIELD_NODE_TYPES.has(member.type)) continue;
2274
+ const isInputModelMember = isApiCallFromAngularCore(context, member.value, INPUT_MODEL_APIS);
2275
+ const isOutputMember = isApiCallFromAngularCore(context, member.value, OUTPUT_APIS);
2276
+ if (isInputModelMember && isNonPublicMember(member)) {
2277
+ context.report({
2278
+ node: member.key ?? member,
2279
+ messageId: "nonPublicInputModel",
2280
+ data: { name: getPropertyName(member.key) ?? "member" },
2281
+ fix: getVisibilityFix(context, member, "public")
2282
+ });
2283
+ continue;
2131
2284
  }
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({
2285
+ if (isOutputMember && isNonPublicMember(member)) {
2286
+ context.report({
2159
2287
  node: member.key ?? member,
2160
- messageId: "publicInjectMember",
2288
+ messageId: "nonPublicOutput",
2161
2289
  data: { name: getPropertyName(member.key) ?? "member" },
2162
- fix: getVisibilityFix(context, member, "protected")
2290
+ fix: getVisibilityFix(context, member, "public")
2163
2291
  });
2292
+ continue;
2164
2293
  }
2165
- },
2166
- "Program:exit"() {
2167
- inputModelLocalNames.clear();
2168
- outputLocalNames.clear();
2169
- injectLocalNames.clear();
2170
- angularNamespaces.clear();
2294
+ if (isPublicMember(member) && isApiCallFromAngularCore(context, member.value, INJECT_APIS)) context.report({
2295
+ node: member.key ?? member,
2296
+ messageId: "publicInjectMember",
2297
+ data: { name: getPropertyName(member.key) ?? "member" },
2298
+ fix: getVisibilityFix(context, member, "protected")
2299
+ });
2171
2300
  }
2172
- };
2301
+ } };
2173
2302
  }
2174
2303
  });
2175
2304
  //#endregion
2176
2305
  //#region src/rules/restrict-injectable-provided-in/index.ts
2177
2306
  const ALLOWED_PROVIDED_IN_VALUES = new Set(["root", "platform"]);
2178
2307
  const INJECTABLE_DECORATORS = new Set(["Injectable"]);
2179
- function getInjectableMetadata(context, node, decoratorImports) {
2308
+ function getInjectableMetadata(context, node) {
2180
2309
  if (node.type !== "Decorator") return null;
2181
- if (!isAngularCoreDecorator(context, node, decoratorImports)) return null;
2310
+ if (!isAngularCoreDecorator(context, node, INJECTABLE_DECORATORS)) return null;
2182
2311
  const expression = node.expression;
2183
2312
  if (expression?.type !== "CallExpression") return null;
2184
2313
  const metadata = expression.arguments?.[0];
@@ -2203,41 +2332,26 @@ const restrictInjectableProvidedIn = defineRule({
2203
2332
  messages: { disallowedProvidedIn: "@Injectable providedIn should be the literal 'root' or 'platform', not {{actual}}." }
2204
2333
  },
2205
2334
  createOnce(context) {
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
- };
2335
+ return { Decorator(node) {
2336
+ const metadata = getInjectableMetadata(context, node);
2337
+ if (!metadata) return;
2338
+ const providedInProperty = getProvidedInProperty(metadata);
2339
+ if (!providedInProperty) return;
2340
+ const value = providedInProperty.value;
2341
+ if (isAllowedProvidedInValue(value)) return;
2342
+ const actual = context.sourceCode.text.slice(value?.range?.[0] ?? providedInProperty.range?.[0] ?? 0, value?.range?.[1] ?? providedInProperty.range?.[1] ?? 0);
2343
+ context.report({
2344
+ node: value ?? providedInProperty,
2345
+ messageId: "disallowedProvidedIn",
2346
+ data: { actual: actual || "this value" }
2347
+ });
2348
+ } };
2235
2349
  }
2236
2350
  });
2237
2351
  //#endregion
2238
2352
  //#region src/rules/rules-of-inject/index.ts
2239
2353
  const DEFAULT_ALLOWED_FUNCTION_NAMES = [];
2240
- const DEFAULT_INJECT_FUNCTION_PREFIXES = ["injext"];
2354
+ const DEFAULT_INJECT_FUNCTION_PREFIXES = ["inject"];
2241
2355
  const DEFAULT_INJECT_FUNCTION_SUFFIXES = ["Guard"];
2242
2356
  const DEFAULT_RUNS_IN_INJECTION_CONTEXT = [];
2243
2357
  const ROUTER_CONTEXT_PROPERTY_NAMES = new Set([
@@ -2454,12 +2568,18 @@ const plugin = eslintCompatPlugin({
2454
2568
  rules: {
2455
2569
  "avoid-explicit-injection-context": avoidExplicitInjectionContext,
2456
2570
  "avoid-explicit-subscription-management": avoidExplicitSubscriptionManagement,
2571
+ "avoid-inappropriate-intimacy": avoidInappropriateIntimacy,
2457
2572
  "avoid-ng-modules": avoidNgModules,
2458
2573
  "avoid-rxjs-state-in-component": avoidRxjsStateInComponent,
2459
2574
  "avoid-writing-signals-in-reactive-context": avoidWritingSignalsInReactiveContext,
2460
2575
  "class-member-order": classMemberOrder,
2461
- "component-class-matches-filename": componentClassMatchesFilename,
2576
+ "class-matches-filename": classMatchesFilename,
2462
2577
  "component-resource-filenames": componentResourceFilenames,
2578
+ "decorator-filename-suffix": decoratorFilenameSuffix,
2579
+ "injects-tanstack-query-only-in-component-body": injectsTanstackQueryOnlyInComponentBody,
2580
+ "no-resource-api": noResourceApi,
2581
+ "no-route-resolvers": noRouteResolvers,
2582
+ "no-ui-inheritance": noUiInheritance,
2463
2583
  "prefer-load-component-over-load-children": preferLoadComponentOverLoadChildren,
2464
2584
  "prefer-private-elements": preferPrivateElements,
2465
2585
  "prefer-style-url": preferStyleUrl,