diff --git a/lib/cli/options.js b/lib/cli/options.js index b7779025b2..b25fe64db8 100644 --- a/lib/cli/options.js +++ b/lib/cli/options.js @@ -10,7 +10,12 @@ const fs = require('fs'); const ansi = require('ansi-colors'); const yargsParser = require('yargs-parser'); -const {types, aliases} = require('./run-option-metadata'); +const { + types, + aliases, + isMochaFlag, + expectedTypeForFlag +} = require('./run-option-metadata'); const {ONE_AND_DONE_ARGS} = require('./one-and-dones'); const mocharc = require('../mocharc.json'); const {list} = require('./run-helpers'); @@ -18,7 +23,12 @@ const {loadConfig, findConfig} = require('./config'); const findUp = require('find-up'); const debug = require('debug')('mocha:cli:options'); const {isNodeFlag} = require('./node-flags'); -const {createUnparsableFileError} = require('../errors'); +const { + createUnparsableFileError, + createInvalidArgumentTypeError, + createUnsupportedError +} = require('../errors'); +const {isNumeric} = require('../utils'); /** * The `yargs-parser` namespace @@ -108,23 +118,21 @@ const parse = (args = [], defaultValues = {}, ...configObjects) => { // 3. to avoid explicitly defining the set of them, we tell yargs-parser they // are ALL boolean flags. // 4. we can then reapply the values after yargs-parser is done. - const nodeArgs = (Array.isArray(args) ? args : args.split(' ')).reduce( - (acc, arg) => { - if (typeof arg !== 'string') { - throw new Error(`Invalid option ${arg} passed to mocha cli`); - } - const pair = arg.split('='); - let flag = pair[0]; - if (isNodeFlag(flag, false)) { - flag = flag.replace(/^--?/, ''); - return arg.includes('=') - ? acc.concat([[flag, pair[1]]]) - : acc.concat([[flag, true]]); - } - return acc; - }, - [] - ); + const allArgs = Array.isArray(args) ? args : args.split(' '); + const nodeArgs = allArgs.reduce((acc, arg) => { + if (typeof arg !== 'string') { + throw new Error(`Invalid option ${arg} passed to mocha cli`); + } + const pair = arg.split('='); + let flag = pair[0]; + if (isNodeFlag(flag, false)) { + flag = flag.replace(/^--?/, ''); + return arg.includes('=') + ? acc.concat([[flag, pair[1]]]) + : acc.concat([[flag, true]]); + } + return acc; + }, []); const result = yargsParser.detailed(args, { configuration, @@ -148,6 +156,30 @@ const parse = (args = [], defaultValues = {}, ...configObjects) => { result.argv[key] = value; }); + const numericPositionalArgs = result.argv._.filter(arg => isNumeric(arg)); + numericPositionalArgs.forEach(numericArg => { + const flag = allArgs + .map(arg => arg.replace(/^--?/, '')) + .find((arg, index) => { + return ( + isMochaFlag(arg) && + args[index + 1] === String(numericArg) && + String(result.argv[arg]) !== String(numericArg) + ); + }); + if (flag) { + throw createInvalidArgumentTypeError( + `Flag ${flag} has invalid arg ${numericArg}`, + numericArg, + expectedTypeForFlag(flag) + ); + } else { + throw createUnsupportedError( + `Invalid option ${numericArg} passed to mocha cli` + ); + } + }); + return result.argv; }; @@ -195,8 +227,7 @@ const loadPkgRc = (args = {}) => { filepath ); } else { - debug('failed to read default package.json at %s; ignoring', - filepath); + debug('failed to read default package.json at %s; ignoring', filepath); return result; } } diff --git a/lib/cli/run-option-metadata.js b/lib/cli/run-option-metadata.js index 83aa70dd7a..44ebf1e988 100644 --- a/lib/cli/run-option-metadata.js +++ b/lib/cli/run-option-metadata.js @@ -114,3 +114,23 @@ const ALL_MOCHA_FLAGS = Object.keys(TYPES).reduce((acc, key) => { exports.isMochaFlag = flag => { return ALL_MOCHA_FLAGS.has(flag.replace(/^--?/, '')); }; + +/** + * Returns expected yarg option type for a given mocha flag. + * @param {string} flag - Flag to check (can be with out without leading "--"") + * @returns {string | undefined} - If flag is a valid mocha flag, the expected type of argument for this flag is returned, otherwise undefined is returned. + */ +exports.expectedTypeForFlag = flag => { + const normalizedName = flag?.replace(/^--?/, ''); + + // If flag is an alias, get it's full name. + const aliases = exports.aliases; + const fullFlagName = + Object.keys(aliases).find(flagName => + aliases[flagName].includes(normalizedName) + ) || normalizedName; + + return Object.keys(TYPES).find(flagType => + TYPES[flagType].includes(fullFlagName) + ); +}; diff --git a/lib/utils.js b/lib/utils.js index 31b313a6e0..89b21a32d6 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -689,3 +689,10 @@ exports.breakCircularDeps = inputObj => { return _breakCircularDeps(inputObj); }; + +/** + * Checks if provided input can be parsed as a JavaScript Number. + */ +exports.isNumeric = input => { + return !isNaN(parseFloat(input)); +};