@optique/core 1.0.0-dev.1536 → 1.0.0-dev.1553

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/usage.d.ts CHANGED
@@ -270,80 +270,6 @@ declare function extractOptionNames(usage: Usage, includeHidden?: boolean): Set<
270
270
  * @since 0.7.0
271
271
  */
272
272
  declare function extractCommandNames(usage: Usage, includeHidden?: boolean): Set<string>;
273
- /**
274
- * Extracts option names that are reachable at the leading token position
275
- * (before any command or argument gate).
276
- *
277
- * Unlike {@link extractOptionNames}, which traverses the entire usage tree,
278
- * this function stops scanning a terms array after encountering a `command`,
279
- * `argument`, or `literal` term, because subsequent terms are scoped under
280
- * that positional token. It still recurses into `optional`, `multiple`,
281
- * and `exclusive` containers, since they represent alternatives or wrappers
282
- * at the same position.
283
- *
284
- * Known limitation: this function infers token positions from the `usage`
285
- * tree, which is a display-oriented structure. Combinators like `tuple()`
286
- * and `object()` sort or flatten usage by priority rather than token order,
287
- * so the results can be inaccurate—both false positives (e.g., options
288
- * appearing before commands due to priority sorting) and false negatives
289
- * (e.g., options after commands that are actually parallel peers).
290
- * The proper fix is to use `Parser.leadingNames` instead of usage-tree
291
- * analysis.
292
- * See https://github.com/dahlia/optique/issues/735
293
- *
294
- * @param usage The usage description to extract leading option names from.
295
- * @param includeHidden Whether to include fully hidden options
296
- * (`hidden: true`) in the result. Defaults to `false`.
297
- * @returns A set of option names reachable at the leading token position.
298
- * @since 1.0.0
299
- */
300
- declare function extractLeadingOptionNames(usage: Usage, includeHidden?: boolean): Set<string>;
301
- /**
302
- * Extracts command names that could match as the first positional token.
303
- *
304
- * Unlike {@link extractCommandNames}, which traverses the entire usage tree,
305
- * this function stops scanning a terms array after encountering a `command`,
306
- * `argument`, or `literal` term, because subsequent terms in that array are
307
- * scoped under that positional token (reachable only after the leading term
308
- * is consumed). It still recurses into `optional`, `multiple`, and
309
- * `exclusive` containers, since they represent alternatives or optional
310
- * wrappers at the same token position.
311
- *
312
- * Known limitation: this function has the same usage-tree ordering caveat
313
- * as {@link extractLeadingOptionNames}.
314
- * See https://github.com/dahlia/optique/issues/735
315
- *
316
- * @param usage The usage description to extract leading command names from.
317
- * @param includeHidden Whether to include fully hidden commands
318
- * (`hidden: true`) in the result. Defaults to `false`.
319
- * @returns A set of command names that could match at the first token position.
320
- * @since 1.0.0
321
- */
322
- declare function extractLeadingCommandNames(usage: Usage, includeHidden?: boolean): Set<string>;
323
- /**
324
- * Extracts literal values that could match as the first positional token.
325
- *
326
- * Unlike {@link extractLiteralValues}, which traverses the entire usage tree,
327
- * this function stops scanning a terms array after encountering a `command`,
328
- * `argument`, or `literal` term, because subsequent terms in that array are
329
- * scoped under that positional token. It still recurses into `optional`,
330
- * `multiple`, and `exclusive` containers.
331
- *
332
- * Literals tagged with `optionValue: true` (produced by
333
- * `appendLiteralToUsage()` when rewriting option metavars for
334
- * `conditional()` discriminators) are skipped, because they represent
335
- * option values rather than standalone positional tokens.
336
- *
337
- * Known limitation: this function has the same usage-tree ordering caveat
338
- * as {@link extractLeadingOptionNames}.
339
- * See https://github.com/dahlia/optique/issues/735
340
- *
341
- * @param usage The usage description to extract leading literal values from.
342
- * @returns A set of literal values that could match at the first token
343
- * position.
344
- * @since 1.0.0
345
- */
346
- declare function extractLeadingLiteralValues(usage: Usage): Set<string>;
347
273
  /**
348
274
  * Extracts all literal values from a usage description.
349
275
  *
@@ -520,4 +446,4 @@ interface UsageTermFormatOptions extends UsageFormatOptions {
520
446
  */
521
447
  declare function formatUsageTerm(term: UsageTerm, options?: UsageTermFormatOptions): string;
522
448
  //#endregion
523
- export { HiddenVisibility, OptionName, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, cloneUsage, cloneUsageTerm, extractArgumentMetavars, extractCommandNames, extractLeadingCommandNames, extractLeadingLiteralValues, extractLeadingOptionNames, extractLiteralValues, extractOptionNames, formatUsage, formatUsageTerm, isDocHidden, isSuggestionHidden, isUsageHidden, mergeHidden, normalizeUsage };
449
+ export { HiddenVisibility, OptionName, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, cloneUsage, cloneUsageTerm, extractArgumentMetavars, extractCommandNames, extractLiteralValues, extractOptionNames, formatUsage, formatUsageTerm, isDocHidden, isSuggestionHidden, isUsageHidden, mergeHidden, normalizeUsage };
package/dist/usage.js CHANGED
@@ -104,197 +104,6 @@ function extractCommandNames(usage, includeHidden) {
104
104
  return names;
105
105
  }
106
106
  /**
107
- * Extracts option names that are reachable at the leading token position
108
- * (before any command or argument gate).
109
- *
110
- * Unlike {@link extractOptionNames}, which traverses the entire usage tree,
111
- * this function stops scanning a terms array after encountering a `command`,
112
- * `argument`, or `literal` term, because subsequent terms are scoped under
113
- * that positional token. It still recurses into `optional`, `multiple`,
114
- * and `exclusive` containers, since they represent alternatives or wrappers
115
- * at the same position.
116
- *
117
- * Known limitation: this function infers token positions from the `usage`
118
- * tree, which is a display-oriented structure. Combinators like `tuple()`
119
- * and `object()` sort or flatten usage by priority rather than token order,
120
- * so the results can be inaccurate—both false positives (e.g., options
121
- * appearing before commands due to priority sorting) and false negatives
122
- * (e.g., options after commands that are actually parallel peers).
123
- * The proper fix is to use `Parser.leadingNames` instead of usage-tree
124
- * analysis.
125
- * See https://github.com/dahlia/optique/issues/735
126
- *
127
- * @param usage The usage description to extract leading option names from.
128
- * @param includeHidden Whether to include fully hidden options
129
- * (`hidden: true`) in the result. Defaults to `false`.
130
- * @returns A set of option names reachable at the leading token position.
131
- * @since 1.0.0
132
- */
133
- function extractLeadingOptionNames(usage, includeHidden) {
134
- const names = /* @__PURE__ */ new Set();
135
- function collectLeading(terms) {
136
- if (!terms || !Array.isArray(terms)) return;
137
- for (const term of terms) switch (term.type) {
138
- case "option":
139
- if (!includeHidden && isSuggestionHidden(term.hidden)) break;
140
- for (const name of term.names) names.add(name);
141
- break;
142
- case "command": return;
143
- case "argument":
144
- case "literal": return;
145
- case "optional":
146
- collectLeading(term.terms);
147
- break;
148
- case "multiple":
149
- collectLeading(term.terms);
150
- if (term.min > 0 && branchConsumesToken(term.terms)) return;
151
- break;
152
- case "exclusive":
153
- for (const branch of term.terms) collectLeading(branch);
154
- if (exclusiveConsumesToken(term.terms)) return;
155
- break;
156
- default: break;
157
- }
158
- }
159
- collectLeading(usage);
160
- return names;
161
- }
162
- /**
163
- * Extracts command names that could match as the first positional token.
164
- *
165
- * Unlike {@link extractCommandNames}, which traverses the entire usage tree,
166
- * this function stops scanning a terms array after encountering a `command`,
167
- * `argument`, or `literal` term, because subsequent terms in that array are
168
- * scoped under that positional token (reachable only after the leading term
169
- * is consumed). It still recurses into `optional`, `multiple`, and
170
- * `exclusive` containers, since they represent alternatives or optional
171
- * wrappers at the same token position.
172
- *
173
- * Known limitation: this function has the same usage-tree ordering caveat
174
- * as {@link extractLeadingOptionNames}.
175
- * See https://github.com/dahlia/optique/issues/735
176
- *
177
- * @param usage The usage description to extract leading command names from.
178
- * @param includeHidden Whether to include fully hidden commands
179
- * (`hidden: true`) in the result. Defaults to `false`.
180
- * @returns A set of command names that could match at the first token position.
181
- * @since 1.0.0
182
- */
183
- function extractLeadingCommandNames(usage, includeHidden) {
184
- const names = /* @__PURE__ */ new Set();
185
- function collectLeading(terms) {
186
- if (!terms || !Array.isArray(terms)) return;
187
- for (const term of terms) switch (term.type) {
188
- case "command":
189
- if (!includeHidden && isSuggestionHidden(term.hidden)) return;
190
- names.add(term.name);
191
- return;
192
- case "argument":
193
- case "literal": return;
194
- case "optional":
195
- collectLeading(term.terms);
196
- break;
197
- case "multiple":
198
- collectLeading(term.terms);
199
- if (term.min > 0 && branchConsumesToken(term.terms)) return;
200
- break;
201
- case "exclusive":
202
- for (const branch of term.terms) collectLeading(branch);
203
- if (exclusiveConsumesToken(term.terms)) return;
204
- break;
205
- default: break;
206
- }
207
- }
208
- collectLeading(usage);
209
- return names;
210
- }
211
- /**
212
- * Extracts literal values that could match as the first positional token.
213
- *
214
- * Unlike {@link extractLiteralValues}, which traverses the entire usage tree,
215
- * this function stops scanning a terms array after encountering a `command`,
216
- * `argument`, or `literal` term, because subsequent terms in that array are
217
- * scoped under that positional token. It still recurses into `optional`,
218
- * `multiple`, and `exclusive` containers.
219
- *
220
- * Literals tagged with `optionValue: true` (produced by
221
- * `appendLiteralToUsage()` when rewriting option metavars for
222
- * `conditional()` discriminators) are skipped, because they represent
223
- * option values rather than standalone positional tokens.
224
- *
225
- * Known limitation: this function has the same usage-tree ordering caveat
226
- * as {@link extractLeadingOptionNames}.
227
- * See https://github.com/dahlia/optique/issues/735
228
- *
229
- * @param usage The usage description to extract leading literal values from.
230
- * @returns A set of literal values that could match at the first token
231
- * position.
232
- * @since 1.0.0
233
- */
234
- function extractLeadingLiteralValues(usage) {
235
- const values = /* @__PURE__ */ new Set();
236
- function collectLeading(terms) {
237
- if (!terms || !Array.isArray(terms)) return;
238
- for (const term of terms) switch (term.type) {
239
- case "literal":
240
- if (term.optionValue) break;
241
- values.add(term.value);
242
- return;
243
- case "command":
244
- case "argument": return;
245
- case "optional":
246
- collectLeading(term.terms);
247
- break;
248
- case "multiple":
249
- collectLeading(term.terms);
250
- if (term.min > 0 && branchConsumesToken(term.terms, true)) return;
251
- break;
252
- case "exclusive":
253
- for (const branch of term.terms) collectLeading(branch);
254
- if (exclusiveConsumesToken(term.terms, true)) return;
255
- break;
256
- default: break;
257
- }
258
- }
259
- collectLeading(usage);
260
- return values;
261
- }
262
- /**
263
- * Checks whether every branch of an exclusive term must consume a positional
264
- * token. When true, terms after the exclusive are at position N+1 and should
265
- * not be considered "leading".
266
- *
267
- * @param skipOptionValueLiterals When `true`, literals tagged with
268
- * `optionValue` are treated as non-positional. Only
269
- * `extractLeadingLiteralValues()` passes `true`; the option/command
270
- * extractors use the default (`false`) so that option+value pairs
271
- * still act as positional gates.
272
- */
273
- function exclusiveConsumesToken(branches, skipOptionValueLiterals = false) {
274
- if (branches.length === 0) return false;
275
- return branches.every((branch) => branchConsumesToken(branch, skipOptionValueLiterals));
276
- }
277
- function branchConsumesToken(terms, skipOptionValueLiterals = false) {
278
- if (!terms || !Array.isArray(terms)) return false;
279
- for (const term of terms) switch (term.type) {
280
- case "command":
281
- case "argument": return true;
282
- case "literal":
283
- if (skipOptionValueLiterals && term.optionValue) break;
284
- return true;
285
- case "option": break;
286
- case "optional": break;
287
- case "multiple":
288
- if (term.min > 0 && branchConsumesToken(term.terms, skipOptionValueLiterals)) return true;
289
- break;
290
- case "exclusive":
291
- if (exclusiveConsumesToken(term.terms, skipOptionValueLiterals)) return true;
292
- break;
293
- default: break;
294
- }
295
- return false;
296
- }
297
- /**
298
107
  * Extracts all literal values from a usage description.
299
108
  *
300
109
  * This function recursively traverses the usage tree and collects all
@@ -765,4 +574,4 @@ function* formatUsageTermInternal(term, options) {
765
574
  }
766
575
 
767
576
  //#endregion
768
- export { cloneUsage, cloneUsageTerm, extractArgumentMetavars, extractCommandNames, extractLeadingCommandNames, extractLeadingLiteralValues, extractLeadingOptionNames, extractLiteralValues, extractOptionNames, formatUsage, formatUsageTerm, isDocHidden, isSuggestionHidden, isUsageHidden, mergeHidden, normalizeUsage };
577
+ export { cloneUsage, cloneUsageTerm, extractArgumentMetavars, extractCommandNames, extractLiteralValues, extractOptionNames, formatUsage, formatUsageTerm, isDocHidden, isSuggestionHidden, isUsageHidden, mergeHidden, normalizeUsage };
package/dist/validate.cjs CHANGED
@@ -112,19 +112,30 @@ function validateMetaNameCollisions(userNames, metaEntries) {
112
112
  }
113
113
  }
114
114
  }
115
- for (const [kind, label, names, prefixMatch] of metaEntries) {
116
- const optionNames = kind === "command" ? userNames.leadingOptions : userNames.allOptions;
117
- const commandNames = kind === "command" ? userNames.leadingCommands : userNames.allCommands;
118
- for (const name of names) {
119
- if (optionNames.has(name)) throw new TypeError(`User-defined option "${name}" conflicts with the built-in ${label}.`);
120
- if (commandNames.has(name)) throw new TypeError(`User-defined command "${name}" conflicts with the built-in ${label}.`);
121
- const literalNames = kind === "command" ? userNames.leadingLiterals : userNames.allLiterals;
122
- if (literalNames.has(name)) throw new TypeError(`Literal value "${name}" conflicts with the built-in ${label}.`);
123
- if (prefixMatch) {
124
- const prefix = name + "=";
125
- for (const userName of optionNames) if (userName.startsWith(prefix)) throw new TypeError(`User-defined option "${userName}" conflicts with the built-in ${label} (prefix "${prefix}").`);
126
- for (const userName of commandNames) if (userName.startsWith(prefix)) throw new TypeError(`User-defined command "${userName}" conflicts with the built-in ${label} (prefix "${prefix}").`);
127
- for (const literal of literalNames) if (literal.startsWith(prefix)) throw new TypeError(`Literal value "${literal}" conflicts with the built-in ${label} (prefix "${prefix}").`);
115
+ for (const [kind, label, names, prefixMatch] of metaEntries) for (const name of names) {
116
+ if (kind === "command") {
117
+ if (userNames.leadingNames.has(name)) if (userNames.allOptions.has(name)) throw new TypeError(`User-defined option "${name}" conflicts with the built-in ${label}.`);
118
+ else if (userNames.allCommands.has(name)) throw new TypeError(`User-defined command "${name}" conflicts with the built-in ${label}.`);
119
+ else if (userNames.allLiterals.has(name)) throw new TypeError(`Literal value "${name}" conflicts with the built-in ${label}.`);
120
+ else throw new TypeError(`User-defined name "${name}" conflicts with the built-in ${label}.`);
121
+ } else {
122
+ if (userNames.allOptions.has(name)) throw new TypeError(`User-defined option "${name}" conflicts with the built-in ${label}.`);
123
+ if (userNames.allCommands.has(name)) throw new TypeError(`User-defined command "${name}" conflicts with the built-in ${label}.`);
124
+ if (userNames.allLiterals.has(name)) throw new TypeError(`Literal value "${name}" conflicts with the built-in ${label}.`);
125
+ }
126
+ if (prefixMatch) {
127
+ const prefix = name + "=";
128
+ const checkSets = kind === "command" ? [userNames.leadingNames] : [
129
+ userNames.allOptions,
130
+ userNames.allCommands,
131
+ userNames.allLiterals
132
+ ];
133
+ for (const nameSet of checkSets) for (const userName of nameSet) {
134
+ if (!userName.startsWith(prefix)) continue;
135
+ if (userNames.allOptions.has(userName)) throw new TypeError(`User-defined option "${userName}" conflicts with the built-in ${label} (prefix "${prefix}").`);
136
+ else if (userNames.allCommands.has(userName)) throw new TypeError(`User-defined command "${userName}" conflicts with the built-in ${label} (prefix "${prefix}").`);
137
+ else if (userNames.allLiterals.has(userName)) throw new TypeError(`Literal value "${userName}" conflicts with the built-in ${label} (prefix "${prefix}").`);
138
+ else throw new TypeError(`User-defined name "${userName}" conflicts with the built-in ${label} (prefix "${prefix}").`);
128
139
  }
129
140
  }
130
141
  }
package/dist/validate.js CHANGED
@@ -111,19 +111,30 @@ function validateMetaNameCollisions(userNames, metaEntries) {
111
111
  }
112
112
  }
113
113
  }
114
- for (const [kind, label, names, prefixMatch] of metaEntries) {
115
- const optionNames = kind === "command" ? userNames.leadingOptions : userNames.allOptions;
116
- const commandNames = kind === "command" ? userNames.leadingCommands : userNames.allCommands;
117
- for (const name of names) {
118
- if (optionNames.has(name)) throw new TypeError(`User-defined option "${name}" conflicts with the built-in ${label}.`);
119
- if (commandNames.has(name)) throw new TypeError(`User-defined command "${name}" conflicts with the built-in ${label}.`);
120
- const literalNames = kind === "command" ? userNames.leadingLiterals : userNames.allLiterals;
121
- if (literalNames.has(name)) throw new TypeError(`Literal value "${name}" conflicts with the built-in ${label}.`);
122
- if (prefixMatch) {
123
- const prefix = name + "=";
124
- for (const userName of optionNames) if (userName.startsWith(prefix)) throw new TypeError(`User-defined option "${userName}" conflicts with the built-in ${label} (prefix "${prefix}").`);
125
- for (const userName of commandNames) if (userName.startsWith(prefix)) throw new TypeError(`User-defined command "${userName}" conflicts with the built-in ${label} (prefix "${prefix}").`);
126
- for (const literal of literalNames) if (literal.startsWith(prefix)) throw new TypeError(`Literal value "${literal}" conflicts with the built-in ${label} (prefix "${prefix}").`);
114
+ for (const [kind, label, names, prefixMatch] of metaEntries) for (const name of names) {
115
+ if (kind === "command") {
116
+ if (userNames.leadingNames.has(name)) if (userNames.allOptions.has(name)) throw new TypeError(`User-defined option "${name}" conflicts with the built-in ${label}.`);
117
+ else if (userNames.allCommands.has(name)) throw new TypeError(`User-defined command "${name}" conflicts with the built-in ${label}.`);
118
+ else if (userNames.allLiterals.has(name)) throw new TypeError(`Literal value "${name}" conflicts with the built-in ${label}.`);
119
+ else throw new TypeError(`User-defined name "${name}" conflicts with the built-in ${label}.`);
120
+ } else {
121
+ if (userNames.allOptions.has(name)) throw new TypeError(`User-defined option "${name}" conflicts with the built-in ${label}.`);
122
+ if (userNames.allCommands.has(name)) throw new TypeError(`User-defined command "${name}" conflicts with the built-in ${label}.`);
123
+ if (userNames.allLiterals.has(name)) throw new TypeError(`Literal value "${name}" conflicts with the built-in ${label}.`);
124
+ }
125
+ if (prefixMatch) {
126
+ const prefix = name + "=";
127
+ const checkSets = kind === "command" ? [userNames.leadingNames] : [
128
+ userNames.allOptions,
129
+ userNames.allCommands,
130
+ userNames.allLiterals
131
+ ];
132
+ for (const nameSet of checkSets) for (const userName of nameSet) {
133
+ if (!userName.startsWith(prefix)) continue;
134
+ if (userNames.allOptions.has(userName)) throw new TypeError(`User-defined option "${userName}" conflicts with the built-in ${label} (prefix "${prefix}").`);
135
+ else if (userNames.allCommands.has(userName)) throw new TypeError(`User-defined command "${userName}" conflicts with the built-in ${label} (prefix "${prefix}").`);
136
+ else if (userNames.allLiterals.has(userName)) throw new TypeError(`Literal value "${userName}" conflicts with the built-in ${label} (prefix "${prefix}").`);
137
+ else throw new TypeError(`User-defined name "${userName}" conflicts with the built-in ${label} (prefix "${prefix}").`);
127
138
  }
128
139
  }
129
140
  }