@hatem427/code-guard-ci 3.3.0 → 3.4.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 (86) hide show
  1. package/config/angular.config.ts +29 -708
  2. package/config/guidelines.config.ts +5 -130
  3. package/config/nextjs.config.ts +27 -511
  4. package/config/react.config.ts +19 -614
  5. package/dist/config/angular.config.d.ts +5 -8
  6. package/dist/config/angular.config.d.ts.map +1 -1
  7. package/dist/config/angular.config.js +28 -666
  8. package/dist/config/angular.config.js.map +1 -1
  9. package/dist/config/guidelines.config.d.ts.map +1 -1
  10. package/dist/config/guidelines.config.js +5 -127
  11. package/dist/config/guidelines.config.js.map +1 -1
  12. package/dist/config/nextjs.config.d.ts +7 -9
  13. package/dist/config/nextjs.config.d.ts.map +1 -1
  14. package/dist/config/nextjs.config.js +26 -472
  15. package/dist/config/nextjs.config.js.map +1 -1
  16. package/dist/config/react.config.d.ts +4 -5
  17. package/dist/config/react.config.d.ts.map +1 -1
  18. package/dist/config/react.config.js +19 -586
  19. package/dist/config/react.config.js.map +1 -1
  20. package/dist/scripts/auto-fix.d.ts +0 -5
  21. package/dist/scripts/auto-fix.d.ts.map +1 -1
  22. package/dist/scripts/auto-fix.js +0 -5
  23. package/dist/scripts/auto-fix.js.map +1 -1
  24. package/dist/scripts/cli.js +211 -415
  25. package/dist/scripts/cli.js.map +1 -1
  26. package/dist/scripts/config-generators/ai-config-generator.d.ts.map +1 -1
  27. package/dist/scripts/config-generators/ai-config-generator.js +71 -15
  28. package/dist/scripts/config-generators/ai-config-generator.js.map +1 -1
  29. package/dist/scripts/config-generators/eslint-generator.d.ts.map +1 -1
  30. package/dist/scripts/config-generators/eslint-generator.js +13 -625
  31. package/dist/scripts/config-generators/eslint-generator.js.map +1 -1
  32. package/dist/scripts/config-generators/index.d.ts +0 -1
  33. package/dist/scripts/config-generators/index.d.ts.map +1 -1
  34. package/dist/scripts/config-generators/index.js +1 -5
  35. package/dist/scripts/config-generators/index.js.map +1 -1
  36. package/dist/scripts/config-generators/typescript-generator.d.ts.map +1 -1
  37. package/dist/scripts/config-generators/typescript-generator.js +0 -33
  38. package/dist/scripts/config-generators/typescript-generator.js.map +1 -1
  39. package/dist/scripts/config-generators/vscode-generator.d.ts.map +1 -1
  40. package/dist/scripts/config-generators/vscode-generator.js +28 -171
  41. package/dist/scripts/config-generators/vscode-generator.js.map +1 -1
  42. package/dist/scripts/generate-pr-checklist.d.ts +0 -5
  43. package/dist/scripts/generate-pr-checklist.d.ts.map +1 -1
  44. package/dist/scripts/generate-pr-checklist.js +1 -6
  45. package/dist/scripts/generate-pr-checklist.js.map +1 -1
  46. package/dist/scripts/postinstall.js +0 -38
  47. package/dist/scripts/postinstall.js.map +1 -1
  48. package/dist/scripts/precommit-check.d.ts +0 -5
  49. package/dist/scripts/precommit-check.d.ts.map +1 -1
  50. package/dist/scripts/precommit-check.js +92 -149
  51. package/dist/scripts/precommit-check.js.map +1 -1
  52. package/dist/scripts/utils/naming-validator.d.ts.map +1 -1
  53. package/dist/scripts/utils/naming-validator.js +2 -96
  54. package/dist/scripts/utils/naming-validator.js.map +1 -1
  55. package/dist/scripts/utils/project-detector.d.ts +9 -12
  56. package/dist/scripts/utils/project-detector.d.ts.map +1 -1
  57. package/dist/scripts/utils/project-detector.js +11 -63
  58. package/dist/scripts/utils/project-detector.js.map +1 -1
  59. package/dist/scripts/utils/report-generator.js +5 -17
  60. package/dist/scripts/utils/report-generator.js.map +1 -1
  61. package/dist/scripts/utils/structure-validator.d.ts.map +1 -1
  62. package/dist/scripts/utils/structure-validator.js +0 -50
  63. package/dist/scripts/utils/structure-validator.js.map +1 -1
  64. package/package.json +1 -12
  65. package/scripts/auto-fix.ts +0 -5
  66. package/scripts/cli.ts +226 -451
  67. package/scripts/config-generators/ai-config-generator.ts +78 -28
  68. package/scripts/config-generators/eslint-generator.ts +7 -621
  69. package/scripts/config-generators/index.ts +0 -1
  70. package/scripts/config-generators/typescript-generator.ts +0 -36
  71. package/scripts/config-generators/vscode-generator.ts +40 -178
  72. package/scripts/generate-pr-checklist.ts +1 -6
  73. package/scripts/postinstall.ts +0 -38
  74. package/scripts/precommit-check.ts +113 -278
  75. package/scripts/utils/naming-validator.ts +2 -104
  76. package/scripts/utils/project-detector.ts +11 -78
  77. package/scripts/utils/report-generator.ts +5 -19
  78. package/scripts/utils/structure-validator.ts +0 -54
  79. package/config/fastify.config.ts +0 -326
  80. package/config/hono.config.ts +0 -331
  81. package/config/nestjs.config.ts +0 -500
  82. package/config/python.config.ts +0 -512
  83. package/templates/feature-doc-api.md +0 -101
  84. package/templates/feature-doc-backend.md +0 -114
  85. package/templates/feature-doc-service.md +0 -113
  86. package/templates/feature-doc-ui.md +0 -91
@@ -1,133 +1,20 @@
1
1
  /**
2
2
  * ============================================================================
3
- * angular.config.ts — Angular-specific coding rules (v21)
3
+ * angular.config.ts — Angular-specific coding rules
4
4
  * ============================================================================
5
5
  *
6
6
  * Registers rules that only apply to Angular projects:
7
- * - New control flow syntax (@if, @for, @switch, @let)
8
- * - Signals and computed() patterns
9
- * - Effect management
10
- * - input(), output(), model() functions
11
- * - Zoneless change detection
12
- * - Server-side rendering (hydration)
13
- * - RxJS best practices
7
+ * - ngOnInit misuse
8
+ * - Deprecated APIs (::ng-deep, CommonModule, ngClass, ngStyle)
9
+ * - Function calls in templates
10
+ * - RxJS best practices (async pipe, takeUntilDestroyed, signals)
14
11
  */
15
12
 
16
13
  import { registerRules, Rule } from './guidelines.config';
17
14
 
18
15
  const angularRules: Rule[] = [
19
- // ── NEW: Control Flow Syntax (Angular 17+) ────────────────────────────
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
- // ─────────────────────────────────────────
39
- {
40
- id: 'angular-use-new-control-flow',
41
- label: 'Use @if, @for, @switch instead of *ngIf, *ngFor',
42
- description:
43
- 'Angular 17+ introduces new control flow syntax: @if, @for, @switch, @let. They are more performant and syntactically cleaner than structural directives.',
44
- severity: 'warning',
45
- fileExtensions: ['html'],
46
- pattern: null,
47
- customCheck: (file) => {
48
- const violations: Array<{ line: number | null; message: string }> = [];
49
- const lines = file.lines;
50
-
51
- for (let i = 0; i < lines.length; i++) {
52
- if (/\*ngIf|\*ngFor|\*ngSwitch/.test(lines[i])) {
53
- violations.push({
54
- line: i + 1,
55
- message:
56
- 'Found structural directive. Use new Angular control flow syntax for better performance and readability.',
57
- });
58
- }
59
- }
60
- return violations;
61
- },
62
- applicableTo: ['angular'],
63
- category: 'Template Syntax',
64
- },
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
- // ─────────────────────────────────────────
81
- {
82
- id: 'angular-use-let-syntax',
83
- label: 'Use @let for template variables',
84
- description:
85
- 'Angular 17+ @let syntax enables simple variable binding in templates without getters. Example: @let myVar = expression;',
86
- severity: 'info',
87
- fileExtensions: ['html'],
88
- pattern: null,
89
- customCheck: (file) => {
90
- // This is informational - flag templates with complex expressions that could use @let
91
- const violations: Array<{ line: number | null; message: string }> = [];
92
- const hasComplexExpr = /\{\{\s*[\w\.]+\s*\?\s*[\w\.]+\s*:\s*[\w\.]+\s*\}\}/g.test(file.content);
93
- if (hasComplexExpr && !file.content.includes('@let')) {
94
- violations.push({
95
- line: null,
96
- message: 'Consider using @let syntax to simplify complex template expressions.',
97
- });
98
- }
99
- return violations;
100
- },
101
- applicableTo: ['angular'],
102
- category: 'Template Syntax',
103
- },
104
-
105
16
  // ── Lifecycle ──────────────────────────────────────────────────────────
106
17
 
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
- // ─────────────────────────────────────────
131
18
  {
132
19
  id: 'angular-no-ngoninit',
133
20
  label: 'Avoid ngOnInit for simple initialization',
@@ -142,20 +29,6 @@ const angularRules: Rule[] = [
142
29
 
143
30
  // ── Deprecated APIs ────────────────────────────────────────────────────
144
31
 
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
- // ─────────────────────────────────────────
159
32
  {
160
33
  id: 'angular-no-ng-deep',
161
34
  label: 'No ::ng-deep',
@@ -168,25 +41,6 @@ const angularRules: Rule[] = [
168
41
  category: 'Angular Deprecated APIs',
169
42
  },
170
43
 
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
- // ─────────────────────────────────────────
190
44
  {
191
45
  id: 'angular-no-common-module',
192
46
  label: 'No CommonModule import in standalone components',
@@ -199,20 +53,6 @@ const angularRules: Rule[] = [
199
53
  category: 'Angular Deprecated APIs',
200
54
  },
201
55
 
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
- // ─────────────────────────────────────────
216
56
  {
217
57
  id: 'angular-no-ngclass',
218
58
  label: 'No [ngClass] — use [class] binding or Tailwind',
@@ -225,20 +65,6 @@ const angularRules: Rule[] = [
225
65
  category: 'Angular Deprecated APIs',
226
66
  },
227
67
 
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
- // ─────────────────────────────────────────
242
68
  {
243
69
  id: 'angular-no-ngstyle',
244
70
  label: 'No [ngStyle] — use [style] binding or Tailwind',
@@ -253,22 +79,6 @@ const angularRules: Rule[] = [
253
79
 
254
80
  // ── Template best practices ────────────────────────────────────────────
255
81
 
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
- // ─────────────────────────────────────────
272
82
  {
273
83
  id: 'angular-no-function-in-template',
274
84
  label: 'No function calls in templates',
@@ -280,6 +90,16 @@ const angularRules: Rule[] = [
280
90
  customCheck: (file) => {
281
91
  const violations: Array<{ line: number | null; message: string }> = [];
282
92
 
93
+ /**
94
+ * Regex explanation:
95
+ * Match patterns like {{ someFunc() }} or (click)="handler()" but EXCLUDE:
96
+ * - Event handlers: (click)="...", (submit)="...", on*="..."
97
+ * - Known safe pipes: | async, | date, | json, etc.
98
+ * - Structural directives: *ngIf, *ngFor, @if, @for
99
+ *
100
+ * We look for interpolation bindings {{ expr() }} and property bindings [prop]="expr()"
101
+ * that contain function calls which are NOT event bindings.
102
+ */
283
103
  const interpolationRegex = /\{\{[^}]*\w+\s*\([^)]*\)[^}]*\}\}/g;
284
104
  const propertyBindingRegex = /\[(?!click|submit|keyup|keydown|change|input|focus|blur)\w+\]\s*=\s*"[^"]*\w+\s*\([^)]*\)[^"]*"/g;
285
105
 
@@ -289,6 +109,7 @@ const angularRules: Rule[] = [
289
109
  // Check interpolation bindings {{ func() }}
290
110
  interpolationRegex.lastIndex = 0;
291
111
  if (interpolationRegex.test(line)) {
112
+ // Exclude pipe usages like {{ value | pipeName }}
292
113
  const trimmed = line.trim();
293
114
  if (!trimmed.includes(' | ')) {
294
115
  violations.push({
@@ -316,24 +137,6 @@ const angularRules: Rule[] = [
316
137
 
317
138
  // ── RxJS best practices ────────────────────────────────────────────────
318
139
 
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
- // ─────────────────────────────────────────
337
140
  {
338
141
  id: 'angular-use-async-pipe',
339
142
  label: 'Use async pipe or toSignal() instead of manual subscribe',
@@ -346,26 +149,6 @@ const angularRules: Rule[] = [
346
149
  category: 'RxJS',
347
150
  },
348
151
 
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
- // ─────────────────────────────────────────
369
152
  {
370
153
  id: 'angular-use-takeuntildestroyed',
371
154
  label: 'Use takeUntilDestroyed() for subscriptions',
@@ -377,6 +160,7 @@ const angularRules: Rule[] = [
377
160
  customCheck: (file) => {
378
161
  const violations: Array<{ line: number | null; message: string }> = [];
379
162
 
163
+ // Only check Angular component/directive/service files
380
164
  if (
381
165
  !file.content.includes('@Component') &&
382
166
  !file.content.includes('@Directive') &&
@@ -403,20 +187,6 @@ const angularRules: Rule[] = [
403
187
  category: 'RxJS',
404
188
  },
405
189
 
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
- // ─────────────────────────────────────────
420
190
  {
421
191
  id: 'angular-prefer-signals',
422
192
  label: 'Consider using Angular Signals',
@@ -426,6 +196,7 @@ const angularRules: Rule[] = [
426
196
  fileExtensions: ['ts'],
427
197
  pattern: null,
428
198
  customCheck: (file) => {
199
+ // Only flag if the component uses BehaviorSubject but no signals
429
200
  const hasBehaviorSubject = file.content.includes('BehaviorSubject');
430
201
  const hasSignal = file.content.includes('signal(') || file.content.includes('computed(');
431
202
 
@@ -445,28 +216,8 @@ const angularRules: Rule[] = [
445
216
  category: 'Angular Signals',
446
217
  },
447
218
 
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
- // ─────────────────────────────────────────
219
+ // ── Dependency Injection ────────────────────────────────────────────────
220
+
470
221
  {
471
222
  id: 'angular-prefer-inject',
472
223
  label: 'Use inject() instead of constructor injection',
@@ -478,6 +229,7 @@ const angularRules: Rule[] = [
478
229
  customCheck: (file) => {
479
230
  const violations: Array<{ line: number | null; message: string }> = [];
480
231
 
232
+ // Only check Angular component/directive/service files
481
233
  if (
482
234
  !file.content.includes('@Component') &&
483
235
  !file.content.includes('@Directive') &&
@@ -486,9 +238,12 @@ const angularRules: Rule[] = [
486
238
  return [];
487
239
  }
488
240
 
241
+ // Check for constructor with dependency injection parameters
242
+ // Matches: constructor(private service: Type) or constructor(public readonly http: HttpClient)
489
243
  const constructorPattern = /constructor\s*\(\s*(?:private|public|protected|readonly|\s)+\w+\s*:\s*\w+/;
490
244
 
491
245
  if (constructorPattern.test(file.content)) {
246
+ // Find the line number
492
247
  for (let i = 0; i < file.lines.length; i++) {
493
248
  if (constructorPattern.test(file.lines[i])) {
494
249
  violations.push({
@@ -507,248 +262,8 @@ const angularRules: Rule[] = [
507
262
  category: 'Angular Dependency Injection',
508
263
  },
509
264
 
510
- // ── NEW: Signals & Signal-based Features (Angular 16+, essential in v21) ────
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
- // ─────────────────────────────────────────
528
- {
529
- id: 'angular-use-signals',
530
- label: 'Use Signals for reactive state',
531
- description:
532
- 'Angular Signals replace RxJS in many cases. Use signal(), computed(), and effect() for simpler, more performant reactivity. Consider migrating from BehaviorSubject.',
533
- severity: 'warning',
534
- fileExtensions: ['ts'],
535
- pattern: null,
536
- customCheck: (file) => {
537
- const violations: Array<{ line: number | null; message: string }> = [];
538
-
539
- if (
540
- !file.content.includes('@Component') &&
541
- !file.content.includes('@Injectable') &&
542
- !file.content.includes('export')
543
- ) {
544
- return [];
545
- }
546
-
547
- const hasBehaviorSubject = /BehaviorSubject|Subject/.test(file.content);
548
- const hasSignal = /signal\(|computed\(|effect\(/.test(file.content);
549
- const hasRxJS = /rxjs|Observable<|\.pipe\(/.test(file.content);
550
-
551
- if (hasBehaviorSubject && !hasSignal && !hasRxJS) {
552
- violations.push({
553
- line: null,
554
- message:
555
- 'BehaviorSubject detected. Migrate to Angular Signals for cleaner reactive state.\n\nWhy: Signals are simpler, more performant, and better integrated with Angular\'s change detection.\n\n- import { BehaviorSubject } from "rxjs";\n- counter = new BehaviorSubject(0);\n- counter.next(counter.value + 1);\n\n+ import { signal } from "@angular/core";\n+ counter = signal(0);\n+ counter.update(val => val + 1);',
556
- });
557
- }
558
-
559
- return violations;
560
- },
561
- applicableTo: ['angular'],
562
- category: 'Angular Signals',
563
- },
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
- // ─────────────────────────────────────────
581
- {
582
- id: 'angular-use-computed',
583
- label: 'Use computed() for derived state',
584
- description:
585
- 'Instead of getters or pipes, use computed() from @angular/core to create derived signal state. Better type safety and automatic memoization.',
586
- severity: 'info',
587
- fileExtensions: ['ts'],
588
- pattern: null,
589
- customCheck: (file) => {
590
- const violations: Array<{ line: number | null; message: string }> = [];
591
-
592
- if (/get\s+\w+\s*\(\s*\).*\{[\s\S]*?return.*signal|this\.\w+\(\)/.test(file.content)) {
593
- violations.push({
594
- line: null,
595
- message:
596
- 'Found getter function. Use computed() from signals for automatic memoization and better performance.',
597
- });
598
- }
599
- return violations;
600
- },
601
- applicableTo: ['angular'],
602
- category: 'Angular Signals',
603
- },
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
- // ─────────────────────────────────────────
621
- {
622
- id: 'angular-use-effect',
623
- label: 'Use effect() for side effects instead of ngOnInit',
624
- description:
625
- 'Use effect() from @angular/core for side effects that depend on signals. It automatically tracks dependencies and runs when signals change.',
626
- severity: 'info',
627
- fileExtensions: ['ts'],
628
- pattern: null,
629
- customCheck: (file) => {
630
- const violations: Array<{ line: number | null; message: string }> = [];
631
-
632
- if (/ngOnInit\s*\(\)[\s\S]*?\{[\s\S]*?(?:subscribe|addEventListener|setTimeout)/.test(file.content)) {
633
- violations.push({
634
- line: null,
635
- message:
636
- 'Found ngOnInit with side effects. Use effect() from signals for cleaner dependency tracking.',
637
- });
638
- }
639
- return violations;
640
- },
641
- applicableTo: ['angular'],
642
- category: 'Angular Signals',
643
- },
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
- // ─────────────────────────────────────────
661
- {
662
- id: 'angular-use-input-output-functions',
663
- label: 'Use input(), output(), model() functions',
664
- description:
665
- 'Angular 17+ supports input(), output(), and model() functions instead of @Input/@Output decorators. They provide better null-safety and type inference.',
666
- severity: 'warning',
667
- fileExtensions: ['ts'],
668
- pattern: /@Input|@Output/g,
669
- applicableTo: ['angular'],
670
- category: 'Angular Components',
671
- },
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
- // ─────────────────────────────────────────
689
- {
690
- id: 'angular-signal-based-viewchild',
691
- label: 'Use signal-based @ViewChild where applicable',
692
- description:
693
- 'For @ViewChild queries, consider using signal queries with viewChild() function for better type safety and automatic change detection.',
694
- severity: 'info',
695
- fileExtensions: ['ts'],
696
- pattern: /@ViewChild\s*\(|@ViewChildren\s*\(/g,
697
- applicableTo: ['angular'],
698
- category: 'Angular Templates',
699
- },
700
-
701
- // ── NEW: Zoneless Change Detection (Angular 18+, recommended in v21) ───
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
- // ─────────────────────────────────────────
719
- {
720
- id: 'angular-consider-zoneless',
721
- label: 'Consider zoneless change detection',
722
- description:
723
- 'Angular 18+ supports zoneless applications, which improves performance by removing Zone.js. Set "zone": "none" in component config when possible.',
724
- severity: 'info',
725
- fileExtensions: ['ts'],
726
- pattern: null,
727
- applicableTo: ['angular'],
728
- category: 'Performance',
729
- },
730
-
731
265
  // ── Performance Rules ──────────────────────────────────────────────────
732
266
 
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
- // ─────────────────────────────────────────
752
267
  {
753
268
  id: 'angular-onpush-change-detection',
754
269
  label: 'Consider OnPush change detection',
@@ -761,156 +276,18 @@ const angularRules: Rule[] = [
761
276
  category: 'Performance',
762
277
  },
763
278
 
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
- // ─────────────────────────────────────────
779
279
  {
780
- id: 'angular-trackby-for-loops',
781
- label: 'Use trackBy with *ngFor or proper @for iteration',
280
+ id: 'angular-trackby-ngfor',
281
+ label: 'Use trackBy with *ngFor',
782
282
  description:
783
- 'With *ngFor, always provide trackBy. With @for, use @for (item of items; track item.id) for proper identity tracking.',
283
+ 'Always provide trackBy function for *ngFor to prevent unnecessary DOM re-renders when data changes.',
784
284
  severity: 'warning',
785
285
  fileExtensions: ['html'],
786
- pattern: /\*ngFor\s*=\s*"[^"]*"(?![^<]*trackBy)|@for\s*\([^)]*\)\s*\{(?![^}]*track)/g,
286
+ pattern: /\*ngFor\s*=\s*"[^"]*"(?![^<]*trackBy)/g,
787
287
  applicableTo: ['angular'],
788
288
  category: 'Performance',
789
289
  },
790
290
 
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
- // ─────────────────────────────────────────
805
- {
806
- id: 'angular-no-unused-imports',
807
- label: 'Avoid importing RxJS operators not used',
808
- description:
809
- 'Remove unused RxJS imports (Observable, Subject). Prefer Signals if you are not heavily using RxJS pipelines.',
810
- severity: 'info',
811
- fileExtensions: ['ts'],
812
- pattern: /import\s*\{[^}]*Observable[^}]*\}\s*from\s+['"]rxjs['"]/g,
813
- applicableTo: ['angular'],
814
- category: 'Best Practices',
815
- },
816
-
817
- // ── NEW: Server-Side Rendering Hydration (Angular 17+) ────────────────
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
- // ─────────────────────────────────────────
839
- {
840
- id: 'angular-ssr-hydration',
841
- label: 'Ensure SSR hydration compatibility',
842
- description:
843
- 'When using SSR, ensure components are hydration-compatible. Avoid direct DOM manipulation (document.*, window.*) in server contexts.',
844
- severity: 'warning',
845
- fileExtensions: ['ts'],
846
- pattern: /(?:document\.|window\.|localStorage|sessionStorage)/g,
847
- applicableTo: ['angular'],
848
- category: 'Server-Side Rendering',
849
- },
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
- // ─────────────────────────────────────────
870
- {
871
- id: 'angular-ssr-safe-initialization',
872
- label: 'Guard browser-only APIs in SSR',
873
- description:
874
- 'Wrap browser-only code in isPlatformBrowser() or effect() guards to prevent errors during SSR pre-rendering.',
875
- severity: 'warning',
876
- fileExtensions: ['ts'],
877
- pattern: null,
878
- customCheck: (file) => {
879
- const violations: Array<{ line: number | null; message: string }> = [];
880
-
881
- const hasBrowserAPI = /(?:document\.|window\.|localStorage|navigator|sessionStorage)/.test(file.content);
882
- const hasGuard = /isPlatformBrowser|platformBrowser|isBrowser/.test(file.content);
883
-
884
- if (hasBrowserAPI && !hasGuard && !file.relativePath.includes('client')) {
885
- violations.push({
886
- line: null,
887
- message:
888
- 'Browser-only API detected without SSR guard. Wrap in isPlatformBrowser() or use effect() for safe execution.',
889
- });
890
- }
891
- return violations;
892
- },
893
- applicableTo: ['angular'],
894
- category: 'Server-Side Rendering',
895
- },
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
- // ─────────────────────────────────────────
914
291
  {
915
292
  id: 'angular-lazy-load-modules',
916
293
  label: 'Use lazy loading for routes',
@@ -918,34 +295,13 @@ const angularRules: Rule[] = [
918
295
  'Lazy load feature modules in routing to reduce initial bundle size and improve load time.',
919
296
  severity: 'info',
920
297
  fileExtensions: ['ts'],
921
- pattern: /loadChildren:\s*\(\)\s*=>\s*import\(/g,
298
+ pattern: /loadChildren:\s*\(\)\s*=>\s*import\(['"]\.['"]\)/g,
922
299
  applicableTo: ['angular'],
923
300
  category: 'Performance',
924
301
  },
925
302
 
926
303
  // ── Best Practices ─────────────────────────────────────────────────────
927
304
 
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
- // ─────────────────────────────────────────
949
305
  {
950
306
  id: 'angular-standalone-components',
951
307
  label: 'Prefer standalone components',
@@ -958,24 +314,6 @@ const angularRules: Rule[] = [
958
314
  category: 'Best Practices',
959
315
  },
960
316
 
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
- // ─────────────────────────────────────────
979
317
  {
980
318
  id: 'angular-strict-templates',
981
319
  label: 'Enable strict template checking',
@@ -988,23 +326,6 @@ const angularRules: Rule[] = [
988
326
  category: 'TypeScript',
989
327
  },
990
328
 
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
- // ─────────────────────────────────────────
1008
329
  {
1009
330
  id: 'angular-defer-for-lazy',
1010
331
  label: 'Use @defer for lazy views',