@loomcli/core 0.5.0 → 0.6.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/dist/application.d.ts +18 -2
- package/dist/application.js +266 -71
- package/dist/bindings.d.ts +15 -10
- package/dist/bindings.js +34 -15
- package/dist/chain.d.ts +15 -6
- package/dist/chain.js +40 -20
- package/dist/command-rules.d.ts +55 -0
- package/dist/command-rules.js +142 -0
- package/dist/command.d.ts +65 -23
- package/dist/command.js +695 -226
- package/dist/controls.d.ts +8 -0
- package/dist/controls.js +23 -0
- package/dist/defect.d.ts +18 -0
- package/dist/defect.js +272 -0
- package/dist/developer.d.ts +24 -0
- package/dist/developer.js +52 -0
- package/dist/diagnostic-text.d.ts +81 -0
- package/dist/diagnostic-text.js +283 -0
- package/dist/diagnostic.d.ts +11 -0
- package/dist/diagnostic.js +70 -0
- package/dist/errors.d.ts +108 -24
- package/dist/errors.js +324 -53
- package/dist/exit-codes.d.ts +45 -0
- package/dist/exit-codes.js +46 -0
- package/dist/extension.d.ts +41 -8
- package/dist/extension.js +142 -57
- package/dist/facts.d.ts +66 -11
- package/dist/facts.js +98 -22
- package/dist/globals.d.ts +29 -21
- package/dist/globals.js +115 -46
- package/dist/hints.d.ts +79 -0
- package/dist/hints.js +247 -0
- package/dist/host.d.ts +13 -0
- package/dist/host.js +43 -1
- package/dist/identity.d.ts +19 -0
- package/dist/identity.js +72 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.js +5 -0
- package/dist/input-rules.d.ts +64 -0
- package/dist/input-rules.js +145 -0
- package/dist/inspect.d.ts +12 -4
- package/dist/inspect.js +88 -28
- package/dist/lanes.js +1 -1
- package/dist/locate.js +4 -4
- package/dist/options.d.ts +23 -2
- package/dist/options.js +135 -44
- package/dist/output.d.ts +9 -2
- package/dist/output.js +18 -2
- package/dist/plain.d.ts +6 -0
- package/dist/plain.js +12 -0
- package/dist/plugin-rules.d.ts +62 -0
- package/dist/plugin-rules.js +155 -0
- package/dist/plugin.d.ts +22 -10
- package/dist/plugin.js +348 -116
- package/dist/prototypes.d.ts +7 -0
- package/dist/prototypes.js +29 -0
- package/dist/rendering.d.ts +6 -1
- package/dist/rendering.js +23 -5
- package/dist/rules.d.ts +51 -0
- package/dist/rules.js +115 -0
- package/dist/sequence.js +6 -1
- package/dist/sources.d.ts +6 -4
- package/dist/sources.js +22 -13
- package/dist/style-wire.js +1 -1
- package/dist/style.js +1 -1
- package/dist/theme.d.ts +4 -0
- package/dist/theme.js +25 -5
- package/dist/thenable.d.ts +15 -0
- package/dist/thenable.js +29 -0
- package/dist/translators.d.ts +69 -0
- package/dist/translators.js +253 -0
- package/dist/types.d.ts +11 -1
- package/dist/validation.d.ts +44 -3
- package/dist/validation.js +156 -57
- package/dist/view.d.ts +48 -14
- package/dist/view.js +155 -77
- package/package.json +1 -1
package/dist/command.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
var _a, _b;
|
|
2
2
|
import { checkEnvBinding, checkNoArgumentBinding } from './bindings.js';
|
|
3
|
-
import {
|
|
3
|
+
import { aliasWithoutNames, argumentDeclaredTwice, argumentsBesideChildren, commandAttachedTwice, commandGlobals, commandWithoutAction, declaredAfterAction, declaredName, groupOption, multipleActions, multipleResults, nestingDepth, notACommand, optionalArgumentLast, portableName, repeatedAlias, resultWithoutAction, resultWithoutViews, rowViewOnValue, siblingNameTaken, unknownDefaultView, variadicArgumentLast, viewName, viewShape, viewsWithoutResult, } from './command-rules.js';
|
|
4
|
+
import { quoteString, spelled } from './diagnostic-text.js';
|
|
5
|
+
import { asSentence, commandSentence, commandSubject, DeclarationError, NonCallableCommandError, quoted, ResultError, toFailure, UnexpectedArgumentError, UnknownCommandError, reasonOf, } from './errors.js';
|
|
4
6
|
import { buildExtensions, extendStore, publishStore, registerDescriptor, storeCommandLayers, validateLayer, } from './extension.js';
|
|
5
|
-
import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts,
|
|
7
|
+
import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts, siteFinding, } from './facts.js';
|
|
6
8
|
import { buildGlobals, checkLocalOptions } from './globals.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
+
import { nameSharedAcrossKinds, optionDeclaredTwice, spellingTaken } from './input-rules.js';
|
|
10
|
+
import { graphMismatch, nodeAt, resultNode, snapshot } from './inspect.js';
|
|
11
|
+
import { checkOptionName, compileOptions, copyValues, emptyValues, extractGlobals, isOptionToken, mergeValues, parseInputs, spellingMark, } from './options.js';
|
|
12
|
+
import { isPlainObject } from './plain.js';
|
|
13
|
+
import { brokenAttachHook, notAnObject } from './plugin-rules.js';
|
|
9
14
|
import { fillInputs } from './sources.js';
|
|
10
|
-
import { captureConfig, checkDeclarations, validateValues } from './validation.js';
|
|
15
|
+
import { captureConfig, checkInputConfig, checkDeclarations, declaringSite, inputPlace, validateValues, } from './validation.js';
|
|
11
16
|
/**
|
|
12
17
|
* The deepest level below the root a Command may sit at, and the word its diagnostic spells it with.
|
|
13
18
|
* A child of the root sits at level 1. Raising the cap relaxes a rule and breaks no application.
|
|
@@ -41,13 +46,48 @@ function nodeHandle(name, state) {
|
|
|
41
46
|
export function commandNode(value) {
|
|
42
47
|
return typeof value === 'object' && value !== null ? nodes.get(value) : undefined;
|
|
43
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* The path a finding opens with for a declaration call on one Command. The root's is empty. A named
|
|
51
|
+
* Command's call cannot know the parent it will join, so its path is its own name.
|
|
52
|
+
*/
|
|
53
|
+
function pathOf(name) {
|
|
54
|
+
return name === null ? [] : [name];
|
|
55
|
+
}
|
|
56
|
+
/** A call's arguments as its author wrote them: the trailing ones left undefined are dropped. */
|
|
57
|
+
export function callArguments(...values) {
|
|
58
|
+
let length = values.length;
|
|
59
|
+
while (length > 0 && values[length - 1] === undefined) {
|
|
60
|
+
length -= 1;
|
|
61
|
+
}
|
|
62
|
+
return values.slice(0, length);
|
|
63
|
+
}
|
|
64
|
+
/** A Command value as a finding prints it, which it would otherwise print as an ellipsis. */
|
|
65
|
+
export function commandCode(name) {
|
|
66
|
+
return spelled(`new Command(${quoteString(name)})`);
|
|
67
|
+
}
|
|
68
|
+
/** The finding for one `command()` call that attached a child under the Command at `path`. */
|
|
69
|
+
export function commandPlacement(path, child) {
|
|
70
|
+
return { arguments: [commandCode(child)], call: 'command', mark: '0', path };
|
|
71
|
+
}
|
|
72
|
+
/** The finding for one `new Command()` call, which carries the name and the options slot. */
|
|
73
|
+
function constructorFinding(name, options, mark) {
|
|
74
|
+
return { arguments: callArguments(name, options), call: 'new Command', mark };
|
|
75
|
+
}
|
|
44
76
|
/** A named Command's options slot holds a plain options object, and never retired globals wiring. */
|
|
45
77
|
function checkCommandOptions(name, options) {
|
|
46
78
|
if (options !== undefined && !isPlainObject(options)) {
|
|
47
|
-
throw new DeclarationError(
|
|
79
|
+
throw new DeclarationError(notAnObject, {
|
|
80
|
+
correction: 'Supply a Command options object.',
|
|
81
|
+
findings: [constructorFinding(name, options, '1')],
|
|
82
|
+
sentence: `${commandSentence(name)} declares options that are not an object.`,
|
|
83
|
+
});
|
|
48
84
|
}
|
|
49
85
|
if (isPlainObject(options) && 'globals' in options) {
|
|
50
|
-
throw new DeclarationError(
|
|
86
|
+
throw new DeclarationError(commandGlobals, {
|
|
87
|
+
correction: 'Declare globals on the Application and register its environment.',
|
|
88
|
+
findings: [constructorFinding(name, options, '1.globals')],
|
|
89
|
+
sentence: `${commandSentence(name)} declares globals.`,
|
|
90
|
+
});
|
|
51
91
|
}
|
|
52
92
|
}
|
|
53
93
|
/** One name rule for an argument, option, or view name: a bare token the parser can read. */
|
|
@@ -66,11 +106,25 @@ export function isPortableName(name) {
|
|
|
66
106
|
/** The one correction every portable name diagnostic ends with. */
|
|
67
107
|
export const portableNameCorrection = 'Use a nonempty name of A-Z, a-z, 0-9, ".", "_", and "-" that does not start with "-" or ".".';
|
|
68
108
|
/** An alias is typed at the prompt the way a Command name is, so it answers to the portable rule. */
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
|
|
109
|
+
function checkAliasNames(command, names) {
|
|
110
|
+
for (const [index, alias] of names.entries()) {
|
|
111
|
+
if (!isPortableName(alias)) {
|
|
112
|
+
throw new DeclarationError(portableName, {
|
|
113
|
+
correction: portableNameCorrection,
|
|
114
|
+
findings: [{ arguments: names, call: 'alias', mark: String(index), path: pathOf(command) }],
|
|
115
|
+
sentence: `${commandSentence(command)} declares an alias named ${quoted(alias)}.`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
72
118
|
}
|
|
73
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* The finding for the `alias()` call that declared one alias on the Command at `path`, rebuilt from
|
|
122
|
+
* every alias the Command holds.
|
|
123
|
+
*/
|
|
124
|
+
function aliasFinding(command, alias, note) {
|
|
125
|
+
const { aliases, path } = command;
|
|
126
|
+
return { arguments: aliases, call: 'alias', mark: String(aliases.indexOf(alias)), note, path };
|
|
127
|
+
}
|
|
74
128
|
/** How the extension diagnostics of one Command's own layers name it. */
|
|
75
129
|
export function layerOf(name) {
|
|
76
130
|
return { phrase: `on ${commandSubject(name)}`, sentence: commandSentence(name) };
|
|
@@ -101,19 +155,28 @@ export function freshState(declaration) {
|
|
|
101
155
|
*/
|
|
102
156
|
function namedState(name, options) {
|
|
103
157
|
if (!isPortableName(name)) {
|
|
104
|
-
throw new DeclarationError(
|
|
158
|
+
throw new DeclarationError(portableName, {
|
|
159
|
+
correction: portableNameCorrection,
|
|
160
|
+
findings: [constructorFinding(name, options, '0')],
|
|
161
|
+
sentence: `Command name ${quoted(name)} is invalid.`,
|
|
162
|
+
});
|
|
105
163
|
}
|
|
106
164
|
checkCommandOptions(name, options);
|
|
107
165
|
const slot = isPlainObject(options) ? options : undefined;
|
|
108
|
-
const
|
|
166
|
+
const site = {
|
|
167
|
+
at: '1',
|
|
168
|
+
declaration: { arguments: [name, options], call: 'new Command' },
|
|
169
|
+
subject: commandSentence(name),
|
|
170
|
+
};
|
|
109
171
|
// The facts are read in the order their diagnostics have always ranked.
|
|
110
|
-
const description = checkDescription(
|
|
111
|
-
const hidden = checkHidden(
|
|
112
|
-
const deprecated = checkDeprecated(
|
|
172
|
+
const description = checkDescription(site, slot?.description);
|
|
173
|
+
const hidden = checkHidden(site, slot?.hidden);
|
|
174
|
+
const deprecated = checkDeprecated(site, slot?.deprecated);
|
|
113
175
|
const descriptors = new Map();
|
|
114
176
|
const extensions = storeCommandLayers({
|
|
115
177
|
descriptors,
|
|
116
178
|
layers: [slot?.extensions],
|
|
179
|
+
site: { ...site, at: '1.extensions' },
|
|
117
180
|
subject: layerOf(name),
|
|
118
181
|
});
|
|
119
182
|
return {
|
|
@@ -130,16 +193,48 @@ function namedState(name, options) {
|
|
|
130
193
|
* A declaration call after `action()` is an order fault the receiver's own earlier call makes
|
|
131
194
|
* certain. The types remove the call for a TypeScript author; a JavaScript author meets it here.
|
|
132
195
|
*/
|
|
133
|
-
function checkOpen(state,
|
|
196
|
+
function checkOpen(state, late) {
|
|
134
197
|
if (state.action !== undefined) {
|
|
135
|
-
throw new DeclarationError(
|
|
198
|
+
throw new DeclarationError(declaredAfterAction, {
|
|
199
|
+
correction: late.remedy,
|
|
200
|
+
findings: [
|
|
201
|
+
{
|
|
202
|
+
arguments: late.arguments,
|
|
203
|
+
call: late.call,
|
|
204
|
+
mark: '0',
|
|
205
|
+
note: 'after action()',
|
|
206
|
+
path: pathOf(state.name),
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
sentence: `${commandSentence(state.name)} ${late.clause} after its action.`,
|
|
210
|
+
});
|
|
136
211
|
}
|
|
137
212
|
}
|
|
138
213
|
/** The remedy a late argument or option earns. */
|
|
139
214
|
const inputRemedy = 'Declare arguments and options before action().';
|
|
215
|
+
/** Where one input was declared: the call on the Command at `path` that declared it. */
|
|
216
|
+
function inputSite(name, path, input) {
|
|
217
|
+
return declaringSite(input, inputPlace(input, { global: false, path }), `${commandSentence(name)} ${input.kind} ${quoted(input.name)}`);
|
|
218
|
+
}
|
|
219
|
+
/** The finding for the `argument()` or `option()` call that declared one input, marking its name. */
|
|
220
|
+
function inputFinding(path, input, note) {
|
|
221
|
+
const site = declaringSite(input, inputPlace(input, { global: false, path }));
|
|
222
|
+
return siteFinding(site, site.named, note);
|
|
223
|
+
}
|
|
224
|
+
/** The scope one Command's own options compile under: its subject, and each option's call. */
|
|
225
|
+
function localScope(name, path) {
|
|
226
|
+
return { siteOf: (input) => inputSite(name, path, input), subject: commandSubject(name) };
|
|
227
|
+
}
|
|
140
228
|
/** One Command declares arguments or attaches children, whichever call came second. */
|
|
141
|
-
function placementFault(
|
|
142
|
-
return new DeclarationError(
|
|
229
|
+
function placementFault(parent, argument, child) {
|
|
230
|
+
return new DeclarationError(argumentsBesideChildren, {
|
|
231
|
+
correction: 'Move the argument into a child Command or remove the children.',
|
|
232
|
+
findings: [
|
|
233
|
+
inputFinding(parent.path, argument, 'the argument'),
|
|
234
|
+
{ ...child.placement, note: 'the child' },
|
|
235
|
+
],
|
|
236
|
+
sentence: `${commandSentence(parent.name)} declares argument "${argument.name}" and attaches child "${child.name}".`,
|
|
237
|
+
});
|
|
143
238
|
}
|
|
144
239
|
/** The options one declaration list holds, in declaration order. */
|
|
145
240
|
function optionsOf(inputs) {
|
|
@@ -163,6 +258,7 @@ function recordInput(state, input) {
|
|
|
163
258
|
const record = buildExtensions({
|
|
164
259
|
declared: input.config.extensions,
|
|
165
260
|
descriptors,
|
|
261
|
+
site: { ...inputSite(name, pathOf(name), input), at: '1.extensions' },
|
|
166
262
|
subject: {
|
|
167
263
|
phrase: `on ${commandSubject(name)} ${input.kind} "${input.name}"`,
|
|
168
264
|
sentence: `${commandSentence(name)} ${input.kind} "${input.name}"`,
|
|
@@ -177,31 +273,61 @@ function recordInput(state, input) {
|
|
|
177
273
|
*/
|
|
178
274
|
function checkArgument(state, input) {
|
|
179
275
|
const { name } = state;
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
throw new DeclarationError(`${commandSentence(name)} declares an argument named "${String(input.name)}". Use a nonempty name without a leading hyphen, whitespace, or "=".`);
|
|
183
|
-
}
|
|
276
|
+
const path = pathOf(name);
|
|
277
|
+
const command = { name, path, subject: commandSubject(name) };
|
|
184
278
|
const declared = state.inputs.filter((entry) => entry.kind === 'argument');
|
|
185
|
-
|
|
186
|
-
throw new DeclarationError(`Argument "${input.name}" is declared more than once on ${subject}. Remove or rename the duplicate.`);
|
|
187
|
-
}
|
|
279
|
+
checkArgumentName(command, declared, input);
|
|
188
280
|
const child = state.children[0];
|
|
189
281
|
if (child) {
|
|
190
|
-
throw placementFault(
|
|
282
|
+
throw placementFault(command, input, child);
|
|
191
283
|
}
|
|
192
284
|
const previous = declared.at(-1);
|
|
193
285
|
if (previous) {
|
|
194
|
-
checkSlotOrder(slotOf(previous), slotOf(input),
|
|
286
|
+
checkSlotOrder(slotOf(previous), slotOf(input), command);
|
|
287
|
+
}
|
|
288
|
+
const site = inputSite(name, path, input);
|
|
289
|
+
checkDescription(site, input.config.description);
|
|
290
|
+
checkNoListingFacts(site, input.config);
|
|
291
|
+
checkNoArgumentBinding(site, input.config);
|
|
292
|
+
checkDeclarations([{ input, site }]);
|
|
293
|
+
}
|
|
294
|
+
/** One argument's name answers the declared-name rule and names no argument declared before it. */
|
|
295
|
+
function checkArgumentName(command, declared, input) {
|
|
296
|
+
const { path } = command;
|
|
297
|
+
if (!isDeclaredName(input.name)) {
|
|
298
|
+
throw new DeclarationError(declaredName, {
|
|
299
|
+
correction: 'Use a nonempty name without a leading hyphen, whitespace, or "=".',
|
|
300
|
+
findings: [inputFinding(path, input)],
|
|
301
|
+
sentence: `${commandSentence(command.name)} declares an argument named ${quoted(input.name)}.`,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
const first = declared.find((entry) => entry.name === input.name);
|
|
305
|
+
if (first) {
|
|
306
|
+
throw new DeclarationError(argumentDeclaredTwice, {
|
|
307
|
+
correction: 'Remove or rename the duplicate.',
|
|
308
|
+
findings: [
|
|
309
|
+
inputFinding(path, first, 'the first declaration'),
|
|
310
|
+
inputFinding(path, input, 'the second declaration'),
|
|
311
|
+
],
|
|
312
|
+
sentence: `Argument "${input.name}" is declared more than once on ${command.subject}.`,
|
|
313
|
+
});
|
|
195
314
|
}
|
|
196
|
-
const sentence = `${commandSentence(name)} argument "${input.name}"`;
|
|
197
|
-
checkDescription(sentence, input.config.description);
|
|
198
|
-
checkNoListingFacts(sentence, input.config);
|
|
199
|
-
checkNoArgumentBinding(sentence, input.config);
|
|
200
|
-
checkDeclarations([input]);
|
|
201
315
|
}
|
|
202
316
|
/** The declared value joins `args` under its literal name, typed by its own config. */
|
|
203
|
-
export function declareArgument(state,
|
|
204
|
-
|
|
317
|
+
export function declareArgument(state, declared) {
|
|
318
|
+
// The call's own input is judged before the receiver's state, as alias() judges its names.
|
|
319
|
+
// A name of another kind then reports as a declared name instead of failing to print in the order diagnostic.
|
|
320
|
+
// The config is judged right after its name, and only then captured.
|
|
321
|
+
const { name } = state;
|
|
322
|
+
checkArgumentName({ name, path: pathOf(name), subject: commandSubject(name) }, [], declared);
|
|
323
|
+
checkInputConfig(declared, { call: 'argument', path: pathOf(name) });
|
|
324
|
+
const input = { ...declared, config: captureConfig(declared.config) };
|
|
325
|
+
checkOpen(state, {
|
|
326
|
+
arguments: [input.name, input.config],
|
|
327
|
+
call: 'argument',
|
|
328
|
+
clause: `declares argument ${quoted(input.name)}`,
|
|
329
|
+
remedy: inputRemedy,
|
|
330
|
+
});
|
|
205
331
|
checkArgument(state, input);
|
|
206
332
|
const recorded = recordInput(state, input);
|
|
207
333
|
const previous = state.bind;
|
|
@@ -222,16 +348,26 @@ const noGlobals = { names: new Map(), options: new Map(), variables: new Map() }
|
|
|
222
348
|
* options also meet the Application's globals table, which a named Command meets when its subtree
|
|
223
349
|
* joins an Application.
|
|
224
350
|
*/
|
|
225
|
-
export function declareOption(state,
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
351
|
+
export function declareOption(state, declared, table = noGlobals) {
|
|
352
|
+
// The call's own input is judged before the receiver's state, as alias() judges its names.
|
|
353
|
+
// The config is judged right after its name, and only then captured.
|
|
354
|
+
checkOptionName(declared.name, inputSite(state.name, pathOf(state.name), declared));
|
|
355
|
+
checkInputConfig(declared, { call: 'option', path: pathOf(state.name) });
|
|
356
|
+
const input = { ...declared, config: captureConfig(declared.config) };
|
|
357
|
+
const site = inputSite(state.name, pathOf(state.name), input);
|
|
358
|
+
checkOpen(state, {
|
|
359
|
+
arguments: [input.name, input.config],
|
|
360
|
+
call: 'option',
|
|
361
|
+
clause: `declares option ${quoted(input.name)}`,
|
|
362
|
+
remedy: inputRemedy,
|
|
363
|
+
});
|
|
364
|
+
checkDescription(site, input.config.description);
|
|
365
|
+
checkHidden(site, input.config.hidden);
|
|
366
|
+
checkDeprecated(site, input.config.deprecated);
|
|
367
|
+
checkEnvBinding(site, input.config);
|
|
232
368
|
const recorded = recordInput(state, input);
|
|
233
|
-
checkLocalOptions([...optionsOf(state.inputs), input], table,
|
|
234
|
-
checkDeclarations([input]);
|
|
369
|
+
checkLocalOptions([...optionsOf(state.inputs), input], table, localScope(state.name, pathOf(state.name)));
|
|
370
|
+
checkDeclarations([{ input, site }]);
|
|
235
371
|
const previous = state.bind;
|
|
236
372
|
return {
|
|
237
373
|
...state,
|
|
@@ -249,23 +385,40 @@ export function declareOption(state, input, table = noGlobals) {
|
|
|
249
385
|
*/
|
|
250
386
|
export function declareAlias(state, names) {
|
|
251
387
|
const { name } = state;
|
|
388
|
+
const path = pathOf(name);
|
|
252
389
|
const first = names[0];
|
|
253
390
|
if (first === undefined) {
|
|
254
|
-
throw new DeclarationError(
|
|
391
|
+
throw new DeclarationError(aliasWithoutNames, {
|
|
392
|
+
correction: 'Supply at least one name.',
|
|
393
|
+
findings: [{ arguments: [], call: 'alias', path }],
|
|
394
|
+
sentence: `${commandSentence(name)} declares an alias with no names.`,
|
|
395
|
+
});
|
|
255
396
|
}
|
|
256
397
|
// The call's own input is judged before the receiver's state.
|
|
257
398
|
// A non-string name then reports as an alias name instead of failing to print in the order diagnostic.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
399
|
+
checkAliasNames(name, names);
|
|
400
|
+
checkOpen(state, {
|
|
401
|
+
arguments: names,
|
|
402
|
+
call: 'alias',
|
|
403
|
+
clause: `declares alias "${first}"`,
|
|
404
|
+
remedy: 'Declare aliases before action().',
|
|
405
|
+
});
|
|
262
406
|
const aliases = [...state.aliases];
|
|
263
|
-
for (const alias of names) {
|
|
407
|
+
for (const [index, alias] of names.entries()) {
|
|
408
|
+
const repeated = { arguments: names, call: 'alias', mark: String(index), path };
|
|
264
409
|
if (alias === name) {
|
|
265
|
-
throw new DeclarationError(
|
|
410
|
+
throw new DeclarationError(repeatedAlias, {
|
|
411
|
+
correction: 'Remove the alias.',
|
|
412
|
+
findings: [{ ...repeated, note: 'its own name' }],
|
|
413
|
+
sentence: `${commandSentence(name)} declares alias "${alias}", which is its own name.`,
|
|
414
|
+
});
|
|
266
415
|
}
|
|
267
416
|
if (aliases.includes(alias)) {
|
|
268
|
-
throw new DeclarationError(
|
|
417
|
+
throw new DeclarationError(repeatedAlias, {
|
|
418
|
+
correction: 'Remove the repeated alias.',
|
|
419
|
+
findings: [{ ...repeated, note: 'already an alias' }],
|
|
420
|
+
sentence: `${commandSentence(name)} declares alias "${alias}" twice.`,
|
|
421
|
+
});
|
|
269
422
|
}
|
|
270
423
|
aliases.push(alias);
|
|
271
424
|
}
|
|
@@ -280,6 +433,7 @@ export function declareExtensions(state, values) {
|
|
|
280
433
|
const layer = validateLayer({
|
|
281
434
|
declared: values,
|
|
282
435
|
descriptors,
|
|
436
|
+
site: { at: '', declaration: { arguments: values, call: 'extend', path: pathOf(state.name) } },
|
|
283
437
|
subject: layerOf(state.name),
|
|
284
438
|
target: 'command',
|
|
285
439
|
});
|
|
@@ -299,7 +453,19 @@ function declaredChannel(out) {
|
|
|
299
453
|
*/
|
|
300
454
|
export function declareAction(state, handler) {
|
|
301
455
|
if (state.action !== undefined) {
|
|
302
|
-
throw new DeclarationError(
|
|
456
|
+
throw new DeclarationError(multipleActions, {
|
|
457
|
+
correction: 'Register one action.',
|
|
458
|
+
findings: [
|
|
459
|
+
{
|
|
460
|
+
arguments: [handler],
|
|
461
|
+
call: 'action',
|
|
462
|
+
mark: '0',
|
|
463
|
+
note: 'the second action',
|
|
464
|
+
path: pathOf(state.name),
|
|
465
|
+
},
|
|
466
|
+
],
|
|
467
|
+
sentence: `${commandSentence(state.name)} has multiple actions.`,
|
|
468
|
+
});
|
|
303
469
|
}
|
|
304
470
|
// The graph and the routed node stay getters, because a spread would build the graph on every run.
|
|
305
471
|
const stored = (context) => handler({
|
|
@@ -324,33 +490,71 @@ export function declareAction(state, handler) {
|
|
|
324
490
|
* here; the empty record and the default wait for attach, because `views()` can still add keys.
|
|
325
491
|
*/
|
|
326
492
|
export function declareResult(state, kind, declaration) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
493
|
+
const call = {
|
|
494
|
+
arguments: callArguments(declaration),
|
|
495
|
+
kind,
|
|
496
|
+
views: recordOf(declaration, 'views'),
|
|
497
|
+
};
|
|
498
|
+
checkOpen(state, {
|
|
499
|
+
arguments: call.arguments,
|
|
500
|
+
call: resultCallName(call),
|
|
501
|
+
clause: 'declares its result',
|
|
502
|
+
remedy: 'Declare result() or rows() before action().',
|
|
503
|
+
});
|
|
504
|
+
const results = [...state.results, call];
|
|
505
|
+
mergeResult({ name: state.name, path: pathOf(state.name) }, results);
|
|
330
506
|
return { ...state, results };
|
|
331
507
|
}
|
|
332
508
|
/** A `views()` call reshapes views and closes nothing, so it is never a late declaration. */
|
|
333
509
|
export function declareResultViews(state, replacements, options) {
|
|
334
|
-
const results = [
|
|
335
|
-
|
|
336
|
-
{ default: recordOf(options, 'default'), kind: 'views', views: replacements },
|
|
337
|
-
];
|
|
338
|
-
mergeResult(state.name, results);
|
|
510
|
+
const results = [...state.results, viewsCall(replacements, options)];
|
|
511
|
+
mergeResult({ name: state.name, path: pathOf(state.name) }, results);
|
|
339
512
|
return { ...state, results };
|
|
340
513
|
}
|
|
514
|
+
/** One `views()` call as the results lane records it. */
|
|
515
|
+
function viewsCall(replacements, options) {
|
|
516
|
+
return {
|
|
517
|
+
arguments: callArguments(replacements, options),
|
|
518
|
+
default: recordOf(options, 'default'),
|
|
519
|
+
kind: 'views',
|
|
520
|
+
views: replacements,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
341
523
|
/** One property of an authoring argument, read defensively: the rules report whatever it holds. */
|
|
342
524
|
function recordOf(declaration, key) {
|
|
343
525
|
return isPlainObject(declaration) ? declaration[key] : undefined;
|
|
344
526
|
}
|
|
527
|
+
/** The authoring call one results-lane call was made through. */
|
|
528
|
+
function resultCallName(call) {
|
|
529
|
+
return call.kind === 'value' ? 'result' : call.kind;
|
|
530
|
+
}
|
|
531
|
+
/** The finding for one results-lane call on the Command at `path`, marking one of its arguments. */
|
|
532
|
+
function resultFinding(path, call, mark) {
|
|
533
|
+
const finding = { arguments: call.arguments, call: resultCallName(call), mark: mark.at, path };
|
|
534
|
+
return mark.note === undefined ? finding : { ...finding, note: mark.note };
|
|
535
|
+
}
|
|
536
|
+
/** Where one views entry sits among its call's arguments: in `views` for a declaration, or first. */
|
|
537
|
+
function entryMark(call, key) {
|
|
538
|
+
return call.kind === 'views' ? `0.${key}` : `0.views.${key}`;
|
|
539
|
+
}
|
|
345
540
|
/** The parent a declaration is, as attach reads it. */
|
|
346
541
|
function parentOf(state) {
|
|
347
542
|
return {
|
|
348
|
-
argument: state.inputs.find((input) => input.kind === 'argument')
|
|
543
|
+
argument: state.inputs.find((input) => input.kind === 'argument'),
|
|
349
544
|
children: state.children,
|
|
350
545
|
hasAction: state.action !== undefined,
|
|
351
546
|
name: state.name,
|
|
547
|
+
path: pathOf(state.name),
|
|
352
548
|
};
|
|
353
549
|
}
|
|
550
|
+
/** The path one child sits at under its parent. */
|
|
551
|
+
function childPath(parent, child) {
|
|
552
|
+
return [...parent.path, child];
|
|
553
|
+
}
|
|
554
|
+
/** The call that attached one child, noted with the child's name. */
|
|
555
|
+
function childFinding(entry) {
|
|
556
|
+
return { ...entry.placement, note: `child "${entry.name}"` };
|
|
557
|
+
}
|
|
354
558
|
/**
|
|
355
559
|
* One parent's namespace, which every canonical name and alias under it shares. The rule reads the
|
|
356
560
|
* same whichever sibling the author attached first.
|
|
@@ -358,21 +562,41 @@ function parentOf(state) {
|
|
|
358
562
|
function checkSiblings(parent, child) {
|
|
359
563
|
const sentence = commandSentence(parent.name);
|
|
360
564
|
const { children } = parent;
|
|
361
|
-
|
|
362
|
-
|
|
565
|
+
const aliased = (entry, alias) => aliasFinding({ aliases: entry.node.declared.aliases, path: childPath(parent, entry.name) }, alias, `alias of child "${entry.name}"`);
|
|
566
|
+
const correction = 'Rename or remove one.';
|
|
567
|
+
const twin = children.find((entry) => entry.name === child.name);
|
|
568
|
+
if (twin) {
|
|
569
|
+
throw new DeclarationError(siblingNameTaken, {
|
|
570
|
+
correction,
|
|
571
|
+
findings: [childFinding(twin), childFinding(child)],
|
|
572
|
+
sentence: `${sentence} attaches two children named "${child.name}".`,
|
|
573
|
+
});
|
|
363
574
|
}
|
|
364
575
|
for (const alias of child.node.declared.aliases) {
|
|
365
|
-
|
|
366
|
-
|
|
576
|
+
const holder = children.find((entry) => entry.name === alias);
|
|
577
|
+
if (holder) {
|
|
578
|
+
throw new DeclarationError(siblingNameTaken, {
|
|
579
|
+
correction,
|
|
580
|
+
findings: [childFinding(holder), aliased(child, alias)],
|
|
581
|
+
sentence: `${sentence} attaches child "${child.name}" with alias "${alias}", which is also the name of child "${alias}".`,
|
|
582
|
+
});
|
|
367
583
|
}
|
|
368
584
|
const owner = children.find((entry) => entry.node.declared.aliases.includes(alias));
|
|
369
585
|
if (owner) {
|
|
370
|
-
throw new DeclarationError(
|
|
586
|
+
throw new DeclarationError(siblingNameTaken, {
|
|
587
|
+
correction,
|
|
588
|
+
findings: [aliased(owner, alias), aliased(child, alias)],
|
|
589
|
+
sentence: `${sentence} attaches child "${child.name}" with alias "${alias}", which is also an alias of child "${owner.name}".`,
|
|
590
|
+
});
|
|
371
591
|
}
|
|
372
592
|
}
|
|
373
|
-
const
|
|
374
|
-
if (
|
|
375
|
-
throw new DeclarationError(
|
|
593
|
+
const owner = children.find((entry) => entry.node.declared.aliases.includes(child.name));
|
|
594
|
+
if (owner) {
|
|
595
|
+
throw new DeclarationError(siblingNameTaken, {
|
|
596
|
+
correction,
|
|
597
|
+
findings: [aliased(owner, child.name), childFinding(child)],
|
|
598
|
+
sentence: `${sentence} attaches child "${owner.name}" with alias "${child.name}", which is also the name of child "${child.name}".`,
|
|
599
|
+
});
|
|
376
600
|
}
|
|
377
601
|
}
|
|
378
602
|
/**
|
|
@@ -381,7 +605,11 @@ function checkSiblings(parent, child) {
|
|
|
381
605
|
*/
|
|
382
606
|
function checkNesting(parent, child, level) {
|
|
383
607
|
if (level >= nestingCap.depth && child.node.declared.children.length > 0) {
|
|
384
|
-
throw new DeclarationError(
|
|
608
|
+
throw new DeclarationError(nestingDepth, {
|
|
609
|
+
correction: `Nest Commands at most ${nestingCap.words} levels below the root.`,
|
|
610
|
+
findings: [{ ...child.placement, note: 'has children of its own' }],
|
|
611
|
+
sentence: `${commandSentence(parent)} attaches child "${child.name}", which has children of its own.`,
|
|
612
|
+
});
|
|
385
613
|
}
|
|
386
614
|
}
|
|
387
615
|
/**
|
|
@@ -389,14 +617,23 @@ function checkNesting(parent, child, level) {
|
|
|
389
617
|
* children. A group with no children receives an invocation no handler can answer, and a local
|
|
390
618
|
* option on a group reaches no handler either, because locals never inherit.
|
|
391
619
|
*/
|
|
392
|
-
function checkGroup(state) {
|
|
620
|
+
function checkGroup(state, place) {
|
|
393
621
|
const { name } = state;
|
|
394
622
|
if (state.children.length === 0) {
|
|
395
|
-
|
|
623
|
+
const { placement } = place;
|
|
624
|
+
throw new DeclarationError(commandWithoutAction, {
|
|
625
|
+
correction: 'Register an action.',
|
|
626
|
+
findings: placement === undefined ? [] : [{ ...placement, note: 'no action and no children' }],
|
|
627
|
+
sentence: `${commandSentence(name)} has no action.`,
|
|
628
|
+
});
|
|
396
629
|
}
|
|
397
630
|
const option = state.inputs.find((input) => input.kind === 'option');
|
|
398
631
|
if (option) {
|
|
399
|
-
throw new DeclarationError(
|
|
632
|
+
throw new DeclarationError(groupOption, {
|
|
633
|
+
correction: 'Register an action or remove the option.',
|
|
634
|
+
findings: [inputFinding(place.path, option, 'no action reads it')],
|
|
635
|
+
sentence: `${commandSentence(name)} declares option "${option.name}" but registers no action to receive it.`,
|
|
636
|
+
});
|
|
400
637
|
}
|
|
401
638
|
}
|
|
402
639
|
/**
|
|
@@ -404,10 +641,10 @@ function checkGroup(state) {
|
|
|
404
641
|
* result needs an action, its merged views record names at least one view and its default, and a
|
|
405
642
|
* Command without an action is a group. It answers with the resolved result.
|
|
406
643
|
*/
|
|
407
|
-
function checkFinished(declared, hasAction) {
|
|
408
|
-
const result = buildResult(declared, hasAction);
|
|
644
|
+
function checkFinished(declared, hasAction, place) {
|
|
645
|
+
const result = buildResult(declared, hasAction, place.path);
|
|
409
646
|
if (!hasAction) {
|
|
410
|
-
checkGroup(declared);
|
|
647
|
+
checkGroup(declared, place);
|
|
411
648
|
}
|
|
412
649
|
return result;
|
|
413
650
|
}
|
|
@@ -415,18 +652,23 @@ function checkFinished(declared, hasAction) {
|
|
|
415
652
|
* The one attach operation `Command.command()`, `Application.command()`, and a plugin's
|
|
416
653
|
* `commands` list share. A Command is an immutable value, so the child is final here: it is checked
|
|
417
654
|
* as a finished Command, against the parent's current children, and against the nesting cap from
|
|
418
|
-
* the shallowest level the parent can sit at.
|
|
655
|
+
* the shallowest level the parent can sit at. The placement is the call that attached it, which a
|
|
656
|
+
* finding about the child as a whole marks.
|
|
419
657
|
*/
|
|
420
|
-
export function attach(parent, node) {
|
|
658
|
+
export function attach(parent, node, placement) {
|
|
421
659
|
const { name } = node;
|
|
660
|
+
const child = { name, node, placement };
|
|
422
661
|
if (parent.hasAction) {
|
|
423
|
-
throw new DeclarationError(
|
|
662
|
+
throw new DeclarationError(declaredAfterAction, {
|
|
663
|
+
correction: 'Attach children before action().',
|
|
664
|
+
findings: [{ ...placement, note: 'after action()' }],
|
|
665
|
+
sentence: `${commandSentence(parent.name)} attaches child "${name}" after its action.`,
|
|
666
|
+
});
|
|
424
667
|
}
|
|
425
668
|
if (parent.argument !== undefined) {
|
|
426
|
-
throw placementFault(parent
|
|
669
|
+
throw placementFault(parent, parent.argument, child);
|
|
427
670
|
}
|
|
428
|
-
checkFinished(node.declared, node.hasAction);
|
|
429
|
-
const child = { name, node };
|
|
671
|
+
checkFinished(node.declared, node.hasAction, { path: childPath(parent, name), placement });
|
|
430
672
|
checkSiblings(parent, child);
|
|
431
673
|
checkNesting(parent.name, child, parentLevel(parent.name) + 1);
|
|
432
674
|
return child;
|
|
@@ -435,13 +677,18 @@ export function attach(parent, node) {
|
|
|
435
677
|
export function childNode(parent, child) {
|
|
436
678
|
const node = commandNode(child);
|
|
437
679
|
if (!node) {
|
|
438
|
-
throw new DeclarationError(
|
|
680
|
+
throw new DeclarationError(notACommand, {
|
|
681
|
+
correction: 'Attach the value returned by new Command(name).',
|
|
682
|
+
findings: [{ arguments: [child], call: 'command', mark: '0', path: pathOf(parent) }],
|
|
683
|
+
sentence: `${commandSentence(parent)} attaches a value that is not a Command.`,
|
|
684
|
+
});
|
|
439
685
|
}
|
|
440
686
|
return node;
|
|
441
687
|
}
|
|
442
688
|
/** Attaching is a declaration call too, so the receiver keeps the children it already had. */
|
|
443
689
|
function attachChild(state, child) {
|
|
444
|
-
const
|
|
690
|
+
const node = childNode(state.name, child);
|
|
691
|
+
const attached = attach(parentOf(state), node, commandPlacement(pathOf(state.name), node.name));
|
|
445
692
|
return { ...state, children: [...state.children, attached] };
|
|
446
693
|
}
|
|
447
694
|
/**
|
|
@@ -453,34 +700,47 @@ function attachChild(state, child) {
|
|
|
453
700
|
* here fires only once the cap rises: attach reads one level, and only this walk knows each level.
|
|
454
701
|
*/
|
|
455
702
|
function joinSubtree(scope, child, { level, parent }) {
|
|
456
|
-
const { name, node } = child;
|
|
457
|
-
checkNesting(parent, child, level);
|
|
458
|
-
|
|
459
|
-
|
|
703
|
+
const { name, node, placement } = child;
|
|
704
|
+
checkNesting(parent.name, child, level);
|
|
705
|
+
const owner = scope.owners.get(node);
|
|
706
|
+
if (owner) {
|
|
707
|
+
throw new DeclarationError(commandAttachedTwice, {
|
|
708
|
+
correction: 'Attach a Command value at one point; create a new Command for each placement.',
|
|
709
|
+
findings: [
|
|
710
|
+
{ ...owner.placement, note: 'the first placement' },
|
|
711
|
+
{ ...placement, note: 'the second placement' },
|
|
712
|
+
],
|
|
713
|
+
sentence: `${commandSentence(parent.name)} attaches child "${name}", which ${commandSubject(owner.name)} also attaches.`,
|
|
714
|
+
});
|
|
460
715
|
}
|
|
461
|
-
scope.owners.set(node, parent);
|
|
462
|
-
for (const descriptor of node.declared.descriptors
|
|
463
|
-
registerDescriptor(scope.descriptors, descriptor);
|
|
716
|
+
scope.owners.set(node, { name: parent.name, placement });
|
|
717
|
+
for (const [key, descriptor] of node.declared.descriptors) {
|
|
718
|
+
registerDescriptor(scope.descriptors, descriptor, { identity: key });
|
|
464
719
|
}
|
|
465
|
-
|
|
720
|
+
const path = childPath(parent, name);
|
|
721
|
+
checkLocalOptions(optionsOf(node.declared.inputs), scope.table, localScope(name, path));
|
|
466
722
|
for (const entry of node.declared.children) {
|
|
467
|
-
|
|
723
|
+
// A nested child's own placement knew its parent alone, so the walk places it under the root.
|
|
724
|
+
const rooted = { ...entry, placement: commandPlacement(path, entry.name) };
|
|
725
|
+
joinSubtree(scope, rooted, { level: level + 1, parent: { name, path } });
|
|
468
726
|
}
|
|
469
727
|
}
|
|
470
728
|
/**
|
|
471
729
|
* Attaches one child to an Application's root and walks the subtree it brings, once. The scope's
|
|
472
730
|
* registers are copies: the caller commits the owners, and the returned root holds the descriptors.
|
|
473
731
|
*/
|
|
474
|
-
export function attachToRoot(state,
|
|
475
|
-
const
|
|
476
|
-
|
|
732
|
+
export function attachToRoot(state, child, scope) {
|
|
733
|
+
const parent = parentOf(state);
|
|
734
|
+
const attached = attach(parent, child.node, child.placement);
|
|
735
|
+
joinSubtree(scope, attached, { level: parentLevel(state.name) + 1, parent });
|
|
477
736
|
return { ...state, children: [...state.children, attached], descriptors: scope.descriptors };
|
|
478
737
|
}
|
|
479
738
|
/** Every attached Command's local options against a globals table that has just grown. */
|
|
480
|
-
function checkAttachedOptions(table, children) {
|
|
739
|
+
function checkAttachedOptions(table, parent, children) {
|
|
481
740
|
for (const { name, node } of children) {
|
|
482
|
-
|
|
483
|
-
|
|
741
|
+
const path = childPath(parent, name);
|
|
742
|
+
checkLocalOptions(optionsOf(node.declared.inputs), table, localScope(name, path));
|
|
743
|
+
checkAttachedOptions(table, { name, path }, node.declared.children);
|
|
484
744
|
}
|
|
485
745
|
}
|
|
486
746
|
/**
|
|
@@ -488,42 +748,54 @@ function checkAttachedOptions(table, children) {
|
|
|
488
748
|
* grown. The table's new option reads as the other side of any collision.
|
|
489
749
|
*/
|
|
490
750
|
export function checkDeclaredOptions(state, table) {
|
|
491
|
-
|
|
492
|
-
|
|
751
|
+
const place = { name: state.name, path: pathOf(state.name) };
|
|
752
|
+
checkLocalOptions(optionsOf(state.inputs), table, localScope(place.name, place.path));
|
|
753
|
+
checkAttachedOptions(table, place, state.children);
|
|
493
754
|
}
|
|
494
755
|
/** A variadic or optional slot ends the positional list, so nothing may follow either one. */
|
|
495
|
-
function checkSlotOrder(slot, next,
|
|
756
|
+
function checkSlotOrder(slot, next, command) {
|
|
757
|
+
const { path, subject } = command;
|
|
758
|
+
const after = inputFinding(path, next.input, 'the argument after it');
|
|
496
759
|
if (slot.variadic) {
|
|
497
|
-
throw new DeclarationError(
|
|
760
|
+
throw new DeclarationError(variadicArgumentLast, {
|
|
761
|
+
correction: 'Declare the variadic argument last.',
|
|
762
|
+
findings: [inputFinding(path, slot.input, 'the variadic argument'), after],
|
|
763
|
+
sentence: `Argument "${slot.input.name}" is variadic and precedes argument "${next.input.name}" on ${subject}.`,
|
|
764
|
+
});
|
|
498
765
|
}
|
|
499
766
|
if (!slot.required) {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
767
|
+
const findings = [inputFinding(path, slot.input, 'the optional argument'), after];
|
|
768
|
+
throw new DeclarationError(optionalArgumentLast, next.required
|
|
769
|
+
? {
|
|
770
|
+
correction: 'Declare optional arguments after required ones.',
|
|
771
|
+
findings,
|
|
772
|
+
sentence: `Argument "${slot.input.name}" is optional and precedes required argument "${next.input.name}" on ${subject}.`,
|
|
773
|
+
}
|
|
774
|
+
: {
|
|
775
|
+
correction: 'Declare an optional argument last.',
|
|
776
|
+
findings,
|
|
777
|
+
sentence: `Argument "${next.input.name}" follows optional argument "${slot.input.name}" on ${subject}.`,
|
|
778
|
+
});
|
|
503
779
|
}
|
|
504
780
|
}
|
|
505
781
|
/**
|
|
506
782
|
* The positional slots one declaration holds. Every authored argument answered these rules at its
|
|
507
783
|
* own call, so they throw here only for an argument a lifecycle hook declared.
|
|
508
784
|
*/
|
|
509
|
-
function collectArguments(state,
|
|
785
|
+
function collectArguments(state, command) {
|
|
510
786
|
const slots = [];
|
|
511
|
-
const
|
|
787
|
+
const declared = [];
|
|
788
|
+
const judged = { ...command, subject: commandSubject(command.name) };
|
|
512
789
|
for (const input of state.inputs.filter((entry) => entry.kind === 'argument')) {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
}
|
|
516
|
-
if (seen.has(input.name)) {
|
|
517
|
-
throw new DeclarationError(`Argument "${input.name}" is declared more than once on ${subject}. Remove or rename the duplicate.`);
|
|
518
|
-
}
|
|
519
|
-
seen.add(input.name);
|
|
790
|
+
checkArgumentName(judged, declared, input);
|
|
791
|
+
declared.push(input);
|
|
520
792
|
slots.push(slotOf(input));
|
|
521
793
|
}
|
|
522
794
|
for (let index = 0; index + 1 < slots.length; index += 1) {
|
|
523
795
|
const slot = slots[index];
|
|
524
796
|
const next = slots[index + 1];
|
|
525
797
|
if (slot && next) {
|
|
526
|
-
checkSlotOrder(slot, next,
|
|
798
|
+
checkSlotOrder(slot, next, judged);
|
|
527
799
|
}
|
|
528
800
|
}
|
|
529
801
|
return slots;
|
|
@@ -564,23 +836,36 @@ function recordEntries(record) {
|
|
|
564
836
|
* One `views` entry under the unit its declaration named, read back as the shape its own functions
|
|
565
837
|
* name. The two shapes are exclusive, and a row view answers a rows declaration alone.
|
|
566
838
|
*/
|
|
567
|
-
function resultView(
|
|
568
|
-
const {
|
|
839
|
+
function resultView(kind, site, entry) {
|
|
840
|
+
const { call, key, path, sentence } = site;
|
|
841
|
+
const findings = [resultFinding(path, call, { at: entryMark(call, key) })];
|
|
569
842
|
const whole = isWholeView(entry);
|
|
570
843
|
const row = isRowView(entry);
|
|
571
844
|
if (whole && row) {
|
|
572
|
-
throw new DeclarationError(
|
|
845
|
+
throw new DeclarationError(viewShape, {
|
|
846
|
+
correction: 'Supply one of the two.',
|
|
847
|
+
findings,
|
|
848
|
+
sentence: `${sentence} names view ${quoted(key)} with render and row.`,
|
|
849
|
+
});
|
|
573
850
|
}
|
|
574
851
|
if (row) {
|
|
575
852
|
if (kind === 'value') {
|
|
576
|
-
throw new DeclarationError(
|
|
853
|
+
throw new DeclarationError(rowViewOnValue, {
|
|
854
|
+
correction: 'Supply a view with render, or declare the result with rows().',
|
|
855
|
+
findings,
|
|
856
|
+
sentence: `${sentence} names row view ${quoted(key)} on a value result.`,
|
|
857
|
+
});
|
|
577
858
|
}
|
|
578
859
|
return entry;
|
|
579
860
|
}
|
|
580
861
|
if (whole) {
|
|
581
862
|
return entry;
|
|
582
863
|
}
|
|
583
|
-
throw new DeclarationError(
|
|
864
|
+
throw new DeclarationError(viewShape, {
|
|
865
|
+
correction: 'Supply a view with render or a row view with row.',
|
|
866
|
+
findings,
|
|
867
|
+
sentence: `${sentence} names view ${quoted(key)} with a value that is not a view.`,
|
|
868
|
+
});
|
|
584
869
|
}
|
|
585
870
|
/**
|
|
586
871
|
* Every rule a results-lane call can judge on its own, applied to one Command's calls in the order
|
|
@@ -588,18 +873,31 @@ function resultView(declaration, name, entry) {
|
|
|
588
873
|
* place and appends a new one, so each name keeps the position the call that first named it gave
|
|
589
874
|
* it. A `default` once named persists through later calls that name none.
|
|
590
875
|
*/
|
|
591
|
-
function mergeResult(
|
|
592
|
-
const
|
|
876
|
+
function mergeResult(command, results) {
|
|
877
|
+
const { path } = command;
|
|
878
|
+
const sentence = commandSentence(command.name);
|
|
593
879
|
const declarations = results.filter((call) => call.kind !== 'views');
|
|
594
|
-
|
|
595
|
-
|
|
880
|
+
const [declaration, second] = declarations;
|
|
881
|
+
if (declaration && second) {
|
|
882
|
+
throw new DeclarationError(multipleResults, {
|
|
883
|
+
correction: 'Declare one result() or rows() call.',
|
|
884
|
+
findings: [
|
|
885
|
+
resultFinding(path, declaration, { at: '0', note: 'the first result' }),
|
|
886
|
+
resultFinding(path, second, { at: '0', note: 'the second result' }),
|
|
887
|
+
],
|
|
888
|
+
sentence: `${sentence} declares two results.`,
|
|
889
|
+
});
|
|
596
890
|
}
|
|
597
|
-
const declaration = declarations[0];
|
|
598
891
|
// A `views()` call reshapes a result's views, so one with no result reshapes nothing.
|
|
599
892
|
// The types publish the call where a result is carried, so this reaches a JavaScript author.
|
|
600
893
|
if (!declaration) {
|
|
601
|
-
|
|
602
|
-
|
|
894
|
+
const [reshape] = results;
|
|
895
|
+
if (reshape) {
|
|
896
|
+
throw new DeclarationError(viewsWithoutResult, {
|
|
897
|
+
correction: 'Declare result() or rows() before action().',
|
|
898
|
+
findings: [resultFinding(path, reshape, { at: '0' })],
|
|
899
|
+
sentence: `${sentence} reshapes its views and declares no result.`,
|
|
900
|
+
});
|
|
603
901
|
}
|
|
604
902
|
return undefined;
|
|
605
903
|
}
|
|
@@ -607,40 +905,57 @@ function mergeResult(name, results) {
|
|
|
607
905
|
let selected = undefined;
|
|
608
906
|
for (const call of results) {
|
|
609
907
|
for (const [key, entry] of recordEntries(call.views)) {
|
|
610
|
-
const view = resultView(
|
|
908
|
+
const view = resultView(declaration.kind, { call, key, path, sentence }, entry);
|
|
611
909
|
if (!isViewName(key)) {
|
|
612
|
-
throw new DeclarationError(
|
|
910
|
+
throw new DeclarationError(viewName, {
|
|
911
|
+
correction: 'Use a nonempty name without whitespace, a leading hyphen, or "=", and not a number.',
|
|
912
|
+
findings: [resultFinding(path, call, { at: entryMark(call, key) })],
|
|
913
|
+
sentence: `${sentence} names view ${quoted(key)}.`,
|
|
914
|
+
});
|
|
613
915
|
}
|
|
614
916
|
views.set(key, view);
|
|
615
917
|
}
|
|
616
|
-
|
|
617
|
-
|
|
918
|
+
const key = call.kind === 'views' ? selectedKey(call.default) : undefined;
|
|
919
|
+
if (key !== undefined) {
|
|
920
|
+
selected = { call, key };
|
|
618
921
|
}
|
|
619
922
|
}
|
|
620
|
-
return { kind: declaration.kind, selected, views };
|
|
923
|
+
return { declaration, kind: declaration.kind, selected, views };
|
|
621
924
|
}
|
|
622
925
|
/**
|
|
623
926
|
* The finished result: the merged record, which needs an action, at least one view, and a default
|
|
624
927
|
* that names one of them. The first key answers until one is named.
|
|
625
928
|
*/
|
|
626
|
-
function buildResult(declared, hasAction) {
|
|
627
|
-
const merged = mergeResult(declared.name, declared.results);
|
|
929
|
+
function buildResult(declared, hasAction, path) {
|
|
930
|
+
const merged = mergeResult({ name: declared.name, path }, declared.results);
|
|
628
931
|
if (!merged) {
|
|
629
932
|
return undefined;
|
|
630
933
|
}
|
|
631
934
|
const sentence = commandSentence(declared.name);
|
|
935
|
+
const { declaration, selected, views } = merged;
|
|
632
936
|
if (!hasAction) {
|
|
633
|
-
throw new DeclarationError(
|
|
937
|
+
throw new DeclarationError(resultWithoutAction, {
|
|
938
|
+
correction: 'Register an action or remove the result.',
|
|
939
|
+
findings: [resultFinding(path, declaration, { at: '0' })],
|
|
940
|
+
sentence: `${sentence} declares a result and no action.`,
|
|
941
|
+
});
|
|
634
942
|
}
|
|
635
|
-
const { selected, views } = merged;
|
|
636
943
|
const first = views.keys().next();
|
|
637
944
|
if (first.done === true) {
|
|
638
|
-
throw new DeclarationError(
|
|
945
|
+
throw new DeclarationError(resultWithoutViews, {
|
|
946
|
+
correction: 'Name at least one view.',
|
|
947
|
+
findings: [resultFinding(path, declaration, { at: '0.views' })],
|
|
948
|
+
sentence: `${sentence} declares a result with no views.`,
|
|
949
|
+
});
|
|
639
950
|
}
|
|
640
|
-
if (selected !== undefined && !views.has(selected)) {
|
|
641
|
-
throw new DeclarationError(
|
|
951
|
+
if (selected !== undefined && !views.has(selected.key)) {
|
|
952
|
+
throw new DeclarationError(unknownDefaultView, {
|
|
953
|
+
correction: 'Name the view or select a named one.',
|
|
954
|
+
findings: [resultFinding(path, selected.call, { at: '1.default' })],
|
|
955
|
+
sentence: `${sentence} selects default view ${quoted(selected.key)}, which it does not name.`,
|
|
956
|
+
});
|
|
642
957
|
}
|
|
643
|
-
return { default: selected ?? first.value, kind: merged.kind, views };
|
|
958
|
+
return { default: selected?.key ?? first.value, kind: merged.kind, views };
|
|
644
959
|
}
|
|
645
960
|
/** The values one build made, so a value a hook returns is one of them and never a forged shape. */
|
|
646
961
|
const attachments = new WeakMap();
|
|
@@ -663,7 +978,7 @@ class AttachedCommandValue {
|
|
|
663
978
|
#result;
|
|
664
979
|
constructor(state) {
|
|
665
980
|
this.#state = state;
|
|
666
|
-
this.#result = resultNode(buildResult(state.declared, state.hasAction));
|
|
981
|
+
this.#result = resultNode(buildResult(state.declared, state.hasAction, state.path));
|
|
667
982
|
attachments.set(this, state);
|
|
668
983
|
Object.freeze(this);
|
|
669
984
|
}
|
|
@@ -690,17 +1005,13 @@ class AttachedCommandValue {
|
|
|
690
1005
|
return publishStore(this.#state.extensions);
|
|
691
1006
|
}
|
|
692
1007
|
argument(name, config) {
|
|
693
|
-
return this.#declare({ config
|
|
1008
|
+
return this.#declare({ config, kind: 'argument', name });
|
|
694
1009
|
}
|
|
695
1010
|
option(name, config) {
|
|
696
|
-
return this.#declare({ config
|
|
1011
|
+
return this.#declare({ config, kind: 'option', name });
|
|
697
1012
|
}
|
|
698
1013
|
views(replacements, options) {
|
|
699
|
-
const call =
|
|
700
|
-
default: recordOf(options, 'default'),
|
|
701
|
-
kind: 'views',
|
|
702
|
-
views: replacements,
|
|
703
|
-
};
|
|
1014
|
+
const call = viewsCall(replacements, options);
|
|
704
1015
|
const { declared } = this.#state;
|
|
705
1016
|
return this.#derive({ call, kind: 'views' }, { ...declared, results: [...declared.results, call] });
|
|
706
1017
|
}
|
|
@@ -714,6 +1025,7 @@ class AttachedCommandValue {
|
|
|
714
1025
|
const layer = validateLayer({
|
|
715
1026
|
declared: values,
|
|
716
1027
|
descriptors: registry,
|
|
1028
|
+
site: { at: '', declaration: { arguments: values, call: 'extend', path: state.path } },
|
|
717
1029
|
subject: state.subject,
|
|
718
1030
|
target: 'command',
|
|
719
1031
|
});
|
|
@@ -724,8 +1036,22 @@ class AttachedCommandValue {
|
|
|
724
1036
|
});
|
|
725
1037
|
}
|
|
726
1038
|
/** One input the running hook declared, which the Command's own names now hold. */
|
|
727
|
-
#declare(
|
|
1039
|
+
#declare(raw) {
|
|
728
1040
|
const { declared, identity } = this.#state;
|
|
1041
|
+
const { path } = this.#state;
|
|
1042
|
+
// The name is judged before the config, as Command and Application judge it.
|
|
1043
|
+
if (raw.kind === 'argument') {
|
|
1044
|
+
const { name } = declared;
|
|
1045
|
+
checkArgumentName({ name, path, subject: commandSubject(name) }, [], raw);
|
|
1046
|
+
}
|
|
1047
|
+
else {
|
|
1048
|
+
checkOptionName(raw.name, inputSite(declared.name, path, raw));
|
|
1049
|
+
}
|
|
1050
|
+
checkInputConfig(raw, { call: raw.kind, path });
|
|
1051
|
+
// Each kind captures its own config, so the input keeps the pairing its kind declares.
|
|
1052
|
+
const input = raw.kind === 'argument'
|
|
1053
|
+
? { ...raw, config: captureConfig(raw.config) }
|
|
1054
|
+
: { ...raw, config: captureConfig(raw.config) };
|
|
729
1055
|
return this.#derive({ identity, input, kind: 'input' }, { ...declared, inputs: [...declared.inputs, input] });
|
|
730
1056
|
}
|
|
731
1057
|
#derive(call, declared) {
|
|
@@ -734,9 +1060,13 @@ class AttachedCommandValue {
|
|
|
734
1060
|
}
|
|
735
1061
|
}
|
|
736
1062
|
_a = AttachedCommandValue;
|
|
737
|
-
/** The
|
|
738
|
-
function
|
|
739
|
-
return
|
|
1063
|
+
/** The finding for the hook one plugin declared, as `plugin(identity, { onCommandAttach })`. */
|
|
1064
|
+
function hookFinding(identity, hook) {
|
|
1065
|
+
return {
|
|
1066
|
+
arguments: [identity, { onCommandAttach: hook }],
|
|
1067
|
+
call: 'plugin',
|
|
1068
|
+
mark: '1.onCommandAttach',
|
|
1069
|
+
};
|
|
740
1070
|
}
|
|
741
1071
|
/** One hook's own call, whose failure is the plugin's, and whose own report is its own. */
|
|
742
1072
|
function callHook(value, hook, named) {
|
|
@@ -748,7 +1078,11 @@ function callHook(value, hook, named) {
|
|
|
748
1078
|
if (error instanceof DeclarationError) {
|
|
749
1079
|
throw error;
|
|
750
1080
|
}
|
|
751
|
-
throw new DeclarationError(
|
|
1081
|
+
throw new DeclarationError(brokenAttachHook, {
|
|
1082
|
+
correction: 'Return the value the hook received or a value derived from it, and throw only a DeclarationError from the hook.',
|
|
1083
|
+
findings: [hookFinding(named.identity, hook)],
|
|
1084
|
+
sentence: `Plugin ${quoted(named.identity)} failed in onCommandAttach for ${named.subject}: ${asSentence(reasonOf(error))}`,
|
|
1085
|
+
}, { cause: error });
|
|
752
1086
|
}
|
|
753
1087
|
}
|
|
754
1088
|
/**
|
|
@@ -759,7 +1093,11 @@ function callHook(value, hook, named) {
|
|
|
759
1093
|
function attachOnce(value, hook, named) {
|
|
760
1094
|
const state = attachments.get(callHook(value, hook, named));
|
|
761
1095
|
if (!state || state.lineage !== named.lineage) {
|
|
762
|
-
throw new DeclarationError(
|
|
1096
|
+
throw new DeclarationError(brokenAttachHook, {
|
|
1097
|
+
correction: 'Return the value it received or a value derived from it.',
|
|
1098
|
+
findings: [hookFinding(named.identity, hook)],
|
|
1099
|
+
sentence: `Plugin ${quoted(named.identity)} returned a value that is not the attached Command from onCommandAttach for ${named.subject}.`,
|
|
1100
|
+
});
|
|
763
1101
|
}
|
|
764
1102
|
return state;
|
|
765
1103
|
}
|
|
@@ -817,10 +1155,28 @@ function applyAttachCalls(state, calls) {
|
|
|
817
1155
|
}
|
|
818
1156
|
return next;
|
|
819
1157
|
}
|
|
1158
|
+
/**
|
|
1159
|
+
* The reason rule one name collision breaks.
|
|
1160
|
+
* Two options or two arguments break the rule the application's own collision of the pair breaks.
|
|
1161
|
+
* An argument and an option under one name break name-shared-across-kinds, which only a hook reaches.
|
|
1162
|
+
*/
|
|
1163
|
+
function nameCollisionRule(declared, held) {
|
|
1164
|
+
if (declared !== held) {
|
|
1165
|
+
return nameSharedAcrossKinds;
|
|
1166
|
+
}
|
|
1167
|
+
return declared === 'option' ? optionDeclaredTwice : argumentDeclaredTwice;
|
|
1168
|
+
}
|
|
1169
|
+
/** The note a finding for one hook-declared input carries. */
|
|
1170
|
+
function hookNote(identity) {
|
|
1171
|
+
return `declared by plugin ${quoted(identity)}`;
|
|
1172
|
+
}
|
|
820
1173
|
/** The clause and the remedy an input another plugin's hook already declared earns. */
|
|
821
|
-
function hookClause(
|
|
1174
|
+
function hookClause(earlier, path) {
|
|
1175
|
+
const { identity, input } = earlier;
|
|
822
1176
|
return {
|
|
823
|
-
clause: `an ${kind} plugin
|
|
1177
|
+
clause: `an ${input.kind} plugin ${quoted(identity)} declared through onCommandAttach`,
|
|
1178
|
+
held: inputFinding(path, input, hookNote(identity)),
|
|
1179
|
+
kind: input.kind,
|
|
824
1180
|
remedy: 'Install one of them.',
|
|
825
1181
|
};
|
|
826
1182
|
}
|
|
@@ -842,6 +1198,25 @@ function attachedRemedy(target) {
|
|
|
842
1198
|
}
|
|
843
1199
|
return 'Install one of them.';
|
|
844
1200
|
}
|
|
1201
|
+
/** The collision with one option the globals table holds, a global or a plugin's own option. */
|
|
1202
|
+
function tableCollision(entry) {
|
|
1203
|
+
const { owner, site } = entry;
|
|
1204
|
+
if (owner.kind === 'plugin') {
|
|
1205
|
+
const clause = `an option of plugin ${quoted(owner.identity)}`;
|
|
1206
|
+
return {
|
|
1207
|
+
clause,
|
|
1208
|
+
held: siteFinding(site, site.named, clause),
|
|
1209
|
+
kind: 'option',
|
|
1210
|
+
remedy: attachedRemedy('plugin'),
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
return {
|
|
1214
|
+
clause: 'a global option',
|
|
1215
|
+
held: siteFinding(site, site.named, 'the global option'),
|
|
1216
|
+
kind: 'option',
|
|
1217
|
+
remedy: attachedRemedy('global'),
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
845
1220
|
/**
|
|
846
1221
|
* What one name a hook-declared input of either kind collides with, in the order the scopes are
|
|
847
1222
|
* reported: an earlier hook's option, an earlier hook's argument, a local option, a global or
|
|
@@ -850,32 +1225,36 @@ function attachedRemedy(target) {
|
|
|
850
1225
|
*/
|
|
851
1226
|
function attachedCollision(input, held) {
|
|
852
1227
|
const { name } = input;
|
|
853
|
-
const
|
|
1228
|
+
const { path } = held;
|
|
1229
|
+
const hook = held.hooks.get(name) ?? held.hookArguments.get(name);
|
|
854
1230
|
if (hook !== undefined) {
|
|
855
|
-
return hookClause(
|
|
1231
|
+
return hookClause(hook, path);
|
|
856
1232
|
}
|
|
857
|
-
const
|
|
858
|
-
if (
|
|
859
|
-
return
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1233
|
+
const local = held.locals.get(name);
|
|
1234
|
+
if (local !== undefined) {
|
|
1235
|
+
return {
|
|
1236
|
+
clause: 'a local option',
|
|
1237
|
+
held: inputFinding(path, local, 'the local option'),
|
|
1238
|
+
kind: 'option',
|
|
1239
|
+
remedy: attachedRemedy('local'),
|
|
1240
|
+
};
|
|
863
1241
|
}
|
|
864
|
-
const
|
|
865
|
-
if (
|
|
866
|
-
|
|
867
|
-
const clause = claimed.kind === 'plugin' ? `an option of plugin "${claimed.identity}"` : 'a global option';
|
|
868
|
-
return { clause, remedy: attachedRemedy(target) };
|
|
1242
|
+
const entry = held.globals.names.get(name);
|
|
1243
|
+
if (entry) {
|
|
1244
|
+
return tableCollision(entry);
|
|
869
1245
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
1246
|
+
const argument = held.arguments.get(name);
|
|
1247
|
+
return argument === undefined
|
|
1248
|
+
? undefined
|
|
1249
|
+
: {
|
|
1250
|
+
clause: 'an argument',
|
|
1251
|
+
held: inputFinding(path, argument, 'the argument'),
|
|
1252
|
+
kind: 'argument',
|
|
1253
|
+
remedy: attachedRemedy('argument'),
|
|
1254
|
+
};
|
|
873
1255
|
}
|
|
874
|
-
/**
|
|
875
|
-
|
|
876
|
-
* its long form where it declares one and the colliding spelling itself where it declares none.
|
|
877
|
-
*/
|
|
878
|
-
function readSpellings(table, claimed) {
|
|
1256
|
+
/** The spellings one compiled table holds, each with its form and the place that declared it. */
|
|
1257
|
+
function readSpellings(table, claimed, placeOf) {
|
|
879
1258
|
const longs = new Map();
|
|
880
1259
|
for (const [spelling, option] of table) {
|
|
881
1260
|
if (option.role === 'long') {
|
|
@@ -884,53 +1263,107 @@ function readSpellings(table, claimed) {
|
|
|
884
1263
|
}
|
|
885
1264
|
for (const [spelling, option] of table) {
|
|
886
1265
|
if (!claimed.has(spelling)) {
|
|
887
|
-
|
|
1266
|
+
const form = longs.get(option.name) ?? spelling;
|
|
1267
|
+
claimed.set(spelling, { finding: placeOf(option.name, option.role), form });
|
|
888
1268
|
}
|
|
889
1269
|
}
|
|
890
1270
|
}
|
|
1271
|
+
/** The place one input's spelling sits: the key of its call that yields the spelling. */
|
|
1272
|
+
function spellingPlace(site, role, note) {
|
|
1273
|
+
return siteFinding(site, spellingMark(site, role), note);
|
|
1274
|
+
}
|
|
891
1275
|
/** One hook-declared option's spellings, against every spelling the table already claims. */
|
|
892
1276
|
function checkAttachedSpelling(declared, named) {
|
|
893
|
-
const { claimed,
|
|
894
|
-
const
|
|
895
|
-
|
|
1277
|
+
const { claimed, scope } = named;
|
|
1278
|
+
const { identity, input } = declared;
|
|
1279
|
+
const { subject } = scope;
|
|
1280
|
+
const site = scope.siteOf(input);
|
|
1281
|
+
const table = compileOptions([input], scope);
|
|
1282
|
+
for (const [spelling, option] of table) {
|
|
896
1283
|
const used = claimed.get(spelling);
|
|
897
1284
|
if (used !== undefined) {
|
|
898
|
-
throw new DeclarationError(
|
|
1285
|
+
throw new DeclarationError(spellingTaken, {
|
|
1286
|
+
correction: 'Change one of the two spellings or omit the plugin.',
|
|
1287
|
+
findings: [
|
|
1288
|
+
spellingPlace(site, option.role, hookNote(identity)),
|
|
1289
|
+
...(used.finding === undefined ? [] : [used.finding]),
|
|
1290
|
+
],
|
|
1291
|
+
sentence: `Plugin ${quoted(identity)} declares option ${quoted(input.name)} with spelling ${quoted(spelling)} on ${subject}, which ${quoted(used.form)} already uses.`,
|
|
1292
|
+
});
|
|
899
1293
|
}
|
|
900
1294
|
}
|
|
901
|
-
readSpellings(table, claimed);
|
|
1295
|
+
readSpellings(table, claimed, (_name, role) => spellingPlace(site, role, hookNote(identity)));
|
|
1296
|
+
}
|
|
1297
|
+
/** The declarations of one kind, keyed by name, the first of each name winning. */
|
|
1298
|
+
function byName(inputs) {
|
|
1299
|
+
const named = new Map();
|
|
1300
|
+
for (const input of inputs) {
|
|
1301
|
+
if (!named.has(input.name)) {
|
|
1302
|
+
named.set(input.name, input);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
return named;
|
|
1306
|
+
}
|
|
1307
|
+
/** Where the globals table's options declared their spellings, a global or a plugin's option. */
|
|
1308
|
+
function tableSpellings(globals) {
|
|
1309
|
+
return (name, role) => {
|
|
1310
|
+
const entry = globals.names.get(name);
|
|
1311
|
+
if (!entry) {
|
|
1312
|
+
return undefined;
|
|
1313
|
+
}
|
|
1314
|
+
const { owner, site } = entry;
|
|
1315
|
+
const note = owner.kind === 'plugin'
|
|
1316
|
+
? `an option of plugin ${quoted(owner.identity)}`
|
|
1317
|
+
: `the global option "${name}"`;
|
|
1318
|
+
return spellingPlace(site, role, note);
|
|
1319
|
+
};
|
|
902
1320
|
}
|
|
903
1321
|
/**
|
|
904
1322
|
* Every input a hook declared, against the names and spellings the Command, the globals table, and
|
|
905
1323
|
* an earlier hook already hold. It runs before the Command's own inputs compile, so a hook-declared
|
|
906
|
-
* input reports as the plugin's fault and never as the author's.
|
|
1324
|
+
* input reports as the plugin's fault and never as the author's. A collision carries a finding for
|
|
1325
|
+
* the hook's input and one for the declaration that already holds the name or spelling.
|
|
907
1326
|
*/
|
|
908
|
-
function checkAttachedInputs(declared, attached,
|
|
909
|
-
const
|
|
1327
|
+
function checkAttachedInputs(declared, attached, place) {
|
|
1328
|
+
const { globals, path } = place;
|
|
1329
|
+
const scope = localScope(declared.name, path);
|
|
1330
|
+
const { subject } = scope;
|
|
910
1331
|
const hooked = new Set(attached.map((entry) => entry.input));
|
|
911
1332
|
const authored = declared.inputs.filter((input) => !hooked.has(input));
|
|
912
1333
|
const options = authored.filter((input) => input.kind === 'option');
|
|
1334
|
+
const locals = byName(options);
|
|
913
1335
|
const held = {
|
|
914
|
-
arguments:
|
|
1336
|
+
arguments: byName(authored.filter((input) => input.kind === 'argument')),
|
|
915
1337
|
globals,
|
|
916
1338
|
hookArguments: new Map(),
|
|
917
1339
|
hooks: new Map(),
|
|
918
|
-
locals
|
|
1340
|
+
locals,
|
|
1341
|
+
path,
|
|
919
1342
|
};
|
|
920
1343
|
const claimed = new Map();
|
|
921
|
-
readSpellings(globals.options, claimed);
|
|
922
|
-
readSpellings(compileOptions(options,
|
|
923
|
-
|
|
1344
|
+
readSpellings(globals.options, claimed, tableSpellings(globals));
|
|
1345
|
+
readSpellings(compileOptions(options, scope), claimed, (name, role) => {
|
|
1346
|
+
const local = locals.get(name);
|
|
1347
|
+
return local === undefined || local.kind !== 'option'
|
|
1348
|
+
? undefined
|
|
1349
|
+
: spellingPlace(scope.siteOf(local), role, `the local option "${name}"`);
|
|
1350
|
+
});
|
|
1351
|
+
for (const entry of attached) {
|
|
1352
|
+
const { identity, input } = entry;
|
|
924
1353
|
const collision = attachedCollision(input, held);
|
|
925
1354
|
if (collision) {
|
|
926
|
-
throw new DeclarationError(
|
|
1355
|
+
throw new DeclarationError(nameCollisionRule(input.kind, collision.kind), {
|
|
1356
|
+
correction: collision.remedy,
|
|
1357
|
+
findings: [inputFinding(path, input, hookNote(identity)), collision.held],
|
|
1358
|
+
sentence: `Plugin ${quoted(identity)} declares ${input.kind} ${quoted(input.name)} on ${subject}, which is already declared as ${collision.clause}.`,
|
|
1359
|
+
});
|
|
927
1360
|
}
|
|
928
1361
|
if (input.kind === 'option') {
|
|
929
|
-
checkAttachedSpelling({ identity, input }, { claimed,
|
|
930
|
-
held.hooks.set(input.name,
|
|
1362
|
+
checkAttachedSpelling({ identity, input }, { claimed, scope });
|
|
1363
|
+
held.hooks.set(input.name, entry);
|
|
931
1364
|
}
|
|
932
1365
|
else {
|
|
933
|
-
held.hookArguments.set(input.name,
|
|
1366
|
+
held.hookArguments.set(input.name, entry);
|
|
934
1367
|
}
|
|
935
1368
|
}
|
|
936
1369
|
}
|
|
@@ -969,29 +1402,31 @@ function bindDispatch(state, action, globals) {
|
|
|
969
1402
|
function checkInputFacts(inputs, command, context) {
|
|
970
1403
|
const { name, subject } = command;
|
|
971
1404
|
for (const input of inputs) {
|
|
972
|
-
const
|
|
973
|
-
|
|
1405
|
+
const site = inputSite(name, context.path, input);
|
|
1406
|
+
const sentence = site.subject;
|
|
1407
|
+
checkDescription(site, input.config.description);
|
|
974
1408
|
if (input.kind === 'argument') {
|
|
975
|
-
checkNoListingFacts(
|
|
976
|
-
checkNoArgumentBinding(
|
|
1409
|
+
checkNoListingFacts(site, input.config);
|
|
1410
|
+
checkNoArgumentBinding(site, input.config);
|
|
977
1411
|
}
|
|
978
1412
|
else {
|
|
979
|
-
checkHidden(
|
|
980
|
-
checkDeprecated(
|
|
981
|
-
checkEnvBinding(
|
|
1413
|
+
checkHidden(site, input.config.hidden);
|
|
1414
|
+
checkDeprecated(site, input.config.deprecated);
|
|
1415
|
+
checkEnvBinding(site, input.config);
|
|
982
1416
|
}
|
|
983
1417
|
context.extensions.set(input, buildExtensions({
|
|
984
1418
|
declared: input.config.extensions,
|
|
985
1419
|
descriptors: context.descriptors,
|
|
1420
|
+
site: { ...site, at: '1.extensions' },
|
|
986
1421
|
subject: { phrase: `on ${subject} ${input.kind} "${input.name}"`, sentence },
|
|
987
1422
|
target: input.kind,
|
|
988
1423
|
}));
|
|
989
1424
|
}
|
|
990
1425
|
}
|
|
991
1426
|
/** One Command declares arguments or attaches children, whichever declaration made each of them. */
|
|
992
|
-
function checkArgumentPlacement(
|
|
1427
|
+
function checkArgumentPlacement(command, slot, child) {
|
|
993
1428
|
if (slot && child) {
|
|
994
|
-
throw placementFault(
|
|
1429
|
+
throw placementFault(command, slot.input, child);
|
|
995
1430
|
}
|
|
996
1431
|
}
|
|
997
1432
|
/**
|
|
@@ -1008,7 +1443,8 @@ export function buildCommand(state, context) {
|
|
|
1008
1443
|
for (const [input, record] of state.records) {
|
|
1009
1444
|
context.extensions.set(input, record);
|
|
1010
1445
|
}
|
|
1011
|
-
const
|
|
1446
|
+
const place = { name, path: context.path };
|
|
1447
|
+
const declaredSlots = collectArguments(state, place);
|
|
1012
1448
|
const hasAction = action !== undefined;
|
|
1013
1449
|
/**
|
|
1014
1450
|
* The hooks run once the author's declaration is complete, and each value a hook receives resolves
|
|
@@ -1027,15 +1463,15 @@ export function buildCommand(state, context) {
|
|
|
1027
1463
|
checkInputFacts(hookInputs.map((call) => call.input), { name, subject }, context);
|
|
1028
1464
|
// The hook-declared names are checked first, so a collision reports in the plugin's voice.
|
|
1029
1465
|
// A rule the author's own declaration voices never speaks for a name a hook declared.
|
|
1030
|
-
checkAttachedInputs(hooked, hookInputs, globals);
|
|
1466
|
+
checkAttachedInputs(hooked, hookInputs, { globals, path: context.path });
|
|
1031
1467
|
// Every value was validated once, the author's at each call and each hook's at its `extend()`.
|
|
1032
1468
|
const extensions = publishStore(progress.extensions);
|
|
1033
|
-
const slots = hookInputs.length > 0 ? collectArguments(hooked,
|
|
1034
|
-
checkArgumentPlacement(
|
|
1035
|
-
const result = checkFinished(hooked, hasAction);
|
|
1036
|
-
const options = checkLocalOptions(optionsOf(hooked.inputs), globals,
|
|
1469
|
+
const slots = hookInputs.length > 0 ? collectArguments(hooked, place) : declaredSlots;
|
|
1470
|
+
checkArgumentPlacement(place, slots[0], state.children[0]);
|
|
1471
|
+
const result = checkFinished(hooked, hasAction, { path: context.path });
|
|
1472
|
+
const options = checkLocalOptions(optionsOf(hooked.inputs), globals, localScope(name, context.path));
|
|
1037
1473
|
// A hook's erased calls answer the declaration rules an authored call answers at the call.
|
|
1038
|
-
checkDeclarations(hookInputs.map((
|
|
1474
|
+
checkDeclarations(hookInputs.map(({ input }) => ({ input, site: inputSite(name, context.path, input) })));
|
|
1039
1475
|
const children = new Map();
|
|
1040
1476
|
const routes = new Map();
|
|
1041
1477
|
for (const child of state.children) {
|
|
@@ -1093,7 +1529,7 @@ export class CommandBuilder {
|
|
|
1093
1529
|
}
|
|
1094
1530
|
argument(name, config) {
|
|
1095
1531
|
const input = {
|
|
1096
|
-
config
|
|
1532
|
+
config,
|
|
1097
1533
|
kind: 'argument',
|
|
1098
1534
|
name,
|
|
1099
1535
|
};
|
|
@@ -1101,7 +1537,7 @@ export class CommandBuilder {
|
|
|
1101
1537
|
}
|
|
1102
1538
|
option(name, config) {
|
|
1103
1539
|
const input = {
|
|
1104
|
-
config
|
|
1540
|
+
config,
|
|
1105
1541
|
kind: 'option',
|
|
1106
1542
|
name,
|
|
1107
1543
|
};
|
|
@@ -1172,12 +1608,35 @@ export function collectInputs(command) {
|
|
|
1172
1608
|
];
|
|
1173
1609
|
}
|
|
1174
1610
|
/**
|
|
1175
|
-
*
|
|
1176
|
-
*
|
|
1177
|
-
|
|
1611
|
+
* Where every input of one graph was declared: each global option at its `globalOption()` call,
|
|
1612
|
+
* and each Command's own inputs at their calls on the Command, under its path from the root.
|
|
1613
|
+
*/
|
|
1614
|
+
export function inputPlaces(graph) {
|
|
1615
|
+
const places = new Map();
|
|
1616
|
+
for (const input of graph.globals.inputs) {
|
|
1617
|
+
places.set(input, inputPlace(input, { global: true, path: [] }));
|
|
1618
|
+
}
|
|
1619
|
+
const walk = (command, path) => {
|
|
1620
|
+
for (const input of command.inputs) {
|
|
1621
|
+
places.set(input, inputPlace(input, { global: false, path }));
|
|
1622
|
+
}
|
|
1623
|
+
for (const [name, child] of command.children) {
|
|
1624
|
+
walk(child, [...path, name]);
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
walk(graph.root, []);
|
|
1628
|
+
return places;
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* The names a routing failure offers: the canonical names of the visible, current children, in
|
|
1632
|
+
* authoring order. A candidate list is a listing, so a hidden or a deprecated child is absent from
|
|
1633
|
+
* it, as completion leaves them out, and a parent whose children are all hidden or deprecated
|
|
1634
|
+
* offers none. A deprecated child typed in full still routes.
|
|
1178
1635
|
*/
|
|
1179
1636
|
function candidatesOf(command) {
|
|
1180
|
-
return [...command.children]
|
|
1637
|
+
return [...command.children]
|
|
1638
|
+
.filter(([, child]) => !child.hidden && child.deprecated === undefined)
|
|
1639
|
+
.map(([name]) => name);
|
|
1181
1640
|
}
|
|
1182
1641
|
/**
|
|
1183
1642
|
* Whether a token names one of the Command's children: the Command has children and the token is
|
|
@@ -1186,8 +1645,12 @@ function candidatesOf(command) {
|
|
|
1186
1645
|
export function readsAsChild(command, token) {
|
|
1187
1646
|
return command.children.size > 0 && !isOptionToken(token);
|
|
1188
1647
|
}
|
|
1189
|
-
/**
|
|
1190
|
-
|
|
1648
|
+
/**
|
|
1649
|
+
* Bare tokens, names or aliases, select children until a Command has none; a hyphen commits.
|
|
1650
|
+
* `walked` receives the path after each name routes, so an unknown Command leaves its caller
|
|
1651
|
+
* holding the partial path walked before it.
|
|
1652
|
+
*/
|
|
1653
|
+
export function route(root, tokens, walked) {
|
|
1191
1654
|
let command = root;
|
|
1192
1655
|
const path = [];
|
|
1193
1656
|
let index = 0;
|
|
@@ -1202,6 +1665,7 @@ export function route(root, tokens) {
|
|
|
1202
1665
|
// An alias routes like the canonical name, and the path it walks reports that name alone.
|
|
1203
1666
|
command = child.command;
|
|
1204
1667
|
path.push(child.name);
|
|
1668
|
+
walked?.(Object.freeze([...path]));
|
|
1205
1669
|
index += 1;
|
|
1206
1670
|
}
|
|
1207
1671
|
return { command, path, tokens: tokens.slice(index) };
|
|
@@ -1240,10 +1704,13 @@ export function argumentSlot(slots, position) {
|
|
|
1240
1704
|
const last = slots.at(-1);
|
|
1241
1705
|
return slots[position] ?? (last?.variadic ? last : undefined);
|
|
1242
1706
|
}
|
|
1243
|
-
/**
|
|
1244
|
-
|
|
1707
|
+
/**
|
|
1708
|
+
* Consumes the globals table, then routes the remaining bare tokens to a Command. `walked`
|
|
1709
|
+
* receives the path as routing extends it, the partial path of an unknown Command included.
|
|
1710
|
+
*/
|
|
1711
|
+
export function routeInvocation(graph, argv, walked) {
|
|
1245
1712
|
const scan = extractGlobals(graph.globals.options, [...argv]);
|
|
1246
|
-
const routed = route(graph.root, scan.rest);
|
|
1713
|
+
const routed = route(graph.root, scan.rest, walked);
|
|
1247
1714
|
return {
|
|
1248
1715
|
command: routed.command,
|
|
1249
1716
|
path: routed.path,
|
|
@@ -1306,7 +1773,7 @@ function requestNode(graph, path, place) {
|
|
|
1306
1773
|
const options = place.global ? graph.globals : nodeAt(graph, path).options;
|
|
1307
1774
|
const node = options.find((option) => option.name === place.name);
|
|
1308
1775
|
if (!node) {
|
|
1309
|
-
throw
|
|
1776
|
+
throw graphMismatch(`Option "${place.name}" is not in the inspected graph.`);
|
|
1310
1777
|
}
|
|
1311
1778
|
return node;
|
|
1312
1779
|
}
|
|
@@ -1330,6 +1797,7 @@ async function fillScope({ graph, invocation, routed }, local) {
|
|
|
1330
1797
|
locals: local.kind === 'parsed'
|
|
1331
1798
|
? { global: false, inputs: optionsOf(routed.command.inputs), values: locals }
|
|
1332
1799
|
: undefined,
|
|
1800
|
+
offer: invocation.offer,
|
|
1333
1801
|
out: invocation.sourceOut,
|
|
1334
1802
|
plugins: graph.globals.plugins,
|
|
1335
1803
|
request: (input, global) => requestNode(invocation.inspected(), routed.path, { global, name: input.name }),
|
|
@@ -1426,6 +1894,7 @@ export async function prepareDispatch(graph, routed, invocation) {
|
|
|
1426
1894
|
return { ...ready, globals, kind: 'ready', result };
|
|
1427
1895
|
}
|
|
1428
1896
|
catch (error) {
|
|
1429
|
-
|
|
1897
|
+
// The fault is held as a failure, so a throw from reading a validator's output is never offered.
|
|
1898
|
+
return held(toFailure(error));
|
|
1430
1899
|
}
|
|
1431
1900
|
}
|