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 CHANGED
@@ -13,7 +13,7 @@
13
13
  </p>
14
14
 
15
15
  <p align="center">
16
- 45 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.
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 (45)
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 (13)
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
 
@@ -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$1 = new Set([
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$1.has(d.getName()))) continue;
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) context.report({
836
- filePath: context.filePath,
837
- message: `Async method '${method.getName()}()' has no await expression.`,
838
- help: this.meta.help,
839
- line: method.getStartLineNumber(),
840
- column: 1
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) context.report({
855
- filePath: context.filePath,
856
- message: `Async function '${fn.getName() ?? "anonymous"}()' has no await expression.`,
857
- help: this.meta.help,
858
- line: fn.getStartLineNumber(),
859
- column: 1
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
 
@@ -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":"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"}
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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"}
@@ -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":"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"}
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;;;iBCoEb,QAAA,CAAA,GAAY,OAAA;;;cC9Gf,iBAAA,SAA0B,KAAA;cAC1B,OAAA;AAAA;AAAA,cAMA,kBAAA,SAA2B,iBAAA;cAC3B,OAAA;AAAA;AAAA,cAMA,SAAA,SAAkB,iBAAA;cAClB,OAAA;AAAA;AAAA,cAMA,eAAA,SAAwB,iBAAA;cACxB,OAAA;AAAA;;;UCpBI,KAAA;EAChB,KAAA;EACA,KAAA;AAAA;AAAA,UAGgB,WAAA;EAChB,SAAA;EACA,SAAA;EACA,WAAA;EACA,IAAA;EACA,WAAA;EACA,GAAA;AAAA;AAAA,UAGgB,eAAA;EAChB,UAAA,EAAY,MAAA,CAAO,QAAA;EACnB,MAAA;EACA,IAAA;EACA,KAAA;EACA,QAAA;AAAA;AAAA,UAGgB,aAAA;EAChB,KAAA;EACA,MAAA;AAAA;AAAA,UAGgB,cAAA;EAChB,WAAA,EAAa,UAAA;EACb,SAAA;EACA,OAAA,EAAS,WAAA;EACT,UAAA,EAAY,aAAA;EACZ,KAAA,EAAO,KAAA;EACP,OAAA,EAAS,eAAA;AAAA;AAAA,UAGO,gBAAA;EAChB,IAAA;EACA,MAAA,EAAQ,cAAA;AAAA;AAAA,UAGQ,cAAA;EAChB,QAAA,EAAU,cAAA;EACV,SAAA;EACA,UAAA;EACA,WAAA,EAAa,gBAAA;AAAA;;;;;;;;;;;iBCiBQ,QAAA,CACrB,IAAA,UACA,OAAA;EAAW,MAAA;AAAA,IAAsB,OAAA,CAFJ,cAAA;AR7C9B;;;;;;;;;;;AAAA,iBQgEsB,gBAAA,CACrB,IAAA,UACA,OAAA;EAAW,MAAA;AAAA,IAAsB,OAAA,CAFI,cAAA"}
@@ -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$1 = new Set([
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$1.has(d.getName()))) continue;
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) context.report({
807
- filePath: context.filePath,
808
- message: `Async method '${method.getName()}()' has no await expression.`,
809
- help: this.meta.help,
810
- line: method.getStartLineNumber(),
811
- column: 1
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) context.report({
826
- filePath: context.filePath,
827
- message: `Async function '${fn.getName() ?? "anonymous"}()' has no await expression.`,
828
- help: this.meta.help,
829
- line: fn.getStartLineNumber(),
830
- column: 1
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