diff --git a/src/base-schema.js b/src/base-schema.js index 92ea41e..dd7ece0 100644 --- a/src/base-schema.js +++ b/src/base-schema.js @@ -7,31 +7,13 @@ // Helpers //------------------------------------------------------------------------------ -/** - * Assets that a given value is an array. - * @param {*} value The value to check. - * @returns {void} - * @throws {TypeError} When the value is not an array. - */ -function assertIsArray(value) { - if (!Array.isArray(value)) { - throw new TypeError('Expected value to be an array.'); - } -} - -/** - * Assets that a given value is an array containing only strings and functions. - * @param {*} value The value to check. - * @returns {void} - * @throws {TypeError} When the value is not an array of strings and functions. - */ -function assertIsArrayOfStringsAndFunctions(value, name) { - assertIsArray(value, name); - - if (value.some(item => typeof item !== 'string' && typeof item !== 'function')) { - throw new TypeError('Expected array to only contain strings.'); - } -} +const NOOP_STRATEGY = { + required: false, + merge() { + return undefined; + }, + validate() { } +}; //------------------------------------------------------------------------------ // Exports @@ -53,32 +35,6 @@ export const baseSchema = Object.freeze({ } } }, - files: { - required: false, - merge() { - return undefined; - }, - validate(value) { - - // first check if it's an array - assertIsArray(value); - - // then check each member - value.forEach(item => { - if (Array.isArray(item)) { - assertIsArrayOfStringsAndFunctions(item); - } else if (typeof item !== 'string' && typeof item !== 'function') { - throw new TypeError('Items must be a string, a function, or an array of strings and functions.'); - } - }); - - } - }, - ignores: { - required: false, - merge() { - return undefined; - }, - validate: assertIsArrayOfStringsAndFunctions - } + files: NOOP_STRATEGY, + ignores: NOOP_STRATEGY }); diff --git a/src/config-array.js b/src/config-array.js index 7b6840f..4167a42 100644 --- a/src/config-array.js +++ b/src/config-array.js @@ -13,6 +13,7 @@ import createDebug from 'debug'; import { ObjectSchema } from '@humanwhocodes/object-schema'; import { baseSchema } from './base-schema.js'; +import { filesAndIgnoresSchema } from './files-and-ignores-schema.js'; //------------------------------------------------------------------------------ // Helpers @@ -30,6 +31,8 @@ const MINIMATCH_OPTIONS = { const CONFIG_TYPES = new Set(['array', 'function']); +const FILES_AND_IGNORES_SCHEMA = new ObjectSchema(filesAndIgnoresSchema); + /** * Shorthand for checking if a value is a string. * @param {any} value The value to check. @@ -40,15 +43,23 @@ function isString(value) { } /** - * Asserts that the files key of a config object is a nonempty array. + * Asserts that the files and ignores keys of a config object are valid as per base schema. * @param {object} config The config object to check. * @returns {void} - * @throws {TypeError} If the files key isn't a nonempty array. + * @throws {TypeError} If the files and ignores keys of a config object are not valid. */ -function assertNonEmptyFilesArray(config) { - if (!Array.isArray(config.files) || config.files.length === 0) { - throw new TypeError('The files key must be a non-empty array.'); +function assertValidFilesAndIgnores(config) { + if (!config || typeof config !== 'object') { + return; + } + const validateConfig = { }; + if ('files' in config) { + validateConfig.files = config.files; + } + if ('ignores' in config) { + validateConfig.ignores = config.ignores; } + FILES_AND_IGNORES_SCHEMA.validate(validateConfig); } /** @@ -268,9 +279,6 @@ function pathMatches(filePath, basePath, config) { */ const relativeFilePath = path.relative(basePath, filePath); - // if files isn't an array, throw an error - assertNonEmptyFilesArray(config); - // match both strings and functions const match = pattern => { @@ -577,6 +585,7 @@ export class ConfigArray extends Array { const normalizedConfigs = await normalize(this, context, this.extraConfigTypes); this.length = 0; this.push(...normalizedConfigs.map(this[ConfigArraySymbol.preprocessConfig].bind(this))); + this.forEach(assertValidFilesAndIgnores); this[ConfigArraySymbol.isNormalized] = true; // prevent further changes @@ -598,6 +607,7 @@ export class ConfigArray extends Array { const normalizedConfigs = normalizeSync(this, context, this.extraConfigTypes); this.length = 0; this.push(...normalizedConfigs.map(this[ConfigArraySymbol.preprocessConfig].bind(this))); + this.forEach(assertValidFilesAndIgnores); this[ConfigArraySymbol.isNormalized] = true; // prevent further changes @@ -746,8 +756,6 @@ export class ConfigArray extends Array { return; } - assertNonEmptyFilesArray(config); - /* * If a config has a files pattern ending in /** or /*, and the * filePath only matches those patterns, then the config is only diff --git a/src/files-and-ignores-schema.js b/src/files-and-ignores-schema.js new file mode 100644 index 0000000..8762934 --- /dev/null +++ b/src/files-and-ignores-schema.js @@ -0,0 +1,85 @@ +/** + * @fileoverview ConfigSchema + * @author Nicholas C. Zakas + */ + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Asserts that a given value is an array. + * @param {*} value The value to check. + * @returns {void} + * @throws {TypeError} When the value is not an array. + */ +function assertIsArray(value) { + if (!Array.isArray(value)) { + throw new TypeError('Expected value to be an array.'); + } +} + +/** + * Asserts that a given value is an array containing only strings and functions. + * @param {*} value The value to check. + * @returns {void} + * @throws {TypeError} When the value is not an array of strings and functions. + */ +function assertIsArrayOfStringsAndFunctions(value, name) { + assertIsArray(value, name); + + if (value.some(item => typeof item !== 'string' && typeof item !== 'function')) { + throw new TypeError('Expected array to only contain strings and functions.'); + } +} + +/** + * Asserts that a given value is a non-empty array. + * @param {*} value The value to check. + * @returns {void} + * @throws {TypeError} When the value is not an array or an empty array. + */ +function assertIsNonEmptyArray(value) { + if (!Array.isArray(value) || value.length === 0) { + throw new TypeError('Expected value to be a non-empty array.'); + } +} + +//------------------------------------------------------------------------------ +// Exports +//------------------------------------------------------------------------------ + +/** + * The schema for `files` and `ignores` that every ConfigArray uses. + * @type Object + */ +export const filesAndIgnoresSchema = Object.freeze({ + files: { + required: false, + merge() { + return undefined; + }, + validate(value) { + + // first check if it's an array + assertIsNonEmptyArray(value); + + // then check each member + value.forEach(item => { + if (Array.isArray(item)) { + assertIsArrayOfStringsAndFunctions(item); + } else if (typeof item !== 'string' && typeof item !== 'function') { + throw new TypeError('Items must be a string, a function, or an array of strings and functions.'); + } + }); + + } + }, + ignores: { + required: false, + merge() { + return undefined; + }, + validate: assertIsArrayOfStringsAndFunctions + } +}); diff --git a/tests/config-array.test.js b/tests/config-array.test.js index ee2d3f6..9cbcecb 100644 --- a/tests/config-array.test.js +++ b/tests/config-array.test.js @@ -185,7 +185,7 @@ describe('ConfigArray', () => { it('should not throw an error when objects are allowed', async () => { configs = new ConfigArray([ { - files: '*.js' + files: ['*.js'] } ], { basePath @@ -198,7 +198,7 @@ describe('ConfigArray', () => { configs = new ConfigArray([ [ { - files: '*.js' + files: ['*.js'] } ] ], { @@ -301,11 +301,117 @@ describe('ConfigArray', () => { }); describe('Validation', () => { - it('should throw an error when files is not an array', async () => { - configs = new ConfigArray([ + + function testValidationError({ only = false, title, configs, expectedError }) { + + const localIt = only ? it.only : it; + + localIt(`${title} when calling normalize()`, async () => { + const configArray = new ConfigArray(configs, { basePath }); + + let actualError; + try { + await configArray.normalize(); + } catch (error) { + actualError = error; + } + expect(() => { + if (actualError) { + throw actualError; + } + }) + .to + .throw(expectedError); + + }); + + localIt(`${title} when calling normalizeSync()`, () => { + const configArray = new ConfigArray(configs, { basePath }); + + expect(() => configArray.normalizeSync()) + .to + .throw(expectedError); + + }); + } + + testValidationError({ + title: 'should throw an error when files is not an array', + configs: [ { files: '*.js' } + ], + expectedError: /non-empty array/ + }); + + testValidationError({ + title: 'should throw an error when files is an empty array', + configs: [ + { + files: [] + } + ], + expectedError: /non-empty array/ + }); + + testValidationError({ + title: 'should throw an error when files is undefined', + configs: [ + { + files: undefined + } + ], + expectedError: /non-empty array/ + }); + + testValidationError({ + title: 'should throw an error when files contains an invalid element', + configs: [ + { + files: ['*.js', undefined] + } + ], + expectedError: 'Key "files": Items must be a string, a function, or an array of strings and functions.' + }); + + testValidationError({ + title: 'should throw an error when ignores is undefined', + configs: [ + { + ignores: undefined + } + ], + expectedError: 'Key "ignores": Expected value to be an array.' + }); + + testValidationError({ + title: 'should throw an error when a global ignores contains an invalid element', + configs: [ + { + ignores: ['ignored/**', -1] + } + ], + expectedError: 'Key "ignores": Expected array to only contain strings and functions.' + }); + + testValidationError({ + title: 'should throw an error when a non-global ignores contains an invalid element', + configs: [ + { + files: ['*.js'], + ignores: [-1] + } + ], + expectedError: 'Key "ignores": Expected array to only contain strings and functions.' + }); + + it('should throw an error when a config is not an object', async () => { + configs = new ConfigArray([ + { + files: ['*.js'] + }, + 'eslint:reccommended' // typo ], { basePath }); await configs.normalize(); @@ -313,14 +419,15 @@ describe('ConfigArray', () => { configs.getConfig(path.resolve(basePath, 'foo.js')); }) .to - .throw(/non-empty array/); + .throw('All arguments must be objects.'); }); - it('should throw an error when files is an empty array', async () => { + it('should throw an error when name is not a string', async () => { configs = new ConfigArray([ { - files: [] + files: ['**'], + name: true } ], { basePath }); await configs.normalize(); @@ -329,7 +436,7 @@ describe('ConfigArray', () => { configs.getConfig(path.resolve(basePath, 'foo.js')); }) .to - .throw(/non-empty array/); + .throw('Key "name": Property must be a string.'); }); });