technical-debt-radar 1.0.2 → 1.0.4
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 +493 -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
|
});
|
|
@@ -12896,6 +12957,95 @@ var require_boundary_checker = __commonJS({
|
|
|
12896
12957
|
}
|
|
12897
12958
|
return violations;
|
|
12898
12959
|
}
|
|
12960
|
+
function isInConstructorShorthandParam(node) {
|
|
12961
|
+
let current = node;
|
|
12962
|
+
while (current) {
|
|
12963
|
+
if (current.getKind() === ts_morph_1.SyntaxKind.Parameter) {
|
|
12964
|
+
const paramText = current.getText();
|
|
12965
|
+
if (/^\s*(private|protected|public|readonly)\s/.test(paramText)) {
|
|
12966
|
+
const paramParent = current.getParent();
|
|
12967
|
+
if (paramParent && paramParent.getKind() === ts_morph_1.SyntaxKind.Constructor) {
|
|
12968
|
+
return true;
|
|
12969
|
+
}
|
|
12970
|
+
}
|
|
12971
|
+
return false;
|
|
12972
|
+
}
|
|
12973
|
+
current = current.getParent();
|
|
12974
|
+
}
|
|
12975
|
+
return false;
|
|
12976
|
+
}
|
|
12977
|
+
function isTypePosition(node) {
|
|
12978
|
+
if (isInConstructorShorthandParam(node))
|
|
12979
|
+
return false;
|
|
12980
|
+
const parent = node.getParent();
|
|
12981
|
+
if (!parent)
|
|
12982
|
+
return false;
|
|
12983
|
+
const parentKind = parent.getKind();
|
|
12984
|
+
if (parentKind === ts_morph_1.SyntaxKind.TypeReference || parentKind === ts_morph_1.SyntaxKind.TypeQuery || parentKind === ts_morph_1.SyntaxKind.TypeAliasDeclaration || parentKind === ts_morph_1.SyntaxKind.InterfaceDeclaration || parentKind === ts_morph_1.SyntaxKind.AsExpression || parentKind === ts_morph_1.SyntaxKind.TypeAssertionExpression || parentKind === ts_morph_1.SyntaxKind.ExpressionWithTypeArguments || parentKind === ts_morph_1.SyntaxKind.MappedType || parentKind === ts_morph_1.SyntaxKind.ConditionalType || parentKind === ts_morph_1.SyntaxKind.IntersectionType || parentKind === ts_morph_1.SyntaxKind.UnionType || parentKind === ts_morph_1.SyntaxKind.TupleType || parentKind === ts_morph_1.SyntaxKind.ArrayType || parentKind === ts_morph_1.SyntaxKind.IndexedAccessType || parentKind === ts_morph_1.SyntaxKind.TypeOperator || parentKind === ts_morph_1.SyntaxKind.ParenthesizedType) {
|
|
12985
|
+
return true;
|
|
12986
|
+
}
|
|
12987
|
+
if (parentKind === ts_morph_1.SyntaxKind.Parameter || parentKind === ts_morph_1.SyntaxKind.PropertyDeclaration || parentKind === ts_morph_1.SyntaxKind.PropertySignature || parentKind === ts_morph_1.SyntaxKind.VariableDeclaration || parentKind === ts_morph_1.SyntaxKind.FunctionDeclaration || parentKind === ts_morph_1.SyntaxKind.MethodDeclaration || parentKind === ts_morph_1.SyntaxKind.MethodSignature || parentKind === ts_morph_1.SyntaxKind.ArrowFunction || parentKind === ts_morph_1.SyntaxKind.GetAccessor || parentKind === ts_morph_1.SyntaxKind.SetAccessor) {
|
|
12988
|
+
const colonToken = parent.getFirstChildByKind(ts_morph_1.SyntaxKind.ColonToken);
|
|
12989
|
+
if (colonToken && node.getPos() > colonToken.getPos()) {
|
|
12990
|
+
const equalsToken = parent.getFirstChildByKind(ts_morph_1.SyntaxKind.EqualsToken);
|
|
12991
|
+
if (!equalsToken || node.getPos() < equalsToken.getPos()) {
|
|
12992
|
+
return true;
|
|
12993
|
+
}
|
|
12994
|
+
}
|
|
12995
|
+
}
|
|
12996
|
+
if (parentKind === ts_morph_1.SyntaxKind.HeritageClause) {
|
|
12997
|
+
return true;
|
|
12998
|
+
}
|
|
12999
|
+
if (parentKind === ts_morph_1.SyntaxKind.QualifiedName) {
|
|
13000
|
+
const grandParent = parent.getParent();
|
|
13001
|
+
if (grandParent && grandParent.getKind() === ts_morph_1.SyntaxKind.TypeReference) {
|
|
13002
|
+
return true;
|
|
13003
|
+
}
|
|
13004
|
+
}
|
|
13005
|
+
return false;
|
|
13006
|
+
}
|
|
13007
|
+
function isImplicitlyTypeOnly(decl, sourceFile) {
|
|
13008
|
+
if (decl.isTypeOnly())
|
|
13009
|
+
return false;
|
|
13010
|
+
if (decl.getNamespaceImport())
|
|
13011
|
+
return false;
|
|
13012
|
+
const defaultImport = decl.getDefaultImport();
|
|
13013
|
+
const namedImports = decl.getNamedImports();
|
|
13014
|
+
if (!defaultImport && namedImports.length === 0)
|
|
13015
|
+
return false;
|
|
13016
|
+
const importedNames = [];
|
|
13017
|
+
if (defaultImport) {
|
|
13018
|
+
importedNames.push(defaultImport.getText());
|
|
13019
|
+
}
|
|
13020
|
+
for (const named of namedImports) {
|
|
13021
|
+
if (named.isTypeOnly())
|
|
13022
|
+
continue;
|
|
13023
|
+
importedNames.push(named.getName());
|
|
13024
|
+
}
|
|
13025
|
+
if (importedNames.length === 0)
|
|
13026
|
+
return true;
|
|
13027
|
+
for (const name of importedNames) {
|
|
13028
|
+
const identifiers = sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.Identifier).filter((id) => id.getText() === name);
|
|
13029
|
+
const usages = identifiers.filter((id) => {
|
|
13030
|
+
const parent = id.getParent();
|
|
13031
|
+
if (!parent)
|
|
13032
|
+
return false;
|
|
13033
|
+
const parentKind = parent.getKind();
|
|
13034
|
+
if (parentKind === ts_morph_1.SyntaxKind.ImportSpecifier || parentKind === ts_morph_1.SyntaxKind.ImportClause) {
|
|
13035
|
+
return false;
|
|
13036
|
+
}
|
|
13037
|
+
return true;
|
|
13038
|
+
});
|
|
13039
|
+
if (usages.length === 0)
|
|
13040
|
+
return false;
|
|
13041
|
+
for (const usage of usages) {
|
|
13042
|
+
if (!isTypePosition(usage)) {
|
|
13043
|
+
return false;
|
|
13044
|
+
}
|
|
13045
|
+
}
|
|
13046
|
+
}
|
|
13047
|
+
return true;
|
|
13048
|
+
}
|
|
12899
13049
|
function checkCrossModuleDirectImports(input, policy, activeExceptions) {
|
|
12900
13050
|
const violations = [];
|
|
12901
13051
|
const hasCrossModuleDeny = policy.rules.some((r) => r.type === "deny" && /cross-module/i.test(r.description ?? "") || /cross-module/i.test(r.source));
|
|
@@ -12918,12 +13068,18 @@ var require_boundary_checker = __commonJS({
|
|
|
12918
13068
|
}
|
|
12919
13069
|
const specifiers = [];
|
|
12920
13070
|
for (const decl of sourceFile.getImportDeclarations()) {
|
|
12921
|
-
|
|
13071
|
+
const explicitTypeOnly = decl.isTypeOnly();
|
|
13072
|
+
const implicitTypeOnly = !explicitTypeOnly && isImplicitlyTypeOnly(decl, sourceFile);
|
|
13073
|
+
specifiers.push({
|
|
13074
|
+
specifier: decl.getModuleSpecifierValue(),
|
|
13075
|
+
line: decl.getStartLineNumber(),
|
|
13076
|
+
typeOnly: explicitTypeOnly || implicitTypeOnly
|
|
13077
|
+
});
|
|
12922
13078
|
}
|
|
12923
13079
|
for (const reqSpec of extractRequireSpecifiers(sourceFile)) {
|
|
12924
13080
|
specifiers.push(reqSpec);
|
|
12925
13081
|
}
|
|
12926
|
-
for (const { specifier, line } of specifiers) {
|
|
13082
|
+
for (const { specifier, line, typeOnly } of specifiers) {
|
|
12927
13083
|
if (!specifier.startsWith(".") && !specifier.startsWith("/"))
|
|
12928
13084
|
continue;
|
|
12929
13085
|
const sourceDir = filePath.replace(/\/[^/]+$/, "");
|
|
@@ -12940,9 +13096,14 @@ var require_boundary_checker = __commonJS({
|
|
|
12940
13096
|
if (isExcepted(shared_1.ARCHITECTURE_RULES.MODULE_BOUNDARY, filePath, activeExceptions))
|
|
12941
13097
|
continue;
|
|
12942
13098
|
const isFeatureModule = isFeatureModuleArchitecture(policy);
|
|
13099
|
+
const isNestJS = isNestJSFramework(policy);
|
|
12943
13100
|
if (isEntityCrossModuleImport(filePath, resolvedTarget, isFeatureModule))
|
|
12944
13101
|
continue;
|
|
12945
|
-
if (
|
|
13102
|
+
if (isNestJS && isNestJSExemptImport(filePath, resolvedTarget, policy.sharedInfrastructure))
|
|
13103
|
+
continue;
|
|
13104
|
+
if (!isNestJS && isFeatureModule && isNestJSExemptImport(filePath, resolvedTarget, policy.sharedInfrastructure))
|
|
13105
|
+
continue;
|
|
13106
|
+
if (typeOnly)
|
|
12946
13107
|
continue;
|
|
12947
13108
|
violations.push({
|
|
12948
13109
|
category: "architecture",
|
|
@@ -12971,7 +13132,7 @@ var require_boundary_checker = __commonJS({
|
|
|
12971
13132
|
const isTargetEntity2 = entityFilePattern.test(targetPath);
|
|
12972
13133
|
return isSourceEntity2 && isTargetEntity2;
|
|
12973
13134
|
}
|
|
12974
|
-
const entityDirPattern = /\/entities
|
|
13135
|
+
const entityDirPattern = /\/(entities?|domain)\//;
|
|
12975
13136
|
const isSourceEntity = entityFilePattern.test(sourcePath) && entityDirPattern.test(sourcePath);
|
|
12976
13137
|
const isTargetEntity = entityFilePattern.test(targetPath) && entityDirPattern.test(targetPath);
|
|
12977
13138
|
return isSourceEntity && isTargetEntity;
|
|
@@ -13051,10 +13212,15 @@ var require_boundary_checker = __commonJS({
|
|
|
13051
13212
|
return void 0;
|
|
13052
13213
|
if (edge.sourceModule === edge.targetModule)
|
|
13053
13214
|
return void 0;
|
|
13215
|
+
if (edge.typeOnly)
|
|
13216
|
+
return void 0;
|
|
13054
13217
|
const isFeatureModule = policy ? isFeatureModuleArchitecture(policy) : false;
|
|
13218
|
+
const isNestJS = policy ? isNestJSFramework(policy) : false;
|
|
13055
13219
|
if (isEntityCrossModuleImport(edge.source, edge.target, isFeatureModule))
|
|
13056
13220
|
return void 0;
|
|
13057
|
-
if (
|
|
13221
|
+
if (isNestJS && isNestJSExemptImport(edge.source, edge.target, policy?.sharedInfrastructure))
|
|
13222
|
+
return void 0;
|
|
13223
|
+
if (!isNestJS && isFeatureModule && isNestJSExemptImport(edge.source, edge.target, policy?.sharedInfrastructure))
|
|
13058
13224
|
return void 0;
|
|
13059
13225
|
const matchingRules = rules.filter((r) => matchesPattern(edge.sourceModule, r.source) && matchesPattern(edge.targetModule, r.target));
|
|
13060
13226
|
if (matchingRules.some((r) => r.type === "allow"))
|
|
@@ -14075,16 +14241,22 @@ var require_runtime_risk_detector = __commonJS({
|
|
|
14075
14241
|
}
|
|
14076
14242
|
if (ts_morph_1.Node.isForStatement(node)) {
|
|
14077
14243
|
const condText = node.getCondition()?.getText() ?? "";
|
|
14244
|
+
const bodyText = body.getText();
|
|
14245
|
+
const hasAwait = /\bawait\b/.test(bodyText);
|
|
14246
|
+
const hasCpuWork = /Math\.|\.sqrt|\.pow|\.random|\.toString|\.charCodeAt|parseInt|parseFloat|\+\+|--|\*|\//.test(bodyText);
|
|
14078
14247
|
const largeBoundMatch = condText.match(/[<]=?\s*(\d+)/);
|
|
14079
14248
|
if (largeBoundMatch) {
|
|
14080
14249
|
const bound = parseInt(largeBoundMatch[1], 10);
|
|
14081
|
-
if (bound >= 1e4) {
|
|
14082
|
-
|
|
14083
|
-
|
|
14084
|
-
|
|
14085
|
-
|
|
14086
|
-
|
|
14087
|
-
|
|
14250
|
+
if (bound >= 1e4 && !hasAwait && hasCpuWork) {
|
|
14251
|
+
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"));
|
|
14252
|
+
return;
|
|
14253
|
+
}
|
|
14254
|
+
}
|
|
14255
|
+
if (!largeBoundMatch && !hasAwait && hasCpuWork) {
|
|
14256
|
+
const dynamicBound = condText.match(/[<]=?\s*([a-zA-Z_]\w*)/);
|
|
14257
|
+
if (dynamicBound) {
|
|
14258
|
+
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"));
|
|
14259
|
+
return;
|
|
14088
14260
|
}
|
|
14089
14261
|
}
|
|
14090
14262
|
}
|
|
@@ -14364,7 +14536,13 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
14364
14536
|
}
|
|
14365
14537
|
if (ts_morph_1.Node.isIdentifier(obj)) {
|
|
14366
14538
|
const name = obj.getText();
|
|
14367
|
-
|
|
14539
|
+
const stripped = name.replace(/Repo(sitory)?|Service|Model|Dao$/i, "");
|
|
14540
|
+
if (stripped && stripped !== name)
|
|
14541
|
+
return stripped.toLowerCase();
|
|
14542
|
+
const sourceFile = callExpr.getSourceFile();
|
|
14543
|
+
const fromVar = extractEntityFromVariable(name, sourceFile);
|
|
14544
|
+
if (fromVar)
|
|
14545
|
+
return fromVar;
|
|
14368
14546
|
}
|
|
14369
14547
|
return void 0;
|
|
14370
14548
|
}
|
|
@@ -14405,6 +14583,56 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
14405
14583
|
}
|
|
14406
14584
|
return void 0;
|
|
14407
14585
|
}
|
|
14586
|
+
function extractEntityAny(callExpr, sourceFile) {
|
|
14587
|
+
let entity = extractEntity(callExpr);
|
|
14588
|
+
if (entity)
|
|
14589
|
+
return entity;
|
|
14590
|
+
entity = extractTypeORMEntity(callExpr);
|
|
14591
|
+
if (entity)
|
|
14592
|
+
return entity;
|
|
14593
|
+
entity = extractSequelizeEntity(callExpr);
|
|
14594
|
+
if (entity)
|
|
14595
|
+
return entity;
|
|
14596
|
+
const expr = callExpr.getExpression();
|
|
14597
|
+
if (ts_morph_1.Node.isPropertyAccessExpression(expr)) {
|
|
14598
|
+
const obj = expr.getExpression();
|
|
14599
|
+
if (ts_morph_1.Node.isIdentifier(obj)) {
|
|
14600
|
+
entity = extractEntityFromVariable(obj.getText(), sourceFile);
|
|
14601
|
+
if (entity)
|
|
14602
|
+
return entity;
|
|
14603
|
+
}
|
|
14604
|
+
if (ts_morph_1.Node.isPropertyAccessExpression(obj)) {
|
|
14605
|
+
const propName = obj.getName();
|
|
14606
|
+
entity = extractEntityFromVariable(propName, sourceFile);
|
|
14607
|
+
if (entity)
|
|
14608
|
+
return entity;
|
|
14609
|
+
const stripped = propName.replace(/Repo(sitory)?$/i, "");
|
|
14610
|
+
if (stripped && stripped !== propName)
|
|
14611
|
+
return stripped.toLowerCase();
|
|
14612
|
+
}
|
|
14613
|
+
}
|
|
14614
|
+
return void 0;
|
|
14615
|
+
}
|
|
14616
|
+
function extractEntityFromVariable(varName, sourceFile) {
|
|
14617
|
+
const fileText = sourceFile.getFullText();
|
|
14618
|
+
const typeAnnotation = new RegExp(`\\b${escapeRegex(varName)}\\s*[:\\!]\\s*Repository\\s*<\\s*(\\w+)\\s*>`);
|
|
14619
|
+
const typeMatch = fileText.match(typeAnnotation);
|
|
14620
|
+
if (typeMatch)
|
|
14621
|
+
return typeMatch[1];
|
|
14622
|
+
const asTypeAssert = new RegExp(`\\b${escapeRegex(varName)}\\s*=\\s*[^;]*\\bas\\s+Repository\\s*<\\s*(\\w+)\\s*>`);
|
|
14623
|
+
const asMatch = fileText.match(asTypeAssert);
|
|
14624
|
+
if (asMatch)
|
|
14625
|
+
return asMatch[1];
|
|
14626
|
+
const getRepoAssign = new RegExp(`\\b${escapeRegex(varName)}\\s*=\\s*getRepository\\s*\\(\\s*(\\w+)\\s*\\)`);
|
|
14627
|
+
const getRepoMatch = fileText.match(getRepoAssign);
|
|
14628
|
+
if (getRepoMatch)
|
|
14629
|
+
return getRepoMatch[1];
|
|
14630
|
+
const connRepoAssign = new RegExp(`\\b${escapeRegex(varName)}\\s*=\\s*\\w+\\.getRepository\\s*\\(\\s*(\\w+)\\s*\\)`);
|
|
14631
|
+
const connMatch = fileText.match(connRepoAssign);
|
|
14632
|
+
if (connMatch)
|
|
14633
|
+
return connMatch[1];
|
|
14634
|
+
return void 0;
|
|
14635
|
+
}
|
|
14408
14636
|
function extractSequelizeEntity(callExpr) {
|
|
14409
14637
|
const expr = callExpr.getExpression();
|
|
14410
14638
|
if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
|
|
@@ -15079,15 +15307,30 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15079
15307
|
const relationsProp = objArg.getProperty("relations");
|
|
15080
15308
|
if (relationsProp) {
|
|
15081
15309
|
const relText = relationsProp.getText();
|
|
15082
|
-
const
|
|
15083
|
-
|
|
15310
|
+
const allRelations = relText.match(/['"][^'"]+['"]/g) ?? [];
|
|
15311
|
+
const dottedRelations = allRelations.filter((m) => m.includes("."));
|
|
15312
|
+
const totalRelCount = allRelations.length;
|
|
15313
|
+
const maxDepth = allRelations.length > 0 ? Math.max(...allRelations.map((m) => m.replace(/['"]/g, "").split(".").length)) : 0;
|
|
15314
|
+
if (dottedRelations.length > 0 || totalRelCount >= 4) {
|
|
15084
15315
|
let entity = extractEntity(node);
|
|
15085
15316
|
if (!entity)
|
|
15086
15317
|
entity = extractTypeORMEntity(node);
|
|
15087
15318
|
const vol = entity ? resolveVolume(entity, policy) : void 0;
|
|
15088
15319
|
const fn = getEnclosingFn(node, fns);
|
|
15089
|
-
|
|
15090
|
-
|
|
15320
|
+
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"));
|
|
15321
|
+
}
|
|
15322
|
+
const seqIncludeProp = objArg.getProperty("include");
|
|
15323
|
+
if (seqIncludeProp) {
|
|
15324
|
+
const seqIncText = seqIncludeProp.getText();
|
|
15325
|
+
const nestCount = countNestedIncludes(seqIncText);
|
|
15326
|
+
if (nestCount >= 2) {
|
|
15327
|
+
let entity = extractEntity(node);
|
|
15328
|
+
if (!entity)
|
|
15329
|
+
entity = extractSequelizeEntity(node);
|
|
15330
|
+
const vol = entity ? resolveVolume(entity, policy) : void 0;
|
|
15331
|
+
const fn = getEnclosingFn(node, fns);
|
|
15332
|
+
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"));
|
|
15333
|
+
}
|
|
15091
15334
|
}
|
|
15092
15335
|
}
|
|
15093
15336
|
});
|
|
@@ -15134,6 +15377,7 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15134
15377
|
"countDocuments",
|
|
15135
15378
|
"estimatedDocumentCount"
|
|
15136
15379
|
]);
|
|
15380
|
+
var DB_CALL_METHOD_PATTERN_EXTENDED = /^(find|get|fetch|load|search|query|count|list)(By|All|Many|One|First|Where|With)?/i;
|
|
15137
15381
|
function detectNPlusOne(sourceFile, filePath, fns, policy, violations) {
|
|
15138
15382
|
sourceFile.forEachDescendant((node) => {
|
|
15139
15383
|
if (!ts_morph_1.Node.isAwaitExpression(node))
|
|
@@ -15147,7 +15391,7 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15147
15391
|
const callMethodName = callExpr.getName();
|
|
15148
15392
|
let isDrizzleOrKnexChain = false;
|
|
15149
15393
|
let chainEntity;
|
|
15150
|
-
if (!DB_CALL_METHODS.has(callMethodName)) {
|
|
15394
|
+
if (!DB_CALL_METHODS.has(callMethodName) && !DB_CALL_METHOD_PATTERN_EXTENDED.test(callMethodName)) {
|
|
15151
15395
|
const drizzle = traceDrizzleChain(awaited);
|
|
15152
15396
|
if (drizzle.isDrizzle) {
|
|
15153
15397
|
isDrizzleOrKnexChain = true;
|
|
@@ -15235,7 +15479,7 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15235
15479
|
});
|
|
15236
15480
|
});
|
|
15237
15481
|
}
|
|
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*\(/;
|
|
15482
|
+
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
15483
|
function detectRecursiveNPlusOne(sourceFile, filePath, fns, policy, violations) {
|
|
15240
15484
|
const DB_METHOD_NAMES = /* @__PURE__ */ new Set([
|
|
15241
15485
|
"find",
|
|
@@ -15307,6 +15551,7 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15307
15551
|
}
|
|
15308
15552
|
return false;
|
|
15309
15553
|
}
|
|
15554
|
+
var IN_MEMORY_OPERATIONS = /* @__PURE__ */ new Set(["filter", "sort", "map", "reduce", "flatMap"]);
|
|
15310
15555
|
function detectFetchAllFilterMemory(sourceFile, filePath, fns, policy, violations) {
|
|
15311
15556
|
sourceFile.forEachDescendant((node) => {
|
|
15312
15557
|
if (!ts_morph_1.Node.isCallExpression(node))
|
|
@@ -15314,7 +15559,7 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15314
15559
|
const expr = node.getExpression();
|
|
15315
15560
|
if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
|
|
15316
15561
|
return;
|
|
15317
|
-
if (expr.getName()
|
|
15562
|
+
if (!IN_MEMORY_OPERATIONS.has(expr.getName()))
|
|
15318
15563
|
return;
|
|
15319
15564
|
const obj = expr.getExpression();
|
|
15320
15565
|
if (ts_morph_1.Node.isParenthesizedExpression(obj)) {
|
|
@@ -15342,26 +15587,65 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15342
15587
|
const block = node.getFirstAncestorByKind(ts_morph_1.SyntaxKind.Block);
|
|
15343
15588
|
if (!block)
|
|
15344
15589
|
return;
|
|
15345
|
-
|
|
15346
|
-
|
|
15347
|
-
|
|
15348
|
-
|
|
15349
|
-
|
|
15350
|
-
|
|
15351
|
-
|
|
15352
|
-
|
|
15353
|
-
|
|
15354
|
-
|
|
15355
|
-
|
|
15356
|
-
|
|
15357
|
-
|
|
15358
|
-
|
|
15590
|
+
let foundDbAssignment = false;
|
|
15591
|
+
let entity;
|
|
15592
|
+
block.forEachDescendant((child) => {
|
|
15593
|
+
if (foundDbAssignment)
|
|
15594
|
+
return;
|
|
15595
|
+
if (!ts_morph_1.Node.isVariableDeclaration(child))
|
|
15596
|
+
return;
|
|
15597
|
+
if (child.getName() !== varName)
|
|
15598
|
+
return;
|
|
15599
|
+
if (child.getStart() >= node.getStart())
|
|
15600
|
+
return;
|
|
15601
|
+
const init = child.getInitializer();
|
|
15602
|
+
if (!init)
|
|
15603
|
+
return;
|
|
15604
|
+
let awaitedCall;
|
|
15605
|
+
if (ts_morph_1.Node.isAwaitExpression(init)) {
|
|
15606
|
+
const awaited = init.getExpression();
|
|
15607
|
+
if (ts_morph_1.Node.isCallExpression(awaited))
|
|
15608
|
+
awaitedCall = awaited;
|
|
15609
|
+
} else if (ts_morph_1.Node.isCallExpression(init)) {
|
|
15610
|
+
awaitedCall = init;
|
|
15359
15611
|
}
|
|
15360
|
-
if (!
|
|
15361
|
-
|
|
15362
|
-
|
|
15363
|
-
|
|
15612
|
+
if (!awaitedCall)
|
|
15613
|
+
return;
|
|
15614
|
+
const callExpr = awaitedCall.getExpression();
|
|
15615
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(callExpr))
|
|
15616
|
+
return;
|
|
15617
|
+
const methodName = callExpr.getName();
|
|
15618
|
+
const dbMethods = /* @__PURE__ */ new Set([
|
|
15619
|
+
"findMany",
|
|
15620
|
+
"find",
|
|
15621
|
+
"getMany",
|
|
15622
|
+
"findAll",
|
|
15623
|
+
"findAndCountAll",
|
|
15624
|
+
"from",
|
|
15625
|
+
"select",
|
|
15626
|
+
"getAll",
|
|
15627
|
+
"fetchAll",
|
|
15628
|
+
"findBy",
|
|
15629
|
+
"query"
|
|
15630
|
+
]);
|
|
15631
|
+
if (!dbMethods.has(methodName) && !/^(find|get|fetch|load)(By|All|Many)/.test(methodName))
|
|
15632
|
+
return;
|
|
15633
|
+
foundDbAssignment = true;
|
|
15634
|
+
entity = extractEntityAny(awaitedCall, sourceFile);
|
|
15635
|
+
});
|
|
15636
|
+
if (!foundDbAssignment) {
|
|
15637
|
+
const blockText = block.getText();
|
|
15638
|
+
const nodePos = node.getStart() - block.getStart();
|
|
15639
|
+
const textBefore = blockText.substring(0, nodePos);
|
|
15640
|
+
const assignPattern = new RegExp(`\\b${escapeRegex(varName)}\\s*=\\s*await\\s+[\\w.]+\\.(findMany|find|getMany|findAll|findBy\\w+|getAll|fetchAll|from|select)\\s*\\(`);
|
|
15641
|
+
if (assignPattern.test(textBefore)) {
|
|
15642
|
+
foundDbAssignment = true;
|
|
15643
|
+
const chain = textBefore.match(/(\w+)\.(\w+)\.(findMany|find|getMany|findAll)\s*\(/);
|
|
15644
|
+
if (chain)
|
|
15645
|
+
entity = chain[2];
|
|
15364
15646
|
}
|
|
15647
|
+
}
|
|
15648
|
+
if (foundDbAssignment) {
|
|
15365
15649
|
const vol = entity ? resolveVolume(entity, policy) : void 0;
|
|
15366
15650
|
const fn = getEnclosingFn(node, fns);
|
|
15367
15651
|
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 +15717,11 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15433
15717
|
if (/\.(controller|handler|route|endpoint)\./i.test(fp))
|
|
15434
15718
|
return true;
|
|
15435
15719
|
}
|
|
15720
|
+
if (ts_morph_1.Node.isMethodDeclaration(node)) {
|
|
15721
|
+
const fp = node.getSourceFile().getFilePath();
|
|
15722
|
+
if (/\.(controller|handler)\./i.test(fp))
|
|
15723
|
+
return true;
|
|
15724
|
+
}
|
|
15436
15725
|
return false;
|
|
15437
15726
|
}
|
|
15438
15727
|
function detectMissingPagination(sourceFile, filePath, fns, policy, violations) {
|
|
@@ -15445,7 +15734,17 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15445
15734
|
if (/page|skip|offset|cursor|take|limit|per_page|pageSize/i.test(bodyText))
|
|
15446
15735
|
continue;
|
|
15447
15736
|
const entityMatch = bodyText.match(/(\w+)\.(findMany|find|getMany|findAll)\s*\(/);
|
|
15448
|
-
|
|
15737
|
+
let entity = entityMatch ? entityMatch[1] : void 0;
|
|
15738
|
+
if (entity) {
|
|
15739
|
+
const stripped = entity.replace(/Repo(sitory)?|Service|Model|Dao$/i, "");
|
|
15740
|
+
if (stripped && stripped !== entity)
|
|
15741
|
+
entity = stripped;
|
|
15742
|
+
}
|
|
15743
|
+
if (entity) {
|
|
15744
|
+
const varEntity = extractEntityFromVariable(entity, sourceFile);
|
|
15745
|
+
if (varEntity)
|
|
15746
|
+
entity = varEntity;
|
|
15747
|
+
}
|
|
15449
15748
|
const vol = entity ? resolveVolume(entity, policy) : void 0;
|
|
15450
15749
|
if (vol && vol.size === "S")
|
|
15451
15750
|
continue;
|
|
@@ -15534,6 +15833,13 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15534
15833
|
const args = node.getArguments();
|
|
15535
15834
|
if (args.length > 0 && argsContainKey(node, "where"))
|
|
15536
15835
|
return;
|
|
15836
|
+
{
|
|
15837
|
+
const countObj = expr.getExpression();
|
|
15838
|
+
if (ts_morph_1.Node.isIdentifier(countObj) && /^(em|entityManager|manager|orm)$/i.test(countObj.getText())) {
|
|
15839
|
+
if (args.length > 1)
|
|
15840
|
+
return;
|
|
15841
|
+
}
|
|
15842
|
+
}
|
|
15537
15843
|
let entity = extractEntity(node);
|
|
15538
15844
|
if (!entity)
|
|
15539
15845
|
entity = extractTypeORMEntity(node);
|
|
@@ -15587,7 +15893,7 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15587
15893
|
const args = node.getArguments();
|
|
15588
15894
|
if (args.length === 0)
|
|
15589
15895
|
return;
|
|
15590
|
-
|
|
15896
|
+
let isUnsafe = method === "$queryRawUnsafe" || method === "$executeRawUnsafe";
|
|
15591
15897
|
let sqlText = "";
|
|
15592
15898
|
const firstArg = args[0];
|
|
15593
15899
|
if (ts_morph_1.Node.isStringLiteral(firstArg)) {
|
|
@@ -15596,6 +15902,7 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15596
15902
|
sqlText = firstArg.getText();
|
|
15597
15903
|
} else if (ts_morph_1.Node.isBinaryExpression(firstArg)) {
|
|
15598
15904
|
sqlText = firstArg.getText();
|
|
15905
|
+
isUnsafe = true;
|
|
15599
15906
|
}
|
|
15600
15907
|
if (sqlText) {
|
|
15601
15908
|
checkSqlForLimit(sqlText, filePath, node.getStartLineNumber(), fns, policy, violations, node, isUnsafe);
|
|
@@ -15616,6 +15923,21 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15616
15923
|
debtPoints: policy.scoring.performance_risk_critical,
|
|
15617
15924
|
gateAction: "block"
|
|
15618
15925
|
});
|
|
15926
|
+
violations.push({
|
|
15927
|
+
category: "performance",
|
|
15928
|
+
type: shared_1.PERFORMANCE_RULES.RAW_SQL_UNSAFE,
|
|
15929
|
+
ruleId: shared_1.PERFORMANCE_RULES.RAW_SQL_UNSAFE,
|
|
15930
|
+
severity: "critical",
|
|
15931
|
+
source: "deterministic",
|
|
15932
|
+
confidence: "high",
|
|
15933
|
+
file: filePath,
|
|
15934
|
+
line: node.getStartLineNumber(),
|
|
15935
|
+
function: fn?.name,
|
|
15936
|
+
message: `${method}() used \u2014 SQL injection risk via string interpolation/concatenation`,
|
|
15937
|
+
suggestion: "Use $queryRaw with tagged template literals and parameterized queries instead",
|
|
15938
|
+
debtPoints: policy.scoring.performance_risk_critical,
|
|
15939
|
+
gateAction: "block"
|
|
15940
|
+
});
|
|
15619
15941
|
}
|
|
15620
15942
|
}
|
|
15621
15943
|
if (ts_morph_1.Node.isTaggedTemplateExpression(node)) {
|
|
@@ -15638,15 +15960,15 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15638
15960
|
const fn2 = getEnclosingFn(node, fns);
|
|
15639
15961
|
violations.push({
|
|
15640
15962
|
category: "performance",
|
|
15641
|
-
type: shared_1.PERFORMANCE_RULES.
|
|
15642
|
-
ruleId: shared_1.PERFORMANCE_RULES.
|
|
15963
|
+
type: shared_1.PERFORMANCE_RULES.RAW_SQL_UNSAFE,
|
|
15964
|
+
ruleId: shared_1.PERFORMANCE_RULES.RAW_SQL_UNSAFE,
|
|
15643
15965
|
severity: "critical",
|
|
15644
15966
|
source: "deterministic",
|
|
15645
15967
|
confidence: "high",
|
|
15646
15968
|
file: filePath,
|
|
15647
15969
|
line,
|
|
15648
15970
|
function: fn2?.name,
|
|
15649
|
-
message: `$queryRawUnsafe() used \u2014 SQL injection risk
|
|
15971
|
+
message: `$queryRawUnsafe() used \u2014 SQL injection risk`,
|
|
15650
15972
|
suggestion: "Use $queryRaw with tagged template literals instead of $queryRawUnsafe",
|
|
15651
15973
|
debtPoints: policy.scoring.performance_risk_critical,
|
|
15652
15974
|
gateAction: "block"
|
|
@@ -15664,22 +15986,24 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
15664
15986
|
const vol = entity ? resolveVolume(entity, policy) : void 0;
|
|
15665
15987
|
const fn = getEnclosingFn(node, fns);
|
|
15666
15988
|
if (isUnsafe) {
|
|
15667
|
-
const severity = hasLimit ? "warning" : "critical";
|
|
15668
15989
|
violations.push({
|
|
15669
15990
|
category: "performance",
|
|
15670
|
-
type: shared_1.PERFORMANCE_RULES.
|
|
15671
|
-
ruleId: shared_1.PERFORMANCE_RULES.
|
|
15672
|
-
severity,
|
|
15991
|
+
type: shared_1.PERFORMANCE_RULES.RAW_SQL_UNSAFE,
|
|
15992
|
+
ruleId: shared_1.PERFORMANCE_RULES.RAW_SQL_UNSAFE,
|
|
15993
|
+
severity: "critical",
|
|
15673
15994
|
source: "deterministic",
|
|
15674
15995
|
confidence: "high",
|
|
15675
15996
|
file: filePath,
|
|
15676
15997
|
line,
|
|
15677
15998
|
function: fn?.name,
|
|
15678
|
-
message:
|
|
15999
|
+
message: `$queryRawUnsafe() used${vol ? ` on '${entity}'` : ""} \u2014 SQL injection risk`,
|
|
15679
16000
|
suggestion: "Use $queryRaw with tagged template literals instead of $queryRawUnsafe",
|
|
15680
|
-
debtPoints:
|
|
15681
|
-
gateAction:
|
|
16001
|
+
debtPoints: policy.scoring.performance_risk_critical,
|
|
16002
|
+
gateAction: "block"
|
|
15682
16003
|
});
|
|
16004
|
+
if (!hasLimit) {
|
|
16005
|
+
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"));
|
|
16006
|
+
}
|
|
15683
16007
|
} else {
|
|
15684
16008
|
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
16009
|
}
|
|
@@ -15788,7 +16112,7 @@ var require_reliability_detector = __commonJS({
|
|
|
15788
16112
|
function isNestJSInjectable(classNode) {
|
|
15789
16113
|
return classNode.getDecorators().some((d) => {
|
|
15790
16114
|
const name = d.getName();
|
|
15791
|
-
return name === "Injectable"
|
|
16115
|
+
return name === "Injectable";
|
|
15792
16116
|
});
|
|
15793
16117
|
}
|
|
15794
16118
|
function isInsideNestJSInjectable(node) {
|
|
@@ -16069,6 +16393,12 @@ var require_reliability_detector = __commonJS({
|
|
|
16069
16393
|
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
16394
|
return;
|
|
16071
16395
|
}
|
|
16396
|
+
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;
|
|
16397
|
+
if (ASYNC_METHOD_PATTERNS.test(calledName)) {
|
|
16398
|
+
const fn = getEnclosingFn(node, fns);
|
|
16399
|
+
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()`));
|
|
16400
|
+
return;
|
|
16401
|
+
}
|
|
16072
16402
|
} else if (ts_morph_1.Node.isIdentifier(callExpr)) {
|
|
16073
16403
|
calledName = callExpr.getText();
|
|
16074
16404
|
const matchFn = fns.find((f) => f.name === calledName && f.isAsync);
|
|
@@ -16276,6 +16606,7 @@ var require_reliability_detector = __commonJS({
|
|
|
16276
16606
|
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
16607
|
});
|
|
16278
16608
|
}
|
|
16609
|
+
var TRANSACTION_METHOD_NAMES = /* @__PURE__ */ new Set(["$transaction", "transaction"]);
|
|
16279
16610
|
function detectTransactionNoTimeout(sourceFile, filePath, fns, policy, violations) {
|
|
16280
16611
|
sourceFile.forEachDescendant((node) => {
|
|
16281
16612
|
if (!ts_morph_1.Node.isCallExpression(node))
|
|
@@ -16283,7 +16614,7 @@ var require_reliability_detector = __commonJS({
|
|
|
16283
16614
|
const expr = node.getExpression();
|
|
16284
16615
|
if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
|
|
16285
16616
|
return;
|
|
16286
|
-
if (expr.getName()
|
|
16617
|
+
if (!TRANSACTION_METHOD_NAMES.has(expr.getName()))
|
|
16287
16618
|
return;
|
|
16288
16619
|
const args = node.getArguments();
|
|
16289
16620
|
const hasTimeout = args.some((arg) => {
|
|
@@ -16299,6 +16630,8 @@ var require_reliability_detector = __commonJS({
|
|
|
16299
16630
|
});
|
|
16300
16631
|
}
|
|
16301
16632
|
function detectMissingNullGuard(sourceFile, filePath, fns, policy, violations) {
|
|
16633
|
+
detectNonNullAssertionOnNullable(sourceFile, filePath, fns, policy, violations);
|
|
16634
|
+
detectUnsafeArrayIndexAccess(sourceFile, filePath, fns, policy, violations);
|
|
16302
16635
|
sourceFile.forEachDescendant((node) => {
|
|
16303
16636
|
if (!ts_morph_1.Node.isVariableDeclaration(node))
|
|
16304
16637
|
return;
|
|
@@ -16352,6 +16685,62 @@ var require_reliability_detector = __commonJS({
|
|
|
16352
16685
|
}
|
|
16353
16686
|
});
|
|
16354
16687
|
}
|
|
16688
|
+
function detectNonNullAssertionOnNullable(sourceFile, filePath, fns, policy, violations) {
|
|
16689
|
+
sourceFile.forEachDescendant((node) => {
|
|
16690
|
+
if (!ts_morph_1.Node.isNonNullExpression(node))
|
|
16691
|
+
return;
|
|
16692
|
+
const inner = node.getExpression();
|
|
16693
|
+
if (!ts_morph_1.Node.isIdentifier(inner))
|
|
16694
|
+
return;
|
|
16695
|
+
const varName = inner.getText();
|
|
16696
|
+
const block = node.getFirstAncestorByKind(ts_morph_1.SyntaxKind.Block);
|
|
16697
|
+
if (!block)
|
|
16698
|
+
return;
|
|
16699
|
+
const blockText = block.getText();
|
|
16700
|
+
const nodePos = node.getStart() - block.getStart();
|
|
16701
|
+
const textBefore = blockText.substring(0, nodePos);
|
|
16702
|
+
const nullableAssign = new RegExp(`\\b${escapeRegex(varName)}\\s*=\\s*await\\s+\\S+\\.(findUnique|findFirst|findOne|findByPk)\\s*\\(`);
|
|
16703
|
+
if (!nullableAssign.test(textBefore))
|
|
16704
|
+
return;
|
|
16705
|
+
const fn = getEnclosingFn(node, fns);
|
|
16706
|
+
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();`));
|
|
16707
|
+
});
|
|
16708
|
+
}
|
|
16709
|
+
function detectUnsafeArrayIndexAccess(sourceFile, filePath, fns, policy, violations) {
|
|
16710
|
+
sourceFile.forEachDescendant((node) => {
|
|
16711
|
+
if (!ts_morph_1.Node.isElementAccessExpression(node))
|
|
16712
|
+
return;
|
|
16713
|
+
const obj = node.getExpression();
|
|
16714
|
+
if (!ts_morph_1.Node.isIdentifier(obj))
|
|
16715
|
+
return;
|
|
16716
|
+
const varName = obj.getText();
|
|
16717
|
+
const indexArg = node.getArgumentExpression();
|
|
16718
|
+
if (!indexArg || !ts_morph_1.Node.isNumericLiteral(indexArg))
|
|
16719
|
+
return;
|
|
16720
|
+
const parent = node.getParent();
|
|
16721
|
+
if (!parent || !ts_morph_1.Node.isPropertyAccessExpression(parent))
|
|
16722
|
+
return;
|
|
16723
|
+
const block = node.getFirstAncestorByKind(ts_morph_1.SyntaxKind.Block);
|
|
16724
|
+
if (!block)
|
|
16725
|
+
return;
|
|
16726
|
+
const blockText = block.getText();
|
|
16727
|
+
const nodePos = node.getStart() - block.getStart();
|
|
16728
|
+
const textBefore = blockText.substring(0, nodePos);
|
|
16729
|
+
const arrayAssign = new RegExp(`\\b${escapeRegex(varName)}\\s*=\\s*await\\s+\\S+\\.(findMany|find|findAll|getMany|query)\\s*\\(`);
|
|
16730
|
+
if (!arrayAssign.test(textBefore))
|
|
16731
|
+
return;
|
|
16732
|
+
const lengthGuard = new RegExp(`${escapeRegex(varName)}\\s*\\.\\s*length|${escapeRegex(varName)}\\s*\\[\\s*0\\s*\\]\\s*&&|if\\s*\\(\\s*!?${escapeRegex(varName)}`);
|
|
16733
|
+
const assignMatch = textBefore.match(arrayAssign);
|
|
16734
|
+
if (assignMatch) {
|
|
16735
|
+
const assignEnd = textBefore.indexOf(assignMatch[0]) + assignMatch[0].length;
|
|
16736
|
+
const textBetween = textBefore.substring(assignEnd);
|
|
16737
|
+
if (lengthGuard.test(textBetween))
|
|
16738
|
+
return;
|
|
16739
|
+
}
|
|
16740
|
+
const fn = getEnclosingFn(node, fns);
|
|
16741
|
+
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();`));
|
|
16742
|
+
});
|
|
16743
|
+
}
|
|
16355
16744
|
function escapeRegex(str) {
|
|
16356
16745
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
16357
16746
|
}
|
|
@@ -18054,6 +18443,7 @@ var require_orchestrator = __commonJS({
|
|
|
18054
18443
|
crossFileAnalysis = await (0, cross_file_analyzer_1.analyzeCrossFile)(unflaggedPatterns, importGraph, filteredInput, policy);
|
|
18055
18444
|
}
|
|
18056
18445
|
const complexityViolations = complexityDeltasToViolations(complexityDeltas, filteredInput);
|
|
18446
|
+
const largeFileViolations = detectLargeFiles(filteredInput);
|
|
18057
18447
|
const violations = [
|
|
18058
18448
|
...boundaryViolations,
|
|
18059
18449
|
...circularViolations,
|
|
@@ -18061,6 +18451,7 @@ var require_orchestrator = __commonJS({
|
|
|
18061
18451
|
...perfViolations,
|
|
18062
18452
|
...reliabilityViolations,
|
|
18063
18453
|
...complexityViolations,
|
|
18454
|
+
...largeFileViolations,
|
|
18064
18455
|
...duplicationResult.violations,
|
|
18065
18456
|
...missingTestsResult?.violations ?? [],
|
|
18066
18457
|
...deadCodeResult.violations,
|
|
@@ -18078,6 +18469,34 @@ var require_orchestrator = __commonJS({
|
|
|
18078
18469
|
crossFileAnalysis
|
|
18079
18470
|
};
|
|
18080
18471
|
}
|
|
18472
|
+
function detectLargeFiles(input) {
|
|
18473
|
+
const threshold = shared_1.DEFAULT_LARGE_FILE_THRESHOLD;
|
|
18474
|
+
const violations = [];
|
|
18475
|
+
for (const file of input.changedFiles) {
|
|
18476
|
+
if (file.status === "deleted")
|
|
18477
|
+
continue;
|
|
18478
|
+
if (!/\.(ts|tsx|js|jsx)$/.test(file.path))
|
|
18479
|
+
continue;
|
|
18480
|
+
const lineCount = file.content.split("\n").length;
|
|
18481
|
+
if (lineCount <= threshold)
|
|
18482
|
+
continue;
|
|
18483
|
+
violations.push({
|
|
18484
|
+
category: "maintainability",
|
|
18485
|
+
type: shared_1.MAINTAINABILITY_RULES.LARGE_FILE,
|
|
18486
|
+
ruleId: shared_1.MAINTAINABILITY_RULES.LARGE_FILE,
|
|
18487
|
+
severity: "warning",
|
|
18488
|
+
source: "deterministic",
|
|
18489
|
+
confidence: "high",
|
|
18490
|
+
file: file.path,
|
|
18491
|
+
line: 1,
|
|
18492
|
+
message: `File has ${lineCount} lines (threshold: ${threshold}) \u2014 consider splitting into smaller modules`,
|
|
18493
|
+
suggestion: "Extract helper functions, types, or constants into separate files",
|
|
18494
|
+
debtPoints: Math.floor((lineCount - threshold) / 50) + 1,
|
|
18495
|
+
gateAction: "warn"
|
|
18496
|
+
});
|
|
18497
|
+
}
|
|
18498
|
+
return violations;
|
|
18499
|
+
}
|
|
18081
18500
|
function complexityDeltasToViolations(deltas, input) {
|
|
18082
18501
|
const threshold = shared_1.DEFAULT_COMPLEXITY_THRESHOLD;
|
|
18083
18502
|
const violations = [];
|