technical-debt-radar 1.6.5 → 1.7.1
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 +461 -32
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -12888,6 +12888,7 @@ var require_boundary_checker = __commonJS({
|
|
|
12888
12888
|
"use strict";
|
|
12889
12889
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
12890
12890
|
exports2.checkBoundaries = checkBoundaries;
|
|
12891
|
+
exports2._resetPathAliasCache = _resetPathAliasCache;
|
|
12891
12892
|
var shared_1 = require_dist();
|
|
12892
12893
|
var minimatch_1 = require_commonjs3();
|
|
12893
12894
|
var ts_morph_1 = require("ts-morph");
|
|
@@ -13191,10 +13192,15 @@ var require_boundary_checker = __commonJS({
|
|
|
13191
13192
|
specifiers.push(reqSpec);
|
|
13192
13193
|
}
|
|
13193
13194
|
for (const { specifier, line, typeOnly } of specifiers) {
|
|
13194
|
-
|
|
13195
|
+
let resolvedTarget;
|
|
13196
|
+
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
13197
|
+
const sourceDir = filePath.replace(/\/[^/]+$/, "");
|
|
13198
|
+
resolvedTarget = resolveRelativePath(sourceDir, specifier);
|
|
13199
|
+
} else {
|
|
13200
|
+
resolvedTarget = resolvePathAliasForBoundary(specifier, input);
|
|
13201
|
+
}
|
|
13202
|
+
if (!resolvedTarget)
|
|
13195
13203
|
continue;
|
|
13196
|
-
const sourceDir = filePath.replace(/\/[^/]+$/, "");
|
|
13197
|
-
const resolvedTarget = resolveRelativePath(sourceDir, specifier);
|
|
13198
13204
|
const targetModule = findModuleForFile(resolvedTarget, policy);
|
|
13199
13205
|
if (!targetModule || targetModule === sourceModule)
|
|
13200
13206
|
continue;
|
|
@@ -13379,6 +13385,82 @@ var require_boundary_checker = __commonJS({
|
|
|
13379
13385
|
const normalized = normalizeForMatching(file);
|
|
13380
13386
|
return exceptions.some((exc) => exc.rule === ruleId && ((0, minimatch_1.minimatch)(file, exc.file) || (0, minimatch_1.minimatch)(normalized, exc.file)));
|
|
13381
13387
|
}
|
|
13388
|
+
var _cachedAliases;
|
|
13389
|
+
function parsePathAliasesFromProject(input) {
|
|
13390
|
+
if (_cachedAliases)
|
|
13391
|
+
return _cachedAliases;
|
|
13392
|
+
let tsconfigContent;
|
|
13393
|
+
const tsconfigFile = input.changedFiles.find((f) => f.status !== "deleted" && /tsconfig(?:\.build|\.app)?\.json$/.test(f.path));
|
|
13394
|
+
if (tsconfigFile) {
|
|
13395
|
+
tsconfigContent = tsconfigFile.content;
|
|
13396
|
+
}
|
|
13397
|
+
const root = input.projectRoot;
|
|
13398
|
+
if (!tsconfigContent && root) {
|
|
13399
|
+
try {
|
|
13400
|
+
const fs9 = require("fs");
|
|
13401
|
+
const path9 = require("path");
|
|
13402
|
+
for (const name of ["tsconfig.json", "tsconfig.app.json", "tsconfig.build.json"]) {
|
|
13403
|
+
const tsconfigPath = path9.join(root, name);
|
|
13404
|
+
if (fs9.existsSync(tsconfigPath)) {
|
|
13405
|
+
tsconfigContent = fs9.readFileSync(tsconfigPath, "utf-8");
|
|
13406
|
+
break;
|
|
13407
|
+
}
|
|
13408
|
+
}
|
|
13409
|
+
} catch {
|
|
13410
|
+
}
|
|
13411
|
+
}
|
|
13412
|
+
if (!tsconfigContent) {
|
|
13413
|
+
_cachedAliases = [];
|
|
13414
|
+
return _cachedAliases;
|
|
13415
|
+
}
|
|
13416
|
+
try {
|
|
13417
|
+
const stripped = tsconfigContent.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,\s*([}\]])/g, "$1");
|
|
13418
|
+
const tsconfig = JSON.parse(stripped);
|
|
13419
|
+
const compilerOptions = tsconfig.compilerOptions ?? {};
|
|
13420
|
+
const rawBaseUrl = (compilerOptions.baseUrl ?? ".").replace(/\/+$/, "");
|
|
13421
|
+
const baseUrl = rawBaseUrl === "" ? "." : rawBaseUrl;
|
|
13422
|
+
const paths = compilerOptions.paths ?? {};
|
|
13423
|
+
const aliases = [];
|
|
13424
|
+
for (const [pattern, targets] of Object.entries(paths)) {
|
|
13425
|
+
const prefix = pattern.replace(/\/?\*$/, "");
|
|
13426
|
+
const resolvedTargets = targets.map((t) => {
|
|
13427
|
+
const target = t.replace(/\/?\*$/, "");
|
|
13428
|
+
if (baseUrl !== "." && !target.startsWith("/")) {
|
|
13429
|
+
return (baseUrl + "/" + target).replace(/\/+/g, "/");
|
|
13430
|
+
}
|
|
13431
|
+
return target;
|
|
13432
|
+
});
|
|
13433
|
+
aliases.push({ prefix, targets: resolvedTargets });
|
|
13434
|
+
}
|
|
13435
|
+
_cachedAliases = aliases;
|
|
13436
|
+
return aliases;
|
|
13437
|
+
} catch {
|
|
13438
|
+
_cachedAliases = [];
|
|
13439
|
+
return _cachedAliases;
|
|
13440
|
+
}
|
|
13441
|
+
}
|
|
13442
|
+
function resolvePathAliasForBoundary(specifier, input) {
|
|
13443
|
+
const aliases = parsePathAliasesFromProject(input);
|
|
13444
|
+
for (const alias of aliases) {
|
|
13445
|
+
if (alias.prefix === "" || specifier === alias.prefix || specifier.startsWith(alias.prefix + "/")) {
|
|
13446
|
+
const remainder = alias.prefix === "" ? specifier : specifier.slice(alias.prefix.length).replace(/^\//, "");
|
|
13447
|
+
for (const target of alias.targets) {
|
|
13448
|
+
const candidate = remainder ? target ? target + "/" + remainder : remainder : target;
|
|
13449
|
+
if (candidate.startsWith("src/") || candidate.startsWith("lib/") || candidate.startsWith("app/")) {
|
|
13450
|
+
return candidate;
|
|
13451
|
+
}
|
|
13452
|
+
}
|
|
13453
|
+
}
|
|
13454
|
+
}
|
|
13455
|
+
if (specifier.startsWith("@/"))
|
|
13456
|
+
return "src/" + specifier.slice(2);
|
|
13457
|
+
if (specifier.startsWith("~/"))
|
|
13458
|
+
return "src/" + specifier.slice(2);
|
|
13459
|
+
return void 0;
|
|
13460
|
+
}
|
|
13461
|
+
function _resetPathAliasCache() {
|
|
13462
|
+
_cachedAliases = void 0;
|
|
13463
|
+
}
|
|
13382
13464
|
function normalizeForMatching(filePath) {
|
|
13383
13465
|
let normalized = filePath.replace(/\\/g, "/");
|
|
13384
13466
|
if (normalized.startsWith("/")) {
|
|
@@ -16250,6 +16332,7 @@ var require_reliability_detector = __commonJS({
|
|
|
16250
16332
|
exports2.detectReliabilityIssues = detectReliabilityIssues;
|
|
16251
16333
|
var shared_1 = require_dist();
|
|
16252
16334
|
var ts_morph_1 = require("ts-morph");
|
|
16335
|
+
var minimatch_1 = require_commonjs3();
|
|
16253
16336
|
async function detectReliabilityIssues(input, policy) {
|
|
16254
16337
|
const violations = [];
|
|
16255
16338
|
const project = new ts_morph_1.Project({ useInMemoryFileSystem: true });
|
|
@@ -16280,6 +16363,10 @@ var require_reliability_detector = __commonJS({
|
|
|
16280
16363
|
detectUnboundedParallelCalls(sourceFile, file.path, fns, policy, violations);
|
|
16281
16364
|
project.removeSourceFile(sourceFile);
|
|
16282
16365
|
}
|
|
16366
|
+
if (/mongoose/i.test(policy.stack.orm)) {
|
|
16367
|
+
const schemaViolations = detectQueryNonexistentField(input);
|
|
16368
|
+
violations.push(...schemaViolations);
|
|
16369
|
+
}
|
|
16283
16370
|
return applyExceptions(deduplicateOverlapping(violations), policy);
|
|
16284
16371
|
}
|
|
16285
16372
|
function deduplicateOverlapping(violations) {
|
|
@@ -17200,9 +17287,44 @@ var require_reliability_detector = __commonJS({
|
|
|
17200
17287
|
});
|
|
17201
17288
|
});
|
|
17202
17289
|
}
|
|
17290
|
+
function stripToSrcRoot(filePath) {
|
|
17291
|
+
const markers = ["src/", "apps/", "lib/", "packages/"];
|
|
17292
|
+
for (const marker of markers) {
|
|
17293
|
+
const idx = filePath.indexOf(marker);
|
|
17294
|
+
if (idx >= 0)
|
|
17295
|
+
return filePath.slice(idx);
|
|
17296
|
+
}
|
|
17297
|
+
return filePath;
|
|
17298
|
+
}
|
|
17299
|
+
function resolveRelativeImport(sourceDir, specifier) {
|
|
17300
|
+
const parts = (sourceDir + "/" + specifier).split("/");
|
|
17301
|
+
const resolved = [];
|
|
17302
|
+
for (const part of parts) {
|
|
17303
|
+
if (part === "." || part === "")
|
|
17304
|
+
continue;
|
|
17305
|
+
if (part === "..")
|
|
17306
|
+
resolved.pop();
|
|
17307
|
+
else
|
|
17308
|
+
resolved.push(part);
|
|
17309
|
+
}
|
|
17310
|
+
return resolved.join("/");
|
|
17311
|
+
}
|
|
17312
|
+
function findModuleName(filePath, policy) {
|
|
17313
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
17314
|
+
const stripped = normalized.startsWith("/") ? normalized.slice(1) : normalized;
|
|
17315
|
+
for (const mod of policy.modules) {
|
|
17316
|
+
if ((0, minimatch_1.minimatch)(stripped, mod.pathPattern) || (0, minimatch_1.minimatch)(normalized, mod.pathPattern)) {
|
|
17317
|
+
return mod.name;
|
|
17318
|
+
}
|
|
17319
|
+
}
|
|
17320
|
+
const match = stripped.match(/^src\/([^/]+)/);
|
|
17321
|
+
return match ? match[1] : void 0;
|
|
17322
|
+
}
|
|
17203
17323
|
function detectProviderOutsideHomeModule(sourceFile, filePath, allFiles, policy, violations) {
|
|
17204
17324
|
if (!filePath.endsWith(".module.ts") && !filePath.endsWith(".module.js"))
|
|
17205
17325
|
return;
|
|
17326
|
+
if (!/nestjs/i.test(policy.stack.framework))
|
|
17327
|
+
return;
|
|
17206
17328
|
sourceFile.forEachDescendant((node) => {
|
|
17207
17329
|
if (!ts_morph_1.Node.isDecorator(node) || node.getName() !== "Module")
|
|
17208
17330
|
return;
|
|
@@ -17233,24 +17355,40 @@ var require_reliability_detector = __commonJS({
|
|
|
17233
17355
|
if (!matchingImport)
|
|
17234
17356
|
continue;
|
|
17235
17357
|
const specifier = importDecl.getModuleSpecifierValue();
|
|
17358
|
+
let resolvedPath;
|
|
17236
17359
|
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
17237
|
-
|
|
17238
|
-
|
|
17239
|
-
|
|
17240
|
-
|
|
17241
|
-
|
|
17242
|
-
|
|
17243
|
-
|
|
17244
|
-
|
|
17245
|
-
|
|
17246
|
-
|
|
17247
|
-
|
|
17248
|
-
|
|
17249
|
-
|
|
17250
|
-
|
|
17251
|
-
|
|
17252
|
-
|
|
17253
|
-
}
|
|
17360
|
+
resolvedPath = resolveRelativeImport(moduleFolder, specifier);
|
|
17361
|
+
} else if (specifier.startsWith("@/")) {
|
|
17362
|
+
resolvedPath = "src/" + specifier.slice(2);
|
|
17363
|
+
} else if (specifier.startsWith("~/")) {
|
|
17364
|
+
resolvedPath = "src/" + specifier.slice(2);
|
|
17365
|
+
}
|
|
17366
|
+
if (!resolvedPath)
|
|
17367
|
+
continue;
|
|
17368
|
+
const normalizedModule = stripToSrcRoot(moduleFolder.replace(/\\/g, "/"));
|
|
17369
|
+
const normalizedResolved = stripToSrcRoot(resolvedPath.replace(/\\/g, "/"));
|
|
17370
|
+
const isFromOutside = !normalizedResolved.startsWith(normalizedModule + "/") && normalizedResolved !== normalizedModule;
|
|
17371
|
+
const sharedFolders = ["shared", "common", "utils", "config", "guards", "interceptors", "decorators", "pipes", "filters", "strategies"];
|
|
17372
|
+
const isShared = sharedFolders.some((f) => normalizedResolved.includes("/" + f + "/"));
|
|
17373
|
+
if (isFromOutside && !isShared) {
|
|
17374
|
+
const sourceModuleName = findModuleName(normalizedResolved, policy);
|
|
17375
|
+
const targetModuleName = findModuleName(filePath, policy);
|
|
17376
|
+
const moduleInfo = sourceModuleName ? ` (${sourceModuleName} module)` : "";
|
|
17377
|
+
const targetInfo = targetModuleName ? ` (${targetModuleName} module)` : "";
|
|
17378
|
+
violations.push({
|
|
17379
|
+
category: "architecture",
|
|
17380
|
+
type: shared_1.ARCHITECTURE_RULES.PROVIDER_OUTSIDE_HOME,
|
|
17381
|
+
ruleId: shared_1.ARCHITECTURE_RULES.PROVIDER_OUTSIDE_HOME,
|
|
17382
|
+
severity: "warning",
|
|
17383
|
+
source: "deterministic",
|
|
17384
|
+
confidence: "high",
|
|
17385
|
+
file: filePath,
|
|
17386
|
+
line: element.getStartLineNumber(),
|
|
17387
|
+
message: `Provider '${providerName}'${moduleInfo} is registered outside its home module${targetInfo} \u2014 move it to its home module's providers and export it`,
|
|
17388
|
+
suggestion: `Move ${providerName} to its home module's providers array and export it, then import that module here`,
|
|
17389
|
+
debtPoints: 3,
|
|
17390
|
+
gateAction: "warn"
|
|
17391
|
+
});
|
|
17254
17392
|
}
|
|
17255
17393
|
}
|
|
17256
17394
|
}
|
|
@@ -17407,6 +17545,163 @@ var require_reliability_detector = __commonJS({
|
|
|
17407
17545
|
function escapeRegex(str) {
|
|
17408
17546
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
17409
17547
|
}
|
|
17548
|
+
var MONGOOSE_QUERY_METHODS = /* @__PURE__ */ new Set([
|
|
17549
|
+
"find",
|
|
17550
|
+
"findOne",
|
|
17551
|
+
"findById",
|
|
17552
|
+
"findOneAndUpdate",
|
|
17553
|
+
"findOneAndDelete",
|
|
17554
|
+
"findOneAndReplace",
|
|
17555
|
+
"findOneAndRemove",
|
|
17556
|
+
"findByIdAndUpdate",
|
|
17557
|
+
"findByIdAndDelete",
|
|
17558
|
+
"updateOne",
|
|
17559
|
+
"updateMany",
|
|
17560
|
+
"deleteOne",
|
|
17561
|
+
"deleteMany",
|
|
17562
|
+
"countDocuments",
|
|
17563
|
+
"exists",
|
|
17564
|
+
"distinct"
|
|
17565
|
+
]);
|
|
17566
|
+
var BUILTIN_FIELDS = /* @__PURE__ */ new Set(["_id", "__v", "id", "createdAt", "updatedAt"]);
|
|
17567
|
+
function detectQueryNonexistentField(input) {
|
|
17568
|
+
const violations = [];
|
|
17569
|
+
const project = new ts_morph_1.Project({ useInMemoryFileSystem: true });
|
|
17570
|
+
const schemaFieldMap = /* @__PURE__ */ new Map();
|
|
17571
|
+
for (const file of input.changedFiles) {
|
|
17572
|
+
if (file.status === "deleted")
|
|
17573
|
+
continue;
|
|
17574
|
+
if (!/\.schema\.(ts|js)$/.test(file.path) && !/schemas?\//.test(file.path))
|
|
17575
|
+
continue;
|
|
17576
|
+
if (!/\.(ts|tsx|js|jsx)$/.test(file.path))
|
|
17577
|
+
continue;
|
|
17578
|
+
const sf = project.createSourceFile(file.path, file.content);
|
|
17579
|
+
for (const cls of sf.getClasses()) {
|
|
17580
|
+
const schemaDecorator = cls.getDecorators().find((d) => d.getName() === "Schema");
|
|
17581
|
+
if (!schemaDecorator)
|
|
17582
|
+
continue;
|
|
17583
|
+
const schemaName = cls.getName();
|
|
17584
|
+
if (!schemaName)
|
|
17585
|
+
continue;
|
|
17586
|
+
const fields = new Set(BUILTIN_FIELDS);
|
|
17587
|
+
for (const prop of cls.getProperties()) {
|
|
17588
|
+
if (prop.getDecorators().some((d) => d.getName() === "Prop")) {
|
|
17589
|
+
fields.add(prop.getName());
|
|
17590
|
+
}
|
|
17591
|
+
}
|
|
17592
|
+
const schemaArgs = schemaDecorator.getCallExpression?.()?.getArguments();
|
|
17593
|
+
if (schemaArgs && schemaArgs.length > 0) {
|
|
17594
|
+
const argText = schemaArgs[0].getText();
|
|
17595
|
+
if (/timestamps\s*:\s*true/.test(argText)) {
|
|
17596
|
+
fields.add("createdAt");
|
|
17597
|
+
fields.add("updatedAt");
|
|
17598
|
+
}
|
|
17599
|
+
}
|
|
17600
|
+
schemaFieldMap.set(schemaName, fields);
|
|
17601
|
+
}
|
|
17602
|
+
project.removeSourceFile(sf);
|
|
17603
|
+
}
|
|
17604
|
+
if (schemaFieldMap.size === 0)
|
|
17605
|
+
return violations;
|
|
17606
|
+
for (const file of input.changedFiles) {
|
|
17607
|
+
if (file.status === "deleted")
|
|
17608
|
+
continue;
|
|
17609
|
+
if (!/\.(ts|tsx|js|jsx)$/.test(file.path))
|
|
17610
|
+
continue;
|
|
17611
|
+
if (/\.schema\.(ts|js)$/.test(file.path))
|
|
17612
|
+
continue;
|
|
17613
|
+
const sf = project.createSourceFile(file.path, file.content);
|
|
17614
|
+
const modelToSchema = /* @__PURE__ */ new Map();
|
|
17615
|
+
for (const cls of sf.getClasses()) {
|
|
17616
|
+
const ctors = cls.getConstructors();
|
|
17617
|
+
for (const ctor of ctors) {
|
|
17618
|
+
for (const param of ctor.getParameters()) {
|
|
17619
|
+
const injectModel = param.getDecorators().find((d) => d.getName() === "InjectModel");
|
|
17620
|
+
if (!injectModel)
|
|
17621
|
+
continue;
|
|
17622
|
+
const callExpr = injectModel.getCallExpression?.();
|
|
17623
|
+
if (!callExpr)
|
|
17624
|
+
continue;
|
|
17625
|
+
const args = callExpr.getArguments();
|
|
17626
|
+
if (args.length === 0)
|
|
17627
|
+
continue;
|
|
17628
|
+
let schemaName = args[0].getText();
|
|
17629
|
+
schemaName = schemaName.replace(/\.name$/, "").replace(/['"]/g, "");
|
|
17630
|
+
const paramName = param.getName();
|
|
17631
|
+
modelToSchema.set(paramName, schemaName);
|
|
17632
|
+
}
|
|
17633
|
+
}
|
|
17634
|
+
}
|
|
17635
|
+
if (modelToSchema.size === 0) {
|
|
17636
|
+
project.removeSourceFile(sf);
|
|
17637
|
+
continue;
|
|
17638
|
+
}
|
|
17639
|
+
sf.forEachDescendant((node) => {
|
|
17640
|
+
if (!ts_morph_1.Node.isCallExpression(node))
|
|
17641
|
+
return;
|
|
17642
|
+
const expr = node.getExpression();
|
|
17643
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
|
|
17644
|
+
return;
|
|
17645
|
+
const methodName = expr.getName();
|
|
17646
|
+
if (!MONGOOSE_QUERY_METHODS.has(methodName))
|
|
17647
|
+
return;
|
|
17648
|
+
const obj = expr.getExpression();
|
|
17649
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(obj))
|
|
17650
|
+
return;
|
|
17651
|
+
const propName = obj.getName();
|
|
17652
|
+
const thisExpr = obj.getExpression();
|
|
17653
|
+
if (thisExpr.getKind() !== ts_morph_1.SyntaxKind.ThisKeyword)
|
|
17654
|
+
return;
|
|
17655
|
+
const schemaName = modelToSchema.get(propName);
|
|
17656
|
+
if (!schemaName)
|
|
17657
|
+
return;
|
|
17658
|
+
const schemaFields = schemaFieldMap.get(schemaName);
|
|
17659
|
+
if (!schemaFields)
|
|
17660
|
+
return;
|
|
17661
|
+
const args = node.getArguments();
|
|
17662
|
+
if (args.length === 0)
|
|
17663
|
+
return;
|
|
17664
|
+
const filterArg = args[0];
|
|
17665
|
+
if (!ts_morph_1.Node.isObjectLiteralExpression(filterArg))
|
|
17666
|
+
return;
|
|
17667
|
+
for (const prop of filterArg.getProperties()) {
|
|
17668
|
+
if (!ts_morph_1.Node.isPropertyAssignment(prop))
|
|
17669
|
+
continue;
|
|
17670
|
+
const nameNode = prop.getNameNode();
|
|
17671
|
+
let fieldName;
|
|
17672
|
+
if (ts_morph_1.Node.isStringLiteral(nameNode)) {
|
|
17673
|
+
fieldName = nameNode.getLiteralValue();
|
|
17674
|
+
} else if (ts_morph_1.Node.isIdentifier(nameNode)) {
|
|
17675
|
+
fieldName = nameNode.getText();
|
|
17676
|
+
} else if (ts_morph_1.Node.isComputedPropertyName(nameNode)) {
|
|
17677
|
+
continue;
|
|
17678
|
+
} else {
|
|
17679
|
+
fieldName = nameNode.getText();
|
|
17680
|
+
}
|
|
17681
|
+
const rootField = fieldName.split(".")[0];
|
|
17682
|
+
if (!schemaFields.has(rootField)) {
|
|
17683
|
+
const availableFields = [...schemaFields].filter((f) => !BUILTIN_FIELDS.has(f) || f === "_id").sort().join(", ");
|
|
17684
|
+
violations.push({
|
|
17685
|
+
category: "reliability",
|
|
17686
|
+
type: "query-references-nonexistent-field",
|
|
17687
|
+
ruleId: "query-references-nonexistent-field",
|
|
17688
|
+
severity: "critical",
|
|
17689
|
+
source: "deterministic",
|
|
17690
|
+
confidence: "high",
|
|
17691
|
+
file: file.path,
|
|
17692
|
+
line: prop.getStartLineNumber(),
|
|
17693
|
+
message: `Query filter references field '${fieldName}' which does not exist in ${schemaName} schema \u2014 available fields: ${availableFields}`,
|
|
17694
|
+
suggestion: `Check the ${schemaName} schema definition for the correct field name`,
|
|
17695
|
+
debtPoints: 8,
|
|
17696
|
+
gateAction: "block"
|
|
17697
|
+
});
|
|
17698
|
+
}
|
|
17699
|
+
}
|
|
17700
|
+
});
|
|
17701
|
+
project.removeSourceFile(sf);
|
|
17702
|
+
}
|
|
17703
|
+
return violations;
|
|
17704
|
+
}
|
|
17410
17705
|
function applyExceptions(violations, policy) {
|
|
17411
17706
|
return violations.filter((v) => !policy.exceptions.some((ex) => ex.isActive && ex.rule === v.ruleId && v.file.includes(ex.file)));
|
|
17412
17707
|
}
|
|
@@ -18474,6 +18769,8 @@ var require_dead_code_detector = __commonJS({
|
|
|
18474
18769
|
debtPoints: 1,
|
|
18475
18770
|
gateAction: "warn"
|
|
18476
18771
|
}));
|
|
18772
|
+
const unusedMethodViolations = detectUnusedPublicMethods(sourceFiles, input);
|
|
18773
|
+
violations.push(...unusedMethodViolations);
|
|
18477
18774
|
const totalExports = Array.from(allExports.values()).reduce((sum, exps) => sum + exps.length, 0);
|
|
18478
18775
|
return {
|
|
18479
18776
|
unusedExports,
|
|
@@ -18829,6 +19126,121 @@ var require_dead_code_detector = __commonJS({
|
|
|
18829
19126
|
}
|
|
18830
19127
|
return result.join("/");
|
|
18831
19128
|
}
|
|
19129
|
+
var NESTJS_METHOD_DECORATORS = /* @__PURE__ */ new Set([
|
|
19130
|
+
"Get",
|
|
19131
|
+
"Post",
|
|
19132
|
+
"Put",
|
|
19133
|
+
"Patch",
|
|
19134
|
+
"Delete",
|
|
19135
|
+
"Options",
|
|
19136
|
+
"Head",
|
|
19137
|
+
"All",
|
|
19138
|
+
"Cron",
|
|
19139
|
+
"OnEvent",
|
|
19140
|
+
"Process",
|
|
19141
|
+
"OnQueueActive",
|
|
19142
|
+
"OnQueueCompleted",
|
|
19143
|
+
"OnQueueFailed",
|
|
19144
|
+
"SubscribeMessage",
|
|
19145
|
+
"OnGatewayInit",
|
|
19146
|
+
"OnGatewayConnection",
|
|
19147
|
+
"OnGatewayDisconnect",
|
|
19148
|
+
"EventPattern",
|
|
19149
|
+
"MessagePattern",
|
|
19150
|
+
"GrpcMethod"
|
|
19151
|
+
]);
|
|
19152
|
+
var LIFECYCLE_HOOKS = /* @__PURE__ */ new Set([
|
|
19153
|
+
"onModuleInit",
|
|
19154
|
+
"onModuleDestroy",
|
|
19155
|
+
"onApplicationBootstrap",
|
|
19156
|
+
"onApplicationShutdown",
|
|
19157
|
+
"beforeApplicationShutdown",
|
|
19158
|
+
"afterInit",
|
|
19159
|
+
"handleConnection",
|
|
19160
|
+
"handleDisconnect",
|
|
19161
|
+
"validate",
|
|
19162
|
+
// Passport strategy method — called by framework
|
|
19163
|
+
"canActivate",
|
|
19164
|
+
// Guard method
|
|
19165
|
+
"intercept",
|
|
19166
|
+
// Interceptor method
|
|
19167
|
+
"transform",
|
|
19168
|
+
// Pipe method
|
|
19169
|
+
"catch",
|
|
19170
|
+
// Exception filter method
|
|
19171
|
+
"use"
|
|
19172
|
+
// Middleware method
|
|
19173
|
+
]);
|
|
19174
|
+
function detectUnusedPublicMethods(sourceFiles, input) {
|
|
19175
|
+
const violations = [];
|
|
19176
|
+
const isNestJS = /nestjs/i.test(input.policy.stack.framework);
|
|
19177
|
+
const allFileTexts = /* @__PURE__ */ new Map();
|
|
19178
|
+
for (const [filePath, sf] of sourceFiles) {
|
|
19179
|
+
allFileTexts.set(filePath, sf.getFullText());
|
|
19180
|
+
}
|
|
19181
|
+
for (const [filePath, sf] of sourceFiles) {
|
|
19182
|
+
if (/\.(spec|test)\./i.test(filePath))
|
|
19183
|
+
continue;
|
|
19184
|
+
if (/\.module\./i.test(filePath))
|
|
19185
|
+
continue;
|
|
19186
|
+
for (const cls of sf.getClasses()) {
|
|
19187
|
+
const hasInjectable = cls.getDecorators().some((d) => d.getName() === "Injectable");
|
|
19188
|
+
if (!hasInjectable)
|
|
19189
|
+
continue;
|
|
19190
|
+
const baseClass = cls.getExtends()?.getText() ?? "";
|
|
19191
|
+
if (/PassportStrategy|Strategy/.test(baseClass))
|
|
19192
|
+
continue;
|
|
19193
|
+
const className = cls.getName();
|
|
19194
|
+
if (!className)
|
|
19195
|
+
continue;
|
|
19196
|
+
for (const method of cls.getMethods()) {
|
|
19197
|
+
const methodName = method.getName();
|
|
19198
|
+
if (!methodName || methodName.startsWith("_"))
|
|
19199
|
+
continue;
|
|
19200
|
+
const scope = method.getScope();
|
|
19201
|
+
if (scope === "private" || scope === "protected")
|
|
19202
|
+
continue;
|
|
19203
|
+
if (LIFECYCLE_HOOKS.has(methodName))
|
|
19204
|
+
continue;
|
|
19205
|
+
if (isNestJS && method.getDecorators().some((d) => NESTJS_METHOD_DECORATORS.has(d.getName())))
|
|
19206
|
+
continue;
|
|
19207
|
+
const callPattern = new RegExp(`\\.${methodName}\\s*\\(`);
|
|
19208
|
+
let hasExternalCall = false;
|
|
19209
|
+
for (const [otherFilePath, otherText] of allFileTexts) {
|
|
19210
|
+
if (otherFilePath === filePath)
|
|
19211
|
+
continue;
|
|
19212
|
+
if (callPattern.test(otherText)) {
|
|
19213
|
+
hasExternalCall = true;
|
|
19214
|
+
break;
|
|
19215
|
+
}
|
|
19216
|
+
}
|
|
19217
|
+
if (!hasExternalCall) {
|
|
19218
|
+
const ownText = allFileTexts.get(filePath) ?? "";
|
|
19219
|
+
const matches = ownText.match(new RegExp(`\\.${methodName}\\s*\\(`, "g"));
|
|
19220
|
+
const internalCallCount = matches?.length ?? 0;
|
|
19221
|
+
if (internalCallCount === 0) {
|
|
19222
|
+
violations.push({
|
|
19223
|
+
category: "maintainability",
|
|
19224
|
+
type: "unused-public-method",
|
|
19225
|
+
ruleId: "unused-public-method",
|
|
19226
|
+
severity: "warning",
|
|
19227
|
+
source: "deterministic",
|
|
19228
|
+
confidence: "medium",
|
|
19229
|
+
file: filePath,
|
|
19230
|
+
line: method.getStartLineNumber(),
|
|
19231
|
+
function: methodName,
|
|
19232
|
+
message: `Public method '${methodName}' in ${className} has no call sites \u2014 consider removing it or marking it private`,
|
|
19233
|
+
suggestion: `Remove the unused method or mark it as private if it's only used internally`,
|
|
19234
|
+
debtPoints: 1,
|
|
19235
|
+
gateAction: "warn"
|
|
19236
|
+
});
|
|
19237
|
+
}
|
|
19238
|
+
}
|
|
19239
|
+
}
|
|
19240
|
+
}
|
|
19241
|
+
}
|
|
19242
|
+
return violations;
|
|
19243
|
+
}
|
|
18832
19244
|
function emptyResult() {
|
|
18833
19245
|
return {
|
|
18834
19246
|
unusedExports: [],
|
|
@@ -19943,19 +20355,35 @@ var RadarApiClient = class _RadarApiClient {
|
|
|
19943
20355
|
return null;
|
|
19944
20356
|
}
|
|
19945
20357
|
async fetch(path9, init) {
|
|
19946
|
-
const
|
|
19947
|
-
|
|
19948
|
-
|
|
19949
|
-
|
|
19950
|
-
|
|
19951
|
-
|
|
19952
|
-
|
|
19953
|
-
|
|
20358
|
+
const url = `${this.apiUrl}${path9}`;
|
|
20359
|
+
let res;
|
|
20360
|
+
try {
|
|
20361
|
+
res = await fetch(url, {
|
|
20362
|
+
...init,
|
|
20363
|
+
headers: {
|
|
20364
|
+
"Authorization": `Bearer ${this.token}`,
|
|
20365
|
+
"Content-Type": "application/json",
|
|
20366
|
+
...init?.headers
|
|
20367
|
+
}
|
|
20368
|
+
});
|
|
20369
|
+
} catch (err) {
|
|
20370
|
+
const cause = err?.cause?.message || err?.cause?.code || "";
|
|
20371
|
+
throw new Error(`Network error connecting to ${url}: ${err.message}${cause ? ` (${cause})` : ""}`);
|
|
20372
|
+
}
|
|
19954
20373
|
if (!res.ok) {
|
|
19955
20374
|
const body = await res.text().catch(() => "");
|
|
19956
|
-
throw new Error(`API error ${res.status}: ${body}`);
|
|
20375
|
+
throw new Error(`API error ${res.status} on ${path9}: ${body}`);
|
|
20376
|
+
}
|
|
20377
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
20378
|
+
const text = await res.text();
|
|
20379
|
+
if (!text || text.trim().length === 0) {
|
|
20380
|
+
return void 0;
|
|
20381
|
+
}
|
|
20382
|
+
try {
|
|
20383
|
+
return JSON.parse(text);
|
|
20384
|
+
} catch {
|
|
20385
|
+
return void 0;
|
|
19957
20386
|
}
|
|
19958
|
-
return res.json();
|
|
19959
20387
|
}
|
|
19960
20388
|
async verifyToken() {
|
|
19961
20389
|
return this.fetch("/cli/auth/verify");
|
|
@@ -20262,8 +20690,9 @@ async function scanCommand(targetPath, options) {
|
|
|
20262
20690
|
}).catch(() => {
|
|
20263
20691
|
});
|
|
20264
20692
|
console.log(import_chalk.default.green(" \u2713 Results synced to dashboard: https://www.technicaldebtradar.com/dashboard"));
|
|
20265
|
-
} catch {
|
|
20266
|
-
|
|
20693
|
+
} catch (err) {
|
|
20694
|
+
const detail = err?.message ? `: ${err.message}` : "";
|
|
20695
|
+
console.log(import_chalk.default.yellow(` \u26A0\uFE0F Could not sync results to dashboard${detail}`));
|
|
20267
20696
|
}
|
|
20268
20697
|
}
|
|
20269
20698
|
if (mode === "warn") {
|