technical-debt-radar 1.3.3 → 1.5.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 +129 -24
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -89,7 +89,9 @@ var require_constants = __commonJS({
|
|
|
89
89
|
TRANSACTION_NO_TIMEOUT: "transaction-no-timeout",
|
|
90
90
|
MISSING_NULL_GUARD: "missing-null-guard",
|
|
91
91
|
MISSING_DTO_VALIDATION: "missing-dto-validation",
|
|
92
|
-
UNGUARDED_ROUTE_TODO: "unguarded-route-todo"
|
|
92
|
+
UNGUARDED_ROUTE_TODO: "unguarded-route-todo",
|
|
93
|
+
ERROR_LOGGED_AS_INFO: "error-logged-as-info",
|
|
94
|
+
CATCH_SWALLOWS_CONTEXT: "catch-block-swallows-error-context"
|
|
93
95
|
};
|
|
94
96
|
exports2.PERFORMANCE_RULES = {
|
|
95
97
|
UNBOUNDED_FIND_MANY: "unbounded-find-many",
|
|
@@ -109,6 +111,7 @@ var require_constants = __commonJS({
|
|
|
109
111
|
CODE_DUPLICATION: "code-duplication",
|
|
110
112
|
MISSING_TEST_FILE: "missing-test-file",
|
|
111
113
|
UNUSED_EXPORT: "unused-export",
|
|
114
|
+
DEBUG_CONSOLE_LOG: "debug-console-log-in-production",
|
|
112
115
|
LOW_TEST_COVERAGE: "low-test-coverage",
|
|
113
116
|
LOW_BRANCH_COVERAGE: "low-branch-coverage",
|
|
114
117
|
COVERAGE_DROP: "coverage-drop"
|
|
@@ -117,7 +120,9 @@ var require_constants = __commonJS({
|
|
|
117
120
|
LAYER_VIOLATION: "layer-boundary-violation",
|
|
118
121
|
CIRCULAR_DEPENDENCY: "circular-dependency",
|
|
119
122
|
MODULE_BOUNDARY: "module-boundary-violation",
|
|
120
|
-
FORBIDDEN_FRAMEWORK: "forbidden-framework-in-layer"
|
|
123
|
+
FORBIDDEN_FRAMEWORK: "forbidden-framework-in-layer",
|
|
124
|
+
PROVIDER_OUTSIDE_HOME: "provider-declared-outside-home-module",
|
|
125
|
+
SCHEMA_MULTI_MODULE: "schema-registered-in-multiple-modules"
|
|
121
126
|
};
|
|
122
127
|
exports2.CROSS_FILE_RULES = {
|
|
123
128
|
INDIRECT_SYNC_FS: "indirect-sync-fs",
|
|
@@ -12762,12 +12767,15 @@ var require_import_graph = __commonJS({
|
|
|
12762
12767
|
return specifiers;
|
|
12763
12768
|
}
|
|
12764
12769
|
function resolveModuleSpecifier(sourcePath, specifier, project) {
|
|
12765
|
-
if (
|
|
12766
|
-
|
|
12770
|
+
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
12771
|
+
const sourceDir = posixDirname(sourcePath);
|
|
12772
|
+
const resolved = posixJoinAndNormalize(sourceDir, specifier);
|
|
12773
|
+
return resolveInProject(resolved, project) ?? resolved;
|
|
12767
12774
|
}
|
|
12768
|
-
const
|
|
12769
|
-
|
|
12770
|
-
|
|
12775
|
+
const directResolved = resolveInProject(specifier, project);
|
|
12776
|
+
if (directResolved)
|
|
12777
|
+
return directResolved;
|
|
12778
|
+
return void 0;
|
|
12771
12779
|
}
|
|
12772
12780
|
function resolveInProject(resolved, project) {
|
|
12773
12781
|
const lookup = "/" + resolved;
|
|
@@ -12786,6 +12794,16 @@ var require_import_graph = __commonJS({
|
|
|
12786
12794
|
return indexPath;
|
|
12787
12795
|
}
|
|
12788
12796
|
}
|
|
12797
|
+
const suffix = "/" + resolved;
|
|
12798
|
+
for (const sf of project.getSourceFiles()) {
|
|
12799
|
+
const fp = sf.getFilePath();
|
|
12800
|
+
if (fp.endsWith(suffix))
|
|
12801
|
+
return normalizeFilePath(fp);
|
|
12802
|
+
for (const ext of extensions) {
|
|
12803
|
+
if (fp.endsWith(suffix + ext))
|
|
12804
|
+
return normalizeFilePath(fp);
|
|
12805
|
+
}
|
|
12806
|
+
}
|
|
12789
12807
|
return void 0;
|
|
12790
12808
|
}
|
|
12791
12809
|
function buildEdge(source, target, importType, policy) {
|
|
@@ -12876,12 +12894,22 @@ var require_boundary_checker = __commonJS({
|
|
|
12876
12894
|
return /nestjs/i.test(policy.stack.framework);
|
|
12877
12895
|
}
|
|
12878
12896
|
var suffixPattern = (suffix) => new RegExp(`\\.${suffix}(\\.(ts|tsx|js|jsx))?$`);
|
|
12879
|
-
var NESTJS_SHARED_INFRA_FOLDERS = ["guards", "decorators", "interceptors", "pipes", "filters", "middleware", "shared"];
|
|
12897
|
+
var NESTJS_SHARED_INFRA_FOLDERS = ["guards", "decorators", "interceptors", "pipes", "filters", "middleware", "shared", "strategies", "common", "config"];
|
|
12880
12898
|
function isNestJSExemptImport(sourcePath, targetPath, customSharedFolders = []) {
|
|
12881
12899
|
const srcBase = sourcePath.replace(/\\/g, "/");
|
|
12882
12900
|
const tgtBase = targetPath.replace(/\\/g, "/");
|
|
12883
12901
|
if (suffixPattern("module").test(srcBase))
|
|
12884
12902
|
return true;
|
|
12903
|
+
if (suffixPattern("strategy").test(srcBase))
|
|
12904
|
+
return true;
|
|
12905
|
+
if (suffixPattern("guard").test(srcBase))
|
|
12906
|
+
return true;
|
|
12907
|
+
if (suffixPattern("interceptor").test(srcBase))
|
|
12908
|
+
return true;
|
|
12909
|
+
if (suffixPattern("pipe").test(srcBase))
|
|
12910
|
+
return true;
|
|
12911
|
+
if (suffixPattern("filter").test(srcBase))
|
|
12912
|
+
return true;
|
|
12885
12913
|
if (suffixPattern("module").test(tgtBase))
|
|
12886
12914
|
return true;
|
|
12887
12915
|
if (suffixPattern("entity").test(tgtBase))
|
|
@@ -12898,6 +12926,8 @@ var require_boundary_checker = __commonJS({
|
|
|
12898
12926
|
return true;
|
|
12899
12927
|
if (suffixPattern("filter").test(tgtBase))
|
|
12900
12928
|
return true;
|
|
12929
|
+
if (suffixPattern("strategy").test(tgtBase))
|
|
12930
|
+
return true;
|
|
12901
12931
|
if (/\/dto\//.test(tgtBase) || suffixPattern("dto").test(tgtBase))
|
|
12902
12932
|
return true;
|
|
12903
12933
|
for (const folder of NESTJS_SHARED_INFRA_FOLDERS) {
|
|
@@ -16173,6 +16203,8 @@ var require_reliability_detector = __commonJS({
|
|
|
16173
16203
|
detectMissingNullGuard(sourceFile, file.path, fns, policy, violations);
|
|
16174
16204
|
detectMissingDtoValidation(sourceFile, file.path, fns, policy, violations);
|
|
16175
16205
|
detectUnguardedRouteTodo(sourceFile, file.path, fns, policy, violations);
|
|
16206
|
+
detectCatchSwallowsContext(sourceFile, file.path, fns, policy, violations);
|
|
16207
|
+
detectDebugConsoleLog(sourceFile, file.path, fns, policy, violations);
|
|
16176
16208
|
project.removeSourceFile(sourceFile);
|
|
16177
16209
|
}
|
|
16178
16210
|
return applyExceptions(violations, policy);
|
|
@@ -16238,18 +16270,12 @@ var require_reliability_detector = __commonJS({
|
|
|
16238
16270
|
}
|
|
16239
16271
|
return false;
|
|
16240
16272
|
}
|
|
16241
|
-
function
|
|
16273
|
+
function isNestJSController(classNode) {
|
|
16242
16274
|
return classNode.getDecorators().some((d) => {
|
|
16243
16275
|
const name = d.getName();
|
|
16244
|
-
return name === "
|
|
16276
|
+
return name === "Controller" || name === "Resolver";
|
|
16245
16277
|
});
|
|
16246
16278
|
}
|
|
16247
|
-
function isInsideNestJSInjectable(node) {
|
|
16248
|
-
const classDecl = node.getFirstAncestorByKind(ts_morph_1.SyntaxKind.ClassDeclaration);
|
|
16249
|
-
if (!classDecl)
|
|
16250
|
-
return false;
|
|
16251
|
-
return isNestJSInjectable(classDecl);
|
|
16252
|
-
}
|
|
16253
16279
|
function fileImportsNestJS(sourceFile) {
|
|
16254
16280
|
for (const decl of sourceFile.getImportDeclarations()) {
|
|
16255
16281
|
const spec = decl.getModuleSpecifierValue();
|
|
@@ -16258,10 +16284,13 @@ var require_reliability_detector = __commonJS({
|
|
|
16258
16284
|
}
|
|
16259
16285
|
return false;
|
|
16260
16286
|
}
|
|
16261
|
-
function
|
|
16287
|
+
function isNestJSControllerMethod(node, sourceFile) {
|
|
16262
16288
|
if (!fileImportsNestJS(sourceFile))
|
|
16263
16289
|
return false;
|
|
16264
|
-
|
|
16290
|
+
const classDecl = node.getFirstAncestorByKind(ts_morph_1.SyntaxKind.ClassDeclaration);
|
|
16291
|
+
if (!classDecl)
|
|
16292
|
+
return false;
|
|
16293
|
+
if (!isNestJSController(classDecl))
|
|
16265
16294
|
return false;
|
|
16266
16295
|
const bgDecorators = /* @__PURE__ */ new Set(["Cron", "Process", "Interval", "Timeout"]);
|
|
16267
16296
|
if (ts_morph_1.Node.isMethodDeclaration(node)) {
|
|
@@ -16269,10 +16298,8 @@ var require_reliability_detector = __commonJS({
|
|
|
16269
16298
|
return false;
|
|
16270
16299
|
}
|
|
16271
16300
|
const fn = node.getFirstAncestorByKind(ts_morph_1.SyntaxKind.MethodDeclaration);
|
|
16272
|
-
if (fn)
|
|
16273
|
-
|
|
16274
|
-
return false;
|
|
16275
|
-
}
|
|
16301
|
+
if (fn && fn.getDecorators().some((d) => bgDecorators.has(d.getName())))
|
|
16302
|
+
return false;
|
|
16276
16303
|
return true;
|
|
16277
16304
|
}
|
|
16278
16305
|
function getSeverity(ruleId, policy) {
|
|
@@ -16357,7 +16384,7 @@ var require_reliability_detector = __commonJS({
|
|
|
16357
16384
|
return;
|
|
16358
16385
|
if (isBootstrapFunction(fn))
|
|
16359
16386
|
return;
|
|
16360
|
-
if (
|
|
16387
|
+
if (isNestJSControllerMethod(node, sourceFile))
|
|
16361
16388
|
return;
|
|
16362
16389
|
if (functionHasTryCatch(fn.node))
|
|
16363
16390
|
return;
|
|
@@ -16545,7 +16572,7 @@ var require_reliability_detector = __commonJS({
|
|
|
16545
16572
|
continue;
|
|
16546
16573
|
if (isBootstrapFunction(fn))
|
|
16547
16574
|
continue;
|
|
16548
|
-
if (
|
|
16575
|
+
if (isNestJSControllerMethod(fn.node, _sourceFile))
|
|
16549
16576
|
continue;
|
|
16550
16577
|
let hasAwait = false;
|
|
16551
16578
|
fn.node.forEachDescendant((child) => {
|
|
@@ -16786,6 +16813,12 @@ var require_reliability_detector = __commonJS({
|
|
|
16786
16813
|
const hasContinue = /\bcontinue\b/.test(blockText);
|
|
16787
16814
|
if (hasIncrement || hasPropertyMethodCall || hasAwaitOrDelay || hasContinue)
|
|
16788
16815
|
return;
|
|
16816
|
+
const hasConsoleLog = /\bconsole\.log\b/.test(blockText);
|
|
16817
|
+
if (hasConsoleLog) {
|
|
16818
|
+
const fn2 = getEnclosingFn(node, fns);
|
|
16819
|
+
violations.push(makeViolation(shared_1.RELIABILITY_RULES.ERROR_LOGGED_AS_INFO, filePath, node.getStartLineNumber(), "Error logged with console.log instead of console.error \u2014 errors should route to stderr", policy, fn2?.name, "Replace console.log with console.error or use a proper logger (winston, pino)"));
|
|
16820
|
+
return;
|
|
16821
|
+
}
|
|
16789
16822
|
const fn = getEnclosingFn(node, fns);
|
|
16790
16823
|
violations.push(makeViolation(shared_1.RELIABILITY_RULES.MISSING_ERROR_LOGGING, filePath, node.getStartLineNumber(), "catch block has no error logging or reporting", policy, fn?.name, "Add logger.error(err) or re-throw the error"));
|
|
16791
16824
|
});
|
|
@@ -16977,6 +17010,78 @@ var require_reliability_detector = __commonJS({
|
|
|
16977
17010
|
violations.push(makeViolation(shared_1.RELIABILITY_RULES.UNGUARDED_ROUTE_TODO, filePath, node.getStartLineNumber(), `Route handler '${node.getName?.() ?? "anonymous"}' has TODO/FIXME comment and no @UseGuards() \u2014 unprotected endpoint`, policy, fn?.name, "Add @UseGuards(JwtAuthGuard) or appropriate guard \u2014 this route is currently unprotected"));
|
|
16978
17011
|
});
|
|
16979
17012
|
}
|
|
17013
|
+
var DEBUG_LOG_PATTERN = /\b(RESULT|DEBUG|TEST|TODO|FIXME|HACK|XXX)\b/i;
|
|
17014
|
+
var EXCLUDE_LOG_FILES = /\.(spec|test)\.(ts|js)|seed|migration|main\.(ts|js)$/;
|
|
17015
|
+
function detectDebugConsoleLog(sourceFile, filePath, fns, policy, violations) {
|
|
17016
|
+
if (EXCLUDE_LOG_FILES.test(filePath))
|
|
17017
|
+
return;
|
|
17018
|
+
sourceFile.forEachDescendant((node) => {
|
|
17019
|
+
if (!ts_morph_1.Node.isCallExpression(node))
|
|
17020
|
+
return;
|
|
17021
|
+
const expr = node.getExpression();
|
|
17022
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
|
|
17023
|
+
return;
|
|
17024
|
+
const obj = expr.getExpression();
|
|
17025
|
+
if (!ts_morph_1.Node.isIdentifier(obj) || obj.getText() !== "console")
|
|
17026
|
+
return;
|
|
17027
|
+
if (expr.getName() !== "log")
|
|
17028
|
+
return;
|
|
17029
|
+
const args = node.getArguments();
|
|
17030
|
+
const argsText = args.map((a) => a.getText()).join(" ");
|
|
17031
|
+
if (!DEBUG_LOG_PATTERN.test(argsText))
|
|
17032
|
+
return;
|
|
17033
|
+
const catchClause = node.getFirstAncestorByKind(ts_morph_1.SyntaxKind.CatchClause);
|
|
17034
|
+
if (catchClause)
|
|
17035
|
+
return;
|
|
17036
|
+
const fn = getEnclosingFn(node, fns);
|
|
17037
|
+
violations.push({
|
|
17038
|
+
category: "maintainability",
|
|
17039
|
+
type: shared_1.MAINTAINABILITY_RULES.DEBUG_CONSOLE_LOG,
|
|
17040
|
+
ruleId: shared_1.MAINTAINABILITY_RULES.DEBUG_CONSOLE_LOG,
|
|
17041
|
+
severity: "warning",
|
|
17042
|
+
source: "deterministic",
|
|
17043
|
+
confidence: "high",
|
|
17044
|
+
file: filePath,
|
|
17045
|
+
line: node.getStartLineNumber(),
|
|
17046
|
+
function: fn?.name,
|
|
17047
|
+
message: `Debug console.log() in production code: ${argsText.substring(0, 60)}`,
|
|
17048
|
+
suggestion: "Remove debug logging or replace with a proper logger (winston, pino)",
|
|
17049
|
+
debtPoints: 1,
|
|
17050
|
+
gateAction: "warn"
|
|
17051
|
+
});
|
|
17052
|
+
});
|
|
17053
|
+
}
|
|
17054
|
+
function detectCatchSwallowsContext(sourceFile, filePath, fns, policy, violations) {
|
|
17055
|
+
sourceFile.forEachDescendant((node) => {
|
|
17056
|
+
if (!ts_morph_1.Node.isCatchClause(node))
|
|
17057
|
+
return;
|
|
17058
|
+
const block = node.getBlock();
|
|
17059
|
+
const errParam = node.getVariableDeclaration()?.getName();
|
|
17060
|
+
if (!errParam)
|
|
17061
|
+
return;
|
|
17062
|
+
let throwsNew = false;
|
|
17063
|
+
let referencesOriginal = false;
|
|
17064
|
+
block.forEachDescendant((child) => {
|
|
17065
|
+
if (ts_morph_1.Node.isThrowStatement(child)) {
|
|
17066
|
+
throwsNew = true;
|
|
17067
|
+
const thrownText = child.getExpression()?.getText() ?? "";
|
|
17068
|
+
if (thrownText.includes(errParam)) {
|
|
17069
|
+
referencesOriginal = true;
|
|
17070
|
+
}
|
|
17071
|
+
}
|
|
17072
|
+
});
|
|
17073
|
+
if (!throwsNew)
|
|
17074
|
+
return;
|
|
17075
|
+
if (referencesOriginal)
|
|
17076
|
+
return;
|
|
17077
|
+
const blockText = block.getText();
|
|
17078
|
+
if (/\b(logger|console\.(?:error|warn))\b/i.test(blockText) && new RegExp(`\\b${escapeRegex(errParam)}\\b`).test(blockText)) {
|
|
17079
|
+
return;
|
|
17080
|
+
}
|
|
17081
|
+
const fn = getEnclosingFn(node, fns);
|
|
17082
|
+
violations.push(makeViolation(shared_1.RELIABILITY_RULES.CATCH_SWALLOWS_CONTEXT, filePath, node.getStartLineNumber(), `Catch block throws new error without including original '${errParam}' \u2014 error context lost`, policy, fn?.name, `Include the original error: throw new Error('message', { cause: ${errParam} }) or throw new HttpException(message, status, { cause: ${errParam} })`));
|
|
17083
|
+
});
|
|
17084
|
+
}
|
|
16980
17085
|
function escapeRegex(str) {
|
|
16981
17086
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
16982
17087
|
}
|