@optique/core 1.0.0-dev.1514 → 1.0.0-dev.1523

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/validate.cjs CHANGED
@@ -63,6 +63,75 @@ function validateCommandNames(names, label) {
63
63
  }
64
64
  }
65
65
  /**
66
+ * Validates that there are no name collisions among meta features
67
+ * (help, version, completion) and between meta features and user parsers.
68
+ *
69
+ * The collision check is *position-aware*:
70
+ *
71
+ * - Meta **command** entries match at `args[0]` only, so they are checked
72
+ * against *leading* user names (those reachable before any positional gate).
73
+ * - Meta **option** entries use lenient scanners that match anywhere in
74
+ * `argv`, so they are checked against *all* user names at every depth,
75
+ * including literal values from conditional discriminators.
76
+ *
77
+ * Meta-vs-meta collisions are always checked in a unified namespace,
78
+ * because a meta command named `"--help"` and a meta option named
79
+ * `"--help"` both compete for the same token.
80
+ *
81
+ * @param userNames User parser names extracted at different scopes.
82
+ * @param metaEntries Active meta feature entries annotated with their kind.
83
+ * @throws {TypeError} If any collision or duplicate is detected.
84
+ * @since 1.0.0
85
+ */
86
+ function validateMetaNameCollisions(userNames, metaEntries) {
87
+ for (const [, label, names] of metaEntries) {
88
+ const seen = /* @__PURE__ */ new Set();
89
+ for (const name of names) {
90
+ if (seen.has(name)) throw new TypeError(`${capitalize(label)} has a duplicate name: "${name}"`);
91
+ seen.add(name);
92
+ }
93
+ }
94
+ const nameToLabel = /* @__PURE__ */ new Map();
95
+ for (const [, label, names] of metaEntries) for (const name of names) {
96
+ const existingLabel = nameToLabel.get(name);
97
+ if (existingLabel != null) throw new TypeError(`Name "${name}" is used by both ${existingLabel} and ${label}.`);
98
+ nameToLabel.set(name, label);
99
+ }
100
+ for (let i = 0; i < metaEntries.length; i++) {
101
+ const [, label, names, prefixMatch] = metaEntries[i];
102
+ if (!prefixMatch) continue;
103
+ for (const name of names) {
104
+ const prefix = name + "=";
105
+ for (let j = 0; j < metaEntries.length; j++) {
106
+ const [, otherLabel, otherNames] = metaEntries[j];
107
+ for (const otherName of otherNames) {
108
+ if (i === j && otherName === name) continue;
109
+ if (!otherName.startsWith(prefix)) continue;
110
+ throw new TypeError("The prefix form of name \"" + name + "\" in " + label + " shadows \"" + otherName + "\" in " + otherLabel + ".");
111
+ }
112
+ }
113
+ }
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
+ if (kind === "option" && userNames.allLiterals.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 userNames.allLiterals) if (literal.startsWith(prefix)) throw new TypeError(`Literal value "${literal}" conflicts with the built-in ${label} (prefix "${prefix}").`);
127
+ }
128
+ }
129
+ }
130
+ }
131
+ function capitalize(s) {
132
+ return s.charAt(0).toUpperCase() + s.slice(1);
133
+ }
134
+ /**
66
135
  * Validates a program name at runtime.
67
136
  *
68
137
  * Program names may contain spaces (e.g., file paths), but must not be empty,
@@ -78,8 +147,28 @@ function validateProgramName(programName) {
78
147
  if (/^\s+$/.test(programName)) throw new TypeError(`Program name must not be whitespace-only: "${escapeControlChars(programName)}".`);
79
148
  if (CONTROL_CHAR_RE.test(programName)) throw new TypeError(`Program name must not contain control characters: "${escapeControlChars(programName)}".`);
80
149
  }
150
+ /**
151
+ * Validates a label at runtime.
152
+ *
153
+ * Labels are used as section titles in documentation output. They may contain
154
+ * spaces (e.g., "Connection options"), but must not be empty, whitespace-only,
155
+ * or contain control characters.
156
+ *
157
+ * @param label The label to validate.
158
+ * @throws {TypeError} If the label is not a string, is empty,
159
+ * whitespace-only, or contains control characters.
160
+ * @since 1.0.0
161
+ */
162
+ function validateLabel(label) {
163
+ if (typeof label !== "string") throw new TypeError("Label must be a string.");
164
+ if (label === "") throw new TypeError("Label must not be empty.");
165
+ if (/^\s+$/.test(label)) throw new TypeError(`Label must not be whitespace-only: "${escapeControlChars(label)}".`);
166
+ if (CONTROL_CHAR_RE.test(label)) throw new TypeError(`Label must not contain control characters: "${escapeControlChars(label)}".`);
167
+ }
81
168
 
82
169
  //#endregion
83
170
  exports.validateCommandNames = validateCommandNames;
171
+ exports.validateLabel = validateLabel;
172
+ exports.validateMetaNameCollisions = validateMetaNameCollisions;
84
173
  exports.validateOptionNames = validateOptionNames;
85
174
  exports.validateProgramName = validateProgramName;
package/dist/validate.js CHANGED
@@ -62,6 +62,75 @@ function validateCommandNames(names, label) {
62
62
  }
63
63
  }
64
64
  /**
65
+ * Validates that there are no name collisions among meta features
66
+ * (help, version, completion) and between meta features and user parsers.
67
+ *
68
+ * The collision check is *position-aware*:
69
+ *
70
+ * - Meta **command** entries match at `args[0]` only, so they are checked
71
+ * against *leading* user names (those reachable before any positional gate).
72
+ * - Meta **option** entries use lenient scanners that match anywhere in
73
+ * `argv`, so they are checked against *all* user names at every depth,
74
+ * including literal values from conditional discriminators.
75
+ *
76
+ * Meta-vs-meta collisions are always checked in a unified namespace,
77
+ * because a meta command named `"--help"` and a meta option named
78
+ * `"--help"` both compete for the same token.
79
+ *
80
+ * @param userNames User parser names extracted at different scopes.
81
+ * @param metaEntries Active meta feature entries annotated with their kind.
82
+ * @throws {TypeError} If any collision or duplicate is detected.
83
+ * @since 1.0.0
84
+ */
85
+ function validateMetaNameCollisions(userNames, metaEntries) {
86
+ for (const [, label, names] of metaEntries) {
87
+ const seen = /* @__PURE__ */ new Set();
88
+ for (const name of names) {
89
+ if (seen.has(name)) throw new TypeError(`${capitalize(label)} has a duplicate name: "${name}"`);
90
+ seen.add(name);
91
+ }
92
+ }
93
+ const nameToLabel = /* @__PURE__ */ new Map();
94
+ for (const [, label, names] of metaEntries) for (const name of names) {
95
+ const existingLabel = nameToLabel.get(name);
96
+ if (existingLabel != null) throw new TypeError(`Name "${name}" is used by both ${existingLabel} and ${label}.`);
97
+ nameToLabel.set(name, label);
98
+ }
99
+ for (let i = 0; i < metaEntries.length; i++) {
100
+ const [, label, names, prefixMatch] = metaEntries[i];
101
+ if (!prefixMatch) continue;
102
+ for (const name of names) {
103
+ const prefix = name + "=";
104
+ for (let j = 0; j < metaEntries.length; j++) {
105
+ const [, otherLabel, otherNames] = metaEntries[j];
106
+ for (const otherName of otherNames) {
107
+ if (i === j && otherName === name) continue;
108
+ if (!otherName.startsWith(prefix)) continue;
109
+ throw new TypeError("The prefix form of name \"" + name + "\" in " + label + " shadows \"" + otherName + "\" in " + otherLabel + ".");
110
+ }
111
+ }
112
+ }
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
+ if (kind === "option" && userNames.allLiterals.has(name)) throw new TypeError(`Literal value "${name}" conflicts with the built-in ${label}.`);
121
+ if (prefixMatch) {
122
+ const prefix = name + "=";
123
+ for (const userName of optionNames) if (userName.startsWith(prefix)) throw new TypeError(`User-defined option "${userName}" conflicts with the built-in ${label} (prefix "${prefix}").`);
124
+ for (const userName of commandNames) if (userName.startsWith(prefix)) throw new TypeError(`User-defined command "${userName}" conflicts with the built-in ${label} (prefix "${prefix}").`);
125
+ for (const literal of userNames.allLiterals) if (literal.startsWith(prefix)) throw new TypeError(`Literal value "${literal}" conflicts with the built-in ${label} (prefix "${prefix}").`);
126
+ }
127
+ }
128
+ }
129
+ }
130
+ function capitalize(s) {
131
+ return s.charAt(0).toUpperCase() + s.slice(1);
132
+ }
133
+ /**
65
134
  * Validates a program name at runtime.
66
135
  *
67
136
  * Program names may contain spaces (e.g., file paths), but must not be empty,
@@ -77,6 +146,24 @@ function validateProgramName(programName) {
77
146
  if (/^\s+$/.test(programName)) throw new TypeError(`Program name must not be whitespace-only: "${escapeControlChars(programName)}".`);
78
147
  if (CONTROL_CHAR_RE.test(programName)) throw new TypeError(`Program name must not contain control characters: "${escapeControlChars(programName)}".`);
79
148
  }
149
+ /**
150
+ * Validates a label at runtime.
151
+ *
152
+ * Labels are used as section titles in documentation output. They may contain
153
+ * spaces (e.g., "Connection options"), but must not be empty, whitespace-only,
154
+ * or contain control characters.
155
+ *
156
+ * @param label The label to validate.
157
+ * @throws {TypeError} If the label is not a string, is empty,
158
+ * whitespace-only, or contains control characters.
159
+ * @since 1.0.0
160
+ */
161
+ function validateLabel(label) {
162
+ if (typeof label !== "string") throw new TypeError("Label must be a string.");
163
+ if (label === "") throw new TypeError("Label must not be empty.");
164
+ if (/^\s+$/.test(label)) throw new TypeError(`Label must not be whitespace-only: "${escapeControlChars(label)}".`);
165
+ if (CONTROL_CHAR_RE.test(label)) throw new TypeError(`Label must not contain control characters: "${escapeControlChars(label)}".`);
166
+ }
80
167
 
81
168
  //#endregion
82
- export { validateCommandNames, validateOptionNames, validateProgramName };
169
+ export { validateCommandNames, validateLabel, validateMetaNameCollisions, validateOptionNames, validateProgramName };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.0.0-dev.1514+78606b85",
3
+ "version": "1.0.0-dev.1523+edc4d966",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",