technical-debt-radar 1.5.0 → 1.6.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 +234 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -77,7 +77,8 @@ var require_constants = __commonJS({
|
|
|
77
77
|
LARGE_JSON_STRINGIFY: "large-json-stringify",
|
|
78
78
|
CPU_HEAVY_LOOP_IN_HANDLER: "cpu-heavy-loop-in-handler",
|
|
79
79
|
UNBOUNDED_ARRAY_OPERATION: "unbounded-array-operation",
|
|
80
|
-
DYNAMIC_BUFFER_ALLOC: "dynamic-buffer-alloc"
|
|
80
|
+
DYNAMIC_BUFFER_ALLOC: "dynamic-buffer-alloc",
|
|
81
|
+
UNBOUNDED_PARALLEL_CALLS: "unbounded-parallel-external-calls"
|
|
81
82
|
};
|
|
82
83
|
exports2.RELIABILITY_RULES = {
|
|
83
84
|
UNHANDLED_PROMISE_REJECTION: "unhandled-promise-rejection",
|
|
@@ -91,7 +92,8 @@ var require_constants = __commonJS({
|
|
|
91
92
|
MISSING_DTO_VALIDATION: "missing-dto-validation",
|
|
92
93
|
UNGUARDED_ROUTE_TODO: "unguarded-route-todo",
|
|
93
94
|
ERROR_LOGGED_AS_INFO: "error-logged-as-info",
|
|
94
|
-
CATCH_SWALLOWS_CONTEXT: "catch-block-swallows-error-context"
|
|
95
|
+
CATCH_SWALLOWS_CONTEXT: "catch-block-swallows-error-context",
|
|
96
|
+
PROMISE_ALL_NO_ISOLATION: "promise-all-no-error-isolation"
|
|
95
97
|
};
|
|
96
98
|
exports2.PERFORMANCE_RULES = {
|
|
97
99
|
UNBOUNDED_FIND_MANY: "unbounded-find-many",
|
|
@@ -111,6 +113,7 @@ var require_constants = __commonJS({
|
|
|
111
113
|
CODE_DUPLICATION: "code-duplication",
|
|
112
114
|
MISSING_TEST_FILE: "missing-test-file",
|
|
113
115
|
UNUSED_EXPORT: "unused-export",
|
|
116
|
+
UNUSED_PUBLIC_METHOD: "unused-public-method",
|
|
114
117
|
DEBUG_CONSOLE_LOG: "debug-console-log-in-production",
|
|
115
118
|
LOW_TEST_COVERAGE: "low-test-coverage",
|
|
116
119
|
LOW_BRANCH_COVERAGE: "low-branch-coverage",
|
|
@@ -12772,6 +12775,10 @@ var require_import_graph = __commonJS({
|
|
|
12772
12775
|
const resolved = posixJoinAndNormalize(sourceDir, specifier);
|
|
12773
12776
|
return resolveInProject(resolved, project) ?? resolved;
|
|
12774
12777
|
}
|
|
12778
|
+
const aliasStripped = specifier.replace(/^@\//, "src/").replace(/^~\//, "src/");
|
|
12779
|
+
const aliasResolved = resolveInProject(aliasStripped, project);
|
|
12780
|
+
if (aliasResolved)
|
|
12781
|
+
return aliasResolved;
|
|
12775
12782
|
const directResolved = resolveInProject(specifier, project);
|
|
12776
12783
|
if (directResolved)
|
|
12777
12784
|
return directResolved;
|
|
@@ -16185,6 +16192,7 @@ var require_reliability_detector = __commonJS({
|
|
|
16185
16192
|
async function detectReliabilityIssues(input, policy) {
|
|
16186
16193
|
const violations = [];
|
|
16187
16194
|
const project = new ts_morph_1.Project({ useInMemoryFileSystem: true });
|
|
16195
|
+
const schemaModuleMap = /* @__PURE__ */ new Map();
|
|
16188
16196
|
for (const file of input.changedFiles) {
|
|
16189
16197
|
if (file.status === "deleted")
|
|
16190
16198
|
continue;
|
|
@@ -16205,6 +16213,10 @@ var require_reliability_detector = __commonJS({
|
|
|
16205
16213
|
detectUnguardedRouteTodo(sourceFile, file.path, fns, policy, violations);
|
|
16206
16214
|
detectCatchSwallowsContext(sourceFile, file.path, fns, policy, violations);
|
|
16207
16215
|
detectDebugConsoleLog(sourceFile, file.path, fns, policy, violations);
|
|
16216
|
+
detectProviderOutsideHomeModule(sourceFile, file.path, input.changedFiles, policy, violations);
|
|
16217
|
+
detectSchemaMultiModule(sourceFile, file.path, schemaModuleMap, policy, violations);
|
|
16218
|
+
detectPromiseAllNoIsolation(sourceFile, file.path, fns, policy, violations);
|
|
16219
|
+
detectUnboundedParallelCalls(sourceFile, file.path, fns, policy, violations);
|
|
16208
16220
|
project.removeSourceFile(sourceFile);
|
|
16209
16221
|
}
|
|
16210
16222
|
return applyExceptions(violations, policy);
|
|
@@ -17010,6 +17022,226 @@ var require_reliability_detector = __commonJS({
|
|
|
17010
17022
|
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"));
|
|
17011
17023
|
});
|
|
17012
17024
|
}
|
|
17025
|
+
function detectPromiseAllNoIsolation(sourceFile, filePath, fns, policy, violations) {
|
|
17026
|
+
sourceFile.forEachDescendant((node) => {
|
|
17027
|
+
if (!ts_morph_1.Node.isCallExpression(node))
|
|
17028
|
+
return;
|
|
17029
|
+
const expr = node.getExpression();
|
|
17030
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
|
|
17031
|
+
return;
|
|
17032
|
+
if (expr.getName() !== "all")
|
|
17033
|
+
return;
|
|
17034
|
+
const obj = expr.getExpression();
|
|
17035
|
+
if (!ts_morph_1.Node.isIdentifier(obj) || obj.getText() !== "Promise")
|
|
17036
|
+
return;
|
|
17037
|
+
const args = node.getArguments();
|
|
17038
|
+
if (args.length === 0)
|
|
17039
|
+
return;
|
|
17040
|
+
const arrayArg = args[0];
|
|
17041
|
+
if (!ts_morph_1.Node.isArrayLiteralExpression(arrayArg))
|
|
17042
|
+
return;
|
|
17043
|
+
const elements = arrayArg.getElements();
|
|
17044
|
+
if (elements.length < 2)
|
|
17045
|
+
return;
|
|
17046
|
+
let hasExternalCalls = false;
|
|
17047
|
+
let hasCatchHandlers = false;
|
|
17048
|
+
for (const el of elements) {
|
|
17049
|
+
const elText = el.getText();
|
|
17050
|
+
if (/fetch|axios|http|this\.\w+Service|this\.\w+Model/.test(elText)) {
|
|
17051
|
+
hasExternalCalls = true;
|
|
17052
|
+
}
|
|
17053
|
+
if (/\.catch\(/.test(elText)) {
|
|
17054
|
+
hasCatchHandlers = true;
|
|
17055
|
+
}
|
|
17056
|
+
}
|
|
17057
|
+
if (!hasExternalCalls)
|
|
17058
|
+
return;
|
|
17059
|
+
if (hasCatchHandlers)
|
|
17060
|
+
return;
|
|
17061
|
+
const fn = getEnclosingFn(node, fns);
|
|
17062
|
+
violations.push(makeViolation(shared_1.RELIABILITY_RULES.PROMISE_ALL_NO_ISOLATION, filePath, node.getStartLineNumber(), `Promise.all() with ${elements.length} I/O calls \u2014 one failure rejects all with no error isolation`, policy, fn?.name, "Add .catch() to individual promises or use Promise.allSettled() for partial failure tolerance"));
|
|
17063
|
+
});
|
|
17064
|
+
}
|
|
17065
|
+
function detectUnboundedParallelCalls(sourceFile, filePath, fns, policy, violations) {
|
|
17066
|
+
sourceFile.forEachDescendant((node) => {
|
|
17067
|
+
if (!ts_morph_1.Node.isCallExpression(node))
|
|
17068
|
+
return;
|
|
17069
|
+
const expr = node.getExpression();
|
|
17070
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
|
|
17071
|
+
return;
|
|
17072
|
+
if (expr.getName() !== "all")
|
|
17073
|
+
return;
|
|
17074
|
+
const obj = expr.getExpression();
|
|
17075
|
+
if (!ts_morph_1.Node.isIdentifier(obj) || obj.getText() !== "Promise")
|
|
17076
|
+
return;
|
|
17077
|
+
const args = node.getArguments();
|
|
17078
|
+
if (args.length === 0)
|
|
17079
|
+
return;
|
|
17080
|
+
const firstArg = args[0];
|
|
17081
|
+
if (!ts_morph_1.Node.isCallExpression(firstArg))
|
|
17082
|
+
return;
|
|
17083
|
+
const mapExpr = firstArg.getExpression();
|
|
17084
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(mapExpr))
|
|
17085
|
+
return;
|
|
17086
|
+
if (mapExpr.getName() !== "map")
|
|
17087
|
+
return;
|
|
17088
|
+
const mapArgs = firstArg.getArguments();
|
|
17089
|
+
if (mapArgs.length === 0)
|
|
17090
|
+
return;
|
|
17091
|
+
const callbackText = mapArgs[0].getText();
|
|
17092
|
+
if (!/fetch|axios|http|this\.\w+Service|this\.\w+Model|\.upload|\.send|\.post|\.get|\.put|\.delete/i.test(callbackText)) {
|
|
17093
|
+
return;
|
|
17094
|
+
}
|
|
17095
|
+
const fn = getEnclosingFn(node, fns);
|
|
17096
|
+
violations.push({
|
|
17097
|
+
category: "runtime_risk",
|
|
17098
|
+
type: shared_1.RUNTIME_RISK_RULES.UNBOUNDED_PARALLEL_CALLS,
|
|
17099
|
+
ruleId: shared_1.RUNTIME_RISK_RULES.UNBOUNDED_PARALLEL_CALLS,
|
|
17100
|
+
severity: "warning",
|
|
17101
|
+
source: "deterministic",
|
|
17102
|
+
confidence: "high",
|
|
17103
|
+
file: filePath,
|
|
17104
|
+
line: node.getStartLineNumber(),
|
|
17105
|
+
function: fn?.name,
|
|
17106
|
+
message: "Promise.all(array.map()) with unbounded I/O calls \u2014 may overwhelm external service or exhaust connections",
|
|
17107
|
+
suggestion: "Use p-limit or batch processing to limit concurrency: const limit = pLimit(5); await Promise.all(items.map(item => limit(() => fn(item))))",
|
|
17108
|
+
debtPoints: 5,
|
|
17109
|
+
gateAction: "warn"
|
|
17110
|
+
});
|
|
17111
|
+
});
|
|
17112
|
+
}
|
|
17113
|
+
function detectProviderOutsideHomeModule(sourceFile, filePath, allFiles, policy, violations) {
|
|
17114
|
+
if (!filePath.endsWith(".module.ts") && !filePath.endsWith(".module.js"))
|
|
17115
|
+
return;
|
|
17116
|
+
sourceFile.forEachDescendant((node) => {
|
|
17117
|
+
if (!ts_morph_1.Node.isDecorator(node) || node.getName() !== "Module")
|
|
17118
|
+
return;
|
|
17119
|
+
const args = node.getArguments?.();
|
|
17120
|
+
if (!args || args.length === 0)
|
|
17121
|
+
return;
|
|
17122
|
+
const callExpr = node.getCallExpression?.();
|
|
17123
|
+
if (!callExpr)
|
|
17124
|
+
return;
|
|
17125
|
+
const moduleArgs = callExpr.getArguments();
|
|
17126
|
+
if (moduleArgs.length === 0 || !ts_morph_1.Node.isObjectLiteralExpression(moduleArgs[0]))
|
|
17127
|
+
return;
|
|
17128
|
+
const objLit = moduleArgs[0];
|
|
17129
|
+
const providersProp = objLit.getProperty("providers");
|
|
17130
|
+
if (!providersProp || !ts_morph_1.Node.isPropertyAssignment(providersProp))
|
|
17131
|
+
return;
|
|
17132
|
+
const providersInit = providersProp.getInitializer();
|
|
17133
|
+
if (!providersInit || !ts_morph_1.Node.isArrayLiteralExpression(providersInit))
|
|
17134
|
+
return;
|
|
17135
|
+
const moduleFolder = filePath.replace(/\/[^/]+$/, "");
|
|
17136
|
+
for (const element of providersInit.getElements()) {
|
|
17137
|
+
if (!ts_morph_1.Node.isIdentifier(element))
|
|
17138
|
+
continue;
|
|
17139
|
+
const providerName = element.getText();
|
|
17140
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
17141
|
+
const namedImports = importDecl.getNamedImports();
|
|
17142
|
+
const matchingImport = namedImports.find((n) => n.getName() === providerName);
|
|
17143
|
+
if (!matchingImport)
|
|
17144
|
+
continue;
|
|
17145
|
+
const specifier = importDecl.getModuleSpecifierValue();
|
|
17146
|
+
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
17147
|
+
const resolvedPath = specifier.replace(/^\.\//, moduleFolder + "/").replace(/^\.\.\//, moduleFolder + "/../");
|
|
17148
|
+
if (!resolvedPath.startsWith(moduleFolder) && !specifier.startsWith("./")) {
|
|
17149
|
+
violations.push({
|
|
17150
|
+
category: "architecture",
|
|
17151
|
+
type: shared_1.ARCHITECTURE_RULES.PROVIDER_OUTSIDE_HOME,
|
|
17152
|
+
ruleId: shared_1.ARCHITECTURE_RULES.PROVIDER_OUTSIDE_HOME,
|
|
17153
|
+
severity: "warning",
|
|
17154
|
+
source: "deterministic",
|
|
17155
|
+
confidence: "high",
|
|
17156
|
+
file: filePath,
|
|
17157
|
+
line: element.getStartLineNumber(),
|
|
17158
|
+
message: `Provider '${providerName}' is imported from outside this module's folder \u2014 consider moving it to its home module or a shared module`,
|
|
17159
|
+
suggestion: `Move ${providerName} to this module's folder or register it in the module where it's defined`,
|
|
17160
|
+
debtPoints: 3,
|
|
17161
|
+
gateAction: "warn"
|
|
17162
|
+
});
|
|
17163
|
+
}
|
|
17164
|
+
}
|
|
17165
|
+
}
|
|
17166
|
+
}
|
|
17167
|
+
});
|
|
17168
|
+
}
|
|
17169
|
+
function detectSchemaMultiModule(sourceFile, filePath, schemaMap, policy, violations) {
|
|
17170
|
+
if (!filePath.endsWith(".module.ts") && !filePath.endsWith(".module.js"))
|
|
17171
|
+
return;
|
|
17172
|
+
sourceFile.forEachDescendant((node) => {
|
|
17173
|
+
if (!ts_morph_1.Node.isCallExpression(node))
|
|
17174
|
+
return;
|
|
17175
|
+
const expr = node.getExpression();
|
|
17176
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
|
|
17177
|
+
return;
|
|
17178
|
+
const methodName = expr.getName();
|
|
17179
|
+
if (methodName !== "forFeature")
|
|
17180
|
+
return;
|
|
17181
|
+
const obj = expr.getExpression();
|
|
17182
|
+
const objText = ts_morph_1.Node.isIdentifier(obj) ? obj.getText() : "";
|
|
17183
|
+
if (objText !== "MongooseModule" && objText !== "TypeOrmModule")
|
|
17184
|
+
return;
|
|
17185
|
+
const args = node.getArguments();
|
|
17186
|
+
if (args.length === 0)
|
|
17187
|
+
return;
|
|
17188
|
+
const arrayArg = args[0];
|
|
17189
|
+
if (!ts_morph_1.Node.isArrayLiteralExpression(arrayArg))
|
|
17190
|
+
return;
|
|
17191
|
+
for (const element of arrayArg.getElements()) {
|
|
17192
|
+
if (ts_morph_1.Node.isObjectLiteralExpression(element)) {
|
|
17193
|
+
const nameProp = element.getProperty("name");
|
|
17194
|
+
if (nameProp && ts_morph_1.Node.isPropertyAssignment(nameProp)) {
|
|
17195
|
+
const init = nameProp.getInitializer();
|
|
17196
|
+
if (init) {
|
|
17197
|
+
const schemaName = init.getText().replace(/\.name$/, "");
|
|
17198
|
+
const existing = schemaMap.get(schemaName) ?? [];
|
|
17199
|
+
existing.push(filePath);
|
|
17200
|
+
schemaMap.set(schemaName, existing);
|
|
17201
|
+
if (existing.length > 1) {
|
|
17202
|
+
violations.push({
|
|
17203
|
+
category: "architecture",
|
|
17204
|
+
type: shared_1.ARCHITECTURE_RULES.SCHEMA_MULTI_MODULE,
|
|
17205
|
+
ruleId: shared_1.ARCHITECTURE_RULES.SCHEMA_MULTI_MODULE,
|
|
17206
|
+
severity: "warning",
|
|
17207
|
+
source: "deterministic",
|
|
17208
|
+
confidence: "high",
|
|
17209
|
+
file: filePath,
|
|
17210
|
+
line: element.getStartLineNumber(),
|
|
17211
|
+
message: `Schema '${schemaName}' registered in multiple modules: ${existing.join(", ")}`,
|
|
17212
|
+
suggestion: "Register the schema in one module and import that module where needed",
|
|
17213
|
+
debtPoints: 3,
|
|
17214
|
+
gateAction: "warn"
|
|
17215
|
+
});
|
|
17216
|
+
}
|
|
17217
|
+
}
|
|
17218
|
+
}
|
|
17219
|
+
}
|
|
17220
|
+
if (ts_morph_1.Node.isIdentifier(element)) {
|
|
17221
|
+
const entityName = element.getText();
|
|
17222
|
+
const existing = schemaMap.get(entityName) ?? [];
|
|
17223
|
+
existing.push(filePath);
|
|
17224
|
+
schemaMap.set(entityName, existing);
|
|
17225
|
+
if (existing.length > 1) {
|
|
17226
|
+
violations.push({
|
|
17227
|
+
category: "architecture",
|
|
17228
|
+
type: shared_1.ARCHITECTURE_RULES.SCHEMA_MULTI_MODULE,
|
|
17229
|
+
ruleId: shared_1.ARCHITECTURE_RULES.SCHEMA_MULTI_MODULE,
|
|
17230
|
+
severity: "warning",
|
|
17231
|
+
source: "deterministic",
|
|
17232
|
+
confidence: "high",
|
|
17233
|
+
file: filePath,
|
|
17234
|
+
line: element.getStartLineNumber(),
|
|
17235
|
+
message: `Entity '${entityName}' registered in multiple modules: ${existing.join(", ")}`,
|
|
17236
|
+
suggestion: "Register the entity in one module and import that module where needed",
|
|
17237
|
+
debtPoints: 3,
|
|
17238
|
+
gateAction: "warn"
|
|
17239
|
+
});
|
|
17240
|
+
}
|
|
17241
|
+
}
|
|
17242
|
+
}
|
|
17243
|
+
});
|
|
17244
|
+
}
|
|
17013
17245
|
var DEBUG_LOG_PATTERN = /\b(RESULT|DEBUG|TEST|TODO|FIXME|HACK|XXX)\b/i;
|
|
17014
17246
|
var EXCLUDE_LOG_FILES = /\.(spec|test)\.(ts|js)|seed|migration|main\.(ts|js)$/;
|
|
17015
17247
|
function detectDebugConsoleLog(sourceFile, filePath, fns, policy, violations) {
|