technical-debt-radar 1.0.3 → 1.0.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 +215 -31
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -12957,6 +12957,95 @@ var require_boundary_checker = __commonJS({
12957
12957
  }
12958
12958
  return violations;
12959
12959
  }
12960
+ function isInConstructorShorthandParam(node) {
12961
+ let current = node;
12962
+ while (current) {
12963
+ if (current.getKind() === ts_morph_1.SyntaxKind.Parameter) {
12964
+ const paramText = current.getText();
12965
+ if (/^\s*(private|protected|public|readonly)\s/.test(paramText)) {
12966
+ const paramParent = current.getParent();
12967
+ if (paramParent && paramParent.getKind() === ts_morph_1.SyntaxKind.Constructor) {
12968
+ return true;
12969
+ }
12970
+ }
12971
+ return false;
12972
+ }
12973
+ current = current.getParent();
12974
+ }
12975
+ return false;
12976
+ }
12977
+ function isTypePosition(node) {
12978
+ if (isInConstructorShorthandParam(node))
12979
+ return false;
12980
+ const parent = node.getParent();
12981
+ if (!parent)
12982
+ return false;
12983
+ const parentKind = parent.getKind();
12984
+ if (parentKind === ts_morph_1.SyntaxKind.TypeReference || parentKind === ts_morph_1.SyntaxKind.TypeQuery || parentKind === ts_morph_1.SyntaxKind.TypeAliasDeclaration || parentKind === ts_morph_1.SyntaxKind.InterfaceDeclaration || parentKind === ts_morph_1.SyntaxKind.AsExpression || parentKind === ts_morph_1.SyntaxKind.TypeAssertionExpression || parentKind === ts_morph_1.SyntaxKind.ExpressionWithTypeArguments || parentKind === ts_morph_1.SyntaxKind.MappedType || parentKind === ts_morph_1.SyntaxKind.ConditionalType || parentKind === ts_morph_1.SyntaxKind.IntersectionType || parentKind === ts_morph_1.SyntaxKind.UnionType || parentKind === ts_morph_1.SyntaxKind.TupleType || parentKind === ts_morph_1.SyntaxKind.ArrayType || parentKind === ts_morph_1.SyntaxKind.IndexedAccessType || parentKind === ts_morph_1.SyntaxKind.TypeOperator || parentKind === ts_morph_1.SyntaxKind.ParenthesizedType) {
12985
+ return true;
12986
+ }
12987
+ if (parentKind === ts_morph_1.SyntaxKind.Parameter || parentKind === ts_morph_1.SyntaxKind.PropertyDeclaration || parentKind === ts_morph_1.SyntaxKind.PropertySignature || parentKind === ts_morph_1.SyntaxKind.VariableDeclaration || parentKind === ts_morph_1.SyntaxKind.FunctionDeclaration || parentKind === ts_morph_1.SyntaxKind.MethodDeclaration || parentKind === ts_morph_1.SyntaxKind.MethodSignature || parentKind === ts_morph_1.SyntaxKind.ArrowFunction || parentKind === ts_morph_1.SyntaxKind.GetAccessor || parentKind === ts_morph_1.SyntaxKind.SetAccessor) {
12988
+ const colonToken = parent.getFirstChildByKind(ts_morph_1.SyntaxKind.ColonToken);
12989
+ if (colonToken && node.getPos() > colonToken.getPos()) {
12990
+ const equalsToken = parent.getFirstChildByKind(ts_morph_1.SyntaxKind.EqualsToken);
12991
+ if (!equalsToken || node.getPos() < equalsToken.getPos()) {
12992
+ return true;
12993
+ }
12994
+ }
12995
+ }
12996
+ if (parentKind === ts_morph_1.SyntaxKind.HeritageClause) {
12997
+ return true;
12998
+ }
12999
+ if (parentKind === ts_morph_1.SyntaxKind.QualifiedName) {
13000
+ const grandParent = parent.getParent();
13001
+ if (grandParent && grandParent.getKind() === ts_morph_1.SyntaxKind.TypeReference) {
13002
+ return true;
13003
+ }
13004
+ }
13005
+ return false;
13006
+ }
13007
+ function isImplicitlyTypeOnly(decl, sourceFile) {
13008
+ if (decl.isTypeOnly())
13009
+ return false;
13010
+ if (decl.getNamespaceImport())
13011
+ return false;
13012
+ const defaultImport = decl.getDefaultImport();
13013
+ const namedImports = decl.getNamedImports();
13014
+ if (!defaultImport && namedImports.length === 0)
13015
+ return false;
13016
+ const importedNames = [];
13017
+ if (defaultImport) {
13018
+ importedNames.push(defaultImport.getText());
13019
+ }
13020
+ for (const named of namedImports) {
13021
+ if (named.isTypeOnly())
13022
+ continue;
13023
+ importedNames.push(named.getName());
13024
+ }
13025
+ if (importedNames.length === 0)
13026
+ return true;
13027
+ for (const name of importedNames) {
13028
+ const identifiers = sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.Identifier).filter((id) => id.getText() === name);
13029
+ const usages = identifiers.filter((id) => {
13030
+ const parent = id.getParent();
13031
+ if (!parent)
13032
+ return false;
13033
+ const parentKind = parent.getKind();
13034
+ if (parentKind === ts_morph_1.SyntaxKind.ImportSpecifier || parentKind === ts_morph_1.SyntaxKind.ImportClause) {
13035
+ return false;
13036
+ }
13037
+ return true;
13038
+ });
13039
+ if (usages.length === 0)
13040
+ return false;
13041
+ for (const usage of usages) {
13042
+ if (!isTypePosition(usage)) {
13043
+ return false;
13044
+ }
13045
+ }
13046
+ }
13047
+ return true;
13048
+ }
12960
13049
  function checkCrossModuleDirectImports(input, policy, activeExceptions) {
12961
13050
  const violations = [];
12962
13051
  const hasCrossModuleDeny = policy.rules.some((r) => r.type === "deny" && /cross-module/i.test(r.description ?? "") || /cross-module/i.test(r.source));
@@ -12979,10 +13068,12 @@ var require_boundary_checker = __commonJS({
12979
13068
  }
12980
13069
  const specifiers = [];
12981
13070
  for (const decl of sourceFile.getImportDeclarations()) {
13071
+ const explicitTypeOnly = decl.isTypeOnly();
13072
+ const implicitTypeOnly = !explicitTypeOnly && isImplicitlyTypeOnly(decl, sourceFile);
12982
13073
  specifiers.push({
12983
13074
  specifier: decl.getModuleSpecifierValue(),
12984
13075
  line: decl.getStartLineNumber(),
12985
- typeOnly: decl.isTypeOnly()
13076
+ typeOnly: explicitTypeOnly || implicitTypeOnly
12986
13077
  });
12987
13078
  }
12988
13079
  for (const reqSpec of extractRequireSpecifiers(sourceFile)) {
@@ -13012,24 +13103,8 @@ var require_boundary_checker = __commonJS({
13012
13103
  continue;
13013
13104
  if (!isNestJS && isFeatureModule && isNestJSExemptImport(filePath, resolvedTarget, policy.sharedInfrastructure))
13014
13105
  continue;
13015
- if (typeOnly) {
13016
- violations.push({
13017
- category: "architecture",
13018
- type: "module-boundary-violation",
13019
- ruleId: shared_1.ARCHITECTURE_RULES.MODULE_BOUNDARY,
13020
- severity: "warning",
13021
- source: "deterministic",
13022
- confidence: "high",
13023
- file: filePath,
13024
- line,
13025
- module: sourceModule,
13026
- message: `Cross-module type import: "${sourceModule}" \u2192 "${targetModule}" \u2014 type-only import from '${specifier}' (${filePath})`,
13027
- explanation: `Type-only import across modules does not create a runtime dependency, but indicates coupling. Consider moving the type to a shared contract.`,
13028
- debtPoints: 1,
13029
- gateAction: "warn"
13030
- });
13106
+ if (typeOnly)
13031
13107
  continue;
13032
- }
13033
13108
  violations.push({
13034
13109
  category: "architecture",
13035
13110
  type: "module-boundary-violation",
@@ -16503,6 +16578,31 @@ var require_reliability_detector = __commonJS({
16503
16578
  });
16504
16579
  }
16505
16580
  var LOGGING_PATTERNS = /\b(logger|console\.(?:error|warn)|report|sentry|bugsnag|datadog|newrelic|winston|pino|bunyan)\b/i;
16581
+ var SAFE_FALLBACK_PATTERN = /^return\s+(true|false|null|undefined|void\s+0|0|\[\s*\]|\{\s*\})\s*;?$/;
16582
+ function isSafeFallbackReturn(block) {
16583
+ const statements = block.getStatements();
16584
+ if (statements.length === 0)
16585
+ return false;
16586
+ let returnStatement;
16587
+ let hasOtherSideEffects = false;
16588
+ for (const stmt of statements) {
16589
+ if (ts_morph_1.Node.isReturnStatement(stmt)) {
16590
+ returnStatement = stmt;
16591
+ } else if (ts_morph_1.Node.isVariableStatement(stmt)) {
16592
+ continue;
16593
+ } else {
16594
+ hasOtherSideEffects = true;
16595
+ }
16596
+ }
16597
+ if (!returnStatement || hasOtherSideEffects)
16598
+ return false;
16599
+ const returnText = returnStatement.getText().trim();
16600
+ if (SAFE_FALLBACK_PATTERN.test(returnText))
16601
+ return true;
16602
+ if (statements.length === 1 && returnText.startsWith("return "))
16603
+ return true;
16604
+ return false;
16605
+ }
16506
16606
  function detectMissingErrorLogging(sourceFile, filePath, fns, policy, violations) {
16507
16607
  sourceFile.forEachDescendant((node) => {
16508
16608
  if (!ts_morph_1.Node.isCatchClause(node))
@@ -16521,6 +16621,8 @@ var require_reliability_detector = __commonJS({
16521
16621
  });
16522
16622
  if (hasThrow)
16523
16623
  return;
16624
+ if (isSafeFallbackReturn(block))
16625
+ return;
16524
16626
  const hasIncrement = blockText.includes("++") || blockText.includes("+=");
16525
16627
  const hasPropertyMethodCall = /this\.\w+/.test(blockText);
16526
16628
  const hasAwaitOrDelay = /\bawait\b|\bsetTimeout\b|\bsleep\b|\bdelay\b/i.test(blockText);
@@ -17640,6 +17742,7 @@ var require_dead_code_detector = __commonJS({
17640
17742
  }
17641
17743
  const project = createProject(input);
17642
17744
  const sourceFiles = /* @__PURE__ */ new Map();
17745
+ const { aliases } = parsePathAliases(input);
17643
17746
  for (const sf of project.getSourceFiles()) {
17644
17747
  const filePath = normalizeFilePath(sf.getFilePath());
17645
17748
  sourceFiles.set(filePath, sf);
@@ -17655,7 +17758,7 @@ var require_dead_code_detector = __commonJS({
17655
17758
  }
17656
17759
  const importRefs = /* @__PURE__ */ new Map();
17657
17760
  for (const [filePath, sf] of sourceFiles) {
17658
- const refs = extractImportReferences(sf, filePath, project);
17761
+ const refs = extractImportReferences(sf, filePath, project, aliases);
17659
17762
  if (refs.size > 0) {
17660
17763
  importRefs.set(filePath, refs);
17661
17764
  }
@@ -17697,6 +17800,9 @@ var require_dead_code_detector = __commonJS({
17697
17800
  continue;
17698
17801
  if (cfg.excludeTypes && isTypeExport(exp.type))
17699
17802
  continue;
17803
+ const sf = sourceFiles.get(filePath);
17804
+ if (sf && exp.type === "class" && isNestJSDIRegistered(sf, exp.name))
17805
+ continue;
17700
17806
  unusedExports.push({
17701
17807
  export: exp,
17702
17808
  file: filePath,
@@ -17774,7 +17880,7 @@ var require_dead_code_detector = __commonJS({
17774
17880
  }
17775
17881
  return exports3;
17776
17882
  }
17777
- function extractImportReferences(sourceFile, filePath, project) {
17883
+ function extractImportReferences(sourceFile, filePath, project, aliases = []) {
17778
17884
  const refs = /* @__PURE__ */ new Map();
17779
17885
  function addRef(resolvedFile, name) {
17780
17886
  if (!refs.has(resolvedFile)) {
@@ -17784,7 +17890,7 @@ var require_dead_code_detector = __commonJS({
17784
17890
  }
17785
17891
  for (const importDecl of sourceFile.getImportDeclarations()) {
17786
17892
  const specifier = importDecl.getModuleSpecifierValue();
17787
- const resolved = resolveModuleSpecifier(filePath, specifier, project);
17893
+ const resolved = resolveModuleSpecifier(filePath, specifier, project, aliases);
17788
17894
  if (!resolved)
17789
17895
  continue;
17790
17896
  const defaultImport = importDecl.getDefaultImport();
@@ -17804,7 +17910,7 @@ var require_dead_code_detector = __commonJS({
17804
17910
  const specifier = exportDecl.getModuleSpecifierValue();
17805
17911
  if (!specifier)
17806
17912
  continue;
17807
- const resolved = resolveModuleSpecifier(filePath, specifier, project);
17913
+ const resolved = resolveModuleSpecifier(filePath, specifier, project, aliases);
17808
17914
  if (!resolved)
17809
17915
  continue;
17810
17916
  if (exportDecl.isNamespaceExport()) {
@@ -17825,9 +17931,7 @@ var require_dead_code_detector = __commonJS({
17825
17931
  if (args.length === 0 || !ts_morph_1.Node.isStringLiteral(args[0]))
17826
17932
  return;
17827
17933
  const modulePath = args[0].getLiteralValue();
17828
- if (!modulePath.startsWith(".") && !modulePath.startsWith("/"))
17829
- return;
17830
- const resolved = resolveModuleSpecifier(filePath, modulePath, project);
17934
+ const resolved = resolveModuleSpecifier(filePath, modulePath, project, aliases);
17831
17935
  if (!resolved)
17832
17936
  return;
17833
17937
  const parent = node.getParent();
@@ -17880,6 +17984,83 @@ var require_dead_code_detector = __commonJS({
17880
17984
  function isTypeExport(type) {
17881
17985
  return type === "type" || type === "interface" || type === "enum";
17882
17986
  }
17987
+ function parsePathAliases(input) {
17988
+ let tsconfigContent;
17989
+ const tsconfigFile = input.changedFiles.find((f) => f.status !== "deleted" && /tsconfig(?:\.build)?\.json$/.test(f.path));
17990
+ if (tsconfigFile) {
17991
+ tsconfigContent = tsconfigFile.content;
17992
+ }
17993
+ if (!tsconfigContent && input.projectRoot) {
17994
+ try {
17995
+ const fs9 = require("fs");
17996
+ const tsconfigPath = path9.join(input.projectRoot, "tsconfig.json");
17997
+ if (fs9.existsSync(tsconfigPath)) {
17998
+ tsconfigContent = fs9.readFileSync(tsconfigPath, "utf-8");
17999
+ }
18000
+ } catch {
18001
+ }
18002
+ }
18003
+ if (!tsconfigContent) {
18004
+ return { aliases: [], baseUrl: "." };
18005
+ }
18006
+ try {
18007
+ const stripped = tsconfigContent.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,\s*([}\]])/g, "$1");
18008
+ const tsconfig = JSON.parse(stripped);
18009
+ const compilerOptions = tsconfig.compilerOptions ?? {};
18010
+ const baseUrl = compilerOptions.baseUrl ?? ".";
18011
+ const paths = compilerOptions.paths ?? {};
18012
+ const aliases = [];
18013
+ for (const [pattern, targets] of Object.entries(paths)) {
18014
+ const prefix = pattern.replace(/\/?\*$/, "");
18015
+ const resolvedTargets = targets.map((t) => {
18016
+ const target = t.replace(/\/?\*$/, "");
18017
+ if (baseUrl !== "." && !target.startsWith("/")) {
18018
+ return posixJoinAndNormalize(baseUrl, target);
18019
+ }
18020
+ return target;
18021
+ });
18022
+ aliases.push({ prefix, targets: resolvedTargets });
18023
+ }
18024
+ return { aliases, baseUrl };
18025
+ } catch {
18026
+ return { aliases: [], baseUrl: "." };
18027
+ }
18028
+ }
18029
+ function resolvePathAlias(specifier, aliases, project) {
18030
+ for (const alias of aliases) {
18031
+ if (alias.prefix === "" || specifier === alias.prefix || specifier.startsWith(alias.prefix + "/")) {
18032
+ const remainder = alias.prefix === "" ? specifier : specifier.slice(alias.prefix.length).replace(/^\//, "");
18033
+ for (const target of alias.targets) {
18034
+ const candidate = remainder ? target ? target + "/" + remainder : remainder : target;
18035
+ const resolved = resolveInProject(candidate, project);
18036
+ if (resolved)
18037
+ return resolved;
18038
+ }
18039
+ }
18040
+ }
18041
+ return void 0;
18042
+ }
18043
+ function isNestJSDIRegistered(sourceFile, exportName) {
18044
+ for (const cls of sourceFile.getClasses()) {
18045
+ const className = cls.getName();
18046
+ if (className !== exportName)
18047
+ continue;
18048
+ const decorators = cls.getDecorators();
18049
+ for (const dec of decorators) {
18050
+ const decName = dec.getName();
18051
+ if (decName === "Injectable" || decName === "Controller" || decName === "Guard" || decName === "Resolver" || decName === "Gateway") {
18052
+ return true;
18053
+ }
18054
+ }
18055
+ const extendsClause = cls.getExtends();
18056
+ if (extendsClause) {
18057
+ const extendsText = extendsClause.getText();
18058
+ if (extendsText.includes("PassportStrategy"))
18059
+ return true;
18060
+ }
18061
+ }
18062
+ return false;
18063
+ }
17883
18064
  function createProject(input) {
17884
18065
  const project = new ts_morph_1.Project({
17885
18066
  useInMemoryFileSystem: true,
@@ -17902,13 +18083,16 @@ var require_dead_code_detector = __commonJS({
17902
18083
  const normalized = filePath.replace(/\\/g, "/");
17903
18084
  return normalized.startsWith("/") ? normalized.slice(1) : normalized;
17904
18085
  }
17905
- function resolveModuleSpecifier(sourcePath, specifier, project) {
17906
- if (!specifier.startsWith(".") && !specifier.startsWith("/")) {
17907
- return void 0;
18086
+ function resolveModuleSpecifier(sourcePath, specifier, project, aliases = []) {
18087
+ if (specifier.startsWith(".") || specifier.startsWith("/")) {
18088
+ const sourceDir = posixDirname(sourcePath);
18089
+ const resolved = posixJoinAndNormalize(sourceDir, specifier);
18090
+ return resolveInProject(resolved, project) ?? resolved;
17908
18091
  }
17909
- const sourceDir = posixDirname(sourcePath);
17910
- const resolved = posixJoinAndNormalize(sourceDir, specifier);
17911
- return resolveInProject(resolved, project) ?? resolved;
18092
+ if (aliases.length > 0) {
18093
+ return resolvePathAlias(specifier, aliases, project);
18094
+ }
18095
+ return void 0;
17912
18096
  }
17913
18097
  function resolveInProject(resolved, project) {
17914
18098
  const lookup = "/" + resolved;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "technical-debt-radar",
3
- "version": "1.0.3",
3
+ "version": "1.0.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",