technical-debt-radar 1.7.1 → 1.9.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 +182 -5
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -12889,6 +12889,7 @@ var require_boundary_checker = __commonJS({
12889
12889
  Object.defineProperty(exports2, "__esModule", { value: true });
12890
12890
  exports2.checkBoundaries = checkBoundaries;
12891
12891
  exports2._resetPathAliasCache = _resetPathAliasCache;
12892
+ exports2._resetModuleParsingCache = _resetModuleParsingCache;
12892
12893
  var shared_1 = require_dist();
12893
12894
  var minimatch_1 = require_commonjs3();
12894
12895
  var ts_morph_1 = require("ts-morph");
@@ -13175,6 +13176,9 @@ var require_boundary_checker = __commonJS({
13175
13176
  const filePath = file.path;
13176
13177
  const sourceModule = findModuleForFile(filePath, policy);
13177
13178
  if (!sourceModule) {
13179
+ if (isNestJSFramework(policy)) {
13180
+ checkSharedFileImports(sourceFile, filePath, policy, input, violations);
13181
+ }
13178
13182
  project.removeSourceFile(sourceFile);
13179
13183
  continue;
13180
13184
  }
@@ -13222,6 +13226,12 @@ var require_boundary_checker = __commonJS({
13222
13226
  continue;
13223
13227
  if (typeOnly)
13224
13228
  continue;
13229
+ if (isNestJS) {
13230
+ const importedNames = getImportedNamesFromSpecifier(sourceFile, specifier);
13231
+ const suppressed = importedNames.length > 0 && importedNames.every((name) => isLegitimateNestJSModuleImport(sourceModule, targetModule, name, input));
13232
+ if (suppressed)
13233
+ continue;
13234
+ }
13225
13235
  violations.push({
13226
13236
  category: "architecture",
13227
13237
  type: "module-boundary-violation",
@@ -13456,11 +13466,172 @@ var require_boundary_checker = __commonJS({
13456
13466
  return "src/" + specifier.slice(2);
13457
13467
  if (specifier.startsWith("~/"))
13458
13468
  return "src/" + specifier.slice(2);
13469
+ if (specifier.startsWith("src/") || specifier.startsWith("lib/") || specifier.startsWith("app/")) {
13470
+ return specifier;
13471
+ }
13459
13472
  return void 0;
13460
13473
  }
13461
13474
  function _resetPathAliasCache() {
13462
13475
  _cachedAliases = void 0;
13463
13476
  }
13477
+ var _cachedModuleParsing;
13478
+ function parseNestJSModules(input) {
13479
+ if (_cachedModuleParsing)
13480
+ return _cachedModuleParsing;
13481
+ const result = /* @__PURE__ */ new Map();
13482
+ const project = new ts_morph_1.Project({ useInMemoryFileSystem: true });
13483
+ for (const file of input.changedFiles) {
13484
+ if (file.status === "deleted")
13485
+ continue;
13486
+ if (!/\.module\.(ts|js)$/.test(file.path))
13487
+ continue;
13488
+ const sf = project.createSourceFile(file.path, file.content);
13489
+ for (const cls of sf.getClasses()) {
13490
+ const moduleDecorator = cls.getDecorators().find((d) => d.getName() === "Module");
13491
+ if (!moduleDecorator)
13492
+ continue;
13493
+ const className = cls.getName() ?? "";
13494
+ const pathMatch = file.path.match(/src\/([^/]+)\//);
13495
+ const moduleName = pathMatch ? pathMatch[1] : "";
13496
+ if (!moduleName)
13497
+ continue;
13498
+ const callExpr = moduleDecorator.getCallExpression?.();
13499
+ if (!callExpr)
13500
+ continue;
13501
+ const args = callExpr.getArguments();
13502
+ if (args.length === 0 || !ts_morph_1.Node.isObjectLiteralExpression(args[0]))
13503
+ continue;
13504
+ const objLit = args[0];
13505
+ const imports = [];
13506
+ const exports3 = [];
13507
+ const hasForwardRef = /* @__PURE__ */ new Map();
13508
+ const importsProp = objLit.getProperty("imports");
13509
+ if (importsProp && ts_morph_1.Node.isPropertyAssignment(importsProp)) {
13510
+ const init = importsProp.getInitializer();
13511
+ if (init && ts_morph_1.Node.isArrayLiteralExpression(init)) {
13512
+ for (const el of init.getElements()) {
13513
+ if (ts_morph_1.Node.isIdentifier(el)) {
13514
+ imports.push(el.getText());
13515
+ hasForwardRef.set(el.getText(), false);
13516
+ } else if (ts_morph_1.Node.isCallExpression(el)) {
13517
+ const callText = el.getText();
13518
+ const fwdMatch = callText.match(/forwardRef\s*\(\s*\(\)\s*=>\s*(\w+)\s*\)/);
13519
+ if (fwdMatch) {
13520
+ imports.push(fwdMatch[1]);
13521
+ hasForwardRef.set(fwdMatch[1], true);
13522
+ } else {
13523
+ const expr = el.getExpression();
13524
+ if (ts_morph_1.Node.isPropertyAccessExpression(expr)) {
13525
+ const obj = expr.getExpression();
13526
+ if (ts_morph_1.Node.isIdentifier(obj)) {
13527
+ imports.push(obj.getText());
13528
+ hasForwardRef.set(obj.getText(), false);
13529
+ }
13530
+ }
13531
+ }
13532
+ }
13533
+ }
13534
+ }
13535
+ }
13536
+ const exportsProp = objLit.getProperty("exports");
13537
+ if (exportsProp && ts_morph_1.Node.isPropertyAssignment(exportsProp)) {
13538
+ const init = exportsProp.getInitializer();
13539
+ if (init && ts_morph_1.Node.isArrayLiteralExpression(init)) {
13540
+ for (const el of init.getElements()) {
13541
+ if (ts_morph_1.Node.isIdentifier(el)) {
13542
+ exports3.push(el.getText());
13543
+ }
13544
+ }
13545
+ }
13546
+ }
13547
+ result.set(moduleName, { moduleName, className, imports, exports: exports3, hasForwardRef });
13548
+ }
13549
+ project.removeSourceFile(sf);
13550
+ }
13551
+ _cachedModuleParsing = result;
13552
+ return result;
13553
+ }
13554
+ function getImportedNamesFromSpecifier(sourceFile, specifier) {
13555
+ const names = [];
13556
+ for (const decl of sourceFile.getImportDeclarations()) {
13557
+ if (decl.getModuleSpecifierValue() !== specifier)
13558
+ continue;
13559
+ const defaultImport = decl.getDefaultImport();
13560
+ if (defaultImport)
13561
+ names.push(defaultImport.getText());
13562
+ for (const named of decl.getNamedImports()) {
13563
+ names.push(named.getName());
13564
+ }
13565
+ }
13566
+ return names;
13567
+ }
13568
+ function isLegitimateNestJSModuleImport(sourceModuleName, targetModuleName, importedClassName, input) {
13569
+ const modules = parseNestJSModules(input);
13570
+ const importingMod = modules.get(sourceModuleName);
13571
+ if (!importingMod)
13572
+ return false;
13573
+ const targetMod = modules.get(targetModuleName);
13574
+ if (!targetMod)
13575
+ return false;
13576
+ const targetClassName = targetMod.className;
13577
+ if (!importingMod.imports.includes(targetClassName))
13578
+ return false;
13579
+ if (importingMod.hasForwardRef.get(targetClassName))
13580
+ return false;
13581
+ if (!targetMod.exports.includes(importedClassName))
13582
+ return false;
13583
+ return true;
13584
+ }
13585
+ function _resetModuleParsingCache() {
13586
+ _cachedModuleParsing = void 0;
13587
+ }
13588
+ function checkSharedFileImports(sourceFile, filePath, policy, input, violations) {
13589
+ const basename2 = filePath.replace(/^.*\//, "");
13590
+ if (/^(main|cli|bootstrap|server)\.(ts|js)$/.test(basename2))
13591
+ return;
13592
+ const normalizedPath = filePath.replace(/\\/g, "/");
13593
+ const isSharedTypeFile = /\/(interfaces|types|shared|common|contracts)\//i.test(normalizedPath);
13594
+ if (!isSharedTypeFile)
13595
+ return;
13596
+ for (const decl of sourceFile.getImportDeclarations()) {
13597
+ const specifier = decl.getModuleSpecifierValue();
13598
+ const line = decl.getStartLineNumber();
13599
+ let resolvedTarget;
13600
+ if (specifier.startsWith(".") || specifier.startsWith("/")) {
13601
+ const sourceDir = filePath.replace(/\/[^/]+$/, "");
13602
+ resolvedTarget = resolveRelativePath(sourceDir, specifier);
13603
+ } else {
13604
+ resolvedTarget = resolvePathAliasForBoundary(specifier, input);
13605
+ }
13606
+ if (!resolvedTarget)
13607
+ continue;
13608
+ const targetModule = findModuleForFile(resolvedTarget, policy);
13609
+ if (!targetModule)
13610
+ continue;
13611
+ if (/\.module\.(ts|js)$/.test(resolvedTarget))
13612
+ continue;
13613
+ const targetBasename = resolvedTarget.replace(/^.*\//, "");
13614
+ if (/\.(interface|dto|enum|types?)\.(ts|js)$/.test(targetBasename))
13615
+ continue;
13616
+ if (decl.isTypeOnly())
13617
+ continue;
13618
+ violations.push({
13619
+ category: "architecture",
13620
+ type: "module-boundary-violation",
13621
+ ruleId: shared_1.ARCHITECTURE_RULES.MODULE_BOUNDARY,
13622
+ severity: "critical",
13623
+ source: "deterministic",
13624
+ confidence: "high",
13625
+ file: filePath,
13626
+ line,
13627
+ module: "shared",
13628
+ message: `Shared file imports from "${targetModule}" module \u2014 shared files should not depend on feature modules (${filePath})`,
13629
+ explanation: `Shared/interfaces files should contain plain types, not dependencies on feature module internals.`,
13630
+ debtPoints: 5,
13631
+ gateAction: "block"
13632
+ });
13633
+ }
13634
+ }
13464
13635
  function normalizeForMatching(filePath) {
13465
13636
  let normalized = filePath.replace(/\\/g, "/");
13466
13637
  if (normalized.startsWith("/")) {
@@ -18746,7 +18917,7 @@ var require_dead_code_detector = __commonJS({
18746
18917
  const sf = sourceFiles.get(filePath);
18747
18918
  if (sf && exp.type === "class" && isNestJSDIRegistered(sf, exp.name))
18748
18919
  continue;
18749
- if (sf && isReferencedInOwnFile(sf, exp.name))
18920
+ if (sf && !isTypeExport(exp.type) && isReferencedInOwnFile(sf, exp.name))
18750
18921
  continue;
18751
18922
  unusedExports.push({
18752
18923
  export: exp,
@@ -19103,6 +19274,8 @@ var require_dead_code_detector = __commonJS({
19103
19274
  for (const ext of extensions) {
19104
19275
  if (fp.endsWith(suffix + ext))
19105
19276
  return normalizeFilePath(fp);
19277
+ if (fp.endsWith(suffix + "/index" + ext))
19278
+ return normalizeFilePath(fp);
19106
19279
  }
19107
19280
  }
19108
19281
  return void 0;
@@ -19209,15 +19382,19 @@ var require_dead_code_detector = __commonJS({
19209
19382
  for (const [otherFilePath, otherText] of allFileTexts) {
19210
19383
  if (otherFilePath === filePath)
19211
19384
  continue;
19212
- if (callPattern.test(otherText)) {
19385
+ if (!callPattern.test(otherText))
19386
+ continue;
19387
+ const importPattern = new RegExp(`import\\s+.*\\b${className}\\b.*from\\s+`);
19388
+ if (importPattern.test(otherText)) {
19213
19389
  hasExternalCall = true;
19214
19390
  break;
19215
19391
  }
19216
19392
  }
19217
19393
  if (!hasExternalCall) {
19218
19394
  const ownText = allFileTexts.get(filePath) ?? "";
19219
- const matches = ownText.match(new RegExp(`\\.${methodName}\\s*\\(`, "g"));
19220
- const internalCallCount = matches?.length ?? 0;
19395
+ const selfCallPattern = new RegExp(`this\\.${methodName}\\s*\\(`, "g");
19396
+ const selfCallMatches = ownText.match(selfCallPattern);
19397
+ const internalCallCount = selfCallMatches?.length ?? 0;
19221
19398
  if (internalCallCount === 0) {
19222
19399
  violations.push({
19223
19400
  category: "maintainability",
@@ -19650,7 +19827,7 @@ var require_orchestrator = __commonJS({
19650
19827
  (0, reliability_detector_1.detectReliabilityIssues)(filteredInput, policy),
19651
19828
  (0, duplication_detector_1.detectDuplication)(filteredInput),
19652
19829
  projectRoot ? (0, missing_tests_detector_1.detectMissingTests)(filteredInput, projectRoot, buildMissingTestsConfig(policy)) : Promise.resolve(null),
19653
- (0, dead_code_detector_1.detectDeadCode)(filteredInput, {}, projectRoot),
19830
+ (0, dead_code_detector_1.detectDeadCode)(filteredInput, { excludeBarrels: false, excludeTypes: false }, projectRoot),
19654
19831
  projectRoot ? Promise.resolve((0, coverage_delta_detector_1.detectCoverageDelta)(projectRoot)) : Promise.resolve(null)
19655
19832
  ]);
19656
19833
  const runtimeViolations = runtimeResult.violations;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "technical-debt-radar",
3
- "version": "1.7.1",
3
+ "version": "1.9.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",