technical-debt-radar 1.6.4 → 1.7.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 +445 -22
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -12888,6 +12888,7 @@ var require_boundary_checker = __commonJS({
12888
12888
  "use strict";
12889
12889
  Object.defineProperty(exports2, "__esModule", { value: true });
12890
12890
  exports2.checkBoundaries = checkBoundaries;
12891
+ exports2._resetPathAliasCache = _resetPathAliasCache;
12891
12892
  var shared_1 = require_dist();
12892
12893
  var minimatch_1 = require_commonjs3();
12893
12894
  var ts_morph_1 = require("ts-morph");
@@ -13191,10 +13192,15 @@ var require_boundary_checker = __commonJS({
13191
13192
  specifiers.push(reqSpec);
13192
13193
  }
13193
13194
  for (const { specifier, line, typeOnly } of specifiers) {
13194
- if (!specifier.startsWith(".") && !specifier.startsWith("/"))
13195
+ let resolvedTarget;
13196
+ if (specifier.startsWith(".") || specifier.startsWith("/")) {
13197
+ const sourceDir = filePath.replace(/\/[^/]+$/, "");
13198
+ resolvedTarget = resolveRelativePath(sourceDir, specifier);
13199
+ } else {
13200
+ resolvedTarget = resolvePathAliasForBoundary(specifier, input);
13201
+ }
13202
+ if (!resolvedTarget)
13195
13203
  continue;
13196
- const sourceDir = filePath.replace(/\/[^/]+$/, "");
13197
- const resolvedTarget = resolveRelativePath(sourceDir, specifier);
13198
13204
  const targetModule = findModuleForFile(resolvedTarget, policy);
13199
13205
  if (!targetModule || targetModule === sourceModule)
13200
13206
  continue;
@@ -13379,6 +13385,82 @@ var require_boundary_checker = __commonJS({
13379
13385
  const normalized = normalizeForMatching(file);
13380
13386
  return exceptions.some((exc) => exc.rule === ruleId && ((0, minimatch_1.minimatch)(file, exc.file) || (0, minimatch_1.minimatch)(normalized, exc.file)));
13381
13387
  }
13388
+ var _cachedAliases;
13389
+ function parsePathAliasesFromProject(input) {
13390
+ if (_cachedAliases)
13391
+ return _cachedAliases;
13392
+ let tsconfigContent;
13393
+ const tsconfigFile = input.changedFiles.find((f) => f.status !== "deleted" && /tsconfig(?:\.build|\.app)?\.json$/.test(f.path));
13394
+ if (tsconfigFile) {
13395
+ tsconfigContent = tsconfigFile.content;
13396
+ }
13397
+ const root = input.projectRoot;
13398
+ if (!tsconfigContent && root) {
13399
+ try {
13400
+ const fs9 = require("fs");
13401
+ const path9 = require("path");
13402
+ for (const name of ["tsconfig.json", "tsconfig.app.json", "tsconfig.build.json"]) {
13403
+ const tsconfigPath = path9.join(root, name);
13404
+ if (fs9.existsSync(tsconfigPath)) {
13405
+ tsconfigContent = fs9.readFileSync(tsconfigPath, "utf-8");
13406
+ break;
13407
+ }
13408
+ }
13409
+ } catch {
13410
+ }
13411
+ }
13412
+ if (!tsconfigContent) {
13413
+ _cachedAliases = [];
13414
+ return _cachedAliases;
13415
+ }
13416
+ try {
13417
+ const stripped = tsconfigContent.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,\s*([}\]])/g, "$1");
13418
+ const tsconfig = JSON.parse(stripped);
13419
+ const compilerOptions = tsconfig.compilerOptions ?? {};
13420
+ const rawBaseUrl = (compilerOptions.baseUrl ?? ".").replace(/\/+$/, "");
13421
+ const baseUrl = rawBaseUrl === "" ? "." : rawBaseUrl;
13422
+ const paths = compilerOptions.paths ?? {};
13423
+ const aliases = [];
13424
+ for (const [pattern, targets] of Object.entries(paths)) {
13425
+ const prefix = pattern.replace(/\/?\*$/, "");
13426
+ const resolvedTargets = targets.map((t) => {
13427
+ const target = t.replace(/\/?\*$/, "");
13428
+ if (baseUrl !== "." && !target.startsWith("/")) {
13429
+ return (baseUrl + "/" + target).replace(/\/+/g, "/");
13430
+ }
13431
+ return target;
13432
+ });
13433
+ aliases.push({ prefix, targets: resolvedTargets });
13434
+ }
13435
+ _cachedAliases = aliases;
13436
+ return aliases;
13437
+ } catch {
13438
+ _cachedAliases = [];
13439
+ return _cachedAliases;
13440
+ }
13441
+ }
13442
+ function resolvePathAliasForBoundary(specifier, input) {
13443
+ const aliases = parsePathAliasesFromProject(input);
13444
+ for (const alias of aliases) {
13445
+ if (alias.prefix === "" || specifier === alias.prefix || specifier.startsWith(alias.prefix + "/")) {
13446
+ const remainder = alias.prefix === "" ? specifier : specifier.slice(alias.prefix.length).replace(/^\//, "");
13447
+ for (const target of alias.targets) {
13448
+ const candidate = remainder ? target ? target + "/" + remainder : remainder : target;
13449
+ if (candidate.startsWith("src/") || candidate.startsWith("lib/") || candidate.startsWith("app/")) {
13450
+ return candidate;
13451
+ }
13452
+ }
13453
+ }
13454
+ }
13455
+ if (specifier.startsWith("@/"))
13456
+ return "src/" + specifier.slice(2);
13457
+ if (specifier.startsWith("~/"))
13458
+ return "src/" + specifier.slice(2);
13459
+ return void 0;
13460
+ }
13461
+ function _resetPathAliasCache() {
13462
+ _cachedAliases = void 0;
13463
+ }
13382
13464
  function normalizeForMatching(filePath) {
13383
13465
  let normalized = filePath.replace(/\\/g, "/");
13384
13466
  if (normalized.startsWith("/")) {
@@ -15332,6 +15414,13 @@ var require_perf_pattern_detector = __commonJS({
15332
15414
  entity = extractTypeORMEntity(node);
15333
15415
  if (!entity)
15334
15416
  entity = extractSequelizeEntity(node);
15417
+ if (methodName === "find") {
15418
+ const typeormEntity = extractTypeORMEntity(node);
15419
+ const seqEntity = extractSequelizeEntity(node);
15420
+ if (!typeormEntity && !seqEntity)
15421
+ return;
15422
+ entity = typeormEntity ?? seqEntity;
15423
+ }
15335
15424
  const vol = entity ? resolveVolume(entity, policy) : void 0;
15336
15425
  const fn = getEnclosingFn(node, fns);
15337
15426
  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"));
@@ -15437,6 +15526,8 @@ var require_perf_pattern_detector = __commonJS({
15437
15526
  entity = extractTypeORMEntity(node);
15438
15527
  if (!entity)
15439
15528
  entity = extractSequelizeEntity(node);
15529
+ if (methodName === "find")
15530
+ return;
15440
15531
  const vol = entity ? resolveVolume(entity, policy) : void 0;
15441
15532
  const hasCursorPagination = argsContainKey(node, "cursor");
15442
15533
  const hasTakePagination = argsContainKey(node, "take") || argsContainKey(node, "limit");
@@ -16241,6 +16332,7 @@ var require_reliability_detector = __commonJS({
16241
16332
  exports2.detectReliabilityIssues = detectReliabilityIssues;
16242
16333
  var shared_1 = require_dist();
16243
16334
  var ts_morph_1 = require("ts-morph");
16335
+ var minimatch_1 = require_commonjs3();
16244
16336
  async function detectReliabilityIssues(input, policy) {
16245
16337
  const violations = [];
16246
16338
  const project = new ts_morph_1.Project({ useInMemoryFileSystem: true });
@@ -16271,6 +16363,10 @@ var require_reliability_detector = __commonJS({
16271
16363
  detectUnboundedParallelCalls(sourceFile, file.path, fns, policy, violations);
16272
16364
  project.removeSourceFile(sourceFile);
16273
16365
  }
16366
+ if (/mongoose/i.test(policy.stack.orm)) {
16367
+ const schemaViolations = detectQueryNonexistentField(input);
16368
+ violations.push(...schemaViolations);
16369
+ }
16274
16370
  return applyExceptions(deduplicateOverlapping(violations), policy);
16275
16371
  }
16276
16372
  function deduplicateOverlapping(violations) {
@@ -16670,9 +16766,11 @@ var require_reliability_detector = __commonJS({
16670
16766
  continue;
16671
16767
  let unhandledAwaitCount = 0;
16672
16768
  let totalAwaitCount = 0;
16673
- fn.node.forEachDescendant((child) => {
16674
- 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)))
16769
+ fn.node.forEachDescendant((child, traversal) => {
16770
+ 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))) {
16771
+ traversal.skip();
16675
16772
  return;
16773
+ }
16676
16774
  if (!ts_morph_1.Node.isAwaitExpression(child))
16677
16775
  return;
16678
16776
  totalAwaitCount++;
@@ -17189,9 +17287,44 @@ var require_reliability_detector = __commonJS({
17189
17287
  });
17190
17288
  });
17191
17289
  }
17290
+ function stripToSrcRoot(filePath) {
17291
+ const markers = ["src/", "apps/", "lib/", "packages/"];
17292
+ for (const marker of markers) {
17293
+ const idx = filePath.indexOf(marker);
17294
+ if (idx >= 0)
17295
+ return filePath.slice(idx);
17296
+ }
17297
+ return filePath;
17298
+ }
17299
+ function resolveRelativeImport(sourceDir, specifier) {
17300
+ const parts = (sourceDir + "/" + specifier).split("/");
17301
+ const resolved = [];
17302
+ for (const part of parts) {
17303
+ if (part === "." || part === "")
17304
+ continue;
17305
+ if (part === "..")
17306
+ resolved.pop();
17307
+ else
17308
+ resolved.push(part);
17309
+ }
17310
+ return resolved.join("/");
17311
+ }
17312
+ function findModuleName(filePath, policy) {
17313
+ const normalized = filePath.replace(/\\/g, "/");
17314
+ const stripped = normalized.startsWith("/") ? normalized.slice(1) : normalized;
17315
+ for (const mod of policy.modules) {
17316
+ if ((0, minimatch_1.minimatch)(stripped, mod.pathPattern) || (0, minimatch_1.minimatch)(normalized, mod.pathPattern)) {
17317
+ return mod.name;
17318
+ }
17319
+ }
17320
+ const match = stripped.match(/^src\/([^/]+)/);
17321
+ return match ? match[1] : void 0;
17322
+ }
17192
17323
  function detectProviderOutsideHomeModule(sourceFile, filePath, allFiles, policy, violations) {
17193
17324
  if (!filePath.endsWith(".module.ts") && !filePath.endsWith(".module.js"))
17194
17325
  return;
17326
+ if (!/nestjs/i.test(policy.stack.framework))
17327
+ return;
17195
17328
  sourceFile.forEachDescendant((node) => {
17196
17329
  if (!ts_morph_1.Node.isDecorator(node) || node.getName() !== "Module")
17197
17330
  return;
@@ -17222,24 +17355,40 @@ var require_reliability_detector = __commonJS({
17222
17355
  if (!matchingImport)
17223
17356
  continue;
17224
17357
  const specifier = importDecl.getModuleSpecifierValue();
17358
+ let resolvedPath;
17225
17359
  if (specifier.startsWith(".") || specifier.startsWith("/")) {
17226
- const resolvedPath = specifier.replace(/^\.\//, moduleFolder + "/").replace(/^\.\.\//, moduleFolder + "/../");
17227
- if (!resolvedPath.startsWith(moduleFolder) && !specifier.startsWith("./")) {
17228
- violations.push({
17229
- category: "architecture",
17230
- type: shared_1.ARCHITECTURE_RULES.PROVIDER_OUTSIDE_HOME,
17231
- ruleId: shared_1.ARCHITECTURE_RULES.PROVIDER_OUTSIDE_HOME,
17232
- severity: "warning",
17233
- source: "deterministic",
17234
- confidence: "high",
17235
- file: filePath,
17236
- line: element.getStartLineNumber(),
17237
- message: `Provider '${providerName}' is imported from outside this module's folder \u2014 consider moving it to its home module or a shared module`,
17238
- suggestion: `Move ${providerName} to this module's folder or register it in the module where it's defined`,
17239
- debtPoints: 3,
17240
- gateAction: "warn"
17241
- });
17242
- }
17360
+ resolvedPath = resolveRelativeImport(moduleFolder, specifier);
17361
+ } else if (specifier.startsWith("@/")) {
17362
+ resolvedPath = "src/" + specifier.slice(2);
17363
+ } else if (specifier.startsWith("~/")) {
17364
+ resolvedPath = "src/" + specifier.slice(2);
17365
+ }
17366
+ if (!resolvedPath)
17367
+ continue;
17368
+ const normalizedModule = stripToSrcRoot(moduleFolder.replace(/\\/g, "/"));
17369
+ const normalizedResolved = stripToSrcRoot(resolvedPath.replace(/\\/g, "/"));
17370
+ const isFromOutside = !normalizedResolved.startsWith(normalizedModule + "/") && normalizedResolved !== normalizedModule;
17371
+ const sharedFolders = ["shared", "common", "utils", "config", "guards", "interceptors", "decorators", "pipes", "filters", "strategies"];
17372
+ const isShared = sharedFolders.some((f) => normalizedResolved.includes("/" + f + "/"));
17373
+ if (isFromOutside && !isShared) {
17374
+ const sourceModuleName = findModuleName(normalizedResolved, policy);
17375
+ const targetModuleName = findModuleName(filePath, policy);
17376
+ const moduleInfo = sourceModuleName ? ` (${sourceModuleName} module)` : "";
17377
+ const targetInfo = targetModuleName ? ` (${targetModuleName} module)` : "";
17378
+ violations.push({
17379
+ category: "architecture",
17380
+ type: shared_1.ARCHITECTURE_RULES.PROVIDER_OUTSIDE_HOME,
17381
+ ruleId: shared_1.ARCHITECTURE_RULES.PROVIDER_OUTSIDE_HOME,
17382
+ severity: "warning",
17383
+ source: "deterministic",
17384
+ confidence: "high",
17385
+ file: filePath,
17386
+ line: element.getStartLineNumber(),
17387
+ message: `Provider '${providerName}'${moduleInfo} is registered outside its home module${targetInfo} \u2014 move it to its home module's providers and export it`,
17388
+ suggestion: `Move ${providerName} to its home module's providers array and export it, then import that module here`,
17389
+ debtPoints: 3,
17390
+ gateAction: "warn"
17391
+ });
17243
17392
  }
17244
17393
  }
17245
17394
  }
@@ -17396,6 +17545,163 @@ var require_reliability_detector = __commonJS({
17396
17545
  function escapeRegex(str) {
17397
17546
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
17398
17547
  }
17548
+ var MONGOOSE_QUERY_METHODS = /* @__PURE__ */ new Set([
17549
+ "find",
17550
+ "findOne",
17551
+ "findById",
17552
+ "findOneAndUpdate",
17553
+ "findOneAndDelete",
17554
+ "findOneAndReplace",
17555
+ "findOneAndRemove",
17556
+ "findByIdAndUpdate",
17557
+ "findByIdAndDelete",
17558
+ "updateOne",
17559
+ "updateMany",
17560
+ "deleteOne",
17561
+ "deleteMany",
17562
+ "countDocuments",
17563
+ "exists",
17564
+ "distinct"
17565
+ ]);
17566
+ var BUILTIN_FIELDS = /* @__PURE__ */ new Set(["_id", "__v", "id", "createdAt", "updatedAt"]);
17567
+ function detectQueryNonexistentField(input) {
17568
+ const violations = [];
17569
+ const project = new ts_morph_1.Project({ useInMemoryFileSystem: true });
17570
+ const schemaFieldMap = /* @__PURE__ */ new Map();
17571
+ for (const file of input.changedFiles) {
17572
+ if (file.status === "deleted")
17573
+ continue;
17574
+ if (!/\.schema\.(ts|js)$/.test(file.path) && !/schemas?\//.test(file.path))
17575
+ continue;
17576
+ if (!/\.(ts|tsx|js|jsx)$/.test(file.path))
17577
+ continue;
17578
+ const sf = project.createSourceFile(file.path, file.content);
17579
+ for (const cls of sf.getClasses()) {
17580
+ const schemaDecorator = cls.getDecorators().find((d) => d.getName() === "Schema");
17581
+ if (!schemaDecorator)
17582
+ continue;
17583
+ const schemaName = cls.getName();
17584
+ if (!schemaName)
17585
+ continue;
17586
+ const fields = new Set(BUILTIN_FIELDS);
17587
+ for (const prop of cls.getProperties()) {
17588
+ if (prop.getDecorators().some((d) => d.getName() === "Prop")) {
17589
+ fields.add(prop.getName());
17590
+ }
17591
+ }
17592
+ const schemaArgs = schemaDecorator.getCallExpression?.()?.getArguments();
17593
+ if (schemaArgs && schemaArgs.length > 0) {
17594
+ const argText = schemaArgs[0].getText();
17595
+ if (/timestamps\s*:\s*true/.test(argText)) {
17596
+ fields.add("createdAt");
17597
+ fields.add("updatedAt");
17598
+ }
17599
+ }
17600
+ schemaFieldMap.set(schemaName, fields);
17601
+ }
17602
+ project.removeSourceFile(sf);
17603
+ }
17604
+ if (schemaFieldMap.size === 0)
17605
+ return violations;
17606
+ for (const file of input.changedFiles) {
17607
+ if (file.status === "deleted")
17608
+ continue;
17609
+ if (!/\.(ts|tsx|js|jsx)$/.test(file.path))
17610
+ continue;
17611
+ if (/\.schema\.(ts|js)$/.test(file.path))
17612
+ continue;
17613
+ const sf = project.createSourceFile(file.path, file.content);
17614
+ const modelToSchema = /* @__PURE__ */ new Map();
17615
+ for (const cls of sf.getClasses()) {
17616
+ const ctors = cls.getConstructors();
17617
+ for (const ctor of ctors) {
17618
+ for (const param of ctor.getParameters()) {
17619
+ const injectModel = param.getDecorators().find((d) => d.getName() === "InjectModel");
17620
+ if (!injectModel)
17621
+ continue;
17622
+ const callExpr = injectModel.getCallExpression?.();
17623
+ if (!callExpr)
17624
+ continue;
17625
+ const args = callExpr.getArguments();
17626
+ if (args.length === 0)
17627
+ continue;
17628
+ let schemaName = args[0].getText();
17629
+ schemaName = schemaName.replace(/\.name$/, "").replace(/['"]/g, "");
17630
+ const paramName = param.getName();
17631
+ modelToSchema.set(paramName, schemaName);
17632
+ }
17633
+ }
17634
+ }
17635
+ if (modelToSchema.size === 0) {
17636
+ project.removeSourceFile(sf);
17637
+ continue;
17638
+ }
17639
+ sf.forEachDescendant((node) => {
17640
+ if (!ts_morph_1.Node.isCallExpression(node))
17641
+ return;
17642
+ const expr = node.getExpression();
17643
+ if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
17644
+ return;
17645
+ const methodName = expr.getName();
17646
+ if (!MONGOOSE_QUERY_METHODS.has(methodName))
17647
+ return;
17648
+ const obj = expr.getExpression();
17649
+ if (!ts_morph_1.Node.isPropertyAccessExpression(obj))
17650
+ return;
17651
+ const propName = obj.getName();
17652
+ const thisExpr = obj.getExpression();
17653
+ if (thisExpr.getKind() !== ts_morph_1.SyntaxKind.ThisKeyword)
17654
+ return;
17655
+ const schemaName = modelToSchema.get(propName);
17656
+ if (!schemaName)
17657
+ return;
17658
+ const schemaFields = schemaFieldMap.get(schemaName);
17659
+ if (!schemaFields)
17660
+ return;
17661
+ const args = node.getArguments();
17662
+ if (args.length === 0)
17663
+ return;
17664
+ const filterArg = args[0];
17665
+ if (!ts_morph_1.Node.isObjectLiteralExpression(filterArg))
17666
+ return;
17667
+ for (const prop of filterArg.getProperties()) {
17668
+ if (!ts_morph_1.Node.isPropertyAssignment(prop))
17669
+ continue;
17670
+ const nameNode = prop.getNameNode();
17671
+ let fieldName;
17672
+ if (ts_morph_1.Node.isStringLiteral(nameNode)) {
17673
+ fieldName = nameNode.getLiteralValue();
17674
+ } else if (ts_morph_1.Node.isIdentifier(nameNode)) {
17675
+ fieldName = nameNode.getText();
17676
+ } else if (ts_morph_1.Node.isComputedPropertyName(nameNode)) {
17677
+ continue;
17678
+ } else {
17679
+ fieldName = nameNode.getText();
17680
+ }
17681
+ const rootField = fieldName.split(".")[0];
17682
+ if (!schemaFields.has(rootField)) {
17683
+ const availableFields = [...schemaFields].filter((f) => !BUILTIN_FIELDS.has(f) || f === "_id").sort().join(", ");
17684
+ violations.push({
17685
+ category: "reliability",
17686
+ type: "query-references-nonexistent-field",
17687
+ ruleId: "query-references-nonexistent-field",
17688
+ severity: "critical",
17689
+ source: "deterministic",
17690
+ confidence: "high",
17691
+ file: file.path,
17692
+ line: prop.getStartLineNumber(),
17693
+ message: `Query filter references field '${fieldName}' which does not exist in ${schemaName} schema \u2014 available fields: ${availableFields}`,
17694
+ suggestion: `Check the ${schemaName} schema definition for the correct field name`,
17695
+ debtPoints: 8,
17696
+ gateAction: "block"
17697
+ });
17698
+ }
17699
+ }
17700
+ });
17701
+ project.removeSourceFile(sf);
17702
+ }
17703
+ return violations;
17704
+ }
17399
17705
  function applyExceptions(violations, policy) {
17400
17706
  return violations.filter((v) => !policy.exceptions.some((ex) => ex.isActive && ex.rule === v.ruleId && v.file.includes(ex.file)));
17401
17707
  }
@@ -18463,6 +18769,8 @@ var require_dead_code_detector = __commonJS({
18463
18769
  debtPoints: 1,
18464
18770
  gateAction: "warn"
18465
18771
  }));
18772
+ const unusedMethodViolations = detectUnusedPublicMethods(sourceFiles, input);
18773
+ violations.push(...unusedMethodViolations);
18466
18774
  const totalExports = Array.from(allExports.values()).reduce((sum, exps) => sum + exps.length, 0);
18467
18775
  return {
18468
18776
  unusedExports,
@@ -18818,6 +19126,121 @@ var require_dead_code_detector = __commonJS({
18818
19126
  }
18819
19127
  return result.join("/");
18820
19128
  }
19129
+ var NESTJS_METHOD_DECORATORS = /* @__PURE__ */ new Set([
19130
+ "Get",
19131
+ "Post",
19132
+ "Put",
19133
+ "Patch",
19134
+ "Delete",
19135
+ "Options",
19136
+ "Head",
19137
+ "All",
19138
+ "Cron",
19139
+ "OnEvent",
19140
+ "Process",
19141
+ "OnQueueActive",
19142
+ "OnQueueCompleted",
19143
+ "OnQueueFailed",
19144
+ "SubscribeMessage",
19145
+ "OnGatewayInit",
19146
+ "OnGatewayConnection",
19147
+ "OnGatewayDisconnect",
19148
+ "EventPattern",
19149
+ "MessagePattern",
19150
+ "GrpcMethod"
19151
+ ]);
19152
+ var LIFECYCLE_HOOKS = /* @__PURE__ */ new Set([
19153
+ "onModuleInit",
19154
+ "onModuleDestroy",
19155
+ "onApplicationBootstrap",
19156
+ "onApplicationShutdown",
19157
+ "beforeApplicationShutdown",
19158
+ "afterInit",
19159
+ "handleConnection",
19160
+ "handleDisconnect",
19161
+ "validate",
19162
+ // Passport strategy method — called by framework
19163
+ "canActivate",
19164
+ // Guard method
19165
+ "intercept",
19166
+ // Interceptor method
19167
+ "transform",
19168
+ // Pipe method
19169
+ "catch",
19170
+ // Exception filter method
19171
+ "use"
19172
+ // Middleware method
19173
+ ]);
19174
+ function detectUnusedPublicMethods(sourceFiles, input) {
19175
+ const violations = [];
19176
+ const isNestJS = /nestjs/i.test(input.policy.stack.framework);
19177
+ const allFileTexts = /* @__PURE__ */ new Map();
19178
+ for (const [filePath, sf] of sourceFiles) {
19179
+ allFileTexts.set(filePath, sf.getFullText());
19180
+ }
19181
+ for (const [filePath, sf] of sourceFiles) {
19182
+ if (/\.(spec|test)\./i.test(filePath))
19183
+ continue;
19184
+ if (/\.module\./i.test(filePath))
19185
+ continue;
19186
+ for (const cls of sf.getClasses()) {
19187
+ const hasInjectable = cls.getDecorators().some((d) => d.getName() === "Injectable");
19188
+ if (!hasInjectable)
19189
+ continue;
19190
+ const baseClass = cls.getExtends()?.getText() ?? "";
19191
+ if (/PassportStrategy|Strategy/.test(baseClass))
19192
+ continue;
19193
+ const className = cls.getName();
19194
+ if (!className)
19195
+ continue;
19196
+ for (const method of cls.getMethods()) {
19197
+ const methodName = method.getName();
19198
+ if (!methodName || methodName.startsWith("_"))
19199
+ continue;
19200
+ const scope = method.getScope();
19201
+ if (scope === "private" || scope === "protected")
19202
+ continue;
19203
+ if (LIFECYCLE_HOOKS.has(methodName))
19204
+ continue;
19205
+ if (isNestJS && method.getDecorators().some((d) => NESTJS_METHOD_DECORATORS.has(d.getName())))
19206
+ continue;
19207
+ const callPattern = new RegExp(`\\.${methodName}\\s*\\(`);
19208
+ let hasExternalCall = false;
19209
+ for (const [otherFilePath, otherText] of allFileTexts) {
19210
+ if (otherFilePath === filePath)
19211
+ continue;
19212
+ if (callPattern.test(otherText)) {
19213
+ hasExternalCall = true;
19214
+ break;
19215
+ }
19216
+ }
19217
+ if (!hasExternalCall) {
19218
+ const ownText = allFileTexts.get(filePath) ?? "";
19219
+ const matches = ownText.match(new RegExp(`\\.${methodName}\\s*\\(`, "g"));
19220
+ const internalCallCount = matches?.length ?? 0;
19221
+ if (internalCallCount === 0) {
19222
+ violations.push({
19223
+ category: "maintainability",
19224
+ type: "unused-public-method",
19225
+ ruleId: "unused-public-method",
19226
+ severity: "warning",
19227
+ source: "deterministic",
19228
+ confidence: "medium",
19229
+ file: filePath,
19230
+ line: method.getStartLineNumber(),
19231
+ function: methodName,
19232
+ message: `Public method '${methodName}' in ${className} has no call sites \u2014 consider removing it or marking it private`,
19233
+ suggestion: `Remove the unused method or mark it as private if it's only used internally`,
19234
+ debtPoints: 1,
19235
+ gateAction: "warn"
19236
+ });
19237
+ }
19238
+ }
19239
+ }
19240
+ }
19241
+ }
19242
+ return violations;
19243
+ }
18821
19244
  function emptyResult() {
18822
19245
  return {
18823
19246
  unusedExports: [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "technical-debt-radar",
3
- "version": "1.6.4",
3
+ "version": "1.7.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",