nestjs-doctor 0.3.2 → 0.4.1
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/README.md +9 -14
- package/dist/api/index.cjs +147 -207
- package/dist/api/index.d.cts +0 -2
- package/dist/api/index.d.cts.map +1 -1
- package/dist/api/index.d.mts +0 -2
- package/dist/api/index.d.mts.map +1 -1
- package/dist/api/index.mjs +147 -207
- package/dist/api/index.mjs.map +1 -1
- package/dist/cli/index.mjs +147 -207
- package/package.json +1 -1
- package/skill/SKILL.md +3 -6
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
15
|
<p align="center">
|
|
16
|
-
|
|
16
|
+
42 built-in rules across <b>security</b>, <b>performance</b>, <b>correctness</b>, and <b>architecture</b>. Outputs a <b>0-100 score</b> with actionable diagnostics. Zero config. Monorepo support. Built to catch the anti-patterns that AI-generated code loves to introduce.
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
19
|
---
|
|
@@ -153,8 +153,6 @@ Optional. Create `nestjs-doctor.config.json` in your project root:
|
|
|
153
153
|
"performance": false
|
|
154
154
|
},
|
|
155
155
|
"thresholds": {
|
|
156
|
-
"godModuleProviders": 15,
|
|
157
|
-
"godModuleImports": 20,
|
|
158
156
|
"godServiceMethods": 12,
|
|
159
157
|
"godServiceDeps": 10
|
|
160
158
|
}
|
|
@@ -166,13 +164,13 @@ Or use a `"nestjs-doctor"` key in `package.json`.
|
|
|
166
164
|
| Key | Type | Description |
|
|
167
165
|
|-----|------|-------------|
|
|
168
166
|
| `include` | `string[]` | Glob patterns to scan (default: `["**/*.ts"]`) |
|
|
169
|
-
| `exclude` | `string[]` | Glob patterns to skip (default includes `node_modules`, `dist`, `build`, `coverage`, `*.spec.ts`, `*.test.ts`, `*.e2e-spec.ts`, `*.e2e-test.ts`, `*.d.ts`, `test/`, `tests/`, `__tests__/`, `__mocks__/`, `__fixtures__
|
|
167
|
+
| `exclude` | `string[]` | Glob patterns to skip (default includes `node_modules`, `dist`, `build`, `coverage`, `*.spec.ts`, `*.test.ts`, `*.e2e-spec.ts`, `*.e2e-test.ts`, `*.d.ts`, `test/`, `tests/`, `__tests__/`, `__mocks__/`, `__fixtures__/`, `mock/`, `mocks/`, `*.mock.ts`, `seeder/`, `seeders/`, `*.seed.ts`, `*.seeder.ts`) |
|
|
170
168
|
| `minScore` | `number` | Minimum passing score (0-100). Exits with code 1 if below threshold |
|
|
171
169
|
| `ignore.rules` | `string[]` | Rule IDs to suppress |
|
|
172
170
|
| `ignore.files` | `string[]` | Glob patterns for files whose diagnostics are hidden |
|
|
173
171
|
| `rules` | `Record<string, boolean>` | Enable/disable individual rules |
|
|
174
172
|
| `categories` | `Record<string, boolean>` | Enable/disable entire categories |
|
|
175
|
-
| `thresholds` | `object` | Customize limits for god-
|
|
173
|
+
| `thresholds` | `object` | Customize limits for god-service rules |
|
|
176
174
|
|
|
177
175
|
---
|
|
178
176
|
|
|
@@ -234,7 +232,7 @@ mono.combined; // Merged DiagnoseResult
|
|
|
234
232
|
|
|
235
233
|
---
|
|
236
234
|
|
|
237
|
-
## Rules (
|
|
235
|
+
## Rules (42)
|
|
238
236
|
|
|
239
237
|
### Security (9)
|
|
240
238
|
|
|
@@ -250,7 +248,7 @@ mono.combined; // Merged DiagnoseResult
|
|
|
250
248
|
| `no-exposed-stack-trace` | warning | `error.stack` exposed in responses |
|
|
251
249
|
| `require-auth-guard` | info | Controller without `@UseGuards()` |
|
|
252
250
|
|
|
253
|
-
### Correctness (
|
|
251
|
+
### Correctness (14)
|
|
254
252
|
|
|
255
253
|
| Rule | Severity | What it catches |
|
|
256
254
|
|------|----------|-----------------|
|
|
@@ -264,11 +262,12 @@ mono.combined; // Merged DiagnoseResult
|
|
|
264
262
|
| `prefer-readonly-injection` | warning | Constructor DI params missing `readonly` |
|
|
265
263
|
| `require-lifecycle-interface` | warning | Lifecycle method without corresponding interface |
|
|
266
264
|
| `no-empty-handlers` | warning | HTTP handler with empty body |
|
|
267
|
-
| `no-async-without-await` | warning | Async function/method with no `await` |
|
|
265
|
+
| `no-async-without-await` | warning | Async function/method with no `await` or redundant `async` on `new Promise()` return |
|
|
266
|
+
| `prefer-await-in-handlers` | warning | Async HTTP handler missing await — risks broken exception filters and lost stack traces |
|
|
268
267
|
| `no-duplicate-module-metadata` | warning | Duplicate entries in `@Module()` arrays |
|
|
269
268
|
| `no-missing-module-decorator` | warning | Class named `*Module` without `@Module()` |
|
|
270
269
|
|
|
271
|
-
### Architecture (
|
|
270
|
+
### Architecture (12)
|
|
272
271
|
|
|
273
272
|
| Rule | Severity | What it catches |
|
|
274
273
|
|------|----------|-----------------|
|
|
@@ -278,7 +277,6 @@ mono.combined; // Merged DiagnoseResult
|
|
|
278
277
|
| `no-circular-module-deps` | error | Cycles in `@Module()` import graph |
|
|
279
278
|
| `no-manual-instantiation` | error | `new SomeService()` for injectable classes |
|
|
280
279
|
| `no-orm-in-services` | warning | Services using ORM directly (should use repositories) |
|
|
281
|
-
| `no-god-module` | warning | >10 providers or >15 imports |
|
|
282
280
|
| `no-god-service` | warning | >10 public methods or >8 dependencies |
|
|
283
281
|
| `require-feature-modules` | warning | AppModule declaring too many providers directly |
|
|
284
282
|
| `prefer-constructor-injection` | warning | `@Inject()` property injection |
|
|
@@ -286,18 +284,15 @@ mono.combined; // Merged DiagnoseResult
|
|
|
286
284
|
| `prefer-interface-injection` | info | Concrete service-to-service injection |
|
|
287
285
|
| `no-barrel-export-internals` | info | Re-exporting repositories from barrel files |
|
|
288
286
|
|
|
289
|
-
### Performance (
|
|
287
|
+
### Performance (7)
|
|
290
288
|
|
|
291
289
|
| Rule | Severity | What it catches |
|
|
292
290
|
|------|----------|-----------------|
|
|
293
291
|
| `no-sync-io` | warning | `readFileSync`, `writeFileSync`, etc. |
|
|
294
|
-
| `no-query-in-loop` | warning | `await` inside loops (N+1 pattern) |
|
|
295
292
|
| `no-blocking-constructor` | warning | Loops/await in Injectable/Controller constructors |
|
|
296
293
|
| `no-dynamic-require` | warning | `require()` with non-literal argument |
|
|
297
294
|
| `no-unused-providers` | warning | Provider never injected anywhere |
|
|
298
|
-
| `no-logging-in-loops` | info | `console.*` / `this.logger.*` in loops |
|
|
299
295
|
| `no-unnecessary-async` | info | Async method with no `await` |
|
|
300
|
-
| `prefer-pagination` | info | `findMany()` / `find()` without pagination args |
|
|
301
296
|
| `no-unused-module-exports` | info | Module exports unused by importers |
|
|
302
297
|
| `no-orphan-modules` | info | Module never imported by any other module |
|
|
303
298
|
|
package/dist/api/index.cjs
CHANGED
|
@@ -220,7 +220,8 @@ function resolveProviders(project, files) {
|
|
|
220
220
|
if (!name) continue;
|
|
221
221
|
const ctor = cls.getConstructors()[0];
|
|
222
222
|
const dependencies = ctor ? ctor.getParameters().map((p) => {
|
|
223
|
-
|
|
223
|
+
const typeNode = p.getTypeNode();
|
|
224
|
+
return extractSimpleTypeName(typeNode ? typeNode.getText() : p.getType().getText());
|
|
224
225
|
}) : [];
|
|
225
226
|
const publicMethodCount = cls.getMethods().filter((m) => {
|
|
226
227
|
const scope = m.getScope();
|
|
@@ -309,7 +310,7 @@ function isModule(cls) {
|
|
|
309
310
|
|
|
310
311
|
//#endregion
|
|
311
312
|
//#region src/rules/architecture/no-business-logic-in-controllers.ts
|
|
312
|
-
const HTTP_DECORATORS$
|
|
313
|
+
const HTTP_DECORATORS$3 = new Set([
|
|
313
314
|
"Get",
|
|
314
315
|
"Post",
|
|
315
316
|
"Put",
|
|
@@ -331,7 +332,7 @@ const noBusinessLogicInControllers = {
|
|
|
331
332
|
for (const cls of context.sourceFile.getClasses()) {
|
|
332
333
|
if (!isController(cls)) continue;
|
|
333
334
|
for (const method of cls.getMethods()) {
|
|
334
|
-
if (!method.getDecorators().some((d) => HTTP_DECORATORS$
|
|
335
|
+
if (!method.getDecorators().some((d) => HTTP_DECORATORS$3.has(d.getName()))) continue;
|
|
335
336
|
const body = method.getBody();
|
|
336
337
|
if (!body) continue;
|
|
337
338
|
const ifStatements = body.getDescendantsOfKind(ts_morph.SyntaxKind.IfStatement);
|
|
@@ -395,41 +396,6 @@ const noCircularModuleDeps = {
|
|
|
395
396
|
}
|
|
396
397
|
};
|
|
397
398
|
|
|
398
|
-
//#endregion
|
|
399
|
-
//#region src/rules/architecture/no-god-module.ts
|
|
400
|
-
const DEFAULT_MAX_PROVIDERS = 10;
|
|
401
|
-
const DEFAULT_MAX_IMPORTS = 15;
|
|
402
|
-
const noGodModule = {
|
|
403
|
-
meta: {
|
|
404
|
-
id: "architecture/no-god-module",
|
|
405
|
-
category: "architecture",
|
|
406
|
-
severity: "warning",
|
|
407
|
-
description: "Modules with too many providers or imports should be split into smaller feature modules",
|
|
408
|
-
help: "Split this module into smaller, focused feature modules.",
|
|
409
|
-
scope: "project"
|
|
410
|
-
},
|
|
411
|
-
check(context) {
|
|
412
|
-
const maxProviders = context.config.thresholds?.godModuleProviders ?? DEFAULT_MAX_PROVIDERS;
|
|
413
|
-
const maxImports = context.config.thresholds?.godModuleImports ?? DEFAULT_MAX_IMPORTS;
|
|
414
|
-
for (const mod of context.moduleGraph.modules.values()) {
|
|
415
|
-
if (mod.providers.length > maxProviders) context.report({
|
|
416
|
-
filePath: mod.filePath,
|
|
417
|
-
message: `Module '${mod.name}' has ${mod.providers.length} providers (max: ${maxProviders}). Consider splitting into smaller modules.`,
|
|
418
|
-
help: this.meta.help,
|
|
419
|
-
line: mod.classDeclaration.getStartLineNumber(),
|
|
420
|
-
column: 1
|
|
421
|
-
});
|
|
422
|
-
if (mod.imports.length > maxImports) context.report({
|
|
423
|
-
filePath: mod.filePath,
|
|
424
|
-
message: `Module '${mod.name}' has ${mod.imports.length} imports (max: ${maxImports}). Consider grouping into feature modules.`,
|
|
425
|
-
help: this.meta.help,
|
|
426
|
-
line: mod.classDeclaration.getStartLineNumber(),
|
|
427
|
-
column: 1
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
};
|
|
432
|
-
|
|
433
399
|
//#endregion
|
|
434
400
|
//#region src/rules/architecture/no-god-service.ts
|
|
435
401
|
const DEFAULT_MAX_METHODS = 10;
|
|
@@ -812,6 +778,23 @@ const requireModuleBoundaries = {
|
|
|
812
778
|
|
|
813
779
|
//#endregion
|
|
814
780
|
//#region src/rules/correctness/no-async-without-await.ts
|
|
781
|
+
const HTTP_DECORATORS$2 = new Set([
|
|
782
|
+
"Get",
|
|
783
|
+
"Post",
|
|
784
|
+
"Put",
|
|
785
|
+
"Delete",
|
|
786
|
+
"Patch",
|
|
787
|
+
"All",
|
|
788
|
+
"Head",
|
|
789
|
+
"Options"
|
|
790
|
+
]);
|
|
791
|
+
function returnsNewPromise$1(body) {
|
|
792
|
+
return body.getDescendantsOfKind(ts_morph.SyntaxKind.ReturnStatement).some((ret) => {
|
|
793
|
+
const expr = ret.getExpression();
|
|
794
|
+
if (!expr || expr.getKind() !== ts_morph.SyntaxKind.NewExpression) return false;
|
|
795
|
+
return expr.asKindOrThrow(ts_morph.SyntaxKind.NewExpression).getExpression().getText() === "Promise";
|
|
796
|
+
});
|
|
797
|
+
}
|
|
815
798
|
const noAsyncWithoutAwait = {
|
|
816
799
|
meta: {
|
|
817
800
|
id: "correctness/no-async-without-await",
|
|
@@ -823,6 +806,9 @@ const noAsyncWithoutAwait = {
|
|
|
823
806
|
check(context) {
|
|
824
807
|
for (const cls of context.sourceFile.getClasses()) for (const method of cls.getMethods()) {
|
|
825
808
|
if (!method.isAsync()) continue;
|
|
809
|
+
if (isController(cls)) {
|
|
810
|
+
if (method.getDecorators().some((d) => HTTP_DECORATORS$2.has(d.getName()))) continue;
|
|
811
|
+
}
|
|
826
812
|
const body = method.getBody();
|
|
827
813
|
if (!body) continue;
|
|
828
814
|
if (body.getDescendantsOfKind(ts_morph.SyntaxKind.AwaitExpression).filter((expr) => {
|
|
@@ -832,13 +818,23 @@ const noAsyncWithoutAwait = {
|
|
|
832
818
|
parent = parent.getParent();
|
|
833
819
|
}
|
|
834
820
|
return true;
|
|
835
|
-
}).length === 0)
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
821
|
+
}).length === 0) {
|
|
822
|
+
const name = method.getName();
|
|
823
|
+
if (returnsNewPromise$1(body)) context.report({
|
|
824
|
+
filePath: context.filePath,
|
|
825
|
+
message: `Async method '${name}()' returns a Promise directly — remove the async keyword.`,
|
|
826
|
+
help: "The async keyword is unnecessary when you are already constructing a Promise manually. Remove async to avoid double-wrapping.",
|
|
827
|
+
line: method.getStartLineNumber(),
|
|
828
|
+
column: 1
|
|
829
|
+
});
|
|
830
|
+
else context.report({
|
|
831
|
+
filePath: context.filePath,
|
|
832
|
+
message: `Async method '${name}()' has no await expression.`,
|
|
833
|
+
help: this.meta.help,
|
|
834
|
+
line: method.getStartLineNumber(),
|
|
835
|
+
column: 1
|
|
836
|
+
});
|
|
837
|
+
}
|
|
842
838
|
}
|
|
843
839
|
for (const fn of context.sourceFile.getFunctions()) {
|
|
844
840
|
if (!fn.isAsync()) continue;
|
|
@@ -851,13 +847,23 @@ const noAsyncWithoutAwait = {
|
|
|
851
847
|
parent = parent.getParent();
|
|
852
848
|
}
|
|
853
849
|
return true;
|
|
854
|
-
}).length === 0)
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
850
|
+
}).length === 0) {
|
|
851
|
+
const name = fn.getName() ?? "anonymous";
|
|
852
|
+
if (returnsNewPromise$1(body)) context.report({
|
|
853
|
+
filePath: context.filePath,
|
|
854
|
+
message: `Async function '${name}()' returns a Promise directly — remove the async keyword.`,
|
|
855
|
+
help: "The async keyword is unnecessary when you are already constructing a Promise manually. Remove async to avoid double-wrapping.",
|
|
856
|
+
line: fn.getStartLineNumber(),
|
|
857
|
+
column: 1
|
|
858
|
+
});
|
|
859
|
+
else context.report({
|
|
860
|
+
filePath: context.filePath,
|
|
861
|
+
message: `Async function '${name}()' has no await expression.`,
|
|
862
|
+
help: this.meta.help,
|
|
863
|
+
line: fn.getStartLineNumber(),
|
|
864
|
+
column: 1
|
|
865
|
+
});
|
|
866
|
+
}
|
|
861
867
|
}
|
|
862
868
|
}
|
|
863
869
|
};
|
|
@@ -956,7 +962,7 @@ const noDuplicateRoutes = {
|
|
|
956
962
|
|
|
957
963
|
//#endregion
|
|
958
964
|
//#region src/rules/correctness/no-empty-handlers.ts
|
|
959
|
-
const HTTP_DECORATORS = new Set([
|
|
965
|
+
const HTTP_DECORATORS$1 = new Set([
|
|
960
966
|
"Get",
|
|
961
967
|
"Post",
|
|
962
968
|
"Put",
|
|
@@ -978,7 +984,7 @@ const noEmptyHandlers = {
|
|
|
978
984
|
for (const cls of context.sourceFile.getClasses()) {
|
|
979
985
|
if (!isController(cls)) continue;
|
|
980
986
|
for (const method of cls.getMethods()) {
|
|
981
|
-
if (!method.getDecorators().some((d) => HTTP_DECORATORS.has(d.getName()))) continue;
|
|
987
|
+
if (!method.getDecorators().some((d) => HTTP_DECORATORS$1.has(d.getName()))) continue;
|
|
982
988
|
const body = method.getBody();
|
|
983
989
|
if (!body) continue;
|
|
984
990
|
if (body.getStatements().length === 0) context.report({
|
|
@@ -1169,6 +1175,69 @@ const noMissingPipeMethod = {
|
|
|
1169
1175
|
}
|
|
1170
1176
|
};
|
|
1171
1177
|
|
|
1178
|
+
//#endregion
|
|
1179
|
+
//#region src/rules/correctness/prefer-await-in-handlers.ts
|
|
1180
|
+
const HTTP_DECORATORS = new Set([
|
|
1181
|
+
"Get",
|
|
1182
|
+
"Post",
|
|
1183
|
+
"Put",
|
|
1184
|
+
"Delete",
|
|
1185
|
+
"Patch",
|
|
1186
|
+
"All",
|
|
1187
|
+
"Head",
|
|
1188
|
+
"Options"
|
|
1189
|
+
]);
|
|
1190
|
+
function returnsNewPromise(body) {
|
|
1191
|
+
return body.getDescendantsOfKind(ts_morph.SyntaxKind.ReturnStatement).some((ret) => {
|
|
1192
|
+
const expr = ret.getExpression();
|
|
1193
|
+
if (!expr || expr.getKind() !== ts_morph.SyntaxKind.NewExpression) return false;
|
|
1194
|
+
return expr.asKindOrThrow(ts_morph.SyntaxKind.NewExpression).getExpression().getText() === "Promise";
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
const preferAwaitInHandlers = {
|
|
1198
|
+
meta: {
|
|
1199
|
+
id: "correctness/prefer-await-in-handlers",
|
|
1200
|
+
category: "correctness",
|
|
1201
|
+
severity: "warning",
|
|
1202
|
+
description: "Async HTTP handler missing await — risks broken exception filters and lost stack traces",
|
|
1203
|
+
help: "Add await when calling services. This ensures exception filters trigger correctly, stack traces point to the handler, and error handling is consistent across handlers."
|
|
1204
|
+
},
|
|
1205
|
+
check(context) {
|
|
1206
|
+
for (const cls of context.sourceFile.getClasses()) {
|
|
1207
|
+
if (!isController(cls)) continue;
|
|
1208
|
+
for (const method of cls.getMethods()) {
|
|
1209
|
+
if (!(method.getDecorators().some((d) => HTTP_DECORATORS.has(d.getName())) && method.isAsync())) continue;
|
|
1210
|
+
const body = method.getBody();
|
|
1211
|
+
if (!body) continue;
|
|
1212
|
+
if (body.getDescendantsOfKind(ts_morph.SyntaxKind.AwaitExpression).filter((expr) => {
|
|
1213
|
+
let parent = expr.getParent();
|
|
1214
|
+
while (parent && parent !== body) {
|
|
1215
|
+
if (parent.getKind() === ts_morph.SyntaxKind.ArrowFunction || parent.getKind() === ts_morph.SyntaxKind.FunctionExpression || parent.getKind() === ts_morph.SyntaxKind.FunctionDeclaration) return false;
|
|
1216
|
+
parent = parent.getParent();
|
|
1217
|
+
}
|
|
1218
|
+
return true;
|
|
1219
|
+
}).length === 0) {
|
|
1220
|
+
const name = method.getName();
|
|
1221
|
+
if (returnsNewPromise(body)) context.report({
|
|
1222
|
+
filePath: context.filePath,
|
|
1223
|
+
message: `Handler '${name}()' returns a Promise directly — remove async and return the promise, or await the resolved value.`,
|
|
1224
|
+
help: this.meta.help,
|
|
1225
|
+
line: method.getStartLineNumber(),
|
|
1226
|
+
column: 1
|
|
1227
|
+
});
|
|
1228
|
+
else context.report({
|
|
1229
|
+
filePath: context.filePath,
|
|
1230
|
+
message: `Async handler '${name}()' does not await any expression.`,
|
|
1231
|
+
help: this.meta.help,
|
|
1232
|
+
line: method.getStartLineNumber(),
|
|
1233
|
+
column: 1
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
|
|
1172
1241
|
//#endregion
|
|
1173
1242
|
//#region src/rules/correctness/prefer-readonly-injection.ts
|
|
1174
1243
|
const preferReadonlyInjection = {
|
|
@@ -1334,56 +1403,6 @@ const noDynamicRequire = {
|
|
|
1334
1403
|
}
|
|
1335
1404
|
};
|
|
1336
1405
|
|
|
1337
|
-
//#endregion
|
|
1338
|
-
//#region src/rules/performance/no-logging-in-loops.ts
|
|
1339
|
-
const LOOP_KINDS$1 = new Set([
|
|
1340
|
-
ts_morph.SyntaxKind.ForStatement,
|
|
1341
|
-
ts_morph.SyntaxKind.ForOfStatement,
|
|
1342
|
-
ts_morph.SyntaxKind.ForInStatement,
|
|
1343
|
-
ts_morph.SyntaxKind.WhileStatement,
|
|
1344
|
-
ts_morph.SyntaxKind.DoStatement
|
|
1345
|
-
]);
|
|
1346
|
-
const CONSOLE_METHODS = new Set([
|
|
1347
|
-
"log",
|
|
1348
|
-
"warn",
|
|
1349
|
-
"error",
|
|
1350
|
-
"info",
|
|
1351
|
-
"debug",
|
|
1352
|
-
"trace"
|
|
1353
|
-
]);
|
|
1354
|
-
const noLoggingInLoops = {
|
|
1355
|
-
meta: {
|
|
1356
|
-
id: "performance/no-logging-in-loops",
|
|
1357
|
-
category: "performance",
|
|
1358
|
-
severity: "info",
|
|
1359
|
-
description: "Logging inside loops can degrade performance and flood log output",
|
|
1360
|
-
help: "Move logging outside the loop, aggregate results, or use conditional logging."
|
|
1361
|
-
},
|
|
1362
|
-
check(context) {
|
|
1363
|
-
const callExpressions = context.sourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.CallExpression);
|
|
1364
|
-
for (const call of callExpressions) {
|
|
1365
|
-
const exprText = call.getExpression().getText();
|
|
1366
|
-
const isConsoleCall = exprText.startsWith("console.") && CONSOLE_METHODS.has(exprText.split(".")[1]);
|
|
1367
|
-
const isLoggerCall = exprText.startsWith("this.logger.") && CONSOLE_METHODS.has(exprText.split(".")[2]);
|
|
1368
|
-
if (!(isConsoleCall || isLoggerCall)) continue;
|
|
1369
|
-
let parent = call.getParent();
|
|
1370
|
-
while (parent) {
|
|
1371
|
-
if (LOOP_KINDS$1.has(parent.getKind())) {
|
|
1372
|
-
context.report({
|
|
1373
|
-
filePath: context.filePath,
|
|
1374
|
-
message: `Logging call '${exprText}()' inside a loop.`,
|
|
1375
|
-
help: this.meta.help,
|
|
1376
|
-
line: call.getStartLineNumber(),
|
|
1377
|
-
column: 1
|
|
1378
|
-
});
|
|
1379
|
-
break;
|
|
1380
|
-
}
|
|
1381
|
-
parent = parent.getParent();
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
};
|
|
1386
|
-
|
|
1387
1406
|
//#endregion
|
|
1388
1407
|
//#region src/rules/performance/no-orphan-modules.ts
|
|
1389
1408
|
const noOrphanModules = {
|
|
@@ -1411,44 +1430,6 @@ const noOrphanModules = {
|
|
|
1411
1430
|
}
|
|
1412
1431
|
};
|
|
1413
1432
|
|
|
1414
|
-
//#endregion
|
|
1415
|
-
//#region src/rules/performance/no-query-in-loop.ts
|
|
1416
|
-
const LOOP_KINDS = new Set([
|
|
1417
|
-
ts_morph.SyntaxKind.ForStatement,
|
|
1418
|
-
ts_morph.SyntaxKind.ForOfStatement,
|
|
1419
|
-
ts_morph.SyntaxKind.ForInStatement,
|
|
1420
|
-
ts_morph.SyntaxKind.WhileStatement,
|
|
1421
|
-
ts_morph.SyntaxKind.DoStatement
|
|
1422
|
-
]);
|
|
1423
|
-
const noQueryInLoop = {
|
|
1424
|
-
meta: {
|
|
1425
|
-
id: "performance/no-query-in-loop",
|
|
1426
|
-
category: "performance",
|
|
1427
|
-
severity: "warning",
|
|
1428
|
-
description: "Await expressions inside loops cause N+1 query patterns — batch or parallelize instead",
|
|
1429
|
-
help: "Use Promise.all(), batch queries, or database-level operations (e.g., WHERE IN) instead of awaiting in a loop."
|
|
1430
|
-
},
|
|
1431
|
-
check(context) {
|
|
1432
|
-
const awaitExpressions = context.sourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.AwaitExpression);
|
|
1433
|
-
for (const awaitExpr of awaitExpressions) {
|
|
1434
|
-
let parent = awaitExpr.getParent();
|
|
1435
|
-
while (parent) {
|
|
1436
|
-
if (LOOP_KINDS.has(parent.getKind())) {
|
|
1437
|
-
context.report({
|
|
1438
|
-
filePath: context.filePath,
|
|
1439
|
-
message: "Await expression inside a loop — potential N+1 query pattern.",
|
|
1440
|
-
help: this.meta.help,
|
|
1441
|
-
line: awaitExpr.getStartLineNumber(),
|
|
1442
|
-
column: 1
|
|
1443
|
-
});
|
|
1444
|
-
break;
|
|
1445
|
-
}
|
|
1446
|
-
parent = parent.getParent();
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
};
|
|
1451
|
-
|
|
1452
1433
|
//#endregion
|
|
1453
1434
|
//#region src/rules/performance/no-sync-io.ts
|
|
1454
1435
|
const SYNC_IO_METHODS = new Set([
|
|
@@ -1550,6 +1531,7 @@ const noUnusedModuleExports = {
|
|
|
1550
1531
|
if (!provider) continue;
|
|
1551
1532
|
for (const dep of provider.dependencies) usedProviders.add(dep);
|
|
1552
1533
|
}
|
|
1534
|
+
if (importer.exports.includes(mod.name)) for (const exp of mod.exports) usedProviders.add(exp);
|
|
1553
1535
|
for (const controllerName of importer.controllers) for (const filePath of context.files) {
|
|
1554
1536
|
const sourceFile = context.project.getSourceFile(filePath);
|
|
1555
1537
|
if (!sourceFile) continue;
|
|
@@ -1558,20 +1540,24 @@ const noUnusedModuleExports = {
|
|
|
1558
1540
|
const ctor = cls.getConstructors()[0];
|
|
1559
1541
|
if (!ctor) continue;
|
|
1560
1542
|
for (const param of ctor.getParameters()) {
|
|
1561
|
-
const
|
|
1543
|
+
const typeNode = param.getTypeNode();
|
|
1544
|
+
const typeText = typeNode ? typeNode.getText() : param.getType().getText();
|
|
1562
1545
|
const simpleName = typeText.split(".").pop()?.split("<")[0] ?? typeText;
|
|
1563
1546
|
usedProviders.add(simpleName);
|
|
1564
1547
|
}
|
|
1565
1548
|
}
|
|
1566
1549
|
}
|
|
1567
1550
|
}
|
|
1568
|
-
for (const exportedName of mod.exports)
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1551
|
+
for (const exportedName of mod.exports) {
|
|
1552
|
+
if (context.moduleGraph.modules.has(exportedName)) continue;
|
|
1553
|
+
if (!usedProviders.has(exportedName)) context.report({
|
|
1554
|
+
filePath: mod.filePath,
|
|
1555
|
+
message: `Module '${mod.name}' exports '${exportedName}' but no importing module uses it.`,
|
|
1556
|
+
help: this.meta.help,
|
|
1557
|
+
line: mod.classDeclaration.getStartLineNumber(),
|
|
1558
|
+
column: 1
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1575
1561
|
}
|
|
1576
1562
|
}
|
|
1577
1563
|
};
|
|
@@ -1605,7 +1591,8 @@ const noUnusedProviders = {
|
|
|
1605
1591
|
const ctor = cls.getConstructors()[0];
|
|
1606
1592
|
if (!ctor) continue;
|
|
1607
1593
|
for (const param of ctor.getParameters()) {
|
|
1608
|
-
const
|
|
1594
|
+
const typeNode = param.getTypeNode();
|
|
1595
|
+
const typeText = typeNode ? typeNode.getText() : param.getType().getText();
|
|
1609
1596
|
const simpleName = typeText.split(".").pop()?.split("<")[0] ?? typeText;
|
|
1610
1597
|
allDependencies.add(simpleName);
|
|
1611
1598
|
}
|
|
@@ -1632,57 +1619,6 @@ const noUnusedProviders = {
|
|
|
1632
1619
|
}
|
|
1633
1620
|
};
|
|
1634
1621
|
|
|
1635
|
-
//#endregion
|
|
1636
|
-
//#region src/rules/performance/prefer-pagination.ts
|
|
1637
|
-
const FIND_MANY_METHODS = new Set(["findMany", "find"]);
|
|
1638
|
-
const PAGINATION_ARGS = new Set([
|
|
1639
|
-
"take",
|
|
1640
|
-
"skip",
|
|
1641
|
-
"limit",
|
|
1642
|
-
"offset",
|
|
1643
|
-
"page",
|
|
1644
|
-
"perPage",
|
|
1645
|
-
"pageSize"
|
|
1646
|
-
]);
|
|
1647
|
-
const preferPagination = {
|
|
1648
|
-
meta: {
|
|
1649
|
-
id: "performance/prefer-pagination",
|
|
1650
|
-
category: "performance",
|
|
1651
|
-
severity: "info",
|
|
1652
|
-
description: "Database findMany/find queries should include pagination to avoid loading excessive data",
|
|
1653
|
-
help: "Add pagination arguments (take/skip, limit/offset) to the query."
|
|
1654
|
-
},
|
|
1655
|
-
check(context) {
|
|
1656
|
-
const callExpressions = context.sourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.CallExpression);
|
|
1657
|
-
for (const call of callExpressions) {
|
|
1658
|
-
const methodName = call.getExpression().getText().split(".").pop() ?? "";
|
|
1659
|
-
if (!FIND_MANY_METHODS.has(methodName)) continue;
|
|
1660
|
-
const args = call.getArguments();
|
|
1661
|
-
if (args.length === 0) {
|
|
1662
|
-
context.report({
|
|
1663
|
-
filePath: context.filePath,
|
|
1664
|
-
message: `'${methodName}()' called without pagination arguments.`,
|
|
1665
|
-
help: this.meta.help,
|
|
1666
|
-
line: call.getStartLineNumber(),
|
|
1667
|
-
column: 1
|
|
1668
|
-
});
|
|
1669
|
-
continue;
|
|
1670
|
-
}
|
|
1671
|
-
const firstArg = args[0];
|
|
1672
|
-
if (firstArg.getKind() !== ts_morph.SyntaxKind.ObjectLiteralExpression) continue;
|
|
1673
|
-
const obj = firstArg.asKind(ts_morph.SyntaxKind.ObjectLiteralExpression);
|
|
1674
|
-
if (!obj) continue;
|
|
1675
|
-
if (!obj.getProperties().map((p) => p.getText().split(":")[0].trim()).some((p) => PAGINATION_ARGS.has(p))) context.report({
|
|
1676
|
-
filePath: context.filePath,
|
|
1677
|
-
message: `'${methodName}()' called without pagination arguments.`,
|
|
1678
|
-
help: this.meta.help,
|
|
1679
|
-
line: call.getStartLineNumber(),
|
|
1680
|
-
column: 1
|
|
1681
|
-
});
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
};
|
|
1685
|
-
|
|
1686
1622
|
//#endregion
|
|
1687
1623
|
//#region src/rules/security/no-csrf-disabled.ts
|
|
1688
1624
|
const noCsrfDisabled = {
|
|
@@ -2075,7 +2011,6 @@ const allRules = [
|
|
|
2075
2011
|
requireModuleBoundaries,
|
|
2076
2012
|
noBarrelExportInternals,
|
|
2077
2013
|
noCircularModuleDeps,
|
|
2078
|
-
noGodModule,
|
|
2079
2014
|
noGodService,
|
|
2080
2015
|
requireFeatureModules,
|
|
2081
2016
|
preferReadonlyInjection,
|
|
@@ -2087,6 +2022,7 @@ const allRules = [
|
|
|
2087
2022
|
noMissingFilterCatch,
|
|
2088
2023
|
noMissingInterceptorMethod,
|
|
2089
2024
|
noAsyncWithoutAwait,
|
|
2025
|
+
preferAwaitInHandlers,
|
|
2090
2026
|
noDuplicateModuleMetadata,
|
|
2091
2027
|
noMissingModuleDecorator,
|
|
2092
2028
|
requireInjectDecorator,
|
|
@@ -2101,11 +2037,8 @@ const allRules = [
|
|
|
2101
2037
|
noExposedStackTrace,
|
|
2102
2038
|
noDangerousRedirects,
|
|
2103
2039
|
noSyncIo,
|
|
2104
|
-
noQueryInLoop,
|
|
2105
|
-
noLoggingInLoops,
|
|
2106
2040
|
noUnnecessaryAsync,
|
|
2107
2041
|
noBlockingConstructor,
|
|
2108
|
-
preferPagination,
|
|
2109
2042
|
noDynamicRequire,
|
|
2110
2043
|
noUnusedProviders,
|
|
2111
2044
|
noUnusedModuleExports,
|
|
@@ -2198,7 +2131,14 @@ const DEFAULT_CONFIG = {
|
|
|
2198
2131
|
"**/tests/**",
|
|
2199
2132
|
"**/__tests__/**",
|
|
2200
2133
|
"**/__mocks__/**",
|
|
2201
|
-
"**/__fixtures__/**"
|
|
2134
|
+
"**/__fixtures__/**",
|
|
2135
|
+
"**/mock/**",
|
|
2136
|
+
"**/mocks/**",
|
|
2137
|
+
"**/*.mock.ts",
|
|
2138
|
+
"**/seeder/**",
|
|
2139
|
+
"**/seeders/**",
|
|
2140
|
+
"**/*.seed.ts",
|
|
2141
|
+
"**/*.seeder.ts"
|
|
2202
2142
|
]
|
|
2203
2143
|
};
|
|
2204
2144
|
|
package/dist/api/index.d.cts
CHANGED
package/dist/api/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/engine/module-graph.ts","../../src/engine/type-resolver.ts","../../src/types/diagnostic.ts","../../src/types/config.ts","../../src/rules/types.ts","../../src/rules/index.ts","../../src/types/errors.ts","../../src/types/result.ts","../../src/api/index.ts"],"mappings":";;;UASiB,UAAA;EAChB,gBAAA,EAAkB,gBAAA;EAClB,WAAA;EACA,OAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;EACA,SAAA;AAAA;AAAA,UAGgB,WAAA;EAChB,KAAA,EAAO,GAAA,SAAY,GAAA;EACnB,OAAA,EAAS,GAAA,SAAY,UAAA;EACrB,gBAAA,EAAkB,GAAA,SAAY,UAAA;AAAA;;;UCjBd,YAAA;EAChB,gBAAA,EAAkB,gBAAA;EAClB,YAAA;EACA,QAAA;EACA,IAAA;EACA,iBAAA;AAAA;;;KCVW,QAAA;AAAA,KACA,QAAA;AAAA,UAMK,UAAA;EAChB,QAAA,EAAU,QAAA;EACV,MAAA;EACA,QAAA;EACA,IAAA;EACA,IAAA;EACA,OAAA;EACA,IAAA;EACA,QAAA,EAAU,QAAA;AAAA;;;UCbM,YAAA;EAChB,OAAA;EACA,QAAA,GAAW,QAAA;AAAA;AAAA,UAGK,wBAAA;EAChB,KAAA;EACA,KAAA;AAAA;AAAA,UAGgB,kBAAA;EAChB,UAAA,GAAa,OAAA,CAAQ,MAAA,CAAO,QAAA;EAC5B,OAAA;EACA,MAAA,GAAS,wBAAA;EACT,OAAA;EACA,QAAA;EACA,KAAA,GAAQ,MAAA,SAAe,YAAA;EACvB,UAAA;IACC,
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/engine/module-graph.ts","../../src/engine/type-resolver.ts","../../src/types/diagnostic.ts","../../src/types/config.ts","../../src/rules/types.ts","../../src/rules/index.ts","../../src/types/errors.ts","../../src/types/result.ts","../../src/api/index.ts"],"mappings":"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"}
|
package/dist/api/index.d.mts
CHANGED
package/dist/api/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/engine/module-graph.ts","../../src/engine/type-resolver.ts","../../src/types/diagnostic.ts","../../src/types/config.ts","../../src/rules/types.ts","../../src/rules/index.ts","../../src/types/errors.ts","../../src/types/result.ts","../../src/api/index.ts"],"mappings":";;;UASiB,UAAA;EAChB,gBAAA,EAAkB,gBAAA;EAClB,WAAA;EACA,OAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;EACA,SAAA;AAAA;AAAA,UAGgB,WAAA;EAChB,KAAA,EAAO,GAAA,SAAY,GAAA;EACnB,OAAA,EAAS,GAAA,SAAY,UAAA;EACrB,gBAAA,EAAkB,GAAA,SAAY,UAAA;AAAA;;;UCjBd,YAAA;EAChB,gBAAA,EAAkB,gBAAA;EAClB,YAAA;EACA,QAAA;EACA,IAAA;EACA,iBAAA;AAAA;;;KCVW,QAAA;AAAA,KACA,QAAA;AAAA,UAMK,UAAA;EAChB,QAAA,EAAU,QAAA;EACV,MAAA;EACA,QAAA;EACA,IAAA;EACA,IAAA;EACA,OAAA;EACA,IAAA;EACA,QAAA,EAAU,QAAA;AAAA;;;UCbM,YAAA;EAChB,OAAA;EACA,QAAA,GAAW,QAAA;AAAA;AAAA,UAGK,wBAAA;EAChB,KAAA;EACA,KAAA;AAAA;AAAA,UAGgB,kBAAA;EAChB,UAAA,GAAa,OAAA,CAAQ,MAAA,CAAO,QAAA;EAC5B,OAAA;EACA,MAAA,GAAS,wBAAA;EACT,OAAA;EACA,QAAA;EACA,KAAA,GAAQ,MAAA,SAAe,YAAA;EACvB,UAAA;IACC,
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/engine/module-graph.ts","../../src/engine/type-resolver.ts","../../src/types/diagnostic.ts","../../src/types/config.ts","../../src/rules/types.ts","../../src/rules/index.ts","../../src/types/errors.ts","../../src/types/result.ts","../../src/api/index.ts"],"mappings":"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"}
|