From c76ac2b95f9de3e8f8935beabde06cbacebc1773 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 3 Oct 2026 09:29:06 -0500 Subject: [PATCH] Expand Node 24 dynamic argument parsing compatibility (#632) * Expand Node 24 dynamic argument parsing compatibility * Match Node argument error string truncation and quoting --- .../node-v24-compatibility-meta.json | 2 +- .../src/generated/node-v24-compatibility.json | 4 +- internal/compatibility/dynamic-support.json | 1 + .../generated/node-v24-backlog.json | 18 +- .../generated/node-v24-internal.json | 21 +- internal/compatibility/src/generate.mjs | 3 + internal/compatibility/static-support.json | 1 + .../test/ts7/baselines/order-parity.json | 12 + packages/runtime/src/scr_island.c | 272 +++++++++--------- packages/runtime/src/scr_json.c | 39 ++- tests/corpus/node24-parseargs.js | 74 +++++ .../npm/cases/node24-parseargs/main.ts | 4 + .../npm/cases/node24-parseargs/package.json | 1 + .../node24-parseargs-fixture/index.d.ts | 1 + .../node24-parseargs-fixture/index.js | 75 +++++ .../node24-parseargs-fixture/package.json | 1 + 16 files changed, 362 insertions(+), 167 deletions(-) create mode 100644 tests/corpus/node24-parseargs.js create mode 100644 tests/fixtures/npm/cases/node24-parseargs/main.ts create mode 100644 tests/fixtures/npm/cases/node24-parseargs/package.json create mode 100644 tests/fixtures/npm/node_modules/node24-parseargs-fixture/index.d.ts create mode 100644 tests/fixtures/npm/node_modules/node24-parseargs-fixture/index.js create mode 100644 tests/fixtures/npm/node_modules/node24-parseargs-fixture/package.json diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json index 36d06b726..c6ec3f344 100644 --- a/docs/src/generated/node-v24-compatibility-meta.json +++ b/docs/src/generated/node-v24-compatibility-meta.json @@ -2,6 +2,6 @@ "schemaVersion": 3, "nodeVersion": "24.15.0", "nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119", - "artifactVersion": "80b5ea587a01ac030822", + "artifactVersion": "3c31fba8150926a20ac8", "rowCount": 3662 } diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index c71fac164..d86ffc75a 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -85436,7 +85436,7 @@ }, "dynamic": { "status": "partial", - "detail": "Available through the embedded Node compatibility shim; exact member and overload coverage may be narrower.", + "detail": "Parses command-line options, positionals, defaults, and tokens with Node-compatible configuration validation.", "verification": "test-backed" } }, @@ -85464,7 +85464,7 @@ }, "dynamic": { "status": "partial", - "detail": "Available through the embedded Node compatibility shim; exact member and overload coverage may be narrower.", + "detail": "Parses command-line options, positionals, defaults, and tokens with Node-compatible configuration validation.", "verification": "test-backed" } }, diff --git a/internal/compatibility/dynamic-support.json b/internal/compatibility/dynamic-support.json index 21460717a..42c5e9376 100644 --- a/internal/compatibility/dynamic-support.json +++ b/internal/compatibility/dynamic-support.json @@ -19,6 +19,7 @@ "wasi": "by-design" }, "features": [ + { "chapter": "util", "symbols": ["util.parseArgs", "parseArgs"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/util-shims/main.ts", "tests/fixtures/npm/cases/node24-parseargs/main.ts"] }, { "chapter": "buffer", "symbols": ["buffer.isAscii", "buffer.isUtf8", "buffer.transcode"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/node24-text-encoding/main.ts"] }, { "chapter": "util", "parent": "Class: `util.TextEncoder`", "signatures": ["`textEncoder.encodeInto(src, dest)`", "`encoding` Type: {string}"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/node24-text-encoding/main.ts"] }, { "chapter": "util", "parent": "Class: `util.TextDecoder`", "signatures": ["`encoding` Type: {string}", "`fatal` Type: {boolean}", "`ignoreBOM` Type: {boolean}"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/node24-text-encoding/main.ts"] }, diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 59b851bfc..e45c5bf86 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -95432,8 +95432,8 @@ "confidence": "high", "source": "surface-manifest:node-builtin.util.parseArgs", "tests": [ - "tests/corpus/2440-console-inspect-args.ts", - "tests/corpus/2912-promisify-fs-readfile.ts" + "tests/corpus/2678-util-parseargs.ts", + "tests/corpus/node24-parseargs.js" ] }, "dynamic": { @@ -95441,9 +95441,10 @@ "action": "audit-partial", "verification": "test-backed", "confidence": "high", - "source": "island-export:node:util.parseArgs", + "source": "island-feature:util.util.parseArgs", "tests": [ - "tests/fixtures/npm/cases/util-shims/main.ts" + "tests/fixtures/npm/cases/util-shims/main.ts", + "tests/fixtures/npm/cases/node24-parseargs/main.ts" ] } } @@ -95470,8 +95471,8 @@ "confidence": "high", "source": "surface-manifest:node-builtin.util.parseArgs", "tests": [ - "tests/corpus/2440-console-inspect-args.ts", - "tests/corpus/2912-promisify-fs-readfile.ts" + "tests/corpus/2678-util-parseargs.ts", + "tests/corpus/node24-parseargs.js" ] }, "dynamic": { @@ -95479,9 +95480,10 @@ "action": "audit-partial", "verification": "test-backed", "confidence": "high", - "source": "island-export:node:util.parseArgs", + "source": "island-feature:util.parseArgs", "tests": [ - "tests/fixtures/npm/cases/util-shims/main.ts" + "tests/fixtures/npm/cases/util-shims/main.ts", + "tests/fixtures/npm/cases/node24-parseargs/main.ts" ] } } diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 770cdbc15..2ffdec739 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -111022,11 +111022,13 @@ "tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2440-console-inspect-args.ts", + "tests/corpus/2678-util-parseargs.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/3133-text-decoder-streaming.ts", "tests/corpus/node24-env-imports.cjs", "tests/corpus/node24-env-parse-errors.ts", "tests/corpus/node24-env-parse.ts", + "tests/corpus/node24-parseargs.js", "tests/corpus/node24-text-codec-properties.ts", "tests/corpus/node24-text-encode-into.ts", "tests/corpus/text-codec-values/main.ts", @@ -111041,6 +111043,7 @@ "evidence": "derived:descendants", "tests": [ "tests/fixtures/npm/cases/node24-env/main.ts", + "tests/fixtures/npm/cases/node24-parseargs/main.ts", "tests/fixtures/npm/cases/node24-text-encoding/main.ts", "tests/fixtures/npm/cases/util-shims/main.ts" ] @@ -112716,15 +112719,16 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.util.parseArgs", "tests": [ - "tests/corpus/2440-console-inspect-args.ts", - "tests/corpus/2912-promisify-fs-readfile.ts" + "tests/corpus/2678-util-parseargs.ts", + "tests/corpus/node24-parseargs.js" ] }, "dynamic": { "status": "partial", - "evidence": "island-export:node:util.parseArgs", + "evidence": "island-feature:util.util.parseArgs", "tests": [ - "tests/fixtures/npm/cases/util-shims/main.ts" + "tests/fixtures/npm/cases/util-shims/main.ts", + "tests/fixtures/npm/cases/node24-parseargs/main.ts" ] }, "anchorSource": "exact" @@ -112750,15 +112754,16 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.util.parseArgs", "tests": [ - "tests/corpus/2440-console-inspect-args.ts", - "tests/corpus/2912-promisify-fs-readfile.ts" + "tests/corpus/2678-util-parseargs.ts", + "tests/corpus/node24-parseargs.js" ] }, "dynamic": { "status": "partial", - "evidence": "island-export:node:util.parseArgs", + "evidence": "island-feature:util.parseArgs", "tests": [ - "tests/fixtures/npm/cases/util-shims/main.ts" + "tests/fixtures/npm/cases/util-shims/main.ts", + "tests/fixtures/npm/cases/node24-parseargs/main.ts" ] }, "anchorSource": "exact" diff --git a/internal/compatibility/src/generate.mjs b/internal/compatibility/src/generate.mjs index a2f5e1710..2d6ee1a23 100644 --- a/internal/compatibility/src/generate.mjs +++ b/internal/compatibility/src/generate.mjs @@ -678,6 +678,9 @@ function publicDetail(tier) { return "No dynamic-island implementation exists for this Node API family yet."; } if (source.startsWith("island-feature:")) { + if (/^island-feature:util\.(?:util\.)?parseArgs$/.test(source)) { + return "Parses command-line options, positionals, defaults, and tokens with Node-compatible configuration validation."; + } if (/^island-feature:buffer\.buffer\.(?:isAscii|isUtf8)$/.test(source)) { return "Validates bytes in ArrayBuffer and typed-array views with Node-compatible argument errors."; } diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 4df0fdacb..9a0f93975 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -87,6 +87,7 @@ "node-builtin.url": ["tests/corpus/1794-searchparams-url-live.ts"], "node-builtin.url.pathToFileURL": ["tests/corpus/path-to-file-url-options.ts"], "node-builtin.url.fileURLToPath": ["tests/corpus/file-url-percent-decoding.ts"], + "node-builtin.util.parseArgs": ["tests/corpus/2678-util-parseargs.ts", "tests/corpus/node24-parseargs.js"], "node-builtin.util": ["tests/corpus/2440-console-inspect-args.ts", "tests/corpus/2912-promisify-fs-readfile.ts"], "node-builtin.worker_threads": ["tests/corpus/957-builtins-namespace.ts"], "node-builtin.zlib": ["tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", "tests/corpus/3115-zlib-static-compression-levels.ts"], diff --git a/packages/compiler/test/ts7/baselines/order-parity.json b/packages/compiler/test/ts7/baselines/order-parity.json index a2c9e31af..1466c0df7 100644 --- a/packages/compiler/test/ts7/baselines/order-parity.json +++ b/packages/compiler/test/ts7/baselines/order-parity.json @@ -13396,6 +13396,18 @@ "/tests/fixtures/npm/cases/node24-string-decoder/main.ts" ], "diags": [] + }, + "/tests/corpus/node24-parseargs.js": { + "order": [ + "/tests/corpus/node24-parseargs.js" + ], + "diags": [] + }, + "/tests/fixtures/npm/cases/node24-parseargs/main.ts": { + "order": [ + "/tests/fixtures/npm/cases/node24-parseargs/main.ts" + ], + "diags": [] } } } diff --git a/packages/runtime/src/scr_island.c b/packages/runtime/src/scr_island.c index 428f0e847..87ec78776 100644 --- a/packages/runtime/src/scr_island.c +++ b/packages/runtime/src/scr_island.c @@ -8374,147 +8374,151 @@ static const char isl_modules_bootstrap[] = " }\n" " return colorize ? left + text + right : text;\n" " };\n" - " const parseArgs = (config) => {\n" - " config = config === undefined ? {} : config;\n" - " const args = config.args !== undefined ? config.args : env.argv.slice(2);\n" - " const strict = config.strict !== undefined ? !!config.strict : true;\n" - " const allowPositionals = config.allowPositionals !== undefined ? !!config.allowPositionals : !strict;\n" - " const allowNegative = !!config.allowNegative;\n" - " const returnTokens = !!config.tokens;\n" - " const options = config.options !== undefined ? config.options : {};\n" - " const result = { values: { __proto__: null }, positionals: [] };\n" + " const parseArgs = (config = {}) => {\n" + " if (config === null) throw new TypeError(\"Cannot convert undefined or null to object\");\n" + " const own = (o, k) => Object.prototype.hasOwnProperty.call(o, k);\n" + " const get = (o, k) => own(o, k) ? o[k] : undefined;\n" + " const args = get(config, 'args') ?? env.argv.slice(2);\n" + " const strict = get(config, 'strict') ?? true;\n" + " const allowPositionals = get(config, 'allowPositionals') ?? !strict;\n" + " const returnTokens = get(config, 'tokens') ?? false;\n" + " const allowNegative = get(config, 'allowNegative') ?? false;\n" + " const options = get(config, 'options') ?? Object.create(null);\n" + " const fail = (code, message) => { const e = new TypeError(message); e.code = code; throw e; };\n" + " const received = (v) => {\n" + " if (v === null || v === undefined) return String(v);\n" + " if (typeof v === 'object') return v.constructor && 'name' in v.constructor ? 'an instance of ' + v.constructor.name : inspect(v, { depth: -1 });\n" + " if (typeof v === 'function') return 'function ' + v.name;\n" + " if (typeof v === 'string') {\n" + " if (v.length > 28) v = v.slice(0, 25) + '...';\n" + " return 'type string (' + (v.includes(\"'\") ? JSON.stringify(v) : \"'\" + v + \"'\") + ')';\n" + " }\n" + " if (typeof v === 'number') return 'type number (' + (Object.is(v, -0) ? '-0' : String(v)) + ')';\n" + " if (typeof v === 'bigint') return 'type bigint (' + v + 'n)';\n" + " if (typeof v === 'symbol') return 'type symbol (' + String(v) + ')';\n" + " return 'type ' + typeof v + ' (' + String(v) + ')';\n" + " };\n" + " const validate = (v, name, expected, ok) => {\n" + " if (!ok) fail('ERR_INVALID_ARG_TYPE', 'The \"' + name + '\" ' + (name.startsWith('options.') ? 'property' : 'argument') + ' must be ' + expected + '. Received ' + received(v));\n" + " };\n" + " const object = (v, name) => validate(v, name, 'of type object', v !== null && typeof v === 'object' && !Array.isArray(v));\n" + " const boolean = (v, name) => validate(v, name, 'of type boolean', typeof v === 'boolean');\n" + " const string = (v, name) => validate(v, name, 'of type string', typeof v === 'string');\n" + " validate(args, 'args', 'an instance of Array', Array.isArray(args));\n" + " boolean(strict, 'strict');\n" + " boolean(allowPositionals, 'allowPositionals');\n" + " boolean(returnTokens, 'tokens');\n" + " boolean(allowNegative, 'allowNegative');\n" + " object(options, 'options');\n" + " for (const [name, option] of Object.entries(options)) {\n" + " const path = 'options.' + name;\n" + " object(option, path);\n" + " const type = get(option, 'type');\n" + " validate(type, path + '.type', \"('string|boolean')\", type === 'string' || type === 'boolean');\n" + " if (own(option, 'short')) {\n" + " const short = option.short;\n" + " string(short, path + '.short');\n" + " if (short.length !== 1) fail('ERR_INVALID_ARG_VALUE', \"The property '\" + path + \".short' must be a single character. Received \" + inspect(short));\n" + " }\n" + " const multiple = get(option, 'multiple');\n" + " if (own(option, 'multiple')) boolean(multiple, path + '.multiple');\n" + " const value = get(option, 'default');\n" + " if (value !== undefined) {\n" + " const check = type === 'string' ? string : boolean;\n" + " if (multiple) {\n" + " validate(value, path + '.default', 'an instance of Array', Array.isArray(value));\n" + " for (let i = 0; i < value.length; i++) check(value[i], path + '.default[' + i + ']');\n" + " } else check(value, path + '.default');\n" + " }\n" + " }\n" + " const optionGet = (name, key) => own(options, name) ? get(options[name], key) : undefined;\n" + " const shortName = (ch) => {\n" + " const entry = Object.entries(options).find(([, option]) => get(option, 'short') === ch);\n" + " return entry ? entry[0] : ch;\n" + " };\n" " const tokens = [];\n" - " const shortOf = (ch) => {\n" - " for (const name of Object.keys(options)) {\n" - " if (options[name].short === ch) return name;\n" + " // Tokenize the full argument snapshot before checking usage, as Node does.\n" + " const remaining = Array.prototype.slice.call(args);\n" + " let index = -1;\n" + " let groupCount = 0;\n" + " while (remaining.length) {\n" + " const arg = remaining.shift();\n" + " if (groupCount > 0) groupCount--; else index++;\n" + " if (arg === '--') {\n" + " tokens.push({ kind: 'option-terminator', index });\n" + " for (const value of remaining) tokens.push({ kind: 'positional', index: ++index, value });\n" + " break;\n" " }\n" - " return undefined;\n" - " };\n" - " const unknownError = (raw) => {\n" - " const e = new TypeError(\"Unknown option '\" + raw + \"'. To specify a positional argument starting with a '-', place it at the end of the command after '--', as in '-- \\\"\" + raw + \"\\\"\");\n" - " e.code = \"ERR_PARSE_ARGS_UNKNOWN_OPTION\";\n" - " throw e;\n" - " };\n" - " const store = (name, value, raw) => {\n" - " const cfg = options[name] || {};\n" - " const type = cfg.type;\n" - " if (strict) {\n" - " if (options[name] === undefined) unknownError(raw);\n" - " if (type === \"string\" && value === undefined) {\n" - " const e = new TypeError(\"Option '\" + raw + (cfg.short && raw.startsWith(\"--\") === false ? \"\" : \"\") + \" ' argument missing\");\n" - " e.code = \"ERR_PARSE_ARGS_INVALID_OPTION_VALUE\";\n" - " throw e;\n" - " }\n" - " if (type === \"boolean\" && value !== undefined) {\n" - " const e = new TypeError(\"Option '\" + raw + \"' does not take an argument\");\n" - " e.code = \"ERR_PARSE_ARGS_INVALID_OPTION_VALUE\";\n" - " throw e;\n" - " }\n" - " }\n" - " const finalValue = value === undefined ? true : value;\n" - " tokens.push({ kind: \"option\", name, rawName: raw, index: tokenIndex, value: value === undefined ? undefined : value, inlineValue: inline });\n" - " if (cfg.multiple) {\n" - " if (result.values[name] === undefined) result.values[name] = [];\n" - " result.values[name].push(finalValue);\n" - " } else {\n" - " result.values[name] = finalValue;\n" - " }\n" - " };\n" - " let tokenIndex = -1;\n" - " let inline;\n" - " let afterDashDash = false;\n" - " for (let i = 0; i < args.length; i++) {\n" - " const arg = args[i];\n" - " tokenIndex = i;\n" - " inline = undefined;\n" - " if (afterDashDash) {\n" - " tokens.push({ kind: \"positional\", index: i, value: arg });\n" - " result.positionals.push(arg);\n" - " continue;\n" - " }\n" - " if (arg === \"--\") {\n" - " afterDashDash = true;\n" - " tokens.push({ kind: \"option-terminator\", index: i });\n" - " continue;\n" - " }\n" - " if (arg.startsWith(\"--\")) {\n" - " const eq = arg.indexOf(\"=\");\n" - " if (eq !== -1) {\n" - " const name = arg.slice(2, eq);\n" - " inline = true;\n" - " store(name, arg.slice(eq + 1), \"--\" + name);\n" + " // Reading length first deliberately preserves Node's nullish-argument error.\n" + " if (arg == null) throw new TypeError(\"Cannot read properties of \" + arg + \" (reading 'length')\");\n" + " const length = arg.length;\n" + " if (length === 2 && String.prototype.charAt.call(arg, 0) === '-' && String.prototype.charAt.call(arg, 1) !== '-') {\n" + " const name = shortName(String.prototype.charAt.call(arg, 1));\n" + " let value, inlineValue;\n" + " if (optionGet(name, 'type') === 'string' && remaining[0] != null) { value = remaining.shift(); inlineValue = false; }\n" + " tokens.push({ kind: 'option', name, rawName: arg, index, value, inlineValue });\n" + " if (value != null) index++;\n" + " } else if (length > 2 && String.prototype.charAt.call(arg, 0) === '-' && String.prototype.charAt.call(arg, 1) !== '-') {\n" + " const firstName = shortName(String.prototype.charAt.call(arg, 1));\n" + " if (optionGet(firstName, 'type') === 'string') {\n" + " tokens.push({ kind: 'option', name: firstName, rawName: '-' + String.prototype.charAt.call(arg, 1), index, value: String.prototype.slice.call(arg, 2), inlineValue: true });\n" " } else {\n" - " const name = arg.slice(2);\n" - " const cfg = options[name];\n" - " if (cfg !== undefined && cfg.type === \"string\" && i + 1 < args.length) {\n" - " inline = false;\n" - " store(name, args[++i], arg);\n" - " } else if (allowNegative && name.startsWith(\"no-\") && (options[name.slice(3)] || {}).type === \"boolean\") {\n" - " const positive = name.slice(3);\n" - " tokens.push({ kind: \"option\", name: positive, rawName: arg, index: i, value: undefined, inlineValue: undefined });\n" - " result.values[positive] = false;\n" - " } else {\n" - " if (strict && cfg === undefined) unknownError(arg);\n" - " if (strict && cfg.type === \"string\") {\n" - " const e = new TypeError(\"Option '--\" + name + \" ' argument missing\");\n" - " e.code = \"ERR_PARSE_ARGS_INVALID_OPTION_VALUE\";\n" - " throw e;\n" - " }\n" - " store(name, undefined, arg);\n" + " const expanded = [];\n" + " for (let i = 1; i < length; i++) {\n" + " const ch = String.prototype.charAt.call(arg, i);\n" + " const name = shortName(ch);\n" + " if (optionGet(name, 'type') === 'string' && i !== length - 1) { expanded.push('-' + String.prototype.slice.call(arg, i)); break; }\n" + " expanded.push('-' + ch);\n" " }\n" + " remaining.unshift(...expanded);\n" + " groupCount = expanded.length;\n" " }\n" - " continue;\n" - " }\n" - " if (arg.length > 1 && arg[0] === \"-\") {\n" - " const chars = arg.slice(1);\n" - " let consumed = false;\n" - " for (let c = 0; c < chars.length; c++) {\n" - " const ch = chars[c];\n" - " const name = shortOf(ch);\n" - " const raw = \"-\" + ch;\n" - " const cfg = name !== undefined ? options[name] : undefined;\n" - " if (cfg !== undefined && cfg.type === \"string\") {\n" - " if (c < chars.length - 1) {\n" - " inline = true;\n" - " store(name, chars.slice(c + 1), raw);\n" - " } else if (i + 1 < args.length) {\n" - " inline = false;\n" - " store(name, args[++i], raw);\n" - " } else if (strict) {\n" - " const e = new TypeError(\"Option '\" + raw + \", --\" + name + \" ' argument missing\");\n" - " e.code = \"ERR_PARSE_ARGS_INVALID_OPTION_VALUE\";\n" - " throw e;\n" - " } else {\n" - " store(name, undefined, raw);\n" - " }\n" - " consumed = true;\n" - " break;\n" - " }\n" - " if (name === undefined) {\n" - " if (strict) unknownError(raw);\n" - " store(ch, undefined, raw);\n" - " } else {\n" - " store(name, undefined, raw);\n" - " }\n" - " }\n" - " void consumed;\n" - " continue;\n" - " }\n" - " if (strict && !allowPositionals) {\n" - " const e = new TypeError(\"Unexpected argument '\" + arg + \"'. This command does not take positional arguments\");\n" - " e.code = \"ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL\";\n" - " throw e;\n" - " }\n" - " tokens.push({ kind: \"positional\", index: i, value: arg });\n" - " result.positionals.push(arg);\n" - " }\n" - " for (const name of Object.keys(options)) {\n" - " const cfg = options[name];\n" - " if (cfg.default !== undefined && !(name in result.values)) {\n" - " result.values[name] = cfg.default;\n" - " }\n" + " } else if (length > 2 && String.prototype.startsWith.call(arg, '--')) {\n" + " const eq = String.prototype.indexOf.call(arg, '=', 3);\n" + " const name = String.prototype.slice.call(arg, 2, eq < 0 ? undefined : eq);\n" + " let value, inlineValue;\n" + " if (eq >= 0) { value = String.prototype.slice.call(arg, eq + 1); inlineValue = true; }\n" + " else if (optionGet(name, 'type') === 'string' && remaining[0] != null) { value = remaining.shift(); inlineValue = false; }\n" + " tokens.push({ kind: 'option', name, rawName: '--' + name, index, value, inlineValue });\n" + " if (eq < 0 && value != null) index++;\n" + " } else tokens.push({ kind: 'positional', index, value: arg });\n" " }\n" + " const result = { values: Object.create(null), positionals: [] };\n" " if (returnTokens) result.tokens = tokens;\n" + " for (const token of tokens) {\n" + " if (token.kind === 'positional') {\n" + " if (!allowPositionals) fail('ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL', \"Unexpected argument '\" + token.value + \"'. This command does not take positional arguments\");\n" + " result.positionals.push(token.value);\n" + " } else if (token.kind === 'option') {\n" + " let name = token.name;\n" + " if (strict) {\n" + " if (!own(options, name)) {\n" + " if (allowNegative && name.startsWith('no-') && own(options, name.slice(3)) && optionGet(name.slice(3), 'type') === 'boolean') name = name.slice(3);\n" + " else fail('ERR_PARSE_ARGS_UNKNOWN_OPTION', \"Unknown option '\" + token.rawName + \"'\" + (allowPositionals ? \". To specify a positional argument starting with a '-', place it at the end of the command after '--', as in '-- \\\"\" + token.rawName + \"\\\"\" : ''));\n" + " }\n" + " const short = optionGet(name, 'short');\n" + " const label = (short ? '-' + short + ', ' : '') + '--' + name;\n" + " if (optionGet(name, 'type') === 'string' && typeof token.value !== 'string') fail('ERR_PARSE_ARGS_INVALID_OPTION_VALUE', \"Option '\" + label + \" ' argument missing\");\n" + " if (optionGet(name, 'type') === 'boolean' && token.value != null) fail('ERR_PARSE_ARGS_INVALID_OPTION_VALUE', \"Option '\" + label + \"' does not take an argument\");\n" + " if (!token.inlineValue && token.value != null && token.value.length > 1 && String.prototype.charAt.call(token.value, 0) === '-') {\n" + " const example = token.rawName.startsWith('--') ? \"'\" + token.rawName + \"=-XYZ'\" : \"'--\" + token.name + \"=-XYZ' or '\" + token.rawName + \"-XYZ'\";\n" + " fail('ERR_PARSE_ARGS_INVALID_OPTION_VALUE', \"Option '\" + token.rawName + \"' argument is ambiguous.\\nDid you forget to specify the option argument for '\" + token.rawName + \"'?\\nTo specify an option argument starting with a dash use \" + example + '.');\n" + " }\n" + " }\n" + " name = token.name;\n" + " let value = token.value;\n" + " if (name === '__proto__') continue;\n" + " if (allowNegative && name.startsWith('no-') && value === undefined) { name = name.slice(3); token.name = name; value = false; }\n" + " value = value ?? true;\n" + " if (optionGet(name, 'multiple')) {\n" + " if (!result.values[name]) result.values[name] = [];\n" + " result.values[name].push(value);\n" + " } else result.values[name] = value;\n" + " }\n" + " }\n" + " for (const [name, option] of Object.entries(options)) {\n" + " if (name !== '__proto__' && get(option, 'default') !== undefined && result.values[name] === undefined) result.values[name] = get(option, 'default');\n" + " }\n" " return result;\n" " };\n" " const toUSVString = (s) => String(s).replace(/[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?v.str; - char insp[32]; - size_t n = 0; - insp[n++] = '\''; - for (size_t i = 0; i < sv->len && n < 28; i++) insp[n++] = sv->data[i]; - if (sv->len + 2 > 28) { - n = 25; - memcpy(insp + n, "...", 3); - n += 3; - } else { - insp[n++] = '\''; + ScrStr *value = scr_str_retain(cb->v.str); + if (scr_str_utf16_len(value) > 28) { + ScrStr *prefix = scr_str_slice(value, 0, 25); + ScrStr *dots = scr_str_new("...", 3); + scr_str_release(value); + value = scr_str_concat(prefix, dots); + scr_str_release(prefix); + scr_str_release(dots); } - snprintf(detail, cap, "type string (%.*s)", (int)n, insp); + ScrJsonBuf shown; + scr_jb_init(&shown); + scr_jb_puts(&shown, "type string ("); + if (memchr(value->data, '\'', value->len)) { + scr_jb_put_json_str(&shown, value); + } else { + scr_jb_putc(&shown, '\''); + scr_jb_put_str(&shown, value); + scr_jb_putc(&shown, '\''); + } + scr_jb_putc(&shown, ')'); + ScrStr *text = scr_jb_finish(&shown); + snprintf(detail, cap, "%.*s", (int)text->len, text->data); + scr_str_release(text); + scr_str_release(value); break; } default: d = "an instance of Object"; break; @@ -2391,7 +2402,7 @@ void scr_throw_arg_type(const ScrStr *argname, const ScrStr *expected, const Scr } void scr_dyn_arg_type_fail(const char *argname, const char *expected, const ScrDyn *got) { - char detail[64]; + char detail[256]; const char *d = scr_dyn_specific_type(got, detail, sizeof detail); ScrJsonBuf b; scr_jb_init(&b); @@ -2412,7 +2423,7 @@ void scr_dyn_arg_type_fail(const char *argname, const char *expected, const ScrD * wording on the name, but every property-path caller here knows it is * one). Same runtime-rendered Received tail; always throws catchably. */ void scr_dyn_prop_type_fail(const char *name, const char *expected, const ScrDyn *got) { - char detail[64]; + char detail[256]; const char *d = scr_dyn_specific_type(got, detail, sizeof detail); ScrJsonBuf b; scr_jb_init(&b); diff --git a/tests/corpus/node24-parseargs.js b/tests/corpus/node24-parseargs.js new file mode 100644 index 000000000..c32528828 --- /dev/null +++ b/tests/corpus/node24-parseargs.js @@ -0,0 +1,74 @@ +import { parseArgs } from "node:util"; + +/** @param {any} config */ +function show(config) { + try { + const result = parseArgs(config); + console.log(JSON.stringify(result)); + console.log("null-prototype", Object.getPrototypeOf(result.values) === null); + } catch (error) { + console.log(error.name, error.code, error.message); + } +} + +function report() { + const options = { + alpha: { type: "boolean", short: "a" }, + beta: { type: "boolean", short: "b" }, + name: { type: "string", short: "n" }, + flag: { type: "boolean", short: "f", multiple: true }, + }; + const argsCases = [ + [], ["-abnhello", "--name=last"], ["-abn", "hello", "tail"], + ["--name", "-dash"], ["-n", "--flag"], ["--name=-dash"], ["-n-dash"], + ["--name"], ["-n"], ["--flag=yes"], ["--unknown"], ["--unknown=value"], + ["--", "--unknown"], ["-"], ["--flag", "--no-flag", "--flag"], + ["--no-unknown"], ["--=odd"], ["--__proto__=x", "--constructor=y"], + ]; + for (const args of argsCases) { + for (const strict of [true, false]) { + for (const allowPositionals of [true, false]) { + show({ args, options, strict, allowPositionals, allowNegative: true, tokens: true }); + } + } + } + for (const options of [ + { "no-x": { type: "boolean" }, x: { type: "boolean", multiple: true } }, + { x: { type: "boolean", multiple: true, default: [true] } }, + { x: { type: "string", default: "default" } }, + { "__proto__": null, constructor: { type: "boolean", default: true } }, + ]) show({ args: ["--no-x"], options, allowNegative: true, strict: false, tokens: true }); + for (const config of [ + null, + { args: [], strict: "has'quote" }, { args: [], strict: "line\nend" }, + { args: [], strict: "x".repeat(28) }, { args: [], strict: "x".repeat(29) }, + { args: [], options: { x: { type: "invalid".repeat(8) } } }, + { args: "bad", strict: 1 }, { args: [], strict: 1 }, { args: [], allowPositionals: 1 }, + { args: [], tokens: "yes" }, { args: [], allowNegative: 0 }, { args: [], options: [] }, + { args: [], options: { x: null } }, { args: [], options: { x: {} } }, + { args: [], options: { x: { type: "number" } } }, + { args: [], options: { x: { type: "string", short: "ab" } } }, + { args: [], options: { x: { type: "string", short: undefined } } }, + { args: [], options: { x: { type: "boolean", multiple: null } } }, + { args: [], options: { x: { type: "string", default: 2 } } }, + { args: [], options: { x: { type: "boolean", multiple: true, default: [true, 2] } } }, + { args: [], options: { x: { type: "string", multiple: true, default: "bad" } } }, + { args: ["--unknown", null] }, { args: ["--", null], strict: false }, + { args: ["--name", null], options }, { args: [1, false, {}], strict: false }, + { args: [], strict: null, tokens: null, allowNegative: null, options: null }, + ]) show(config); + show(Object.assign(Object.create({ strict: false, options: { inherited: { type: "boolean" } } }), { args: ["--inherited"] })); + show({ args: ["--inherited"], options: Object.create({ inherited: { type: "boolean" } }) }); + show({ args: [], options: { x: Object.create({ type: "boolean" }) } }); + show({ args: [], options: { x: Object.assign(Object.create({ multiple: true, default: ["ignored"] }), { type: "string" }) } }); + const protoOptions = Object.create(null); + protoOptions.__proto__ = { type: "string", default: "ignored" }; + show({ args: ["--__proto__=ignored"], options: protoOptions, tokens: true }); + const defaults = ["initial"]; + const result = parseArgs({ args: [], options: { name: { type: "string", multiple: true, default: defaults } } }); + console.log("default-identity", result.values.name === defaults); + defaults.push("updated"); + console.log("default-live", JSON.stringify(result)); +} + +report(); diff --git a/tests/fixtures/npm/cases/node24-parseargs/main.ts b/tests/fixtures/npm/cases/node24-parseargs/main.ts new file mode 100644 index 000000000..7a6bfebd2 --- /dev/null +++ b/tests/fixtures/npm/cases/node24-parseargs/main.ts @@ -0,0 +1,4 @@ +// @dynamic +// @stderr +import { report } from "node24-parseargs-fixture"; +report(); diff --git a/tests/fixtures/npm/cases/node24-parseargs/package.json b/tests/fixtures/npm/cases/node24-parseargs/package.json new file mode 100644 index 000000000..089153bcb --- /dev/null +++ b/tests/fixtures/npm/cases/node24-parseargs/package.json @@ -0,0 +1 @@ +{"type":"module"} diff --git a/tests/fixtures/npm/node_modules/node24-parseargs-fixture/index.d.ts b/tests/fixtures/npm/node_modules/node24-parseargs-fixture/index.d.ts new file mode 100644 index 000000000..826d89dbd --- /dev/null +++ b/tests/fixtures/npm/node_modules/node24-parseargs-fixture/index.d.ts @@ -0,0 +1 @@ +export function report(): void; diff --git a/tests/fixtures/npm/node_modules/node24-parseargs-fixture/index.js b/tests/fixtures/npm/node_modules/node24-parseargs-fixture/index.js new file mode 100644 index 000000000..3d234c8fc --- /dev/null +++ b/tests/fixtures/npm/node_modules/node24-parseargs-fixture/index.js @@ -0,0 +1,75 @@ +import { parseArgs } from "node:util"; + +/** @param {any} config */ +function show(config) { + try { + const result = parseArgs(config); + console.log(JSON.stringify(result)); + console.log("null-prototype", Object.getPrototypeOf(result.values) === null); + } catch (error) { + console.log(error.name, error.code, error.message); + } +} + +export function report() { + const options = { + alpha: { type: "boolean", short: "a" }, + beta: { type: "boolean", short: "b" }, + name: { type: "string", short: "n" }, + flag: { type: "boolean", short: "f", multiple: true }, + }; + const argsCases = [ + [], ["-abnhello", "--name=last"], ["-abn", "hello", "tail"], + ["--name", "-dash"], ["-n", "--flag"], ["--name=-dash"], ["-n-dash"], + ["--name"], ["-n"], ["--flag=yes"], ["--unknown"], ["--unknown=value"], + ["--", "--unknown"], ["-"], ["--flag", "--no-flag", "--flag"], + ["--no-unknown"], ["--=odd"], ["--__proto__=x", "--constructor=y"], + ]; + for (const args of argsCases) { + for (const strict of [true, false]) { + for (const allowPositionals of [true, false]) { + show({ args, options, strict, allowPositionals, allowNegative: true, tokens: true }); + } + } + } + for (const options of [ + { "no-x": { type: "boolean" }, x: { type: "boolean", multiple: true } }, + { x: { type: "boolean", multiple: true, default: [true] } }, + { x: { type: "string", default: "default" } }, + { "__proto__": null, constructor: { type: "boolean", default: true } }, + ]) show({ args: ["--no-x"], options, allowNegative: true, strict: false, tokens: true }); + for (const config of [ + null, + { args: [], strict: "has'quote" }, { args: [], strict: "line\nend" }, + { args: [], strict: "x".repeat(28) }, { args: [], strict: "x".repeat(29) }, + { args: [], options: { x: { type: "invalid".repeat(8) } } }, + { args: "bad", strict: 1 }, { args: [], strict: 1 }, { args: [], allowPositionals: 1 }, + { args: [], tokens: "yes" }, { args: [], allowNegative: 0 }, { args: [], options: [] }, + { args: [], options: { x: null } }, { args: [], options: { x: {} } }, + { args: [], options: { x: { type: "number" } } }, + { args: [], options: { x: { type: "string", short: "ab" } } }, + { args: [], options: { x: { type: "string", short: undefined } } }, + { args: [], options: { x: { type: "boolean", multiple: null } } }, + { args: [], options: { x: { type: "string", default: 2 } } }, + { args: [], options: { x: { type: "boolean", multiple: true, default: [true, 2] } } }, + { args: [], options: { x: { type: "string", multiple: true, default: "bad" } } }, + { args: ["--unknown", null] }, { args: ["--", null], strict: false }, + { args: ["--name", null], options }, { args: [1, false, {}], strict: false }, + { args: [], strict: null, tokens: null, allowNegative: null, options: null }, + ]) show(config); + show(Object.assign(Object.create({ strict: false, options: { inherited: { type: "boolean" } } }), { args: ["--inherited"] })); + show({ args: ["--inherited"], options: Object.create({ inherited: { type: "boolean" } }) }); + show({ args: [], options: { x: Object.create({ type: "boolean" }) } }); + show({ args: [], options: { x: Object.assign(Object.create({ multiple: true, default: ["ignored"] }), { type: "string" }) } }); + const protoOptions = Object.create(null); + protoOptions.__proto__ = { type: "string", default: "ignored" }; + show({ args: ["--__proto__=ignored"], options: protoOptions, tokens: true }); + const argumentsWithIterator = ["--name=actual"]; + argumentsWithIterator[Symbol.iterator] = function* () { yield "--unknown"; }; + show({ args: argumentsWithIterator, options, tokens: true }); + const defaults = ["initial"]; + const result = parseArgs({ args: [], options: { name: { type: "string", multiple: true, default: defaults } } }); + console.log("default-identity", result.values.name === defaults); + defaults.push("updated"); + console.log("default-live", JSON.stringify(result)); +} diff --git a/tests/fixtures/npm/node_modules/node24-parseargs-fixture/package.json b/tests/fixtures/npm/node_modules/node24-parseargs-fixture/package.json new file mode 100644 index 000000000..b0b987093 --- /dev/null +++ b/tests/fixtures/npm/node_modules/node24-parseargs-fixture/package.json @@ -0,0 +1 @@ +{"name":"node24-parseargs-fixture","version":"1.0.0","type":"module","main":"index.js","types":"index.d.ts"}