nestjs-doctor 0.3.1 → 0.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.
- package/README.md +6 -5
- package/dist/api/index.cjs +152 -20
- package/dist/api/index.d.cts.map +1 -1
- package/dist/api/index.d.mts.map +1 -1
- package/dist/api/index.mjs +152 -20
- package/dist/api/index.mjs.map +1 -1
- package/dist/cli/index.mjs +162 -27
- package/package.json +1 -1
- package/skill/SKILL.md +4 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
15
|
<p align="center">
|
|
16
|
-
|
|
16
|
+
46 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
|
---
|
|
@@ -166,7 +166,7 @@ Or use a `"nestjs-doctor"` key in `package.json`.
|
|
|
166
166
|
| Key | Type | Description |
|
|
167
167
|
|-----|------|-------------|
|
|
168
168
|
| `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__
|
|
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__/`, `mock/`, `mocks/`, `*.mock.ts`, `seeder/`, `seeders/`, `*.seed.ts`, `*.seeder.ts`) |
|
|
170
170
|
| `minScore` | `number` | Minimum passing score (0-100). Exits with code 1 if below threshold |
|
|
171
171
|
| `ignore.rules` | `string[]` | Rule IDs to suppress |
|
|
172
172
|
| `ignore.files` | `string[]` | Glob patterns for files whose diagnostics are hidden |
|
|
@@ -234,7 +234,7 @@ mono.combined; // Merged DiagnoseResult
|
|
|
234
234
|
|
|
235
235
|
---
|
|
236
236
|
|
|
237
|
-
## Rules (
|
|
237
|
+
## Rules (46)
|
|
238
238
|
|
|
239
239
|
### Security (9)
|
|
240
240
|
|
|
@@ -250,7 +250,7 @@ mono.combined; // Merged DiagnoseResult
|
|
|
250
250
|
| `no-exposed-stack-trace` | warning | `error.stack` exposed in responses |
|
|
251
251
|
| `require-auth-guard` | info | Controller without `@UseGuards()` |
|
|
252
252
|
|
|
253
|
-
### Correctness (
|
|
253
|
+
### Correctness (14)
|
|
254
254
|
|
|
255
255
|
| Rule | Severity | What it catches |
|
|
256
256
|
|------|----------|-----------------|
|
|
@@ -264,7 +264,8 @@ mono.combined; // Merged DiagnoseResult
|
|
|
264
264
|
| `prefer-readonly-injection` | warning | Constructor DI params missing `readonly` |
|
|
265
265
|
| `require-lifecycle-interface` | warning | Lifecycle method without corresponding interface |
|
|
266
266
|
| `no-empty-handlers` | warning | HTTP handler with empty body |
|
|
267
|
-
| `no-async-without-await` | warning | Async function/method with no `await` |
|
|
267
|
+
| `no-async-without-await` | warning | Async function/method with no `await` or redundant `async` on `new Promise()` return |
|
|
268
|
+
| `prefer-await-in-handlers` | warning | Async HTTP handler missing await — risks broken exception filters and lost stack traces |
|
|
268
269
|
| `no-duplicate-module-metadata` | warning | Duplicate entries in `@Module()` arrays |
|
|
269
270
|
| `no-missing-module-decorator` | warning | Class named `*Module` without `@Module()` |
|
|
270
271
|
|
package/dist/api/index.cjs
CHANGED
|
@@ -309,7 +309,7 @@ function isModule(cls) {
|
|
|
309
309
|
|
|
310
310
|
//#endregion
|
|
311
311
|
//#region src/rules/architecture/no-business-logic-in-controllers.ts
|
|
312
|
-
const HTTP_DECORATORS$
|
|
312
|
+
const HTTP_DECORATORS$3 = new Set([
|
|
313
313
|
"Get",
|
|
314
314
|
"Post",
|
|
315
315
|
"Put",
|
|
@@ -331,7 +331,7 @@ const noBusinessLogicInControllers = {
|
|
|
331
331
|
for (const cls of context.sourceFile.getClasses()) {
|
|
332
332
|
if (!isController(cls)) continue;
|
|
333
333
|
for (const method of cls.getMethods()) {
|
|
334
|
-
if (!method.getDecorators().some((d) => HTTP_DECORATORS$
|
|
334
|
+
if (!method.getDecorators().some((d) => HTTP_DECORATORS$3.has(d.getName()))) continue;
|
|
335
335
|
const body = method.getBody();
|
|
336
336
|
if (!body) continue;
|
|
337
337
|
const ifStatements = body.getDescendantsOfKind(ts_morph.SyntaxKind.IfStatement);
|
|
@@ -467,6 +467,18 @@ const noGodService = {
|
|
|
467
467
|
|
|
468
468
|
//#endregion
|
|
469
469
|
//#region src/rules/architecture/no-manual-instantiation.ts
|
|
470
|
+
const DI_ONLY_SUFFIXES = [
|
|
471
|
+
"Service",
|
|
472
|
+
"Repository",
|
|
473
|
+
"Gateway",
|
|
474
|
+
"Resolver"
|
|
475
|
+
];
|
|
476
|
+
const CONTEXT_AWARE_SUFFIXES = [
|
|
477
|
+
"Guard",
|
|
478
|
+
"Interceptor",
|
|
479
|
+
"Pipe",
|
|
480
|
+
"Filter"
|
|
481
|
+
];
|
|
470
482
|
const noManualInstantiation = {
|
|
471
483
|
meta: {
|
|
472
484
|
id: "architecture/no-manual-instantiation",
|
|
@@ -479,7 +491,16 @@ const noManualInstantiation = {
|
|
|
479
491
|
const newExpressions = context.sourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.NewExpression);
|
|
480
492
|
for (const expr of newExpressions) {
|
|
481
493
|
const exprText = expr.getExpression().getText();
|
|
482
|
-
|
|
494
|
+
const isDiOnly = DI_ONLY_SUFFIXES.some((s) => exprText.endsWith(s));
|
|
495
|
+
const isContextAware = CONTEXT_AWARE_SUFFIXES.some((s) => exprText.endsWith(s));
|
|
496
|
+
if (!(isDiOnly || isContextAware)) continue;
|
|
497
|
+
if (isContextAware) {
|
|
498
|
+
if (expr.getFirstAncestorByKind(ts_morph.SyntaxKind.Decorator)) continue;
|
|
499
|
+
const inMethod = expr.getFirstAncestorByKind(ts_morph.SyntaxKind.MethodDeclaration);
|
|
500
|
+
const inConstructor = expr.getFirstAncestorByKind(ts_morph.SyntaxKind.Constructor);
|
|
501
|
+
if (!(inMethod || inConstructor)) continue;
|
|
502
|
+
}
|
|
503
|
+
context.report({
|
|
483
504
|
filePath: context.filePath,
|
|
484
505
|
message: `Manual instantiation of '${exprText}' detected. Use dependency injection instead.`,
|
|
485
506
|
help: this.meta.help,
|
|
@@ -791,6 +812,23 @@ const requireModuleBoundaries = {
|
|
|
791
812
|
|
|
792
813
|
//#endregion
|
|
793
814
|
//#region src/rules/correctness/no-async-without-await.ts
|
|
815
|
+
const HTTP_DECORATORS$2 = new Set([
|
|
816
|
+
"Get",
|
|
817
|
+
"Post",
|
|
818
|
+
"Put",
|
|
819
|
+
"Delete",
|
|
820
|
+
"Patch",
|
|
821
|
+
"All",
|
|
822
|
+
"Head",
|
|
823
|
+
"Options"
|
|
824
|
+
]);
|
|
825
|
+
function returnsNewPromise$1(body) {
|
|
826
|
+
return body.getDescendantsOfKind(ts_morph.SyntaxKind.ReturnStatement).some((ret) => {
|
|
827
|
+
const expr = ret.getExpression();
|
|
828
|
+
if (!expr || expr.getKind() !== ts_morph.SyntaxKind.NewExpression) return false;
|
|
829
|
+
return expr.asKindOrThrow(ts_morph.SyntaxKind.NewExpression).getExpression().getText() === "Promise";
|
|
830
|
+
});
|
|
831
|
+
}
|
|
794
832
|
const noAsyncWithoutAwait = {
|
|
795
833
|
meta: {
|
|
796
834
|
id: "correctness/no-async-without-await",
|
|
@@ -802,6 +840,9 @@ const noAsyncWithoutAwait = {
|
|
|
802
840
|
check(context) {
|
|
803
841
|
for (const cls of context.sourceFile.getClasses()) for (const method of cls.getMethods()) {
|
|
804
842
|
if (!method.isAsync()) continue;
|
|
843
|
+
if (isController(cls)) {
|
|
844
|
+
if (method.getDecorators().some((d) => HTTP_DECORATORS$2.has(d.getName()))) continue;
|
|
845
|
+
}
|
|
805
846
|
const body = method.getBody();
|
|
806
847
|
if (!body) continue;
|
|
807
848
|
if (body.getDescendantsOfKind(ts_morph.SyntaxKind.AwaitExpression).filter((expr) => {
|
|
@@ -811,13 +852,23 @@ const noAsyncWithoutAwait = {
|
|
|
811
852
|
parent = parent.getParent();
|
|
812
853
|
}
|
|
813
854
|
return true;
|
|
814
|
-
}).length === 0)
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
855
|
+
}).length === 0) {
|
|
856
|
+
const name = method.getName();
|
|
857
|
+
if (returnsNewPromise$1(body)) context.report({
|
|
858
|
+
filePath: context.filePath,
|
|
859
|
+
message: `Async method '${name}()' returns a Promise directly — remove the async keyword.`,
|
|
860
|
+
help: "The async keyword is unnecessary when you are already constructing a Promise manually. Remove async to avoid double-wrapping.",
|
|
861
|
+
line: method.getStartLineNumber(),
|
|
862
|
+
column: 1
|
|
863
|
+
});
|
|
864
|
+
else context.report({
|
|
865
|
+
filePath: context.filePath,
|
|
866
|
+
message: `Async method '${name}()' has no await expression.`,
|
|
867
|
+
help: this.meta.help,
|
|
868
|
+
line: method.getStartLineNumber(),
|
|
869
|
+
column: 1
|
|
870
|
+
});
|
|
871
|
+
}
|
|
821
872
|
}
|
|
822
873
|
for (const fn of context.sourceFile.getFunctions()) {
|
|
823
874
|
if (!fn.isAsync()) continue;
|
|
@@ -830,13 +881,23 @@ const noAsyncWithoutAwait = {
|
|
|
830
881
|
parent = parent.getParent();
|
|
831
882
|
}
|
|
832
883
|
return true;
|
|
833
|
-
}).length === 0)
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
884
|
+
}).length === 0) {
|
|
885
|
+
const name = fn.getName() ?? "anonymous";
|
|
886
|
+
if (returnsNewPromise$1(body)) context.report({
|
|
887
|
+
filePath: context.filePath,
|
|
888
|
+
message: `Async function '${name}()' returns a Promise directly — remove the async keyword.`,
|
|
889
|
+
help: "The async keyword is unnecessary when you are already constructing a Promise manually. Remove async to avoid double-wrapping.",
|
|
890
|
+
line: fn.getStartLineNumber(),
|
|
891
|
+
column: 1
|
|
892
|
+
});
|
|
893
|
+
else context.report({
|
|
894
|
+
filePath: context.filePath,
|
|
895
|
+
message: `Async function '${name}()' has no await expression.`,
|
|
896
|
+
help: this.meta.help,
|
|
897
|
+
line: fn.getStartLineNumber(),
|
|
898
|
+
column: 1
|
|
899
|
+
});
|
|
900
|
+
}
|
|
840
901
|
}
|
|
841
902
|
}
|
|
842
903
|
};
|
|
@@ -935,7 +996,7 @@ const noDuplicateRoutes = {
|
|
|
935
996
|
|
|
936
997
|
//#endregion
|
|
937
998
|
//#region src/rules/correctness/no-empty-handlers.ts
|
|
938
|
-
const HTTP_DECORATORS = new Set([
|
|
999
|
+
const HTTP_DECORATORS$1 = new Set([
|
|
939
1000
|
"Get",
|
|
940
1001
|
"Post",
|
|
941
1002
|
"Put",
|
|
@@ -957,7 +1018,7 @@ const noEmptyHandlers = {
|
|
|
957
1018
|
for (const cls of context.sourceFile.getClasses()) {
|
|
958
1019
|
if (!isController(cls)) continue;
|
|
959
1020
|
for (const method of cls.getMethods()) {
|
|
960
|
-
if (!method.getDecorators().some((d) => HTTP_DECORATORS.has(d.getName()))) continue;
|
|
1021
|
+
if (!method.getDecorators().some((d) => HTTP_DECORATORS$1.has(d.getName()))) continue;
|
|
961
1022
|
const body = method.getBody();
|
|
962
1023
|
if (!body) continue;
|
|
963
1024
|
if (body.getStatements().length === 0) context.report({
|
|
@@ -1148,6 +1209,69 @@ const noMissingPipeMethod = {
|
|
|
1148
1209
|
}
|
|
1149
1210
|
};
|
|
1150
1211
|
|
|
1212
|
+
//#endregion
|
|
1213
|
+
//#region src/rules/correctness/prefer-await-in-handlers.ts
|
|
1214
|
+
const HTTP_DECORATORS = new Set([
|
|
1215
|
+
"Get",
|
|
1216
|
+
"Post",
|
|
1217
|
+
"Put",
|
|
1218
|
+
"Delete",
|
|
1219
|
+
"Patch",
|
|
1220
|
+
"All",
|
|
1221
|
+
"Head",
|
|
1222
|
+
"Options"
|
|
1223
|
+
]);
|
|
1224
|
+
function returnsNewPromise(body) {
|
|
1225
|
+
return body.getDescendantsOfKind(ts_morph.SyntaxKind.ReturnStatement).some((ret) => {
|
|
1226
|
+
const expr = ret.getExpression();
|
|
1227
|
+
if (!expr || expr.getKind() !== ts_morph.SyntaxKind.NewExpression) return false;
|
|
1228
|
+
return expr.asKindOrThrow(ts_morph.SyntaxKind.NewExpression).getExpression().getText() === "Promise";
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
const preferAwaitInHandlers = {
|
|
1232
|
+
meta: {
|
|
1233
|
+
id: "correctness/prefer-await-in-handlers",
|
|
1234
|
+
category: "correctness",
|
|
1235
|
+
severity: "warning",
|
|
1236
|
+
description: "Async HTTP handler missing await — risks broken exception filters and lost stack traces",
|
|
1237
|
+
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."
|
|
1238
|
+
},
|
|
1239
|
+
check(context) {
|
|
1240
|
+
for (const cls of context.sourceFile.getClasses()) {
|
|
1241
|
+
if (!isController(cls)) continue;
|
|
1242
|
+
for (const method of cls.getMethods()) {
|
|
1243
|
+
if (!(method.getDecorators().some((d) => HTTP_DECORATORS.has(d.getName())) && method.isAsync())) continue;
|
|
1244
|
+
const body = method.getBody();
|
|
1245
|
+
if (!body) continue;
|
|
1246
|
+
if (body.getDescendantsOfKind(ts_morph.SyntaxKind.AwaitExpression).filter((expr) => {
|
|
1247
|
+
let parent = expr.getParent();
|
|
1248
|
+
while (parent && parent !== body) {
|
|
1249
|
+
if (parent.getKind() === ts_morph.SyntaxKind.ArrowFunction || parent.getKind() === ts_morph.SyntaxKind.FunctionExpression || parent.getKind() === ts_morph.SyntaxKind.FunctionDeclaration) return false;
|
|
1250
|
+
parent = parent.getParent();
|
|
1251
|
+
}
|
|
1252
|
+
return true;
|
|
1253
|
+
}).length === 0) {
|
|
1254
|
+
const name = method.getName();
|
|
1255
|
+
if (returnsNewPromise(body)) context.report({
|
|
1256
|
+
filePath: context.filePath,
|
|
1257
|
+
message: `Handler '${name}()' returns a Promise directly — remove async and return the promise, or await the resolved value.`,
|
|
1258
|
+
help: this.meta.help,
|
|
1259
|
+
line: method.getStartLineNumber(),
|
|
1260
|
+
column: 1
|
|
1261
|
+
});
|
|
1262
|
+
else context.report({
|
|
1263
|
+
filePath: context.filePath,
|
|
1264
|
+
message: `Async handler '${name}()' does not await any expression.`,
|
|
1265
|
+
help: this.meta.help,
|
|
1266
|
+
line: method.getStartLineNumber(),
|
|
1267
|
+
column: 1
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
1274
|
+
|
|
1151
1275
|
//#endregion
|
|
1152
1276
|
//#region src/rules/correctness/prefer-readonly-injection.ts
|
|
1153
1277
|
const preferReadonlyInjection = {
|
|
@@ -2066,6 +2190,7 @@ const allRules = [
|
|
|
2066
2190
|
noMissingFilterCatch,
|
|
2067
2191
|
noMissingInterceptorMethod,
|
|
2068
2192
|
noAsyncWithoutAwait,
|
|
2193
|
+
preferAwaitInHandlers,
|
|
2069
2194
|
noDuplicateModuleMetadata,
|
|
2070
2195
|
noMissingModuleDecorator,
|
|
2071
2196
|
requireInjectDecorator,
|
|
@@ -2177,7 +2302,14 @@ const DEFAULT_CONFIG = {
|
|
|
2177
2302
|
"**/tests/**",
|
|
2178
2303
|
"**/__tests__/**",
|
|
2179
2304
|
"**/__mocks__/**",
|
|
2180
|
-
"**/__fixtures__/**"
|
|
2305
|
+
"**/__fixtures__/**",
|
|
2306
|
+
"**/mock/**",
|
|
2307
|
+
"**/mocks/**",
|
|
2308
|
+
"**/*.mock.ts",
|
|
2309
|
+
"**/seeder/**",
|
|
2310
|
+
"**/seeders/**",
|
|
2311
|
+
"**/*.seed.ts",
|
|
2312
|
+
"**/*.seeder.ts"
|
|
2181
2313
|
]
|
|
2182
2314
|
};
|
|
2183
2315
|
|
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,kBAAA;IACA,gBAAA;IACA,iBAAA;IACA,cAAA;EAAA;AAAA;;;KCjBU,SAAA;AAAA,UAEK,QAAA;EAChB,QAAA,EAAU,QAAA;EACV,WAAA;EACA,IAAA;EACA,EAAA;EACA,KAAA,GAAQ,SAAA;EACR,QAAA,EAAU,QAAA;AAAA;AAAA,UAGM,WAAA;EAChB,QAAA;EACA,MAAA,CAAO,UAAA,EAAY,IAAA,CAAK,UAAA;EACxB,UAAA,EAAY,UAAA;AAAA;AAAA,UAGI,kBAAA;EAChB,MAAA,EAAQ,kBAAA;EACR,KAAA;EACA,WAAA,EAAa,WAAA;EACb,OAAA,EAAS,OAAA;EACT,SAAA,EAAW,GAAA,SAAY,YAAA;EACvB,MAAA,CAAO,UAAA,EAAY,IAAA,CAAK,UAAA;AAAA;AAAA,UAGR,IAAA;EAChB,KAAA,CAAM,OAAA,EAAS,WAAA;EACf,IAAA,EAAM,QAAA;AAAA;AAAA,UAGU,WAAA;EAChB,KAAA,CAAM,OAAA,EAAS,kBAAA;EACf,IAAA,EAAM,QAAA;AAAA;AAAA,KAGK,OAAA,GAAU,IAAA,GAAO,WAAA;;;
|
|
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.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,kBAAA;IACA,gBAAA;IACA,iBAAA;IACA,cAAA;EAAA;AAAA;;;KCjBU,SAAA;AAAA,UAEK,QAAA;EAChB,QAAA,EAAU,QAAA;EACV,WAAA;EACA,IAAA;EACA,EAAA;EACA,KAAA,GAAQ,SAAA;EACR,QAAA,EAAU,QAAA;AAAA;AAAA,UAGM,WAAA;EAChB,QAAA;EACA,MAAA,CAAO,UAAA,EAAY,IAAA,CAAK,UAAA;EACxB,UAAA,EAAY,UAAA;AAAA;AAAA,UAGI,kBAAA;EAChB,MAAA,EAAQ,kBAAA;EACR,KAAA;EACA,WAAA,EAAa,WAAA;EACb,OAAA,EAAS,OAAA;EACT,SAAA,EAAW,GAAA,SAAY,YAAA;EACvB,MAAA,CAAO,UAAA,EAAY,IAAA,CAAK,UAAA;AAAA;AAAA,UAGR,IAAA;EAChB,KAAA,CAAM,OAAA,EAAS,WAAA;EACf,IAAA,EAAM,QAAA;AAAA;AAAA,UAGU,WAAA;EAChB,KAAA,CAAM,OAAA,EAAS,kBAAA;EACf,IAAA,EAAM,QAAA;AAAA;AAAA,KAGK,OAAA,GAAU,IAAA,GAAO,WAAA;;;
|
|
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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"}
|
package/dist/api/index.mjs
CHANGED
|
@@ -280,7 +280,7 @@ function isModule(cls) {
|
|
|
280
280
|
|
|
281
281
|
//#endregion
|
|
282
282
|
//#region src/rules/architecture/no-business-logic-in-controllers.ts
|
|
283
|
-
const HTTP_DECORATORS$
|
|
283
|
+
const HTTP_DECORATORS$3 = new Set([
|
|
284
284
|
"Get",
|
|
285
285
|
"Post",
|
|
286
286
|
"Put",
|
|
@@ -302,7 +302,7 @@ const noBusinessLogicInControllers = {
|
|
|
302
302
|
for (const cls of context.sourceFile.getClasses()) {
|
|
303
303
|
if (!isController(cls)) continue;
|
|
304
304
|
for (const method of cls.getMethods()) {
|
|
305
|
-
if (!method.getDecorators().some((d) => HTTP_DECORATORS$
|
|
305
|
+
if (!method.getDecorators().some((d) => HTTP_DECORATORS$3.has(d.getName()))) continue;
|
|
306
306
|
const body = method.getBody();
|
|
307
307
|
if (!body) continue;
|
|
308
308
|
const ifStatements = body.getDescendantsOfKind(SyntaxKind.IfStatement);
|
|
@@ -438,6 +438,18 @@ const noGodService = {
|
|
|
438
438
|
|
|
439
439
|
//#endregion
|
|
440
440
|
//#region src/rules/architecture/no-manual-instantiation.ts
|
|
441
|
+
const DI_ONLY_SUFFIXES = [
|
|
442
|
+
"Service",
|
|
443
|
+
"Repository",
|
|
444
|
+
"Gateway",
|
|
445
|
+
"Resolver"
|
|
446
|
+
];
|
|
447
|
+
const CONTEXT_AWARE_SUFFIXES = [
|
|
448
|
+
"Guard",
|
|
449
|
+
"Interceptor",
|
|
450
|
+
"Pipe",
|
|
451
|
+
"Filter"
|
|
452
|
+
];
|
|
441
453
|
const noManualInstantiation = {
|
|
442
454
|
meta: {
|
|
443
455
|
id: "architecture/no-manual-instantiation",
|
|
@@ -450,7 +462,16 @@ const noManualInstantiation = {
|
|
|
450
462
|
const newExpressions = context.sourceFile.getDescendantsOfKind(SyntaxKind.NewExpression);
|
|
451
463
|
for (const expr of newExpressions) {
|
|
452
464
|
const exprText = expr.getExpression().getText();
|
|
453
|
-
|
|
465
|
+
const isDiOnly = DI_ONLY_SUFFIXES.some((s) => exprText.endsWith(s));
|
|
466
|
+
const isContextAware = CONTEXT_AWARE_SUFFIXES.some((s) => exprText.endsWith(s));
|
|
467
|
+
if (!(isDiOnly || isContextAware)) continue;
|
|
468
|
+
if (isContextAware) {
|
|
469
|
+
if (expr.getFirstAncestorByKind(SyntaxKind.Decorator)) continue;
|
|
470
|
+
const inMethod = expr.getFirstAncestorByKind(SyntaxKind.MethodDeclaration);
|
|
471
|
+
const inConstructor = expr.getFirstAncestorByKind(SyntaxKind.Constructor);
|
|
472
|
+
if (!(inMethod || inConstructor)) continue;
|
|
473
|
+
}
|
|
474
|
+
context.report({
|
|
454
475
|
filePath: context.filePath,
|
|
455
476
|
message: `Manual instantiation of '${exprText}' detected. Use dependency injection instead.`,
|
|
456
477
|
help: this.meta.help,
|
|
@@ -762,6 +783,23 @@ const requireModuleBoundaries = {
|
|
|
762
783
|
|
|
763
784
|
//#endregion
|
|
764
785
|
//#region src/rules/correctness/no-async-without-await.ts
|
|
786
|
+
const HTTP_DECORATORS$2 = new Set([
|
|
787
|
+
"Get",
|
|
788
|
+
"Post",
|
|
789
|
+
"Put",
|
|
790
|
+
"Delete",
|
|
791
|
+
"Patch",
|
|
792
|
+
"All",
|
|
793
|
+
"Head",
|
|
794
|
+
"Options"
|
|
795
|
+
]);
|
|
796
|
+
function returnsNewPromise$1(body) {
|
|
797
|
+
return body.getDescendantsOfKind(SyntaxKind.ReturnStatement).some((ret) => {
|
|
798
|
+
const expr = ret.getExpression();
|
|
799
|
+
if (!expr || expr.getKind() !== SyntaxKind.NewExpression) return false;
|
|
800
|
+
return expr.asKindOrThrow(SyntaxKind.NewExpression).getExpression().getText() === "Promise";
|
|
801
|
+
});
|
|
802
|
+
}
|
|
765
803
|
const noAsyncWithoutAwait = {
|
|
766
804
|
meta: {
|
|
767
805
|
id: "correctness/no-async-without-await",
|
|
@@ -773,6 +811,9 @@ const noAsyncWithoutAwait = {
|
|
|
773
811
|
check(context) {
|
|
774
812
|
for (const cls of context.sourceFile.getClasses()) for (const method of cls.getMethods()) {
|
|
775
813
|
if (!method.isAsync()) continue;
|
|
814
|
+
if (isController(cls)) {
|
|
815
|
+
if (method.getDecorators().some((d) => HTTP_DECORATORS$2.has(d.getName()))) continue;
|
|
816
|
+
}
|
|
776
817
|
const body = method.getBody();
|
|
777
818
|
if (!body) continue;
|
|
778
819
|
if (body.getDescendantsOfKind(SyntaxKind.AwaitExpression).filter((expr) => {
|
|
@@ -782,13 +823,23 @@ const noAsyncWithoutAwait = {
|
|
|
782
823
|
parent = parent.getParent();
|
|
783
824
|
}
|
|
784
825
|
return true;
|
|
785
|
-
}).length === 0)
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
826
|
+
}).length === 0) {
|
|
827
|
+
const name = method.getName();
|
|
828
|
+
if (returnsNewPromise$1(body)) context.report({
|
|
829
|
+
filePath: context.filePath,
|
|
830
|
+
message: `Async method '${name}()' returns a Promise directly — remove the async keyword.`,
|
|
831
|
+
help: "The async keyword is unnecessary when you are already constructing a Promise manually. Remove async to avoid double-wrapping.",
|
|
832
|
+
line: method.getStartLineNumber(),
|
|
833
|
+
column: 1
|
|
834
|
+
});
|
|
835
|
+
else context.report({
|
|
836
|
+
filePath: context.filePath,
|
|
837
|
+
message: `Async method '${name}()' has no await expression.`,
|
|
838
|
+
help: this.meta.help,
|
|
839
|
+
line: method.getStartLineNumber(),
|
|
840
|
+
column: 1
|
|
841
|
+
});
|
|
842
|
+
}
|
|
792
843
|
}
|
|
793
844
|
for (const fn of context.sourceFile.getFunctions()) {
|
|
794
845
|
if (!fn.isAsync()) continue;
|
|
@@ -801,13 +852,23 @@ const noAsyncWithoutAwait = {
|
|
|
801
852
|
parent = parent.getParent();
|
|
802
853
|
}
|
|
803
854
|
return true;
|
|
804
|
-
}).length === 0)
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
855
|
+
}).length === 0) {
|
|
856
|
+
const name = fn.getName() ?? "anonymous";
|
|
857
|
+
if (returnsNewPromise$1(body)) context.report({
|
|
858
|
+
filePath: context.filePath,
|
|
859
|
+
message: `Async function '${name}()' returns a Promise directly — remove the async keyword.`,
|
|
860
|
+
help: "The async keyword is unnecessary when you are already constructing a Promise manually. Remove async to avoid double-wrapping.",
|
|
861
|
+
line: fn.getStartLineNumber(),
|
|
862
|
+
column: 1
|
|
863
|
+
});
|
|
864
|
+
else context.report({
|
|
865
|
+
filePath: context.filePath,
|
|
866
|
+
message: `Async function '${name}()' has no await expression.`,
|
|
867
|
+
help: this.meta.help,
|
|
868
|
+
line: fn.getStartLineNumber(),
|
|
869
|
+
column: 1
|
|
870
|
+
});
|
|
871
|
+
}
|
|
811
872
|
}
|
|
812
873
|
}
|
|
813
874
|
};
|
|
@@ -906,7 +967,7 @@ const noDuplicateRoutes = {
|
|
|
906
967
|
|
|
907
968
|
//#endregion
|
|
908
969
|
//#region src/rules/correctness/no-empty-handlers.ts
|
|
909
|
-
const HTTP_DECORATORS = new Set([
|
|
970
|
+
const HTTP_DECORATORS$1 = new Set([
|
|
910
971
|
"Get",
|
|
911
972
|
"Post",
|
|
912
973
|
"Put",
|
|
@@ -928,7 +989,7 @@ const noEmptyHandlers = {
|
|
|
928
989
|
for (const cls of context.sourceFile.getClasses()) {
|
|
929
990
|
if (!isController(cls)) continue;
|
|
930
991
|
for (const method of cls.getMethods()) {
|
|
931
|
-
if (!method.getDecorators().some((d) => HTTP_DECORATORS.has(d.getName()))) continue;
|
|
992
|
+
if (!method.getDecorators().some((d) => HTTP_DECORATORS$1.has(d.getName()))) continue;
|
|
932
993
|
const body = method.getBody();
|
|
933
994
|
if (!body) continue;
|
|
934
995
|
if (body.getStatements().length === 0) context.report({
|
|
@@ -1119,6 +1180,69 @@ const noMissingPipeMethod = {
|
|
|
1119
1180
|
}
|
|
1120
1181
|
};
|
|
1121
1182
|
|
|
1183
|
+
//#endregion
|
|
1184
|
+
//#region src/rules/correctness/prefer-await-in-handlers.ts
|
|
1185
|
+
const HTTP_DECORATORS = new Set([
|
|
1186
|
+
"Get",
|
|
1187
|
+
"Post",
|
|
1188
|
+
"Put",
|
|
1189
|
+
"Delete",
|
|
1190
|
+
"Patch",
|
|
1191
|
+
"All",
|
|
1192
|
+
"Head",
|
|
1193
|
+
"Options"
|
|
1194
|
+
]);
|
|
1195
|
+
function returnsNewPromise(body) {
|
|
1196
|
+
return body.getDescendantsOfKind(SyntaxKind.ReturnStatement).some((ret) => {
|
|
1197
|
+
const expr = ret.getExpression();
|
|
1198
|
+
if (!expr || expr.getKind() !== SyntaxKind.NewExpression) return false;
|
|
1199
|
+
return expr.asKindOrThrow(SyntaxKind.NewExpression).getExpression().getText() === "Promise";
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
const preferAwaitInHandlers = {
|
|
1203
|
+
meta: {
|
|
1204
|
+
id: "correctness/prefer-await-in-handlers",
|
|
1205
|
+
category: "correctness",
|
|
1206
|
+
severity: "warning",
|
|
1207
|
+
description: "Async HTTP handler missing await — risks broken exception filters and lost stack traces",
|
|
1208
|
+
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."
|
|
1209
|
+
},
|
|
1210
|
+
check(context) {
|
|
1211
|
+
for (const cls of context.sourceFile.getClasses()) {
|
|
1212
|
+
if (!isController(cls)) continue;
|
|
1213
|
+
for (const method of cls.getMethods()) {
|
|
1214
|
+
if (!(method.getDecorators().some((d) => HTTP_DECORATORS.has(d.getName())) && method.isAsync())) continue;
|
|
1215
|
+
const body = method.getBody();
|
|
1216
|
+
if (!body) continue;
|
|
1217
|
+
if (body.getDescendantsOfKind(SyntaxKind.AwaitExpression).filter((expr) => {
|
|
1218
|
+
let parent = expr.getParent();
|
|
1219
|
+
while (parent && parent !== body) {
|
|
1220
|
+
if (parent.getKind() === SyntaxKind.ArrowFunction || parent.getKind() === SyntaxKind.FunctionExpression || parent.getKind() === SyntaxKind.FunctionDeclaration) return false;
|
|
1221
|
+
parent = parent.getParent();
|
|
1222
|
+
}
|
|
1223
|
+
return true;
|
|
1224
|
+
}).length === 0) {
|
|
1225
|
+
const name = method.getName();
|
|
1226
|
+
if (returnsNewPromise(body)) context.report({
|
|
1227
|
+
filePath: context.filePath,
|
|
1228
|
+
message: `Handler '${name}()' returns a Promise directly — remove async and return the promise, or await the resolved value.`,
|
|
1229
|
+
help: this.meta.help,
|
|
1230
|
+
line: method.getStartLineNumber(),
|
|
1231
|
+
column: 1
|
|
1232
|
+
});
|
|
1233
|
+
else context.report({
|
|
1234
|
+
filePath: context.filePath,
|
|
1235
|
+
message: `Async handler '${name}()' does not await any expression.`,
|
|
1236
|
+
help: this.meta.help,
|
|
1237
|
+
line: method.getStartLineNumber(),
|
|
1238
|
+
column: 1
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
|
|
1122
1246
|
//#endregion
|
|
1123
1247
|
//#region src/rules/correctness/prefer-readonly-injection.ts
|
|
1124
1248
|
const preferReadonlyInjection = {
|
|
@@ -2037,6 +2161,7 @@ const allRules = [
|
|
|
2037
2161
|
noMissingFilterCatch,
|
|
2038
2162
|
noMissingInterceptorMethod,
|
|
2039
2163
|
noAsyncWithoutAwait,
|
|
2164
|
+
preferAwaitInHandlers,
|
|
2040
2165
|
noDuplicateModuleMetadata,
|
|
2041
2166
|
noMissingModuleDecorator,
|
|
2042
2167
|
requireInjectDecorator,
|
|
@@ -2148,7 +2273,14 @@ const DEFAULT_CONFIG = {
|
|
|
2148
2273
|
"**/tests/**",
|
|
2149
2274
|
"**/__tests__/**",
|
|
2150
2275
|
"**/__mocks__/**",
|
|
2151
|
-
"**/__fixtures__/**"
|
|
2276
|
+
"**/__fixtures__/**",
|
|
2277
|
+
"**/mock/**",
|
|
2278
|
+
"**/mocks/**",
|
|
2279
|
+
"**/*.mock.ts",
|
|
2280
|
+
"**/seeder/**",
|
|
2281
|
+
"**/seeders/**",
|
|
2282
|
+
"**/*.seed.ts",
|
|
2283
|
+
"**/*.seeder.ts"
|
|
2152
2284
|
]
|
|
2153
2285
|
};
|
|
2154
2286
|
|