technical-debt-radar 1.3.3 → 1.5.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.
Files changed (2) hide show
  1. package/dist/index.js +129 -24
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -89,7 +89,9 @@ var require_constants = __commonJS({
89
89
  TRANSACTION_NO_TIMEOUT: "transaction-no-timeout",
90
90
  MISSING_NULL_GUARD: "missing-null-guard",
91
91
  MISSING_DTO_VALIDATION: "missing-dto-validation",
92
- UNGUARDED_ROUTE_TODO: "unguarded-route-todo"
92
+ UNGUARDED_ROUTE_TODO: "unguarded-route-todo",
93
+ ERROR_LOGGED_AS_INFO: "error-logged-as-info",
94
+ CATCH_SWALLOWS_CONTEXT: "catch-block-swallows-error-context"
93
95
  };
94
96
  exports2.PERFORMANCE_RULES = {
95
97
  UNBOUNDED_FIND_MANY: "unbounded-find-many",
@@ -109,6 +111,7 @@ var require_constants = __commonJS({
109
111
  CODE_DUPLICATION: "code-duplication",
110
112
  MISSING_TEST_FILE: "missing-test-file",
111
113
  UNUSED_EXPORT: "unused-export",
114
+ DEBUG_CONSOLE_LOG: "debug-console-log-in-production",
112
115
  LOW_TEST_COVERAGE: "low-test-coverage",
113
116
  LOW_BRANCH_COVERAGE: "low-branch-coverage",
114
117
  COVERAGE_DROP: "coverage-drop"
@@ -117,7 +120,9 @@ var require_constants = __commonJS({
117
120
  LAYER_VIOLATION: "layer-boundary-violation",
118
121
  CIRCULAR_DEPENDENCY: "circular-dependency",
119
122
  MODULE_BOUNDARY: "module-boundary-violation",
120
- FORBIDDEN_FRAMEWORK: "forbidden-framework-in-layer"
123
+ FORBIDDEN_FRAMEWORK: "forbidden-framework-in-layer",
124
+ PROVIDER_OUTSIDE_HOME: "provider-declared-outside-home-module",
125
+ SCHEMA_MULTI_MODULE: "schema-registered-in-multiple-modules"
121
126
  };
122
127
  exports2.CROSS_FILE_RULES = {
123
128
  INDIRECT_SYNC_FS: "indirect-sync-fs",
@@ -12762,12 +12767,15 @@ var require_import_graph = __commonJS({
12762
12767
  return specifiers;
12763
12768
  }
12764
12769
  function resolveModuleSpecifier(sourcePath, specifier, project) {
12765
- if (!specifier.startsWith(".") && !specifier.startsWith("/")) {
12766
- return void 0;
12770
+ if (specifier.startsWith(".") || specifier.startsWith("/")) {
12771
+ const sourceDir = posixDirname(sourcePath);
12772
+ const resolved = posixJoinAndNormalize(sourceDir, specifier);
12773
+ return resolveInProject(resolved, project) ?? resolved;
12767
12774
  }
12768
- const sourceDir = posixDirname(sourcePath);
12769
- const resolved = posixJoinAndNormalize(sourceDir, specifier);
12770
- return resolveInProject(resolved, project) ?? resolved;
12775
+ const directResolved = resolveInProject(specifier, project);
12776
+ if (directResolved)
12777
+ return directResolved;
12778
+ return void 0;
12771
12779
  }
12772
12780
  function resolveInProject(resolved, project) {
12773
12781
  const lookup = "/" + resolved;
@@ -12786,6 +12794,16 @@ var require_import_graph = __commonJS({
12786
12794
  return indexPath;
12787
12795
  }
12788
12796
  }
12797
+ const suffix = "/" + resolved;
12798
+ for (const sf of project.getSourceFiles()) {
12799
+ const fp = sf.getFilePath();
12800
+ if (fp.endsWith(suffix))
12801
+ return normalizeFilePath(fp);
12802
+ for (const ext of extensions) {
12803
+ if (fp.endsWith(suffix + ext))
12804
+ return normalizeFilePath(fp);
12805
+ }
12806
+ }
12789
12807
  return void 0;
12790
12808
  }
12791
12809
  function buildEdge(source, target, importType, policy) {
@@ -12876,12 +12894,22 @@ var require_boundary_checker = __commonJS({
12876
12894
  return /nestjs/i.test(policy.stack.framework);
12877
12895
  }
12878
12896
  var suffixPattern = (suffix) => new RegExp(`\\.${suffix}(\\.(ts|tsx|js|jsx))?$`);
12879
- var NESTJS_SHARED_INFRA_FOLDERS = ["guards", "decorators", "interceptors", "pipes", "filters", "middleware", "shared"];
12897
+ var NESTJS_SHARED_INFRA_FOLDERS = ["guards", "decorators", "interceptors", "pipes", "filters", "middleware", "shared", "strategies", "common", "config"];
12880
12898
  function isNestJSExemptImport(sourcePath, targetPath, customSharedFolders = []) {
12881
12899
  const srcBase = sourcePath.replace(/\\/g, "/");
12882
12900
  const tgtBase = targetPath.replace(/\\/g, "/");
12883
12901
  if (suffixPattern("module").test(srcBase))
12884
12902
  return true;
12903
+ if (suffixPattern("strategy").test(srcBase))
12904
+ return true;
12905
+ if (suffixPattern("guard").test(srcBase))
12906
+ return true;
12907
+ if (suffixPattern("interceptor").test(srcBase))
12908
+ return true;
12909
+ if (suffixPattern("pipe").test(srcBase))
12910
+ return true;
12911
+ if (suffixPattern("filter").test(srcBase))
12912
+ return true;
12885
12913
  if (suffixPattern("module").test(tgtBase))
12886
12914
  return true;
12887
12915
  if (suffixPattern("entity").test(tgtBase))
@@ -12898,6 +12926,8 @@ var require_boundary_checker = __commonJS({
12898
12926
  return true;
12899
12927
  if (suffixPattern("filter").test(tgtBase))
12900
12928
  return true;
12929
+ if (suffixPattern("strategy").test(tgtBase))
12930
+ return true;
12901
12931
  if (/\/dto\//.test(tgtBase) || suffixPattern("dto").test(tgtBase))
12902
12932
  return true;
12903
12933
  for (const folder of NESTJS_SHARED_INFRA_FOLDERS) {
@@ -16173,6 +16203,8 @@ var require_reliability_detector = __commonJS({
16173
16203
  detectMissingNullGuard(sourceFile, file.path, fns, policy, violations);
16174
16204
  detectMissingDtoValidation(sourceFile, file.path, fns, policy, violations);
16175
16205
  detectUnguardedRouteTodo(sourceFile, file.path, fns, policy, violations);
16206
+ detectCatchSwallowsContext(sourceFile, file.path, fns, policy, violations);
16207
+ detectDebugConsoleLog(sourceFile, file.path, fns, policy, violations);
16176
16208
  project.removeSourceFile(sourceFile);
16177
16209
  }
16178
16210
  return applyExceptions(violations, policy);
@@ -16238,18 +16270,12 @@ var require_reliability_detector = __commonJS({
16238
16270
  }
16239
16271
  return false;
16240
16272
  }
16241
- function isNestJSInjectable(classNode) {
16273
+ function isNestJSController(classNode) {
16242
16274
  return classNode.getDecorators().some((d) => {
16243
16275
  const name = d.getName();
16244
- return name === "Injectable";
16276
+ return name === "Controller" || name === "Resolver";
16245
16277
  });
16246
16278
  }
16247
- function isInsideNestJSInjectable(node) {
16248
- const classDecl = node.getFirstAncestorByKind(ts_morph_1.SyntaxKind.ClassDeclaration);
16249
- if (!classDecl)
16250
- return false;
16251
- return isNestJSInjectable(classDecl);
16252
- }
16253
16279
  function fileImportsNestJS(sourceFile) {
16254
16280
  for (const decl of sourceFile.getImportDeclarations()) {
16255
16281
  const spec = decl.getModuleSpecifierValue();
@@ -16258,10 +16284,13 @@ var require_reliability_detector = __commonJS({
16258
16284
  }
16259
16285
  return false;
16260
16286
  }
16261
- function isNestJSServiceMethod(node, sourceFile) {
16287
+ function isNestJSControllerMethod(node, sourceFile) {
16262
16288
  if (!fileImportsNestJS(sourceFile))
16263
16289
  return false;
16264
- if (!isInsideNestJSInjectable(node))
16290
+ const classDecl = node.getFirstAncestorByKind(ts_morph_1.SyntaxKind.ClassDeclaration);
16291
+ if (!classDecl)
16292
+ return false;
16293
+ if (!isNestJSController(classDecl))
16265
16294
  return false;
16266
16295
  const bgDecorators = /* @__PURE__ */ new Set(["Cron", "Process", "Interval", "Timeout"]);
16267
16296
  if (ts_morph_1.Node.isMethodDeclaration(node)) {
@@ -16269,10 +16298,8 @@ var require_reliability_detector = __commonJS({
16269
16298
  return false;
16270
16299
  }
16271
16300
  const fn = node.getFirstAncestorByKind(ts_morph_1.SyntaxKind.MethodDeclaration);
16272
- if (fn) {
16273
- if (fn.getDecorators().some((d) => bgDecorators.has(d.getName())))
16274
- return false;
16275
- }
16301
+ if (fn && fn.getDecorators().some((d) => bgDecorators.has(d.getName())))
16302
+ return false;
16276
16303
  return true;
16277
16304
  }
16278
16305
  function getSeverity(ruleId, policy) {
@@ -16357,7 +16384,7 @@ var require_reliability_detector = __commonJS({
16357
16384
  return;
16358
16385
  if (isBootstrapFunction(fn))
16359
16386
  return;
16360
- if (isNestJSServiceMethod(node, sourceFile))
16387
+ if (isNestJSControllerMethod(node, sourceFile))
16361
16388
  return;
16362
16389
  if (functionHasTryCatch(fn.node))
16363
16390
  return;
@@ -16545,7 +16572,7 @@ var require_reliability_detector = __commonJS({
16545
16572
  continue;
16546
16573
  if (isBootstrapFunction(fn))
16547
16574
  continue;
16548
- if (isNestJSServiceMethod(fn.node, _sourceFile))
16575
+ if (isNestJSControllerMethod(fn.node, _sourceFile))
16549
16576
  continue;
16550
16577
  let hasAwait = false;
16551
16578
  fn.node.forEachDescendant((child) => {
@@ -16786,6 +16813,12 @@ var require_reliability_detector = __commonJS({
16786
16813
  const hasContinue = /\bcontinue\b/.test(blockText);
16787
16814
  if (hasIncrement || hasPropertyMethodCall || hasAwaitOrDelay || hasContinue)
16788
16815
  return;
16816
+ const hasConsoleLog = /\bconsole\.log\b/.test(blockText);
16817
+ if (hasConsoleLog) {
16818
+ const fn2 = getEnclosingFn(node, fns);
16819
+ violations.push(makeViolation(shared_1.RELIABILITY_RULES.ERROR_LOGGED_AS_INFO, filePath, node.getStartLineNumber(), "Error logged with console.log instead of console.error \u2014 errors should route to stderr", policy, fn2?.name, "Replace console.log with console.error or use a proper logger (winston, pino)"));
16820
+ return;
16821
+ }
16789
16822
  const fn = getEnclosingFn(node, fns);
16790
16823
  violations.push(makeViolation(shared_1.RELIABILITY_RULES.MISSING_ERROR_LOGGING, filePath, node.getStartLineNumber(), "catch block has no error logging or reporting", policy, fn?.name, "Add logger.error(err) or re-throw the error"));
16791
16824
  });
@@ -16977,6 +17010,78 @@ var require_reliability_detector = __commonJS({
16977
17010
  violations.push(makeViolation(shared_1.RELIABILITY_RULES.UNGUARDED_ROUTE_TODO, filePath, node.getStartLineNumber(), `Route handler '${node.getName?.() ?? "anonymous"}' has TODO/FIXME comment and no @UseGuards() \u2014 unprotected endpoint`, policy, fn?.name, "Add @UseGuards(JwtAuthGuard) or appropriate guard \u2014 this route is currently unprotected"));
16978
17011
  });
16979
17012
  }
17013
+ var DEBUG_LOG_PATTERN = /\b(RESULT|DEBUG|TEST|TODO|FIXME|HACK|XXX)\b/i;
17014
+ var EXCLUDE_LOG_FILES = /\.(spec|test)\.(ts|js)|seed|migration|main\.(ts|js)$/;
17015
+ function detectDebugConsoleLog(sourceFile, filePath, fns, policy, violations) {
17016
+ if (EXCLUDE_LOG_FILES.test(filePath))
17017
+ return;
17018
+ sourceFile.forEachDescendant((node) => {
17019
+ if (!ts_morph_1.Node.isCallExpression(node))
17020
+ return;
17021
+ const expr = node.getExpression();
17022
+ if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
17023
+ return;
17024
+ const obj = expr.getExpression();
17025
+ if (!ts_morph_1.Node.isIdentifier(obj) || obj.getText() !== "console")
17026
+ return;
17027
+ if (expr.getName() !== "log")
17028
+ return;
17029
+ const args = node.getArguments();
17030
+ const argsText = args.map((a) => a.getText()).join(" ");
17031
+ if (!DEBUG_LOG_PATTERN.test(argsText))
17032
+ return;
17033
+ const catchClause = node.getFirstAncestorByKind(ts_morph_1.SyntaxKind.CatchClause);
17034
+ if (catchClause)
17035
+ return;
17036
+ const fn = getEnclosingFn(node, fns);
17037
+ violations.push({
17038
+ category: "maintainability",
17039
+ type: shared_1.MAINTAINABILITY_RULES.DEBUG_CONSOLE_LOG,
17040
+ ruleId: shared_1.MAINTAINABILITY_RULES.DEBUG_CONSOLE_LOG,
17041
+ severity: "warning",
17042
+ source: "deterministic",
17043
+ confidence: "high",
17044
+ file: filePath,
17045
+ line: node.getStartLineNumber(),
17046
+ function: fn?.name,
17047
+ message: `Debug console.log() in production code: ${argsText.substring(0, 60)}`,
17048
+ suggestion: "Remove debug logging or replace with a proper logger (winston, pino)",
17049
+ debtPoints: 1,
17050
+ gateAction: "warn"
17051
+ });
17052
+ });
17053
+ }
17054
+ function detectCatchSwallowsContext(sourceFile, filePath, fns, policy, violations) {
17055
+ sourceFile.forEachDescendant((node) => {
17056
+ if (!ts_morph_1.Node.isCatchClause(node))
17057
+ return;
17058
+ const block = node.getBlock();
17059
+ const errParam = node.getVariableDeclaration()?.getName();
17060
+ if (!errParam)
17061
+ return;
17062
+ let throwsNew = false;
17063
+ let referencesOriginal = false;
17064
+ block.forEachDescendant((child) => {
17065
+ if (ts_morph_1.Node.isThrowStatement(child)) {
17066
+ throwsNew = true;
17067
+ const thrownText = child.getExpression()?.getText() ?? "";
17068
+ if (thrownText.includes(errParam)) {
17069
+ referencesOriginal = true;
17070
+ }
17071
+ }
17072
+ });
17073
+ if (!throwsNew)
17074
+ return;
17075
+ if (referencesOriginal)
17076
+ return;
17077
+ const blockText = block.getText();
17078
+ if (/\b(logger|console\.(?:error|warn))\b/i.test(blockText) && new RegExp(`\\b${escapeRegex(errParam)}\\b`).test(blockText)) {
17079
+ return;
17080
+ }
17081
+ const fn = getEnclosingFn(node, fns);
17082
+ violations.push(makeViolation(shared_1.RELIABILITY_RULES.CATCH_SWALLOWS_CONTEXT, filePath, node.getStartLineNumber(), `Catch block throws new error without including original '${errParam}' \u2014 error context lost`, policy, fn?.name, `Include the original error: throw new Error('message', { cause: ${errParam} }) or throw new HttpException(message, status, { cause: ${errParam} })`));
17083
+ });
17084
+ }
16980
17085
  function escapeRegex(str) {
16981
17086
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16982
17087
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "technical-debt-radar",
3
- "version": "1.3.3",
3
+ "version": "1.5.0",
4
4
  "description": "Stop Node.js production crashes before merge. 47 detection patterns across 5 categories.",
5
5
  "bin": {
6
6
  "radar": "dist/index.js",