@hatem427/code-guard-ci 2.2.9 → 3.0.0

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 (70) hide show
  1. package/config/angular.config.ts +468 -27
  2. package/config/guidelines.config.ts +130 -5
  3. package/config/nextjs.config.ts +284 -11
  4. package/config/react.config.ts +440 -16
  5. package/dist/config/angular.config.d.ts.map +1 -1
  6. package/dist/config/angular.config.js +468 -26
  7. package/dist/config/angular.config.js.map +1 -1
  8. package/dist/config/guidelines.config.d.ts.map +1 -1
  9. package/dist/config/guidelines.config.js +127 -5
  10. package/dist/config/guidelines.config.js.map +1 -1
  11. package/dist/config/nextjs.config.d.ts.map +1 -1
  12. package/dist/config/nextjs.config.js +284 -11
  13. package/dist/config/nextjs.config.js.map +1 -1
  14. package/dist/config/react.config.d.ts.map +1 -1
  15. package/dist/config/react.config.js +440 -16
  16. package/dist/config/react.config.js.map +1 -1
  17. package/dist/scripts/config-generators/ai-config-generator.d.ts.map +1 -1
  18. package/dist/scripts/config-generators/ai-config-generator.js +9 -71
  19. package/dist/scripts/config-generators/ai-config-generator.js.map +1 -1
  20. package/dist/scripts/config-generators/eslint-generator.d.ts.map +1 -1
  21. package/dist/scripts/config-generators/eslint-generator.js +517 -13
  22. package/dist/scripts/config-generators/eslint-generator.js.map +1 -1
  23. package/dist/scripts/config-generators/frameworks/angular.d.ts +6 -0
  24. package/dist/scripts/config-generators/frameworks/angular.d.ts.map +1 -0
  25. package/dist/scripts/config-generators/frameworks/angular.js +81 -0
  26. package/dist/scripts/config-generators/frameworks/angular.js.map +1 -0
  27. package/dist/scripts/config-generators/frameworks/general.d.ts +6 -0
  28. package/dist/scripts/config-generators/frameworks/general.d.ts.map +1 -0
  29. package/dist/scripts/config-generators/frameworks/general.js +15 -0
  30. package/dist/scripts/config-generators/frameworks/general.js.map +1 -0
  31. package/dist/scripts/config-generators/frameworks/index.d.ts +17 -0
  32. package/dist/scripts/config-generators/frameworks/index.d.ts.map +1 -0
  33. package/dist/scripts/config-generators/frameworks/index.js +28 -0
  34. package/dist/scripts/config-generators/frameworks/index.js.map +1 -0
  35. package/dist/scripts/config-generators/frameworks/nextjs.d.ts +6 -0
  36. package/dist/scripts/config-generators/frameworks/nextjs.d.ts.map +1 -0
  37. package/dist/scripts/config-generators/frameworks/nextjs.js +115 -0
  38. package/dist/scripts/config-generators/frameworks/nextjs.js.map +1 -0
  39. package/dist/scripts/config-generators/frameworks/node.d.ts +6 -0
  40. package/dist/scripts/config-generators/frameworks/node.d.ts.map +1 -0
  41. package/dist/scripts/config-generators/frameworks/node.js +19 -0
  42. package/dist/scripts/config-generators/frameworks/node.js.map +1 -0
  43. package/dist/scripts/config-generators/frameworks/nuxt.d.ts +6 -0
  44. package/dist/scripts/config-generators/frameworks/nuxt.d.ts.map +1 -0
  45. package/dist/scripts/config-generators/frameworks/nuxt.js +18 -0
  46. package/dist/scripts/config-generators/frameworks/nuxt.js.map +1 -0
  47. package/dist/scripts/config-generators/frameworks/react.d.ts +6 -0
  48. package/dist/scripts/config-generators/frameworks/react.d.ts.map +1 -0
  49. package/dist/scripts/config-generators/frameworks/react.js +117 -0
  50. package/dist/scripts/config-generators/frameworks/react.js.map +1 -0
  51. package/dist/scripts/config-generators/frameworks/svelte.d.ts +6 -0
  52. package/dist/scripts/config-generators/frameworks/svelte.d.ts.map +1 -0
  53. package/dist/scripts/config-generators/frameworks/svelte.js +17 -0
  54. package/dist/scripts/config-generators/frameworks/svelte.js.map +1 -0
  55. package/dist/scripts/config-generators/frameworks/vue.d.ts +6 -0
  56. package/dist/scripts/config-generators/frameworks/vue.d.ts.map +1 -0
  57. package/dist/scripts/config-generators/frameworks/vue.js +19 -0
  58. package/dist/scripts/config-generators/frameworks/vue.js.map +1 -0
  59. package/package.json +1 -1
  60. package/scripts/config-generators/ai-config-generator.ts +19 -78
  61. package/scripts/config-generators/eslint-generator.ts +511 -7
  62. package/scripts/config-generators/frameworks/angular.ts +78 -0
  63. package/scripts/config-generators/frameworks/general.ts +12 -0
  64. package/scripts/config-generators/frameworks/index.ts +17 -0
  65. package/scripts/config-generators/frameworks/nextjs.ts +112 -0
  66. package/scripts/config-generators/frameworks/node.ts +16 -0
  67. package/scripts/config-generators/frameworks/nuxt.ts +15 -0
  68. package/scripts/config-generators/frameworks/react.ts +114 -0
  69. package/scripts/config-generators/frameworks/svelte.ts +14 -0
  70. package/scripts/config-generators/frameworks/vue.ts +16 -0
@@ -18,6 +18,24 @@ import { registerRules, Rule } from './guidelines.config';
18
18
  const angularRules: Rule[] = [
19
19
  // ── NEW: Control Flow Syntax (Angular 17+) ────────────────────────────
20
20
 
21
+ // ─────────────────────────────────────────
22
+ // RULE: angular-use-new-control-flow
23
+ // ROLE: Enforce Angular 17+ @if/@for/@switch syntax
24
+ // PURPOSE: Prevents legacy *ngIf, *ngFor, *ngSwitch usage — the new
25
+ // control flow is built-in, tree-shakeable, and has better
26
+ // type narrowing than structural directives.
27
+ // EXAMPLE:
28
+ // WRONG:
29
+ // <div *ngIf="isVisible">
30
+ // <li *ngFor="let item of items">{{ item }}</li>
31
+ // </div>
32
+ // RIGHT:
33
+ // @if (isVisible) {
34
+ // @for (item of items; track item.id) {
35
+ // <li>{{ item }}</li>
36
+ // }
37
+ // }
38
+ // ─────────────────────────────────────────
21
39
  {
22
40
  id: 'angular-use-new-control-flow',
23
41
  label: 'Use @if, @for, @switch instead of *ngIf, *ngFor',
@@ -35,7 +53,7 @@ const angularRules: Rule[] = [
35
53
  violations.push({
36
54
  line: i + 1,
37
55
  message:
38
- 'Found structural directive. Use new Angular control flow syntax for better performance and readability.\n\nWhy: @if/@for are built-in Angular features with less overhead than directive matching.\n\n- <div *ngIf="isVisible">\n- <li *ngFor="let item of items">{{ item }}</li>\n- </div>\n\n+ @if (isVisible) {\n+ @for (item of items; track item.id) {\n+ <li>{{ item }}</li>\n+ }\n+ }',
56
+ 'Found structural directive. Use new Angular control flow syntax for better performance and readability.',
39
57
  });
40
58
  }
41
59
  }
@@ -45,6 +63,21 @@ const angularRules: Rule[] = [
45
63
  category: 'Template Syntax',
46
64
  },
47
65
 
66
+ // ─────────────────────────────────────────
67
+ // RULE: angular-use-let-syntax
68
+ // ROLE: Recommend Angular 17+ @let template variables
69
+ // PURPOSE: Simplifies complex template expressions by extracting them
70
+ // into named variables, improving readability and avoiding
71
+ // repeated evaluation.
72
+ // EXAMPLE:
73
+ // WRONG:
74
+ // {{ user?.profile?.name ? user.profile.name : 'Guest' }}
75
+ // {{ user?.profile?.name ? user.profile.name : 'Guest' }}
76
+ // RIGHT:
77
+ // @let displayName = user?.profile?.name ?? 'Guest';
78
+ // {{ displayName }}
79
+ // {{ displayName }}
80
+ // ─────────────────────────────────────────
48
81
  {
49
82
  id: 'angular-use-let-syntax',
50
83
  label: 'Use @let for template variables',
@@ -71,6 +104,30 @@ const angularRules: Rule[] = [
71
104
 
72
105
  // ── Lifecycle ──────────────────────────────────────────────────────────
73
106
 
107
+ // ─────────────────────────────────────────
108
+ // RULE: angular-no-ngoninit
109
+ // ROLE: Enforce modern Angular initialization patterns
110
+ // PURPOSE: ngOnInit was needed when constructor injection was the norm.
111
+ // With inject() + signals, field initializers and effect()
112
+ // cover most use cases without lifecycle hooks.
113
+ // EXAMPLE:
114
+ // WRONG:
115
+ // export class UserComponent implements OnInit {
116
+ // private userService = inject(UserService);
117
+ // user: User | null = null;
118
+ // ngOnInit() {
119
+ // this.user = this.userService.getUser();
120
+ // }
121
+ // }
122
+ // RIGHT:
123
+ // export class UserComponent {
124
+ // private userService = inject(UserService);
125
+ // user = signal<User | null>(null);
126
+ // constructor() {
127
+ // effect(() => this.user.set(this.userService.getUser()));
128
+ // }
129
+ // }
130
+ // ─────────────────────────────────────────
74
131
  {
75
132
  id: 'angular-no-ngoninit',
76
133
  label: 'Avoid ngOnInit for simple initialization',
@@ -85,6 +142,20 @@ const angularRules: Rule[] = [
85
142
 
86
143
  // ── Deprecated APIs ────────────────────────────────────────────────────
87
144
 
145
+ // ─────────────────────────────────────────
146
+ // RULE: angular-no-ng-deep
147
+ // ROLE: Block deprecated view encapsulation piercing
148
+ // PURPOSE: ::ng-deep breaks component encapsulation and is officially
149
+ // deprecated. It leads to unpredictable style leakage and
150
+ // makes refactoring dangerous.
151
+ // EXAMPLE:
152
+ // WRONG:
153
+ // :host ::ng-deep .mat-card { background: red; }
154
+ // RIGHT:
155
+ // :host { --card-bg: red; }
156
+ // /* In child component CSS: */
157
+ // .mat-card { background: var(--card-bg); }
158
+ // ─────────────────────────────────────────
88
159
  {
89
160
  id: 'angular-no-ng-deep',
90
161
  label: 'No ::ng-deep',
@@ -97,6 +168,25 @@ const angularRules: Rule[] = [
97
168
  category: 'Angular Deprecated APIs',
98
169
  },
99
170
 
171
+ // ─────────────────────────────────────────
172
+ // RULE: angular-no-common-module
173
+ // ROLE: Enforce tree-shakeable standalone imports
174
+ // PURPOSE: CommonModule imports NgIf, NgFor, NgClass, AsyncPipe, and
175
+ // 30+ other directives/pipes. Standalone components should
176
+ // import ONLY what they use — or better, use @if/@for.
177
+ // EXAMPLE:
178
+ // WRONG:
179
+ // @Component({
180
+ // standalone: true,
181
+ // imports: [CommonModule],
182
+ // })
183
+ // RIGHT:
184
+ // @Component({
185
+ // standalone: true,
186
+ // imports: [NgClass, NgTemplateOutlet],
187
+ // })
188
+ // // Or even better — use @if/@for and import nothing
189
+ // ─────────────────────────────────────────
100
190
  {
101
191
  id: 'angular-no-common-module',
102
192
  label: 'No CommonModule import in standalone components',
@@ -109,6 +199,20 @@ const angularRules: Rule[] = [
109
199
  category: 'Angular Deprecated APIs',
110
200
  },
111
201
 
202
+ // ─────────────────────────────────────────
203
+ // RULE: angular-no-ngclass
204
+ // ROLE: Enforce modern class binding patterns
205
+ // PURPOSE: [ngClass] requires importing NgClass and has complex object
206
+ // syntax. [class.name] binding is simpler, and Tailwind
207
+ // pseudo-classes (disabled:, hover:) are even cleaner.
208
+ // EXAMPLE:
209
+ // WRONG:
210
+ // <button [ngClass]="{ 'btn-active': isActive, 'btn-disabled': isDisabled }">
211
+ // RIGHT:
212
+ // <button [class.btn-active]="isActive()" [class.btn-disabled]="isDisabled()">
213
+ // <!-- Or with Tailwind: -->
214
+ // <button class="not-disabled:bg-blue-500 disabled:bg-gray-300">
215
+ // ─────────────────────────────────────────
112
216
  {
113
217
  id: 'angular-no-ngclass',
114
218
  label: 'No [ngClass] — use [class] binding or Tailwind',
@@ -121,6 +225,20 @@ const angularRules: Rule[] = [
121
225
  category: 'Angular Deprecated APIs',
122
226
  },
123
227
 
228
+ // ─────────────────────────────────────────
229
+ // RULE: angular-no-ngstyle
230
+ // ROLE: Enforce modern style binding patterns
231
+ // PURPOSE: [ngStyle] is verbose and requires importing NgStyle.
232
+ // Direct [style.prop] binding or Tailwind utilities are
233
+ // simpler and more performant.
234
+ // EXAMPLE:
235
+ // WRONG:
236
+ // <div [ngStyle]="{ 'background-color': bgColor, 'font-size': fontSize + 'px' }">
237
+ // RIGHT:
238
+ // <div [style.background-color]="bgColor" [style.font-size.rem]="fontSize">
239
+ // <!-- Or with Tailwind: -->
240
+ // <div class="bg-primary text-base">
241
+ // ─────────────────────────────────────────
124
242
  {
125
243
  id: 'angular-no-ngstyle',
126
244
  label: 'No [ngStyle] — use [style] binding or Tailwind',
@@ -135,6 +253,22 @@ const angularRules: Rule[] = [
135
253
 
136
254
  // ── Template best practices ────────────────────────────────────────────
137
255
 
256
+ // ─────────────────────────────────────────
257
+ // RULE: angular-no-function-in-template
258
+ // ROLE: Block function calls in template bindings
259
+ // PURPOSE: Functions in templates execute on EVERY change detection
260
+ // cycle, causing severe performance degradation. Signals and
261
+ // computed() are memoized and only re-evaluate when deps change.
262
+ // EXAMPLE:
263
+ // WRONG:
264
+ // <span>{{ getFullName() }}</span>
265
+ // <div [class.active]="isItemActive(item)">
266
+ // RIGHT:
267
+ // fullName = computed(() => this.firstName() + ' ' + this.lastName());
268
+ // <span>{{ fullName() }}</span>
269
+ // <!-- Or use a pipe: -->
270
+ // <span>{{ user | fullName }}</span>
271
+ // ─────────────────────────────────────────
138
272
  {
139
273
  id: 'angular-no-function-in-template',
140
274
  label: 'No function calls in templates',
@@ -146,16 +280,6 @@ const angularRules: Rule[] = [
146
280
  customCheck: (file) => {
147
281
  const violations: Array<{ line: number | null; message: string }> = [];
148
282
 
149
- /**
150
- * Regex explanation:
151
- * Match patterns like {{ someFunc() }} or (click)="handler()" but EXCLUDE:
152
- * - Event handlers: (click)="...", (submit)="...", on*="..."
153
- * - Known safe pipes: | async, | date, | json, etc.
154
- * - Structural directives: *ngIf, *ngFor, @if, @for
155
- *
156
- * We look for interpolation bindings {{ expr() }} and property bindings [prop]="expr()"
157
- * that contain function calls which are NOT event bindings.
158
- */
159
283
  const interpolationRegex = /\{\{[^}]*\w+\s*\([^)]*\)[^}]*\}\}/g;
160
284
  const propertyBindingRegex = /\[(?!click|submit|keyup|keydown|change|input|focus|blur)\w+\]\s*=\s*"[^"]*\w+\s*\([^)]*\)[^"]*"/g;
161
285
 
@@ -165,7 +289,6 @@ const angularRules: Rule[] = [
165
289
  // Check interpolation bindings {{ func() }}
166
290
  interpolationRegex.lastIndex = 0;
167
291
  if (interpolationRegex.test(line)) {
168
- // Exclude pipe usages like {{ value | pipeName }}
169
292
  const trimmed = line.trim();
170
293
  if (!trimmed.includes(' | ')) {
171
294
  violations.push({
@@ -193,6 +316,24 @@ const angularRules: Rule[] = [
193
316
 
194
317
  // ── RxJS best practices ────────────────────────────────────────────────
195
318
 
319
+ // ─────────────────────────────────────────
320
+ // RULE: angular-use-async-pipe
321
+ // ROLE: Prevent manual subscription memory leaks
322
+ // PURPOSE: Manual .subscribe() in components is the #1 cause of memory
323
+ // leaks in Angular apps. The async pipe or toSignal()
324
+ // auto-unsubscribes when the component is destroyed.
325
+ // EXAMPLE:
326
+ // WRONG:
327
+ // ngOnInit() {
328
+ // this.userService.getUser().subscribe(user => this.user = user);
329
+ // }
330
+ // RIGHT:
331
+ // user$ = this.userService.getUser();
332
+ // <!-- In template: -->
333
+ // {{ user$ | async }}
334
+ // <!-- Or convert to signal: -->
335
+ // user = toSignal(this.userService.getUser());
336
+ // ─────────────────────────────────────────
196
337
  {
197
338
  id: 'angular-use-async-pipe',
198
339
  label: 'Use async pipe or toSignal() instead of manual subscribe',
@@ -205,6 +346,26 @@ const angularRules: Rule[] = [
205
346
  category: 'RxJS',
206
347
  },
207
348
 
349
+ // ─────────────────────────────────────────
350
+ // RULE: angular-use-takeuntildestroyed
351
+ // ROLE: Enforce subscription cleanup in Angular classes
352
+ // PURPOSE: If subscribe() is unavoidable, takeUntilDestroyed() from
353
+ // @angular/core/rxjs-interop handles cleanup automatically
354
+ // tied to the component's DestroyRef lifecycle.
355
+ // EXAMPLE:
356
+ // WRONG:
357
+ // ngOnInit() {
358
+ // this.data$.subscribe(d => this.process(d));
359
+ // }
360
+ // // Memory leak — never unsubscribes!
361
+ // RIGHT:
362
+ // private destroyRef = inject(DestroyRef);
363
+ // ngOnInit() {
364
+ // this.data$.pipe(
365
+ // takeUntilDestroyed(this.destroyRef)
366
+ // ).subscribe(d => this.process(d));
367
+ // }
368
+ // ─────────────────────────────────────────
208
369
  {
209
370
  id: 'angular-use-takeuntildestroyed',
210
371
  label: 'Use takeUntilDestroyed() for subscriptions',
@@ -216,7 +377,6 @@ const angularRules: Rule[] = [
216
377
  customCheck: (file) => {
217
378
  const violations: Array<{ line: number | null; message: string }> = [];
218
379
 
219
- // Only check Angular component/directive/service files
220
380
  if (
221
381
  !file.content.includes('@Component') &&
222
382
  !file.content.includes('@Directive') &&
@@ -243,6 +403,20 @@ const angularRules: Rule[] = [
243
403
  category: 'RxJS',
244
404
  },
245
405
 
406
+ // ─────────────────────────────────────────
407
+ // RULE: angular-prefer-signals
408
+ // ROLE: Recommend migration from BehaviorSubject to Signals
409
+ // PURPOSE: Angular Signals (signal(), computed(), effect()) are simpler,
410
+ // synchronous, and deeply integrated with change detection.
411
+ // BehaviorSubject is async and requires subscription management.
412
+ // EXAMPLE:
413
+ // WRONG:
414
+ // count$ = new BehaviorSubject<number>(0);
415
+ // increment() { this.count$.next(this.count$.value + 1); }
416
+ // RIGHT:
417
+ // count = signal(0);
418
+ // increment() { this.count.update(v => v + 1); }
419
+ // ─────────────────────────────────────────
246
420
  {
247
421
  id: 'angular-prefer-signals',
248
422
  label: 'Consider using Angular Signals',
@@ -252,7 +426,6 @@ const angularRules: Rule[] = [
252
426
  fileExtensions: ['ts'],
253
427
  pattern: null,
254
428
  customCheck: (file) => {
255
- // Only flag if the component uses BehaviorSubject but no signals
256
429
  const hasBehaviorSubject = file.content.includes('BehaviorSubject');
257
430
  const hasSignal = file.content.includes('signal(') || file.content.includes('computed(');
258
431
 
@@ -272,6 +445,28 @@ const angularRules: Rule[] = [
272
445
  category: 'Angular Signals',
273
446
  },
274
447
 
448
+ // ─────────────────────────────────────────
449
+ // RULE: angular-prefer-inject
450
+ // ROLE: Enforce inject() function over constructor DI
451
+ // PURPOSE: inject() enables dependency injection at field declaration
452
+ // level, works with standalone components, and doesn't require
453
+ // constructor parameter boilerplate.
454
+ // EXAMPLE:
455
+ // WRONG:
456
+ // export class UserComponent {
457
+ // constructor(
458
+ // private userService: UserService,
459
+ // private router: Router,
460
+ // private http: HttpClient
461
+ // ) {}
462
+ // }
463
+ // RIGHT:
464
+ // export class UserComponent {
465
+ // private userService = inject(UserService);
466
+ // private router = inject(Router);
467
+ // private http = inject(HttpClient);
468
+ // }
469
+ // ─────────────────────────────────────────
275
470
  {
276
471
  id: 'angular-prefer-inject',
277
472
  label: 'Use inject() instead of constructor injection',
@@ -283,7 +478,6 @@ const angularRules: Rule[] = [
283
478
  customCheck: (file) => {
284
479
  const violations: Array<{ line: number | null; message: string }> = [];
285
480
 
286
- // Only check Angular component/directive/service files
287
481
  if (
288
482
  !file.content.includes('@Component') &&
289
483
  !file.content.includes('@Directive') &&
@@ -292,12 +486,9 @@ const angularRules: Rule[] = [
292
486
  return [];
293
487
  }
294
488
 
295
- // Check for constructor with dependency injection parameters
296
- // Matches: constructor(private service: Type) or constructor(public readonly http: HttpClient)
297
489
  const constructorPattern = /constructor\s*\(\s*(?:private|public|protected|readonly|\s)+\w+\s*:\s*\w+/;
298
490
 
299
491
  if (constructorPattern.test(file.content)) {
300
- // Find the line number
301
492
  for (let i = 0; i < file.lines.length; i++) {
302
493
  if (constructorPattern.test(file.lines[i])) {
303
494
  violations.push({
@@ -318,6 +509,22 @@ const angularRules: Rule[] = [
318
509
 
319
510
  // ── NEW: Signals & Signal-based Features (Angular 16+, essential in v21) ────
320
511
 
512
+ // ─────────────────────────────────────────
513
+ // RULE: angular-use-signals
514
+ // ROLE: Enforce Signal-based reactive state
515
+ // PURPOSE: Signals are the future of Angular reactivity. They are
516
+ // synchronous, automatically tracked by change detection,
517
+ // and eliminate the need for most RxJS subscriptions.
518
+ // EXAMPLE:
519
+ // WRONG:
520
+ // import { BehaviorSubject } from 'rxjs';
521
+ // counter = new BehaviorSubject(0);
522
+ // counter.next(counter.value + 1);
523
+ // RIGHT:
524
+ // import { signal } from '@angular/core';
525
+ // counter = signal(0);
526
+ // counter.update(val => val + 1);
527
+ // ─────────────────────────────────────────
321
528
  {
322
529
  id: 'angular-use-signals',
323
530
  label: 'Use Signals for reactive state',
@@ -329,7 +536,6 @@ const angularRules: Rule[] = [
329
536
  customCheck: (file) => {
330
537
  const violations: Array<{ line: number | null; message: string }> = [];
331
538
 
332
- // Only check component/service files
333
539
  if (
334
540
  !file.content.includes('@Component') &&
335
541
  !file.content.includes('@Injectable') &&
@@ -338,7 +544,6 @@ const angularRules: Rule[] = [
338
544
  return [];
339
545
  }
340
546
 
341
- // Flag BehaviorSubject usage without signal migration
342
547
  const hasBehaviorSubject = /BehaviorSubject|Subject/.test(file.content);
343
548
  const hasSignal = /signal\(|computed\(|effect\(/.test(file.content);
344
549
  const hasRxJS = /rxjs|Observable<|\.pipe\(/.test(file.content);
@@ -357,6 +562,22 @@ const angularRules: Rule[] = [
357
562
  category: 'Angular Signals',
358
563
  },
359
564
 
565
+ // ─────────────────────────────────────────
566
+ // RULE: angular-use-computed
567
+ // ROLE: Enforce computed() for derived state
568
+ // PURPOSE: computed() is memoized and only recalculates when its
569
+ // signal dependencies change. Getters re-evaluate on every
570
+ // change detection cycle.
571
+ // EXAMPLE:
572
+ // WRONG:
573
+ // get totalPrice(): number {
574
+ // return this.items.reduce((sum, item) => sum + item.price, 0);
575
+ // }
576
+ // RIGHT:
577
+ // totalPrice = computed(() =>
578
+ // this.items().reduce((sum, item) => sum + item.price, 0)
579
+ // );
580
+ // ─────────────────────────────────────────
360
581
  {
361
582
  id: 'angular-use-computed',
362
583
  label: 'Use computed() for derived state',
@@ -368,12 +589,11 @@ const angularRules: Rule[] = [
368
589
  customCheck: (file) => {
369
590
  const violations: Array<{ line: number | null; message: string }> = [];
370
591
 
371
- // Flag getters in component classes that could be computed signals
372
592
  if (/get\s+\w+\s*\(\s*\).*\{[\s\S]*?return.*signal|this\.\w+\(\)/.test(file.content)) {
373
593
  violations.push({
374
594
  line: null,
375
595
  message:
376
- 'Found getter function. Use computed() from signals for automatic memoization and better performance.\n\nWhy: computed() caches results and only recalculates when dependencies change.\n\n- get totalPrice() {\n- return this.items.reduce((sum, item) => sum + item.price, 0);\n- }\n\n+ import { computed } from "@angular/core";\n+ totalPrice = computed(() => \n+ this.items().reduce((sum, item) => sum + item.price, 0)\n+ );',
596
+ 'Found getter function. Use computed() from signals for automatic memoization and better performance.',
377
597
  });
378
598
  }
379
599
  return violations;
@@ -382,6 +602,22 @@ const angularRules: Rule[] = [
382
602
  category: 'Angular Signals',
383
603
  },
384
604
 
605
+ // ─────────────────────────────────────────
606
+ // RULE: angular-use-effect
607
+ // ROLE: Enforce effect() for signal-driven side effects
608
+ // PURPOSE: effect() automatically tracks signal dependencies and
609
+ // re-runs when they change, replacing ngOnInit-based
610
+ // subscription patterns with declarative reactivity.
611
+ // EXAMPLE:
612
+ // WRONG:
613
+ // ngOnInit() {
614
+ // this.data$.subscribe(data => this.processData(data));
615
+ // }
616
+ // RIGHT:
617
+ // constructor() {
618
+ // effect(() => this.processData(this.data()));
619
+ // }
620
+ // ─────────────────────────────────────────
385
621
  {
386
622
  id: 'angular-use-effect',
387
623
  label: 'Use effect() for side effects instead of ngOnInit',
@@ -393,12 +629,11 @@ const angularRules: Rule[] = [
393
629
  customCheck: (file) => {
394
630
  const violations: Array<{ line: number | null; message: string }> = [];
395
631
 
396
- // Flag ngOnInit with subscriptions or side effects
397
632
  if (/ngOnInit\s*\(\)[\s\S]*?\{[\s\S]*?(?:subscribe|addEventListener|setTimeout)/.test(file.content)) {
398
633
  violations.push({
399
634
  line: null,
400
635
  message:
401
- 'Found ngOnInit with side effects. Use effect() from signals for cleaner dependency tracking.\n\nWhy: effect() automatically tracks signal dependencies and runs when they change, without lifecycle timing issues.\n\n- ngOnInit() {\n- this.data$.subscribe(data => this.processData(data));\n- }\n\n+ import { effect } from "@angular/core";\n+ constructor() {\n+ effect(() => this.processData(this.data()));\n+ }',
636
+ 'Found ngOnInit with side effects. Use effect() from signals for cleaner dependency tracking.',
402
637
  });
403
638
  }
404
639
  return violations;
@@ -407,6 +642,22 @@ const angularRules: Rule[] = [
407
642
  category: 'Angular Signals',
408
643
  },
409
644
 
645
+ // ─────────────────────────────────────────
646
+ // RULE: angular-use-input-output-functions
647
+ // ROLE: Enforce signal-based input/output API
648
+ // PURPOSE: input(), output(), and model() functions provide better
649
+ // null-safety, type inference, and signal integration than
650
+ // @Input/@Output decorators.
651
+ // EXAMPLE:
652
+ // WRONG:
653
+ // @Input() title: string = '';
654
+ // @Output() clicked = new EventEmitter<void>();
655
+ // RIGHT:
656
+ // title = input<string>('');
657
+ // clicked = output<void>();
658
+ // // Two-way binding:
659
+ // name = model<string>('');
660
+ // ─────────────────────────────────────────
410
661
  {
411
662
  id: 'angular-use-input-output-functions',
412
663
  label: 'Use input(), output(), model() functions',
@@ -419,6 +670,22 @@ const angularRules: Rule[] = [
419
670
  category: 'Angular Components',
420
671
  },
421
672
 
673
+ // ─────────────────────────────────────────
674
+ // RULE: angular-signal-based-viewchild
675
+ // ROLE: Recommend signal-based queries
676
+ // PURPOSE: viewChild() and viewChildren() return signals, eliminating
677
+ // the need for AfterViewInit lifecycle hooks and providing
678
+ // automatic change detection integration.
679
+ // EXAMPLE:
680
+ // WRONG:
681
+ // @ViewChild('myInput') myInput!: ElementRef;
682
+ // ngAfterViewInit() { this.myInput.nativeElement.focus(); }
683
+ // RIGHT:
684
+ // myInput = viewChild<ElementRef>('myInput');
685
+ // constructor() {
686
+ // afterNextRender(() => this.myInput()?.nativeElement.focus());
687
+ // }
688
+ // ─────────────────────────────────────────
422
689
  {
423
690
  id: 'angular-signal-based-viewchild',
424
691
  label: 'Use signal-based @ViewChild where applicable',
@@ -433,6 +700,22 @@ const angularRules: Rule[] = [
433
700
 
434
701
  // ── NEW: Zoneless Change Detection (Angular 18+, recommended in v21) ───
435
702
 
703
+ // ─────────────────────────────────────────
704
+ // RULE: angular-consider-zoneless
705
+ // ROLE: Recommend zoneless change detection
706
+ // PURPOSE: Zone.js patches every async API (setTimeout, Promise, XHR)
707
+ // adding ~100KB to the bundle. Signals make Zone.js optional
708
+ // since they notify Angular directly of changes.
709
+ // EXAMPLE:
710
+ // WRONG:
711
+ // // app.config.ts (with Zone.js)
712
+ // provideZoneChangeDetection({ eventCoalescing: true })
713
+ // RIGHT:
714
+ // // app.config.ts (zoneless)
715
+ // provideExperimentalZonelessChangeDetection()
716
+ // // angular.json
717
+ // "polyfills": [] // remove zone.js
718
+ // ─────────────────────────────────────────
436
719
  {
437
720
  id: 'angular-consider-zoneless',
438
721
  label: 'Consider zoneless change detection',
@@ -445,10 +728,27 @@ const angularRules: Rule[] = [
445
728
  category: 'Performance',
446
729
  },
447
730
 
448
- // ── NEW: Server-Side Rendering (Angular 17+) ──────────────────────────
449
-
450
731
  // ── Performance Rules ──────────────────────────────────────────────────
451
732
 
733
+ // ─────────────────────────────────────────
734
+ // RULE: angular-onpush-change-detection
735
+ // ROLE: Recommend OnPush change detection strategy
736
+ // PURPOSE: Default change detection runs on EVERY browser event.
737
+ // OnPush only checks when @Input references change or signals
738
+ // update, dramatically reducing unnecessary checks.
739
+ // EXAMPLE:
740
+ // WRONG:
741
+ // @Component({
742
+ // selector: 'app-user-card',
743
+ // templateUrl: './user-card.component.html',
744
+ // })
745
+ // RIGHT:
746
+ // @Component({
747
+ // selector: 'app-user-card',
748
+ // templateUrl: './user-card.component.html',
749
+ // changeDetection: ChangeDetectionStrategy.OnPush,
750
+ // })
751
+ // ─────────────────────────────────────────
452
752
  {
453
753
  id: 'angular-onpush-change-detection',
454
754
  label: 'Consider OnPush change detection',
@@ -461,6 +761,21 @@ const angularRules: Rule[] = [
461
761
  category: 'Performance',
462
762
  },
463
763
 
764
+ // ─────────────────────────────────────────
765
+ // RULE: angular-trackby-for-loops
766
+ // ROLE: Enforce proper DOM identity tracking in loops
767
+ // PURPOSE: Without track, Angular destroys and recreates ALL DOM nodes
768
+ // on every list change. track item.id lets Angular reuse
769
+ // existing DOM nodes, improving performance 10-100x for lists.
770
+ // EXAMPLE:
771
+ // WRONG:
772
+ // <li *ngFor="let item of items">{{ item.name }}</li>
773
+ // @for (item of items) { <li>{{ item.name }}</li> }
774
+ // RIGHT:
775
+ // @for (item of items; track item.id) {
776
+ // <li>{{ item.name }}</li>
777
+ // }
778
+ // ─────────────────────────────────────────
464
779
  {
465
780
  id: 'angular-trackby-for-loops',
466
781
  label: 'Use trackBy with *ngFor or proper @for iteration',
@@ -473,6 +788,20 @@ const angularRules: Rule[] = [
473
788
  category: 'Performance',
474
789
  },
475
790
 
791
+ // ─────────────────────────────────────────
792
+ // RULE: angular-no-unused-imports
793
+ // ROLE: Flag unnecessary RxJS imports when Signals are available
794
+ // PURPOSE: Importing Observable/Subject when Signals could be used
795
+ // adds unnecessary bundle weight and cognitive overhead.
796
+ // Prefer signals for simple reactive state.
797
+ // EXAMPLE:
798
+ // WRONG:
799
+ // import { Observable, BehaviorSubject } from 'rxjs';
800
+ // data$ = new BehaviorSubject<string[]>([]);
801
+ // RIGHT:
802
+ // import { signal } from '@angular/core';
803
+ // data = signal<string[]>([]);
804
+ // ─────────────────────────────────────────
476
805
  {
477
806
  id: 'angular-no-unused-imports',
478
807
  label: 'Avoid importing RxJS operators not used',
@@ -487,6 +816,26 @@ const angularRules: Rule[] = [
487
816
 
488
817
  // ── NEW: Server-Side Rendering Hydration (Angular 17+) ────────────────
489
818
 
819
+ // ─────────────────────────────────────────
820
+ // RULE: angular-ssr-hydration
821
+ // ROLE: Block direct DOM access in SSR-compatible components
822
+ // PURPOSE: document.*, window.*, localStorage do not exist on the
823
+ // server. Direct usage crashes SSR pre-rendering. Always
824
+ // guard with isPlatformBrowser() or use afterNextRender().
825
+ // EXAMPLE:
826
+ // WRONG:
827
+ // ngOnInit() {
828
+ // const token = localStorage.getItem('token');
829
+ // document.title = 'My App';
830
+ // }
831
+ // RIGHT:
832
+ // constructor() {
833
+ // afterNextRender(() => {
834
+ // const token = localStorage.getItem('token');
835
+ // document.title = 'My App';
836
+ // });
837
+ // }
838
+ // ─────────────────────────────────────────
490
839
  {
491
840
  id: 'angular-ssr-hydration',
492
841
  label: 'Ensure SSR hydration compatibility',
@@ -499,6 +848,25 @@ const angularRules: Rule[] = [
499
848
  category: 'Server-Side Rendering',
500
849
  },
501
850
 
851
+ // ─────────────────────────────────────────
852
+ // RULE: angular-ssr-safe-initialization
853
+ // ROLE: Enforce browser API guards for SSR safety
854
+ // PURPOSE: Browser-only code (navigator, localStorage, window) must
855
+ // be wrapped in platform guards to prevent server crashes
856
+ // during pre-rendering.
857
+ // EXAMPLE:
858
+ // WRONG:
859
+ // const lang = navigator.language;
860
+ // window.scrollTo(0, 0);
861
+ // RIGHT:
862
+ // private platformId = inject(PLATFORM_ID);
863
+ // constructor() {
864
+ // if (isPlatformBrowser(this.platformId)) {
865
+ // const lang = navigator.language;
866
+ // window.scrollTo(0, 0);
867
+ // }
868
+ // }
869
+ // ─────────────────────────────────────────
502
870
  {
503
871
  id: 'angular-ssr-safe-initialization',
504
872
  label: 'Guard browser-only APIs in SSR',
@@ -526,6 +894,23 @@ const angularRules: Rule[] = [
526
894
  category: 'Server-Side Rendering',
527
895
  },
528
896
 
897
+ // ─────────────────────────────────────────
898
+ // RULE: angular-lazy-load-modules
899
+ // ROLE: Enforce lazy loading for route modules
900
+ // PURPOSE: Eagerly loading all routes increases the initial bundle
901
+ // size and slows Time-to-Interactive. Lazy loading defers
902
+ // feature code until the user navigates to it.
903
+ // EXAMPLE:
904
+ // WRONG:
905
+ // import { UsersComponent } from './users/users.component';
906
+ // { path: 'users', component: UsersComponent }
907
+ // RIGHT:
908
+ // {
909
+ // path: 'users',
910
+ // loadComponent: () => import('./users/users.component')
911
+ // .then(m => m.UsersComponent)
912
+ // }
913
+ // ─────────────────────────────────────────
529
914
  {
530
915
  id: 'angular-lazy-load-modules',
531
916
  label: 'Use lazy loading for routes',
@@ -533,13 +918,34 @@ const angularRules: Rule[] = [
533
918
  'Lazy load feature modules in routing to reduce initial bundle size and improve load time.',
534
919
  severity: 'info',
535
920
  fileExtensions: ['ts'],
536
- pattern: /loadChildren:\s*\(\)\s*=>\s*import\(['"]\.['"]\)/g,
921
+ pattern: /loadChildren:\s*\(\)\s*=>\s*import\(/g,
537
922
  applicableTo: ['angular'],
538
923
  category: 'Performance',
539
924
  },
540
925
 
541
926
  // ── Best Practices ─────────────────────────────────────────────────────
542
927
 
928
+ // ─────────────────────────────────────────
929
+ // RULE: angular-standalone-components
930
+ // ROLE: Enforce standalone component architecture
931
+ // PURPOSE: NgModules add indirection and make dependency tracking hard.
932
+ // Standalone components declare their own imports, are
933
+ // tree-shakeable, and are the recommended Angular architecture.
934
+ // EXAMPLE:
935
+ // WRONG:
936
+ // @NgModule({
937
+ // declarations: [UserComponent],
938
+ // imports: [CommonModule],
939
+ // })
940
+ // export class UserModule {}
941
+ // RIGHT:
942
+ // @Component({
943
+ // standalone: true,
944
+ // imports: [NgClass],
945
+ // selector: 'app-user',
946
+ // })
947
+ // export class UserComponent {}
948
+ // ─────────────────────────────────────────
543
949
  {
544
950
  id: 'angular-standalone-components',
545
951
  label: 'Prefer standalone components',
@@ -552,6 +958,24 @@ const angularRules: Rule[] = [
552
958
  category: 'Best Practices',
553
959
  },
554
960
 
961
+ // ─────────────────────────────────────────
962
+ // RULE: angular-strict-templates
963
+ // ROLE: Enforce strict template type checking
964
+ // PURPOSE: Without strictTemplates, Angular templates are essentially
965
+ // untyped — typos in property names, wrong types, and missing
966
+ // fields are silently ignored at compile time.
967
+ // EXAMPLE:
968
+ // WRONG (tsconfig.json):
969
+ // "angularCompilerOptions": {
970
+ // "strictTemplates": false
971
+ // }
972
+ // RIGHT (tsconfig.json):
973
+ // "angularCompilerOptions": {
974
+ // "strictTemplates": true,
975
+ // "strictInjectionParameters": true,
976
+ // "strictInputAccessModifiers": true
977
+ // }
978
+ // ─────────────────────────────────────────
555
979
  {
556
980
  id: 'angular-strict-templates',
557
981
  label: 'Enable strict template checking',
@@ -564,6 +988,23 @@ const angularRules: Rule[] = [
564
988
  category: 'TypeScript',
565
989
  },
566
990
 
991
+ // ─────────────────────────────────────────
992
+ // RULE: angular-defer-for-lazy
993
+ // ROLE: Recommend @defer for template-level lazy loading
994
+ // PURPOSE: @defer blocks let you lazy-load sections of a template
995
+ // (heavy charts, tables, modals) without component-level
996
+ // code splitting. Triggers include viewport, interaction, etc.
997
+ // EXAMPLE:
998
+ // WRONG:
999
+ // <app-heavy-chart [data]="chartData()"></app-heavy-chart>
1000
+ // <!-- Always loaded, even if below the fold -->
1001
+ // RIGHT:
1002
+ // @defer (on viewport) {
1003
+ // <app-heavy-chart [data]="chartData()"></app-heavy-chart>
1004
+ // } @placeholder {
1005
+ // <div class="h-64 animate-pulse bg-gray-200"></div>
1006
+ // }
1007
+ // ─────────────────────────────────────────
567
1008
  {
568
1009
  id: 'angular-defer-for-lazy',
569
1010
  label: 'Use @defer for lazy views',