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.
Files changed (2) hide show
  1. package/dist/index.js +344 -9
  2. 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 (!specifier.startsWith(".") && !specifier.startsWith("/")) {
12767
- return void 0;
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 sourceDir = posixDirname(sourcePath);
12770
- const resolved = posixJoinAndNormalize(sourceDir, specifier);
12771
- return resolveInProject(resolved, project) ?? resolved;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "technical-debt-radar",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Stop Node.js production crashes before merge. 47 detection patterns across 5 categories.",
5
5
  "bin": {
6
6
  "radar": "dist/index.js",