Skip to content

Commit 867e175

Browse files
mark-wiemerJoshuaKGoldbergampagentclaude
authored
refactor: extract stripLeadingDashes helper + cover CLI arg-parsing edge cases (#6171)
* refactor: replace yargs-parser in CLI options Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-019f1368-7b8d-775f-bf67-9d38a255d13f * test: cover CLI arg-parsing edge cases Adds unit tests for scenarios in the new parseArgs-based CLI option loading that were previously unexercised: - parse-args: bare numeric positional -> UNSUPPORTED error; string and empty-string args input; falsy config objects skipped while merging; positionals merged across multiple config objects; non-boolean value kept for a boolean option in equals form. - options: non-Mocha parse errors are printed and exit with code 1. Test-only change; no production code is modified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor: extract stripLeadingDashes helper Replace seven copies of `flag.replace(/^--?/, "")` across parse-args.js, run-option-metadata.cjs, and node-flags.cjs with a single exported `stripLeadingDashes` helper in run-option-metadata.cjs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Revert changes to package-lock * Format --------- Co-authored-by: Josh Goldberg <git@joshuakgoldberg.com> Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5fcc0fe commit 867e175

5 files changed

Lines changed: 110 additions & 8 deletions

File tree

lib/cli/node-flags.cjs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
*/
88

99
const nodeFlags = process.allowedNodeEnvironmentFlags;
10-
const { isMochaFlag } = require("./run-option-metadata.cjs");
10+
const {
11+
isMochaFlag,
12+
stripLeadingDashes,
13+
} = require("./run-option-metadata.cjs");
1114
const { unparseNodeFlags } = require("./unparse-args.js");
1215

1316
/**
@@ -41,7 +44,7 @@ function isNodeFlag(flag, bareword = true) {
4144
return false;
4245
}
4346
// strip the leading dashes to match against subsequent checks
44-
flag = flag.replace(/^--?/, "");
47+
flag = stripLeadingDashes(flag);
4548
}
4649
return (
4750
// check actual node flags from `process.allowedNodeEnvironmentFlags`,

lib/cli/parse-args.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
aliases,
1010
expectedTypeForFlag,
1111
isMochaFlag,
12+
stripLeadingDashes,
1213
types,
1314
} from "./run-option-metadata.cjs";
1415
import { isNodeFlag } from "./node-flags.cjs";
@@ -161,7 +162,7 @@ const mergeConfigObjects = (defaultValues, configObjects) => {
161162

162163
const preprocessArgs = (allArgs) => {
163164
return allArgs.map((arg) => {
164-
const noPrefix = arg.replace(/^--?/, "");
165+
const noPrefix = stripLeadingDashes(arg);
165166
const [name, ...rest] = noPrefix.split("=");
166167
const canonical = canonicalOptionName(name.replace(/^no-/, ""));
167168
const negated = name.startsWith("no-");
@@ -194,7 +195,7 @@ const validateArgsBeforeParse = (allArgs) => {
194195
return;
195196
}
196197

197-
const name = canonicalOptionName(arg.replace(/^--?/, ""));
198+
const name = canonicalOptionName(stripLeadingDashes(arg));
198199
const next = allArgs[index + 1];
199200

200201
if (requiresValue(name) && (next === undefined || next.startsWith("-"))) {
@@ -249,7 +250,7 @@ const createErrorForNumericPositionalArg = (
249250
// 2. parseArgs() could not assign the numericArg to that flag because the
250251
// option has an incompatible datatype.
251252
const flag = allArgs.find((arg, index) => {
252-
const normalizedArg = arg.replace(/^--?/, "");
253+
const normalizedArg = stripLeadingDashes(arg);
253254
return (
254255
isMochaFlag(arg) &&
255256
allArgs[index + 1] === String(numericArg) &&
@@ -283,7 +284,7 @@ export const parseMochaArgs = (
283284
const pair = arg.split("=");
284285
let flag = pair[0];
285286
if (isNodeFlag(flag, false)) {
286-
flag = flag.replace(/^--?/, "");
287+
flag = stripLeadingDashes(flag);
287288
return acc.concat([[flag, arg.includes("=") ? pair[1] : true]]);
288289
}
289290
return acc;

lib/cli/run-option-metadata.cjs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,14 +169,23 @@ const ALL_MOCHA_FLAGS = Object.keys(types).reduce((acc, key) => {
169169
return acc;
170170
}, new Set());
171171

172+
/**
173+
* Strips one or two leading dashes from a flag, e.g. `--grep` becomes `grep`
174+
* and `-g` becomes `g`. Barewords are returned unchanged.
175+
* @param {string} flag - Flag to normalize (with or without leading dashes)
176+
* @returns {string} The flag without its leading dashes
177+
* @private
178+
*/
179+
const stripLeadingDashes = (flag) => flag.replace(/^--?/, "");
180+
172181
/**
173182
* Returns `true` if the provided `flag` is known to Mocha.
174183
* @param {string} flag - Flag to check
175184
* @returns {boolean} If `true`, this is a Mocha flag
176185
* @private
177186
*/
178187
const isMochaFlag = (flag) => {
179-
return ALL_MOCHA_FLAGS.has(flag.replace(/^--?/, ""));
188+
return ALL_MOCHA_FLAGS.has(stripLeadingDashes(flag));
180189
};
181190

182191
/**
@@ -186,7 +195,7 @@ const isMochaFlag = (flag) => {
186195
* @private
187196
*/
188197
const expectedTypeForFlag = (flag) => {
189-
const normalizedName = flag.replace(/^--?/, "");
198+
const normalizedName = stripLeadingDashes(flag);
190199

191200
// If flag is an alias, get it's full name.
192201
const fullFlagName =
@@ -204,3 +213,4 @@ exports.aliases = aliases;
204213
exports.getRunOptionDefinitions = getRunOptionDefinitions;
205214
exports.isMochaFlag = isMochaFlag;
206215
exports.expectedTypeForFlag = expectedTypeForFlag;
216+
exports.stripLeadingDashes = stripLeadingDashes;

test/node-unit/cli/options.spec.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -788,5 +788,44 @@ describe("options", function () {
788788
expect(() => loadOptions(`--spec ${numericArg}`), "not to throw");
789789
});
790790
});
791+
792+
describe("when parsing throws a non-Mocha error", function () {
793+
let exit;
794+
let consoleError;
795+
796+
beforeEach(function () {
797+
readFileSync = sinon.stub();
798+
findConfig = sinon.stub();
799+
loadConfig = sinon.stub();
800+
findupSync = sinon.stub();
801+
loadOptions = proxyLoadOptions({
802+
readFileSync,
803+
findConfig,
804+
loadConfig,
805+
findupSync,
806+
});
807+
exit = sinon.stub(process, "exit");
808+
consoleError = sinon.stub(console, "error");
809+
});
810+
811+
it("prints the error message and exits with code 1", function () {
812+
try {
813+
// `--grep` requires a value, so a dash-prefixed follower throws a
814+
// plain (non-Mocha) Error rather than a Mocha error.
815+
loadOptions(["--grep", "-foo"]);
816+
} catch {
817+
// `process.exit` is stubbed, so execution continues past it and may
818+
// throw later; we only assert that the error was reported and exit
819+
// was requested.
820+
}
821+
expect(exit, "to have a call satisfying", [1]);
822+
expect(consoleError, "was called");
823+
expect(
824+
consoleError.firstCall.args[0],
825+
"to contain",
826+
"Not enough arguments following: grep",
827+
);
828+
});
829+
});
791830
});
792831
});

test/node-unit/cli/parse-args.spec.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,4 +289,53 @@ describe("parse-args", function () {
289289
expected: "boolean",
290290
});
291291
});
292+
293+
it("throws an unsupported error for a bare numeric positional argument", function () {
294+
expect(() => parseMochaArgs(["123"], defaults), "to throw", {
295+
message: "Option 123 is unsupported by the mocha cli",
296+
code: constants.UNSUPPORTED,
297+
});
298+
});
299+
300+
it("accepts a space-delimited string of arguments", function () {
301+
expect(parseMochaArgs("--bail --grep foo", defaults), "to satisfy", {
302+
bail: true,
303+
grep: "foo",
304+
});
305+
});
306+
307+
it("treats an empty string of arguments as no arguments", function () {
308+
expect(parseMochaArgs("", defaults), "to satisfy", {
309+
_: [],
310+
timeout: 1000,
311+
extension: ["js"],
312+
});
313+
});
314+
315+
it("ignores falsy config objects while merging the rest", function () {
316+
expect(
317+
parseMochaArgs(["--bail"], defaults, null, undefined, { grep: "x" }),
318+
"to satisfy",
319+
{
320+
bail: true,
321+
grep: "x",
322+
},
323+
);
324+
});
325+
326+
it("merges positional arguments across multiple config objects", function () {
327+
expect(
328+
parseMochaArgs([], defaults, { _: ["a.js"] }, { _: ["b.js"] }),
329+
"to satisfy",
330+
{
331+
_: ["a.js", "b.js"],
332+
},
333+
);
334+
});
335+
336+
it("keeps a non-boolean value for a boolean option given in equals form", function () {
337+
expect(parseMochaArgs(["--bail=maybe"], defaults), "to satisfy", {
338+
bail: "maybe",
339+
});
340+
});
292341
});

0 commit comments

Comments
 (0)