technical-debt-radar 1.0.2 → 1.0.3
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 +418 -74
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -39,7 +39,7 @@ var require_constants = __commonJS({
|
|
|
39
39
|
"../../packages/shared/dist/constants/index.js"(exports2) {
|
|
40
40
|
"use strict";
|
|
41
41
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
42
|
-
exports2.DEBT_DELTA_WARN_THRESHOLD = exports2.DEBT_DELTA_BLOCK_THRESHOLD = exports2.DEFAULT_COMPLEXITY_THRESHOLD = exports2.MAX_CHANGED_FILES_PER_PR = exports2.FIRST_SCAN_TIMEOUT_MS = exports2.PR_ANALYSIS_TIMEOUT_MS = exports2.MAX_AI_OUTPUT_TOKENS = exports2.MAX_AI_TOKENS_PER_FUNCTION = exports2.MAX_SUSPECT_FUNCTIONS_PER_PR = exports2.MAX_CALLER_TRACE_DEPTH = exports2.MAX_CROSS_FILE_SUSPECTS = exports2.CROSS_FILE_RULES = exports2.ARCHITECTURE_RULES = exports2.MAINTAINABILITY_RULES = exports2.PERFORMANCE_RULES = exports2.RELIABILITY_RULES = exports2.RUNTIME_RISK_RULES = exports2.VOLUME_THRESHOLDS = exports2.DEFAULT_SCORING = exports2.DEFAULT_MODE = void 0;
|
|
42
|
+
exports2.DEBT_DELTA_WARN_THRESHOLD = exports2.DEBT_DELTA_BLOCK_THRESHOLD = exports2.DEFAULT_LARGE_FILE_THRESHOLD = exports2.DEFAULT_COMPLEXITY_THRESHOLD = exports2.MAX_CHANGED_FILES_PER_PR = exports2.FIRST_SCAN_TIMEOUT_MS = exports2.PR_ANALYSIS_TIMEOUT_MS = exports2.MAX_AI_OUTPUT_TOKENS = exports2.MAX_AI_TOKENS_PER_FUNCTION = exports2.MAX_SUSPECT_FUNCTIONS_PER_PR = exports2.MAX_CALLER_TRACE_DEPTH = exports2.MAX_CROSS_FILE_SUSPECTS = exports2.CROSS_FILE_RULES = exports2.ARCHITECTURE_RULES = exports2.MAINTAINABILITY_RULES = exports2.PERFORMANCE_RULES = exports2.RELIABILITY_RULES = exports2.RUNTIME_RISK_RULES = exports2.VOLUME_THRESHOLDS = exports2.DEFAULT_SCORING = exports2.DEFAULT_MODE = void 0;
|
|
43
43
|
exports2.DEFAULT_MODE = "warn";
|
|
44
44
|
exports2.DEFAULT_SCORING = {
|
|
45
45
|
architecture_violation: 5,
|
|
@@ -97,10 +97,12 @@ var require_constants = __commonJS({
|
|
|
97
97
|
FETCH_ALL_FILTER_MEMORY: "fetch-all-filter-in-memory",
|
|
98
98
|
MISSING_PAGINATION: "missing-pagination-endpoint",
|
|
99
99
|
UNFILTERED_COUNT: "unfiltered-count-large-table",
|
|
100
|
-
RAW_SQL_NO_LIMIT: "raw-sql-no-limit"
|
|
100
|
+
RAW_SQL_NO_LIMIT: "raw-sql-no-limit",
|
|
101
|
+
RAW_SQL_UNSAFE: "raw-sql-unsafe"
|
|
101
102
|
};
|
|
102
103
|
exports2.MAINTAINABILITY_RULES = {
|
|
103
104
|
HIGH_COMPLEXITY: "high-complexity",
|
|
105
|
+
LARGE_FILE: "large-file",
|
|
104
106
|
CODE_DUPLICATION: "code-duplication",
|
|
105
107
|
MISSING_TEST_FILE: "missing-test-file",
|
|
106
108
|
UNUSED_EXPORT: "unused-export",
|
|
@@ -131,6 +133,7 @@ var require_constants = __commonJS({
|
|
|
131
133
|
exports2.FIRST_SCAN_TIMEOUT_MS = 9e5;
|
|
132
134
|
exports2.MAX_CHANGED_FILES_PER_PR = 100;
|
|
133
135
|
exports2.DEFAULT_COMPLEXITY_THRESHOLD = 10;
|
|
136
|
+
exports2.DEFAULT_LARGE_FILE_THRESHOLD = 300;
|
|
134
137
|
exports2.DEBT_DELTA_BLOCK_THRESHOLD = 15;
|
|
135
138
|
exports2.DEBT_DELTA_WARN_THRESHOLD = 8;
|
|
136
139
|
}
|
|
@@ -495,6 +498,7 @@ var require_parser = __commonJS({
|
|
|
495
498
|
const exceptions = parseExceptions(raw.exceptions);
|
|
496
499
|
const standards = parseStandards(raw.standards);
|
|
497
500
|
const scoring = parseScoring(raw.scoring);
|
|
501
|
+
const shared_infrastructure = parseSharedInfrastructure(raw.shared_infrastructure);
|
|
498
502
|
const result = {
|
|
499
503
|
mode,
|
|
500
504
|
stack,
|
|
@@ -507,7 +511,8 @@ var require_parser = __commonJS({
|
|
|
507
511
|
gates,
|
|
508
512
|
exceptions,
|
|
509
513
|
standards,
|
|
510
|
-
scoring
|
|
514
|
+
scoring,
|
|
515
|
+
shared_infrastructure
|
|
511
516
|
};
|
|
512
517
|
if (architecture !== void 0) {
|
|
513
518
|
result.architecture = architecture;
|
|
@@ -845,6 +850,19 @@ var require_parser = __commonJS({
|
|
|
845
850
|
}
|
|
846
851
|
return result;
|
|
847
852
|
}
|
|
853
|
+
function parseSharedInfrastructure(value) {
|
|
854
|
+
if (value === void 0 || value === null)
|
|
855
|
+
return [];
|
|
856
|
+
if (!Array.isArray(value)) {
|
|
857
|
+
throw new PolicyValidationError("shared_infrastructure must be an array of folder names");
|
|
858
|
+
}
|
|
859
|
+
return value.map((item, i) => {
|
|
860
|
+
if (typeof item !== "string") {
|
|
861
|
+
throw new PolicyValidationError(`shared_infrastructure[${i}] must be a string`);
|
|
862
|
+
}
|
|
863
|
+
return item;
|
|
864
|
+
});
|
|
865
|
+
}
|
|
848
866
|
function parseScoring(value) {
|
|
849
867
|
if (value === void 0 || value === null)
|
|
850
868
|
return {};
|
|
@@ -7489,6 +7507,10 @@ var require_validator = __commonJS({
|
|
|
7489
7507
|
items: { type: "string", enum: ["ddd", "hexagonal", "clean", "layered", "mvc", "event-driven", "feature-module"] }
|
|
7490
7508
|
}
|
|
7491
7509
|
]
|
|
7510
|
+
},
|
|
7511
|
+
shared_infrastructure: {
|
|
7512
|
+
type: "array",
|
|
7513
|
+
items: { type: "string" }
|
|
7492
7514
|
}
|
|
7493
7515
|
},
|
|
7494
7516
|
additionalProperties: false
|
|
@@ -9531,7 +9553,8 @@ var require_compiler = __commonJS({
|
|
|
9531
9553
|
exceptions,
|
|
9532
9554
|
standards: config.standards,
|
|
9533
9555
|
scoring,
|
|
9534
|
-
architecture: config.architecture
|
|
9556
|
+
architecture: config.architecture,
|
|
9557
|
+
sharedInfrastructure: config.shared_infrastructure ?? []
|
|
9535
9558
|
};
|
|
9536
9559
|
}
|
|
9537
9560
|
function compileLayers(layers) {
|
|
@@ -10058,9 +10081,9 @@ var require_layered = __commonJS({
|
|
|
10058
10081
|
gates: shared_rules_1.DEFAULT_GATES,
|
|
10059
10082
|
scoring: shared_rules_1.DEFAULT_PRESET_SCORING,
|
|
10060
10083
|
layerMapping: {
|
|
10061
|
-
presentation: "api",
|
|
10062
|
-
business: "application",
|
|
10063
|
-
"data-access": "infrastructure"
|
|
10084
|
+
presentation: ["api", "controllers", "routes", "handlers", "web", "rest", "graphql"],
|
|
10085
|
+
business: ["application", "services", "use-cases", "usecases", "domain"],
|
|
10086
|
+
"data-access": ["infrastructure", "repositories", "persistence", "data", "dal", "db"]
|
|
10064
10087
|
}
|
|
10065
10088
|
};
|
|
10066
10089
|
}
|
|
@@ -10507,7 +10530,13 @@ var require_project_detector = __commonJS({
|
|
|
10507
10530
|
} catch {
|
|
10508
10531
|
return [];
|
|
10509
10532
|
}
|
|
10510
|
-
|
|
10533
|
+
const uniqueByName = /* @__PURE__ */ new Map();
|
|
10534
|
+
for (const layer of found.values()) {
|
|
10535
|
+
if (!uniqueByName.has(layer.name)) {
|
|
10536
|
+
uniqueByName.set(layer.name, layer);
|
|
10537
|
+
}
|
|
10538
|
+
}
|
|
10539
|
+
return Array.from(uniqueByName.values());
|
|
10511
10540
|
}
|
|
10512
10541
|
async function scanForLayers(dirPath, relativePath, found) {
|
|
10513
10542
|
let entries;
|
|
@@ -10618,7 +10647,7 @@ var require_project_detector = __commonJS({
|
|
|
10618
10647
|
for (const [pattern, scoreMap] of Object.entries(SCORING_TABLE)) {
|
|
10619
10648
|
if (detectedPatterns.has(pattern)) {
|
|
10620
10649
|
for (const [arch, points] of Object.entries(scoreMap)) {
|
|
10621
|
-
scores[arch] += points;
|
|
10650
|
+
scores[arch] += points ?? 0;
|
|
10622
10651
|
}
|
|
10623
10652
|
}
|
|
10624
10653
|
}
|
|
@@ -11519,9 +11548,12 @@ var require_rules_generator = __commonJS({
|
|
|
11519
11548
|
if (placeholder === "contracts_path")
|
|
11520
11549
|
return null;
|
|
11521
11550
|
if (presetMapping && placeholder in presetMapping) {
|
|
11522
|
-
const
|
|
11523
|
-
|
|
11524
|
-
|
|
11551
|
+
const mapped = presetMapping[placeholder];
|
|
11552
|
+
const candidates = Array.isArray(mapped) ? mapped : [mapped];
|
|
11553
|
+
for (const candidate of candidates) {
|
|
11554
|
+
if (layerMap.has(candidate)) {
|
|
11555
|
+
return layerMap.get(candidate);
|
|
11556
|
+
}
|
|
11525
11557
|
}
|
|
11526
11558
|
}
|
|
11527
11559
|
if (layerMap.has(placeholder)) {
|
|
@@ -12567,7 +12599,11 @@ var require_import_graph = __commonJS({
|
|
|
12567
12599
|
const specifier = decl.getModuleSpecifierValue();
|
|
12568
12600
|
const resolved = resolveModuleSpecifier(sourcePath, specifier, project);
|
|
12569
12601
|
if (resolved) {
|
|
12570
|
-
|
|
12602
|
+
const isTypeOnly = decl.isTypeOnly();
|
|
12603
|
+
const edge = buildEdge(sourcePath, resolved, "static", policy);
|
|
12604
|
+
if (isTypeOnly)
|
|
12605
|
+
edge.typeOnly = true;
|
|
12606
|
+
edges.push(edge);
|
|
12571
12607
|
}
|
|
12572
12608
|
}
|
|
12573
12609
|
for (const decl of sourceFile.getExportDeclarations()) {
|
|
@@ -12762,10 +12798,14 @@ var require_boundary_checker = __commonJS({
|
|
|
12762
12798
|
const patterns = Array.isArray(policy.architecture) ? policy.architecture : [policy.architecture];
|
|
12763
12799
|
return patterns.includes("feature-module");
|
|
12764
12800
|
}
|
|
12765
|
-
function
|
|
12801
|
+
function isNestJSFramework(policy) {
|
|
12802
|
+
return /nestjs/i.test(policy.stack.framework);
|
|
12803
|
+
}
|
|
12804
|
+
var suffixPattern = (suffix) => new RegExp(`\\.${suffix}(\\.(ts|tsx|js|jsx))?$`);
|
|
12805
|
+
var NESTJS_SHARED_INFRA_FOLDERS = ["guards", "decorators", "interceptors", "pipes", "filters", "middleware", "shared"];
|
|
12806
|
+
function isNestJSExemptImport(sourcePath, targetPath, customSharedFolders = []) {
|
|
12766
12807
|
const srcBase = sourcePath.replace(/\\/g, "/");
|
|
12767
12808
|
const tgtBase = targetPath.replace(/\\/g, "/");
|
|
12768
|
-
const suffixPattern = (suffix) => new RegExp(`\\.${suffix}(\\.(ts|tsx|js|jsx))?$`);
|
|
12769
12809
|
if (suffixPattern("module").test(srcBase))
|
|
12770
12810
|
return true;
|
|
12771
12811
|
if (suffixPattern("module").test(tgtBase))
|
|
@@ -12776,10 +12816,24 @@ var require_boundary_checker = __commonJS({
|
|
|
12776
12816
|
return true;
|
|
12777
12817
|
if (suffixPattern("decorator").test(tgtBase))
|
|
12778
12818
|
return true;
|
|
12779
|
-
if (
|
|
12819
|
+
if (suffixPattern("guard").test(tgtBase))
|
|
12820
|
+
return true;
|
|
12821
|
+
if (suffixPattern("interceptor").test(tgtBase))
|
|
12780
12822
|
return true;
|
|
12781
|
-
if (
|
|
12823
|
+
if (suffixPattern("pipe").test(tgtBase))
|
|
12782
12824
|
return true;
|
|
12825
|
+
if (suffixPattern("filter").test(tgtBase))
|
|
12826
|
+
return true;
|
|
12827
|
+
if (/\/dto\//.test(tgtBase) || suffixPattern("dto").test(tgtBase))
|
|
12828
|
+
return true;
|
|
12829
|
+
for (const folder of NESTJS_SHARED_INFRA_FOLDERS) {
|
|
12830
|
+
if (new RegExp(`/${folder}/`).test(tgtBase))
|
|
12831
|
+
return true;
|
|
12832
|
+
}
|
|
12833
|
+
for (const folder of customSharedFolders) {
|
|
12834
|
+
if (new RegExp(`/${folder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/`).test(tgtBase))
|
|
12835
|
+
return true;
|
|
12836
|
+
}
|
|
12783
12837
|
return false;
|
|
12784
12838
|
}
|
|
12785
12839
|
var NPM_PACKAGE_LAYER_MAP = {
|
|
@@ -12814,6 +12868,10 @@ var require_boundary_checker = __commonJS({
|
|
|
12814
12868
|
const violations = [];
|
|
12815
12869
|
const activeExceptions = policy.exceptions.filter((e) => e.isActive);
|
|
12816
12870
|
for (const edge of edges) {
|
|
12871
|
+
if (edge.typeOnly)
|
|
12872
|
+
continue;
|
|
12873
|
+
if (/\.module(\.(ts|tsx|js|jsx))?$/.test(edge.source))
|
|
12874
|
+
continue;
|
|
12817
12875
|
const violation = checkEdge(edge, policy, activeExceptions);
|
|
12818
12876
|
if (violation) {
|
|
12819
12877
|
violations.push(violation);
|
|
@@ -12873,20 +12931,23 @@ var require_boundary_checker = __commonJS({
|
|
|
12873
12931
|
continue;
|
|
12874
12932
|
const hasDenyRule = policy.rules.some((r) => r.type === "deny" && matchesPattern(sourceLayer, r.source) && (matchesPattern(targetLayer, r.target) || r.target === "__db__" && targetLayer === "infrastructure"));
|
|
12875
12933
|
if (hasDenyRule) {
|
|
12876
|
-
|
|
12934
|
+
const isFrameworkImport = targetLayer === "api";
|
|
12935
|
+
const effectiveRuleId = isFrameworkImport ? shared_1.ARCHITECTURE_RULES.FORBIDDEN_FRAMEWORK : shared_1.ARCHITECTURE_RULES.LAYER_VIOLATION;
|
|
12936
|
+
const effectiveType = isFrameworkImport ? "forbidden-framework-in-layer" : "layer-boundary-violation";
|
|
12937
|
+
if (isExcepted(effectiveRuleId, filePath, activeExceptions))
|
|
12877
12938
|
continue;
|
|
12878
12939
|
violations.push({
|
|
12879
12940
|
category: "architecture",
|
|
12880
|
-
type:
|
|
12881
|
-
ruleId:
|
|
12941
|
+
type: effectiveType,
|
|
12942
|
+
ruleId: effectiveRuleId,
|
|
12882
12943
|
severity: "critical",
|
|
12883
12944
|
source: "deterministic",
|
|
12884
12945
|
confidence: "high",
|
|
12885
12946
|
file: filePath,
|
|
12886
12947
|
line,
|
|
12887
12948
|
layer: sourceLayer,
|
|
12888
|
-
message: `Forbidden dependency: "${sourceLayer}" \u2192 "${targetLayer}" \u2014 import '${pkgName}' violates layer boundary (${filePath})`,
|
|
12889
|
-
explanation: `The ${sourceLayer} layer must not depend on ${targetLayer} packages. Move this import to the ${targetLayer} layer.`,
|
|
12949
|
+
message: isFrameworkImport ? `Forbidden framework import: "${sourceLayer}" imports '${pkgName}' \u2014 framework symbols not allowed in ${sourceLayer} layer (${filePath})` : `Forbidden dependency: "${sourceLayer}" \u2192 "${targetLayer}" \u2014 import '${pkgName}' violates layer boundary (${filePath})`,
|
|
12950
|
+
explanation: isFrameworkImport ? `The ${sourceLayer} layer must not import framework packages. Move this import to the api/controller layer.` : `The ${sourceLayer} layer must not depend on ${targetLayer} packages. Move this import to the ${targetLayer} layer.`,
|
|
12890
12951
|
debtPoints: 5,
|
|
12891
12952
|
gateAction: "block"
|
|
12892
12953
|
});
|
|
@@ -12918,12 +12979,16 @@ var require_boundary_checker = __commonJS({
|
|
|
12918
12979
|
}
|
|
12919
12980
|
const specifiers = [];
|
|
12920
12981
|
for (const decl of sourceFile.getImportDeclarations()) {
|
|
12921
|
-
specifiers.push({
|
|
12982
|
+
specifiers.push({
|
|
12983
|
+
specifier: decl.getModuleSpecifierValue(),
|
|
12984
|
+
line: decl.getStartLineNumber(),
|
|
12985
|
+
typeOnly: decl.isTypeOnly()
|
|
12986
|
+
});
|
|
12922
12987
|
}
|
|
12923
12988
|
for (const reqSpec of extractRequireSpecifiers(sourceFile)) {
|
|
12924
12989
|
specifiers.push(reqSpec);
|
|
12925
12990
|
}
|
|
12926
|
-
for (const { specifier, line } of specifiers) {
|
|
12991
|
+
for (const { specifier, line, typeOnly } of specifiers) {
|
|
12927
12992
|
if (!specifier.startsWith(".") && !specifier.startsWith("/"))
|
|
12928
12993
|
continue;
|
|
12929
12994
|
const sourceDir = filePath.replace(/\/[^/]+$/, "");
|
|
@@ -12940,10 +13005,31 @@ var require_boundary_checker = __commonJS({
|
|
|
12940
13005
|
if (isExcepted(shared_1.ARCHITECTURE_RULES.MODULE_BOUNDARY, filePath, activeExceptions))
|
|
12941
13006
|
continue;
|
|
12942
13007
|
const isFeatureModule = isFeatureModuleArchitecture(policy);
|
|
13008
|
+
const isNestJS = isNestJSFramework(policy);
|
|
12943
13009
|
if (isEntityCrossModuleImport(filePath, resolvedTarget, isFeatureModule))
|
|
12944
13010
|
continue;
|
|
12945
|
-
if (
|
|
13011
|
+
if (isNestJS && isNestJSExemptImport(filePath, resolvedTarget, policy.sharedInfrastructure))
|
|
13012
|
+
continue;
|
|
13013
|
+
if (!isNestJS && isFeatureModule && isNestJSExemptImport(filePath, resolvedTarget, policy.sharedInfrastructure))
|
|
13014
|
+
continue;
|
|
13015
|
+
if (typeOnly) {
|
|
13016
|
+
violations.push({
|
|
13017
|
+
category: "architecture",
|
|
13018
|
+
type: "module-boundary-violation",
|
|
13019
|
+
ruleId: shared_1.ARCHITECTURE_RULES.MODULE_BOUNDARY,
|
|
13020
|
+
severity: "warning",
|
|
13021
|
+
source: "deterministic",
|
|
13022
|
+
confidence: "high",
|
|
13023
|
+
file: filePath,
|
|
13024
|
+
line,
|
|
13025
|
+
module: sourceModule,
|
|
13026
|
+
message: `Cross-module type import: "${sourceModule}" \u2192 "${targetModule}" \u2014 type-only import from '${specifier}' (${filePath})`,
|
|
13027
|
+
explanation: `Type-only import across modules does not create a runtime dependency, but indicates coupling. Consider moving the type to a shared contract.`,
|
|
13028
|
+
debtPoints: 1,
|
|
13029
|
+
gateAction: "warn"
|
|
13030
|
+
});
|
|
12946
13031
|
continue;
|
|
13032
|
+
}
|
|
12947
13033
|
violations.push({
|
|
12948
13034
|
category: "architecture",
|
|
12949
13035
|
type: "module-boundary-violation",
|
|
@@ -12971,7 +13057,7 @@ var require_boundary_checker = __commonJS({
|
|
|
12971
13057
|
const isTargetEntity2 = entityFilePattern.test(targetPath);
|
|
12972
13058
|
return isSourceEntity2 && isTargetEntity2;
|
|
12973
13059
|
}
|
|
12974
|
-
const entityDirPattern = /\/entities
|
|
13060
|
+
const entityDirPattern = /\/(entities?|domain)\//;
|
|
12975
13061
|
const isSourceEntity = entityFilePattern.test(sourcePath) && entityDirPattern.test(sourcePath);
|
|
12976
13062
|
const isTargetEntity = entityFilePattern.test(targetPath) && entityDirPattern.test(targetPath);
|
|
12977
13063
|
return isSourceEntity && isTargetEntity;
|
|
@@ -13051,10 +13137,15 @@ var require_boundary_checker = __commonJS({
|
|
|
13051
13137
|
return void 0;
|
|
13052
13138
|
if (edge.sourceModule === edge.targetModule)
|
|
13053
13139
|
return void 0;
|
|
13140
|
+
if (edge.typeOnly)
|
|
13141
|
+
return void 0;
|
|
13054
13142
|
const isFeatureModule = policy ? isFeatureModuleArchitecture(policy) : false;
|
|
13143
|
+
const isNestJS = policy ? isNestJSFramework(policy) : false;
|
|
13055
13144
|
if (isEntityCrossModuleImport(edge.source, edge.target, isFeatureModule))
|
|
13056
13145
|
return void 0;
|
|
13057
|
-
if (
|
|
13146
|
+
if (isNestJS && isNestJSExemptImport(edge.source, edge.target, policy?.sharedInfrastructure))
|
|
13147
|
+
return void 0;
|
|
13148
|
+
if (!isNestJS && isFeatureModule && isNestJSExemptImport(edge.source, edge.target, policy?.sharedInfrastructure))
|
|
13058
13149
|
return void 0;
|
|
13059
13150
|
const matchingRules = rules.filter((r) => matchesPattern(edge.sourceModule, r.source) && matchesPattern(edge.targetModule, r.target));
|
|
13060
13151
|
if (matchingRules.some((r) => r.type === "allow"))
|
|
@@ -14075,16 +14166,22 @@ var require_runtime_risk_detector = __commonJS({
|
|
|
14075
14166
|
}
|
|
14076
14167
|
if (ts_morph_1.Node.isForStatement(node)) {
|
|
14077
14168
|
const condText = node.getCondition()?.getText() ?? "";
|
|
14169
|
+
const bodyText = body.getText();
|
|
14170
|
+
const hasAwait = /\bawait\b/.test(bodyText);
|
|
14171
|
+
const hasCpuWork = /Math\.|\.sqrt|\.pow|\.random|\.toString|\.charCodeAt|parseInt|parseFloat|\+\+|--|\*|\//.test(bodyText);
|
|
14078
14172
|
const largeBoundMatch = condText.match(/[<]=?\s*(\d+)/);
|
|
14079
14173
|
if (largeBoundMatch) {
|
|
14080
14174
|
const bound = parseInt(largeBoundMatch[1], 10);
|
|
14081
|
-
if (bound >= 1e4) {
|
|
14082
|
-
|
|
14083
|
-
|
|
14084
|
-
|
|
14085
|
-
|
|
14086
|
-
|
|
14087
|
-
|
|
14175
|
+
if (bound >= 1e4 && !hasAwait && hasCpuWork) {
|
|
14176
|
+
violations.push(makeViolation(shared_1.RUNTIME_RISK_RULES.CPU_HEAVY_LOOP_IN_HANDLER, filePath, node.getStartLineNumber(), `CPU-heavy loop (${bound.toLocaleString()} iterations) inside handler '${handler.name}' blocks the event loop`, policy, handler.name, "Move heavy computation to a worker thread or break into chunks with setImmediate"));
|
|
14177
|
+
return;
|
|
14178
|
+
}
|
|
14179
|
+
}
|
|
14180
|
+
if (!largeBoundMatch && !hasAwait && hasCpuWork) {
|
|
14181
|
+
const dynamicBound = condText.match(/[<]=?\s*([a-zA-Z_]\w*)/);
|
|
14182
|
+
if (dynamicBound) {
|
|
14183
|
+
violations.push(makeViolation(shared_1.RUNTIME_RISK_RULES.CPU_HEAVY_LOOP_IN_HANDLER, filePath, node.getStartLineNumber(), `CPU-heavy loop with dynamic bound '${dynamicBound[1]}' inside handler '${handler.name}' \u2014 user-controlled iteration count blocks event loop`, policy, handler.name, "Cap the iteration count, move to a worker thread, or break into chunks with setImmediate"));
|
|
14184
|
+
return;
|
|
14088
14185
|
}
|
|
14089
14186
|
}
|
|
14090
14187
|
}
|
|
@@ -14364,7 +14461,13 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
14364
14461
|
}
|
|
14365
14462
|
if (ts_morph_1.Node.isIdentifier(obj)) {
|
|
14366
14463
|
const name = obj.getText();
|
|
14367
|
-
|
|
14464
|
+
const stripped = name.replace(/Repo(sitory)?|Service|Model|Dao$/i, "");
|
|
14465
|
+
if (stripped && stripped !== name)
|
|
14466
|
+
return stripped.toLowerCase();
|
|
14467
|
+
const sourceFile = callExpr.getSourceFile();
|
|
14468
|
+
const fromVar = extractEntityFromVariable(name, sourceFile);
|
|
14469
|
+
if (fromVar)
|
|
14470
|
+
return fromVar;
|
|
14368
14471
|
}
|
|
14369
14472
|
return void 0;
|
|
14370
14473
|
}
|
|
@@ -14405,6 +14508,56 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
14405
14508
|
}
|
|
14406
14509
|
return void 0;
|
|
14407
14510
|
}
|
|
14511
|
+
function extractEntityAny(callExpr, sourceFile) {
|
|
14512
|
+
let entity = extractEntity(callExpr);
|
|
14513
|
+
if (entity)
|
|
14514
|
+
return entity;
|
|
14515
|
+
entity = extractTypeORMEntity(callExpr);
|
|
14516
|
+
if (entity)
|
|
14517
|
+
return entity;
|
|
14518
|
+
entity = extractSequelizeEntity(callExpr);
|
|
14519
|
+
if (entity)
|
|
14520
|
+
return entity;
|
|
14521
|
+
const expr = callExpr.getExpression();
|
|
14522
|
+
if (ts_morph_1.Node.isPropertyAccessExpression(expr)) {
|
|
14523
|
+
const obj = expr.getExpression();
|
|
14524
|
+
if (ts_morph_1.Node.isIdentifier(obj)) {
|
|
14525
|
+
entity = extractEntityFromVariable(obj.getText(), sourceFile);
|
|
14526
|
+
if (entity)
|
|
14527
|
+
return entity;
|
|
14528
|
+
}
|
|
14529
|
+
if (ts_morph_1.Node.isPropertyAccessExpression(obj)) {
|
|
14530
|
+
const propName = obj.getName();
|
|
14531
|
+
entity = extractEntityFromVariable(propName, sourceFile);
|
|
14532
|
+
if (entity)
|
|
14533
|
+
return entity;
|
|
14534
|
+
const stripped = propName.replace(/Repo(sitory)?$/i, "");
|
|
14535
|
+
if (stripped && stripped !== propName)
|
|
14536
|
+
return stripped.toLowerCase();
|
|
14537
|
+
}
|
|
14538
|
+
}
|
|
14539
|
+
return void 0;
|
|
14540
|
+
}
|
|
14541
|
+
function extractEntityFromVariable(varName, sourceFile) {
|
|
14542
|
+
const fileText = sourceFile.getFullText();
|
|
14543
|
+
const typeAnnotation = new RegExp(`\\b${escapeRegex(varName)}\\s*[:\\!]\\s*Repository\\s*<\\s*(\\w+)\\s*>`);
|
|
14544
|
+
const typeMatch = fileText.match(typeAnnotation);
|
|
14545
|
+
if (typeMatch)
|
|
14546
|
+
return typeMatch[1];
|
|
14547
|
+
const asTypeAssert = new RegExp(`\\b${escapeRegex(varName)}\\s*=\\s*[^;]*\\bas\\s+Repository\\s*<\\s*(\\w+)\\s*>`);
|
|
14548
|
+
const asMatch = fileText.match(asTypeAssert);
|
|
14549
|
+
if (asMatch)
|
|
14550
|
+
return asMatch[1];
|
|
14551
|
+
const getRepoAssign = new RegExp(`\\b${escapeRegex(varName)}\\s*=\\s*getRepository\\s*\\(\\s*(\\w+)\\s*\\)`);
|
|
14552
|
+
const getRepoMatch = fileText.match(getRepoAssign);
|
|
14553
|
+
if (getRepoMatch)
|
|
14554
|
+
return getRepoMatch[1];
|
|
14555
|
+
const connRepoAssign = new RegExp(`\\b${escapeRegex(varName)}\\s*=\\s*\\w+\\.getRepository\\s*\\(\\s*(\\w+)\\s*\\)`);
|
|
14556
|
+
const connMatch = fileText.match(connRepoAssign);
|
|
14557
|
+
if (connMatch)
|
|
14558
|
+
return connMatch[1];
|
|
14559
|
+
return void 0;
|
|
14560
|
+
}
|
|
14408
14561
|
function extractSequelizeEntity(callExpr) {
|
|
14409
14562
|
const expr = callExpr.getExpression();
|
|
14410
14563
|
if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
|
|
@@ -15079,15 +15232,30 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15079
15232
|
const relationsProp = objArg.getProperty("relations");
|
|
15080
15233
|
if (relationsProp) {
|
|
15081
15234
|
const relText = relationsProp.getText();
|
|
15082
|
-
const
|
|
15083
|
-
|
|
15235
|
+
const allRelations = relText.match(/['"][^'"]+['"]/g) ?? [];
|
|
15236
|
+
const dottedRelations = allRelations.filter((m) => m.includes("."));
|
|
15237
|
+
const totalRelCount = allRelations.length;
|
|
15238
|
+
const maxDepth = allRelations.length > 0 ? Math.max(...allRelations.map((m) => m.replace(/['"]/g, "").split(".").length)) : 0;
|
|
15239
|
+
if (dottedRelations.length > 0 || totalRelCount >= 4) {
|
|
15084
15240
|
let entity = extractEntity(node);
|
|
15085
15241
|
if (!entity)
|
|
15086
15242
|
entity = extractTypeORMEntity(node);
|
|
15087
15243
|
const vol = entity ? resolveVolume(entity, policy) : void 0;
|
|
15088
15244
|
const fn = getEnclosingFn(node, fns);
|
|
15089
|
-
|
|
15090
|
-
|
|
15245
|
+
pushIfNotNull(violations, makeViolation(shared_1.PERFORMANCE_RULES.NESTED_INCLUDE_LARGE, filePath, node.getStartLineNumber(), `Deeply nested relations (depth ${maxDepth}, ${totalRelCount} relations)${vol ? ` on '${entity}' (${vol.size} volume)` : ""} \u2014 may cause cartesian explosion`, policy, vol, fn?.name, "Flatten relations or use separate queries to avoid loading excessive data"));
|
|
15246
|
+
}
|
|
15247
|
+
const seqIncludeProp = objArg.getProperty("include");
|
|
15248
|
+
if (seqIncludeProp) {
|
|
15249
|
+
const seqIncText = seqIncludeProp.getText();
|
|
15250
|
+
const nestCount = countNestedIncludes(seqIncText);
|
|
15251
|
+
if (nestCount >= 2) {
|
|
15252
|
+
let entity = extractEntity(node);
|
|
15253
|
+
if (!entity)
|
|
15254
|
+
entity = extractSequelizeEntity(node);
|
|
15255
|
+
const vol = entity ? resolveVolume(entity, policy) : void 0;
|
|
15256
|
+
const fn = getEnclosingFn(node, fns);
|
|
15257
|
+
pushIfNotNull(violations, makeViolation(shared_1.PERFORMANCE_RULES.NESTED_INCLUDE_LARGE, filePath, node.getStartLineNumber(), `Deeply nested include (depth ${nestCount})${vol ? ` on '${entity}' (${vol.size} volume)` : ""} \u2014 may cause cartesian explosion`, policy, vol, fn?.name, "Flatten includes or use separate queries to avoid loading excessive data"));
|
|
15258
|
+
}
|
|
15091
15259
|
}
|
|
15092
15260
|
}
|
|
15093
15261
|
});
|
|
@@ -15134,6 +15302,7 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15134
15302
|
"countDocuments",
|
|
15135
15303
|
"estimatedDocumentCount"
|
|
15136
15304
|
]);
|
|
15305
|
+
var DB_CALL_METHOD_PATTERN_EXTENDED = /^(find|get|fetch|load|search|query|count|list)(By|All|Many|One|First|Where|With)?/i;
|
|
15137
15306
|
function detectNPlusOne(sourceFile, filePath, fns, policy, violations) {
|
|
15138
15307
|
sourceFile.forEachDescendant((node) => {
|
|
15139
15308
|
if (!ts_morph_1.Node.isAwaitExpression(node))
|
|
@@ -15147,7 +15316,7 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15147
15316
|
const callMethodName = callExpr.getName();
|
|
15148
15317
|
let isDrizzleOrKnexChain = false;
|
|
15149
15318
|
let chainEntity;
|
|
15150
|
-
if (!DB_CALL_METHODS.has(callMethodName)) {
|
|
15319
|
+
if (!DB_CALL_METHODS.has(callMethodName) && !DB_CALL_METHOD_PATTERN_EXTENDED.test(callMethodName)) {
|
|
15151
15320
|
const drizzle = traceDrizzleChain(awaited);
|
|
15152
15321
|
if (drizzle.isDrizzle) {
|
|
15153
15322
|
isDrizzleOrKnexChain = true;
|
|
@@ -15235,7 +15404,7 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15235
15404
|
});
|
|
15236
15405
|
});
|
|
15237
15406
|
}
|
|
15238
|
-
var DB_CALL_METHODS_PATTERN = /\.(findMany|findFirst|findUnique|findOne|find|findById|create|update|delete|upsert|count|countDocuments|estimatedDocumentCount|aggregate|query|execute|save|remove|insert|getMany|getOne|findAll|findByPk|findAndCountAll|findOrCreate|destroy|from|select|first|raw)\s*\(/;
|
|
15407
|
+
var DB_CALL_METHODS_PATTERN = /\.(findMany|findFirst|findUnique|findOne|find|findById|findBy\w+|getBy\w+|fetchBy\w+|create|update|delete|upsert|count|countDocuments|estimatedDocumentCount|aggregate|query|execute|save|remove|insert|getMany|getOne|findAll|findByPk|findAndCountAll|findOrCreate|destroy|from|select|first|raw)\s*\(/;
|
|
15239
15408
|
function detectRecursiveNPlusOne(sourceFile, filePath, fns, policy, violations) {
|
|
15240
15409
|
const DB_METHOD_NAMES = /* @__PURE__ */ new Set([
|
|
15241
15410
|
"find",
|
|
@@ -15307,6 +15476,7 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15307
15476
|
}
|
|
15308
15477
|
return false;
|
|
15309
15478
|
}
|
|
15479
|
+
var IN_MEMORY_OPERATIONS = /* @__PURE__ */ new Set(["filter", "sort", "map", "reduce", "flatMap"]);
|
|
15310
15480
|
function detectFetchAllFilterMemory(sourceFile, filePath, fns, policy, violations) {
|
|
15311
15481
|
sourceFile.forEachDescendant((node) => {
|
|
15312
15482
|
if (!ts_morph_1.Node.isCallExpression(node))
|
|
@@ -15314,7 +15484,7 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15314
15484
|
const expr = node.getExpression();
|
|
15315
15485
|
if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
|
|
15316
15486
|
return;
|
|
15317
|
-
if (expr.getName()
|
|
15487
|
+
if (!IN_MEMORY_OPERATIONS.has(expr.getName()))
|
|
15318
15488
|
return;
|
|
15319
15489
|
const obj = expr.getExpression();
|
|
15320
15490
|
if (ts_morph_1.Node.isParenthesizedExpression(obj)) {
|
|
@@ -15342,26 +15512,65 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15342
15512
|
const block = node.getFirstAncestorByKind(ts_morph_1.SyntaxKind.Block);
|
|
15343
15513
|
if (!block)
|
|
15344
15514
|
return;
|
|
15345
|
-
|
|
15346
|
-
|
|
15347
|
-
|
|
15348
|
-
|
|
15349
|
-
|
|
15350
|
-
|
|
15351
|
-
|
|
15352
|
-
|
|
15353
|
-
|
|
15354
|
-
|
|
15355
|
-
|
|
15356
|
-
|
|
15357
|
-
|
|
15358
|
-
|
|
15515
|
+
let foundDbAssignment = false;
|
|
15516
|
+
let entity;
|
|
15517
|
+
block.forEachDescendant((child) => {
|
|
15518
|
+
if (foundDbAssignment)
|
|
15519
|
+
return;
|
|
15520
|
+
if (!ts_morph_1.Node.isVariableDeclaration(child))
|
|
15521
|
+
return;
|
|
15522
|
+
if (child.getName() !== varName)
|
|
15523
|
+
return;
|
|
15524
|
+
if (child.getStart() >= node.getStart())
|
|
15525
|
+
return;
|
|
15526
|
+
const init = child.getInitializer();
|
|
15527
|
+
if (!init)
|
|
15528
|
+
return;
|
|
15529
|
+
let awaitedCall;
|
|
15530
|
+
if (ts_morph_1.Node.isAwaitExpression(init)) {
|
|
15531
|
+
const awaited = init.getExpression();
|
|
15532
|
+
if (ts_morph_1.Node.isCallExpression(awaited))
|
|
15533
|
+
awaitedCall = awaited;
|
|
15534
|
+
} else if (ts_morph_1.Node.isCallExpression(init)) {
|
|
15535
|
+
awaitedCall = init;
|
|
15359
15536
|
}
|
|
15360
|
-
if (!
|
|
15361
|
-
|
|
15362
|
-
|
|
15363
|
-
|
|
15537
|
+
if (!awaitedCall)
|
|
15538
|
+
return;
|
|
15539
|
+
const callExpr = awaitedCall.getExpression();
|
|
15540
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(callExpr))
|
|
15541
|
+
return;
|
|
15542
|
+
const methodName = callExpr.getName();
|
|
15543
|
+
const dbMethods = /* @__PURE__ */ new Set([
|
|
15544
|
+
"findMany",
|
|
15545
|
+
"find",
|
|
15546
|
+
"getMany",
|
|
15547
|
+
"findAll",
|
|
15548
|
+
"findAndCountAll",
|
|
15549
|
+
"from",
|
|
15550
|
+
"select",
|
|
15551
|
+
"getAll",
|
|
15552
|
+
"fetchAll",
|
|
15553
|
+
"findBy",
|
|
15554
|
+
"query"
|
|
15555
|
+
]);
|
|
15556
|
+
if (!dbMethods.has(methodName) && !/^(find|get|fetch|load)(By|All|Many)/.test(methodName))
|
|
15557
|
+
return;
|
|
15558
|
+
foundDbAssignment = true;
|
|
15559
|
+
entity = extractEntityAny(awaitedCall, sourceFile);
|
|
15560
|
+
});
|
|
15561
|
+
if (!foundDbAssignment) {
|
|
15562
|
+
const blockText = block.getText();
|
|
15563
|
+
const nodePos = node.getStart() - block.getStart();
|
|
15564
|
+
const textBefore = blockText.substring(0, nodePos);
|
|
15565
|
+
const assignPattern = new RegExp(`\\b${escapeRegex(varName)}\\s*=\\s*await\\s+[\\w.]+\\.(findMany|find|getMany|findAll|findBy\\w+|getAll|fetchAll|from|select)\\s*\\(`);
|
|
15566
|
+
if (assignPattern.test(textBefore)) {
|
|
15567
|
+
foundDbAssignment = true;
|
|
15568
|
+
const chain = textBefore.match(/(\w+)\.(\w+)\.(findMany|find|getMany|findAll)\s*\(/);
|
|
15569
|
+
if (chain)
|
|
15570
|
+
entity = chain[2];
|
|
15364
15571
|
}
|
|
15572
|
+
}
|
|
15573
|
+
if (foundDbAssignment) {
|
|
15365
15574
|
const vol = entity ? resolveVolume(entity, policy) : void 0;
|
|
15366
15575
|
const fn = getEnclosingFn(node, fns);
|
|
15367
15576
|
pushIfNotNull(violations, makeViolation(shared_1.PERFORMANCE_RULES.FETCH_ALL_FILTER_MEMORY, filePath, node.getStartLineNumber(), `Fetching all '${varName}' then filtering in memory${vol ? ` on '${entity}' (${vol.size} volume)` : ""} \u2014 move filter to where clause`, policy, vol, fn?.name, "Use a where clause in the query instead of .filter() in memory"));
|
|
@@ -15433,6 +15642,11 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15433
15642
|
if (/\.(controller|handler|route|endpoint)\./i.test(fp))
|
|
15434
15643
|
return true;
|
|
15435
15644
|
}
|
|
15645
|
+
if (ts_morph_1.Node.isMethodDeclaration(node)) {
|
|
15646
|
+
const fp = node.getSourceFile().getFilePath();
|
|
15647
|
+
if (/\.(controller|handler)\./i.test(fp))
|
|
15648
|
+
return true;
|
|
15649
|
+
}
|
|
15436
15650
|
return false;
|
|
15437
15651
|
}
|
|
15438
15652
|
function detectMissingPagination(sourceFile, filePath, fns, policy, violations) {
|
|
@@ -15445,7 +15659,17 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15445
15659
|
if (/page|skip|offset|cursor|take|limit|per_page|pageSize/i.test(bodyText))
|
|
15446
15660
|
continue;
|
|
15447
15661
|
const entityMatch = bodyText.match(/(\w+)\.(findMany|find|getMany|findAll)\s*\(/);
|
|
15448
|
-
|
|
15662
|
+
let entity = entityMatch ? entityMatch[1] : void 0;
|
|
15663
|
+
if (entity) {
|
|
15664
|
+
const stripped = entity.replace(/Repo(sitory)?|Service|Model|Dao$/i, "");
|
|
15665
|
+
if (stripped && stripped !== entity)
|
|
15666
|
+
entity = stripped;
|
|
15667
|
+
}
|
|
15668
|
+
if (entity) {
|
|
15669
|
+
const varEntity = extractEntityFromVariable(entity, sourceFile);
|
|
15670
|
+
if (varEntity)
|
|
15671
|
+
entity = varEntity;
|
|
15672
|
+
}
|
|
15449
15673
|
const vol = entity ? resolveVolume(entity, policy) : void 0;
|
|
15450
15674
|
if (vol && vol.size === "S")
|
|
15451
15675
|
continue;
|
|
@@ -15534,6 +15758,13 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15534
15758
|
const args = node.getArguments();
|
|
15535
15759
|
if (args.length > 0 && argsContainKey(node, "where"))
|
|
15536
15760
|
return;
|
|
15761
|
+
{
|
|
15762
|
+
const countObj = expr.getExpression();
|
|
15763
|
+
if (ts_morph_1.Node.isIdentifier(countObj) && /^(em|entityManager|manager|orm)$/i.test(countObj.getText())) {
|
|
15764
|
+
if (args.length > 1)
|
|
15765
|
+
return;
|
|
15766
|
+
}
|
|
15767
|
+
}
|
|
15537
15768
|
let entity = extractEntity(node);
|
|
15538
15769
|
if (!entity)
|
|
15539
15770
|
entity = extractTypeORMEntity(node);
|
|
@@ -15587,7 +15818,7 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15587
15818
|
const args = node.getArguments();
|
|
15588
15819
|
if (args.length === 0)
|
|
15589
15820
|
return;
|
|
15590
|
-
|
|
15821
|
+
let isUnsafe = method === "$queryRawUnsafe" || method === "$executeRawUnsafe";
|
|
15591
15822
|
let sqlText = "";
|
|
15592
15823
|
const firstArg = args[0];
|
|
15593
15824
|
if (ts_morph_1.Node.isStringLiteral(firstArg)) {
|
|
@@ -15596,6 +15827,7 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15596
15827
|
sqlText = firstArg.getText();
|
|
15597
15828
|
} else if (ts_morph_1.Node.isBinaryExpression(firstArg)) {
|
|
15598
15829
|
sqlText = firstArg.getText();
|
|
15830
|
+
isUnsafe = true;
|
|
15599
15831
|
}
|
|
15600
15832
|
if (sqlText) {
|
|
15601
15833
|
checkSqlForLimit(sqlText, filePath, node.getStartLineNumber(), fns, policy, violations, node, isUnsafe);
|
|
@@ -15616,6 +15848,21 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15616
15848
|
debtPoints: policy.scoring.performance_risk_critical,
|
|
15617
15849
|
gateAction: "block"
|
|
15618
15850
|
});
|
|
15851
|
+
violations.push({
|
|
15852
|
+
category: "performance",
|
|
15853
|
+
type: shared_1.PERFORMANCE_RULES.RAW_SQL_UNSAFE,
|
|
15854
|
+
ruleId: shared_1.PERFORMANCE_RULES.RAW_SQL_UNSAFE,
|
|
15855
|
+
severity: "critical",
|
|
15856
|
+
source: "deterministic",
|
|
15857
|
+
confidence: "high",
|
|
15858
|
+
file: filePath,
|
|
15859
|
+
line: node.getStartLineNumber(),
|
|
15860
|
+
function: fn?.name,
|
|
15861
|
+
message: `${method}() used \u2014 SQL injection risk via string interpolation/concatenation`,
|
|
15862
|
+
suggestion: "Use $queryRaw with tagged template literals and parameterized queries instead",
|
|
15863
|
+
debtPoints: policy.scoring.performance_risk_critical,
|
|
15864
|
+
gateAction: "block"
|
|
15865
|
+
});
|
|
15619
15866
|
}
|
|
15620
15867
|
}
|
|
15621
15868
|
if (ts_morph_1.Node.isTaggedTemplateExpression(node)) {
|
|
@@ -15638,15 +15885,15 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15638
15885
|
const fn2 = getEnclosingFn(node, fns);
|
|
15639
15886
|
violations.push({
|
|
15640
15887
|
category: "performance",
|
|
15641
|
-
type: shared_1.PERFORMANCE_RULES.
|
|
15642
|
-
ruleId: shared_1.PERFORMANCE_RULES.
|
|
15888
|
+
type: shared_1.PERFORMANCE_RULES.RAW_SQL_UNSAFE,
|
|
15889
|
+
ruleId: shared_1.PERFORMANCE_RULES.RAW_SQL_UNSAFE,
|
|
15643
15890
|
severity: "critical",
|
|
15644
15891
|
source: "deterministic",
|
|
15645
15892
|
confidence: "high",
|
|
15646
15893
|
file: filePath,
|
|
15647
15894
|
line,
|
|
15648
15895
|
function: fn2?.name,
|
|
15649
|
-
message: `$queryRawUnsafe() used \u2014 SQL injection risk
|
|
15896
|
+
message: `$queryRawUnsafe() used \u2014 SQL injection risk`,
|
|
15650
15897
|
suggestion: "Use $queryRaw with tagged template literals instead of $queryRawUnsafe",
|
|
15651
15898
|
debtPoints: policy.scoring.performance_risk_critical,
|
|
15652
15899
|
gateAction: "block"
|
|
@@ -15664,22 +15911,24 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15664
15911
|
const vol = entity ? resolveVolume(entity, policy) : void 0;
|
|
15665
15912
|
const fn = getEnclosingFn(node, fns);
|
|
15666
15913
|
if (isUnsafe) {
|
|
15667
|
-
const severity = hasLimit ? "warning" : "critical";
|
|
15668
15914
|
violations.push({
|
|
15669
15915
|
category: "performance",
|
|
15670
|
-
type: shared_1.PERFORMANCE_RULES.
|
|
15671
|
-
ruleId: shared_1.PERFORMANCE_RULES.
|
|
15672
|
-
severity,
|
|
15916
|
+
type: shared_1.PERFORMANCE_RULES.RAW_SQL_UNSAFE,
|
|
15917
|
+
ruleId: shared_1.PERFORMANCE_RULES.RAW_SQL_UNSAFE,
|
|
15918
|
+
severity: "critical",
|
|
15673
15919
|
source: "deterministic",
|
|
15674
15920
|
confidence: "high",
|
|
15675
15921
|
file: filePath,
|
|
15676
15922
|
line,
|
|
15677
15923
|
function: fn?.name,
|
|
15678
|
-
message:
|
|
15924
|
+
message: `$queryRawUnsafe() used${vol ? ` on '${entity}'` : ""} \u2014 SQL injection risk`,
|
|
15679
15925
|
suggestion: "Use $queryRaw with tagged template literals instead of $queryRawUnsafe",
|
|
15680
|
-
debtPoints:
|
|
15681
|
-
gateAction:
|
|
15926
|
+
debtPoints: policy.scoring.performance_risk_critical,
|
|
15927
|
+
gateAction: "block"
|
|
15682
15928
|
});
|
|
15929
|
+
if (!hasLimit) {
|
|
15930
|
+
pushIfNotNull(violations, makeViolation(shared_1.PERFORMANCE_RULES.RAW_SQL_NO_LIMIT, filePath, line, `Raw SQL SELECT without LIMIT${vol ? ` on '${entity}' (${vol.size} volume)` : ""} \u2014 may return unbounded results`, policy, vol, fn?.name, "Add LIMIT clause to the SQL query"));
|
|
15931
|
+
}
|
|
15683
15932
|
} else {
|
|
15684
15933
|
pushIfNotNull(violations, makeViolation(shared_1.PERFORMANCE_RULES.RAW_SQL_NO_LIMIT, filePath, line, `Raw SQL SELECT without LIMIT${vol ? ` on '${entity}' (${vol.size} volume)` : ""} \u2014 may return unbounded results`, policy, vol, fn?.name, "Add LIMIT clause to the SQL query"));
|
|
15685
15934
|
}
|
|
@@ -15788,7 +16037,7 @@ var require_reliability_detector = __commonJS({
|
|
|
15788
16037
|
function isNestJSInjectable(classNode) {
|
|
15789
16038
|
return classNode.getDecorators().some((d) => {
|
|
15790
16039
|
const name = d.getName();
|
|
15791
|
-
return name === "Injectable"
|
|
16040
|
+
return name === "Injectable";
|
|
15792
16041
|
});
|
|
15793
16042
|
}
|
|
15794
16043
|
function isInsideNestJSInjectable(node) {
|
|
@@ -16069,6 +16318,12 @@ var require_reliability_detector = __commonJS({
|
|
|
16069
16318
|
violations.push(makeViolation(shared_1.RELIABILITY_RULES.UNHANDLED_PROMISE_REJECTION, filePath, node.getStartLineNumber(), `Fire-and-forget: '${calledName}' called without await \u2014 promise result is silently dropped`, policy, fn?.name, `Add await before the call or handle with .catch()`));
|
|
16070
16319
|
return;
|
|
16071
16320
|
}
|
|
16321
|
+
const ASYNC_METHOD_PATTERNS = /^(fetch|get|send|post|put|patch|delete|process|execute|run|dispatch|emit|publish|notify|sync|upload|download|import|export|create|update|remove|find|search|load|save|handle|trigger)/i;
|
|
16322
|
+
if (ASYNC_METHOD_PATTERNS.test(calledName)) {
|
|
16323
|
+
const fn = getEnclosingFn(node, fns);
|
|
16324
|
+
violations.push(makeViolation(shared_1.RELIABILITY_RULES.UNHANDLED_PROMISE_REJECTION, filePath, node.getStartLineNumber(), `Fire-and-forget: '${calledName}' called without await \u2014 promise result is silently dropped`, policy, fn?.name, `Add await before the call or handle with .catch()`));
|
|
16325
|
+
return;
|
|
16326
|
+
}
|
|
16072
16327
|
} else if (ts_morph_1.Node.isIdentifier(callExpr)) {
|
|
16073
16328
|
calledName = callExpr.getText();
|
|
16074
16329
|
const matchFn = fns.find((f) => f.name === calledName && f.isAsync);
|
|
@@ -16276,6 +16531,7 @@ var require_reliability_detector = __commonJS({
|
|
|
16276
16531
|
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"));
|
|
16277
16532
|
});
|
|
16278
16533
|
}
|
|
16534
|
+
var TRANSACTION_METHOD_NAMES = /* @__PURE__ */ new Set(["$transaction", "transaction"]);
|
|
16279
16535
|
function detectTransactionNoTimeout(sourceFile, filePath, fns, policy, violations) {
|
|
16280
16536
|
sourceFile.forEachDescendant((node) => {
|
|
16281
16537
|
if (!ts_morph_1.Node.isCallExpression(node))
|
|
@@ -16283,7 +16539,7 @@ var require_reliability_detector = __commonJS({
|
|
|
16283
16539
|
const expr = node.getExpression();
|
|
16284
16540
|
if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
|
|
16285
16541
|
return;
|
|
16286
|
-
if (expr.getName()
|
|
16542
|
+
if (!TRANSACTION_METHOD_NAMES.has(expr.getName()))
|
|
16287
16543
|
return;
|
|
16288
16544
|
const args = node.getArguments();
|
|
16289
16545
|
const hasTimeout = args.some((arg) => {
|
|
@@ -16299,6 +16555,8 @@ var require_reliability_detector = __commonJS({
|
|
|
16299
16555
|
});
|
|
16300
16556
|
}
|
|
16301
16557
|
function detectMissingNullGuard(sourceFile, filePath, fns, policy, violations) {
|
|
16558
|
+
detectNonNullAssertionOnNullable(sourceFile, filePath, fns, policy, violations);
|
|
16559
|
+
detectUnsafeArrayIndexAccess(sourceFile, filePath, fns, policy, violations);
|
|
16302
16560
|
sourceFile.forEachDescendant((node) => {
|
|
16303
16561
|
if (!ts_morph_1.Node.isVariableDeclaration(node))
|
|
16304
16562
|
return;
|
|
@@ -16352,6 +16610,62 @@ var require_reliability_detector = __commonJS({
|
|
|
16352
16610
|
}
|
|
16353
16611
|
});
|
|
16354
16612
|
}
|
|
16613
|
+
function detectNonNullAssertionOnNullable(sourceFile, filePath, fns, policy, violations) {
|
|
16614
|
+
sourceFile.forEachDescendant((node) => {
|
|
16615
|
+
if (!ts_morph_1.Node.isNonNullExpression(node))
|
|
16616
|
+
return;
|
|
16617
|
+
const inner = node.getExpression();
|
|
16618
|
+
if (!ts_morph_1.Node.isIdentifier(inner))
|
|
16619
|
+
return;
|
|
16620
|
+
const varName = inner.getText();
|
|
16621
|
+
const block = node.getFirstAncestorByKind(ts_morph_1.SyntaxKind.Block);
|
|
16622
|
+
if (!block)
|
|
16623
|
+
return;
|
|
16624
|
+
const blockText = block.getText();
|
|
16625
|
+
const nodePos = node.getStart() - block.getStart();
|
|
16626
|
+
const textBefore = blockText.substring(0, nodePos);
|
|
16627
|
+
const nullableAssign = new RegExp(`\\b${escapeRegex(varName)}\\s*=\\s*await\\s+\\S+\\.(findUnique|findFirst|findOne|findByPk)\\s*\\(`);
|
|
16628
|
+
if (!nullableAssign.test(textBefore))
|
|
16629
|
+
return;
|
|
16630
|
+
const fn = getEnclosingFn(node, fns);
|
|
16631
|
+
violations.push(makeViolation(shared_1.RELIABILITY_RULES.MISSING_NULL_GUARD, filePath, node.getStartLineNumber(), `Non-null assertion on '${varName}!' after nullable query \u2014 may throw at runtime`, policy, fn?.name, `Add a null guard: if (!${varName}) throw new NotFoundException();`));
|
|
16632
|
+
});
|
|
16633
|
+
}
|
|
16634
|
+
function detectUnsafeArrayIndexAccess(sourceFile, filePath, fns, policy, violations) {
|
|
16635
|
+
sourceFile.forEachDescendant((node) => {
|
|
16636
|
+
if (!ts_morph_1.Node.isElementAccessExpression(node))
|
|
16637
|
+
return;
|
|
16638
|
+
const obj = node.getExpression();
|
|
16639
|
+
if (!ts_morph_1.Node.isIdentifier(obj))
|
|
16640
|
+
return;
|
|
16641
|
+
const varName = obj.getText();
|
|
16642
|
+
const indexArg = node.getArgumentExpression();
|
|
16643
|
+
if (!indexArg || !ts_morph_1.Node.isNumericLiteral(indexArg))
|
|
16644
|
+
return;
|
|
16645
|
+
const parent = node.getParent();
|
|
16646
|
+
if (!parent || !ts_morph_1.Node.isPropertyAccessExpression(parent))
|
|
16647
|
+
return;
|
|
16648
|
+
const block = node.getFirstAncestorByKind(ts_morph_1.SyntaxKind.Block);
|
|
16649
|
+
if (!block)
|
|
16650
|
+
return;
|
|
16651
|
+
const blockText = block.getText();
|
|
16652
|
+
const nodePos = node.getStart() - block.getStart();
|
|
16653
|
+
const textBefore = blockText.substring(0, nodePos);
|
|
16654
|
+
const arrayAssign = new RegExp(`\\b${escapeRegex(varName)}\\s*=\\s*await\\s+\\S+\\.(findMany|find|findAll|getMany|query)\\s*\\(`);
|
|
16655
|
+
if (!arrayAssign.test(textBefore))
|
|
16656
|
+
return;
|
|
16657
|
+
const lengthGuard = new RegExp(`${escapeRegex(varName)}\\s*\\.\\s*length|${escapeRegex(varName)}\\s*\\[\\s*0\\s*\\]\\s*&&|if\\s*\\(\\s*!?${escapeRegex(varName)}`);
|
|
16658
|
+
const assignMatch = textBefore.match(arrayAssign);
|
|
16659
|
+
if (assignMatch) {
|
|
16660
|
+
const assignEnd = textBefore.indexOf(assignMatch[0]) + assignMatch[0].length;
|
|
16661
|
+
const textBetween = textBefore.substring(assignEnd);
|
|
16662
|
+
if (lengthGuard.test(textBetween))
|
|
16663
|
+
return;
|
|
16664
|
+
}
|
|
16665
|
+
const fn = getEnclosingFn(node, fns);
|
|
16666
|
+
violations.push(makeViolation(shared_1.RELIABILITY_RULES.MISSING_NULL_GUARD, filePath, node.getStartLineNumber(), `Array index access '${varName}[${indexArg.getText()}]' without length guard \u2014 may throw on empty results`, policy, fn?.name, `Add a guard: if (${varName}.length === 0) throw new NotFoundException();`));
|
|
16667
|
+
});
|
|
16668
|
+
}
|
|
16355
16669
|
function escapeRegex(str) {
|
|
16356
16670
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
16357
16671
|
}
|
|
@@ -18054,6 +18368,7 @@ var require_orchestrator = __commonJS({
|
|
|
18054
18368
|
crossFileAnalysis = await (0, cross_file_analyzer_1.analyzeCrossFile)(unflaggedPatterns, importGraph, filteredInput, policy);
|
|
18055
18369
|
}
|
|
18056
18370
|
const complexityViolations = complexityDeltasToViolations(complexityDeltas, filteredInput);
|
|
18371
|
+
const largeFileViolations = detectLargeFiles(filteredInput);
|
|
18057
18372
|
const violations = [
|
|
18058
18373
|
...boundaryViolations,
|
|
18059
18374
|
...circularViolations,
|
|
@@ -18061,6 +18376,7 @@ var require_orchestrator = __commonJS({
|
|
|
18061
18376
|
...perfViolations,
|
|
18062
18377
|
...reliabilityViolations,
|
|
18063
18378
|
...complexityViolations,
|
|
18379
|
+
...largeFileViolations,
|
|
18064
18380
|
...duplicationResult.violations,
|
|
18065
18381
|
...missingTestsResult?.violations ?? [],
|
|
18066
18382
|
...deadCodeResult.violations,
|
|
@@ -18078,6 +18394,34 @@ var require_orchestrator = __commonJS({
|
|
|
18078
18394
|
crossFileAnalysis
|
|
18079
18395
|
};
|
|
18080
18396
|
}
|
|
18397
|
+
function detectLargeFiles(input) {
|
|
18398
|
+
const threshold = shared_1.DEFAULT_LARGE_FILE_THRESHOLD;
|
|
18399
|
+
const violations = [];
|
|
18400
|
+
for (const file of input.changedFiles) {
|
|
18401
|
+
if (file.status === "deleted")
|
|
18402
|
+
continue;
|
|
18403
|
+
if (!/\.(ts|tsx|js|jsx)$/.test(file.path))
|
|
18404
|
+
continue;
|
|
18405
|
+
const lineCount = file.content.split("\n").length;
|
|
18406
|
+
if (lineCount <= threshold)
|
|
18407
|
+
continue;
|
|
18408
|
+
violations.push({
|
|
18409
|
+
category: "maintainability",
|
|
18410
|
+
type: shared_1.MAINTAINABILITY_RULES.LARGE_FILE,
|
|
18411
|
+
ruleId: shared_1.MAINTAINABILITY_RULES.LARGE_FILE,
|
|
18412
|
+
severity: "warning",
|
|
18413
|
+
source: "deterministic",
|
|
18414
|
+
confidence: "high",
|
|
18415
|
+
file: file.path,
|
|
18416
|
+
line: 1,
|
|
18417
|
+
message: `File has ${lineCount} lines (threshold: ${threshold}) \u2014 consider splitting into smaller modules`,
|
|
18418
|
+
suggestion: "Extract helper functions, types, or constants into separate files",
|
|
18419
|
+
debtPoints: Math.floor((lineCount - threshold) / 50) + 1,
|
|
18420
|
+
gateAction: "warn"
|
|
18421
|
+
});
|
|
18422
|
+
}
|
|
18423
|
+
return violations;
|
|
18424
|
+
}
|
|
18081
18425
|
function complexityDeltasToViolations(deltas, input) {
|
|
18082
18426
|
const threshold = shared_1.DEFAULT_COMPLEXITY_THRESHOLD;
|
|
18083
18427
|
const violations = [];
|