technical-debt-radar 1.6.3 → 1.6.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 +155 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -15200,6 +15200,47 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15200
15200
|
}
|
|
15201
15201
|
return void 0;
|
|
15202
15202
|
}
|
|
15203
|
+
function isServiceMethodCall(callExpr) {
|
|
15204
|
+
const expr = callExpr.getExpression();
|
|
15205
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
|
|
15206
|
+
return false;
|
|
15207
|
+
const obj = expr.getExpression();
|
|
15208
|
+
if (ts_morph_1.Node.isPropertyAccessExpression(obj)) {
|
|
15209
|
+
const propName = obj.getName();
|
|
15210
|
+
if (/Service$/i.test(propName))
|
|
15211
|
+
return true;
|
|
15212
|
+
}
|
|
15213
|
+
if (ts_morph_1.Node.isIdentifier(obj)) {
|
|
15214
|
+
const name = obj.getText();
|
|
15215
|
+
if (/Service$/i.test(name))
|
|
15216
|
+
return true;
|
|
15217
|
+
}
|
|
15218
|
+
return false;
|
|
15219
|
+
}
|
|
15220
|
+
var BOUNDED_QUERY_METHODS = /* @__PURE__ */ new Set([
|
|
15221
|
+
"findOne",
|
|
15222
|
+
"findById",
|
|
15223
|
+
"findByIdAndUpdate",
|
|
15224
|
+
"findByIdAndDelete",
|
|
15225
|
+
"findOneAndUpdate",
|
|
15226
|
+
"findOneAndDelete",
|
|
15227
|
+
"findOneAndReplace",
|
|
15228
|
+
"findOneAndRemove",
|
|
15229
|
+
"findByPk",
|
|
15230
|
+
// Sequelize
|
|
15231
|
+
"findUnique",
|
|
15232
|
+
"findUniqueOrThrow",
|
|
15233
|
+
"findFirst",
|
|
15234
|
+
"findFirstOrThrow",
|
|
15235
|
+
// Prisma
|
|
15236
|
+
"first",
|
|
15237
|
+
// Knex
|
|
15238
|
+
"get",
|
|
15239
|
+
// DynamoDB
|
|
15240
|
+
"getOne",
|
|
15241
|
+
"getRawOne"
|
|
15242
|
+
// TypeORM QueryBuilder
|
|
15243
|
+
]);
|
|
15203
15244
|
var FIND_MANY_METHODS = /* @__PURE__ */ new Set(["findMany", "find", "getMany", "findAndCount"]);
|
|
15204
15245
|
function detectUnboundedFindMany(sourceFile, filePath, fns, policy, violations) {
|
|
15205
15246
|
sourceFile.forEachDescendant((node) => {
|
|
@@ -15209,6 +15250,10 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15209
15250
|
if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
|
|
15210
15251
|
return;
|
|
15211
15252
|
const methodName = expr.getName();
|
|
15253
|
+
if (BOUNDED_QUERY_METHODS.has(methodName))
|
|
15254
|
+
return;
|
|
15255
|
+
if (isServiceMethodCall(node))
|
|
15256
|
+
return;
|
|
15212
15257
|
if (TYPEORM_QB_TERMINAL.has(methodName) && methodName !== "getCount" && methodName !== "getOne" && methodName !== "getRawOne") {
|
|
15213
15258
|
const chain = traceQueryBuilderChain(node);
|
|
15214
15259
|
if (chain.hasTake)
|
|
@@ -15287,6 +15332,13 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15287
15332
|
entity = extractTypeORMEntity(node);
|
|
15288
15333
|
if (!entity)
|
|
15289
15334
|
entity = extractSequelizeEntity(node);
|
|
15335
|
+
if (methodName === "find") {
|
|
15336
|
+
const typeormEntity = extractTypeORMEntity(node);
|
|
15337
|
+
const seqEntity = extractSequelizeEntity(node);
|
|
15338
|
+
if (!typeormEntity && !seqEntity)
|
|
15339
|
+
return;
|
|
15340
|
+
entity = typeormEntity ?? seqEntity;
|
|
15341
|
+
}
|
|
15290
15342
|
const vol = entity ? resolveVolume(entity, policy) : void 0;
|
|
15291
15343
|
const fn = getEnclosingFn(node, fns);
|
|
15292
15344
|
pushIfNotNull(violations, makeViolation(shared_1.PERFORMANCE_RULES.UNBOUNDED_FIND_MANY, filePath, node.getStartLineNumber(), `${methodName}() without take/limit${vol ? ` on '${entity}' (${vol.size} volume)` : ""} \u2014 may fetch entire table`, policy, vol, fn?.name, "Add take: N or limit to bound the result set"));
|
|
@@ -15300,6 +15352,10 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15300
15352
|
if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
|
|
15301
15353
|
return;
|
|
15302
15354
|
const methodName = expr.getName();
|
|
15355
|
+
if (BOUNDED_QUERY_METHODS.has(methodName))
|
|
15356
|
+
return;
|
|
15357
|
+
if (isServiceMethodCall(node))
|
|
15358
|
+
return;
|
|
15303
15359
|
if (TYPEORM_QB_TERMINAL.has(methodName) && methodName !== "getCount" && methodName !== "getOne" && methodName !== "getRawOne") {
|
|
15304
15360
|
const chain = traceQueryBuilderChain(node);
|
|
15305
15361
|
if (chain.hasWhere)
|
|
@@ -15388,6 +15444,8 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15388
15444
|
entity = extractTypeORMEntity(node);
|
|
15389
15445
|
if (!entity)
|
|
15390
15446
|
entity = extractSequelizeEntity(node);
|
|
15447
|
+
if (methodName === "find")
|
|
15448
|
+
return;
|
|
15391
15449
|
const vol = entity ? resolveVolume(entity, policy) : void 0;
|
|
15392
15450
|
const hasCursorPagination = argsContainKey(node, "cursor");
|
|
15393
15451
|
const hasTakePagination = argsContainKey(node, "take") || argsContainKey(node, "limit");
|
|
@@ -15876,6 +15934,9 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15876
15934
|
const bodyText = fn.node.getText();
|
|
15877
15935
|
if (!/\.(findMany|find|getMany|findAll|from)\s*\(/.test(bodyText) && !/knex\s*\(/.test(bodyText))
|
|
15878
15936
|
continue;
|
|
15937
|
+
const findCallMatches = [...bodyText.matchAll(/(\w+)\.(findMany|find|getMany|findAll|from)\s*\(/g)];
|
|
15938
|
+
if (findCallMatches.length > 0 && findCallMatches.every((m) => /Service$/i.test(m[1])))
|
|
15939
|
+
continue;
|
|
15879
15940
|
if (/page|skip|offset|cursor|take|limit|per_page|pageSize/i.test(bodyText))
|
|
15880
15941
|
continue;
|
|
15881
15942
|
const entityMatch = bodyText.match(/(\w+)\.(findMany|find|getMany|findAll)\s*\(/);
|
|
@@ -16618,9 +16679,11 @@ var require_reliability_detector = __commonJS({
|
|
|
16618
16679
|
continue;
|
|
16619
16680
|
let unhandledAwaitCount = 0;
|
|
16620
16681
|
let totalAwaitCount = 0;
|
|
16621
|
-
fn.node.forEachDescendant((child) => {
|
|
16622
|
-
if (child !== fn.node && (ts_morph_1.Node.isFunctionDeclaration(child) || ts_morph_1.Node.isMethodDeclaration(child) || ts_morph_1.Node.isArrowFunction(child) || ts_morph_1.Node.isFunctionExpression(child)))
|
|
16682
|
+
fn.node.forEachDescendant((child, traversal) => {
|
|
16683
|
+
if (child !== fn.node && (ts_morph_1.Node.isFunctionDeclaration(child) || ts_morph_1.Node.isMethodDeclaration(child) || ts_morph_1.Node.isArrowFunction(child) || ts_morph_1.Node.isFunctionExpression(child))) {
|
|
16684
|
+
traversal.skip();
|
|
16623
16685
|
return;
|
|
16686
|
+
}
|
|
16624
16687
|
if (!ts_morph_1.Node.isAwaitExpression(child))
|
|
16625
16688
|
return;
|
|
16626
16689
|
totalAwaitCount++;
|
|
@@ -17022,7 +17085,7 @@ var require_reliability_detector = __commonJS({
|
|
|
17022
17085
|
});
|
|
17023
17086
|
}
|
|
17024
17087
|
var ROUTE_DECORATORS = /* @__PURE__ */ new Set(["Get", "Post", "Put", "Delete", "Patch", "All", "Head", "Options"]);
|
|
17025
|
-
var GUARD_TODO_PATTERN =
|
|
17088
|
+
var GUARD_TODO_PATTERN = /\b(TODO|FIXME|HACK|XXX)\b/i;
|
|
17026
17089
|
function detectUnguardedRouteTodo(sourceFile, filePath, fns, policy, violations) {
|
|
17027
17090
|
if (!filePath.includes("controller"))
|
|
17028
17091
|
return;
|
|
@@ -18710,6 +18773,12 @@ var require_dead_code_detector = __commonJS({
|
|
|
18710
18773
|
if (aliasResolved)
|
|
18711
18774
|
return aliasResolved;
|
|
18712
18775
|
}
|
|
18776
|
+
const aliasStripped = specifier.replace(/^@\//, "src/").replace(/^~\//, "src/");
|
|
18777
|
+
if (aliasStripped !== specifier) {
|
|
18778
|
+
const aliasResolved = resolveInProject(aliasStripped, project);
|
|
18779
|
+
if (aliasResolved)
|
|
18780
|
+
return aliasResolved;
|
|
18781
|
+
}
|
|
18713
18782
|
const directResolved = resolveInProject(specifier, project);
|
|
18714
18783
|
if (directResolved)
|
|
18715
18784
|
return directResolved;
|
|
@@ -19184,7 +19253,7 @@ var require_orchestrator = __commonJS({
|
|
|
19184
19253
|
}
|
|
19185
19254
|
const complexityViolations = complexityDeltasToViolations(complexityDeltas, filteredInput);
|
|
19186
19255
|
const largeFileViolations = detectLargeFiles(filteredInput);
|
|
19187
|
-
const
|
|
19256
|
+
const rawViolations = [
|
|
19188
19257
|
...boundaryViolations,
|
|
19189
19258
|
...circularViolations,
|
|
19190
19259
|
...runtimeViolations,
|
|
@@ -19198,6 +19267,7 @@ var require_orchestrator = __commonJS({
|
|
|
19198
19267
|
...coverageDeltaResult?.violations ?? [],
|
|
19199
19268
|
...crossFileAnalysis?.violations ?? []
|
|
19200
19269
|
];
|
|
19270
|
+
const violations = deduplicateViolations(rawViolations);
|
|
19201
19271
|
const suspectFunctions = selectSuspectFunctions(violations, complexityDeltas, filteredInput);
|
|
19202
19272
|
const metrics = computeMetrics(filteredInput, importGraph, violations, complexityDeltas, suspectFunctions, startMs, duplicationResult, missingTestsResult, deadCodeResult, crossFileAnalysis, coverageDeltaResult);
|
|
19203
19273
|
return {
|
|
@@ -19209,6 +19279,87 @@ var require_orchestrator = __commonJS({
|
|
|
19209
19279
|
crossFileAnalysis
|
|
19210
19280
|
};
|
|
19211
19281
|
}
|
|
19282
|
+
var PERF_DEDUP_RULES = /* @__PURE__ */ new Set([
|
|
19283
|
+
"unbounded-find-many",
|
|
19284
|
+
"find-many-no-where",
|
|
19285
|
+
"missing-pagination-endpoint"
|
|
19286
|
+
]);
|
|
19287
|
+
function deduplicateViolations(violations) {
|
|
19288
|
+
let result = violations;
|
|
19289
|
+
result = deduplicateControllerService(result);
|
|
19290
|
+
result = deduplicateNestedScopes(result);
|
|
19291
|
+
return result;
|
|
19292
|
+
}
|
|
19293
|
+
function deduplicateControllerService(violations) {
|
|
19294
|
+
const perfViolations = violations.filter((v) => PERF_DEDUP_RULES.has(v.ruleId));
|
|
19295
|
+
if (perfViolations.length === 0)
|
|
19296
|
+
return violations;
|
|
19297
|
+
const serviceViolationKeys = /* @__PURE__ */ new Set();
|
|
19298
|
+
for (const v of perfViolations) {
|
|
19299
|
+
if (/\.service\./i.test(v.file)) {
|
|
19300
|
+
serviceViolationKeys.add(v.ruleId);
|
|
19301
|
+
}
|
|
19302
|
+
}
|
|
19303
|
+
const serviceByRuleAndDir = /* @__PURE__ */ new Map();
|
|
19304
|
+
for (const v of perfViolations) {
|
|
19305
|
+
if (/\.service\./i.test(v.file)) {
|
|
19306
|
+
const dir = v.file.substring(0, v.file.lastIndexOf("/") + 1);
|
|
19307
|
+
const key = `${v.ruleId}::${dir}`;
|
|
19308
|
+
if (!serviceByRuleAndDir.has(key))
|
|
19309
|
+
serviceByRuleAndDir.set(key, /* @__PURE__ */ new Set());
|
|
19310
|
+
serviceByRuleAndDir.get(key).add(v.function ?? "");
|
|
19311
|
+
}
|
|
19312
|
+
}
|
|
19313
|
+
return violations.filter((v) => {
|
|
19314
|
+
if (!PERF_DEDUP_RULES.has(v.ruleId))
|
|
19315
|
+
return true;
|
|
19316
|
+
if (!/\.controller\./i.test(v.file))
|
|
19317
|
+
return true;
|
|
19318
|
+
const dir = v.file.substring(0, v.file.lastIndexOf("/") + 1);
|
|
19319
|
+
const key = `${v.ruleId}::${dir}`;
|
|
19320
|
+
if (serviceByRuleAndDir.has(key))
|
|
19321
|
+
return false;
|
|
19322
|
+
if (serviceViolationKeys.has(v.ruleId) && /\.controller\./i.test(v.file))
|
|
19323
|
+
return false;
|
|
19324
|
+
return true;
|
|
19325
|
+
});
|
|
19326
|
+
}
|
|
19327
|
+
var NESTED_SCOPE_DEDUP_RULES = /* @__PURE__ */ new Set([
|
|
19328
|
+
"missing-try-catch",
|
|
19329
|
+
"unhandled-promise-rejection"
|
|
19330
|
+
]);
|
|
19331
|
+
function deduplicateNestedScopes(violations) {
|
|
19332
|
+
const groups = /* @__PURE__ */ new Map();
|
|
19333
|
+
for (const v of violations) {
|
|
19334
|
+
if (!NESTED_SCOPE_DEDUP_RULES.has(v.ruleId))
|
|
19335
|
+
continue;
|
|
19336
|
+
const key = `${v.file}::${v.ruleId}`;
|
|
19337
|
+
if (!groups.has(key))
|
|
19338
|
+
groups.set(key, []);
|
|
19339
|
+
groups.get(key).push(v);
|
|
19340
|
+
}
|
|
19341
|
+
const toRemove = /* @__PURE__ */ new Set();
|
|
19342
|
+
for (const [, group] of groups) {
|
|
19343
|
+
if (group.length < 2)
|
|
19344
|
+
continue;
|
|
19345
|
+
const byFunction = /* @__PURE__ */ new Map();
|
|
19346
|
+
for (const v of group) {
|
|
19347
|
+
const fn = v.function ?? "";
|
|
19348
|
+
if (!byFunction.has(fn))
|
|
19349
|
+
byFunction.set(fn, []);
|
|
19350
|
+
byFunction.get(fn).push(v);
|
|
19351
|
+
}
|
|
19352
|
+
for (const [, fnGroup] of byFunction) {
|
|
19353
|
+
if (fnGroup.length < 2)
|
|
19354
|
+
continue;
|
|
19355
|
+
fnGroup.sort((a, b) => a.line - b.line);
|
|
19356
|
+
for (let i = 1; i < fnGroup.length; i++) {
|
|
19357
|
+
toRemove.add(fnGroup[i]);
|
|
19358
|
+
}
|
|
19359
|
+
}
|
|
19360
|
+
}
|
|
19361
|
+
return violations.filter((v) => !toRemove.has(v));
|
|
19362
|
+
}
|
|
19212
19363
|
function detectLargeFiles(input) {
|
|
19213
19364
|
const threshold = shared_1.DEFAULT_LARGE_FILE_THRESHOLD;
|
|
19214
19365
|
const violations = [];
|