@benjavicente/lint-angular 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +18 -17
  2. package/dist/index.mjs +530 -647
  3. package/package.json +12 -12
package/dist/index.mjs CHANGED
@@ -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
+ }
178
188
  }
179
- if (specifier.type === "ImportNamespaceSpecifier") imports.angularNamespaces.add(specifier.local.name);
189
+ return null;
180
190
  }
181
- function isAngularCoreDecorator(context, decorator, imports) {
191
+ function isNamespaceImport(context, node, source) {
192
+ if (node?.type !== "Identifier") return false;
193
+ if (isShadowedIdentifier(context, node)) return false;
194
+ const program = getProgramNode(context, node);
195
+ for (const statement of program?.body ?? []) {
196
+ if (statement.type !== "ImportDeclaration" || statement.source?.value !== source) continue;
197
+ if ((statement.specifiers ?? []).some((specifier) => specifier.type === "ImportNamespaceSpecifier" && specifier.local.name === node.name)) return true;
198
+ }
199
+ return false;
200
+ }
201
+ function isImportedReference(context, node, source, importedNames) {
202
+ const importedName = getImportedName(context, node, source);
203
+ return !!importedName && importedNames.has(importedName);
204
+ }
205
+ function isImportedNamespaceMember(context, node, source, memberNames) {
206
+ return node?.type === "MemberExpression" && node.object?.type === "Identifier" && isNamespaceImport(context, node.object, source) && memberNames.has(getPropertyName(node.property) ?? "");
207
+ }
208
+ function isAngularCoreDecorator(context, decorator, decoratorNames) {
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
@@ -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;
253
+ function hasDecorator(context, element, decoratorNames) {
254
+ return Array.isArray(element.decorators) ? element.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, decoratorNames)) : false;
234
255
  }
235
- function classifyMember(context, element, imports) {
256
+ function classifyMember(context, element) {
236
257
  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;
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;
@@ -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,47 +467,20 @@ 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
@@ -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$3(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$2));
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$3(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) => {
@@ -902,18 +849,15 @@ const avoidExplicitSubscriptionManagement = defineRule({
902
849
  const DEFAULT_ALLOW_FOR_GROUPING = true;
903
850
  const DEFAULT_ALLOW_FOR_PROVIDING = false;
904
851
  const DEFAULT_ALLOW_FOR_ROUTING = false;
905
- function isNgModuleDecorator(context, node, importBindings, namespaceImportBindings) {
852
+ function isNgModuleDecorator(context, node) {
906
853
  if (node.type !== "Decorator") return false;
907
854
  const expression = node.expression ?? node;
908
855
  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";
856
+ if (callee?.type === "Identifier") return getImportedName(context, callee, "@angular/core") === "NgModule";
857
+ return callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && isNamespaceImport(context, callee.object, "@angular/core") && getPropertyName(callee.property) === "NgModule";
914
858
  }
915
- function getNgModuleMetadata(context, node, importBindings, namespaceImportBindings) {
916
- if (!isNgModuleDecorator(context, node, importBindings, namespaceImportBindings)) return null;
859
+ function getNgModuleMetadata(context, node) {
860
+ if (!isNgModuleDecorator(context, node)) return null;
917
861
  const expression = node.expression;
918
862
  if (expression?.type !== "CallExpression") return null;
919
863
  const metadata = expression.arguments?.[0];
@@ -927,18 +871,15 @@ function getArrayElementsFromProperty(metadata, propertyName) {
927
871
  function isMemberCall(node, methodName) {
928
872
  return node.type === "CallExpression" && getPropertyName(node.callee?.property) === methodName;
929
873
  }
930
- function isModuleCallFromImport(node, expectedSource, expectedModuleName, importBindings, namespaceImportBindings) {
874
+ function isModuleCallFromImport(context, node, expectedSource, expectedModuleName) {
931
875
  if (node.type !== "CallExpression") return false;
932
876
  const callee = node.callee;
933
877
  if (callee?.type !== "MemberExpression") return false;
934
878
  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
- }
879
+ if (moduleRef?.type === "Identifier") return getImportedName(context, moduleRef, expectedSource) === expectedModuleName;
939
880
  if (moduleRef?.type !== "MemberExpression") return false;
940
881
  if (moduleRef.object?.type !== "Identifier") return false;
941
- if (namespaceImportBindings.get(moduleRef.object.name) !== expectedSource) return false;
882
+ if (!isNamespaceImport(context, moduleRef.object, expectedSource)) return false;
942
883
  return getPropertyName(moduleRef.property) === expectedModuleName;
943
884
  }
944
885
  function getNgModuleClassNode(decoratorNode) {
@@ -989,118 +930,94 @@ const avoidNgModules = defineRule({
989
930
  }
990
931
  },
991
932
  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
933
+ return { Decorator(node) {
934
+ const options = context.options[0] ?? {};
935
+ const allowForGrouping = options.allowForGrouping ?? DEFAULT_ALLOW_FOR_GROUPING;
936
+ const allowForProviding = options.allowForProviding ?? DEFAULT_ALLOW_FOR_PROVIDING;
937
+ const allowForRouting = options.allowForRouting ?? DEFAULT_ALLOW_FOR_ROUTING;
938
+ const metadata = getNgModuleMetadata(context, node);
939
+ if (!metadata) return;
940
+ const importsElements = getArrayElementsFromProperty(metadata, "imports");
941
+ const exportsElements = getArrayElementsFromProperty(metadata, "exports");
942
+ if (!allowForGrouping && (importsElements.length > 0 || exportsElements.length > 0)) context.report({
943
+ node,
944
+ messageId: "avoidModuleImportsExports"
945
+ });
946
+ const importCalls = importsElements.filter((element) => element.type === "CallExpression");
947
+ if (!allowForProviding) {
948
+ for (const call of importCalls) {
949
+ const methodName = getPropertyName(call.callee?.property);
950
+ if (methodName === "forRoot" && isModuleCallFromImport(context, call, "@angular/router", "RouterModule") && !allowForRouting) {
951
+ context.report({
952
+ node: call,
953
+ messageId: "avoidRouterForRoot"
1009
954
  });
1010
955
  continue;
1011
956
  }
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;
957
+ if (methodName === "forRoot" && isModuleCallFromImport(context, call, "@ngrx/store", "StoreModule")) {
1083
958
  context.report({
1084
959
  node: call,
1085
- messageId: "avoidForRoot"
960
+ messageId: "avoidNgrxStoreForRoot"
1086
961
  });
962
+ continue;
1087
963
  }
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;
964
+ if (methodName === "forRoot" && isModuleCallFromImport(context, call, "@ngrx/effects", "EffectsModule")) {
965
+ context.report({
966
+ node: call,
967
+ messageId: "avoidNgrxEffectsForRoot"
968
+ });
969
+ continue;
970
+ }
971
+ if (methodName === "forFeature" && isModuleCallFromImport(context, call, "@ngrx/store", "StoreModule")) {
972
+ context.report({
973
+ node: call,
974
+ messageId: "avoidNgrxStoreForFeature"
975
+ });
976
+ continue;
977
+ }
978
+ if (methodName === "forFeature" && isModuleCallFromImport(context, call, "@ngrx/effects", "EffectsModule")) {
979
+ context.report({
980
+ node: call,
981
+ messageId: "avoidNgrxEffectsForFeature"
982
+ });
983
+ continue;
984
+ }
985
+ if (methodName === "instrument" && isModuleCallFromImport(context, call, "@ngrx/store-devtools", "StoreDevtoolsModule")) {
986
+ context.report({
987
+ node: call,
988
+ messageId: "avoidNgrxStoreDevtoolsInstrument"
989
+ });
990
+ continue;
991
+ }
992
+ if (methodName === "forRoot" && isModuleCallFromImport(context, call, "@ngrx/router-store", "StoreRouterConnectingModule")) {
993
+ context.report({
994
+ node: call,
995
+ messageId: "avoidNgrxRouterStoreForRoot"
996
+ });
997
+ continue;
998
+ }
999
+ if (methodName === "forRoot" && isModuleCallFromImport(context, call, "@angular/router", "RouterModule") && allowForRouting) continue;
1000
+ if (!isMemberCall(call, "forRoot")) continue;
1097
1001
  context.report({
1098
1002
  node: call,
1099
- messageId: "avoidRouterForChild"
1003
+ messageId: "avoidForRoot"
1100
1004
  });
1101
1005
  }
1006
+ const staticForRootMethod = getStaticForRootMethod(getNgModuleClassNode(node));
1007
+ if (staticForRootMethod) context.report({
1008
+ node: staticForRootMethod.key ?? staticForRootMethod,
1009
+ messageId: "avoidForRoot"
1010
+ });
1102
1011
  }
1103
- };
1012
+ if (!allowForRouting) for (const call of importCalls) {
1013
+ if (!isMemberCall(call, "forChild")) continue;
1014
+ if (!isModuleCallFromImport(context, call, "@angular/router", "RouterModule")) continue;
1015
+ context.report({
1016
+ node: call,
1017
+ messageId: "avoidRouterForChild"
1018
+ });
1019
+ }
1020
+ } };
1104
1021
  }
1105
1022
  });
1106
1023
  //#endregion
@@ -1116,37 +1033,31 @@ const FIELD_NODE_TYPES$1 = new Set([
1116
1033
  "FieldDefinition",
1117
1034
  "PropertyDefinition"
1118
1035
  ]);
1119
- function hasTargetDecorator$1(context, classNode, decoratorImports) {
1036
+ function hasTargetDecorator$2(context, classNode) {
1120
1037
  if (!classNode || !Array.isArray(classNode.decorators)) return false;
1121
- return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, decoratorImports));
1038
+ return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS$1));
1122
1039
  }
1123
- function getImportedSubjectKind(localName, subjectLocalNames) {
1124
- return subjectLocalNames.get(localName) ?? null;
1040
+ function getImportedSubjectKind(context, node) {
1041
+ const importedName = getImportedName(context, node, "rxjs");
1042
+ return SUBJECT_NAMES.has(importedName) ? importedName : null;
1125
1043
  }
1126
- function getSubjectKindFromType(context, node, subjectLocalNames, rxjsNamespaces) {
1044
+ function getSubjectKindFromType(context, node) {
1127
1045
  if (!node) return null;
1128
1046
  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;
1047
+ if (node.typeName?.type === "Identifier") return getImportedSubjectKind(context, node.typeName);
1048
+ if (node.typeName?.type === "TSQualifiedName" && node.typeName.left?.type === "Identifier") {
1049
+ const memberName = getPropertyName(node.typeName.right);
1050
+ return isNamespaceImport(context, node.typeName.left, "rxjs") && SUBJECT_NAMES.has(memberName) ? memberName : null;
1051
+ }
1052
+ return null;
1138
1053
  }
1139
- function getSubjectKindFromConstructor(context, node, subjectLocalNames, rxjsNamespaces) {
1054
+ function getSubjectKindFromConstructor(context, node) {
1140
1055
  const expression = unwrapExpression(node);
1141
1056
  if (expression?.type !== "NewExpression") return null;
1142
1057
  const callee = unwrapExpression(expression.callee);
1143
- if (callee?.type === "Identifier") {
1144
- if (isShadowedIdentifier(context, callee)) return null;
1145
- return getImportedSubjectKind(callee.name, subjectLocalNames);
1146
- }
1058
+ if (callee?.type === "Identifier") return getImportedSubjectKind(context, callee);
1147
1059
  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;
1060
+ if (callee.object?.type !== "Identifier" || !isNamespaceImport(context, callee.object, "rxjs")) return null;
1150
1061
  const memberName = getPropertyName(callee.property);
1151
1062
  return SUBJECT_NAMES.has(memberName) ? memberName : null;
1152
1063
  }
@@ -1205,14 +1116,14 @@ function getUsage(usages, fieldName) {
1205
1116
  usages.set(fieldName, next);
1206
1117
  return next;
1207
1118
  }
1208
- function collectSubjectFields(context, classBody, subjectLocalNames, rxjsNamespaces) {
1119
+ function collectSubjectFields(context, classBody) {
1209
1120
  const fields = /* @__PURE__ */ new Map();
1210
1121
  for (const member of classBody.body ?? []) {
1211
1122
  if (!FIELD_NODE_TYPES$1.has(member.type)) continue;
1212
1123
  const fieldName = getMemberName(member.key);
1213
1124
  if (!fieldName) continue;
1214
1125
  const typeNode = member.typeAnnotation?.typeAnnotation;
1215
- const kind = getSubjectKindFromConstructor(context, member.value, subjectLocalNames, rxjsNamespaces) ?? getSubjectKindFromType(context, typeNode, subjectLocalNames, rxjsNamespaces);
1126
+ const kind = getSubjectKindFromConstructor(context, member.value) ?? getSubjectKindFromType(context, typeNode);
1216
1127
  if (!kind) continue;
1217
1128
  fields.set(fieldName, {
1218
1129
  key: member.key ?? member,
@@ -1260,58 +1171,28 @@ const avoidRxjsStateInComponent = defineRule({
1260
1171
  }
1261
1172
  },
1262
1173
  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
- }
1174
+ return { ClassBody(node) {
1175
+ const classBody = node;
1176
+ const classNode = classBody.parent;
1177
+ if (!hasTargetDecorator$2(context, classNode)) return;
1178
+ const fields = collectSubjectFields(context, classBody);
1179
+ const usages = collectFieldUsages(classBody, fields);
1180
+ for (const [fieldName, field] of fields) {
1181
+ const usage = usages.get(fieldName);
1182
+ if (field.kind === "Subject" && isDestroySubjectUsage(usage)) {
1307
1183
  context.report({
1308
1184
  node: field.key,
1309
- messageId: "avoidRxjsState",
1310
- data: { kind: field.kind }
1185
+ messageId: "avoidDestroySubject"
1311
1186
  });
1187
+ continue;
1312
1188
  }
1189
+ context.report({
1190
+ node: field.key,
1191
+ messageId: "avoidRxjsState",
1192
+ data: { kind: field.kind }
1193
+ });
1313
1194
  }
1314
- };
1195
+ } };
1315
1196
  }
1316
1197
  });
1317
1198
  //#endregion
@@ -1328,22 +1209,21 @@ const KNOWN_SIGNAL_CREATION_FUNCTIONS = new Set([
1328
1209
  ]);
1329
1210
  const LINKED_SIGNAL_CREATOR_NAME = "linkedSignal";
1330
1211
  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) {
1212
+ const EFFECT_CREATOR_NAMES = new Set(["effect"]);
1213
+ const COMPUTED_CREATOR_NAMES = new Set([COMPUTED_CREATOR_NAME]);
1214
+ const LINKED_SIGNAL_CREATOR_NAMES = new Set([LINKED_SIGNAL_CREATOR_NAME]);
1215
+ function isSignalCreatorCall(context, node) {
1336
1216
  if (node?.type !== "CallExpression") return false;
1337
1217
  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));
1218
+ return isImportedReference(context, callee, "@angular/core", KNOWN_SIGNAL_CREATION_FUNCTIONS) || isImportedNamespaceMember(context, callee, "@angular/core", KNOWN_SIGNAL_CREATION_FUNCTIONS);
1339
1219
  }
1340
- function isEffectCall(context, node, effectNames, angularNamespaces) {
1220
+ function isEffectCall(context, node) {
1341
1221
  const callee = node.callee;
1342
- return callee?.type === "Identifier" && effectNames.has(callee.name) && !isShadowedIdentifier(context, callee) || isAngularCoreNamespaceMember(context, callee, angularNamespaces, "effect");
1222
+ return isImportedReference(context, callee, "@angular/core", EFFECT_CREATOR_NAMES) || isImportedNamespaceMember(context, callee, "@angular/core", EFFECT_CREATOR_NAMES);
1343
1223
  }
1344
- function isReactiveCreatorCall(context, node, creatorNames, angularNamespaces, angularMemberName) {
1224
+ function isReactiveCreatorCall(context, node, creatorNames) {
1345
1225
  const callee = node.callee;
1346
- return callee?.type === "Identifier" && creatorNames.has(callee.name) && !isShadowedIdentifier(context, callee) || isAngularCoreNamespaceMember(context, callee, angularNamespaces, angularMemberName);
1226
+ return isImportedReference(context, callee, "@angular/core", creatorNames) || isImportedNamespaceMember(context, callee, "@angular/core", creatorNames);
1347
1227
  }
1348
1228
  function isKnownSignalObject(context, objectNode, signalVariableBindings, classSignalProperties) {
1349
1229
  if (!objectNode) return false;
@@ -1398,40 +1278,17 @@ const avoidWritingSignalsInReactiveContext = defineRule({
1398
1278
  messages: { avoidSignalWriteInReactiveContext: "Avoid setting signal values inside {{contextName}}; move writes outside reactive derivations and effects." }
1399
1279
  },
1400
1280
  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
1281
  const signalVariableBindings = /* @__PURE__ */ new Map();
1407
1282
  const classSignalProperties = /* @__PURE__ */ new Set();
1408
1283
  return {
1409
1284
  before() {
1410
- effectNames.clear();
1411
- computedNames.clear();
1412
- linkedSignalNames.clear();
1413
- signalCreatorNames.clear();
1414
- angularNamespaces.clear();
1415
1285
  signalVariableBindings.clear();
1416
1286
  classSignalProperties.clear();
1417
1287
  },
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
1288
  VariableDeclarator(node) {
1432
1289
  const declarator = node;
1433
1290
  if (declarator.id?.type !== "Identifier") return;
1434
- if (!isSignalCreatorCall(context, declarator.init, signalCreatorNames, angularNamespaces)) return;
1291
+ if (!isSignalCreatorCall(context, declarator.init)) return;
1435
1292
  const bindings = signalVariableBindings.get(declarator.id.name) ?? /* @__PURE__ */ new Set();
1436
1293
  bindings.add(declarator.id);
1437
1294
  signalVariableBindings.set(declarator.id.name, bindings);
@@ -1439,7 +1296,7 @@ const avoidWritingSignalsInReactiveContext = defineRule({
1439
1296
  "PropertyDefinition, FieldDefinition, AccessorProperty"(node) {
1440
1297
  const property = node;
1441
1298
  if (property.key?.type !== "Identifier") return;
1442
- if (!isSignalCreatorCall(context, property.value, signalCreatorNames, angularNamespaces)) return;
1299
+ if (!isSignalCreatorCall(context, property.value)) return;
1443
1300
  classSignalProperties.add(property.key.name);
1444
1301
  },
1445
1302
  CallExpression(node) {
@@ -1448,21 +1305,21 @@ const avoidWritingSignalsInReactiveContext = defineRule({
1448
1305
  const allowComputedAndLinkedSignals = options.allowComputedAndLinkedSignals ?? false;
1449
1306
  const callNode = node;
1450
1307
  const callbackCandidates = [];
1451
- if (!allowEffects && isEffectCall(context, callNode, effectNames, angularNamespaces)) {
1308
+ if (!allowEffects && isEffectCall(context, callNode)) {
1452
1309
  const callback = callNode.arguments?.[0];
1453
1310
  if (callback?.type === "ArrowFunctionExpression" || callback?.type === "FunctionExpression") callbackCandidates.push({
1454
1311
  callback,
1455
1312
  contextName: "effect()"
1456
1313
  });
1457
1314
  }
1458
- if (!allowComputedAndLinkedSignals && isReactiveCreatorCall(context, callNode, computedNames, angularNamespaces, COMPUTED_CREATOR_NAME)) {
1315
+ if (!allowComputedAndLinkedSignals && isReactiveCreatorCall(context, callNode, COMPUTED_CREATOR_NAMES)) {
1459
1316
  const callback = callNode.arguments?.[0];
1460
1317
  if (callback?.type === "ArrowFunctionExpression" || callback?.type === "FunctionExpression") callbackCandidates.push({
1461
1318
  callback,
1462
1319
  contextName: "computed()"
1463
1320
  });
1464
1321
  }
1465
- if (!allowComputedAndLinkedSignals && isReactiveCreatorCall(context, callNode, linkedSignalNames, angularNamespaces, LINKED_SIGNAL_CREATOR_NAME)) for (const argumentNode of callNode.arguments ?? []) {
1322
+ if (!allowComputedAndLinkedSignals && isReactiveCreatorCall(context, callNode, LINKED_SIGNAL_CREATOR_NAMES)) for (const argumentNode of callNode.arguments ?? []) {
1466
1323
  const argument = argumentNode;
1467
1324
  if (argument.type === "ArrowFunctionExpression" || argument.type === "FunctionExpression") {
1468
1325
  callbackCandidates.push({
@@ -1498,11 +1355,6 @@ const avoidWritingSignalsInReactiveContext = defineRule({
1498
1355
  }, true);
1499
1356
  },
1500
1357
  after() {
1501
- effectNames.clear();
1502
- computedNames.clear();
1503
- linkedSignalNames.clear();
1504
- signalCreatorNames.clear();
1505
- angularNamespaces.clear();
1506
1358
  signalVariableBindings.clear();
1507
1359
  classSignalProperties.clear();
1508
1360
  }
@@ -1510,64 +1362,145 @@ const avoidWritingSignalsInReactiveContext = defineRule({
1510
1362
  }
1511
1363
  });
1512
1364
  //#endregion
1513
- //#region src/rules/component-class-matches-filename/index.ts
1514
- const COMPONENT_DECORATORS$1 = new Set(["Component"]);
1365
+ //#region src/rules/class-matches-filename/index.ts
1366
+ const CLASS_FILENAME_MATCHERS = [
1367
+ {
1368
+ pattern: /^(.*)\.component\.ts$/u,
1369
+ decoratorNames: ["Component"]
1370
+ },
1371
+ {
1372
+ pattern: /^(.*)\.directive\.ts$/u,
1373
+ decoratorNames: ["Directive"]
1374
+ },
1375
+ {
1376
+ pattern: /^(.*)\.service\.ts$/u,
1377
+ decoratorNames: ["Service", "Injectable"]
1378
+ }
1379
+ ];
1515
1380
  function toPascalCase(raw) {
1516
1381
  return raw.split(/[-_\s.]+/u).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
1517
1382
  }
1518
- function getExpectedComponentClassName(filename) {
1383
+ function getClassKind(suffix) {
1384
+ return suffix.toLowerCase();
1385
+ }
1386
+ function getExpectedClassNames(filename, ignoreClassSuffix) {
1519
1387
  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;
1388
+ for (const { decoratorNames, pattern } of CLASS_FILENAME_MATCHERS) {
1389
+ const match = pattern.exec(base);
1390
+ if (!match) continue;
1391
+ const stem = match[1];
1392
+ const pascal = toPascalCase(stem);
1393
+ if (!pascal) return null;
1394
+ const suffix = decoratorNames[0];
1395
+ return ignoreClassSuffix ? [`${pascal}${suffix}`, pascal] : [`${pascal}${suffix}`];
1396
+ }
1397
+ return null;
1398
+ }
1399
+ function getFileMatcher(filename) {
1400
+ const base = filename.split(/[/\\]/u).at(-1) ?? "";
1401
+ return CLASS_FILENAME_MATCHERS.find(({ pattern }) => pattern.test(base)) ?? null;
1402
+ }
1403
+ function getIgnoreClassSuffixOption(options, kind) {
1404
+ const ignoreClassSuffix = options.ignoreClassSuffix ?? false;
1405
+ if (typeof ignoreClassSuffix === "boolean") return ignoreClassSuffix;
1406
+ return ignoreClassSuffix[kind] ?? false;
1407
+ }
1408
+ function getFileKind(matcher) {
1409
+ return getClassKind(matcher.decoratorNames[0]);
1525
1410
  }
1526
- const componentClassMatchesFilename = defineRule({
1411
+ function formatExpectedNames(expectedNames) {
1412
+ return expectedNames.join("' or '");
1413
+ }
1414
+ function getNodeName(node) {
1415
+ return node.id?.name ?? null;
1416
+ }
1417
+ function getBaseFilename$1(filename) {
1418
+ return filename.split(/[/\\]/u).at(-1) ?? filename;
1419
+ }
1420
+ function hasTargetDecorator$1(context, classNode, decoratorNames) {
1421
+ if (!Array.isArray(classNode.decorators)) return false;
1422
+ return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, decoratorNames));
1423
+ }
1424
+ function getDecoratedClassCountDescription(count) {
1425
+ return count === 0 ? "no decorated classes" : `${count} decorated classes`;
1426
+ }
1427
+ const classMatchesFilename = defineRule({
1527
1428
  meta: {
1528
1429
  type: "suggestion",
1529
1430
  docs: {
1530
- description: "Require the component class name to match its *.component.ts filename.",
1431
+ description: "Require Angular component, directive, and service class names to match their filenames.",
1531
1432
  recommended: true
1532
1433
  },
1533
- schema: [],
1534
- messages: { classNameMismatch: "Component class name should be '{{expectedName}}' to match the filename '{{filename}}'." }
1434
+ schema: [{
1435
+ type: "object",
1436
+ additionalProperties: false,
1437
+ properties: { ignoreClassSuffix: {
1438
+ anyOf: [{ type: "boolean" }, {
1439
+ type: "object",
1440
+ additionalProperties: false,
1441
+ properties: {
1442
+ component: { type: "boolean" },
1443
+ directive: { type: "boolean" },
1444
+ service: { type: "boolean" }
1445
+ }
1446
+ }],
1447
+ default: false
1448
+ } }
1449
+ }],
1450
+ messages: {
1451
+ classNameMismatch: "Angular class name should be '{{expectedName}}' to match the filename '{{filename}}'.",
1452
+ decoratedClassCount: "Angular filename '{{filename}}' should contain exactly one matching decorated class, but found {{actual}}."
1453
+ }
1535
1454
  },
1536
1455
  createOnce(context) {
1537
- const componentClasses = [];
1538
- const decoratorImports = {
1539
- decoratorNames: COMPONENT_DECORATORS$1,
1540
- decoratorLocalNames: /* @__PURE__ */ new Set(),
1541
- angularNamespaces: /* @__PURE__ */ new Set()
1542
- };
1456
+ let programNode = null;
1457
+ let fileMatcher = null;
1458
+ let fileDecoratorNames = /* @__PURE__ */ new Set();
1459
+ const decoratedClasses = [];
1543
1460
  return {
1544
1461
  before() {
1545
- componentClasses.length = 0;
1546
- decoratorImports.decoratorLocalNames.clear();
1547
- decoratorImports.angularNamespaces.clear();
1462
+ programNode = null;
1463
+ decoratedClasses.length = 0;
1464
+ fileMatcher = getFileMatcher(context.filename ?? "");
1465
+ if (!fileMatcher) return false;
1466
+ fileDecoratorNames = new Set(fileMatcher.decoratorNames);
1548
1467
  },
1549
- ImportDeclaration(node) {
1550
- if (node.source?.value !== "@angular/core") return;
1551
- for (const specifier of node.specifiers ?? []) addAngularCoreDecoratorImport(specifier, COMPONENT_DECORATORS$1, decoratorImports);
1468
+ Program(node) {
1469
+ programNode = node;
1552
1470
  },
1553
1471
  ClassDeclaration(node) {
1472
+ if (!fileMatcher) return;
1554
1473
  const classNode = node;
1555
- if (!Array.isArray(classNode.decorators)) return;
1556
- if (classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, decoratorImports))) componentClasses.push(classNode);
1474
+ if (hasTargetDecorator$1(context, classNode, fileDecoratorNames)) decoratedClasses.push(classNode);
1557
1475
  },
1558
1476
  after() {
1559
1477
  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;
1478
+ if (!fileMatcher) return;
1479
+ const baseFilename = getBaseFilename$1(filename);
1480
+ if (decoratedClasses.length !== 1) {
1481
+ const reportNode = decoratedClasses[0]?.id ?? decoratedClasses[0] ?? programNode;
1482
+ if (!reportNode) return;
1483
+ context.report({
1484
+ node: reportNode,
1485
+ messageId: "decoratedClassCount",
1486
+ data: {
1487
+ actual: getDecoratedClassCountDescription(decoratedClasses.length),
1488
+ filename: baseFilename
1489
+ }
1490
+ });
1491
+ return;
1492
+ }
1493
+ const expectedNames = getExpectedClassNames(filename, getIgnoreClassSuffixOption(context.options[0] ?? {}, getFileKind(fileMatcher)));
1494
+ if (!expectedNames) return;
1495
+ const classNode = decoratedClasses[0];
1496
+ const className = getNodeName(classNode);
1497
+ if (!className) return;
1498
+ if (expectedNames.includes(className)) return;
1566
1499
  context.report({
1567
1500
  node: classNode.id,
1568
1501
  messageId: "classNameMismatch",
1569
1502
  data: {
1570
- expectedName,
1503
+ expectedName: formatExpectedNames(expectedNames),
1571
1504
  filename: baseFilename
1572
1505
  }
1573
1506
  });
@@ -1577,7 +1510,7 @@ const componentClassMatchesFilename = defineRule({
1577
1510
  });
1578
1511
  //#endregion
1579
1512
  //#region src/rules/component-resource-filenames/index.ts
1580
- const COMPONENT_DECORATORS = new Set(["Component"]);
1513
+ const COMPONENT_DECORATORS$1 = new Set(["Component"]);
1581
1514
  const STYLE_EXTENSIONS = new Set([
1582
1515
  "css",
1583
1516
  "less",
@@ -1609,9 +1542,9 @@ function getStaticString(node) {
1609
1542
  if (node.type === "TemplateLiteral" && node.expressions?.length === 0) return { value: node.quasis?.[0]?.value?.cooked ?? node.quasis?.[0]?.value?.raw ?? "" };
1610
1543
  return null;
1611
1544
  }
1612
- function getComponentMetadata(context, node, decoratorImports) {
1545
+ function getComponentMetadata(context, node) {
1613
1546
  if (node.type !== "Decorator") return null;
1614
- if (!isAngularCoreDecorator(context, node, decoratorImports)) return null;
1547
+ if (!isAngularCoreDecorator(context, node, COMPONENT_DECORATORS$1)) return null;
1615
1548
  const expression = node.expression;
1616
1549
  if (expression?.type !== "CallExpression") return null;
1617
1550
  const metadata = expression.arguments?.[0];
@@ -1632,13 +1565,8 @@ const componentResourceFilenames = defineRule({
1632
1565
  }
1633
1566
  },
1634
1567
  createOnce(context) {
1635
- const decoratorImports = {
1636
- decoratorNames: COMPONENT_DECORATORS,
1637
- decoratorLocalNames: /* @__PURE__ */ new Set(),
1638
- angularNamespaces: /* @__PURE__ */ new Set()
1639
- };
1568
+ let expectedStem = null;
1640
1569
  function reportTemplateUrl(valueNode) {
1641
- const expectedStem = getExpectedStem(context.filename ?? "");
1642
1570
  if (!expectedStem) return;
1643
1571
  const staticString = getStaticString(valueNode);
1644
1572
  if (!staticString) return;
@@ -1651,7 +1579,6 @@ const componentResourceFilenames = defineRule({
1651
1579
  });
1652
1580
  }
1653
1581
  function reportStyleUrl(valueNode) {
1654
- const expectedStem = getExpectedStem(context.filename ?? "");
1655
1582
  if (!expectedStem) return;
1656
1583
  const staticString = getStaticString(valueNode);
1657
1584
  if (!staticString) return;
@@ -1673,15 +1600,11 @@ const componentResourceFilenames = defineRule({
1673
1600
  }
1674
1601
  return {
1675
1602
  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);
1603
+ expectedStem = getExpectedStem(context.filename ?? "");
1604
+ if (!expectedStem) return false;
1682
1605
  },
1683
1606
  Decorator(node) {
1684
- const metadata = getComponentMetadata(context, node, decoratorImports);
1607
+ const metadata = getComponentMetadata(context, node);
1685
1608
  if (!metadata) return;
1686
1609
  for (const property of metadata.properties ?? []) {
1687
1610
  if (property.type !== "Property" || property.computed) continue;
@@ -1700,34 +1623,94 @@ const componentResourceFilenames = defineRule({
1700
1623
  }
1701
1624
  });
1702
1625
  //#endregion
1626
+ //#region src/rules/decorator-filename-suffix/index.ts
1627
+ const FILENAME_SUFFIX_CONVENTIONS = [
1628
+ {
1629
+ kind: "component",
1630
+ suffix: ".component.ts",
1631
+ decoratorNames: new Set(["Component"])
1632
+ },
1633
+ {
1634
+ kind: "directive",
1635
+ suffix: ".directive.ts",
1636
+ decoratorNames: new Set(["Directive"])
1637
+ },
1638
+ {
1639
+ kind: "service",
1640
+ suffix: ".service.ts",
1641
+ decoratorNames: new Set(["Service", "Injectable"])
1642
+ }
1643
+ ];
1644
+ function getBaseFilename(filename) {
1645
+ return filename.split(/[/\\]/u).at(-1) ?? filename;
1646
+ }
1647
+ function isTypeScriptFile(filename) {
1648
+ return getBaseFilename(filename).endsWith(".ts");
1649
+ }
1650
+ function getMatchingConvention(context, decorator) {
1651
+ return FILENAME_SUFFIX_CONVENTIONS.find((convention) => isAngularCoreDecorator(context, decorator, convention.decoratorNames)) ?? null;
1652
+ }
1653
+ const decoratorFilenameSuffix = defineRule({
1654
+ meta: {
1655
+ type: "suggestion",
1656
+ docs: {
1657
+ description: "Require Angular component, directive, and service decorators to be declared in files with matching suffixes.",
1658
+ recommended: true
1659
+ },
1660
+ schema: [],
1661
+ messages: { filenameSuffix: "Angular {{kind}} decorator should be declared in a file ending with '{{suffix}}'." }
1662
+ },
1663
+ createOnce(context) {
1664
+ return {
1665
+ before() {
1666
+ if (!isTypeScriptFile(context.filename ?? "")) return false;
1667
+ },
1668
+ Decorator(node) {
1669
+ const convention = getMatchingConvention(context, node);
1670
+ if (!convention) return;
1671
+ if (getBaseFilename(context.filename ?? "").endsWith(convention.suffix)) return;
1672
+ context.report({
1673
+ node,
1674
+ messageId: "filenameSuffix",
1675
+ data: {
1676
+ kind: convention.kind,
1677
+ suffix: convention.suffix
1678
+ }
1679
+ });
1680
+ }
1681
+ };
1682
+ }
1683
+ });
1684
+ //#endregion
1703
1685
  //#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);
1686
+ const ROUTE_TYPE_NAMES = new Set(["Route"]);
1687
+ const ROUTES_TYPE_NAMES = new Set(["Routes"]);
1688
+ function isImportedTypeName(context, typeNode, importedNames) {
1689
+ if (typeNode?.type === "Identifier") {
1690
+ const importedName = getImportedName(context, typeNode, "@angular/router");
1691
+ return !!importedName && importedNames.has(importedName);
1692
+ }
1693
+ return typeNode?.type === "TSQualifiedName" && typeNode.left?.type === "Identifier" && isNamespaceImport(context, typeNode.left, "@angular/router") && importedNames.has(getPropertyName(typeNode.right) ?? "");
1710
1694
  }
1711
1695
  function getTypeParameterNodes(typeNode) {
1712
1696
  return typeNode.typeParameters?.params ?? typeNode.typeArguments?.params ?? [];
1713
1697
  }
1714
- function isRouteType(typeNode, imports) {
1715
- return typeNode?.type === "TSTypeReference" && isImportedTypeName(typeNode.typeName, imports.routeTypeLocalNames, imports.routerNamespaces);
1698
+ function isRouteType(context, typeNode) {
1699
+ return typeNode?.type === "TSTypeReference" && isImportedTypeName(context, typeNode.typeName, ROUTE_TYPE_NAMES);
1716
1700
  }
1717
- function isRouteArrayType(typeNode, imports) {
1701
+ function isRouteArrayType(context, typeNode) {
1718
1702
  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);
1703
+ if (typeNode.type === "TSTypeReference" && isImportedTypeName(context, typeNode.typeName, ROUTES_TYPE_NAMES)) return true;
1704
+ if (typeNode.type === "TSArrayType") return isRouteType(context, typeNode.elementType);
1721
1705
  if (typeNode.type !== "TSTypeReference") return false;
1722
- const typeName = getTypeName(typeNode.typeName);
1723
- if (typeName !== "Array" && typeName !== "ReadonlyArray") return false;
1706
+ if (getPropertyName(typeNode.typeName) !== "Array" && getPropertyName(typeNode.typeName) !== "ReadonlyArray") return false;
1724
1707
  const [elementType] = getTypeParameterNodes(typeNode);
1725
- return isRouteType(elementType, imports);
1708
+ return isRouteType(context, elementType);
1726
1709
  }
1727
- function isRoutesTypeAnnotation(node, imports) {
1710
+ function isRoutesTypeAnnotation(context, node) {
1728
1711
  const typeAnnotation = node?.typeAnnotation;
1729
1712
  if (!typeAnnotation || typeAnnotation.type !== "TSTypeAnnotation") return false;
1730
- return isRouteArrayType(typeAnnotation.typeAnnotation, imports);
1713
+ return isRouteArrayType(context, typeAnnotation.typeAnnotation);
1731
1714
  }
1732
1715
  function isExportedConstDeclarator(node) {
1733
1716
  if (node.type !== "VariableDeclarator") return false;
@@ -1746,11 +1729,6 @@ const preferLoadComponentOverLoadChildren = defineRule({
1746
1729
  messages: { avoidLoadChildren: "Avoid loadChildren in Routes arrays; prefer loadComponent for lazy-loading standalone route components." }
1747
1730
  },
1748
1731
  createOnce(context) {
1749
- const imports = {
1750
- routeTypeLocalNames: /* @__PURE__ */ new Set(),
1751
- routesTypeLocalNames: /* @__PURE__ */ new Set(),
1752
- routerNamespaces: /* @__PURE__ */ new Set()
1753
- };
1754
1732
  function reportLoadChildrenInRouteObject(routeObject) {
1755
1733
  for (const property of routeObject.properties ?? []) {
1756
1734
  if (property.type !== "Property" || property.computed) continue;
@@ -1768,36 +1746,14 @@ const preferLoadComponentOverLoadChildren = defineRule({
1768
1746
  function reportLoadChildrenInRouteArray(routeArray) {
1769
1747
  for (const element of routeArray.elements ?? []) if (element?.type === "ObjectExpression") reportLoadChildrenInRouteObject(element);
1770
1748
  }
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
- };
1749
+ return { VariableDeclarator(node) {
1750
+ const declarator = node;
1751
+ if (!isExportedConstDeclarator(declarator)) return;
1752
+ if (declarator.id?.type !== "Identifier") return;
1753
+ if (!isRoutesTypeAnnotation(context, declarator.id)) return;
1754
+ if (declarator.init?.type !== "ArrayExpression") return;
1755
+ reportLoadChildrenInRouteArray(declarator.init);
1756
+ } };
1801
1757
  }
1802
1758
  });
1803
1759
  //#endregion
@@ -1960,10 +1916,10 @@ const preferPrivateElements = defineRule({
1960
1916
  });
1961
1917
  //#endregion
1962
1918
  //#region src/rules/prefer-style-url/index.ts
1963
- function isComponentDecoratorCall(context, node, componentLocalNames, angularNamespaces) {
1919
+ const COMPONENT_DECORATORS = new Set(["Component"]);
1920
+ function isComponentDecoratorCall(context, node) {
1964
1921
  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";
1922
+ return isImportedReference(context, callee, "@angular/core", COMPONENT_DECORATORS) || isImportedNamespaceMember(context, callee, "@angular/core", COMPONENT_DECORATORS);
1967
1923
  }
1968
1924
  function isSingleStyleFileNode(node) {
1969
1925
  if (!node) return false;
@@ -1982,47 +1938,30 @@ const preferStyleUrl = defineRule({
1982
1938
  messages: { preferStyleUrl: "Use styleUrl instead of styleUrls when a component has one style file." }
1983
1939
  },
1984
1940
  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
- }
1941
+ return { CallExpression(node) {
1942
+ const call = node;
1943
+ if (!isComponentDecoratorCall(context, call)) return;
1944
+ const metadata = call.arguments?.[0];
1945
+ if (metadata?.type !== "ObjectExpression") return;
1946
+ for (const property of metadata.properties ?? []) {
1947
+ if (property.type !== "Property") continue;
1948
+ if (property.computed) continue;
1949
+ if (getPropertyName(property.key) !== "styleUrls") continue;
1950
+ if (property.value?.type !== "ArrayExpression") continue;
1951
+ const elements = property.value.elements?.filter(Boolean) ?? [];
1952
+ if (elements.length !== 1) continue;
1953
+ const styleFile = elements[0];
1954
+ if (!isSingleStyleFileNode(styleFile)) continue;
1955
+ const keyRange = getRange(property.key);
1956
+ const valueRange = getRange(property.value);
1957
+ const styleFileRange = getRange(styleFile);
1958
+ context.report({
1959
+ node: property.key,
1960
+ messageId: "preferStyleUrl",
1961
+ fix: keyRange && valueRange && styleFileRange ? (fixer) => [fixer.replaceTextRange(keyRange, "styleUrl"), fixer.replaceTextRange(valueRange, context.sourceCode.text.slice(styleFileRange[0], styleFileRange[1]))] : void 0
1962
+ });
2024
1963
  }
2025
- };
1964
+ } };
2026
1965
  }
2027
1966
  });
2028
1967
  //#endregion
@@ -2036,24 +1975,21 @@ const FIELD_NODE_TYPES = new Set([
2036
1975
  "FieldDefinition",
2037
1976
  "PropertyDefinition"
2038
1977
  ]);
2039
- function hasTargetDecorator(context, classNode, decoratorImports) {
1978
+ function hasTargetDecorator(context, classNode) {
2040
1979
  if (!classNode || !Array.isArray(classNode.decorators)) return false;
2041
- return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, decoratorImports));
1980
+ return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS));
2042
1981
  }
2043
- function isApiCallFromTrackedImports(context, node, importedApiLocalNames, angularNamespaces, apiNames) {
1982
+ function isApiCallFromAngularCore(context, node, apiNames) {
2044
1983
  if (!node || node.type !== "CallExpression") return false;
2045
1984
  const callee = node.callee;
2046
- if (callee?.type === "Identifier") return importedApiLocalNames.has(callee.name) && !isShadowedIdentifier(context, callee);
1985
+ if (isImportedReference(context, callee, "@angular/core", apiNames)) return true;
2047
1986
  if (callee?.type !== "MemberExpression") return false;
2048
1987
  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)) {
1988
+ if (getPropertyName(callee.property) === "required" && callee.object?.type === "Identifier") return supportsRequiredApi && isImportedReference(context, callee.object, "@angular/core", apiNames);
1989
+ if (isImportedNamespaceMember(context, callee, "@angular/core", apiNames)) return true;
1990
+ if (getPropertyName(callee.property) === "required" && callee.object?.type === "MemberExpression" && callee.object.object?.type === "Identifier") {
2055
1991
  const namespaceApiName = getPropertyName(callee.object.property);
2056
- return supportsRequiredApi && !!namespaceApiName && apiNames.has(namespaceApiName) && !isShadowedIdentifier(context, callee.object.object);
1992
+ return supportsRequiredApi && !!namespaceApiName && apiNames.has(namespaceApiName) && isImportedNamespaceMember(context, callee.object, "@angular/core", apiNames);
2057
1993
  }
2058
1994
  return false;
2059
1995
  }
@@ -2098,87 +2034,48 @@ const publicComponentInterface = defineRule({
2098
2034
  }
2099
2035
  },
2100
2036
  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);
2037
+ return { ClassBody(node) {
2038
+ const classNode = node.parent;
2039
+ if (!hasTargetDecorator(context, classNode)) return;
2040
+ for (const member of node.body ?? []) {
2041
+ if (!FIELD_NODE_TYPES.has(member.type)) continue;
2042
+ const isInputModelMember = isApiCallFromAngularCore(context, member.value, INPUT_MODEL_APIS);
2043
+ const isOutputMember = isApiCallFromAngularCore(context, member.value, OUTPUT_APIS);
2044
+ if (isInputModelMember && isNonPublicMember(member)) {
2045
+ context.report({
2046
+ node: member.key ?? member,
2047
+ messageId: "nonPublicInputModel",
2048
+ data: { name: getPropertyName(member.key) ?? "member" },
2049
+ fix: getVisibilityFix(context, member, "public")
2050
+ });
2051
+ continue;
2131
2052
  }
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({
2053
+ if (isOutputMember && isNonPublicMember(member)) {
2054
+ context.report({
2159
2055
  node: member.key ?? member,
2160
- messageId: "publicInjectMember",
2056
+ messageId: "nonPublicOutput",
2161
2057
  data: { name: getPropertyName(member.key) ?? "member" },
2162
- fix: getVisibilityFix(context, member, "protected")
2058
+ fix: getVisibilityFix(context, member, "public")
2163
2059
  });
2060
+ continue;
2164
2061
  }
2165
- },
2166
- "Program:exit"() {
2167
- inputModelLocalNames.clear();
2168
- outputLocalNames.clear();
2169
- injectLocalNames.clear();
2170
- angularNamespaces.clear();
2062
+ if (isPublicMember(member) && isApiCallFromAngularCore(context, member.value, INJECT_APIS)) context.report({
2063
+ node: member.key ?? member,
2064
+ messageId: "publicInjectMember",
2065
+ data: { name: getPropertyName(member.key) ?? "member" },
2066
+ fix: getVisibilityFix(context, member, "protected")
2067
+ });
2171
2068
  }
2172
- };
2069
+ } };
2173
2070
  }
2174
2071
  });
2175
2072
  //#endregion
2176
2073
  //#region src/rules/restrict-injectable-provided-in/index.ts
2177
2074
  const ALLOWED_PROVIDED_IN_VALUES = new Set(["root", "platform"]);
2178
2075
  const INJECTABLE_DECORATORS = new Set(["Injectable"]);
2179
- function getInjectableMetadata(context, node, decoratorImports) {
2076
+ function getInjectableMetadata(context, node) {
2180
2077
  if (node.type !== "Decorator") return null;
2181
- if (!isAngularCoreDecorator(context, node, decoratorImports)) return null;
2078
+ if (!isAngularCoreDecorator(context, node, INJECTABLE_DECORATORS)) return null;
2182
2079
  const expression = node.expression;
2183
2080
  if (expression?.type !== "CallExpression") return null;
2184
2081
  const metadata = expression.arguments?.[0];
@@ -2203,35 +2100,20 @@ const restrictInjectableProvidedIn = defineRule({
2203
2100
  messages: { disallowedProvidedIn: "@Injectable providedIn should be the literal 'root' or 'platform', not {{actual}}." }
2204
2101
  },
2205
2102
  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
- };
2103
+ return { Decorator(node) {
2104
+ const metadata = getInjectableMetadata(context, node);
2105
+ if (!metadata) return;
2106
+ const providedInProperty = getProvidedInProperty(metadata);
2107
+ if (!providedInProperty) return;
2108
+ const value = providedInProperty.value;
2109
+ if (isAllowedProvidedInValue(value)) return;
2110
+ const actual = context.sourceCode.text.slice(value?.range?.[0] ?? providedInProperty.range?.[0] ?? 0, value?.range?.[1] ?? providedInProperty.range?.[1] ?? 0);
2111
+ context.report({
2112
+ node: value ?? providedInProperty,
2113
+ messageId: "disallowedProvidedIn",
2114
+ data: { actual: actual || "this value" }
2115
+ });
2116
+ } };
2235
2117
  }
2236
2118
  });
2237
2119
  //#endregion
@@ -2458,8 +2340,9 @@ const plugin = eslintCompatPlugin({
2458
2340
  "avoid-rxjs-state-in-component": avoidRxjsStateInComponent,
2459
2341
  "avoid-writing-signals-in-reactive-context": avoidWritingSignalsInReactiveContext,
2460
2342
  "class-member-order": classMemberOrder,
2461
- "component-class-matches-filename": componentClassMatchesFilename,
2343
+ "class-matches-filename": classMatchesFilename,
2462
2344
  "component-resource-filenames": componentResourceFilenames,
2345
+ "decorator-filename-suffix": decoratorFilenameSuffix,
2463
2346
  "prefer-load-component-over-load-children": preferLoadComponentOverLoadChildren,
2464
2347
  "prefer-private-elements": preferPrivateElements,
2465
2348
  "prefer-style-url": preferStyleUrl,