diff options
-rw-r--r-- | lib/config/validation-helpers/utils.spec.ts | 17 | ||||
-rw-r--r-- | lib/config/validation-helpers/utils.ts | 138 | ||||
-rw-r--r-- | lib/config/validation.spec.ts | 16 | ||||
-rw-r--r-- | lib/config/validation.ts | 151 |
4 files changed, 174 insertions, 148 deletions
diff --git a/lib/config/validation-helpers/utils.spec.ts b/lib/config/validation-helpers/utils.spec.ts new file mode 100644 index 00000000000..8344eed039e --- /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 00000000000..5d676bed324 --- /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 a49e75c0de3..927650a07d7 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 d139cb42c10..551c07ba9f0 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; -} |