technical-debt-radar 1.5.0 → 1.6.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.
Files changed (2) hide show
  1. package/dist/index.js +245 -2
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -77,7 +77,8 @@ var require_constants = __commonJS({
77
77
  LARGE_JSON_STRINGIFY: "large-json-stringify",
78
78
  CPU_HEAVY_LOOP_IN_HANDLER: "cpu-heavy-loop-in-handler",
79
79
  UNBOUNDED_ARRAY_OPERATION: "unbounded-array-operation",
80
- DYNAMIC_BUFFER_ALLOC: "dynamic-buffer-alloc"
80
+ DYNAMIC_BUFFER_ALLOC: "dynamic-buffer-alloc",
81
+ UNBOUNDED_PARALLEL_CALLS: "unbounded-parallel-external-calls"
81
82
  };
82
83
  exports2.RELIABILITY_RULES = {
83
84
  UNHANDLED_PROMISE_REJECTION: "unhandled-promise-rejection",
@@ -91,7 +92,8 @@ var require_constants = __commonJS({
91
92
  MISSING_DTO_VALIDATION: "missing-dto-validation",
92
93
  UNGUARDED_ROUTE_TODO: "unguarded-route-todo",
93
94
  ERROR_LOGGED_AS_INFO: "error-logged-as-info",
94
- CATCH_SWALLOWS_CONTEXT: "catch-block-swallows-error-context"
95
+ CATCH_SWALLOWS_CONTEXT: "catch-block-swallows-error-context",
96
+ PROMISE_ALL_NO_ISOLATION: "promise-all-no-error-isolation"
95
97
  };
96
98
  exports2.PERFORMANCE_RULES = {
97
99
  UNBOUNDED_FIND_MANY: "unbounded-find-many",
@@ -111,6 +113,7 @@ var require_constants = __commonJS({
111
113
  CODE_DUPLICATION: "code-duplication",
112
114
  MISSING_TEST_FILE: "missing-test-file",
113
115
  UNUSED_EXPORT: "unused-export",
116
+ UNUSED_PUBLIC_METHOD: "unused-public-method",
114
117
  DEBUG_CONSOLE_LOG: "debug-console-log-in-production",
115
118
  LOW_TEST_COVERAGE: "low-test-coverage",
116
119
  LOW_BRANCH_COVERAGE: "low-branch-coverage",
@@ -12772,6 +12775,10 @@ var require_import_graph = __commonJS({
12772
12775
  const resolved = posixJoinAndNormalize(sourceDir, specifier);
12773
12776
  return resolveInProject(resolved, project) ?? resolved;
12774
12777
  }
12778
+ const aliasStripped = specifier.replace(/^@\//, "src/").replace(/^~\//, "src/");
12779
+ const aliasResolved = resolveInProject(aliasStripped, project);
12780
+ if (aliasResolved)
12781
+ return aliasResolved;
12775
12782
  const directResolved = resolveInProject(specifier, project);
12776
12783
  if (directResolved)
12777
12784
  return directResolved;
@@ -16185,6 +16192,7 @@ var require_reliability_detector = __commonJS({
16185
16192
  async function detectReliabilityIssues(input, policy) {
16186
16193
  const violations = [];
16187
16194
  const project = new ts_morph_1.Project({ useInMemoryFileSystem: true });
16195
+ const schemaModuleMap = /* @__PURE__ */ new Map();
16188
16196
  for (const file of input.changedFiles) {
16189
16197
  if (file.status === "deleted")
16190
16198
  continue;
@@ -16205,6 +16213,10 @@ var require_reliability_detector = __commonJS({
16205
16213
  detectUnguardedRouteTodo(sourceFile, file.path, fns, policy, violations);
16206
16214
  detectCatchSwallowsContext(sourceFile, file.path, fns, policy, violations);
16207
16215
  detectDebugConsoleLog(sourceFile, file.path, fns, policy, violations);
16216
+ detectProviderOutsideHomeModule(sourceFile, file.path, input.changedFiles, policy, violations);
16217
+ detectSchemaMultiModule(sourceFile, file.path, schemaModuleMap, policy, violations);
16218
+ detectPromiseAllNoIsolation(sourceFile, file.path, fns, policy, violations);
16219
+ detectUnboundedParallelCalls(sourceFile, file.path, fns, policy, violations);
16208
16220
  project.removeSourceFile(sourceFile);
16209
16221
  }
16210
16222
  return applyExceptions(violations, policy);
@@ -16270,12 +16282,21 @@ var require_reliability_detector = __commonJS({
16270
16282
  }
16271
16283
  return false;
16272
16284
  }
16285
+ function isNestJSInjectable(classNode) {
16286
+ return classNode.getDecorators().some((d) => d.getName() === "Injectable");
16287
+ }
16273
16288
  function isNestJSController(classNode) {
16274
16289
  return classNode.getDecorators().some((d) => {
16275
16290
  const name = d.getName();
16276
16291
  return name === "Controller" || name === "Resolver";
16277
16292
  });
16278
16293
  }
16294
+ function isInsideNestJSInjectable(node) {
16295
+ const classDecl = node.getFirstAncestorByKind(ts_morph_1.SyntaxKind.ClassDeclaration);
16296
+ if (!classDecl)
16297
+ return false;
16298
+ return isNestJSInjectable(classDecl);
16299
+ }
16279
16300
  function fileImportsNestJS(sourceFile) {
16280
16301
  for (const decl of sourceFile.getImportDeclarations()) {
16281
16302
  const spec = decl.getModuleSpecifierValue();
@@ -16386,6 +16407,8 @@ var require_reliability_detector = __commonJS({
16386
16407
  return;
16387
16408
  if (isNestJSControllerMethod(node, sourceFile))
16388
16409
  return;
16410
+ if (fileImportsNestJS(sourceFile) && isInsideNestJSInjectable(node))
16411
+ return;
16389
16412
  if (functionHasTryCatch(fn.node))
16390
16413
  return;
16391
16414
  const isRisky = isRiskyAwaitCall(node);
@@ -17010,6 +17033,226 @@ var require_reliability_detector = __commonJS({
17010
17033
  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"));
17011
17034
  });
17012
17035
  }
17036
+ function detectPromiseAllNoIsolation(sourceFile, filePath, fns, policy, violations) {
17037
+ sourceFile.forEachDescendant((node) => {
17038
+ if (!ts_morph_1.Node.isCallExpression(node))
17039
+ return;
17040
+ const expr = node.getExpression();
17041
+ if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
17042
+ return;
17043
+ if (expr.getName() !== "all")
17044
+ return;
17045
+ const obj = expr.getExpression();
17046
+ if (!ts_morph_1.Node.isIdentifier(obj) || obj.getText() !== "Promise")
17047
+ return;
17048
+ const args = node.getArguments();
17049
+ if (args.length === 0)
17050
+ return;
17051
+ const arrayArg = args[0];
17052
+ if (!ts_morph_1.Node.isArrayLiteralExpression(arrayArg))
17053
+ return;
17054
+ const elements = arrayArg.getElements();
17055
+ if (elements.length < 2)
17056
+ return;
17057
+ let hasExternalCalls = false;
17058
+ let hasCatchHandlers = false;
17059
+ for (const el of elements) {
17060
+ const elText = el.getText();
17061
+ if (/fetch|axios|http|this\.\w+Service|this\.\w+Model/.test(elText)) {
17062
+ hasExternalCalls = true;
17063
+ }
17064
+ if (/\.catch\(/.test(elText)) {
17065
+ hasCatchHandlers = true;
17066
+ }
17067
+ }
17068
+ if (!hasExternalCalls)
17069
+ return;
17070
+ if (hasCatchHandlers)
17071
+ return;
17072
+ const fn = getEnclosingFn(node, fns);
17073
+ violations.push(makeViolation(shared_1.RELIABILITY_RULES.PROMISE_ALL_NO_ISOLATION, filePath, node.getStartLineNumber(), `Promise.all() with ${elements.length} I/O calls \u2014 one failure rejects all with no error isolation`, policy, fn?.name, "Add .catch() to individual promises or use Promise.allSettled() for partial failure tolerance"));
17074
+ });
17075
+ }
17076
+ function detectUnboundedParallelCalls(sourceFile, filePath, fns, policy, violations) {
17077
+ sourceFile.forEachDescendant((node) => {
17078
+ if (!ts_morph_1.Node.isCallExpression(node))
17079
+ return;
17080
+ const expr = node.getExpression();
17081
+ if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
17082
+ return;
17083
+ if (expr.getName() !== "all")
17084
+ return;
17085
+ const obj = expr.getExpression();
17086
+ if (!ts_morph_1.Node.isIdentifier(obj) || obj.getText() !== "Promise")
17087
+ return;
17088
+ const args = node.getArguments();
17089
+ if (args.length === 0)
17090
+ return;
17091
+ const firstArg = args[0];
17092
+ if (!ts_morph_1.Node.isCallExpression(firstArg))
17093
+ return;
17094
+ const mapExpr = firstArg.getExpression();
17095
+ if (!ts_morph_1.Node.isPropertyAccessExpression(mapExpr))
17096
+ return;
17097
+ if (mapExpr.getName() !== "map")
17098
+ return;
17099
+ const mapArgs = firstArg.getArguments();
17100
+ if (mapArgs.length === 0)
17101
+ return;
17102
+ const callbackText = mapArgs[0].getText();
17103
+ if (!/fetch|axios|http|this\.\w+Service|this\.\w+Model|\.upload|\.send|\.post|\.get|\.put|\.delete/i.test(callbackText)) {
17104
+ return;
17105
+ }
17106
+ const fn = getEnclosingFn(node, fns);
17107
+ violations.push({
17108
+ category: "runtime_risk",
17109
+ type: shared_1.RUNTIME_RISK_RULES.UNBOUNDED_PARALLEL_CALLS,
17110
+ ruleId: shared_1.RUNTIME_RISK_RULES.UNBOUNDED_PARALLEL_CALLS,
17111
+ severity: "warning",
17112
+ source: "deterministic",
17113
+ confidence: "high",
17114
+ file: filePath,
17115
+ line: node.getStartLineNumber(),
17116
+ function: fn?.name,
17117
+ message: "Promise.all(array.map()) with unbounded I/O calls \u2014 may overwhelm external service or exhaust connections",
17118
+ suggestion: "Use p-limit or batch processing to limit concurrency: const limit = pLimit(5); await Promise.all(items.map(item => limit(() => fn(item))))",
17119
+ debtPoints: 5,
17120
+ gateAction: "warn"
17121
+ });
17122
+ });
17123
+ }
17124
+ function detectProviderOutsideHomeModule(sourceFile, filePath, allFiles, policy, violations) {
17125
+ if (!filePath.endsWith(".module.ts") && !filePath.endsWith(".module.js"))
17126
+ return;
17127
+ sourceFile.forEachDescendant((node) => {
17128
+ if (!ts_morph_1.Node.isDecorator(node) || node.getName() !== "Module")
17129
+ return;
17130
+ const args = node.getArguments?.();
17131
+ if (!args || args.length === 0)
17132
+ return;
17133
+ const callExpr = node.getCallExpression?.();
17134
+ if (!callExpr)
17135
+ return;
17136
+ const moduleArgs = callExpr.getArguments();
17137
+ if (moduleArgs.length === 0 || !ts_morph_1.Node.isObjectLiteralExpression(moduleArgs[0]))
17138
+ return;
17139
+ const objLit = moduleArgs[0];
17140
+ const providersProp = objLit.getProperty("providers");
17141
+ if (!providersProp || !ts_morph_1.Node.isPropertyAssignment(providersProp))
17142
+ return;
17143
+ const providersInit = providersProp.getInitializer();
17144
+ if (!providersInit || !ts_morph_1.Node.isArrayLiteralExpression(providersInit))
17145
+ return;
17146
+ const moduleFolder = filePath.replace(/\/[^/]+$/, "");
17147
+ for (const element of providersInit.getElements()) {
17148
+ if (!ts_morph_1.Node.isIdentifier(element))
17149
+ continue;
17150
+ const providerName = element.getText();
17151
+ for (const importDecl of sourceFile.getImportDeclarations()) {
17152
+ const namedImports = importDecl.getNamedImports();
17153
+ const matchingImport = namedImports.find((n) => n.getName() === providerName);
17154
+ if (!matchingImport)
17155
+ continue;
17156
+ const specifier = importDecl.getModuleSpecifierValue();
17157
+ if (specifier.startsWith(".") || specifier.startsWith("/")) {
17158
+ const resolvedPath = specifier.replace(/^\.\//, moduleFolder + "/").replace(/^\.\.\//, moduleFolder + "/../");
17159
+ if (!resolvedPath.startsWith(moduleFolder) && !specifier.startsWith("./")) {
17160
+ violations.push({
17161
+ category: "architecture",
17162
+ type: shared_1.ARCHITECTURE_RULES.PROVIDER_OUTSIDE_HOME,
17163
+ ruleId: shared_1.ARCHITECTURE_RULES.PROVIDER_OUTSIDE_HOME,
17164
+ severity: "warning",
17165
+ source: "deterministic",
17166
+ confidence: "high",
17167
+ file: filePath,
17168
+ line: element.getStartLineNumber(),
17169
+ message: `Provider '${providerName}' is imported from outside this module's folder \u2014 consider moving it to its home module or a shared module`,
17170
+ suggestion: `Move ${providerName} to this module's folder or register it in the module where it's defined`,
17171
+ debtPoints: 3,
17172
+ gateAction: "warn"
17173
+ });
17174
+ }
17175
+ }
17176
+ }
17177
+ }
17178
+ });
17179
+ }
17180
+ function detectSchemaMultiModule(sourceFile, filePath, schemaMap, policy, violations) {
17181
+ if (!filePath.endsWith(".module.ts") && !filePath.endsWith(".module.js"))
17182
+ return;
17183
+ sourceFile.forEachDescendant((node) => {
17184
+ if (!ts_morph_1.Node.isCallExpression(node))
17185
+ return;
17186
+ const expr = node.getExpression();
17187
+ if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
17188
+ return;
17189
+ const methodName = expr.getName();
17190
+ if (methodName !== "forFeature")
17191
+ return;
17192
+ const obj = expr.getExpression();
17193
+ const objText = ts_morph_1.Node.isIdentifier(obj) ? obj.getText() : "";
17194
+ if (objText !== "MongooseModule" && objText !== "TypeOrmModule")
17195
+ return;
17196
+ const args = node.getArguments();
17197
+ if (args.length === 0)
17198
+ return;
17199
+ const arrayArg = args[0];
17200
+ if (!ts_morph_1.Node.isArrayLiteralExpression(arrayArg))
17201
+ return;
17202
+ for (const element of arrayArg.getElements()) {
17203
+ if (ts_morph_1.Node.isObjectLiteralExpression(element)) {
17204
+ const nameProp = element.getProperty("name");
17205
+ if (nameProp && ts_morph_1.Node.isPropertyAssignment(nameProp)) {
17206
+ const init = nameProp.getInitializer();
17207
+ if (init) {
17208
+ const schemaName = init.getText().replace(/\.name$/, "");
17209
+ const existing = schemaMap.get(schemaName) ?? [];
17210
+ existing.push(filePath);
17211
+ schemaMap.set(schemaName, existing);
17212
+ if (existing.length > 1) {
17213
+ violations.push({
17214
+ category: "architecture",
17215
+ type: shared_1.ARCHITECTURE_RULES.SCHEMA_MULTI_MODULE,
17216
+ ruleId: shared_1.ARCHITECTURE_RULES.SCHEMA_MULTI_MODULE,
17217
+ severity: "warning",
17218
+ source: "deterministic",
17219
+ confidence: "high",
17220
+ file: filePath,
17221
+ line: element.getStartLineNumber(),
17222
+ message: `Schema '${schemaName}' registered in multiple modules: ${existing.join(", ")}`,
17223
+ suggestion: "Register the schema in one module and import that module where needed",
17224
+ debtPoints: 3,
17225
+ gateAction: "warn"
17226
+ });
17227
+ }
17228
+ }
17229
+ }
17230
+ }
17231
+ if (ts_morph_1.Node.isIdentifier(element)) {
17232
+ const entityName = element.getText();
17233
+ const existing = schemaMap.get(entityName) ?? [];
17234
+ existing.push(filePath);
17235
+ schemaMap.set(entityName, existing);
17236
+ if (existing.length > 1) {
17237
+ violations.push({
17238
+ category: "architecture",
17239
+ type: shared_1.ARCHITECTURE_RULES.SCHEMA_MULTI_MODULE,
17240
+ ruleId: shared_1.ARCHITECTURE_RULES.SCHEMA_MULTI_MODULE,
17241
+ severity: "warning",
17242
+ source: "deterministic",
17243
+ confidence: "high",
17244
+ file: filePath,
17245
+ line: element.getStartLineNumber(),
17246
+ message: `Entity '${entityName}' registered in multiple modules: ${existing.join(", ")}`,
17247
+ suggestion: "Register the entity in one module and import that module where needed",
17248
+ debtPoints: 3,
17249
+ gateAction: "warn"
17250
+ });
17251
+ }
17252
+ }
17253
+ }
17254
+ });
17255
+ }
17013
17256
  var DEBUG_LOG_PATTERN = /\b(RESULT|DEBUG|TEST|TODO|FIXME|HACK|XXX)\b/i;
17014
17257
  var EXCLUDE_LOG_FILES = /\.(spec|test)\.(ts|js)|seed|migration|main\.(ts|js)$/;
17015
17258
  function detectDebugConsoleLog(sourceFile, filePath, fns, policy, violations) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "technical-debt-radar",
3
- "version": "1.5.0",
3
+ "version": "1.6.1",
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",