angular-doctor 1.1.3 → 1.3.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.
package/dist/index.d.mts CHANGED
@@ -4,9 +4,13 @@ interface ProjectInfo {
4
4
  rootDirectory: string;
5
5
  projectName: string;
6
6
  angularVersion: string | null;
7
+ angularMajorVersion: number | null;
7
8
  framework: AngularFramework;
8
9
  hasTypeScript: boolean;
9
10
  hasStandaloneComponents: boolean;
11
+ hasNgRx: boolean;
12
+ hasAngularMaterial: boolean;
13
+ hasSignals: boolean;
10
14
  sourceFileCount: number;
11
15
  }
12
16
  interface Diagnostic {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/utils/get-diff-files.ts","../src/index.ts"],"mappings":";KAAY,gBAAA;AAAA,UAQK,WAAA;EACf,aAAA;EACA,WAAA;EACA,cAAA;EACA,SAAA,EAAW,gBAAA;EACX,aAAA;EACA,uBAAA;EACA,eAAA;AAAA;AAAA,UAGe,UAAA;EACf,QAAA;EACA,MAAA;EACA,IAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;EACA,IAAA;EACA,MAAA;EACA,QAAA;EACA,MAAA;AAAA;AAAA,UAWe,WAAA;EACf,KAAA;EACA,KAAA;AAAA;AAAA,UA4Ce,QAAA;EACf,aAAA;EACA,UAAA;EACA,YAAA;EACA,gBAAA;AAAA;AAAA,UAGe,yBAAA;EACf,KAAA;EACA,KAAA;AAAA;AAAA,UAGe,mBAAA;EACf,MAAA,GAAS,yBAAA;EACT,IAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;EACA,IAAA;AAAA;;;cC7DW,iBAAA,GAAqB,KAAA;AAAA,cAGrB,WAAA,GAAe,SAAA,UAAmB,kBAAA,cAA8B,QAAA;;;UChC5D,eAAA;EACf,IAAA;EACA,QAAA;EACA,YAAA;AAAA;AAAA,UAGe,cAAA;EACf,WAAA,EAAa,UAAA;EACb,KAAA,EAAO,WAAA;EACP,OAAA,EAAS,WAAA;EACT,mBAAA;AAAA;AAAA,cAGW,QAAA,GACX,SAAA,UACA,OAAA,GAAS,eAAA,KACR,OAAA,CAAQ,cAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/utils/get-diff-files.ts","../src/index.ts"],"mappings":";KAAY,gBAAA;AAAA,UAQK,WAAA;EACf,aAAA;EACA,WAAA;EACA,cAAA;EACA,mBAAA;EACA,SAAA,EAAW,gBAAA;EACX,aAAA;EACA,uBAAA;EACA,OAAA;EACA,kBAAA;EACA,UAAA;EACA,eAAA;AAAA;AAAA,UAGe,UAAA;EACf,QAAA;EACA,MAAA;EACA,IAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;EACA,IAAA;EACA,MAAA;EACA,QAAA;EACA,MAAA;AAAA;AAAA,UAWe,WAAA;EACf,KAAA;EACA,KAAA;AAAA;AAAA,UA+Ce,QAAA;EACf,aAAA;EACA,UAAA;EACA,YAAA;EACA,gBAAA;AAAA;AAAA,UAGe,yBAAA;EACf,KAAA;EACA,KAAA;AAAA;AAAA,UAGe,mBAAA;EACf,MAAA,GAAS,yBAAA;EACT,IAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;EACA,IAAA;AAAA;;;cCpEW,iBAAA,GAAqB,KAAA;AAAA,cAGrB,WAAA,GAAe,SAAA,UAAmB,kBAAA,cAA8B,QAAA;;;UChC5D,eAAA;EACf,IAAA;EACA,QAAA;EACA,YAAA;AAAA;AAAA,UAGe,cAAA;EACf,WAAA,EAAa,UAAA;EACb,KAAA,EAAO,WAAA;EACP,OAAA,EAAS,WAAA;EACT,mBAAA;AAAA;AAAA,cAGW,QAAA,GACX,SAAA,UACA,OAAA,GAAS,eAAA,KACR,OAAA,CAAQ,cAAA"}
package/dist/index.mjs CHANGED
@@ -103,12 +103,26 @@ const detectFramework = (dependencies) => {
103
103
  return "unknown";
104
104
  };
105
105
  const detectAngularVersion = (dependencies) => dependencies["@angular/core"] ?? null;
106
+ const detectAngularMajorVersion = (version) => {
107
+ if (!version) return null;
108
+ const major = parseInt(version.match(/\d+/)?.[0] ?? "", 10);
109
+ return isNaN(major) ? null : major;
110
+ };
106
111
  const detectStandaloneComponents = (packageJson) => {
107
112
  const angularVersion = collectAllDependencies(packageJson)["@angular/core"];
108
113
  if (!angularVersion) return false;
109
114
  const majorVersion = parseInt(angularVersion.match(/\d+/)?.[0] ?? "", 10);
110
115
  return !isNaN(majorVersion) && majorVersion >= 14;
111
116
  };
117
+ const detectNgRxPackages = (dependencies) => {
118
+ return Object.keys(dependencies).some((dep) => dep.startsWith("@ngrx/") || dep === "@ngrx/store");
119
+ };
120
+ const detectAngularMaterial = (dependencies) => {
121
+ return Object.keys(dependencies).includes("@angular/material");
122
+ };
123
+ const detectSignals = (angularMajorVersion) => {
124
+ return angularMajorVersion !== null && angularMajorVersion >= 17;
125
+ };
112
126
  const countSourceFiles = (rootDirectory) => {
113
127
  const result = spawnSync("git", [
114
128
  "ls-files",
@@ -142,9 +156,13 @@ const discoverProject = (directory) => {
142
156
  const packageJson = readPackageJson(path.join(packageJsonDir, "package.json"));
143
157
  const allDeps = collectAllDependencies(packageJson);
144
158
  const angularVersion = detectAngularVersion(allDeps);
159
+ const angularMajorVersion = detectAngularMajorVersion(angularVersion);
145
160
  const framework = detectFramework(allDeps);
146
161
  const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json")) || fs.existsSync(path.join(packageJsonDir, "tsconfig.json"));
147
162
  const hasStandaloneComponents = detectStandaloneComponents(packageJson);
163
+ const hasNgRx = detectNgRxPackages(allDeps);
164
+ const hasAngularMaterial = detectAngularMaterial(allDeps);
165
+ const hasSignals = detectSignals(angularMajorVersion);
148
166
  const sourceFileCount = countSourceFiles(directory);
149
167
  const angularJsonPath = path.join(packageJsonDir, "angular.json");
150
168
  let projectName = packageJson.name ?? path.basename(directory);
@@ -153,9 +171,13 @@ const discoverProject = (directory) => {
153
171
  rootDirectory: directory,
154
172
  projectName,
155
173
  angularVersion,
174
+ angularMajorVersion,
156
175
  framework,
157
176
  hasTypeScript,
158
177
  hasStandaloneComponents,
178
+ hasNgRx,
179
+ hasAngularMaterial,
180
+ hasSignals,
159
181
  sourceFileCount
160
182
  };
161
183
  };
@@ -194,30 +216,88 @@ const loadConfig = (rootDirectory) => {
194
216
  //#region src/utils/run-eslint.ts
195
217
  const RULE_CATEGORY_MAP = {
196
218
  "@angular-eslint/component-class-suffix": "Components",
219
+ "@angular-eslint/component-max-inline-declarations": "Performance",
220
+ "@angular-eslint/component-selector": "Components",
197
221
  "@angular-eslint/directive-class-suffix": "Components",
222
+ "@angular-eslint/directive-selector": "Components",
198
223
  "@angular-eslint/pipe-prefix": "Components",
199
224
  "@angular-eslint/use-pipe-transform-interface": "Components",
200
225
  "@angular-eslint/no-empty-lifecycle-method": "Components",
201
226
  "@angular-eslint/use-lifecycle-interface": "Components",
202
227
  "@angular-eslint/consistent-component-styles": "Components",
228
+ "@angular-eslint/sort-lifecycle-methods": "Components",
229
+ "@angular-eslint/use-component-selector": "Components",
203
230
  "@angular-eslint/prefer-on-push-component-change-detection": "Performance",
204
231
  "@angular-eslint/no-output-native": "Performance",
232
+ "@angular-eslint/no-pipe-impure": "Performance",
205
233
  "@angular-eslint/no-conflicting-lifecycle": "Correctness",
206
234
  "@angular-eslint/contextual-lifecycle": "Correctness",
235
+ "@angular-eslint/contextual-decorator": "Correctness",
236
+ "@angular-eslint/no-async-lifecycle-method": "Correctness",
237
+ "@angular-eslint/no-duplicates-in-metadata-arrays": "Correctness",
238
+ "@angular-eslint/no-lifecycle-call": "Correctness",
239
+ "@angular-eslint/require-lifecycle-on-prototype": "Correctness",
240
+ "@angular-eslint/no-attribute-decorator": "Architecture",
207
241
  "@angular-eslint/no-forward-ref": "Architecture",
208
242
  "@angular-eslint/no-input-rename": "Architecture",
209
243
  "@angular-eslint/no-output-rename": "Architecture",
210
244
  "@angular-eslint/no-inputs-metadata-property": "Architecture",
211
245
  "@angular-eslint/no-outputs-metadata-property": "Architecture",
246
+ "@angular-eslint/no-queries-metadata-property": "Architecture",
212
247
  "@angular-eslint/prefer-standalone": "Architecture",
248
+ "@angular-eslint/prefer-host-metadata-property": "Architecture",
249
+ "@angular-eslint/prefer-inject": "Architecture",
250
+ "@angular-eslint/prefer-output-emitter-ref": "Architecture",
251
+ "@angular-eslint/prefer-output-readonly": "Architecture",
252
+ "@angular-eslint/use-component-view-encapsulation": "Architecture",
253
+ "@angular-eslint/use-injectable-provided-in": "Architecture",
254
+ "@angular-eslint/no-input-prefix": "Architecture",
255
+ "@angular-eslint/no-output-on-prefix": "Architecture",
256
+ "@angular-eslint/relative-url-prefix": "Security",
257
+ "@angular-eslint/prefer-signals": "Signals",
258
+ "@angular-eslint/prefer-signal-model": "Signals",
259
+ "@angular-eslint/no-uncalled-signals": "Signals",
260
+ "@angular-eslint/template/accessibility": "Accessibility",
261
+ "@angular-eslint/template/alt-text": "Accessibility",
262
+ "@angular-eslint/template/click-events-have-key-events": "Accessibility",
263
+ "@angular-eslint/template/control-events-have-key-events": "Accessibility",
264
+ "@angular-eslint/template/elements-have-content": "Accessibility",
265
+ "@angular-eslint/template/interactive-supports-focus": "Accessibility",
266
+ "@angular-eslint/template/mouse-events-have-key-events": "Accessibility",
267
+ "@angular-eslint/template/no-any": "Accessibility",
268
+ "@angular-eslint/template/table-scope": "Accessibility",
269
+ "@angular-eslint/template/valid-aria": "Accessibility",
270
+ "@ngrx/contextual-action-creator": "NgRx",
271
+ "@ngrx/no-cyclic-action-creators": "NgRx",
272
+ "@ngrx/no-discrete-actions": "NgRx",
273
+ "@ngrx/no-effect-decorator": "NgRx",
274
+ "@ngrx/no-effect-decorator-and-creator": "NgRx",
275
+ "@ngrx/no-multiple-actions-in-effects": "NgRx",
276
+ "@ngrx/no-reordering-in-effect-reducers": "NgRx",
277
+ "@ngrx/no-typed-global-store": "NgRx",
278
+ "@ngrx/on-function-explicit-return-type": "NgRx",
279
+ "@ngrx/prefix-selectors-with-namespace": "NgRx",
280
+ "@ngrx/require-middleware-selector": "NgRx",
281
+ "@ngrx/select-style": "NgRx",
282
+ "@ngrx/use-consumer-selector": "NgRx",
283
+ "@angular/material/prefix-selector": "Material",
284
+ "@angular/material/no-conflicting-mixins": "Material",
213
285
  "@typescript-eslint/no-explicit-any": "TypeScript",
214
- "@typescript-eslint/no-unused-vars": "Dead Code"
286
+ "@typescript-eslint/no-unused-vars": "Dead Code",
287
+ "@typescript-eslint/sort-keys": "TypeScript"
215
288
  };
216
289
  const RULE_SEVERITY_MAP = {
217
290
  "@angular-eslint/no-conflicting-lifecycle": "error",
218
291
  "@angular-eslint/contextual-lifecycle": "error",
219
292
  "@angular-eslint/use-pipe-transform-interface": "error",
220
293
  "@angular-eslint/no-output-native": "error",
294
+ "@angular-eslint/no-async-lifecycle-method": "error",
295
+ "@angular-eslint/no-lifecycle-call": "error",
296
+ "@angular-eslint/no-duplicates-in-metadata-arrays": "error",
297
+ "@angular-eslint/require-lifecycle-on-prototype": "error",
298
+ "@angular-eslint/relative-url-prefix": "error",
299
+ "@angular-eslint/contextual-decorator": "error",
300
+ "@angular-eslint/no-uncalled-signals": "error",
221
301
  "@angular-eslint/component-class-suffix": "warning",
222
302
  "@angular-eslint/directive-class-suffix": "warning",
223
303
  "@angular-eslint/pipe-prefix": "warning",
@@ -231,8 +311,42 @@ const RULE_SEVERITY_MAP = {
231
311
  "@angular-eslint/no-inputs-metadata-property": "warning",
232
312
  "@angular-eslint/no-outputs-metadata-property": "warning",
233
313
  "@angular-eslint/prefer-standalone": "warning",
314
+ "@angular-eslint/component-selector": "warning",
315
+ "@angular-eslint/directive-selector": "warning",
316
+ "@angular-eslint/no-pipe-impure": "warning",
317
+ "@angular-eslint/no-attribute-decorator": "warning",
318
+ "@angular-eslint/no-queries-metadata-property": "warning",
319
+ "@angular-eslint/prefer-host-metadata-property": "warning",
320
+ "@angular-eslint/prefer-inject": "warning",
321
+ "@angular-eslint/prefer-output-emitter-ref": "warning",
322
+ "@angular-eslint/prefer-output-readonly": "warning",
323
+ "@angular-eslint/use-component-selector": "warning",
324
+ "@angular-eslint/use-component-view-encapsulation": "warning",
325
+ "@angular-eslint/use-injectable-provided-in": "warning",
326
+ "@angular-eslint/component-max-inline-declarations": "warning",
327
+ "@angular-eslint/sort-lifecycle-methods": "warning",
328
+ "@angular-eslint/no-input-prefix": "warning",
329
+ "@angular-eslint/no-output-on-prefix": "warning",
330
+ "@angular-eslint/prefer-signals": "warning",
331
+ "@angular-eslint/prefer-signal-model": "warning",
234
332
  "@typescript-eslint/no-explicit-any": "warning",
235
- "@typescript-eslint/no-unused-vars": "warning"
333
+ "@typescript-eslint/no-unused-vars": "warning",
334
+ "@typescript-eslint/sort-keys": "warning",
335
+ "@ngrx/contextual-action-creator": "warning",
336
+ "@ngrx/no-cyclic-action-creators": "error",
337
+ "@ngrx/no-discrete-actions": "warning",
338
+ "@ngrx/no-effect-decorator": "warning",
339
+ "@ngrx/no-effect-decorator-and-creator": "error",
340
+ "@ngrx/no-multiple-actions-in-effects": "error",
341
+ "@ngrx/no-reordering-in-effect-reducers": "error",
342
+ "@ngrx/no-typed-global-store": "error",
343
+ "@ngrx/on-function-explicit-return-type": "warning",
344
+ "@ngrx/prefix-selectors-with-namespace": "warning",
345
+ "@ngrx/require-middleware-selector": "error",
346
+ "@ngrx/select-style": "warning",
347
+ "@ngrx/use-consumer-selector": "warning",
348
+ "@angular/material/prefix-selector": "warning",
349
+ "@angular/material/no-conflicting-mixins": "error"
236
350
  };
237
351
  const RULE_MESSAGE_MAP = {
238
352
  "@angular-eslint/component-class-suffix": "Component class should end with 'Component'",
@@ -246,14 +360,55 @@ const RULE_MESSAGE_MAP = {
246
360
  "@angular-eslint/no-output-native": "Avoid shadowing native DOM events in output names",
247
361
  "@angular-eslint/no-conflicting-lifecycle": "Lifecycle hooks DoCheck and OnChanges cannot be used together",
248
362
  "@angular-eslint/contextual-lifecycle": "Lifecycle hook is not available in this context",
363
+ "@angular-eslint/contextual-decorator": "Use contextual decorator to specify injection context",
249
364
  "@angular-eslint/no-forward-ref": "Avoid using forwardRef — restructure to avoid circular dependency",
250
365
  "@angular-eslint/no-input-rename": "Avoid renaming directive inputs — use the property name as the binding name",
251
366
  "@angular-eslint/no-output-rename": "Avoid renaming directive outputs — use the property name as the binding name",
252
367
  "@angular-eslint/no-inputs-metadata-property": "Use @Input() decorator instead of inputs metadata property",
253
368
  "@angular-eslint/no-outputs-metadata-property": "Use @Output() decorator instead of outputs metadata property",
254
369
  "@angular-eslint/prefer-standalone": "Prefer standalone components over NgModule-based components",
370
+ "@angular-eslint/component-selector": "Component selector should follow naming convention",
371
+ "@angular-eslint/directive-selector": "Directive selector should follow naming convention",
372
+ "@angular-eslint/no-pipe-impure": "Avoid impure pipes — they run on every change detection cycle",
373
+ "@angular-eslint/no-async-lifecycle-method": "Avoid async lifecycle methods — use signals instead",
374
+ "@angular-eslint/no-duplicates-in-metadata-arrays": "Remove duplicate entries in decorator metadata arrays",
375
+ "@angular-eslint/no-lifecycle-call": "Don't call lifecycle method directly — let Angular call them",
376
+ "@angular-eslint/require-lifecycle-on-prototype": "Lifecycle methods should be declared on prototype",
377
+ "@angular-eslint/no-attribute-decorator": "Avoid @Attribute() — use @Input() for property bindings",
378
+ "@angular-eslint/no-queries-metadata-property": "Use decorator-based queries instead of metadata properties",
379
+ "@angular-eslint/prefer-host-metadata-property": "Prefer host metadata property over @Host decorator",
380
+ "@angular-eslint/prefer-inject": "Prefer inject() function over constructor dependency injection",
381
+ "@angular-eslint/prefer-output-emitter-ref": "Use EventEmitter with Output instead of Subject",
382
+ "@angular-eslint/prefer-output-readonly": "Mark outputs as readonly when possible",
383
+ "@angular-eslint/use-component-selector": "Components must have selector for proper encapsulation",
384
+ "@angular-eslint/use-component-view-encapsulation": "Specify view encapsulation strategy explicitly",
385
+ "@angular-eslint/use-injectable-provided-in": "Specify providedIn scope for @Injectable()",
386
+ "@angular-eslint/component-max-inline-declarations": "Too many inline declarations in component — extract to separate files",
387
+ "@angular-eslint/sort-lifecycle-methods": "Lifecycle methods should be declared in correct order",
388
+ "@angular-eslint/no-input-prefix": "Avoid prefix for input property names",
389
+ "@angular-eslint/no-output-on-prefix": "Avoid 'on' prefix for output event names",
390
+ "@angular-eslint/relative-url-prefix": "Use relative URL prefixes for better security",
391
+ "@angular-eslint/prefer-signals": "Prefer Angular signals over other reactive patterns",
392
+ "@angular-eslint/prefer-signal-model": "Prefer signal-based model for component state",
393
+ "@angular-eslint/no-uncalled-signals": "Signal getters must be called to access value",
255
394
  "@typescript-eslint/no-explicit-any": "Avoid 'any' type — use specific types for better type safety",
256
- "@typescript-eslint/no-unused-vars": "Remove unused variable declaration"
395
+ "@typescript-eslint/no-unused-vars": "Remove unused variable declaration",
396
+ "@typescript-eslint/sort-keys": "Sort object keys consistently",
397
+ "@ngrx/contextual-action-creator": "Use contextual action creators for typed actions",
398
+ "@ngrx/no-cyclic-action-creators": "Action creators should not reference each other cyclically",
399
+ "@ngrx/no-discrete-actions": "Use discrete actions instead of broad action types",
400
+ "@ngrx/no-effect-decorator": "Consider using functional effects instead of @Effect decorator",
401
+ "@ngrx/no-effect-decorator-and-creator": "Don't use both @Effect decorator and createEffect function",
402
+ "@ngrx/no-multiple-actions-in-effects": "Effects should dispatch a single action or none",
403
+ "@ngrx/no-reordering-in-effect-reducers": "Don't reorder actions in effect reducers",
404
+ "@ngrx/no-typed-global-store": "Use typed GlobalStore for better type safety",
405
+ "@ngrx/on-function-explicit-return-type": "Specify explicit return type for on() reducer functions",
406
+ "@ngrx/prefix-selectors-with-namespace": "Prefix selectors with feature namespace",
407
+ "@ngrx/require-middleware-selector": "Middleware must have selector for proper scoping",
408
+ "@ngrx/select-style": "Prefer selector functions over props in select",
409
+ "@ngrx/use-consumer-selector": "Use useSelector with selector function for proper memoization",
410
+ "@angular/material/prefix-selector": "Material components should use proper selector prefix",
411
+ "@angular/material/no-conflicting-mixins": "Avoid conflicting mixins in Material components"
257
412
  };
258
413
  const RULE_HELP_MAP = {
259
414
  "@angular-eslint/component-class-suffix": "Add 'Component' suffix: `export class UserProfileComponent { }`",
@@ -270,9 +425,31 @@ const RULE_HELP_MAP = {
270
425
  "@angular-eslint/no-outputs-metadata-property": "Use `@Output() myEvent = new EventEmitter()` instead of `outputs: ['myEvent']` in the decorator metadata",
271
426
  "@angular-eslint/prefer-standalone": "Add `standalone: true` to component: `@Component({ standalone: true, ... })`",
272
427
  "@typescript-eslint/no-explicit-any": "Replace `any` with a specific type or `unknown` if the type is truly unknown",
273
- "@typescript-eslint/no-unused-vars": "Remove the unused variable or prefix with `_` to indicate it's intentionally unused"
428
+ "@typescript-eslint/no-unused-vars": "Remove the unused variable or prefix with `_` to indicate it's intentionally unused",
429
+ "@typescript-eslint/sort-keys": "Sort object keys alphabetically or by a consistent pattern",
430
+ "@angular-eslint/prefer-inject": "Use `inject(MyService)` instead of constructor injection: `constructor(private myService: MyService) {}`",
431
+ "@angular-eslint/no-pipe-impure": "Remove `pure: false` from pipe decorator or refactor to use a service",
432
+ "@angular-eslint/prefer-signals": "Replace observables with signals for simpler reactive state: `count = signal(0)`",
433
+ "@angular-eslint/prefer-signal-model": "Use signal-based input/output model: `count = model(0)` instead of `@Input() count: number`",
434
+ "@angular-eslint/no-uncalled-signals": "Call the signal getter to access its value: `this.count()` not `this.count`",
435
+ "@angular-eslint/component-max-inline-declarations": "Move templates/styles to separate files or use inline with caution — consider extracting when > 3 inline declarations",
436
+ "@angular-eslint/relative-url-prefix": "Use relative URLs (no leading slash) or ensure absolute URLs are intentional for security",
437
+ "@ngrx/contextual-action-creator": "Use `createActionGroup` or `createAction` with props for type-safe actions",
438
+ "@ngrx/no-multiple-actions-in-effects": "Split effect into multiple effects or use `mergeMap` with individual actions"
439
+ };
440
+ const detectPackagePresence = (packageJson) => {
441
+ const deps = {
442
+ ...packageJson.dependencies,
443
+ ...packageJson.devDependencies,
444
+ ...packageJson.peerDependencies
445
+ };
446
+ return {
447
+ hasNgRx: Object.keys(deps).some((dep) => dep.startsWith("@ngrx/") && !dep.includes("store")) || Object.keys(deps).includes("@ngrx/store"),
448
+ hasAngularMaterial: Object.keys(deps).includes("@angular/material"),
449
+ hasSignals: true
450
+ };
274
451
  };
275
- const buildEslintConfig = (hasTypeScript, tsconfigPath, useTypeAware) => {
452
+ const buildEslintConfig = (hasTypeScript, tsconfigPath, useTypeAware, packagePresence) => {
276
453
  const languageOptions = {
277
454
  parser: tsEslint.parser,
278
455
  parserOptions: {
@@ -284,20 +461,73 @@ const buildEslintConfig = (hasTypeScript, tsconfigPath, useTypeAware) => {
284
461
  const angularRules = {
285
462
  "@angular-eslint/component-class-suffix": "warn",
286
463
  "@angular-eslint/directive-class-suffix": "warn",
464
+ "@angular-eslint/pipe-prefix": "warn",
287
465
  "@angular-eslint/no-empty-lifecycle-method": "warn",
288
466
  "@angular-eslint/use-lifecycle-interface": "warn",
289
- "@angular-eslint/use-pipe-transform-interface": "error",
467
+ "@angular-eslint/consistent-component-styles": "warn",
468
+ "@angular-eslint/sort-lifecycle-methods": "warn",
469
+ "@angular-eslint/use-component-selector": "warn",
470
+ "@angular-eslint/component-selector": "warn",
471
+ "@angular-eslint/directive-selector": "warn",
290
472
  "@angular-eslint/prefer-on-push-component-change-detection": "warn",
291
473
  "@angular-eslint/no-output-native": "error",
474
+ "@angular-eslint/no-pipe-impure": "warn",
475
+ "@angular-eslint/component-max-inline-declarations": "warn",
476
+ "@angular-eslint/use-pipe-transform-interface": "error",
292
477
  "@angular-eslint/no-conflicting-lifecycle": "error",
293
478
  "@angular-eslint/contextual-lifecycle": "error",
479
+ "@angular-eslint/contextual-decorator": "error",
480
+ "@angular-eslint/no-async-lifecycle-method": "error",
481
+ "@angular-eslint/no-duplicates-in-metadata-arrays": "error",
482
+ "@angular-eslint/no-lifecycle-call": "error",
483
+ "@angular-eslint/require-lifecycle-on-prototype": "error",
294
484
  "@angular-eslint/no-forward-ref": "warn",
295
485
  "@angular-eslint/no-input-rename": "warn",
296
486
  "@angular-eslint/no-output-rename": "warn",
297
487
  "@angular-eslint/no-inputs-metadata-property": "warn",
298
- "@angular-eslint/no-outputs-metadata-property": "warn"
488
+ "@angular-eslint/no-outputs-metadata-property": "warn",
489
+ "@angular-eslint/no-queries-metadata-property": "warn",
490
+ "@angular-eslint/prefer-standalone": "warn",
491
+ "@angular-eslint/prefer-host-metadata-property": "warn",
492
+ "@angular-eslint/prefer-inject": "warn",
493
+ "@angular-eslint/prefer-output-emitter-ref": "warn",
494
+ "@angular-eslint/prefer-output-readonly": "warn",
495
+ "@angular-eslint/use-component-view-encapsulation": "warn",
496
+ "@angular-eslint/use-injectable-provided-in": "warn",
497
+ "@angular-eslint/no-attribute-decorator": "warn",
498
+ "@angular-eslint/no-input-prefix": "warn",
499
+ "@angular-eslint/no-output-on-prefix": "warn",
500
+ "@angular-eslint/relative-url-prefix": "error"
299
501
  };
300
- const tsRules = { "@typescript-eslint/no-explicit-any": "warn" };
502
+ const signalsRules = packagePresence.hasSignals ? {
503
+ "@angular-eslint/prefer-signals": "warn",
504
+ "@angular-eslint/prefer-signal-model": "warn",
505
+ "@angular-eslint/no-uncalled-signals": "error"
506
+ } : {};
507
+ const tsRules = {
508
+ "@typescript-eslint/no-explicit-any": "warn",
509
+ "@typescript-eslint/no-unused-vars": "warn",
510
+ "@typescript-eslint/sort-keys": "warn"
511
+ };
512
+ const ngrxRules = packagePresence.hasNgRx ? {
513
+ "@ngrx/contextual-action-creator": "warn",
514
+ "@ngrx/no-cyclic-action-creators": "error",
515
+ "@ngrx/no-discrete-actions": "warn",
516
+ "@ngrx/no-effect-decorator": "warn",
517
+ "@ngrx/no-effect-decorator-and-creator": "error",
518
+ "@ngrx/no-multiple-actions-in-effects": "error",
519
+ "@ngrx/no-reordering-in-effect-reducers": "error",
520
+ "@ngrx/no-typed-global-store": "error",
521
+ "@ngrx/on-function-explicit-return-type": "warn",
522
+ "@ngrx/prefix-selectors-with-namespace": "warn",
523
+ "@ngrx/require-middleware-selector": "error",
524
+ "@ngrx/select-style": "warn",
525
+ "@ngrx/use-consumer-selector": "warn"
526
+ } : {};
527
+ const materialRules = packagePresence.hasAngularMaterial ? {
528
+ "@angular/material/prefix-selector": "warn",
529
+ "@angular/material/no-conflicting-mixins": "error"
530
+ } : {};
301
531
  return [{
302
532
  files: ["**/*.ts"],
303
533
  plugins: {
@@ -307,7 +537,10 @@ const buildEslintConfig = (hasTypeScript, tsconfigPath, useTypeAware) => {
307
537
  languageOptions,
308
538
  rules: {
309
539
  ...angularRules,
310
- ...tsRules
540
+ ...tsRules,
541
+ ...signalsRules,
542
+ ...ngrxRules,
543
+ ...materialRules
311
544
  }
312
545
  }];
313
546
  };
@@ -330,24 +563,70 @@ const parsePluginAndRule = (ruleId) => {
330
563
  };
331
564
  };
332
565
  const runEslint = async (rootDirectory, hasTypeScript, includePaths, options) => {
333
- if (includePaths !== void 0 && includePaths.length === 0) return [];
566
+ if (includePaths !== void 0 && includePaths.length === 0) return {
567
+ diagnostics: [],
568
+ errors: []
569
+ };
334
570
  const tsconfigPath = hasTypeScript ? path.join(rootDirectory, "tsconfig.json") : null;
571
+ let packagePresence;
572
+ if (options?.frameworkInfo) packagePresence = {
573
+ hasNgRx: options.frameworkInfo.hasNgRx,
574
+ hasAngularMaterial: options.frameworkInfo.hasAngularMaterial,
575
+ hasSignals: options.frameworkInfo.hasSignals
576
+ };
577
+ else {
578
+ packagePresence = {
579
+ hasNgRx: false,
580
+ hasAngularMaterial: false,
581
+ hasSignals: true
582
+ };
583
+ try {
584
+ const packageJsonPath = path.join(rootDirectory, "package.json");
585
+ if (fs.existsSync(packageJsonPath)) {
586
+ const packageJsonContent = fs.readFileSync(packageJsonPath, "utf-8");
587
+ packagePresence = detectPackagePresence(JSON.parse(packageJsonContent));
588
+ }
589
+ } catch {}
590
+ }
335
591
  const cacheRoot = path.join(rootDirectory, "node_modules", ".cache", "angular-doctor");
336
592
  fs.mkdirSync(cacheRoot, { recursive: true });
337
593
  const eslint = new ESLint({
338
594
  cwd: rootDirectory,
339
595
  overrideConfigFile: null,
340
- overrideConfig: buildEslintConfig(hasTypeScript, tsconfigPath && fs.existsSync(tsconfigPath) ? tsconfigPath : null, options?.useTypeAware ?? true),
596
+ overrideConfig: buildEslintConfig(hasTypeScript, tsconfigPath && fs.existsSync(tsconfigPath) ? tsconfigPath : null, options?.useTypeAware ?? true, packagePresence),
341
597
  ignore: true,
342
598
  cache: true,
343
599
  cacheLocation: path.join(cacheRoot, ".eslintcache")
344
600
  });
345
601
  const patterns = includePaths ?? ["**/*.ts"];
346
602
  let results;
603
+ const errors = [];
347
604
  try {
348
605
  results = await eslint.lintFiles(patterns);
349
- } catch {
350
- return [];
606
+ } catch (error) {
607
+ const errorMessage = error instanceof Error ? error.message : String(error);
608
+ const errorStack = error instanceof Error ? error.stack : void 0;
609
+ const filePathMatch = errorMessage.match(/^(.+?):\s* /);
610
+ const errorFilePath = filePathMatch ? path.relative(rootDirectory, filePathMatch[1]) : void 0;
611
+ errors.push({
612
+ message: errorMessage,
613
+ stack: errorStack,
614
+ filePath: errorFilePath
615
+ });
616
+ return {
617
+ diagnostics: [{
618
+ filePath: errorFilePath ?? "unknown",
619
+ plugin: "eslint",
620
+ rule: "parse-error",
621
+ severity: "error",
622
+ message: errorMessage,
623
+ help: "Fix the syntax error in this file. ESLint could not parse the file.",
624
+ line: 0,
625
+ column: 0,
626
+ category: "Parse Error"
627
+ }],
628
+ errors
629
+ };
351
630
  }
352
631
  const diagnostics = [];
353
632
  for (const result of results) for (const message of result.messages) {
@@ -366,7 +645,10 @@ const runEslint = async (rootDirectory, hasTypeScript, includePaths, options) =>
366
645
  category: resolveDiagnosticCategory(ruleKey)
367
646
  });
368
647
  }
369
- return diagnostics;
648
+ return {
649
+ diagnostics,
650
+ errors
651
+ };
370
652
  };
371
653
 
372
654
  //#endregion
@@ -583,13 +865,20 @@ const diagnose = async (directory, options = {}) => {
583
865
  const emptyDiagnostics = [];
584
866
  const lintPromise = effectiveLint ? runEslint(resolvedDirectory, projectInfo.hasTypeScript, computedIncludePaths).catch((error) => {
585
867
  console.error("Lint failed:", error);
586
- return emptyDiagnostics;
587
- }) : Promise.resolve(emptyDiagnostics);
868
+ return {
869
+ diagnostics: emptyDiagnostics,
870
+ errors: []
871
+ };
872
+ }) : Promise.resolve({
873
+ diagnostics: emptyDiagnostics,
874
+ errors: []
875
+ });
588
876
  const deadCodePromise = effectiveDeadCode && !isDiffMode ? runKnip(resolvedDirectory).catch((error) => {
589
877
  console.error("Dead code analysis failed:", error);
590
878
  return emptyDiagnostics;
591
879
  }) : Promise.resolve(emptyDiagnostics);
592
- const [lintDiagnostics, deadCodeDiagnostics] = await Promise.all([lintPromise, deadCodePromise]);
880
+ const [lintResult, deadCodeDiagnostics] = await Promise.all([lintPromise, deadCodePromise]);
881
+ const lintDiagnostics = lintResult.diagnostics;
593
882
  const diagnostics = combineDiagnostics(lintDiagnostics, deadCodeDiagnostics, userConfig);
594
883
  const elapsedMilliseconds = performance.now() - startTime;
595
884
  return {