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.
- package/dist/index.js +215 -31
- 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:
|
|
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
|
-
|
|
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 (
|
|
17907
|
-
|
|
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
|
-
|
|
17910
|
-
|
|
17911
|
-
|
|
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;
|