Skip to content
Snippets Groups Projects
Select Git revision
21 results Searching

validation.js

Blame
    • Rhys Arkins's avatar
      9753f9dc
      feat: modular branchName/prTitle/commitMessage templating (#1760) · 9753f9dc
      Rhys Arkins authored
      This PR refactors `branchName`, `commitMessage` and `prTitle` so that they are more easily editable and hopefully more understandable. By breaking each up into subsections, users can modify one part without needing to copy/paste the entire string.
      
      Directly editing any of these fields will now be deprecated and a warning issued.
      feat: modular branchName/prTitle/commitMessage templating (#1760)
      Rhys Arkins authored
      This PR refactors `branchName`, `commitMessage` and `prTitle` so that they are more easily editable and hopefully more understandable. By breaking each up into subsections, users can modify one part without needing to copy/paste the entire string.
      
      Directly editing any of these fields will now be deprecated and a warning issued.
    validation.js 8.18 KiB
    const options = require('./definitions').getOptions();
    const { isValidSemver } = require('../util/semver');
    const { resolveConfigPresets } = require('./presets');
    const {
      hasValidSchedule,
      hasValidTimezone,
    } = require('../workers/branch/schedule');
    const safe = require('safe-regex');
    
    let optionTypes;
    
    module.exports = {
      validateConfig,
    };
    
    async function validateConfig(config, isPreset, parentPath) {
      if (!optionTypes) {
        optionTypes = {};
        options.forEach(option => {
          optionTypes[option.name] = option.type;
        });
      }
      let errors = [];
      let warnings = [];
    
      function getDeprecationMessage(option) {
        const deprecatedOptions = {
          branchName: `Direct editing of branchName is now deprecated. Please edit branchPrefix, managerBranchPrefix, or branchTopic instead`,
          commitMessage: `Direct editing of commitMessage is now deprecated. Please edit commitMessage's subcomponents instead.`,
          prTitle: `Direct editing of prTitle is now deprecated. Please edit commitMessage subcomponents instead as they will be passed through to prTitle.`,
        };
        return deprecatedOptions[option];
      }
    
      function isIgnored(key) {
        const ignoredNodes = [
          'prBanner',
          'depType',
          'npmToken',
          'packageFile',
          'forkToken',
          'repository',
        ];
        return ignoredNodes.indexOf(key) !== -1;
      }
    
      function isAFunction(value) {
        const getType = {};
        return value && getType.toString.call(value) === '[object Function]';
      }
    
      function isObject(obj) {
        return Object.prototype.toString.call(obj) === '[object Object]';
      }
    
      function isString(val) {
        return typeof val === 'string' || val instanceof String;
      }
    
      for (const [key, val] of Object.entries(config)) {
        const currentPath = parentPath ? `${parentPath}.${key}` : key;
        if (
          !isIgnored(key) && // We need to ignore some reserved keys
          !isAFunction(val) // Ignore all functions
        ) {
          if (getDeprecationMessage(key)) {
            warnings.push({
              depName: 'Deprecation Warning',
              message: getDeprecationMessage(key),
            });
          }
          if (!optionTypes[key]) {
            errors.push({
              depName: 'Configuration Error',
              message: `Invalid configuration option: \`${currentPath}\``,
            });
          } else if (key === 'schedule') {
            const [validSchedule, errorMessage] = hasValidSchedule(val);
            if (!validSchedule) {
              errors.push({
                depName: 'Configuration Error',
                message: `Invalid ${currentPath}: \`${errorMessage}\``,
              });
            }
          } else if (key === 'timezone' && val !== null) {
            const [validTimezone, errorMessage] = hasValidTimezone(val);
            if (!validTimezone) {
              errors.push({
                depName: 'Configuration Error',
                message: `${currentPath}: ${errorMessage}`,
              });
            }
          } else if (key === 'allowedVersions' && val !== null) {
            if (!isValidSemver(val)) {
              errors.push({
                depName: 'Configuration Error',
                message: `Invalid semver range for ${currentPath}: \`${val}\``,
              });
            }
          } else if (val != null) {
            const type = optionTypes[key];
            if (type === 'boolean') {
              if (val !== true && val !== false) {
                errors.push({
                  depName: 'Configuration Error',
                  message: `Configuration option \`${currentPath}\` should be boolean. Found: ${JSON.stringify(
                    val
                  )} (${typeof val})`,
                });
              }
            } else if (type === 'list' && val) {
              if (!Array.isArray(val)) {
                errors.push({
                  depName: 'Configuration Error',
                  message: `Configuration option \`${currentPath}\` should be a list (Array)`,
                });
              } else {
                for (const [subIndex, subval] of val.entries()) {
                  if (isObject(subval)) {
                    const subValidation = await module.exports.validateConfig(
                      subval,
                      isPreset,
                      `${currentPath}[${subIndex}]`
                    );
                    warnings = warnings.concat(subValidation.warnings);
                    errors = errors.concat(subValidation.errors);
                  }
                }
                if (key === 'extends') {
                  for (const subval of val) {
                    if (isString(subval) && subval.match(/^:timezone(.+)$/)) {
                      const [, timezone] = subval.match(/^:timezone\((.+)\)$/);
                      const [validTimezone, errorMessage] = hasValidTimezone(
                        timezone
                      );
                      if (!validTimezone) {
                        errors.push({
                          depName: 'Configuration Error',
                          message: `${currentPath}: ${errorMessage}`,
                        });
                      }
                    }
                  }
                }
    
                const selectors = [
                  'depTypeList',
                  'packageNames',
                  'packagePatterns',
                  'excludePackageNames',
                  'excludePackagePatterns',
                ];
                if (key === 'packageRules') {
                  for (const packageRule of val) {
                    let hasSelector = false;
                    if (isObject(packageRule)) {
                      const resolvedRule = await resolveConfigPresets(packageRule);
                      for (const pKey of Object.keys(resolvedRule)) {
                        if (selectors.includes(pKey)) {
                          hasSelector = true;
                        }
                      }
                      if (!hasSelector) {
                        const message = `${currentPath}: Each packageRule must contain at least one selector (${selectors.join(
                          ', '
                        )}). If you wish for configuration to apply to all packages, it is not necessary to place it inside a packageRule at all.`;
                        errors.push({
                          depName: 'Configuration Error',
                          message,
                        });
                      }
                    } else {
                      errors.push({
                        depName: 'Configuration Error',
                        message: `${currentPath} must contain JSON objects`,
                      });
                    }
                  }
                }
                if (
                  (key === 'packagePatterns' || key === 'excludePackagePatterns') &&
                  !(val && val.length === 1 && val[0] === '*')
                ) {
                  try {
                    RegExp(val);
                    if (!safe(val)) {
                      errors.push({
                        depName: 'Configuration Error',
                        message: `Unsafe regExp for ${currentPath}: \`${val}\``,
                      });
                    }
                  } catch (e) {
                    errors.push({
                      depName: 'Configuration Error',
                      message: `Invalid regExp for ${currentPath}: \`${val}\``,
                    });
                  }
                }
                if (
                  selectors.includes(key) &&
                  !(parentPath && parentPath.match(/packageRules\[\d+\]$/)) && // Inside a packageRule
                  (parentPath || !isPreset) // top level in a preset
                ) {
                  errors.push({
                    depName: 'Configuration Error',
                    message: `${currentPath}: ${key} should be inside a \`packageRule\` only`,
                  });
                }
              }
            } else if (type === 'string') {
              if (!isString(val)) {
                errors.push({
                  depName: 'Configuration Error',
                  message: `Configuration option \`${currentPath}\` should be a string`,
                });
              }
            } else if (type === 'json') {
              if (isObject(val)) {
                const subValidation = await module.exports.validateConfig(
                  val,
                  isPreset,
                  currentPath
                );
                warnings = warnings.concat(subValidation.warnings);
                errors = errors.concat(subValidation.errors);
              } else {
                errors.push({
                  depName: 'Configuration Error',
                  message: `Configuration option \`${currentPath}\` should be a json object`,
                });
              }
            }
          }
        }
      }
      return { errors, warnings };
    }