renovate/lib/config/validation.js
Rhys Arkins 1da53671b8 feat: vulnerabilityAlertsOnly
For use in new app
2018-07-29 09:03:54 +02:00

248 lines
8.6 KiB
JavaScript

const is = require('@sindresorhus/is');
const safe = require('safe-regex');
const options = require('./definitions').getOptions();
const { resolveConfigPresets } = require('./presets');
const {
hasValidSchedule,
hasValidTimezone,
} = require('../workers/branch/schedule');
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 = [
'$schema',
'prBanner',
'depType',
'npmToken',
'packageFile',
'forkToken',
'repository',
'vulnerabilityAlertsOnly',
];
return ignoredNodes.includes(key);
}
for (const [key, val] of Object.entries(config)) {
const currentPath = parentPath ? `${parentPath}.${key}` : key;
if (
!isIgnored(key) && // We need to ignore some reserved keys
!is.function(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 (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 (!is.array(val)) {
errors.push({
depName: 'Configuration Error',
message: `Configuration option \`${currentPath}\` should be a list (Array)`,
});
} else {
for (const [subIndex, subval] of val.entries()) {
if (is.object(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 (is.string(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 = [
'paths',
'depTypeList',
'packageNames',
'packagePatterns',
'excludePackageNames',
'excludePackagePatterns',
'updateTypes',
];
if (key === 'packageRules') {
for (const packageRule of val) {
let hasSelector = false;
if (is.object(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 (key === 'fileMatch') {
try {
for (const fileMatch of val) {
RegExp(fileMatch);
if (!safe(fileMatch)) {
errors.push({
depName: 'Configuration Error',
message: `Unsafe regExp for ${currentPath}: \`${fileMatch}\``,
});
}
}
} catch (e) {
errors.push({
depName: 'Configuration Error',
message: `Invalid regExp for ${currentPath}: \`${val}\``,
});
}
}
if (
(selectors.includes(key) || key === 'matchCurrentVersion') &&
!(parentPath && parentPath.match(/p.*Rules\[\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 (!is.string(val)) {
errors.push({
depName: 'Configuration Error',
message: `Configuration option \`${currentPath}\` should be a string`,
});
}
} else if (type === 'json') {
if (is.object(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`,
});
}
}
}
}
}
function sortAll(a, b) {
if (a.depName === b.depName) {
return a.message > b.message;
}
// istanbul ignore next
return a.depName > b.depName;
}
errors.sort(sortAll);
warnings.sort(sortAll);
return { errors, warnings };
}