Skip to content
This repository has been archived by the owner on Jun 10, 2024. It is now read-only.

fix: validate files and ignores elements #103

Merged
merged 7 commits into from
Aug 29, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 9 additions & 53 deletions src/base-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
});
33 changes: 22 additions & 11 deletions src/config-array.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -40,15 +43,20 @@ 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) {
const validateConfig = { };
if ('files' in config) {
validateConfig.files = config.files;
}
if ('ignores' in config) {
validateConfig.ignores = config.ignores;
}
FILES_AND_IGNORES_SCHEMA.validate(validateConfig);
}

/**
Expand Down Expand Up @@ -268,9 +276,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 => {

Expand Down Expand Up @@ -425,7 +430,8 @@ export class ConfigArray extends Array {
explicitMatches: new Map(),
directoryMatches: new Map(),
files: undefined,
ignores: undefined
ignores: undefined,
filesAndIgnoresValidated: false
});

// load the configs into this array
Expand Down Expand Up @@ -697,6 +703,13 @@ export class ConfigArray extends Array {
if (finalConfig) {
return finalConfig;
}

// check if files and ignores have been already validated, otherwise validate them
const configArrayCache = dataCache.get(this);
if (!configArrayCache.filesAndIgnoresValidated) {
this.forEach(assertValidFilesAndIgnores);
configArrayCache.filesAndIgnoresValidated = true;
}

// next check to see if the file should be ignored

Expand Down Expand Up @@ -746,8 +759,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
Expand Down
85 changes: 85 additions & 0 deletions src/files-and-ignores-schema.js
Original file line number Diff line number Diff line change
@@ -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
}
});
98 changes: 98 additions & 0 deletions tests/config-array.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,104 @@ describe('ConfigArray', () => {
.throw(/non-empty array/);

});

it('should throw an error when files is undefined', async () => {
configs = new ConfigArray([
{
files: undefined
}
], { basePath });
await configs.normalize();

expect(() => {
configs.getConfig(path.resolve(basePath, 'foo.js'));
})
.to
.throw(/non-empty array/);

});

it('should throw an error when files contains an invalid element', async () => {
configs = new ConfigArray([
{
files: ['*.js', undefined]
}
], { basePath });
await configs.normalize();

expect(() => {
configs.getConfig(path.resolve(basePath, 'foo.js'));
})
.to
.throw('Key "files": Items must be a string, a function, or an array of strings and functions.');

});

it('should throw an error when ignores is undefined', async () => {
configs = new ConfigArray([
{
ignores: undefined
}
], { basePath });
await configs.normalize();

expect(() => {
configs.getConfig(path.resolve(basePath, 'foo.js'));
})
.to
.throw('Key "ignores": Expected value to be an array.');

});

it('should throw an error when a global ignores contains an invalid element', async () => {
configs = new ConfigArray([
{
ignores: ['ignored/**', -1]
}
], { basePath });
await configs.normalize();

expect(() => {
configs.getConfig(path.resolve(basePath, 'foo.js'));
})
.to
.throw('Key "ignores": Expected array to only contain strings and functions.');

});

it('should throw an error when a non-global ignores contains an invalid element', async () => {
configs = new ConfigArray([
{
files: ['*.js'],
ignores: [-1]
}
], { basePath });
await configs.normalize();

expect(() => {
configs.getConfig(path.resolve(basePath, 'foo.js'));
})
.to
.throw('Key "ignores": Expected array to only contain strings and functions.');

});

it('should throw an error when name is not a string', async () => {
configs = new ConfigArray([
{
files: ['**'],
name: true
}
], { basePath });
await configs.normalize();

expect(() => {
configs.getConfig(path.resolve(basePath, 'foo.js'));
})
.to
.throw('Key "name": Property must be a string.');

});
});

describe('ConfigArray members', () => {
Expand Down