technical-debt-radar 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +344 -9
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -77,7 +77,8 @@ var require_constants = __commonJS({
|
|
|
77
77
|
LARGE_JSON_STRINGIFY: "large-json-stringify",
|
|
78
78
|
CPU_HEAVY_LOOP_IN_HANDLER: "cpu-heavy-loop-in-handler",
|
|
79
79
|
UNBOUNDED_ARRAY_OPERATION: "unbounded-array-operation",
|
|
80
|
-
DYNAMIC_BUFFER_ALLOC: "dynamic-buffer-alloc"
|
|
80
|
+
DYNAMIC_BUFFER_ALLOC: "dynamic-buffer-alloc",
|
|
81
|
+
UNBOUNDED_PARALLEL_CALLS: "unbounded-parallel-external-calls"
|
|
81
82
|
};
|
|
82
83
|
exports2.RELIABILITY_RULES = {
|
|
83
84
|
UNHANDLED_PROMISE_REJECTION: "unhandled-promise-rejection",
|
|
@@ -90,7 +91,9 @@ var require_constants = __commonJS({
|
|
|
90
91
|
MISSING_NULL_GUARD: "missing-null-guard",
|
|
91
92
|
MISSING_DTO_VALIDATION: "missing-dto-validation",
|
|
92
93
|
UNGUARDED_ROUTE_TODO: "unguarded-route-todo",
|
|
93
|
-
ERROR_LOGGED_AS_INFO: "error-logged-as-info"
|
|
94
|
+
ERROR_LOGGED_AS_INFO: "error-logged-as-info",
|
|
95
|
+
CATCH_SWALLOWS_CONTEXT: "catch-block-swallows-error-context",
|
|
96
|
+
PROMISE_ALL_NO_ISOLATION: "promise-all-no-error-isolation"
|
|
94
97
|
};
|
|
95
98
|
exports2.PERFORMANCE_RULES = {
|
|
96
99
|
UNBOUNDED_FIND_MANY: "unbounded-find-many",
|
|
@@ -110,6 +113,8 @@ var require_constants = __commonJS({
|
|
|
110
113
|
CODE_DUPLICATION: "code-duplication",
|
|
111
114
|
MISSING_TEST_FILE: "missing-test-file",
|
|
112
115
|
UNUSED_EXPORT: "unused-export",
|
|
116
|
+
UNUSED_PUBLIC_METHOD: "unused-public-method",
|
|
117
|
+
DEBUG_CONSOLE_LOG: "debug-console-log-in-production",
|
|
113
118
|
LOW_TEST_COVERAGE: "low-test-coverage",
|
|
114
119
|
LOW_BRANCH_COVERAGE: "low-branch-coverage",
|
|
115
120
|
COVERAGE_DROP: "coverage-drop"
|
|
@@ -118,7 +123,9 @@ var require_constants = __commonJS({
|
|
|
118
123
|
LAYER_VIOLATION: "layer-boundary-violation",
|
|
119
124
|
CIRCULAR_DEPENDENCY: "circular-dependency",
|
|
120
125
|
MODULE_BOUNDARY: "module-boundary-violation",
|
|
121
|
-
FORBIDDEN_FRAMEWORK: "forbidden-framework-in-layer"
|
|
126
|
+
FORBIDDEN_FRAMEWORK: "forbidden-framework-in-layer",
|
|
127
|
+
PROVIDER_OUTSIDE_HOME: "provider-declared-outside-home-module",
|
|
128
|
+
SCHEMA_MULTI_MODULE: "schema-registered-in-multiple-modules"
|
|
122
129
|
};
|
|
123
130
|
exports2.CROSS_FILE_RULES = {
|
|
124
131
|
INDIRECT_SYNC_FS: "indirect-sync-fs",
|
|
@@ -12763,12 +12770,19 @@ var require_import_graph = __commonJS({
|
|
|
12763
12770
|
return specifiers;
|
|
12764
12771
|
}
|
|
12765
12772
|
function resolveModuleSpecifier(sourcePath, specifier, project) {
|
|
12766
|
-
if (
|
|
12767
|
-
|
|
12773
|
+
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
12774
|
+
const sourceDir = posixDirname(sourcePath);
|
|
12775
|
+
const resolved = posixJoinAndNormalize(sourceDir, specifier);
|
|
12776
|
+
return resolveInProject(resolved, project) ?? resolved;
|
|
12768
12777
|
}
|
|
12769
|
-
const
|
|
12770
|
-
const
|
|
12771
|
-
|
|
12778
|
+
const aliasStripped = specifier.replace(/^@\//, "src/").replace(/^~\//, "src/");
|
|
12779
|
+
const aliasResolved = resolveInProject(aliasStripped, project);
|
|
12780
|
+
if (aliasResolved)
|
|
12781
|
+
return aliasResolved;
|
|
12782
|
+
const directResolved = resolveInProject(specifier, project);
|
|
12783
|
+
if (directResolved)
|
|
12784
|
+
return directResolved;
|
|
12785
|
+
return void 0;
|
|
12772
12786
|
}
|
|
12773
12787
|
function resolveInProject(resolved, project) {
|
|
12774
12788
|
const lookup = "/" + resolved;
|
|
@@ -12787,6 +12801,16 @@ var require_import_graph = __commonJS({
|
|
|
12787
12801
|
return indexPath;
|
|
12788
12802
|
}
|
|
12789
12803
|
}
|
|
12804
|
+
const suffix = "/" + resolved;
|
|
12805
|
+
for (const sf of project.getSourceFiles()) {
|
|
12806
|
+
const fp = sf.getFilePath();
|
|
12807
|
+
if (fp.endsWith(suffix))
|
|
12808
|
+
return normalizeFilePath(fp);
|
|
12809
|
+
for (const ext of extensions) {
|
|
12810
|
+
if (fp.endsWith(suffix + ext))
|
|
12811
|
+
return normalizeFilePath(fp);
|
|
12812
|
+
}
|
|
12813
|
+
}
|
|
12790
12814
|
return void 0;
|
|
12791
12815
|
}
|
|
12792
12816
|
function buildEdge(source, target, importType, policy) {
|
|
@@ -12877,12 +12901,22 @@ var require_boundary_checker = __commonJS({
|
|
|
12877
12901
|
return /nestjs/i.test(policy.stack.framework);
|
|
12878
12902
|
}
|
|
12879
12903
|
var suffixPattern = (suffix) => new RegExp(`\\.${suffix}(\\.(ts|tsx|js|jsx))?$`);
|
|
12880
|
-
var NESTJS_SHARED_INFRA_FOLDERS = ["guards", "decorators", "interceptors", "pipes", "filters", "middleware", "shared"];
|
|
12904
|
+
var NESTJS_SHARED_INFRA_FOLDERS = ["guards", "decorators", "interceptors", "pipes", "filters", "middleware", "shared", "strategies", "common", "config"];
|
|
12881
12905
|
function isNestJSExemptImport(sourcePath, targetPath, customSharedFolders = []) {
|
|
12882
12906
|
const srcBase = sourcePath.replace(/\\/g, "/");
|
|
12883
12907
|
const tgtBase = targetPath.replace(/\\/g, "/");
|
|
12884
12908
|
if (suffixPattern("module").test(srcBase))
|
|
12885
12909
|
return true;
|
|
12910
|
+
if (suffixPattern("strategy").test(srcBase))
|
|
12911
|
+
return true;
|
|
12912
|
+
if (suffixPattern("guard").test(srcBase))
|
|
12913
|
+
return true;
|
|
12914
|
+
if (suffixPattern("interceptor").test(srcBase))
|
|
12915
|
+
return true;
|
|
12916
|
+
if (suffixPattern("pipe").test(srcBase))
|
|
12917
|
+
return true;
|
|
12918
|
+
if (suffixPattern("filter").test(srcBase))
|
|
12919
|
+
return true;
|
|
12886
12920
|
if (suffixPattern("module").test(tgtBase))
|
|
12887
12921
|
return true;
|
|
12888
12922
|
if (suffixPattern("entity").test(tgtBase))
|
|
@@ -12899,6 +12933,8 @@ var require_boundary_checker = __commonJS({
|
|
|
12899
12933
|
return true;
|
|
12900
12934
|
if (suffixPattern("filter").test(tgtBase))
|
|
12901
12935
|
return true;
|
|
12936
|
+
if (suffixPattern("strategy").test(tgtBase))
|
|
12937
|
+
return true;
|
|
12902
12938
|
if (/\/dto\//.test(tgtBase) || suffixPattern("dto").test(tgtBase))
|
|
12903
12939
|
return true;
|
|
12904
12940
|
for (const folder of NESTJS_SHARED_INFRA_FOLDERS) {
|
|
@@ -16156,6 +16192,7 @@ var require_reliability_detector = __commonJS({
|
|
|
16156
16192
|
async function detectReliabilityIssues(input, policy) {
|
|
16157
16193
|
const violations = [];
|
|
16158
16194
|
const project = new ts_morph_1.Project({ useInMemoryFileSystem: true });
|
|
16195
|
+
const schemaModuleMap = /* @__PURE__ */ new Map();
|
|
16159
16196
|
for (const file of input.changedFiles) {
|
|
16160
16197
|
if (file.status === "deleted")
|
|
16161
16198
|
continue;
|
|
@@ -16174,6 +16211,12 @@ var require_reliability_detector = __commonJS({
|
|
|
16174
16211
|
detectMissingNullGuard(sourceFile, file.path, fns, policy, violations);
|
|
16175
16212
|
detectMissingDtoValidation(sourceFile, file.path, fns, policy, violations);
|
|
16176
16213
|
detectUnguardedRouteTodo(sourceFile, file.path, fns, policy, violations);
|
|
16214
|
+
detectCatchSwallowsContext(sourceFile, file.path, fns, policy, violations);
|
|
16215
|
+
detectDebugConsoleLog(sourceFile, file.path, fns, policy, violations);
|
|
16216
|
+
detectProviderOutsideHomeModule(sourceFile, file.path, input.changedFiles, policy, violations);
|
|
16217
|
+
detectSchemaMultiModule(sourceFile, file.path, schemaModuleMap, policy, violations);
|
|
16218
|
+
detectPromiseAllNoIsolation(sourceFile, file.path, fns, policy, violations);
|
|
16219
|
+
detectUnboundedParallelCalls(sourceFile, file.path, fns, policy, violations);
|
|
16177
16220
|
project.removeSourceFile(sourceFile);
|
|
16178
16221
|
}
|
|
16179
16222
|
return applyExceptions(violations, policy);
|
|
@@ -16979,6 +17022,298 @@ var require_reliability_detector = __commonJS({
|
|
|
16979
17022
|
violations.push(makeViolation(shared_1.RELIABILITY_RULES.UNGUARDED_ROUTE_TODO, filePath, node.getStartLineNumber(), `Route handler '${node.getName?.() ?? "anonymous"}' has TODO/FIXME comment and no @UseGuards() \u2014 unprotected endpoint`, policy, fn?.name, "Add @UseGuards(JwtAuthGuard) or appropriate guard \u2014 this route is currently unprotected"));
|
|
16980
17023
|
});
|
|
16981
17024
|
}
|
|
17025
|
+
function detectPromiseAllNoIsolation(sourceFile, filePath, fns, policy, violations) {
|
|
17026
|
+
sourceFile.forEachDescendant((node) => {
|
|
17027
|
+
if (!ts_morph_1.Node.isCallExpression(node))
|
|
17028
|
+
return;
|
|
17029
|
+
const expr = node.getExpression();
|
|
17030
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
|
|
17031
|
+
return;
|
|
17032
|
+
if (expr.getName() !== "all")
|
|
17033
|
+
return;
|
|
17034
|
+
const obj = expr.getExpression();
|
|
17035
|
+
if (!ts_morph_1.Node.isIdentifier(obj) || obj.getText() !== "Promise")
|
|
17036
|
+
return;
|
|
17037
|
+
const args = node.getArguments();
|
|
17038
|
+
if (args.length === 0)
|
|
17039
|
+
return;
|
|
17040
|
+
const arrayArg = args[0];
|
|
17041
|
+
if (!ts_morph_1.Node.isArrayLiteralExpression(arrayArg))
|
|
17042
|
+
return;
|
|
17043
|
+
const elements = arrayArg.getElements();
|
|
17044
|
+
if (elements.length < 2)
|
|
17045
|
+
return;
|
|
17046
|
+
let hasExternalCalls = false;
|
|
17047
|
+
let hasCatchHandlers = false;
|
|
17048
|
+
for (const el of elements) {
|
|
17049
|
+
const elText = el.getText();
|
|
17050
|
+
if (/fetch|axios|http|this\.\w+Service|this\.\w+Model/.test(elText)) {
|
|
17051
|
+
hasExternalCalls = true;
|
|
17052
|
+
}
|
|
17053
|
+
if (/\.catch\(/.test(elText)) {
|
|
17054
|
+
hasCatchHandlers = true;
|
|
17055
|
+
}
|
|
17056
|
+
}
|
|
17057
|
+
if (!hasExternalCalls)
|
|
17058
|
+
return;
|
|
17059
|
+
if (hasCatchHandlers)
|
|
17060
|
+
return;
|
|
17061
|
+
const fn = getEnclosingFn(node, fns);
|
|
17062
|
+
violations.push(makeViolation(shared_1.RELIABILITY_RULES.PROMISE_ALL_NO_ISOLATION, filePath, node.getStartLineNumber(), `Promise.all() with ${elements.length} I/O calls \u2014 one failure rejects all with no error isolation`, policy, fn?.name, "Add .catch() to individual promises or use Promise.allSettled() for partial failure tolerance"));
|
|
17063
|
+
});
|
|
17064
|
+
}
|
|
17065
|
+
function detectUnboundedParallelCalls(sourceFile, filePath, fns, policy, violations) {
|
|
17066
|
+
sourceFile.forEachDescendant((node) => {
|
|
17067
|
+
if (!ts_morph_1.Node.isCallExpression(node))
|
|
17068
|
+
return;
|
|
17069
|
+
const expr = node.getExpression();
|
|
17070
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
|
|
17071
|
+
return;
|
|
17072
|
+
if (expr.getName() !== "all")
|
|
17073
|
+
return;
|
|
17074
|
+
const obj = expr.getExpression();
|
|
17075
|
+
if (!ts_morph_1.Node.isIdentifier(obj) || obj.getText() !== "Promise")
|
|
17076
|
+
return;
|
|
17077
|
+
const args = node.getArguments();
|
|
17078
|
+
if (args.length === 0)
|
|
17079
|
+
return;
|
|
17080
|
+
const firstArg = args[0];
|
|
17081
|
+
if (!ts_morph_1.Node.isCallExpression(firstArg))
|
|
17082
|
+
return;
|
|
17083
|
+
const mapExpr = firstArg.getExpression();
|
|
17084
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(mapExpr))
|
|
17085
|
+
return;
|
|
17086
|
+
if (mapExpr.getName() !== "map")
|
|
17087
|
+
return;
|
|
17088
|
+
const mapArgs = firstArg.getArguments();
|
|
17089
|
+
if (mapArgs.length === 0)
|
|
17090
|
+
return;
|
|
17091
|
+
const callbackText = mapArgs[0].getText();
|
|
17092
|
+
if (!/fetch|axios|http|this\.\w+Service|this\.\w+Model|\.upload|\.send|\.post|\.get|\.put|\.delete/i.test(callbackText)) {
|
|
17093
|
+
return;
|
|
17094
|
+
}
|
|
17095
|
+
const fn = getEnclosingFn(node, fns);
|
|
17096
|
+
violations.push({
|
|
17097
|
+
category: "runtime_risk",
|
|
17098
|
+
type: shared_1.RUNTIME_RISK_RULES.UNBOUNDED_PARALLEL_CALLS,
|
|
17099
|
+
ruleId: shared_1.RUNTIME_RISK_RULES.UNBOUNDED_PARALLEL_CALLS,
|
|
17100
|
+
severity: "warning",
|
|
17101
|
+
source: "deterministic",
|
|
17102
|
+
confidence: "high",
|
|
17103
|
+
file: filePath,
|
|
17104
|
+
line: node.getStartLineNumber(),
|
|
17105
|
+
function: fn?.name,
|
|
17106
|
+
message: "Promise.all(array.map()) with unbounded I/O calls \u2014 may overwhelm external service or exhaust connections",
|
|
17107
|
+
suggestion: "Use p-limit or batch processing to limit concurrency: const limit = pLimit(5); await Promise.all(items.map(item => limit(() => fn(item))))",
|
|
17108
|
+
debtPoints: 5,
|
|
17109
|
+
gateAction: "warn"
|
|
17110
|
+
});
|
|
17111
|
+
});
|
|
17112
|
+
}
|
|
17113
|
+
function detectProviderOutsideHomeModule(sourceFile, filePath, allFiles, policy, violations) {
|
|
17114
|
+
if (!filePath.endsWith(".module.ts") && !filePath.endsWith(".module.js"))
|
|
17115
|
+
return;
|
|
17116
|
+
sourceFile.forEachDescendant((node) => {
|
|
17117
|
+
if (!ts_morph_1.Node.isDecorator(node) || node.getName() !== "Module")
|
|
17118
|
+
return;
|
|
17119
|
+
const args = node.getArguments?.();
|
|
17120
|
+
if (!args || args.length === 0)
|
|
17121
|
+
return;
|
|
17122
|
+
const callExpr = node.getCallExpression?.();
|
|
17123
|
+
if (!callExpr)
|
|
17124
|
+
return;
|
|
17125
|
+
const moduleArgs = callExpr.getArguments();
|
|
17126
|
+
if (moduleArgs.length === 0 || !ts_morph_1.Node.isObjectLiteralExpression(moduleArgs[0]))
|
|
17127
|
+
return;
|
|
17128
|
+
const objLit = moduleArgs[0];
|
|
17129
|
+
const providersProp = objLit.getProperty("providers");
|
|
17130
|
+
if (!providersProp || !ts_morph_1.Node.isPropertyAssignment(providersProp))
|
|
17131
|
+
return;
|
|
17132
|
+
const providersInit = providersProp.getInitializer();
|
|
17133
|
+
if (!providersInit || !ts_morph_1.Node.isArrayLiteralExpression(providersInit))
|
|
17134
|
+
return;
|
|
17135
|
+
const moduleFolder = filePath.replace(/\/[^/]+$/, "");
|
|
17136
|
+
for (const element of providersInit.getElements()) {
|
|
17137
|
+
if (!ts_morph_1.Node.isIdentifier(element))
|
|
17138
|
+
continue;
|
|
17139
|
+
const providerName = element.getText();
|
|
17140
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
17141
|
+
const namedImports = importDecl.getNamedImports();
|
|
17142
|
+
const matchingImport = namedImports.find((n) => n.getName() === providerName);
|
|
17143
|
+
if (!matchingImport)
|
|
17144
|
+
continue;
|
|
17145
|
+
const specifier = importDecl.getModuleSpecifierValue();
|
|
17146
|
+
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
17147
|
+
const resolvedPath = specifier.replace(/^\.\//, moduleFolder + "/").replace(/^\.\.\//, moduleFolder + "/../");
|
|
17148
|
+
if (!resolvedPath.startsWith(moduleFolder) && !specifier.startsWith("./")) {
|
|
17149
|
+
violations.push({
|
|
17150
|
+
category: "architecture",
|
|
17151
|
+
type: shared_1.ARCHITECTURE_RULES.PROVIDER_OUTSIDE_HOME,
|
|
17152
|
+
ruleId: shared_1.ARCHITECTURE_RULES.PROVIDER_OUTSIDE_HOME,
|
|
17153
|
+
severity: "warning",
|
|
17154
|
+
source: "deterministic",
|
|
17155
|
+
confidence: "high",
|
|
17156
|
+
file: filePath,
|
|
17157
|
+
line: element.getStartLineNumber(),
|
|
17158
|
+
message: `Provider '${providerName}' is imported from outside this module's folder \u2014 consider moving it to its home module or a shared module`,
|
|
17159
|
+
suggestion: `Move ${providerName} to this module's folder or register it in the module where it's defined`,
|
|
17160
|
+
debtPoints: 3,
|
|
17161
|
+
gateAction: "warn"
|
|
17162
|
+
});
|
|
17163
|
+
}
|
|
17164
|
+
}
|
|
17165
|
+
}
|
|
17166
|
+
}
|
|
17167
|
+
});
|
|
17168
|
+
}
|
|
17169
|
+
function detectSchemaMultiModule(sourceFile, filePath, schemaMap, policy, violations) {
|
|
17170
|
+
if (!filePath.endsWith(".module.ts") && !filePath.endsWith(".module.js"))
|
|
17171
|
+
return;
|
|
17172
|
+
sourceFile.forEachDescendant((node) => {
|
|
17173
|
+
if (!ts_morph_1.Node.isCallExpression(node))
|
|
17174
|
+
return;
|
|
17175
|
+
const expr = node.getExpression();
|
|
17176
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
|
|
17177
|
+
return;
|
|
17178
|
+
const methodName = expr.getName();
|
|
17179
|
+
if (methodName !== "forFeature")
|
|
17180
|
+
return;
|
|
17181
|
+
const obj = expr.getExpression();
|
|
17182
|
+
const objText = ts_morph_1.Node.isIdentifier(obj) ? obj.getText() : "";
|
|
17183
|
+
if (objText !== "MongooseModule" && objText !== "TypeOrmModule")
|
|
17184
|
+
return;
|
|
17185
|
+
const args = node.getArguments();
|
|
17186
|
+
if (args.length === 0)
|
|
17187
|
+
return;
|
|
17188
|
+
const arrayArg = args[0];
|
|
17189
|
+
if (!ts_morph_1.Node.isArrayLiteralExpression(arrayArg))
|
|
17190
|
+
return;
|
|
17191
|
+
for (const element of arrayArg.getElements()) {
|
|
17192
|
+
if (ts_morph_1.Node.isObjectLiteralExpression(element)) {
|
|
17193
|
+
const nameProp = element.getProperty("name");
|
|
17194
|
+
if (nameProp && ts_morph_1.Node.isPropertyAssignment(nameProp)) {
|
|
17195
|
+
const init = nameProp.getInitializer();
|
|
17196
|
+
if (init) {
|
|
17197
|
+
const schemaName = init.getText().replace(/\.name$/, "");
|
|
17198
|
+
const existing = schemaMap.get(schemaName) ?? [];
|
|
17199
|
+
existing.push(filePath);
|
|
17200
|
+
schemaMap.set(schemaName, existing);
|
|
17201
|
+
if (existing.length > 1) {
|
|
17202
|
+
violations.push({
|
|
17203
|
+
category: "architecture",
|
|
17204
|
+
type: shared_1.ARCHITECTURE_RULES.SCHEMA_MULTI_MODULE,
|
|
17205
|
+
ruleId: shared_1.ARCHITECTURE_RULES.SCHEMA_MULTI_MODULE,
|
|
17206
|
+
severity: "warning",
|
|
17207
|
+
source: "deterministic",
|
|
17208
|
+
confidence: "high",
|
|
17209
|
+
file: filePath,
|
|
17210
|
+
line: element.getStartLineNumber(),
|
|
17211
|
+
message: `Schema '${schemaName}' registered in multiple modules: ${existing.join(", ")}`,
|
|
17212
|
+
suggestion: "Register the schema in one module and import that module where needed",
|
|
17213
|
+
debtPoints: 3,
|
|
17214
|
+
gateAction: "warn"
|
|
17215
|
+
});
|
|
17216
|
+
}
|
|
17217
|
+
}
|
|
17218
|
+
}
|
|
17219
|
+
}
|
|
17220
|
+
if (ts_morph_1.Node.isIdentifier(element)) {
|
|
17221
|
+
const entityName = element.getText();
|
|
17222
|
+
const existing = schemaMap.get(entityName) ?? [];
|
|
17223
|
+
existing.push(filePath);
|
|
17224
|
+
schemaMap.set(entityName, existing);
|
|
17225
|
+
if (existing.length > 1) {
|
|
17226
|
+
violations.push({
|
|
17227
|
+
category: "architecture",
|
|
17228
|
+
type: shared_1.ARCHITECTURE_RULES.SCHEMA_MULTI_MODULE,
|
|
17229
|
+
ruleId: shared_1.ARCHITECTURE_RULES.SCHEMA_MULTI_MODULE,
|
|
17230
|
+
severity: "warning",
|
|
17231
|
+
source: "deterministic",
|
|
17232
|
+
confidence: "high",
|
|
17233
|
+
file: filePath,
|
|
17234
|
+
line: element.getStartLineNumber(),
|
|
17235
|
+
message: `Entity '${entityName}' registered in multiple modules: ${existing.join(", ")}`,
|
|
17236
|
+
suggestion: "Register the entity in one module and import that module where needed",
|
|
17237
|
+
debtPoints: 3,
|
|
17238
|
+
gateAction: "warn"
|
|
17239
|
+
});
|
|
17240
|
+
}
|
|
17241
|
+
}
|
|
17242
|
+
}
|
|
17243
|
+
});
|
|
17244
|
+
}
|
|
17245
|
+
var DEBUG_LOG_PATTERN = /\b(RESULT|DEBUG|TEST|TODO|FIXME|HACK|XXX)\b/i;
|
|
17246
|
+
var EXCLUDE_LOG_FILES = /\.(spec|test)\.(ts|js)|seed|migration|main\.(ts|js)$/;
|
|
17247
|
+
function detectDebugConsoleLog(sourceFile, filePath, fns, policy, violations) {
|
|
17248
|
+
if (EXCLUDE_LOG_FILES.test(filePath))
|
|
17249
|
+
return;
|
|
17250
|
+
sourceFile.forEachDescendant((node) => {
|
|
17251
|
+
if (!ts_morph_1.Node.isCallExpression(node))
|
|
17252
|
+
return;
|
|
17253
|
+
const expr = node.getExpression();
|
|
17254
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
|
|
17255
|
+
return;
|
|
17256
|
+
const obj = expr.getExpression();
|
|
17257
|
+
if (!ts_morph_1.Node.isIdentifier(obj) || obj.getText() !== "console")
|
|
17258
|
+
return;
|
|
17259
|
+
if (expr.getName() !== "log")
|
|
17260
|
+
return;
|
|
17261
|
+
const args = node.getArguments();
|
|
17262
|
+
const argsText = args.map((a) => a.getText()).join(" ");
|
|
17263
|
+
if (!DEBUG_LOG_PATTERN.test(argsText))
|
|
17264
|
+
return;
|
|
17265
|
+
const catchClause = node.getFirstAncestorByKind(ts_morph_1.SyntaxKind.CatchClause);
|
|
17266
|
+
if (catchClause)
|
|
17267
|
+
return;
|
|
17268
|
+
const fn = getEnclosingFn(node, fns);
|
|
17269
|
+
violations.push({
|
|
17270
|
+
category: "maintainability",
|
|
17271
|
+
type: shared_1.MAINTAINABILITY_RULES.DEBUG_CONSOLE_LOG,
|
|
17272
|
+
ruleId: shared_1.MAINTAINABILITY_RULES.DEBUG_CONSOLE_LOG,
|
|
17273
|
+
severity: "warning",
|
|
17274
|
+
source: "deterministic",
|
|
17275
|
+
confidence: "high",
|
|
17276
|
+
file: filePath,
|
|
17277
|
+
line: node.getStartLineNumber(),
|
|
17278
|
+
function: fn?.name,
|
|
17279
|
+
message: `Debug console.log() in production code: ${argsText.substring(0, 60)}`,
|
|
17280
|
+
suggestion: "Remove debug logging or replace with a proper logger (winston, pino)",
|
|
17281
|
+
debtPoints: 1,
|
|
17282
|
+
gateAction: "warn"
|
|
17283
|
+
});
|
|
17284
|
+
});
|
|
17285
|
+
}
|
|
17286
|
+
function detectCatchSwallowsContext(sourceFile, filePath, fns, policy, violations) {
|
|
17287
|
+
sourceFile.forEachDescendant((node) => {
|
|
17288
|
+
if (!ts_morph_1.Node.isCatchClause(node))
|
|
17289
|
+
return;
|
|
17290
|
+
const block = node.getBlock();
|
|
17291
|
+
const errParam = node.getVariableDeclaration()?.getName();
|
|
17292
|
+
if (!errParam)
|
|
17293
|
+
return;
|
|
17294
|
+
let throwsNew = false;
|
|
17295
|
+
let referencesOriginal = false;
|
|
17296
|
+
block.forEachDescendant((child) => {
|
|
17297
|
+
if (ts_morph_1.Node.isThrowStatement(child)) {
|
|
17298
|
+
throwsNew = true;
|
|
17299
|
+
const thrownText = child.getExpression()?.getText() ?? "";
|
|
17300
|
+
if (thrownText.includes(errParam)) {
|
|
17301
|
+
referencesOriginal = true;
|
|
17302
|
+
}
|
|
17303
|
+
}
|
|
17304
|
+
});
|
|
17305
|
+
if (!throwsNew)
|
|
17306
|
+
return;
|
|
17307
|
+
if (referencesOriginal)
|
|
17308
|
+
return;
|
|
17309
|
+
const blockText = block.getText();
|
|
17310
|
+
if (/\b(logger|console\.(?:error|warn))\b/i.test(blockText) && new RegExp(`\\b${escapeRegex(errParam)}\\b`).test(blockText)) {
|
|
17311
|
+
return;
|
|
17312
|
+
}
|
|
17313
|
+
const fn = getEnclosingFn(node, fns);
|
|
17314
|
+
violations.push(makeViolation(shared_1.RELIABILITY_RULES.CATCH_SWALLOWS_CONTEXT, filePath, node.getStartLineNumber(), `Catch block throws new error without including original '${errParam}' \u2014 error context lost`, policy, fn?.name, `Include the original error: throw new Error('message', { cause: ${errParam} }) or throw new HttpException(message, status, { cause: ${errParam} })`));
|
|
17315
|
+
});
|
|
17316
|
+
}
|
|
16982
17317
|
function escapeRegex(str) {
|
|
16983
17318
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
16984
17319
|
}
|