Repository: denoland/deno
Pull request: fix(ext/node): add support for --no- prefix in parseArgs()

closes #28606

I have the implementation working and validated against the Node.js tests. I'm looking for knowledge on integrating the tests properly.

The existing parseArgs tests seem to be managed by a script that pulls them from Node.js. What's the preferred approach: update the files generated by that script, or add these tests separately? Also, what version of Node will the newly pulled tests come from?

Changed files:
filename: ext/node/polyfills/internal/util/parse_args/parse_args.js
patch: @@ -332,8 +332,15 @@ export const parseArgs = (config = { __proto__: null }) => {
   const allowPositionals = objectGetOwn(config, "allowPositionals") ?? !strict;
   const returnTokens = objectGetOwn(config, "tokens") ?? false;
   const options = objectGetOwn(config, "options") ?? { __proto__: null };
+  const allowNegative = objectGetOwn(config, "allowNegative") ?? false;
   // Bundle these up for passing to strict-mode checks.
-  const parseConfig = { args, strict, options, allowPositionals };
+  const parseConfig = {
+    args,
+    strict,
+    options,
+    allowPositionals,
+    allowNegative,
+  };
 
   // Validate input configuration.
   validateArray(args, "args");
@@ -400,11 +407,39 @@ export const parseArgs = (config = { __proto__: null }) => {
   }
   ArrayPrototypeForEach(tokens, (token) => {
     if (token.kind === "option") {
+      let name = token.name;
+      let value = token.value;
+
+      if (allowNegative && StringPrototypeStartsWith(token.rawName, "--no-")) {
+        name = StringPrototypeSlice(token.rawName, 5);
+        if (!ObjectHasOwn(options, name)) {
+          if (strict) {
+            throw new ERR_PARSE_ARGS_UNKNOWN_OPTION(
+              token.rawName,
+              allowPositionals,
+            );
+          }
+          value = false;
+        } else if (optionsGetOwn(options, name, "type") !== "boolean") {
+          throw new ERR_PARSE_ARGS_UNKNOWN_OPTION(
+            token.rawName,
+            allowPositionals,
+          );
+        } else {
+          value = false;
+        }
+      }
+
+      const checkToken = {
+        ...token,
+        name: name, // Use the processed name (without --no- prefix if applicable)
+      };
+
       if (strict) {
-        checkOptionUsage(parseConfig, token);
-        checkOptionLikeValue(token);
+        checkOptionUsage(parseConfig, checkToken);
+        checkOptionLikeValue(checkToken);
       }
-      storeOption(token.name, token.value, options, result.values);
+      storeOption(name, value, options, result.values);
     } else if (token.kind === "positional") {
       if (!allowPositionals) {
         throw new ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL(token.value);

filename: tests/node_compat/config.jsonc
patch: @@ -116,7 +116,6 @@
       "test-net-server-try-ports.js",
       "test-net-socket-timeout.js",
       "test-net-write-arguments.js",
-      "test-parse-args.mjs",
       "test-path-join.js",
       "test-path-makelong.js",
       "test-path-normalize.js",

filename: tests/node_compat/test/parallel/test-parse-args.mjs
patch: @@ -2,7 +2,7 @@
 // deno-lint-ignore-file
 
 // Copyright Joyent and Node contributors. All rights reserved. MIT license.
-// Taken from Node 20.11.1
+// Taken from Node 23.9.0
 // This file is automatically generated by `tests/node_compat/runner/setup.ts`. Do not modify this file manually.
 
 import '../common/index.mjs';
@@ -999,3 +999,73 @@ test('multiple as false should expect a String', () => {
   }, /"options\.alpha\.default" property must be of type string/
   );
 });
+
+// Test negative options
+test('disable negative options and args are started with "--no-" prefix', () => {
+  const args = ['--no-alpha'];
+  const options = { alpha: { type: 'boolean' } };
+  assert.throws(() => {
+    parseArgs({ args, options });
+  }, {
+    code: 'ERR_PARSE_ARGS_UNKNOWN_OPTION'
+  });
+});
+
+test('args are passed `type: "string"` and allow negative options', () => {
+  const args = ['--no-alpha', 'value'];
+  const options = { alpha: { type: 'string' } };
+  assert.throws(() => {
+    parseArgs({ args, options, allowNegative: true });
+  }, {
+    code: 'ERR_PARSE_ARGS_UNKNOWN_OPTION'
+  });
+});
+
+test('args are passed `type: "boolean"` and allow negative options', () => {
+  const args = ['--no-alpha'];
+  const options = { alpha: { type: 'boolean' } };
+  const expected = { values: { __proto__: null, alpha: false }, positionals: [] };
+  assert.deepStrictEqual(parseArgs({ args, options, allowNegative: true }), expected);
+});
+
+test('args are passed `default: "true"` and allow negative options', () => {
+  const args = ['--no-alpha'];
+  const options = { alpha: { type: 'boolean', default: true } };
+  const expected = { values: { __proto__: null, alpha: false }, positionals: [] };
+  assert.deepStrictEqual(parseArgs({ args, options, allowNegative: true }), expected);
+});
+
+test('args are passed `default: "false" and allow negative options', () => {
+  const args = ['--no-alpha'];
+  const options = { alpha: { type: 'boolean', default: false } };
+  const expected = { values: { __proto__: null, alpha: false }, positionals: [] };
+  assert.deepStrictEqual(parseArgs({ args, options, allowNegative: true }), expected);
+});
+
+test('allow negative options and multiple as true', () => {
+  const args = ['--no-alpha', '--alpha', '--no-alpha'];
+  const options = { alpha: { type: 'boolean', multiple: true } };
+  const expected = { values: { __proto__: null, alpha: [false, true, false] }, positionals: [] };
+  assert.deepStrictEqual(parseArgs({ args, options, allowNegative: true }), expected);
+});
+
+test('allow negative options and passed multiple arguments', () => {
+  const args = ['--no-alpha', '--alpha'];
+  const options = { alpha: { type: 'boolean' } };
+  const expected = { values: { __proto__: null, alpha: true }, positionals: [] };
+  assert.deepStrictEqual(parseArgs({ args, options, allowNegative: true }), expected);
+});
+
+test('auto-detect --no-foo as negated when strict:false and allowNegative', () => {
+  const holdArgv = process.argv;
+  process.argv = [process.argv0, 'script.js', '--no-foo'];
+  const holdExecArgv = process.execArgv;
+  process.execArgv = [];
+  const result = parseArgs({ strict: false, allowNegative: true });
+
+  const expected = { values: { __proto__: null, foo: false },
+                     positionals: [] };
+  assert.deepStrictEqual(result, expected);
+  process.argv = holdArgv;
+  process.execArgv = holdExecArgv;
+});