@loomcli/core 0.6.0 → 0.8.0
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/NOTICE +34 -0
- package/dist/application.d.ts +11 -6
- package/dist/application.js +57 -19
- package/dist/capture.d.ts +65 -0
- package/dist/capture.js +99 -0
- package/dist/chain.d.ts +28 -17
- package/dist/chain.js +11 -10
- package/dist/command-rules.d.ts +1 -1
- package/dist/command-rules.js +2 -2
- package/dist/command.d.ts +31 -47
- package/dist/command.js +204 -209
- package/dist/errors.d.ts +22 -21
- package/dist/errors.js +38 -18
- package/dist/facts.d.ts +5 -0
- package/dist/facts.js +8 -1
- package/dist/globals.d.ts +48 -22
- package/dist/globals.js +72 -29
- package/dist/glyphs.generated.js +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -1
- package/dist/input-rules.d.ts +20 -2
- package/dist/input-rules.js +47 -5
- package/dist/inspect.d.ts +33 -17
- package/dist/inspect.js +74 -87
- package/dist/locate.js +52 -60
- package/dist/options.d.ts +101 -83
- package/dist/options.js +311 -267
- package/dist/parse.d.ts +162 -0
- package/dist/parse.js +601 -0
- package/dist/plain.d.ts +52 -2
- package/dist/plain.js +228 -2
- package/dist/plugin-rules.d.ts +8 -4
- package/dist/plugin-rules.js +13 -9
- package/dist/plugin-settings.d.ts +18 -0
- package/dist/plugin-settings.js +38 -0
- package/dist/plugin.d.ts +32 -27
- package/dist/plugin.js +107 -89
- package/dist/sources.d.ts +18 -9
- package/dist/sources.js +50 -20
- package/dist/style-layout.js +2 -2
- package/dist/style-width.d.ts +13 -0
- package/dist/style-width.js +170 -0
- package/dist/types.d.ts +70 -30
- package/dist/unicode.generated.d.ts +27 -0
- package/dist/unicode.generated.js +1036 -0
- package/dist/validation.d.ts +108 -34
- package/dist/validation.js +282 -125
- package/dist/view.d.ts +16 -11
- package/dist/view.js +13 -4
- package/licenses/unicode-LICENSE.txt +41 -0
- package/licenses/uucode-LICENSE.md +35 -0
- package/package.json +9 -5
package/dist/validation.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { captureDeclaration, elidedRead, unreadableFault } from './capture.js';
|
|
1
2
|
import { schemaOptions } from './context.js';
|
|
2
3
|
import { escapeControlCharacters } from './controls.js';
|
|
4
|
+
import { elided, spelled } from './diagnostic-text.js';
|
|
3
5
|
import { asSentence, DeclarationError, InputError, quoted, reasonOf } from './errors.js';
|
|
4
6
|
import { callSite, factFault, flagFault, partOf, siteFinding } from './facts.js';
|
|
5
|
-
import { booleanOptionValueRule, defaultShape, invalidDefault, notAValidator, omissionAlreadyDecided, omissionWithoutValidator, requiredWithDefault, } from './input-rules.js';
|
|
6
|
-
import { booleanValue } from './options.js';
|
|
7
|
-
import {
|
|
7
|
+
import { booleanOptionValueRule, countOptionValueRule, defaultDepth, defaultLevels, defaultShape, invalidDefault, invalidImplied, notAValidator, omissionAlreadyDecided, omissionWithoutValidator, requiredWithDefault, } from './input-rules.js';
|
|
8
|
+
import { booleanValue, reportedOf, spellingsOf } from './options.js';
|
|
9
|
+
import { boundedSnapshot, NestedTooDeepError, shallowList } from './plain.js';
|
|
8
10
|
import { notAnObject } from './plugin-rules.js';
|
|
9
11
|
import { validatorFailed } from './rules.js';
|
|
10
12
|
/** Every declaration in validation order: the globals first, then the reading Command's own. */
|
|
@@ -42,6 +44,10 @@ class ValidatedInputs {
|
|
|
42
44
|
read(input) {
|
|
43
45
|
return this.#values.get(input);
|
|
44
46
|
}
|
|
47
|
+
/** Whether this pass validated the declaration's value. */
|
|
48
|
+
has(input) {
|
|
49
|
+
return this.#values.has(input);
|
|
50
|
+
}
|
|
45
51
|
#field(input) {
|
|
46
52
|
// Last resort: no typed path exists. The map stores every validated value as `unknown`.
|
|
47
53
|
// An object literal with a generic computed key does not type as `Record<Name, _>` either.
|
|
@@ -53,34 +59,66 @@ class ValidatedInputs {
|
|
|
53
59
|
}
|
|
54
60
|
}
|
|
55
61
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
62
|
+
* Authoring's one read of a config, through `captureDeclaration`: the prototype verdict, the copy of
|
|
63
|
+
* every own string key, the copies of its `extensions` and `aliases` lists, and the snapshot of its
|
|
64
|
+
* default, inside one try. Every later check, the registry entry, and every sentence read the copy
|
|
65
|
+
* and never the author's object again, so a getter runs once and the caller's later changes reach
|
|
66
|
+
* nothing. The default is copied and frozen to `defaultLevels` levels, cycles included, and that
|
|
67
|
+
* copy is the value the graph publishes and a run validates; every other property is captured as
|
|
68
|
+
* declared, because core clones no library object. A read that throws, from a getter or a proxy
|
|
69
|
+
* trap, is the unreadable fault, named by the key it threw in, a default nested deeper is the
|
|
70
|
+
* too-deep fault, and a value that is not a plain object is the not-an-object fault.
|
|
59
71
|
*/
|
|
60
|
-
export function
|
|
61
|
-
const {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
export function captureConfig(config, faults) {
|
|
73
|
+
const { notAnObject: notAnObjectFault, tooDeep, unreadable } = faults;
|
|
74
|
+
return captureDeclaration(config, (copy, read) => {
|
|
75
|
+
// Presence is the key, so a declared `default: undefined` stays a default.
|
|
76
|
+
read.nested(copy, 'default', (value) => boundedSnapshot(value, defaultLevels));
|
|
77
|
+
read.nested(copy, 'extensions', shallowList);
|
|
78
|
+
read.nested(copy, 'aliases', shallowList);
|
|
79
|
+
}, {
|
|
80
|
+
notAnObject: notAnObjectFault,
|
|
81
|
+
unreadable: (thrown, slot) => thrown instanceof NestedTooDeepError ? tooDeep() : unreadable(thrown, slot),
|
|
69
82
|
});
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
83
|
+
}
|
|
84
|
+
/** The fault of a default nested deeper than `defaultLevels`, declared by `subject`. */
|
|
85
|
+
export function defaultDepthFault(subject, findings) {
|
|
86
|
+
return new DeclarationError(defaultDepth, {
|
|
87
|
+
correction: `Nest a default at most ${String(defaultLevels)} levels deep.`,
|
|
88
|
+
findings,
|
|
89
|
+
sentence: `${subject} default nests deeper than ${String(defaultLevels)} levels.`,
|
|
74
90
|
});
|
|
75
91
|
}
|
|
76
92
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
93
|
+
* The config every `argument()`, `option()`, and `globalOption()` call, and every input a lifecycle
|
|
94
|
+
* hook declares, reads through `captureConfig`. A call in JavaScript that supplies none, or a value
|
|
95
|
+
* of another kind, reports a declaration fault and not a TypeError, and so does a config whose read
|
|
96
|
+
* throws. Each fault marks the config on the call at `place`.
|
|
80
97
|
*/
|
|
81
|
-
export function
|
|
82
|
-
const
|
|
83
|
-
|
|
98
|
+
export function captureInputConfig(declared, place) {
|
|
99
|
+
const { config, kind, name } = declared;
|
|
100
|
+
const subject = `${kind === 'argument' ? 'Argument' : 'Option'} ${quoted(name)}`;
|
|
101
|
+
// The finding marks the config, or one key inside it, which `shown` stands for when the call prints.
|
|
102
|
+
const findings = (shown, keys = []) => [
|
|
103
|
+
siteFinding(callSite(subject, { arguments: [name, shown], ...place }), ['1', ...keys].join('.')),
|
|
104
|
+
];
|
|
105
|
+
return captureConfig(config, {
|
|
106
|
+
notAnObject: () => new DeclarationError(notAnObject, {
|
|
107
|
+
correction: `Supply ${kind === 'argument' ? 'an argument' : 'an option'} config object, such as ${kind === 'argument' ? '{}' : "{ type: 'string' }"}.`,
|
|
108
|
+
findings: findings(config),
|
|
109
|
+
sentence: `${subject} declares a config that is not an object.`,
|
|
110
|
+
}),
|
|
111
|
+
// The default's walk stopped at the limit, so the finding prints it elided.
|
|
112
|
+
tooDeep: () => {
|
|
113
|
+
const { keys, shown } = elidedRead('default');
|
|
114
|
+
return defaultDepthFault(subject, findings(shown, keys));
|
|
115
|
+
},
|
|
116
|
+
// A part whose read threw is never read again, so the finding prints it elided.
|
|
117
|
+
unreadable: (thrown, slot) => {
|
|
118
|
+
const { keys, shown } = elidedRead(slot);
|
|
119
|
+
return unreadableFault({ declared: 'config', findings: findings(shown, keys), subject }, thrown);
|
|
120
|
+
},
|
|
121
|
+
});
|
|
84
122
|
}
|
|
85
123
|
/**
|
|
86
124
|
* A declaration error names the declaration, because the author reads the declaration to fix it.
|
|
@@ -92,11 +130,11 @@ export function declarationSubject(input) {
|
|
|
92
130
|
: `Option ${quoted(input.name)}`;
|
|
93
131
|
}
|
|
94
132
|
/**
|
|
95
|
-
* Where one input was declared:
|
|
96
|
-
*
|
|
133
|
+
* Where one Command's own input was declared: its `argument()` or `option()` call on the Command at
|
|
134
|
+
* `path`. A global option's site is the built table's, under `BuiltGlobals.sites`.
|
|
97
135
|
*/
|
|
98
|
-
export function inputPlace(input,
|
|
99
|
-
return
|
|
136
|
+
export function inputPlace(input, path) {
|
|
137
|
+
return { call: input.kind, path };
|
|
100
138
|
}
|
|
101
139
|
/**
|
|
102
140
|
* The site of the call that declared one input at `place`, rebuilt as `call(name, config)`. Every
|
|
@@ -107,23 +145,24 @@ export function declaringSite(input, place, subject = declarationSubject(input))
|
|
|
107
145
|
return callSite(subject, { arguments: [input.name, input.config], ...place });
|
|
108
146
|
}
|
|
109
147
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
148
|
+
* One input's site with its config printed elided, for a fault judged before the config is read,
|
|
149
|
+
* such as an invalid name, so the fault reads none of the config.
|
|
150
|
+
*/
|
|
151
|
+
export function configUnread(site) {
|
|
152
|
+
const [name, config] = site.declaration.arguments;
|
|
153
|
+
const shown = config === undefined ? [name] : [name, spelled(elided)];
|
|
154
|
+
return { ...site, declaration: { ...site.declaration, arguments: shown } };
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The token an operator would type for one declaration: an option's reported spelling, read from
|
|
158
|
+
* the routed Command's table, and the declared name for an argument. Every input diagnostic and
|
|
112
159
|
* every reported problem names the declaration this way, so an omission and a rejected value read
|
|
113
160
|
* alike and a `shortOnly` option is never named by a long form it does not accept.
|
|
114
161
|
*/
|
|
115
|
-
function spellingOf(input) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const { config } = input;
|
|
120
|
-
if (config.shortOnly === true && config.short !== undefined) {
|
|
121
|
-
return `-${config.short}`;
|
|
122
|
-
}
|
|
123
|
-
// A negative-only Boolean option accepts its negative form alone.
|
|
124
|
-
return config.type === 'boolean' && config.polarity === 'negative'
|
|
125
|
-
? `--no-${input.name}`
|
|
126
|
-
: `--${input.name}`;
|
|
162
|
+
function spellingOf(input, table) {
|
|
163
|
+
return input.kind === 'argument'
|
|
164
|
+
? input.name
|
|
165
|
+
: reportedOf(spellingsOf(table, input.name), input.name);
|
|
127
166
|
}
|
|
128
167
|
/**
|
|
129
168
|
* An input diagnostic names the declaration by kind and by the spelling that reaches it. A value an
|
|
@@ -207,7 +246,7 @@ function checkOmissionValidation(input, site, subject) {
|
|
|
207
246
|
});
|
|
208
247
|
}
|
|
209
248
|
}
|
|
210
|
-
/** The value rules a Boolean option may not declare, in the order
|
|
249
|
+
/** The value rules a Boolean or counted option may not declare, in the order a diagnostic names them. */
|
|
211
250
|
const valueRules = ['validate', 'default', 'required', 'validateOmitted'];
|
|
212
251
|
/** Whether a `validate` value is a Standard Schema v1 object that core can call. */
|
|
213
252
|
function isStandardSchema(validator) {
|
|
@@ -221,17 +260,32 @@ function isStandardSchema(validator) {
|
|
|
221
260
|
typeof Reflect.get(props, 'vendor') === 'string' &&
|
|
222
261
|
typeof Reflect.get(props, 'validate') === 'function');
|
|
223
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* The value rules a Boolean or counted option declares, which take a value it never consumes. Each
|
|
265
|
+
* kind names its own absent value in the correction.
|
|
266
|
+
*/
|
|
267
|
+
function checkValueless(config, site, subject) {
|
|
268
|
+
const declared = valueRules.find((key) => key in config);
|
|
269
|
+
if (declared === undefined) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (config.type === 'count') {
|
|
273
|
+
throw factFault(countOptionValueRule, site, {
|
|
274
|
+
correction: `Remove ${declared}; a counted option reads 0 when no occurrence supplies it.`,
|
|
275
|
+
fact: declared,
|
|
276
|
+
sentence: `${subject} is a counted option and declares ${declared}.`,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
throw factFault(booleanOptionValueRule, site, {
|
|
280
|
+
correction: `Remove ${declared}; use polarity to control its absent value.`,
|
|
281
|
+
fact: declared,
|
|
282
|
+
sentence: `${subject} is Boolean and declares ${declared}.`,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
224
285
|
function checkDeclaration(input, site, subject) {
|
|
225
286
|
const { config } = input;
|
|
226
|
-
if (input.kind === 'option' && input.config.type
|
|
227
|
-
|
|
228
|
-
if (declared !== undefined) {
|
|
229
|
-
throw factFault(booleanOptionValueRule, site, {
|
|
230
|
-
correction: `Remove ${declared}; use polarity to control its absent value.`,
|
|
231
|
-
fact: declared,
|
|
232
|
-
sentence: `${subject} is Boolean and declares ${declared}.`,
|
|
233
|
-
});
|
|
234
|
-
}
|
|
287
|
+
if (input.kind === 'option' && input.config.type !== 'string') {
|
|
288
|
+
checkValueless(input.config, site, subject);
|
|
235
289
|
return;
|
|
236
290
|
}
|
|
237
291
|
if (config.required !== undefined && typeof config.required !== 'boolean') {
|
|
@@ -299,7 +353,7 @@ function readIssue(issue) {
|
|
|
299
353
|
* belong to the value, so the caller names those.
|
|
300
354
|
*/
|
|
301
355
|
async function validate(input, raw, call) {
|
|
302
|
-
const { context,
|
|
356
|
+
const { context, site } = call;
|
|
303
357
|
const validator = input.config.validate;
|
|
304
358
|
if (validator === undefined) {
|
|
305
359
|
return { value: raw };
|
|
@@ -320,7 +374,6 @@ async function validate(input, raw, call) {
|
|
|
320
374
|
}
|
|
321
375
|
catch (error) {
|
|
322
376
|
// The reason is the author's detail: a distributed build shows the generic defect message.
|
|
323
|
-
const site = place === undefined ? undefined : declaringSite(input, place);
|
|
324
377
|
throw new DeclarationError(validatorFailed, {
|
|
325
378
|
correction: 'Fix the validator.',
|
|
326
379
|
findings: site === undefined ? [] : [siteFinding(site, partOf(site, 'validate'))],
|
|
@@ -328,17 +381,34 @@ async function validate(input, raw, call) {
|
|
|
328
381
|
}, { cause: error });
|
|
329
382
|
}
|
|
330
383
|
}
|
|
384
|
+
/**
|
|
385
|
+
* Where a bare spelling supplied one option's implied value, with that value's prepared output, or
|
|
386
|
+
* `undefined` when no bare spelling supplied it.
|
|
387
|
+
*/
|
|
388
|
+
function impliedPositions(input, read) {
|
|
389
|
+
const positions = input.kind === 'option' ? read.options.implied.get(input.name) : undefined;
|
|
390
|
+
return positions === undefined
|
|
391
|
+
? undefined
|
|
392
|
+
: { output: read.declaredValues.implied.get(input), positions };
|
|
393
|
+
}
|
|
331
394
|
/**
|
|
332
395
|
* One validation of a declared value. A multiple option or a variadic argument passes each of its
|
|
333
396
|
* values through the validator in order, and each issue reads at its value's position before its
|
|
334
397
|
* own path, so the action receives the array of outputs. Every other input passes its value once.
|
|
335
|
-
*
|
|
336
|
-
*
|
|
398
|
+
* A bare spelling's implied value never meets the validator here: its output was prepared before
|
|
399
|
+
* any token, so a scalar that a bare spelling supplied, and each bare occurrence in a multiple
|
|
400
|
+
* option's list, reuse a copy of that output in place. Each call reads a fresh context whose
|
|
401
|
+
* arrays are copies, so a write to them never reaches the next call. The host is the one captured
|
|
402
|
+
* object that every call and the action share.
|
|
337
403
|
*/
|
|
338
404
|
async function validateDeclared(input, raw, call) {
|
|
339
|
-
const { context,
|
|
405
|
+
const { context, implied, signal, site } = call;
|
|
406
|
+
if (!collects(input) && implied?.positions.includes(0) === true) {
|
|
407
|
+
// A bare spelling supplied the implied value, whose output was prepared before any token.
|
|
408
|
+
return { value: freshDeclaredValue(implied.output) };
|
|
409
|
+
}
|
|
340
410
|
if (!collects(input) || input.config.validate === undefined) {
|
|
341
|
-
return validate(input, raw, { context: context(),
|
|
411
|
+
return validate(input, raw, { context: context(), site });
|
|
342
412
|
}
|
|
343
413
|
if (!Array.isArray(raw)) {
|
|
344
414
|
// The parser, the input sources, and the declaration rules only ever supply an array here.
|
|
@@ -351,7 +421,10 @@ async function validateDeclared(input, raw, call) {
|
|
|
351
421
|
// A cancelled run starts no further call; the run resolves its cancellation code instead.
|
|
352
422
|
break;
|
|
353
423
|
}
|
|
354
|
-
|
|
424
|
+
// A bare occurrence holds the implied value, whose output was prepared before any token.
|
|
425
|
+
const result = implied?.positions.includes(position) === true
|
|
426
|
+
? { value: freshDeclaredValue(implied.output) }
|
|
427
|
+
: await validate(input, value, { context: context(), site });
|
|
355
428
|
if (result.issues === undefined) {
|
|
356
429
|
outputs.push(result.value);
|
|
357
430
|
}
|
|
@@ -432,44 +505,84 @@ export function checkDeclarations(inputs, named) {
|
|
|
432
505
|
}
|
|
433
506
|
}
|
|
434
507
|
/**
|
|
435
|
-
* Every declared default, validated before any token is read
|
|
436
|
-
*
|
|
437
|
-
*
|
|
508
|
+
* Every declared default and implied value, validated before any token is read, in declaration
|
|
509
|
+
* order, a declaration's default before its implied value. The host is captured by then, so each
|
|
510
|
+
* validator reads the same Host its action will, under the `default` phase. `places` says where each
|
|
511
|
+
* declaration sits, which a rejected value's finding rebuilds. An implied value is judged whether or
|
|
512
|
+
* not an invocation holds a bare spelling, because the author declared it.
|
|
438
513
|
*/
|
|
439
|
-
export async function
|
|
440
|
-
const declarations = scoped(inputs);
|
|
514
|
+
export async function prepareDeclaredValues(inputs, host, places) {
|
|
441
515
|
const defaults = new Map();
|
|
442
|
-
|
|
516
|
+
const implied = new Map();
|
|
517
|
+
for (const entry of scoped(inputs)) {
|
|
443
518
|
const { input } = entry;
|
|
444
|
-
const
|
|
445
|
-
const place = places.get(input);
|
|
446
|
-
const result = await validateDeclared(input, input.config.default, {
|
|
519
|
+
const call = {
|
|
447
520
|
context: () => ({ host, input: identityOf(entry), phase: 'default' }),
|
|
448
|
-
|
|
449
|
-
}
|
|
450
|
-
if (
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
});
|
|
521
|
+
site: places.get(input),
|
|
522
|
+
};
|
|
523
|
+
if (hasDefault(input)) {
|
|
524
|
+
const result = await validateDeclared(input, input.config.default, call);
|
|
525
|
+
defaults.set(input, declaredOutput(input, result, { fault: 'default', site: call.site }));
|
|
526
|
+
}
|
|
527
|
+
const value = impliedOf(input);
|
|
528
|
+
if (value !== undefined) {
|
|
529
|
+
// A bare spelling supplies one value, so even a multiple option's validator reads it alone.
|
|
530
|
+
const result = await validate(input, value, { context: call.context(), site: call.site });
|
|
531
|
+
implied.set(input, declaredOutput(input, result, { fault: 'implied', site: call.site }));
|
|
460
532
|
}
|
|
461
|
-
defaults.set(input, result.value);
|
|
462
533
|
}
|
|
463
|
-
return defaults;
|
|
534
|
+
return { defaults, implied };
|
|
535
|
+
}
|
|
536
|
+
/** How each declared value names itself when its validator rejects it, and the fix. */
|
|
537
|
+
const declaredFaults = {
|
|
538
|
+
default: {
|
|
539
|
+
correction: 'Fix the default or its validator.',
|
|
540
|
+
noun: 'default',
|
|
541
|
+
rule: invalidDefault,
|
|
542
|
+
},
|
|
543
|
+
implied: {
|
|
544
|
+
correction: 'Fix the implied value or its validator.',
|
|
545
|
+
noun: 'implied value',
|
|
546
|
+
rule: invalidImplied,
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
/**
|
|
550
|
+
* The output of a declared value its validator accepted, or the declaration fault it is when the
|
|
551
|
+
* validator rejected it, one line per issue under the sentence, marking the key that declares it.
|
|
552
|
+
*/
|
|
553
|
+
function declaredOutput(input, result, declared) {
|
|
554
|
+
if (result.issues === undefined) {
|
|
555
|
+
return result.value;
|
|
556
|
+
}
|
|
557
|
+
const { fault, site } = declared;
|
|
558
|
+
const { correction, noun, rule } = declaredFaults[fault];
|
|
559
|
+
const subject = declarationSubject(input);
|
|
560
|
+
throw new DeclarationError(rule, {
|
|
561
|
+
correction,
|
|
562
|
+
findings: site === undefined ? [] : [siteFinding(site, partOf(site, fault))],
|
|
563
|
+
sentence: [
|
|
564
|
+
`${subject} has an invalid ${noun}.`,
|
|
565
|
+
...messages(subject, reported(result.issues)),
|
|
566
|
+
].join('\n'),
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
/** The implied value a string option declares, which a bare spelling supplies, or `undefined`. */
|
|
570
|
+
function impliedOf(input) {
|
|
571
|
+
return input.kind === 'option' && input.config.type === 'string'
|
|
572
|
+
? input.config.implied
|
|
573
|
+
: undefined;
|
|
464
574
|
}
|
|
465
575
|
/**
|
|
466
|
-
* An array default reaches the action as its own copy, so an action that
|
|
467
|
-
* rewrites neither the declaration nor the next invocation. One prepared
|
|
468
|
-
* invocation of a run, and an unvalidated default is the
|
|
469
|
-
* copies it.
|
|
576
|
+
* An array default or implied output reaches the action as its own mutable copy, so an action that
|
|
577
|
+
* mutates its array rewrites neither the declaration nor the next invocation. One prepared declared
|
|
578
|
+
* value serves every invocation of a run, and an unvalidated default is the frozen snapshot the
|
|
579
|
+
* declaring call took, so each read copies it. The copy keeps a hole where the value has one, as
|
|
580
|
+
* the graph's does. Every other output passes through unchanged.
|
|
470
581
|
*/
|
|
471
|
-
function
|
|
472
|
-
|
|
582
|
+
function freshDeclaredValue(value) {
|
|
583
|
+
// A spread reads a hole as `undefined`, and `slice` keeps it.
|
|
584
|
+
// oxlint-disable-next-line unicorn/prefer-spread
|
|
585
|
+
return Array.isArray(value) ? value.slice() : value;
|
|
473
586
|
}
|
|
474
587
|
/**
|
|
475
588
|
* The raw tokens of one invocation, keyed by declared name. Every declared input of the routed
|
|
@@ -487,6 +600,9 @@ function suppliedInputs(declarations, supplied) {
|
|
|
487
600
|
else if (input.config.type === 'boolean') {
|
|
488
601
|
options[input.name] = supplied.options.booleans.get(input.name);
|
|
489
602
|
}
|
|
603
|
+
else if (input.config.type === 'count') {
|
|
604
|
+
options[input.name] = supplied.options.counts.get(input.name);
|
|
605
|
+
}
|
|
490
606
|
else {
|
|
491
607
|
options[input.name] =
|
|
492
608
|
copied(suppliedOption(supplied.options, input.name, collected)) ??
|
|
@@ -495,22 +611,29 @@ function suppliedInputs(declarations, supplied) {
|
|
|
495
611
|
}
|
|
496
612
|
return { args, options };
|
|
497
613
|
}
|
|
498
|
-
/** The Boolean grammar's one issue, which a variable outside it reports. */
|
|
499
|
-
const grammarIssues = [{ message: 'Use true, false, 1, or 0.' }];
|
|
500
614
|
/**
|
|
501
|
-
*
|
|
502
|
-
*
|
|
615
|
+
* The one issue a variable outside its option's grammar reports: the Boolean grammar, or the count
|
|
616
|
+
* grammar of ASCII decimal digits.
|
|
503
617
|
*/
|
|
504
|
-
function
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
.filter((input) => invocation.sources.rejected.has(input.name))
|
|
509
|
-
.map((input) => ({ global: true, input }));
|
|
510
|
-
return [...globals, ...rejected, ...declarations.filter((entry) => !entry.global)];
|
|
618
|
+
function grammarIssues(input) {
|
|
619
|
+
return input.config.type === 'count'
|
|
620
|
+
? [{ message: 'Use a whole number of 0 or more.' }]
|
|
621
|
+
: [{ message: 'Use true, false, 1, or 0.' }];
|
|
511
622
|
}
|
|
623
|
+
/**
|
|
624
|
+
* Whether a pass rejected a global option, which leaves no global value a middleware may read. A
|
|
625
|
+
* global option declares no presence rule, so every problem it reports is a rejected value.
|
|
626
|
+
*/
|
|
627
|
+
export function rejectsGlobal(validation) {
|
|
628
|
+
return validation.failure?.problems.some((problem) => problem.input.global) ?? false;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Validates one invocation in reporting order: the global options, the application's and then each
|
|
632
|
+
* plugin's, and then the routed Command's own declarations, each in authoring order.
|
|
633
|
+
*/
|
|
512
634
|
export async function validateValues(invocation) {
|
|
513
|
-
const {
|
|
635
|
+
const { declaredValues, prior, sources, supplied } = invocation;
|
|
636
|
+
const { defaults } = declaredValues;
|
|
514
637
|
const declarations = scoped(invocation.inputs);
|
|
515
638
|
/** Where a filled option's value came from, which its diagnostic names; argv names none. */
|
|
516
639
|
const originOf = (input) => input.kind === 'option' ? sources.labels.get(input.name) : undefined;
|
|
@@ -543,24 +666,40 @@ export async function validateValues(invocation) {
|
|
|
543
666
|
};
|
|
544
667
|
};
|
|
545
668
|
const values = new Map();
|
|
546
|
-
|
|
547
|
-
const
|
|
669
|
+
// Each input reports at most once, so insertion order is the order this phase reaches them.
|
|
670
|
+
const reports = new Map();
|
|
671
|
+
/**
|
|
672
|
+
* One missing input, in the order this phase reaches it, unless a skipped configuration source
|
|
673
|
+
* would have been asked to fill it.
|
|
674
|
+
*/
|
|
675
|
+
const omit = (entry, spelling, line) => {
|
|
676
|
+
if (sources.unanswered?.has(entry.input) === true) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
reports.set(entry.input, {
|
|
680
|
+
lines: [line],
|
|
681
|
+
problem: { input: identityOf(entry), reason: 'missing', spelling },
|
|
682
|
+
});
|
|
683
|
+
};
|
|
548
684
|
/** One rejected input, whatever rejected it, in the order this phase reaches it. */
|
|
549
685
|
const reject = (entry, issues, subject) => {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
686
|
+
reports.set(entry.input, {
|
|
687
|
+
lines: messages(subject, issues),
|
|
688
|
+
problem: {
|
|
689
|
+
input: identityOf(entry),
|
|
690
|
+
issues,
|
|
691
|
+
reason: 'invalid',
|
|
692
|
+
spelling: spellingOf(entry.input, invocation.table),
|
|
693
|
+
},
|
|
555
694
|
});
|
|
556
|
-
lines.push(...messages(subject, issues));
|
|
557
695
|
};
|
|
558
696
|
/** One path for every value a validator reads, so a raw shape and its issues meet it once. */
|
|
559
697
|
const accept = async (entry, raw, spelling) => {
|
|
560
698
|
const result = await validateDeclared(entry.input, raw, {
|
|
561
699
|
context: () => contextOf(entry),
|
|
562
|
-
|
|
700
|
+
implied: impliedPositions(entry.input, { declaredValues, options: supplied.options }),
|
|
563
701
|
signal: invocation.signal,
|
|
702
|
+
site: invocation.places.get(entry.input),
|
|
564
703
|
});
|
|
565
704
|
if (result.issues === undefined) {
|
|
566
705
|
values.set(entry.input, result.value);
|
|
@@ -568,7 +707,9 @@ export async function validateValues(invocation) {
|
|
|
568
707
|
}
|
|
569
708
|
reject(entry, reported(result.issues), suppliedName(entry.input, spelling, originOf(entry.input)));
|
|
570
709
|
};
|
|
571
|
-
|
|
710
|
+
const { only } = invocation;
|
|
711
|
+
const validated = only ? declarations.filter(({ input }) => only.has(input)) : declarations;
|
|
712
|
+
for (const entry of validated) {
|
|
572
713
|
if (invocation.signal.aborted) {
|
|
573
714
|
/**
|
|
574
715
|
* A cancelled run starts no further validator call. The one already in flight was awaited
|
|
@@ -579,16 +720,29 @@ export async function validateValues(invocation) {
|
|
|
579
720
|
}
|
|
580
721
|
const { input } = entry;
|
|
581
722
|
const variable = sources.rejected.get(input.name);
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
723
|
+
const earlier = prior?.reports.get(input);
|
|
724
|
+
if (prior?.values.has(input) === true) {
|
|
725
|
+
// An earlier pass validated this value once, and one value meets its validator once.
|
|
726
|
+
values.set(input, prior.values.read(input));
|
|
727
|
+
}
|
|
728
|
+
else if (earlier !== undefined) {
|
|
729
|
+
// The earlier pass's problem reports here, in this pass's order.
|
|
730
|
+
reports.set(input, earlier);
|
|
731
|
+
}
|
|
732
|
+
else if (input.kind === 'option' && variable !== undefined) {
|
|
733
|
+
// A variable outside its option's grammar filled nothing, so it is the option's problem.
|
|
734
|
+
reject(entry, grammarIssues(input), suppliedName(input, spellingOf(input, invocation.table), variable));
|
|
585
735
|
}
|
|
586
736
|
else if (input.kind === 'option' && input.config.type === 'boolean') {
|
|
587
737
|
values.set(input, booleanValue(supplied.options, input.name, input.config));
|
|
588
738
|
}
|
|
739
|
+
else if (input.kind === 'option' && input.config.type === 'count') {
|
|
740
|
+
// A count passes no validator, and no occurrence and no source reads 0.
|
|
741
|
+
values.set(input, supplied.options.counts.get(input.name) ?? 0);
|
|
742
|
+
}
|
|
589
743
|
else {
|
|
590
744
|
const collected = collects(input);
|
|
591
|
-
const spelling = spellingOf(input);
|
|
745
|
+
const spelling = spellingOf(input, invocation.table);
|
|
592
746
|
const raw = input.kind === 'argument'
|
|
593
747
|
? supplied.args.get(input)
|
|
594
748
|
: suppliedOption(supplied.options, input.name, collected);
|
|
@@ -596,34 +750,37 @@ export async function validateValues(invocation) {
|
|
|
596
750
|
if (input.config.required) {
|
|
597
751
|
// An omitted required argument arrives here too, so omission has one class.
|
|
598
752
|
// One aggregated diagnostic covers an omitted argument and an omitted option alike.
|
|
599
|
-
|
|
600
|
-
lines.push(missingMessage(input, spelling, { collected }));
|
|
753
|
+
omit(entry, spelling, missingMessage(input, spelling, { collected }));
|
|
601
754
|
}
|
|
602
755
|
else if (collected && !defaults.has(input)) {
|
|
603
756
|
// No occurrence has no value to validate, so the action receives an empty array.
|
|
604
757
|
values.set(input, []);
|
|
605
758
|
}
|
|
606
|
-
else if (validatesOmission(input)) {
|
|
759
|
+
else if (validatesOmission(input) && sources.unanswered?.has(input) !== true) {
|
|
607
760
|
// The flag sends the omission itself to the validator.
|
|
761
|
+
// An option a skipped configuration source would have filled is not judged absent.
|
|
608
762
|
// An absence rule reads the context a supplied value reads, and reports input issues.
|
|
609
763
|
await accept(entry, undefined, spelling);
|
|
610
764
|
}
|
|
611
765
|
else {
|
|
612
|
-
values.set(input,
|
|
766
|
+
values.set(input, freshDeclaredValue(defaults.get(input)));
|
|
613
767
|
}
|
|
614
768
|
}
|
|
615
769
|
else if (input.config.required && Array.isArray(raw) && raw.length === 0) {
|
|
616
770
|
// A filled list satisfies the at-least-one rule by its length, so an empty one is missing.
|
|
617
|
-
|
|
618
|
-
lines.push(missingMessage(input, spelling, { collected, origin: originOf(input) }));
|
|
771
|
+
omit(entry, spelling, missingMessage(input, spelling, { collected, origin: originOf(input) }));
|
|
619
772
|
}
|
|
620
773
|
else {
|
|
621
774
|
await accept(entry, raw, spelling);
|
|
622
775
|
}
|
|
623
776
|
}
|
|
624
777
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
778
|
+
const found = [...reports.values()];
|
|
779
|
+
return {
|
|
780
|
+
failure: found.length > 0
|
|
781
|
+
? new InputError(found.flatMap((report) => report.lines).join('\n'), found.map((report) => report.problem))
|
|
782
|
+
: undefined,
|
|
783
|
+
reports,
|
|
784
|
+
values: new ValidatedInputs(values),
|
|
785
|
+
};
|
|
629
786
|
}
|