nestjs-doctor 0.3.2 → 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 +130 -19
- package/dist/api/index.d.cts.map +1 -1
- package/dist/api/index.d.mts.map +1 -1
- package/dist/api/index.mjs +130 -19
- package/dist/api/index.mjs.map +1 -1
- package/dist/cli/index.mjs +130 -19
- package/package.json +1 -1
- package/skill/SKILL.md +3 -2
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);
|
|
@@ -812,6 +812,23 @@ const requireModuleBoundaries = {
|
|
|
812
812
|
|
|
813
813
|
//#endregion
|
|
814
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
|
+
}
|
|
815
832
|
const noAsyncWithoutAwait = {
|
|
816
833
|
meta: {
|
|
817
834
|
id: "correctness/no-async-without-await",
|
|
@@ -823,6 +840,9 @@ const noAsyncWithoutAwait = {
|
|
|
823
840
|
check(context) {
|
|
824
841
|
for (const cls of context.sourceFile.getClasses()) for (const method of cls.getMethods()) {
|
|
825
842
|
if (!method.isAsync()) continue;
|
|
843
|
+
if (isController(cls)) {
|
|
844
|
+
if (method.getDecorators().some((d) => HTTP_DECORATORS$2.has(d.getName()))) continue;
|
|
845
|
+
}
|
|
826
846
|
const body = method.getBody();
|
|
827
847
|
if (!body) continue;
|
|
828
848
|
if (body.getDescendantsOfKind(ts_morph.SyntaxKind.AwaitExpression).filter((expr) => {
|
|
@@ -832,13 +852,23 @@ const noAsyncWithoutAwait = {
|
|
|
832
852
|
parent = parent.getParent();
|
|
833
853
|
}
|
|
834
854
|
return true;
|
|
835
|
-
}).length === 0)
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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
|
+
}
|
|
842
872
|
}
|
|
843
873
|
for (const fn of context.sourceFile.getFunctions()) {
|
|
844
874
|
if (!fn.isAsync()) continue;
|
|
@@ -851,13 +881,23 @@ const noAsyncWithoutAwait = {
|
|
|
851
881
|
parent = parent.getParent();
|
|
852
882
|
}
|
|
853
883
|
return true;
|
|
854
|
-
}).length === 0)
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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
|
+
}
|
|
861
901
|
}
|
|
862
902
|
}
|
|
863
903
|
};
|
|
@@ -956,7 +996,7 @@ const noDuplicateRoutes = {
|
|
|
956
996
|
|
|
957
997
|
//#endregion
|
|
958
998
|
//#region src/rules/correctness/no-empty-handlers.ts
|
|
959
|
-
const HTTP_DECORATORS = new Set([
|
|
999
|
+
const HTTP_DECORATORS$1 = new Set([
|
|
960
1000
|
"Get",
|
|
961
1001
|
"Post",
|
|
962
1002
|
"Put",
|
|
@@ -978,7 +1018,7 @@ const noEmptyHandlers = {
|
|
|
978
1018
|
for (const cls of context.sourceFile.getClasses()) {
|
|
979
1019
|
if (!isController(cls)) continue;
|
|
980
1020
|
for (const method of cls.getMethods()) {
|
|
981
|
-
if (!method.getDecorators().some((d) => HTTP_DECORATORS.has(d.getName()))) continue;
|
|
1021
|
+
if (!method.getDecorators().some((d) => HTTP_DECORATORS$1.has(d.getName()))) continue;
|
|
982
1022
|
const body = method.getBody();
|
|
983
1023
|
if (!body) continue;
|
|
984
1024
|
if (body.getStatements().length === 0) context.report({
|
|
@@ -1169,6 +1209,69 @@ const noMissingPipeMethod = {
|
|
|
1169
1209
|
}
|
|
1170
1210
|
};
|
|
1171
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
|
+
|
|
1172
1275
|
//#endregion
|
|
1173
1276
|
//#region src/rules/correctness/prefer-readonly-injection.ts
|
|
1174
1277
|
const preferReadonlyInjection = {
|
|
@@ -2087,6 +2190,7 @@ const allRules = [
|
|
|
2087
2190
|
noMissingFilterCatch,
|
|
2088
2191
|
noMissingInterceptorMethod,
|
|
2089
2192
|
noAsyncWithoutAwait,
|
|
2193
|
+
preferAwaitInHandlers,
|
|
2090
2194
|
noDuplicateModuleMetadata,
|
|
2091
2195
|
noMissingModuleDecorator,
|
|
2092
2196
|
requireInjectDecorator,
|
|
@@ -2198,7 +2302,14 @@ const DEFAULT_CONFIG = {
|
|
|
2198
2302
|
"**/tests/**",
|
|
2199
2303
|
"**/__tests__/**",
|
|
2200
2304
|
"**/__mocks__/**",
|
|
2201
|
-
"**/__fixtures__/**"
|
|
2305
|
+
"**/__fixtures__/**",
|
|
2306
|
+
"**/mock/**",
|
|
2307
|
+
"**/mocks/**",
|
|
2308
|
+
"**/*.mock.ts",
|
|
2309
|
+
"**/seeder/**",
|
|
2310
|
+
"**/seeders/**",
|
|
2311
|
+
"**/*.seed.ts",
|
|
2312
|
+
"**/*.seeder.ts"
|
|
2202
2313
|
]
|
|
2203
2314
|
};
|
|
2204
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);
|
|
@@ -783,6 +783,23 @@ const requireModuleBoundaries = {
|
|
|
783
783
|
|
|
784
784
|
//#endregion
|
|
785
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
|
+
}
|
|
786
803
|
const noAsyncWithoutAwait = {
|
|
787
804
|
meta: {
|
|
788
805
|
id: "correctness/no-async-without-await",
|
|
@@ -794,6 +811,9 @@ const noAsyncWithoutAwait = {
|
|
|
794
811
|
check(context) {
|
|
795
812
|
for (const cls of context.sourceFile.getClasses()) for (const method of cls.getMethods()) {
|
|
796
813
|
if (!method.isAsync()) continue;
|
|
814
|
+
if (isController(cls)) {
|
|
815
|
+
if (method.getDecorators().some((d) => HTTP_DECORATORS$2.has(d.getName()))) continue;
|
|
816
|
+
}
|
|
797
817
|
const body = method.getBody();
|
|
798
818
|
if (!body) continue;
|
|
799
819
|
if (body.getDescendantsOfKind(SyntaxKind.AwaitExpression).filter((expr) => {
|
|
@@ -803,13 +823,23 @@ const noAsyncWithoutAwait = {
|
|
|
803
823
|
parent = parent.getParent();
|
|
804
824
|
}
|
|
805
825
|
return true;
|
|
806
|
-
}).length === 0)
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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
|
+
}
|
|
813
843
|
}
|
|
814
844
|
for (const fn of context.sourceFile.getFunctions()) {
|
|
815
845
|
if (!fn.isAsync()) continue;
|
|
@@ -822,13 +852,23 @@ const noAsyncWithoutAwait = {
|
|
|
822
852
|
parent = parent.getParent();
|
|
823
853
|
}
|
|
824
854
|
return true;
|
|
825
|
-
}).length === 0)
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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
|
+
}
|
|
832
872
|
}
|
|
833
873
|
}
|
|
834
874
|
};
|
|
@@ -927,7 +967,7 @@ const noDuplicateRoutes = {
|
|
|
927
967
|
|
|
928
968
|
//#endregion
|
|
929
969
|
//#region src/rules/correctness/no-empty-handlers.ts
|
|
930
|
-
const HTTP_DECORATORS = new Set([
|
|
970
|
+
const HTTP_DECORATORS$1 = new Set([
|
|
931
971
|
"Get",
|
|
932
972
|
"Post",
|
|
933
973
|
"Put",
|
|
@@ -949,7 +989,7 @@ const noEmptyHandlers = {
|
|
|
949
989
|
for (const cls of context.sourceFile.getClasses()) {
|
|
950
990
|
if (!isController(cls)) continue;
|
|
951
991
|
for (const method of cls.getMethods()) {
|
|
952
|
-
if (!method.getDecorators().some((d) => HTTP_DECORATORS.has(d.getName()))) continue;
|
|
992
|
+
if (!method.getDecorators().some((d) => HTTP_DECORATORS$1.has(d.getName()))) continue;
|
|
953
993
|
const body = method.getBody();
|
|
954
994
|
if (!body) continue;
|
|
955
995
|
if (body.getStatements().length === 0) context.report({
|
|
@@ -1140,6 +1180,69 @@ const noMissingPipeMethod = {
|
|
|
1140
1180
|
}
|
|
1141
1181
|
};
|
|
1142
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
|
+
|
|
1143
1246
|
//#endregion
|
|
1144
1247
|
//#region src/rules/correctness/prefer-readonly-injection.ts
|
|
1145
1248
|
const preferReadonlyInjection = {
|
|
@@ -2058,6 +2161,7 @@ const allRules = [
|
|
|
2058
2161
|
noMissingFilterCatch,
|
|
2059
2162
|
noMissingInterceptorMethod,
|
|
2060
2163
|
noAsyncWithoutAwait,
|
|
2164
|
+
preferAwaitInHandlers,
|
|
2061
2165
|
noDuplicateModuleMetadata,
|
|
2062
2166
|
noMissingModuleDecorator,
|
|
2063
2167
|
requireInjectDecorator,
|
|
@@ -2169,7 +2273,14 @@ const DEFAULT_CONFIG = {
|
|
|
2169
2273
|
"**/tests/**",
|
|
2170
2274
|
"**/__tests__/**",
|
|
2171
2275
|
"**/__mocks__/**",
|
|
2172
|
-
"**/__fixtures__/**"
|
|
2276
|
+
"**/__fixtures__/**",
|
|
2277
|
+
"**/mock/**",
|
|
2278
|
+
"**/mocks/**",
|
|
2279
|
+
"**/*.mock.ts",
|
|
2280
|
+
"**/seeder/**",
|
|
2281
|
+
"**/seeders/**",
|
|
2282
|
+
"**/*.seed.ts",
|
|
2283
|
+
"**/*.seeder.ts"
|
|
2173
2284
|
]
|
|
2174
2285
|
};
|
|
2175
2286
|
|