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.
- package/dist/index.js +182 -5
- 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
|
|
19220
|
-
const
|
|
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;
|