technical-debt-radar 1.6.4 → 1.7.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 +445 -22
- 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("/")) {
|
|
@@ -15332,6 +15414,13 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15332
15414
|
entity = extractTypeORMEntity(node);
|
|
15333
15415
|
if (!entity)
|
|
15334
15416
|
entity = extractSequelizeEntity(node);
|
|
15417
|
+
if (methodName === "find") {
|
|
15418
|
+
const typeormEntity = extractTypeORMEntity(node);
|
|
15419
|
+
const seqEntity = extractSequelizeEntity(node);
|
|
15420
|
+
if (!typeormEntity && !seqEntity)
|
|
15421
|
+
return;
|
|
15422
|
+
entity = typeormEntity ?? seqEntity;
|
|
15423
|
+
}
|
|
15335
15424
|
const vol = entity ? resolveVolume(entity, policy) : void 0;
|
|
15336
15425
|
const fn = getEnclosingFn(node, fns);
|
|
15337
15426
|
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"));
|
|
@@ -15437,6 +15526,8 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15437
15526
|
entity = extractTypeORMEntity(node);
|
|
15438
15527
|
if (!entity)
|
|
15439
15528
|
entity = extractSequelizeEntity(node);
|
|
15529
|
+
if (methodName === "find")
|
|
15530
|
+
return;
|
|
15440
15531
|
const vol = entity ? resolveVolume(entity, policy) : void 0;
|
|
15441
15532
|
const hasCursorPagination = argsContainKey(node, "cursor");
|
|
15442
15533
|
const hasTakePagination = argsContainKey(node, "take") || argsContainKey(node, "limit");
|
|
@@ -16241,6 +16332,7 @@ var require_reliability_detector = __commonJS({
|
|
|
16241
16332
|
exports2.detectReliabilityIssues = detectReliabilityIssues;
|
|
16242
16333
|
var shared_1 = require_dist();
|
|
16243
16334
|
var ts_morph_1 = require("ts-morph");
|
|
16335
|
+
var minimatch_1 = require_commonjs3();
|
|
16244
16336
|
async function detectReliabilityIssues(input, policy) {
|
|
16245
16337
|
const violations = [];
|
|
16246
16338
|
const project = new ts_morph_1.Project({ useInMemoryFileSystem: true });
|
|
@@ -16271,6 +16363,10 @@ var require_reliability_detector = __commonJS({
|
|
|
16271
16363
|
detectUnboundedParallelCalls(sourceFile, file.path, fns, policy, violations);
|
|
16272
16364
|
project.removeSourceFile(sourceFile);
|
|
16273
16365
|
}
|
|
16366
|
+
if (/mongoose/i.test(policy.stack.orm)) {
|
|
16367
|
+
const schemaViolations = detectQueryNonexistentField(input);
|
|
16368
|
+
violations.push(...schemaViolations);
|
|
16369
|
+
}
|
|
16274
16370
|
return applyExceptions(deduplicateOverlapping(violations), policy);
|
|
16275
16371
|
}
|
|
16276
16372
|
function deduplicateOverlapping(violations) {
|
|
@@ -16670,9 +16766,11 @@ var require_reliability_detector = __commonJS({
|
|
|
16670
16766
|
continue;
|
|
16671
16767
|
let unhandledAwaitCount = 0;
|
|
16672
16768
|
let totalAwaitCount = 0;
|
|
16673
|
-
fn.node.forEachDescendant((child) => {
|
|
16674
|
-
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)))
|
|
16769
|
+
fn.node.forEachDescendant((child, traversal) => {
|
|
16770
|
+
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))) {
|
|
16771
|
+
traversal.skip();
|
|
16675
16772
|
return;
|
|
16773
|
+
}
|
|
16676
16774
|
if (!ts_morph_1.Node.isAwaitExpression(child))
|
|
16677
16775
|
return;
|
|
16678
16776
|
totalAwaitCount++;
|
|
@@ -17189,9 +17287,44 @@ var require_reliability_detector = __commonJS({
|
|
|
17189
17287
|
});
|
|
17190
17288
|
});
|
|
17191
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
|
+
}
|
|
17192
17323
|
function detectProviderOutsideHomeModule(sourceFile, filePath, allFiles, policy, violations) {
|
|
17193
17324
|
if (!filePath.endsWith(".module.ts") && !filePath.endsWith(".module.js"))
|
|
17194
17325
|
return;
|
|
17326
|
+
if (!/nestjs/i.test(policy.stack.framework))
|
|
17327
|
+
return;
|
|
17195
17328
|
sourceFile.forEachDescendant((node) => {
|
|
17196
17329
|
if (!ts_morph_1.Node.isDecorator(node) || node.getName() !== "Module")
|
|
17197
17330
|
return;
|
|
@@ -17222,24 +17355,40 @@ var require_reliability_detector = __commonJS({
|
|
|
17222
17355
|
if (!matchingImport)
|
|
17223
17356
|
continue;
|
|
17224
17357
|
const specifier = importDecl.getModuleSpecifierValue();
|
|
17358
|
+
let resolvedPath;
|
|
17225
17359
|
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
17226
|
-
|
|
17227
|
-
|
|
17228
|
-
|
|
17229
|
-
|
|
17230
|
-
|
|
17231
|
-
|
|
17232
|
-
|
|
17233
|
-
|
|
17234
|
-
|
|
17235
|
-
|
|
17236
|
-
|
|
17237
|
-
|
|
17238
|
-
|
|
17239
|
-
|
|
17240
|
-
|
|
17241
|
-
|
|
17242
|
-
}
|
|
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
|
+
});
|
|
17243
17392
|
}
|
|
17244
17393
|
}
|
|
17245
17394
|
}
|
|
@@ -17396,6 +17545,163 @@ var require_reliability_detector = __commonJS({
|
|
|
17396
17545
|
function escapeRegex(str) {
|
|
17397
17546
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
17398
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
|
+
}
|
|
17399
17705
|
function applyExceptions(violations, policy) {
|
|
17400
17706
|
return violations.filter((v) => !policy.exceptions.some((ex) => ex.isActive && ex.rule === v.ruleId && v.file.includes(ex.file)));
|
|
17401
17707
|
}
|
|
@@ -18463,6 +18769,8 @@ var require_dead_code_detector = __commonJS({
|
|
|
18463
18769
|
debtPoints: 1,
|
|
18464
18770
|
gateAction: "warn"
|
|
18465
18771
|
}));
|
|
18772
|
+
const unusedMethodViolations = detectUnusedPublicMethods(sourceFiles, input);
|
|
18773
|
+
violations.push(...unusedMethodViolations);
|
|
18466
18774
|
const totalExports = Array.from(allExports.values()).reduce((sum, exps) => sum + exps.length, 0);
|
|
18467
18775
|
return {
|
|
18468
18776
|
unusedExports,
|
|
@@ -18818,6 +19126,121 @@ var require_dead_code_detector = __commonJS({
|
|
|
18818
19126
|
}
|
|
18819
19127
|
return result.join("/");
|
|
18820
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
|
+
}
|
|
18821
19244
|
function emptyResult() {
|
|
18822
19245
|
return {
|
|
18823
19246
|
unusedExports: [],
|