From f98db7404be885925d4fdde15f2128710f7e2bd7 Mon Sep 17 00:00:00 2001
From: RahulGautamSingh <rahultesnik@gmail.com>
Date: Thu, 19 Dec 2024 19:55:35 +0530
Subject: [PATCH] refactor(config/validation): move helper fns to separate file
 (#33206)

---
 lib/config/validation-helpers/utils.spec.ts |  17 +++
 lib/config/validation-helpers/utils.ts      | 138 ++++++++++++++++++
 lib/config/validation.spec.ts               |  16 ---
 lib/config/validation.ts                    | 151 +++-----------------
 4 files changed, 174 insertions(+), 148 deletions(-)
 create mode 100644 lib/config/validation-helpers/utils.spec.ts
 create mode 100644 lib/config/validation-helpers/utils.ts

diff --git a/lib/config/validation-helpers/utils.spec.ts b/lib/config/validation-helpers/utils.spec.ts
new file mode 100644
index 0000000000..8344eed039
--- /dev/null
+++ b/lib/config/validation-helpers/utils.spec.ts
@@ -0,0 +1,17 @@
+import { getParentName } from './utils';
+
+describe('config/validation-helpers/utils', () => {
+  describe('getParentName()', () => {
+    it('ignores encrypted in root', () => {
+      expect(getParentName('encrypted')).toBeEmptyString();
+    });
+
+    it('handles array types', () => {
+      expect(getParentName('hostRules[1]')).toBe('hostRules');
+    });
+
+    it('handles encrypted within array types', () => {
+      expect(getParentName('hostRules[0].encrypted')).toBe('hostRules');
+    });
+  });
+});
diff --git a/lib/config/validation-helpers/utils.ts b/lib/config/validation-helpers/utils.ts
new file mode 100644
index 0000000000..5d676bed32
--- /dev/null
+++ b/lib/config/validation-helpers/utils.ts
@@ -0,0 +1,138 @@
+import is from '@sindresorhus/is';
+import { logger } from '../../logger';
+import type {
+  RegexManagerConfig,
+  RegexManagerTemplates,
+} from '../../modules/manager/custom/regex/types';
+import { regEx } from '../../util/regex';
+import type { ValidationMessage } from '../types';
+
+export function getParentName(parentPath: string | undefined): string {
+  return parentPath
+    ? parentPath
+        .replace(regEx(/\.?encrypted$/), '')
+        .replace(regEx(/\[\d+\]$/), '')
+        .split('.')
+        .pop()!
+    : '.';
+}
+
+export function validatePlainObject(
+  val: Record<string, unknown>,
+): true | string {
+  for (const [key, value] of Object.entries(val)) {
+    if (!is.string(value)) {
+      return key;
+    }
+  }
+  return true;
+}
+
+export function validateNumber(
+  key: string,
+  val: unknown,
+  allowsNegative: boolean,
+  currentPath?: string,
+  subKey?: string,
+): ValidationMessage[] {
+  const errors: ValidationMessage[] = [];
+  const path = `${currentPath}${subKey ? '.' + subKey : ''}`;
+  if (is.number(val)) {
+    if (val < 0 && !allowsNegative) {
+      errors.push({
+        topic: 'Configuration Error',
+        message: `Configuration option \`${path}\` should be a positive integer. Found negative value instead.`,
+      });
+    }
+  } else {
+    errors.push({
+      topic: 'Configuration Error',
+      message: `Configuration option \`${path}\` should be an integer. Found: ${JSON.stringify(
+        val,
+      )} (${typeof val}).`,
+    });
+  }
+
+  return errors;
+}
+
+/**  An option is a false global if it has the same name as a global only option
+ *   but is actually just the field of a non global option or field an children of the non global option
+ *   eg. token: it's global option used as the bot's token as well and
+ *   also it can be the token used for a platform inside the hostRules configuration
+ */
+export function isFalseGlobal(
+  optionName: string,
+  parentPath?: string,
+): boolean {
+  if (parentPath?.includes('hostRules')) {
+    if (
+      optionName === 'token' ||
+      optionName === 'username' ||
+      optionName === 'password'
+    ) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+function hasField(
+  customManager: Partial<RegexManagerConfig>,
+  field: string,
+): boolean {
+  const templateField = `${field}Template` as keyof RegexManagerTemplates;
+  return !!(
+    customManager[templateField] ??
+    customManager.matchStrings?.some((matchString) =>
+      matchString.includes(`(?<${field}>`),
+    )
+  );
+}
+
+export function validateRegexManagerFields(
+  customManager: Partial<RegexManagerConfig>,
+  currentPath: string,
+  errors: ValidationMessage[],
+): void {
+  if (is.nonEmptyArray(customManager.matchStrings)) {
+    for (const matchString of customManager.matchStrings) {
+      try {
+        regEx(matchString);
+      } catch (err) {
+        logger.debug(
+          { err },
+          'customManager.matchStrings regEx validation error',
+        );
+        errors.push({
+          topic: 'Configuration Error',
+          message: `Invalid regExp for ${currentPath}: \`${matchString}\``,
+        });
+      }
+    }
+  } else {
+    errors.push({
+      topic: 'Configuration Error',
+      message: `Each Custom Manager must contain a non-empty matchStrings array`,
+    });
+  }
+
+  const mandatoryFields = ['currentValue', 'datasource'];
+  for (const field of mandatoryFields) {
+    if (!hasField(customManager, field)) {
+      errors.push({
+        topic: 'Configuration Error',
+        message: `Regex Managers must contain ${field}Template configuration or regex group named ${field}`,
+      });
+    }
+  }
+
+  const nameFields = ['depName', 'packageName'];
+  if (!nameFields.some((field) => hasField(customManager, field))) {
+    errors.push({
+      topic: 'Configuration Error',
+      message: `Regex Managers must contain depName or packageName regex groups or templates`,
+    });
+  }
+}
diff --git a/lib/config/validation.spec.ts b/lib/config/validation.spec.ts
index a49e75c0de..927650a07d 100644
--- a/lib/config/validation.spec.ts
+++ b/lib/config/validation.spec.ts
@@ -4,22 +4,6 @@ import type { RenovateConfig } from './types';
 import * as configValidation from './validation';
 
 describe('config/validation', () => {
-  describe('getParentName()', () => {
-    it('ignores encrypted in root', () => {
-      expect(configValidation.getParentName('encrypted')).toBeEmptyString();
-    });
-
-    it('handles array types', () => {
-      expect(configValidation.getParentName('hostRules[1]')).toBe('hostRules');
-    });
-
-    it('handles encrypted within array types', () => {
-      expect(configValidation.getParentName('hostRules[0].encrypted')).toBe(
-        'hostRules',
-      );
-    });
-  });
-
   describe('validateConfig(config)', () => {
     it('returns deprecation warnings', async () => {
       const config = {
diff --git a/lib/config/validation.ts b/lib/config/validation.ts
index d139cb42c1..551c07ba9f 100644
--- a/lib/config/validation.ts
+++ b/lib/config/validation.ts
@@ -1,11 +1,6 @@
 import is from '@sindresorhus/is';
-import { logger } from '../logger';
 import { allManagersList, getManagerList } from '../modules/manager';
 import { isCustomManager } from '../modules/manager/custom';
-import type {
-  RegexManagerConfig,
-  RegexManagerTemplates,
-} from '../modules/manager/custom/regex/types';
 import type { CustomManager } from '../modules/manager/custom/types';
 import type { HostRule } from '../types';
 import { getExpression } from '../util/jsonata';
@@ -39,6 +34,13 @@ import { allowedStatusCheckStrings } from './types';
 import * as managerValidator from './validation-helpers/managers';
 import * as matchBaseBranchesValidator from './validation-helpers/match-base-branches';
 import * as regexOrGlobValidator from './validation-helpers/regex-glob-matchers';
+import {
+  getParentName,
+  isFalseGlobal,
+  validateNumber,
+  validatePlainObject,
+  validateRegexManagerFields,
+} from './validation-helpers/utils';
 
 const options = getOptions();
 
@@ -84,42 +86,6 @@ function isIgnored(key: string): boolean {
   return ignoredNodes.includes(key);
 }
 
-function validatePlainObject(val: Record<string, unknown>): true | string {
-  for (const [key, value] of Object.entries(val)) {
-    if (!is.string(value)) {
-      return key;
-    }
-  }
-  return true;
-}
-
-function validateNumber(
-  key: string,
-  val: unknown,
-  currentPath?: string,
-  subKey?: string,
-): ValidationMessage[] {
-  const errors: ValidationMessage[] = [];
-  const path = `${currentPath}${subKey ? '.' + subKey : ''}`;
-  if (is.number(val)) {
-    if (val < 0 && !optionAllowsNegativeIntegers.has(key)) {
-      errors.push({
-        topic: 'Configuration Error',
-        message: `Configuration option \`${path}\` should be a positive integer. Found negative value instead.`,
-      });
-    }
-  } else {
-    errors.push({
-      topic: 'Configuration Error',
-      message: `Configuration option \`${path}\` should be an integer. Found: ${JSON.stringify(
-        val,
-      )} (${typeof val}).`,
-    });
-  }
-
-  return errors;
-}
-
 function getUnsupportedEnabledManagers(enabledManagers: string[]): string[] {
   return enabledManagers.filter(
     (manager) => !allManagersList.includes(manager.replace('custom.', '')),
@@ -186,16 +152,6 @@ function initOptions(): void {
   optionsInitialized = true;
 }
 
-export function getParentName(parentPath: string | undefined): string {
-  return parentPath
-    ? parentPath
-        .replace(regEx(/\.?encrypted$/), '')
-        .replace(regEx(/\[\d+\]$/), '')
-        .split('.')
-        .pop()!
-    : '.';
-}
-
 export async function validateConfig(
   configType: 'global' | 'inherit' | 'repo',
   config: RenovateConfig,
@@ -370,7 +326,8 @@ export async function validateConfig(
             });
           }
         } else if (type === 'integer') {
-          errors.push(...validateNumber(key, val, currentPath));
+          const allowsNegative = optionAllowsNegativeIntegers.has(key);
+          errors.push(...validateNumber(key, val, allowsNegative, currentPath));
         } else if (type === 'array' && val) {
           if (is.array(val)) {
             for (const [subIndex, subval] of val.entries()) {
@@ -865,65 +822,6 @@ export async function validateConfig(
   return { errors, warnings };
 }
 
-function hasField(
-  customManager: Partial<RegexManagerConfig>,
-  field: string,
-): boolean {
-  const templateField = `${field}Template` as keyof RegexManagerTemplates;
-  return !!(
-    customManager[templateField] ??
-    customManager.matchStrings?.some((matchString) =>
-      matchString.includes(`(?<${field}>`),
-    )
-  );
-}
-
-function validateRegexManagerFields(
-  customManager: Partial<RegexManagerConfig>,
-  currentPath: string,
-  errors: ValidationMessage[],
-): void {
-  if (is.nonEmptyArray(customManager.matchStrings)) {
-    for (const matchString of customManager.matchStrings) {
-      try {
-        regEx(matchString);
-      } catch (err) {
-        logger.debug(
-          { err },
-          'customManager.matchStrings regEx validation error',
-        );
-        errors.push({
-          topic: 'Configuration Error',
-          message: `Invalid regExp for ${currentPath}: \`${matchString}\``,
-        });
-      }
-    }
-  } else {
-    errors.push({
-      topic: 'Configuration Error',
-      message: `Each Custom Manager must contain a non-empty matchStrings array`,
-    });
-  }
-
-  const mandatoryFields = ['currentValue', 'datasource'];
-  for (const field of mandatoryFields) {
-    if (!hasField(customManager, field)) {
-      errors.push({
-        topic: 'Configuration Error',
-        message: `Regex Managers must contain ${field}Template configuration or regex group named ${field}`,
-      });
-    }
-  }
-
-  const nameFields = ['depName', 'packageName'];
-  if (!nameFields.some((field) => hasField(customManager, field))) {
-    errors.push({
-      topic: 'Configuration Error',
-      message: `Regex Managers must contain depName or packageName regex groups or templates`,
-    });
-  }
-}
-
 /**
  * Basic validation for global config options
  */
@@ -1013,7 +911,8 @@ async function validateGlobalConfig(
         });
       }
     } else if (type === 'integer') {
-      warnings.push(...validateNumber(key, val, currentPath));
+      const allowsNegative = optionAllowsNegativeIntegers.has(key);
+      warnings.push(...validateNumber(key, val, allowsNegative, currentPath));
     } else if (type === 'boolean') {
       if (val !== true && val !== false) {
         warnings.push({
@@ -1079,8 +978,15 @@ async function validateGlobalConfig(
           }
         } else if (key === 'cacheTtlOverride') {
           for (const [subKey, subValue] of Object.entries(val)) {
+            const allowsNegative = optionAllowsNegativeIntegers.has(key);
             warnings.push(
-              ...validateNumber(key, subValue, currentPath, subKey),
+              ...validateNumber(
+                key,
+                subValue,
+                allowsNegative,
+                currentPath,
+                subKey,
+              ),
             );
           }
         } else {
@@ -1101,22 +1007,3 @@ async function validateGlobalConfig(
     }
   }
 }
-
-/**  An option is a false global if it has the same name as a global only option
- *   but is actually just the field of a non global option or field an children of the non global option
- *   eg. token: it's global option used as the bot's token as well and
- *   also it can be the token used for a platform inside the hostRules configuration
- */
-function isFalseGlobal(optionName: string, parentPath?: string): boolean {
-  if (parentPath?.includes('hostRules')) {
-    if (
-      optionName === 'token' ||
-      optionName === 'username' ||
-      optionName === 'password'
-    ) {
-      return true;
-    }
-  }
-
-  return false;
-}
-- 
GitLab