@optique/core 1.0.0-dev.1533 → 1.0.0-dev.1547

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.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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.0.0-dev.1533+25e020de",
3
+ "version": "1.0.0-dev.1547+bde3f632",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",