technical-debt-radar 1.6.3 → 1.6.5

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 +155 -4
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -15200,6 +15200,47 @@ var require_perf_pattern_detector = __commonJS({
15200
15200
  }
15201
15201
  return void 0;
15202
15202
  }
15203
+ function isServiceMethodCall(callExpr) {
15204
+ const expr = callExpr.getExpression();
15205
+ if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
15206
+ return false;
15207
+ const obj = expr.getExpression();
15208
+ if (ts_morph_1.Node.isPropertyAccessExpression(obj)) {
15209
+ const propName = obj.getName();
15210
+ if (/Service$/i.test(propName))
15211
+ return true;
15212
+ }
15213
+ if (ts_morph_1.Node.isIdentifier(obj)) {
15214
+ const name = obj.getText();
15215
+ if (/Service$/i.test(name))
15216
+ return true;
15217
+ }
15218
+ return false;
15219
+ }
15220
+ var BOUNDED_QUERY_METHODS = /* @__PURE__ */ new Set([
15221
+ "findOne",
15222
+ "findById",
15223
+ "findByIdAndUpdate",
15224
+ "findByIdAndDelete",
15225
+ "findOneAndUpdate",
15226
+ "findOneAndDelete",
15227
+ "findOneAndReplace",
15228
+ "findOneAndRemove",
15229
+ "findByPk",
15230
+ // Sequelize
15231
+ "findUnique",
15232
+ "findUniqueOrThrow",
15233
+ "findFirst",
15234
+ "findFirstOrThrow",
15235
+ // Prisma
15236
+ "first",
15237
+ // Knex
15238
+ "get",
15239
+ // DynamoDB
15240
+ "getOne",
15241
+ "getRawOne"
15242
+ // TypeORM QueryBuilder
15243
+ ]);
15203
15244
  var FIND_MANY_METHODS = /* @__PURE__ */ new Set(["findMany", "find", "getMany", "findAndCount"]);
15204
15245
  function detectUnboundedFindMany(sourceFile, filePath, fns, policy, violations) {
15205
15246
  sourceFile.forEachDescendant((node) => {
@@ -15209,6 +15250,10 @@ var require_perf_pattern_detector = __commonJS({
15209
15250
  if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
15210
15251
  return;
15211
15252
  const methodName = expr.getName();
15253
+ if (BOUNDED_QUERY_METHODS.has(methodName))
15254
+ return;
15255
+ if (isServiceMethodCall(node))
15256
+ return;
15212
15257
  if (TYPEORM_QB_TERMINAL.has(methodName) && methodName !== "getCount" && methodName !== "getOne" && methodName !== "getRawOne") {
15213
15258
  const chain = traceQueryBuilderChain(node);
15214
15259
  if (chain.hasTake)
@@ -15287,6 +15332,13 @@ var require_perf_pattern_detector = __commonJS({
15287
15332
  entity = extractTypeORMEntity(node);
15288
15333
  if (!entity)
15289
15334
  entity = extractSequelizeEntity(node);
15335
+ if (methodName === "find") {
15336
+ const typeormEntity = extractTypeORMEntity(node);
15337
+ const seqEntity = extractSequelizeEntity(node);
15338
+ if (!typeormEntity && !seqEntity)
15339
+ return;
15340
+ entity = typeormEntity ?? seqEntity;
15341
+ }
15290
15342
  const vol = entity ? resolveVolume(entity, policy) : void 0;
15291
15343
  const fn = getEnclosingFn(node, fns);
15292
15344
  pushIfNotNull(violations, makeViolation(shared_1.PERFORMANCE_RULES.UNBOUNDED_FIND_MANY, filePath, node.getStartLineNumber(), `${methodName}() without take/limit${vol ? ` on '${entity}' (${vol.size} volume)` : ""} \u2014 may fetch entire table`, policy, vol, fn?.name, "Add take: N or limit to bound the result set"));
@@ -15300,6 +15352,10 @@ var require_perf_pattern_detector = __commonJS({
15300
15352
  if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
15301
15353
  return;
15302
15354
  const methodName = expr.getName();
15355
+ if (BOUNDED_QUERY_METHODS.has(methodName))
15356
+ return;
15357
+ if (isServiceMethodCall(node))
15358
+ return;
15303
15359
  if (TYPEORM_QB_TERMINAL.has(methodName) && methodName !== "getCount" && methodName !== "getOne" && methodName !== "getRawOne") {
15304
15360
  const chain = traceQueryBuilderChain(node);
15305
15361
  if (chain.hasWhere)
@@ -15388,6 +15444,8 @@ var require_perf_pattern_detector = __commonJS({
15388
15444
  entity = extractTypeORMEntity(node);
15389
15445
  if (!entity)
15390
15446
  entity = extractSequelizeEntity(node);
15447
+ if (methodName === "find")
15448
+ return;
15391
15449
  const vol = entity ? resolveVolume(entity, policy) : void 0;
15392
15450
  const hasCursorPagination = argsContainKey(node, "cursor");
15393
15451
  const hasTakePagination = argsContainKey(node, "take") || argsContainKey(node, "limit");
@@ -15876,6 +15934,9 @@ var require_perf_pattern_detector = __commonJS({
15876
15934
  const bodyText = fn.node.getText();
15877
15935
  if (!/\.(findMany|find|getMany|findAll|from)\s*\(/.test(bodyText) && !/knex\s*\(/.test(bodyText))
15878
15936
  continue;
15937
+ const findCallMatches = [...bodyText.matchAll(/(\w+)\.(findMany|find|getMany|findAll|from)\s*\(/g)];
15938
+ if (findCallMatches.length > 0 && findCallMatches.every((m) => /Service$/i.test(m[1])))
15939
+ continue;
15879
15940
  if (/page|skip|offset|cursor|take|limit|per_page|pageSize/i.test(bodyText))
15880
15941
  continue;
15881
15942
  const entityMatch = bodyText.match(/(\w+)\.(findMany|find|getMany|findAll)\s*\(/);
@@ -16618,9 +16679,11 @@ var require_reliability_detector = __commonJS({
16618
16679
  continue;
16619
16680
  let unhandledAwaitCount = 0;
16620
16681
  let totalAwaitCount = 0;
16621
- fn.node.forEachDescendant((child) => {
16622
- if (child !== fn.node && (ts_morph_1.Node.isFunctionDeclaration(child) || ts_morph_1.Node.isMethodDeclaration(child) || ts_morph_1.Node.isArrowFunction(child) || ts_morph_1.Node.isFunctionExpression(child)))
16682
+ fn.node.forEachDescendant((child, traversal) => {
16683
+ if (child !== fn.node && (ts_morph_1.Node.isFunctionDeclaration(child) || ts_morph_1.Node.isMethodDeclaration(child) || ts_morph_1.Node.isArrowFunction(child) || ts_morph_1.Node.isFunctionExpression(child))) {
16684
+ traversal.skip();
16623
16685
  return;
16686
+ }
16624
16687
  if (!ts_morph_1.Node.isAwaitExpression(child))
16625
16688
  return;
16626
16689
  totalAwaitCount++;
@@ -17022,7 +17085,7 @@ var require_reliability_detector = __commonJS({
17022
17085
  });
17023
17086
  }
17024
17087
  var ROUTE_DECORATORS = /* @__PURE__ */ new Set(["Get", "Post", "Put", "Delete", "Patch", "All", "Head", "Options"]);
17025
- var GUARD_TODO_PATTERN = /TODO|FIXME|HACK|guard|auth|security|protected/i;
17088
+ var GUARD_TODO_PATTERN = /\b(TODO|FIXME|HACK|XXX)\b/i;
17026
17089
  function detectUnguardedRouteTodo(sourceFile, filePath, fns, policy, violations) {
17027
17090
  if (!filePath.includes("controller"))
17028
17091
  return;
@@ -18710,6 +18773,12 @@ var require_dead_code_detector = __commonJS({
18710
18773
  if (aliasResolved)
18711
18774
  return aliasResolved;
18712
18775
  }
18776
+ const aliasStripped = specifier.replace(/^@\//, "src/").replace(/^~\//, "src/");
18777
+ if (aliasStripped !== specifier) {
18778
+ const aliasResolved = resolveInProject(aliasStripped, project);
18779
+ if (aliasResolved)
18780
+ return aliasResolved;
18781
+ }
18713
18782
  const directResolved = resolveInProject(specifier, project);
18714
18783
  if (directResolved)
18715
18784
  return directResolved;
@@ -19184,7 +19253,7 @@ var require_orchestrator = __commonJS({
19184
19253
  }
19185
19254
  const complexityViolations = complexityDeltasToViolations(complexityDeltas, filteredInput);
19186
19255
  const largeFileViolations = detectLargeFiles(filteredInput);
19187
- const violations = [
19256
+ const rawViolations = [
19188
19257
  ...boundaryViolations,
19189
19258
  ...circularViolations,
19190
19259
  ...runtimeViolations,
@@ -19198,6 +19267,7 @@ var require_orchestrator = __commonJS({
19198
19267
  ...coverageDeltaResult?.violations ?? [],
19199
19268
  ...crossFileAnalysis?.violations ?? []
19200
19269
  ];
19270
+ const violations = deduplicateViolations(rawViolations);
19201
19271
  const suspectFunctions = selectSuspectFunctions(violations, complexityDeltas, filteredInput);
19202
19272
  const metrics = computeMetrics(filteredInput, importGraph, violations, complexityDeltas, suspectFunctions, startMs, duplicationResult, missingTestsResult, deadCodeResult, crossFileAnalysis, coverageDeltaResult);
19203
19273
  return {
@@ -19209,6 +19279,87 @@ var require_orchestrator = __commonJS({
19209
19279
  crossFileAnalysis
19210
19280
  };
19211
19281
  }
19282
+ var PERF_DEDUP_RULES = /* @__PURE__ */ new Set([
19283
+ "unbounded-find-many",
19284
+ "find-many-no-where",
19285
+ "missing-pagination-endpoint"
19286
+ ]);
19287
+ function deduplicateViolations(violations) {
19288
+ let result = violations;
19289
+ result = deduplicateControllerService(result);
19290
+ result = deduplicateNestedScopes(result);
19291
+ return result;
19292
+ }
19293
+ function deduplicateControllerService(violations) {
19294
+ const perfViolations = violations.filter((v) => PERF_DEDUP_RULES.has(v.ruleId));
19295
+ if (perfViolations.length === 0)
19296
+ return violations;
19297
+ const serviceViolationKeys = /* @__PURE__ */ new Set();
19298
+ for (const v of perfViolations) {
19299
+ if (/\.service\./i.test(v.file)) {
19300
+ serviceViolationKeys.add(v.ruleId);
19301
+ }
19302
+ }
19303
+ const serviceByRuleAndDir = /* @__PURE__ */ new Map();
19304
+ for (const v of perfViolations) {
19305
+ if (/\.service\./i.test(v.file)) {
19306
+ const dir = v.file.substring(0, v.file.lastIndexOf("/") + 1);
19307
+ const key = `${v.ruleId}::${dir}`;
19308
+ if (!serviceByRuleAndDir.has(key))
19309
+ serviceByRuleAndDir.set(key, /* @__PURE__ */ new Set());
19310
+ serviceByRuleAndDir.get(key).add(v.function ?? "");
19311
+ }
19312
+ }
19313
+ return violations.filter((v) => {
19314
+ if (!PERF_DEDUP_RULES.has(v.ruleId))
19315
+ return true;
19316
+ if (!/\.controller\./i.test(v.file))
19317
+ return true;
19318
+ const dir = v.file.substring(0, v.file.lastIndexOf("/") + 1);
19319
+ const key = `${v.ruleId}::${dir}`;
19320
+ if (serviceByRuleAndDir.has(key))
19321
+ return false;
19322
+ if (serviceViolationKeys.has(v.ruleId) && /\.controller\./i.test(v.file))
19323
+ return false;
19324
+ return true;
19325
+ });
19326
+ }
19327
+ var NESTED_SCOPE_DEDUP_RULES = /* @__PURE__ */ new Set([
19328
+ "missing-try-catch",
19329
+ "unhandled-promise-rejection"
19330
+ ]);
19331
+ function deduplicateNestedScopes(violations) {
19332
+ const groups = /* @__PURE__ */ new Map();
19333
+ for (const v of violations) {
19334
+ if (!NESTED_SCOPE_DEDUP_RULES.has(v.ruleId))
19335
+ continue;
19336
+ const key = `${v.file}::${v.ruleId}`;
19337
+ if (!groups.has(key))
19338
+ groups.set(key, []);
19339
+ groups.get(key).push(v);
19340
+ }
19341
+ const toRemove = /* @__PURE__ */ new Set();
19342
+ for (const [, group] of groups) {
19343
+ if (group.length < 2)
19344
+ continue;
19345
+ const byFunction = /* @__PURE__ */ new Map();
19346
+ for (const v of group) {
19347
+ const fn = v.function ?? "";
19348
+ if (!byFunction.has(fn))
19349
+ byFunction.set(fn, []);
19350
+ byFunction.get(fn).push(v);
19351
+ }
19352
+ for (const [, fnGroup] of byFunction) {
19353
+ if (fnGroup.length < 2)
19354
+ continue;
19355
+ fnGroup.sort((a, b) => a.line - b.line);
19356
+ for (let i = 1; i < fnGroup.length; i++) {
19357
+ toRemove.add(fnGroup[i]);
19358
+ }
19359
+ }
19360
+ }
19361
+ return violations.filter((v) => !toRemove.has(v));
19362
+ }
19212
19363
  function detectLargeFiles(input) {
19213
19364
  const threshold = shared_1.DEFAULT_LARGE_FILE_THRESHOLD;
19214
19365
  const violations = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "technical-debt-radar",
3
- "version": "1.6.3",
3
+ "version": "1.6.5",
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",