nestjs-doctor 0.2.0 → 0.3.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 CHANGED
@@ -13,7 +13,7 @@
13
13
  </p>
14
14
 
15
15
  <p align="center">
16
- 47 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
+ 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.
17
17
  </p>
18
18
 
19
19
  ---
@@ -96,11 +96,45 @@ Usage: nestjs-doctor [directory] [options]
96
96
  --json JSON output (for tooling)
97
97
  --min-score <n> Minimum passing score (0-100). Exits with code 1 if below threshold
98
98
  --config <p> Path to config file
99
+ --init Set up the /nestjs-doctor Claude Code skill
99
100
  -h, --help Show help
100
101
  ```
101
102
 
102
103
  ---
103
104
 
105
+ ## Claude Code
106
+
107
+ nestjs-doctor ships with a [Claude Code skill](https://docs.anthropic.com/en/docs/claude-code/skills) that scans your codebase and fixes issues interactively.
108
+
109
+ ### Setup
110
+
111
+ If you haven't already, install as a devDependency:
112
+
113
+ ```bash
114
+ pnpm add -D nestjs-doctor
115
+ ```
116
+
117
+ Then scaffold the skill:
118
+
119
+ ```bash
120
+ npx nestjs-doctor --init
121
+ ```
122
+
123
+ This creates `.claude/skills/nestjs-doctor/SKILL.md` in your project. Commit it so every contributor gets the skill automatically.
124
+
125
+ ### Usage
126
+
127
+ In Claude Code, type:
128
+
129
+ ```
130
+ /nestjs-doctor
131
+ /nestjs-doctor src/
132
+ ```
133
+
134
+ Claude will scan your codebase, present a prioritized health report, and offer to fix every issue found.
135
+
136
+ ---
137
+
104
138
  ## Configuration
105
139
 
106
140
  Optional. Create `nestjs-doctor.config.json` in your project root:
@@ -132,7 +166,7 @@ Or use a `"nestjs-doctor"` key in `package.json`.
132
166
  | Key | Type | Description |
133
167
  |-----|------|-------------|
134
168
  | `include` | `string[]` | Glob patterns to scan (default: `["**/*.ts"]`) |
135
- | `exclude` | `string[]` | Glob patterns to skip (default includes `node_modules`, `dist`, test files) |
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__/`) |
136
170
  | `minScore` | `number` | Minimum passing score (0-100). Exits with code 1 if below threshold |
137
171
  | `ignore.rules` | `string[]` | Rule IDs to suppress |
138
172
  | `ignore.files` | `string[]` | Glob patterns for files whose diagnostics are hidden |
@@ -200,15 +234,13 @@ mono.combined; // Merged DiagnoseResult
200
234
 
201
235
  ---
202
236
 
203
- ## Rules (47)
237
+ ## Rules (45)
204
238
 
205
- ### Security (11)
239
+ ### Security (9)
206
240
 
207
241
  | Rule | Severity | What it catches |
208
242
  |------|----------|-----------------|
209
243
  | `no-hardcoded-secrets` | error | API keys, tokens, passwords in source code |
210
- | `no-wildcard-cors` | error | CORS with `origin: '*'` or `origin: true` |
211
- | `no-unsafe-raw-query` | error | Template literal interpolation in raw SQL |
212
244
  | `no-eval` | error | `eval()` or `new Function()` usage |
213
245
  | `no-csrf-disabled` | error | Explicitly disabling CSRF protection |
214
246
  | `no-dangerous-redirects` | error | Redirects with user-controlled input |
@@ -223,7 +255,7 @@ mono.combined; // Merged DiagnoseResult
223
255
  | Rule | Severity | What it catches |
224
256
  |------|----------|-----------------|
225
257
  | `no-missing-injectable` | error | Provider in module missing `@Injectable()` |
226
- | `no-duplicate-routes` | error | Same method + path twice in a controller |
258
+ | `no-duplicate-routes` | error | Same method + path + version twice in a controller |
227
259
  | `no-missing-guard-method` | error | Guard class missing `canActivate()` |
228
260
  | `no-missing-pipe-method` | error | Pipe class missing `transform()` |
229
261
  | `no-missing-filter-catch` | error | `@Catch()` class missing `catch()` |
@@ -905,7 +905,7 @@ const noDuplicateRoutes = {
905
905
  id: "correctness/no-duplicate-routes",
906
906
  category: "correctness",
907
907
  severity: "error",
908
- description: "Same HTTP method + route path should not appear twice in a single controller",
908
+ description: "Same HTTP method + route path + version should not appear twice in a single controller",
909
909
  help: "Remove or rename one of the duplicate route handlers."
910
910
  },
911
911
  check(context) {
@@ -917,7 +917,8 @@ const noDuplicateRoutes = {
917
917
  if (!HTTP_METHOD_DECORATORS.has(decoratorName)) continue;
918
918
  const args = decorator.getArguments();
919
919
  const path = args.length > 0 ? args[0].getText() : "\"\"";
920
- const routeKey = `${decoratorName}:${path}`;
920
+ const versionDecorator = method.getDecorator("Version");
921
+ const routeKey = `${decoratorName}:${path}:${versionDecorator ? versionDecorator.getArguments()[0]?.getText() ?? "" : ""}`;
921
922
  const existing = routeMap.get(routeKey);
922
923
  if (existing) context.report({
923
924
  filePath: context.filePath,
@@ -984,6 +985,7 @@ const noMissingFilterCatch = {
984
985
  check(context) {
985
986
  for (const cls of context.sourceFile.getClasses()) {
986
987
  if (!hasDecorator(cls, "Catch")) continue;
988
+ if (cls.getExtends()) continue;
987
989
  if (!cls.getMethods().some((m) => m.getName() === "catch")) context.report({
988
990
  filePath: context.filePath,
989
991
  message: `Exception filter '${cls.getName()}' has @Catch() but is missing the 'catch()' method.`,
@@ -1010,6 +1012,7 @@ const noMissingGuardMethod = {
1010
1012
  const name = cls.getName() ?? "";
1011
1013
  if (!name.endsWith("Guard")) continue;
1012
1014
  if (!hasDecorator(cls, "Injectable")) continue;
1015
+ if (cls.getExtends()) continue;
1013
1016
  if (!cls.getMethods().some((m) => m.getName() === "canActivate")) context.report({
1014
1017
  filePath: context.filePath,
1015
1018
  message: `Guard '${name}' is missing the 'canActivate()' method.`,
@@ -1080,6 +1083,7 @@ const noMissingInterceptorMethod = {
1080
1083
  const name = cls.getName() ?? "";
1081
1084
  if (!name.endsWith("Interceptor")) continue;
1082
1085
  if (!hasDecorator(cls, "Injectable")) continue;
1086
+ if (cls.getExtends()) continue;
1083
1087
  if (!cls.getMethods().some((m) => m.getName() === "intercept")) context.report({
1084
1088
  filePath: context.filePath,
1085
1089
  message: `Interceptor '${name}' is missing the 'intercept()' method.`,
@@ -1132,6 +1136,7 @@ const noMissingPipeMethod = {
1132
1136
  const name = cls.getName() ?? "";
1133
1137
  if (!name.endsWith("Pipe")) continue;
1134
1138
  if (!hasDecorator(cls, "Injectable")) continue;
1139
+ if (cls.getExtends()) continue;
1135
1140
  if (!cls.getMethods().some((m) => m.getName() === "transform")) context.report({
1136
1141
  filePath: context.filePath,
1137
1142
  message: `Pipe '${name}' is missing the 'transform()' method.`,
@@ -1825,7 +1830,7 @@ const noExposedStackTrace = {
1825
1830
  //#region src/rules/security/no-hardcoded-secrets.ts
1826
1831
  const SECRET_PATTERNS = [
1827
1832
  {
1828
- pattern: /^[A-Za-z0-9+/]{40,}={0,2}$/,
1833
+ pattern: /^(?=.*\d)[A-Za-z0-9+/]{40,}={0,2}$/,
1829
1834
  name: "Base64 key"
1830
1835
  },
1831
1836
  {
@@ -1880,8 +1885,15 @@ const PLACEHOLDER_VALUES = new Set([
1880
1885
  "changeme",
1881
1886
  "password"
1882
1887
  ]);
1888
+ const DOT_SEPARATED_CONSTANT = /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$/;
1883
1889
  function isSuspiciousValue(value) {
1884
- return value.length >= 8 && !value.includes("${") && !value.startsWith("process.env") && !PLACEHOLDER_VALUES.has(value);
1890
+ if (value.length < 8) return false;
1891
+ if (value.includes("${")) return false;
1892
+ if (value.startsWith("process.env")) return false;
1893
+ if (PLACEHOLDER_VALUES.has(value)) return false;
1894
+ if (value.includes(" ")) return false;
1895
+ if (DOT_SEPARATED_CONSTANT.test(value)) return false;
1896
+ return true;
1885
1897
  }
1886
1898
  function hasSuspiciousName(name) {
1887
1899
  return VARIABLE_NAME_PATTERNS.some((p) => p.test(name));
@@ -1942,41 +1954,6 @@ const noHardcodedSecrets = {
1942
1954
  }
1943
1955
  };
1944
1956
 
1945
- //#endregion
1946
- //#region src/rules/security/no-unsafe-raw-query.ts
1947
- const RAW_QUERY_METHODS = new Set([
1948
- "$executeRaw",
1949
- "$queryRaw",
1950
- "$executeRawUnsafe",
1951
- "$queryRawUnsafe",
1952
- "query"
1953
- ]);
1954
- const noUnsafeRawQuery = {
1955
- meta: {
1956
- id: "security/no-unsafe-raw-query",
1957
- category: "security",
1958
- severity: "error",
1959
- description: "Raw SQL queries with template literal interpolation are a SQL injection risk",
1960
- help: "Use parameterized queries or Prisma's tagged template (Prisma.sql`...`) instead of string interpolation."
1961
- },
1962
- check(context) {
1963
- const callExpressions = context.sourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.CallExpression);
1964
- for (const call of callExpressions) {
1965
- const methodName = call.getExpression().getText().split(".").pop() ?? "";
1966
- if (!RAW_QUERY_METHODS.has(methodName)) continue;
1967
- const args = call.getArguments();
1968
- if (args.length === 0) continue;
1969
- if (args[0].getKind() === ts_morph.SyntaxKind.TemplateExpression) context.report({
1970
- filePath: context.filePath,
1971
- message: `Raw SQL query '${methodName}()' uses template literal interpolation — SQL injection risk.`,
1972
- help: this.meta.help,
1973
- line: call.getStartLineNumber(),
1974
- column: 1
1975
- });
1976
- }
1977
- }
1978
- };
1979
-
1980
1957
  //#endregion
1981
1958
  //#region src/rules/security/no-weak-crypto.ts
1982
1959
  const WEAK_ALGORITHMS = new Set(["md5", "sha1"]);
@@ -2008,34 +1985,6 @@ const noWeakCrypto = {
2008
1985
  }
2009
1986
  };
2010
1987
 
2011
- //#endregion
2012
- //#region src/rules/security/no-wildcard-cors.ts
2013
- const noWildcardCors = {
2014
- meta: {
2015
- id: "security/no-wildcard-cors",
2016
- category: "security",
2017
- severity: "error",
2018
- description: "CORS should not be configured with origin: '*' or origin: true — allows any domain",
2019
- help: "Specify allowed origins explicitly instead of using wildcard CORS."
2020
- },
2021
- check(context) {
2022
- const propertyAssignments = context.sourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.PropertyAssignment);
2023
- for (const prop of propertyAssignments) {
2024
- if (prop.getName() !== "origin") continue;
2025
- const initializer = prop.getInitializer();
2026
- if (!initializer) continue;
2027
- const text = initializer.getText();
2028
- if (text === "'*'" || text === "\"*\"" || text === "true") context.report({
2029
- filePath: context.filePath,
2030
- message: `CORS configured with origin: ${text} — allows requests from any domain.`,
2031
- help: this.meta.help,
2032
- line: prop.getStartLineNumber(),
2033
- column: 1
2034
- });
2035
- }
2036
- }
2037
- };
2038
-
2039
1988
  //#endregion
2040
1989
  //#region src/rules/security/require-auth-guard.ts
2041
1990
  const requireAuthGuard = {
@@ -2122,8 +2071,6 @@ const allRules = [
2122
2071
  requireInjectDecorator,
2123
2072
  noMissingInjectable,
2124
2073
  noHardcodedSecrets,
2125
- noWildcardCors,
2126
- noUnsafeRawQuery,
2127
2074
  requireAuthGuard,
2128
2075
  noEval,
2129
2076
  noWeakCrypto,
@@ -2224,7 +2171,13 @@ const DEFAULT_CONFIG = {
2224
2171
  "**/*.spec.ts",
2225
2172
  "**/*.test.ts",
2226
2173
  "**/*.e2e-spec.ts",
2227
- "**/*.d.ts"
2174
+ "**/*.e2e-test.ts",
2175
+ "**/*.d.ts",
2176
+ "**/test/**",
2177
+ "**/tests/**",
2178
+ "**/__tests__/**",
2179
+ "**/__mocks__/**",
2180
+ "**/__fixtures__/**"
2228
2181
  ]
2229
2182
  };
2230
2183
 
@@ -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":"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"}
@@ -876,7 +876,7 @@ const noDuplicateRoutes = {
876
876
  id: "correctness/no-duplicate-routes",
877
877
  category: "correctness",
878
878
  severity: "error",
879
- description: "Same HTTP method + route path should not appear twice in a single controller",
879
+ description: "Same HTTP method + route path + version should not appear twice in a single controller",
880
880
  help: "Remove or rename one of the duplicate route handlers."
881
881
  },
882
882
  check(context) {
@@ -888,7 +888,8 @@ const noDuplicateRoutes = {
888
888
  if (!HTTP_METHOD_DECORATORS.has(decoratorName)) continue;
889
889
  const args = decorator.getArguments();
890
890
  const path = args.length > 0 ? args[0].getText() : "\"\"";
891
- const routeKey = `${decoratorName}:${path}`;
891
+ const versionDecorator = method.getDecorator("Version");
892
+ const routeKey = `${decoratorName}:${path}:${versionDecorator ? versionDecorator.getArguments()[0]?.getText() ?? "" : ""}`;
892
893
  const existing = routeMap.get(routeKey);
893
894
  if (existing) context.report({
894
895
  filePath: context.filePath,
@@ -955,6 +956,7 @@ const noMissingFilterCatch = {
955
956
  check(context) {
956
957
  for (const cls of context.sourceFile.getClasses()) {
957
958
  if (!hasDecorator(cls, "Catch")) continue;
959
+ if (cls.getExtends()) continue;
958
960
  if (!cls.getMethods().some((m) => m.getName() === "catch")) context.report({
959
961
  filePath: context.filePath,
960
962
  message: `Exception filter '${cls.getName()}' has @Catch() but is missing the 'catch()' method.`,
@@ -981,6 +983,7 @@ const noMissingGuardMethod = {
981
983
  const name = cls.getName() ?? "";
982
984
  if (!name.endsWith("Guard")) continue;
983
985
  if (!hasDecorator(cls, "Injectable")) continue;
986
+ if (cls.getExtends()) continue;
984
987
  if (!cls.getMethods().some((m) => m.getName() === "canActivate")) context.report({
985
988
  filePath: context.filePath,
986
989
  message: `Guard '${name}' is missing the 'canActivate()' method.`,
@@ -1051,6 +1054,7 @@ const noMissingInterceptorMethod = {
1051
1054
  const name = cls.getName() ?? "";
1052
1055
  if (!name.endsWith("Interceptor")) continue;
1053
1056
  if (!hasDecorator(cls, "Injectable")) continue;
1057
+ if (cls.getExtends()) continue;
1054
1058
  if (!cls.getMethods().some((m) => m.getName() === "intercept")) context.report({
1055
1059
  filePath: context.filePath,
1056
1060
  message: `Interceptor '${name}' is missing the 'intercept()' method.`,
@@ -1103,6 +1107,7 @@ const noMissingPipeMethod = {
1103
1107
  const name = cls.getName() ?? "";
1104
1108
  if (!name.endsWith("Pipe")) continue;
1105
1109
  if (!hasDecorator(cls, "Injectable")) continue;
1110
+ if (cls.getExtends()) continue;
1106
1111
  if (!cls.getMethods().some((m) => m.getName() === "transform")) context.report({
1107
1112
  filePath: context.filePath,
1108
1113
  message: `Pipe '${name}' is missing the 'transform()' method.`,
@@ -1796,7 +1801,7 @@ const noExposedStackTrace = {
1796
1801
  //#region src/rules/security/no-hardcoded-secrets.ts
1797
1802
  const SECRET_PATTERNS = [
1798
1803
  {
1799
- pattern: /^[A-Za-z0-9+/]{40,}={0,2}$/,
1804
+ pattern: /^(?=.*\d)[A-Za-z0-9+/]{40,}={0,2}$/,
1800
1805
  name: "Base64 key"
1801
1806
  },
1802
1807
  {
@@ -1851,8 +1856,15 @@ const PLACEHOLDER_VALUES = new Set([
1851
1856
  "changeme",
1852
1857
  "password"
1853
1858
  ]);
1859
+ const DOT_SEPARATED_CONSTANT = /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$/;
1854
1860
  function isSuspiciousValue(value) {
1855
- return value.length >= 8 && !value.includes("${") && !value.startsWith("process.env") && !PLACEHOLDER_VALUES.has(value);
1861
+ if (value.length < 8) return false;
1862
+ if (value.includes("${")) return false;
1863
+ if (value.startsWith("process.env")) return false;
1864
+ if (PLACEHOLDER_VALUES.has(value)) return false;
1865
+ if (value.includes(" ")) return false;
1866
+ if (DOT_SEPARATED_CONSTANT.test(value)) return false;
1867
+ return true;
1856
1868
  }
1857
1869
  function hasSuspiciousName(name) {
1858
1870
  return VARIABLE_NAME_PATTERNS.some((p) => p.test(name));
@@ -1913,41 +1925,6 @@ const noHardcodedSecrets = {
1913
1925
  }
1914
1926
  };
1915
1927
 
1916
- //#endregion
1917
- //#region src/rules/security/no-unsafe-raw-query.ts
1918
- const RAW_QUERY_METHODS = new Set([
1919
- "$executeRaw",
1920
- "$queryRaw",
1921
- "$executeRawUnsafe",
1922
- "$queryRawUnsafe",
1923
- "query"
1924
- ]);
1925
- const noUnsafeRawQuery = {
1926
- meta: {
1927
- id: "security/no-unsafe-raw-query",
1928
- category: "security",
1929
- severity: "error",
1930
- description: "Raw SQL queries with template literal interpolation are a SQL injection risk",
1931
- help: "Use parameterized queries or Prisma's tagged template (Prisma.sql`...`) instead of string interpolation."
1932
- },
1933
- check(context) {
1934
- const callExpressions = context.sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
1935
- for (const call of callExpressions) {
1936
- const methodName = call.getExpression().getText().split(".").pop() ?? "";
1937
- if (!RAW_QUERY_METHODS.has(methodName)) continue;
1938
- const args = call.getArguments();
1939
- if (args.length === 0) continue;
1940
- if (args[0].getKind() === SyntaxKind.TemplateExpression) context.report({
1941
- filePath: context.filePath,
1942
- message: `Raw SQL query '${methodName}()' uses template literal interpolation — SQL injection risk.`,
1943
- help: this.meta.help,
1944
- line: call.getStartLineNumber(),
1945
- column: 1
1946
- });
1947
- }
1948
- }
1949
- };
1950
-
1951
1928
  //#endregion
1952
1929
  //#region src/rules/security/no-weak-crypto.ts
1953
1930
  const WEAK_ALGORITHMS = new Set(["md5", "sha1"]);
@@ -1979,34 +1956,6 @@ const noWeakCrypto = {
1979
1956
  }
1980
1957
  };
1981
1958
 
1982
- //#endregion
1983
- //#region src/rules/security/no-wildcard-cors.ts
1984
- const noWildcardCors = {
1985
- meta: {
1986
- id: "security/no-wildcard-cors",
1987
- category: "security",
1988
- severity: "error",
1989
- description: "CORS should not be configured with origin: '*' or origin: true — allows any domain",
1990
- help: "Specify allowed origins explicitly instead of using wildcard CORS."
1991
- },
1992
- check(context) {
1993
- const propertyAssignments = context.sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAssignment);
1994
- for (const prop of propertyAssignments) {
1995
- if (prop.getName() !== "origin") continue;
1996
- const initializer = prop.getInitializer();
1997
- if (!initializer) continue;
1998
- const text = initializer.getText();
1999
- if (text === "'*'" || text === "\"*\"" || text === "true") context.report({
2000
- filePath: context.filePath,
2001
- message: `CORS configured with origin: ${text} — allows requests from any domain.`,
2002
- help: this.meta.help,
2003
- line: prop.getStartLineNumber(),
2004
- column: 1
2005
- });
2006
- }
2007
- }
2008
- };
2009
-
2010
1959
  //#endregion
2011
1960
  //#region src/rules/security/require-auth-guard.ts
2012
1961
  const requireAuthGuard = {
@@ -2093,8 +2042,6 @@ const allRules = [
2093
2042
  requireInjectDecorator,
2094
2043
  noMissingInjectable,
2095
2044
  noHardcodedSecrets,
2096
- noWildcardCors,
2097
- noUnsafeRawQuery,
2098
2045
  requireAuthGuard,
2099
2046
  noEval,
2100
2047
  noWeakCrypto,
@@ -2195,7 +2142,13 @@ const DEFAULT_CONFIG = {
2195
2142
  "**/*.spec.ts",
2196
2143
  "**/*.test.ts",
2197
2144
  "**/*.e2e-spec.ts",
2198
- "**/*.d.ts"
2145
+ "**/*.e2e-test.ts",
2146
+ "**/*.d.ts",
2147
+ "**/test/**",
2148
+ "**/tests/**",
2149
+ "**/__tests__/**",
2150
+ "**/__mocks__/**",
2151
+ "**/__fixtures__/**"
2199
2152
  ]
2200
2153
  };
2201
2154