diff --git a/bin/config-validator.js b/bin/config-validator.js index 5448b3a369bf206f19aafddc96881e74922b416c..2b921495ae35d4de8b34c157c878968bb7130684 100644 --- a/bin/config-validator.js +++ b/bin/config-validator.js @@ -11,8 +11,8 @@ initLogger(); let returnVal = 0; -async function validate(desc, config) { - const res = await validateConfig(massageConfig(config)); +async function validate(desc, config, isPreset = false) { + const res = await validateConfig(massageConfig(config), isPreset); if (res.errors.length) { console.log( `${desc} contains errors:\n\n${JSON.stringify(res.errors, null, 2)}` @@ -57,7 +57,7 @@ async function validate(desc, config) { if (pkgJson['renovate-config']) { console.log(`Validating package.json > renovate-config`); for (const presetConfig of Object.values(pkgJson['renovate-config'])) { - await validate('package.json > renovate-config', presetConfig); + await validate('package.json > renovate-config', presetConfig, true); } } } catch (err) { diff --git a/lib/config/validation.js b/lib/config/validation.js index 8c146badf2154311bec7ab3d00d7bf2f4e565345..a95fab4202ddf945e18ec6b03c31049c02b6eef6 100644 --- a/lib/config/validation.js +++ b/lib/config/validation.js @@ -12,7 +12,7 @@ module.exports = { validateConfig, }; -async function validateConfig(config, parentPath) { +async function validateConfig(config, isPreset, parentPath) { if (!optionTypes) { optionTypes = {}; options.forEach(option => { @@ -103,6 +103,7 @@ async function validateConfig(config, parentPath) { if (isObject(subval)) { const subValidation = await module.exports.validateConfig( subval, + isPreset, `${currentPath}[${subIndex}]` ); warnings = warnings.concat(subValidation.warnings); @@ -125,14 +126,15 @@ async function validateConfig(config, parentPath) { } } } + + const selectors = [ + 'depTypeList', + 'packageNames', + 'packagePatterns', + 'excludePackageNames', + 'excludePackagePatterns', + ]; if (key === 'packageRules') { - const selectors = [ - 'depTypeList', - 'packageNames', - 'packagePatterns', - 'excludePackageNames', - 'excludePackagePatterns', - ]; for (const packageRule of val) { let hasSelector = false; if (isObject(packageRule)) { @@ -172,6 +174,16 @@ async function validateConfig(config, parentPath) { }); } } + 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)) { @@ -184,6 +196,7 @@ async function validateConfig(config, parentPath) { if (isObject(val)) { const subValidation = await module.exports.validateConfig( val, + isPreset, currentPath ); warnings = warnings.concat(subValidation.warnings); diff --git a/test/config/__snapshots__/validation.spec.js.snap b/test/config/__snapshots__/validation.spec.js.snap index 10014e6753630eb2596cf5f671e4102e9a676150..d2ac49cb51c6f315497a285d9e2cc0b45ef04da5 100644 --- a/test/config/__snapshots__/validation.spec.js.snap +++ b/test/config/__snapshots__/validation.spec.js.snap @@ -57,72 +57,30 @@ Array [ ] `; -exports[`config/validation validateConfig(config) errors for all types 2`] = ` +exports[`config/validation validateConfig(config) ignore packageRule nesting validation for presets 1`] = `Array []`; + +exports[`config/validation validateConfig(config) returns nested errors 1`] = ` Array [ Object { "depName": "Configuration Error", - "message": "Invalid semver range for allowedVersions: \`foo\`", - }, - Object { - "depName": "Configuration Error", - "message": "Configuration option \`enabled\` should be boolean. Found: 1 (number)", - }, - Object { - "depName": "Configuration Error", - "message": "Invalid schedule: \`Schedule \\"every 15 mins every weekday\\" should not specify minutes\`", - }, - Object { - "depName": "Configuration Error", - "message": "timezone: Invalid timezone: Asia", - }, - Object { - "depName": "Configuration Error", - "message": "Configuration option \`labels\` should be a list (Array)", - }, - Object { - "depName": "Configuration Error", - "message": "Configuration option \`semanticCommitType\` should be a string", - }, - Object { - "depName": "Configuration Error", - "message": "Configuration option \`lockFileMaintenance\` should be a json object", - }, - Object { - "depName": "Configuration Error", - "message": "extends: Invalid timezone: Europe/Brussel", - }, - Object { - "depName": "Configuration Error", - "message": "Invalid configuration option: \`packageRules[1].foo\`", - }, - Object { - "depName": "Configuration Error", - "message": "Configuration option \`packageRules[3].packagePatterns\` should be a list (Array)", - }, - Object { - "depName": "Configuration Error", - "message": "Invalid regExp for packageRules[3].excludePackagePatterns: \`abc ([a-z]+) ([a-z]+))\`", - }, - Object { - "depName": "Configuration Error", - "message": "packageRules: Each packageRule must contain at least one selector (depTypeList, packageNames, packagePatterns, excludePackageNames, excludePackagePatterns). If you wish for configuration to apply to all packages, it is not necessary to place it inside a packageRule at all.", + "message": "Invalid configuration option: \`foo\`", }, Object { "depName": "Configuration Error", - "message": "packageRules must contain JSON objects", + "message": "Invalid configuration option: \`lockFileMaintenance.bar\`", }, ] `; -exports[`config/validation validateConfig(config) returns nested errors 1`] = ` +exports[`config/validation validateConfig(config) selectors outside packageRules array trigger errors 1`] = ` Array [ Object { "depName": "Configuration Error", - "message": "Invalid configuration option: \`foo\`", + "message": "packageNames: packageNames should be inside a \`packageRule\` only", }, Object { "depName": "Configuration Error", - "message": "Invalid configuration option: \`lockFileMaintenance.bar\`", + "message": "docker.minor.packageNames: packageNames should be inside a \`packageRule\` only", }, ] `; diff --git a/test/config/validation.spec.js b/test/config/validation.spec.js index 0381dbe8a46c3d1381d6aaf0bab0b86665f97814..65614bd55e4e71c1256f2e9f256a4d8ff3601737 100644 --- a/test/config/validation.spec.js +++ b/test/config/validation.spec.js @@ -57,7 +57,47 @@ describe('config/validation', () => { expect(warnings).toHaveLength(0); expect(errors).toMatchSnapshot(); expect(errors).toHaveLength(13); + }); + it('selectors outside packageRules array trigger errors', async () => { + const config = { + packageNames: ['angular'], + meteor: { + packageRules: [ + { + packageNames: ['meteor'], + }, + ], + }, + docker: { + minor: { + packageNames: ['testPackage'], + }, + }, + }; + const { warnings, errors } = await configValidation.validateConfig( + config + ); + expect(warnings).toHaveLength(0); + expect(errors).toMatchSnapshot(); + expect(errors).toHaveLength(2); + }); + it('ignore packageRule nesting validation for presets', async () => { + const config = { + description: ['All angular.js packages'], + packageNames: [ + 'angular', + 'angular-animate', + 'angular-scroll', + 'angular-sanitize', + ], + }; + const { warnings, errors } = await configValidation.validateConfig( + config, + true + ); + expect(warnings).toHaveLength(0); expect(errors).toMatchSnapshot(); + expect(errors).toHaveLength(0); }); }); });