@loomcli/core 0.5.0 → 0.7.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 +18 -2
- package/dist/application.js +302 -75
- package/dist/bindings.d.ts +15 -10
- package/dist/bindings.js +34 -15
- package/dist/capture.d.ts +65 -0
- package/dist/capture.js +99 -0
- 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 +734 -236
- 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 +71 -11
- package/dist/facts.js +106 -23
- package/dist/globals.d.ts +29 -21
- package/dist/globals.js +117 -46
- package/dist/glyphs.generated.js +1 -1
- 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 +12 -2
- package/dist/index.js +6 -0
- package/dist/input-rules.d.ts +68 -0
- package/dist/input-rules.js +152 -0
- package/dist/inspect.d.ts +12 -12
- package/dist/inspect.js +92 -48
- package/dist/lanes.js +1 -1
- package/dist/locate.js +4 -4
- package/dist/options.d.ts +30 -2
- package/dist/options.js +143 -46
- package/dist/output.d.ts +9 -2
- package/dist/output.js +18 -2
- package/dist/plain.d.ts +56 -0
- package/dist/plain.js +238 -0
- package/dist/plugin-rules.d.ts +68 -0
- package/dist/plugin-rules.js +164 -0
- package/dist/plugin-settings.d.ts +18 -0
- package/dist/plugin-settings.js +38 -0
- package/dist/plugin.d.ts +27 -15
- package/dist/plugin.js +429 -144
- 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 +25 -16
- package/dist/style-layout.js +2 -2
- package/dist/style-width.d.ts +13 -0
- package/dist/style-width.js +170 -0
- 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 +13 -3
- package/dist/unicode.generated.d.ts +27 -0
- package/dist/unicode.generated.js +1036 -0
- package/dist/validation.d.ts +69 -7
- package/dist/validation.js +211 -67
- package/dist/view.d.ts +61 -22
- package/dist/view.js +168 -81
- package/licenses/unicode-LICENSE.txt +41 -0
- package/licenses/uucode-LICENSE.md +35 -0
- package/package.json +9 -5
package/dist/command.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
var _a, _b;
|
|
2
2
|
import { checkEnvBinding, checkNoArgumentBinding } from './bindings.js';
|
|
3
|
-
import {
|
|
3
|
+
import { captureDeclaration, unreadableArgument } from './capture.js';
|
|
4
|
+
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';
|
|
5
|
+
import { elided, quoteString, spelled } from './diagnostic-text.js';
|
|
6
|
+
import { asSentence, commandSentence, commandSubject, DeclarationError, NonCallableCommandError, quoted, ResultError, toFailure, UnexpectedArgumentError, UnknownCommandError, reasonOf, } from './errors.js';
|
|
4
7
|
import { buildExtensions, extendStore, publishStore, registerDescriptor, storeCommandLayers, validateLayer, } from './extension.js';
|
|
5
|
-
import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts,
|
|
8
|
+
import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts, declarerNote, siteFinding, } from './facts.js';
|
|
6
9
|
import { buildGlobals, checkLocalOptions } from './globals.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
10
|
+
import { nameSharedAcrossKinds, optionDeclaredTwice, spellingTaken } from './input-rules.js';
|
|
11
|
+
import { graphMismatch, nodeAt, resultNode } from './inspect.js';
|
|
12
|
+
import { checkOptionName, compileOptions, copyValues, emptyValues, extractGlobals, isOptionToken, mergeValues, parseInputs, spellingMark, } from './options.js';
|
|
13
|
+
import { declaring, isPlainObject, shallowList, snapshot } from './plain.js';
|
|
14
|
+
import { brokenAttachHook, notAnObject } from './plugin-rules.js';
|
|
9
15
|
import { fillInputs } from './sources.js';
|
|
10
|
-
import {
|
|
16
|
+
import { captureInputConfig, configUnread, checkDeclarations, declaringSite, inputPlace, validateValues, } from './validation.js';
|
|
11
17
|
/**
|
|
12
18
|
* The deepest level below the root a Command may sit at, and the word its diagnostic spells it with.
|
|
13
19
|
* A child of the root sits at level 1. Raising the cap relaxes a rule and breaks no application.
|
|
@@ -41,13 +47,62 @@ function nodeHandle(name, state) {
|
|
|
41
47
|
export function commandNode(value) {
|
|
42
48
|
return typeof value === 'object' && value !== null ? nodes.get(value) : undefined;
|
|
43
49
|
}
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
/**
|
|
51
|
+
* The path a finding opens with for a declaration call on one Command. The root's is empty. A named
|
|
52
|
+
* Command's call cannot know the parent it will join, so its path is its own name.
|
|
53
|
+
*/
|
|
54
|
+
function pathOf(name) {
|
|
55
|
+
return name === null ? [] : [name];
|
|
56
|
+
}
|
|
57
|
+
/** A call's arguments as its author wrote them: the trailing ones left undefined are dropped. */
|
|
58
|
+
export function callArguments(...values) {
|
|
59
|
+
let length = values.length;
|
|
60
|
+
while (length > 0 && values[length - 1] === undefined) {
|
|
61
|
+
length -= 1;
|
|
62
|
+
}
|
|
63
|
+
return values.slice(0, length);
|
|
64
|
+
}
|
|
65
|
+
/** A Command value as a finding prints it, which it would otherwise print as an ellipsis. */
|
|
66
|
+
export function commandCode(name) {
|
|
67
|
+
return spelled(`new Command(${quoteString(name)})`);
|
|
68
|
+
}
|
|
69
|
+
/** The finding for one `command()` call that attached a child under the Command at `path`. */
|
|
70
|
+
export function commandPlacement(path, child) {
|
|
71
|
+
return { arguments: [commandCode(child)], call: 'command', mark: '0', path };
|
|
72
|
+
}
|
|
73
|
+
/** The finding for one `new Command()` call, which carries the name and the options slot. */
|
|
74
|
+
function constructorFinding(name, options, mark) {
|
|
75
|
+
return { arguments: callArguments(name, options), call: 'new Command', mark };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The one copy of the options that `new Command(name, options)` reads, taken once the name is
|
|
79
|
+
* judged: the options object and its `extensions` list, whose entries are what their factory built
|
|
80
|
+
* and are not copied. A read that throws is the unreadable fault of the options, a value that is not
|
|
81
|
+
* a plain object is the not-an-object fault, and a Command declared without options has none.
|
|
82
|
+
*/
|
|
83
|
+
function captureCommandOptions(name, options) {
|
|
84
|
+
if (options === undefined) {
|
|
85
|
+
return undefined;
|
|
48
86
|
}
|
|
49
|
-
|
|
50
|
-
|
|
87
|
+
return captureDeclaration(options, (copy, read) => {
|
|
88
|
+
read.nested(copy, 'extensions', shallowList);
|
|
89
|
+
}, {
|
|
90
|
+
notAnObject: () => new DeclarationError(notAnObject, {
|
|
91
|
+
correction: 'Supply a Command options object.',
|
|
92
|
+
findings: [constructorFinding(name, options, '1')],
|
|
93
|
+
sentence: `${commandSentence(name)} declares options that are not an object.`,
|
|
94
|
+
}),
|
|
95
|
+
unreadable: unreadableArgument({ call: 'new Command', named: name, subject: commandSentence(name) }, 'options'),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/** A named Command's options never carry the retired globals wiring. */
|
|
99
|
+
function checkCommandOptions(name, options) {
|
|
100
|
+
if ('globals' in options) {
|
|
101
|
+
throw new DeclarationError(commandGlobals, {
|
|
102
|
+
correction: 'Declare globals on the Application and register its environment.',
|
|
103
|
+
findings: [constructorFinding(name, options, '1.globals')],
|
|
104
|
+
sentence: `${commandSentence(name)} declares globals.`,
|
|
105
|
+
});
|
|
51
106
|
}
|
|
52
107
|
}
|
|
53
108
|
/** One name rule for an argument, option, or view name: a bare token the parser can read. */
|
|
@@ -66,11 +121,25 @@ export function isPortableName(name) {
|
|
|
66
121
|
/** The one correction every portable name diagnostic ends with. */
|
|
67
122
|
export const portableNameCorrection = 'Use a nonempty name of A-Z, a-z, 0-9, ".", "_", and "-" that does not start with "-" or ".".';
|
|
68
123
|
/** An alias is typed at the prompt the way a Command name is, so it answers to the portable rule. */
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
|
|
124
|
+
function checkAliasNames(command, names) {
|
|
125
|
+
for (const [index, alias] of names.entries()) {
|
|
126
|
+
if (!isPortableName(alias)) {
|
|
127
|
+
throw new DeclarationError(portableName, {
|
|
128
|
+
correction: portableNameCorrection,
|
|
129
|
+
findings: [{ arguments: names, call: 'alias', mark: String(index), path: pathOf(command) }],
|
|
130
|
+
sentence: `${commandSentence(command)} declares an alias named ${quoted(alias)}.`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
72
133
|
}
|
|
73
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* The finding for the `alias()` call that declared one alias on the Command at `path`, rebuilt from
|
|
137
|
+
* every alias the Command holds.
|
|
138
|
+
*/
|
|
139
|
+
function aliasFinding(command, alias, note) {
|
|
140
|
+
const { aliases, path } = command;
|
|
141
|
+
return { arguments: aliases, call: 'alias', mark: String(aliases.indexOf(alias)), note, path };
|
|
142
|
+
}
|
|
74
143
|
/** How the extension diagnostics of one Command's own layers name it. */
|
|
75
144
|
export function layerOf(name) {
|
|
76
145
|
return { phrase: `on ${commandSubject(name)}`, sentence: commandSentence(name) };
|
|
@@ -95,25 +164,39 @@ export function freshState(declaration) {
|
|
|
95
164
|
};
|
|
96
165
|
}
|
|
97
166
|
/**
|
|
98
|
-
* A named Command's own declaration, checked before the value exists: the name, the
|
|
99
|
-
* the core facts, and the extension values
|
|
100
|
-
* the author passed changes nothing the declaration holds.
|
|
167
|
+
* A named Command's own declaration, checked before the value exists: the name, then the one copy
|
|
168
|
+
* of the options slot, its core facts, and the extension values it carries. A later change to the
|
|
169
|
+
* options object the author passed changes nothing the declaration holds.
|
|
101
170
|
*/
|
|
102
171
|
function namedState(name, options) {
|
|
103
172
|
if (!isPortableName(name)) {
|
|
104
|
-
|
|
173
|
+
// The options are read after the name is judged, so the name's finding prints them elided.
|
|
174
|
+
throw new DeclarationError(portableName, {
|
|
175
|
+
correction: portableNameCorrection,
|
|
176
|
+
findings: [
|
|
177
|
+
constructorFinding(name, options === undefined ? undefined : spelled(elided), '0'),
|
|
178
|
+
],
|
|
179
|
+
sentence: `Command name ${quoted(name)} is invalid.`,
|
|
180
|
+
});
|
|
105
181
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
182
|
+
const slot = captureCommandOptions(name, options);
|
|
183
|
+
if (slot !== undefined) {
|
|
184
|
+
checkCommandOptions(name, slot);
|
|
185
|
+
}
|
|
186
|
+
const site = {
|
|
187
|
+
at: '1',
|
|
188
|
+
declaration: { arguments: [name, slot], call: 'new Command' },
|
|
189
|
+
subject: commandSentence(name),
|
|
190
|
+
};
|
|
109
191
|
// The facts are read in the order their diagnostics have always ranked.
|
|
110
|
-
const description = checkDescription(
|
|
111
|
-
const hidden = checkHidden(
|
|
112
|
-
const deprecated = checkDeprecated(
|
|
192
|
+
const description = checkDescription(site, slot?.description);
|
|
193
|
+
const hidden = checkHidden(site, slot?.hidden);
|
|
194
|
+
const deprecated = checkDeprecated(site, slot?.deprecated);
|
|
113
195
|
const descriptors = new Map();
|
|
114
196
|
const extensions = storeCommandLayers({
|
|
115
197
|
descriptors,
|
|
116
198
|
layers: [slot?.extensions],
|
|
199
|
+
site: { ...site, at: '1.extensions' },
|
|
117
200
|
subject: layerOf(name),
|
|
118
201
|
});
|
|
119
202
|
return {
|
|
@@ -130,16 +213,56 @@ function namedState(name, options) {
|
|
|
130
213
|
* A declaration call after `action()` is an order fault the receiver's own earlier call makes
|
|
131
214
|
* certain. The types remove the call for a TypeScript author; a JavaScript author meets it here.
|
|
132
215
|
*/
|
|
133
|
-
function checkOpen(state,
|
|
216
|
+
function checkOpen(state, late) {
|
|
134
217
|
if (state.action !== undefined) {
|
|
135
|
-
throw new DeclarationError(
|
|
218
|
+
throw new DeclarationError(declaredAfterAction, {
|
|
219
|
+
correction: late.remedy,
|
|
220
|
+
findings: [
|
|
221
|
+
{
|
|
222
|
+
arguments: late.arguments,
|
|
223
|
+
call: late.call,
|
|
224
|
+
mark: '0',
|
|
225
|
+
note: 'after action()',
|
|
226
|
+
path: pathOf(state.name),
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
sentence: `${commandSentence(state.name)} ${late.clause} after its action.`,
|
|
230
|
+
});
|
|
136
231
|
}
|
|
137
232
|
}
|
|
138
233
|
/** The remedy a late argument or option earns. */
|
|
139
234
|
const inputRemedy = 'Declare arguments and options before action().';
|
|
235
|
+
/** Where one input was declared: the call on the Command at `path` that declared it. */
|
|
236
|
+
function inputSite(name, path, input) {
|
|
237
|
+
return declaringSite(input, inputPlace(input, { global: false, path }), `${commandSentence(name)} ${input.kind} ${quoted(input.name)}`);
|
|
238
|
+
}
|
|
239
|
+
/** The finding for the `argument()` or `option()` call that declared one input, marking its name. */
|
|
240
|
+
function inputFinding(path, input, note) {
|
|
241
|
+
const site = declaringSite(input, inputPlace(input, { global: false, path }));
|
|
242
|
+
return siteFinding(site, site.named, note);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* The finding for an input whose name is invalid, marking the name. The config is read after the
|
|
246
|
+
* name is judged, so it prints elided.
|
|
247
|
+
*/
|
|
248
|
+
function nameFinding(path, input) {
|
|
249
|
+
const site = configUnread(declaringSite(input, inputPlace(input, { global: false, path })));
|
|
250
|
+
return siteFinding(site, site.named);
|
|
251
|
+
}
|
|
252
|
+
/** The scope one Command's own options compile under: its subject, and each option's call. */
|
|
253
|
+
function localScope(name, path) {
|
|
254
|
+
return { siteOf: (input) => inputSite(name, path, input), subject: commandSubject(name) };
|
|
255
|
+
}
|
|
140
256
|
/** One Command declares arguments or attaches children, whichever call came second. */
|
|
141
|
-
function placementFault(
|
|
142
|
-
return new DeclarationError(
|
|
257
|
+
function placementFault(parent, argument, child) {
|
|
258
|
+
return new DeclarationError(argumentsBesideChildren, {
|
|
259
|
+
correction: 'Move the argument into a child Command or remove the children.',
|
|
260
|
+
findings: [
|
|
261
|
+
inputFinding(parent.path, argument, 'the argument'),
|
|
262
|
+
{ ...child.placement, note: 'the child' },
|
|
263
|
+
],
|
|
264
|
+
sentence: `${commandSentence(parent.name)} declares argument "${argument.name}" and attaches child "${child.name}".`,
|
|
265
|
+
});
|
|
143
266
|
}
|
|
144
267
|
/** The options one declaration list holds, in declaration order. */
|
|
145
268
|
function optionsOf(inputs) {
|
|
@@ -163,6 +286,7 @@ function recordInput(state, input) {
|
|
|
163
286
|
const record = buildExtensions({
|
|
164
287
|
declared: input.config.extensions,
|
|
165
288
|
descriptors,
|
|
289
|
+
site: { ...inputSite(name, pathOf(name), input), at: '1.extensions' },
|
|
166
290
|
subject: {
|
|
167
291
|
phrase: `on ${commandSubject(name)} ${input.kind} "${input.name}"`,
|
|
168
292
|
sentence: `${commandSentence(name)} ${input.kind} "${input.name}"`,
|
|
@@ -177,31 +301,63 @@ function recordInput(state, input) {
|
|
|
177
301
|
*/
|
|
178
302
|
function checkArgument(state, input) {
|
|
179
303
|
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
|
-
}
|
|
304
|
+
const path = pathOf(name);
|
|
305
|
+
const command = { name, path, subject: commandSubject(name) };
|
|
184
306
|
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
|
-
}
|
|
307
|
+
checkArgumentName(command, declared, input);
|
|
188
308
|
const child = state.children[0];
|
|
189
309
|
if (child) {
|
|
190
|
-
throw placementFault(
|
|
310
|
+
throw placementFault(command, input, child);
|
|
191
311
|
}
|
|
192
312
|
const previous = declared.at(-1);
|
|
193
313
|
if (previous) {
|
|
194
|
-
checkSlotOrder(slotOf(previous), slotOf(input),
|
|
314
|
+
checkSlotOrder(slotOf(previous), slotOf(input), command);
|
|
315
|
+
}
|
|
316
|
+
const site = inputSite(name, path, input);
|
|
317
|
+
checkDescription(site, input.config.description);
|
|
318
|
+
checkNoListingFacts(site, input.config);
|
|
319
|
+
checkNoArgumentBinding(site, input.config);
|
|
320
|
+
checkDeclarations([{ input, site }]);
|
|
321
|
+
}
|
|
322
|
+
/** One argument's name answers the declared-name rule and names no argument declared before it. */
|
|
323
|
+
function checkArgumentName(command, declared, input) {
|
|
324
|
+
const { path } = command;
|
|
325
|
+
if (!isDeclaredName(input.name)) {
|
|
326
|
+
throw new DeclarationError(declaredName, {
|
|
327
|
+
correction: 'Use a nonempty name without a leading hyphen, whitespace, or "=".',
|
|
328
|
+
findings: [nameFinding(path, input)],
|
|
329
|
+
sentence: `${commandSentence(command.name)} declares an argument named ${quoted(input.name)}.`,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
const first = declared.find((entry) => entry.name === input.name);
|
|
333
|
+
if (first) {
|
|
334
|
+
throw new DeclarationError(argumentDeclaredTwice, {
|
|
335
|
+
correction: 'Remove or rename the duplicate.',
|
|
336
|
+
findings: [
|
|
337
|
+
inputFinding(path, first, 'the first declaration'),
|
|
338
|
+
inputFinding(path, input, 'the second declaration'),
|
|
339
|
+
],
|
|
340
|
+
sentence: `Argument "${input.name}" is declared more than once on ${command.subject}.`,
|
|
341
|
+
});
|
|
195
342
|
}
|
|
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
343
|
}
|
|
202
344
|
/** The declared value joins `args` under its literal name, typed by its own config. */
|
|
203
|
-
export function declareArgument(state,
|
|
204
|
-
|
|
345
|
+
export function declareArgument(state, declared) {
|
|
346
|
+
// The call's own input is judged before the receiver's state, as alias() judges its names.
|
|
347
|
+
// A name of another kind then reports as a declared name instead of failing to print in the order diagnostic.
|
|
348
|
+
// The config is read once right after its name is judged, and every later check reads that copy.
|
|
349
|
+
const { name } = state;
|
|
350
|
+
checkArgumentName({ name, path: pathOf(name), subject: commandSubject(name) }, [], declared);
|
|
351
|
+
const input = {
|
|
352
|
+
...declared,
|
|
353
|
+
config: captureInputConfig(declared, { call: 'argument', path: pathOf(name) }),
|
|
354
|
+
};
|
|
355
|
+
checkOpen(state, {
|
|
356
|
+
arguments: [input.name, input.config],
|
|
357
|
+
call: 'argument',
|
|
358
|
+
clause: `declares argument ${quoted(input.name)}`,
|
|
359
|
+
remedy: inputRemedy,
|
|
360
|
+
});
|
|
205
361
|
checkArgument(state, input);
|
|
206
362
|
const recorded = recordInput(state, input);
|
|
207
363
|
const previous = state.bind;
|
|
@@ -222,16 +378,29 @@ const noGlobals = { names: new Map(), options: new Map(), variables: new Map() }
|
|
|
222
378
|
* options also meet the Application's globals table, which a named Command meets when its subtree
|
|
223
379
|
* joins an Application.
|
|
224
380
|
*/
|
|
225
|
-
export function declareOption(state,
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
381
|
+
export function declareOption(state, declared, table = noGlobals) {
|
|
382
|
+
// The call's own input is judged before the receiver's state, as alias() judges its names.
|
|
383
|
+
// The config is read once right after its name is judged, and every later check reads that copy.
|
|
384
|
+
const path = pathOf(state.name);
|
|
385
|
+
checkOptionName(declared.name, configUnread(inputSite(state.name, path, declared)));
|
|
386
|
+
const input = {
|
|
387
|
+
...declared,
|
|
388
|
+
config: captureInputConfig(declared, { call: 'option', path }),
|
|
389
|
+
};
|
|
390
|
+
const site = inputSite(state.name, path, input);
|
|
391
|
+
checkOpen(state, {
|
|
392
|
+
arguments: [input.name, input.config],
|
|
393
|
+
call: 'option',
|
|
394
|
+
clause: `declares option ${quoted(input.name)}`,
|
|
395
|
+
remedy: inputRemedy,
|
|
396
|
+
});
|
|
397
|
+
checkDescription(site, input.config.description);
|
|
398
|
+
checkHidden(site, input.config.hidden);
|
|
399
|
+
checkDeprecated(site, input.config.deprecated);
|
|
400
|
+
checkEnvBinding(site, input.config);
|
|
232
401
|
const recorded = recordInput(state, input);
|
|
233
|
-
checkLocalOptions([...optionsOf(state.inputs), input], table,
|
|
234
|
-
checkDeclarations([input]);
|
|
402
|
+
checkLocalOptions([...optionsOf(state.inputs), input], table, localScope(state.name, pathOf(state.name)));
|
|
403
|
+
checkDeclarations([{ input, site }]);
|
|
235
404
|
const previous = state.bind;
|
|
236
405
|
return {
|
|
237
406
|
...state,
|
|
@@ -249,23 +418,40 @@ export function declareOption(state, input, table = noGlobals) {
|
|
|
249
418
|
*/
|
|
250
419
|
export function declareAlias(state, names) {
|
|
251
420
|
const { name } = state;
|
|
421
|
+
const path = pathOf(name);
|
|
252
422
|
const first = names[0];
|
|
253
423
|
if (first === undefined) {
|
|
254
|
-
throw new DeclarationError(
|
|
424
|
+
throw new DeclarationError(aliasWithoutNames, {
|
|
425
|
+
correction: 'Supply at least one name.',
|
|
426
|
+
findings: [{ arguments: [], call: 'alias', path }],
|
|
427
|
+
sentence: `${commandSentence(name)} declares an alias with no names.`,
|
|
428
|
+
});
|
|
255
429
|
}
|
|
256
430
|
// The call's own input is judged before the receiver's state.
|
|
257
431
|
// A non-string name then reports as an alias name instead of failing to print in the order diagnostic.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
432
|
+
checkAliasNames(name, names);
|
|
433
|
+
checkOpen(state, {
|
|
434
|
+
arguments: names,
|
|
435
|
+
call: 'alias',
|
|
436
|
+
clause: `declares alias "${first}"`,
|
|
437
|
+
remedy: 'Declare aliases before action().',
|
|
438
|
+
});
|
|
262
439
|
const aliases = [...state.aliases];
|
|
263
|
-
for (const alias of names) {
|
|
440
|
+
for (const [index, alias] of names.entries()) {
|
|
441
|
+
const repeated = { arguments: names, call: 'alias', mark: String(index), path };
|
|
264
442
|
if (alias === name) {
|
|
265
|
-
throw new DeclarationError(
|
|
443
|
+
throw new DeclarationError(repeatedAlias, {
|
|
444
|
+
correction: 'Remove the alias.',
|
|
445
|
+
findings: [{ ...repeated, note: 'its own name' }],
|
|
446
|
+
sentence: `${commandSentence(name)} declares alias "${alias}", which is its own name.`,
|
|
447
|
+
});
|
|
266
448
|
}
|
|
267
449
|
if (aliases.includes(alias)) {
|
|
268
|
-
throw new DeclarationError(
|
|
450
|
+
throw new DeclarationError(repeatedAlias, {
|
|
451
|
+
correction: 'Remove the repeated alias.',
|
|
452
|
+
findings: [{ ...repeated, note: 'already an alias' }],
|
|
453
|
+
sentence: `${commandSentence(name)} declares alias "${alias}" twice.`,
|
|
454
|
+
});
|
|
269
455
|
}
|
|
270
456
|
aliases.push(alias);
|
|
271
457
|
}
|
|
@@ -280,6 +466,7 @@ export function declareExtensions(state, values) {
|
|
|
280
466
|
const layer = validateLayer({
|
|
281
467
|
declared: values,
|
|
282
468
|
descriptors,
|
|
469
|
+
site: { at: '', declaration: { arguments: values, call: 'extend', path: pathOf(state.name) } },
|
|
283
470
|
subject: layerOf(state.name),
|
|
284
471
|
target: 'command',
|
|
285
472
|
});
|
|
@@ -299,7 +486,19 @@ function declaredChannel(out) {
|
|
|
299
486
|
*/
|
|
300
487
|
export function declareAction(state, handler) {
|
|
301
488
|
if (state.action !== undefined) {
|
|
302
|
-
throw new DeclarationError(
|
|
489
|
+
throw new DeclarationError(multipleActions, {
|
|
490
|
+
correction: 'Register one action.',
|
|
491
|
+
findings: [
|
|
492
|
+
{
|
|
493
|
+
arguments: [handler],
|
|
494
|
+
call: 'action',
|
|
495
|
+
mark: '0',
|
|
496
|
+
note: 'the second action',
|
|
497
|
+
path: pathOf(state.name),
|
|
498
|
+
},
|
|
499
|
+
],
|
|
500
|
+
sentence: `${commandSentence(state.name)} has multiple actions.`,
|
|
501
|
+
});
|
|
303
502
|
}
|
|
304
503
|
// The graph and the routed node stay getters, because a spread would build the graph on every run.
|
|
305
504
|
const stored = (context) => handler({
|
|
@@ -324,33 +523,71 @@ export function declareAction(state, handler) {
|
|
|
324
523
|
* here; the empty record and the default wait for attach, because `views()` can still add keys.
|
|
325
524
|
*/
|
|
326
525
|
export function declareResult(state, kind, declaration) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
526
|
+
const call = {
|
|
527
|
+
arguments: callArguments(declaration),
|
|
528
|
+
kind,
|
|
529
|
+
views: recordOf(declaration, 'views'),
|
|
530
|
+
};
|
|
531
|
+
checkOpen(state, {
|
|
532
|
+
arguments: call.arguments,
|
|
533
|
+
call: resultCallName(call),
|
|
534
|
+
clause: 'declares its result',
|
|
535
|
+
remedy: 'Declare result() or rows() before action().',
|
|
536
|
+
});
|
|
537
|
+
const results = [...state.results, call];
|
|
538
|
+
mergeResult({ name: state.name, path: pathOf(state.name) }, results);
|
|
330
539
|
return { ...state, results };
|
|
331
540
|
}
|
|
332
541
|
/** A `views()` call reshapes views and closes nothing, so it is never a late declaration. */
|
|
333
542
|
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);
|
|
543
|
+
const results = [...state.results, viewsCall(replacements, options)];
|
|
544
|
+
mergeResult({ name: state.name, path: pathOf(state.name) }, results);
|
|
339
545
|
return { ...state, results };
|
|
340
546
|
}
|
|
547
|
+
/** One `views()` call as the results lane records it. */
|
|
548
|
+
function viewsCall(replacements, options) {
|
|
549
|
+
return {
|
|
550
|
+
arguments: callArguments(replacements, options),
|
|
551
|
+
default: recordOf(options, 'default'),
|
|
552
|
+
kind: 'views',
|
|
553
|
+
views: replacements,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
341
556
|
/** One property of an authoring argument, read defensively: the rules report whatever it holds. */
|
|
342
557
|
function recordOf(declaration, key) {
|
|
343
558
|
return isPlainObject(declaration) ? declaration[key] : undefined;
|
|
344
559
|
}
|
|
560
|
+
/** The authoring call one results-lane call was made through. */
|
|
561
|
+
function resultCallName(call) {
|
|
562
|
+
return call.kind === 'value' ? 'result' : call.kind;
|
|
563
|
+
}
|
|
564
|
+
/** The finding for one results-lane call on the Command at `path`, marking one of its arguments. */
|
|
565
|
+
function resultFinding(path, call, mark) {
|
|
566
|
+
const finding = { arguments: call.arguments, call: resultCallName(call), mark: mark.at, path };
|
|
567
|
+
return mark.note === undefined ? finding : { ...finding, note: mark.note };
|
|
568
|
+
}
|
|
569
|
+
/** Where one views entry sits among its call's arguments: in `views` for a declaration, or first. */
|
|
570
|
+
function entryMark(call, key) {
|
|
571
|
+
return call.kind === 'views' ? `0.${key}` : `0.views.${key}`;
|
|
572
|
+
}
|
|
345
573
|
/** The parent a declaration is, as attach reads it. */
|
|
346
574
|
function parentOf(state) {
|
|
347
575
|
return {
|
|
348
|
-
argument: state.inputs.find((input) => input.kind === 'argument')
|
|
576
|
+
argument: state.inputs.find((input) => input.kind === 'argument'),
|
|
349
577
|
children: state.children,
|
|
350
578
|
hasAction: state.action !== undefined,
|
|
351
579
|
name: state.name,
|
|
580
|
+
path: pathOf(state.name),
|
|
352
581
|
};
|
|
353
582
|
}
|
|
583
|
+
/** The path one child sits at under its parent. */
|
|
584
|
+
function childPath(parent, child) {
|
|
585
|
+
return [...parent.path, child];
|
|
586
|
+
}
|
|
587
|
+
/** The call that attached one child, noted with the child's name. */
|
|
588
|
+
function childFinding(entry) {
|
|
589
|
+
return { ...entry.placement, note: `child "${entry.name}"` };
|
|
590
|
+
}
|
|
354
591
|
/**
|
|
355
592
|
* One parent's namespace, which every canonical name and alias under it shares. The rule reads the
|
|
356
593
|
* same whichever sibling the author attached first.
|
|
@@ -358,21 +595,41 @@ function parentOf(state) {
|
|
|
358
595
|
function checkSiblings(parent, child) {
|
|
359
596
|
const sentence = commandSentence(parent.name);
|
|
360
597
|
const { children } = parent;
|
|
361
|
-
|
|
362
|
-
|
|
598
|
+
const aliased = (entry, alias) => aliasFinding({ aliases: entry.node.declared.aliases, path: childPath(parent, entry.name) }, alias, `alias of child "${entry.name}"`);
|
|
599
|
+
const correction = 'Rename or remove one.';
|
|
600
|
+
const twin = children.find((entry) => entry.name === child.name);
|
|
601
|
+
if (twin) {
|
|
602
|
+
throw new DeclarationError(siblingNameTaken, {
|
|
603
|
+
correction,
|
|
604
|
+
findings: [childFinding(twin), childFinding(child)],
|
|
605
|
+
sentence: `${sentence} attaches two children named "${child.name}".`,
|
|
606
|
+
});
|
|
363
607
|
}
|
|
364
608
|
for (const alias of child.node.declared.aliases) {
|
|
365
|
-
|
|
366
|
-
|
|
609
|
+
const holder = children.find((entry) => entry.name === alias);
|
|
610
|
+
if (holder) {
|
|
611
|
+
throw new DeclarationError(siblingNameTaken, {
|
|
612
|
+
correction,
|
|
613
|
+
findings: [childFinding(holder), aliased(child, alias)],
|
|
614
|
+
sentence: `${sentence} attaches child "${child.name}" with alias "${alias}", which is also the name of child "${alias}".`,
|
|
615
|
+
});
|
|
367
616
|
}
|
|
368
617
|
const owner = children.find((entry) => entry.node.declared.aliases.includes(alias));
|
|
369
618
|
if (owner) {
|
|
370
|
-
throw new DeclarationError(
|
|
619
|
+
throw new DeclarationError(siblingNameTaken, {
|
|
620
|
+
correction,
|
|
621
|
+
findings: [aliased(owner, alias), aliased(child, alias)],
|
|
622
|
+
sentence: `${sentence} attaches child "${child.name}" with alias "${alias}", which is also an alias of child "${owner.name}".`,
|
|
623
|
+
});
|
|
371
624
|
}
|
|
372
625
|
}
|
|
373
|
-
const
|
|
374
|
-
if (
|
|
375
|
-
throw new DeclarationError(
|
|
626
|
+
const owner = children.find((entry) => entry.node.declared.aliases.includes(child.name));
|
|
627
|
+
if (owner) {
|
|
628
|
+
throw new DeclarationError(siblingNameTaken, {
|
|
629
|
+
correction,
|
|
630
|
+
findings: [aliased(owner, child.name), childFinding(child)],
|
|
631
|
+
sentence: `${sentence} attaches child "${owner.name}" with alias "${child.name}", which is also the name of child "${child.name}".`,
|
|
632
|
+
});
|
|
376
633
|
}
|
|
377
634
|
}
|
|
378
635
|
/**
|
|
@@ -381,7 +638,11 @@ function checkSiblings(parent, child) {
|
|
|
381
638
|
*/
|
|
382
639
|
function checkNesting(parent, child, level) {
|
|
383
640
|
if (level >= nestingCap.depth && child.node.declared.children.length > 0) {
|
|
384
|
-
throw new DeclarationError(
|
|
641
|
+
throw new DeclarationError(nestingDepth, {
|
|
642
|
+
correction: `Nest Commands at most ${nestingCap.words} levels below the root.`,
|
|
643
|
+
findings: [{ ...child.placement, note: 'has children of its own' }],
|
|
644
|
+
sentence: `${commandSentence(parent)} attaches child "${child.name}", which has children of its own.`,
|
|
645
|
+
});
|
|
385
646
|
}
|
|
386
647
|
}
|
|
387
648
|
/**
|
|
@@ -389,14 +650,23 @@ function checkNesting(parent, child, level) {
|
|
|
389
650
|
* children. A group with no children receives an invocation no handler can answer, and a local
|
|
390
651
|
* option on a group reaches no handler either, because locals never inherit.
|
|
391
652
|
*/
|
|
392
|
-
function checkGroup(state) {
|
|
653
|
+
function checkGroup(state, place) {
|
|
393
654
|
const { name } = state;
|
|
394
655
|
if (state.children.length === 0) {
|
|
395
|
-
|
|
656
|
+
const { placement } = place;
|
|
657
|
+
throw new DeclarationError(commandWithoutAction, {
|
|
658
|
+
correction: 'Register an action.',
|
|
659
|
+
findings: placement === undefined ? [] : [{ ...placement, note: 'no action and no children' }],
|
|
660
|
+
sentence: `${commandSentence(name)} has no action.`,
|
|
661
|
+
});
|
|
396
662
|
}
|
|
397
663
|
const option = state.inputs.find((input) => input.kind === 'option');
|
|
398
664
|
if (option) {
|
|
399
|
-
throw new DeclarationError(
|
|
665
|
+
throw new DeclarationError(groupOption, {
|
|
666
|
+
correction: 'Register an action or remove the option.',
|
|
667
|
+
findings: [inputFinding(place.path, option, 'no action reads it')],
|
|
668
|
+
sentence: `${commandSentence(name)} declares option ${quoted(option.name)} but registers no action to receive it.`,
|
|
669
|
+
});
|
|
400
670
|
}
|
|
401
671
|
}
|
|
402
672
|
/**
|
|
@@ -404,10 +674,10 @@ function checkGroup(state) {
|
|
|
404
674
|
* result needs an action, its merged views record names at least one view and its default, and a
|
|
405
675
|
* Command without an action is a group. It answers with the resolved result.
|
|
406
676
|
*/
|
|
407
|
-
function checkFinished(declared, hasAction) {
|
|
408
|
-
const result = buildResult(declared, hasAction);
|
|
677
|
+
function checkFinished(declared, hasAction, place) {
|
|
678
|
+
const result = buildResult(declared, hasAction, place.path);
|
|
409
679
|
if (!hasAction) {
|
|
410
|
-
checkGroup(declared);
|
|
680
|
+
checkGroup(declared, place);
|
|
411
681
|
}
|
|
412
682
|
return result;
|
|
413
683
|
}
|
|
@@ -415,18 +685,23 @@ function checkFinished(declared, hasAction) {
|
|
|
415
685
|
* The one attach operation `Command.command()`, `Application.command()`, and a plugin's
|
|
416
686
|
* `commands` list share. A Command is an immutable value, so the child is final here: it is checked
|
|
417
687
|
* 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.
|
|
688
|
+
* the shallowest level the parent can sit at. The placement is the call that attached it, which a
|
|
689
|
+
* finding about the child as a whole marks.
|
|
419
690
|
*/
|
|
420
|
-
export function attach(parent, node) {
|
|
691
|
+
export function attach(parent, node, placement) {
|
|
421
692
|
const { name } = node;
|
|
693
|
+
const child = { name, node, placement };
|
|
422
694
|
if (parent.hasAction) {
|
|
423
|
-
throw new DeclarationError(
|
|
695
|
+
throw new DeclarationError(declaredAfterAction, {
|
|
696
|
+
correction: 'Attach children before action().',
|
|
697
|
+
findings: [{ ...placement, note: 'after action()' }],
|
|
698
|
+
sentence: `${commandSentence(parent.name)} attaches child "${name}" after its action.`,
|
|
699
|
+
});
|
|
424
700
|
}
|
|
425
701
|
if (parent.argument !== undefined) {
|
|
426
|
-
throw placementFault(parent
|
|
702
|
+
throw placementFault(parent, parent.argument, child);
|
|
427
703
|
}
|
|
428
|
-
checkFinished(node.declared, node.hasAction);
|
|
429
|
-
const child = { name, node };
|
|
704
|
+
checkFinished(node.declared, node.hasAction, { path: childPath(parent, name), placement });
|
|
430
705
|
checkSiblings(parent, child);
|
|
431
706
|
checkNesting(parent.name, child, parentLevel(parent.name) + 1);
|
|
432
707
|
return child;
|
|
@@ -435,13 +710,18 @@ export function attach(parent, node) {
|
|
|
435
710
|
export function childNode(parent, child) {
|
|
436
711
|
const node = commandNode(child);
|
|
437
712
|
if (!node) {
|
|
438
|
-
throw new DeclarationError(
|
|
713
|
+
throw new DeclarationError(notACommand, {
|
|
714
|
+
correction: 'Attach the value returned by new Command(name).',
|
|
715
|
+
findings: [{ arguments: [child], call: 'command', mark: '0', path: pathOf(parent) }],
|
|
716
|
+
sentence: `${commandSentence(parent)} attaches a value that is not a Command.`,
|
|
717
|
+
});
|
|
439
718
|
}
|
|
440
719
|
return node;
|
|
441
720
|
}
|
|
442
721
|
/** Attaching is a declaration call too, so the receiver keeps the children it already had. */
|
|
443
722
|
function attachChild(state, child) {
|
|
444
|
-
const
|
|
723
|
+
const node = childNode(state.name, child);
|
|
724
|
+
const attached = attach(parentOf(state), node, commandPlacement(pathOf(state.name), node.name));
|
|
445
725
|
return { ...state, children: [...state.children, attached] };
|
|
446
726
|
}
|
|
447
727
|
/**
|
|
@@ -453,34 +733,47 @@ function attachChild(state, child) {
|
|
|
453
733
|
* here fires only once the cap rises: attach reads one level, and only this walk knows each level.
|
|
454
734
|
*/
|
|
455
735
|
function joinSubtree(scope, child, { level, parent }) {
|
|
456
|
-
const { name, node } = child;
|
|
457
|
-
checkNesting(parent, child, level);
|
|
458
|
-
|
|
459
|
-
|
|
736
|
+
const { name, node, placement } = child;
|
|
737
|
+
checkNesting(parent.name, child, level);
|
|
738
|
+
const owner = scope.owners.get(node);
|
|
739
|
+
if (owner) {
|
|
740
|
+
throw new DeclarationError(commandAttachedTwice, {
|
|
741
|
+
correction: 'Attach a Command value at one point; create a new Command for each placement.',
|
|
742
|
+
findings: [
|
|
743
|
+
{ ...owner.placement, note: 'the first placement' },
|
|
744
|
+
{ ...placement, note: 'the second placement' },
|
|
745
|
+
],
|
|
746
|
+
sentence: `${commandSentence(parent.name)} attaches child "${name}", which ${commandSubject(owner.name)} also attaches.`,
|
|
747
|
+
});
|
|
460
748
|
}
|
|
461
|
-
scope.owners.set(node, parent);
|
|
462
|
-
for (const descriptor of node.declared.descriptors
|
|
463
|
-
registerDescriptor(scope.descriptors, descriptor);
|
|
749
|
+
scope.owners.set(node, { name: parent.name, placement });
|
|
750
|
+
for (const [key, descriptor] of node.declared.descriptors) {
|
|
751
|
+
registerDescriptor(scope.descriptors, descriptor, { identity: key });
|
|
464
752
|
}
|
|
465
|
-
|
|
753
|
+
const path = childPath(parent, name);
|
|
754
|
+
checkLocalOptions(optionsOf(node.declared.inputs), scope.table, localScope(name, path));
|
|
466
755
|
for (const entry of node.declared.children) {
|
|
467
|
-
|
|
756
|
+
// A nested child's own placement knew its parent alone, so the walk places it under the root.
|
|
757
|
+
const rooted = { ...entry, placement: commandPlacement(path, entry.name) };
|
|
758
|
+
joinSubtree(scope, rooted, { level: level + 1, parent: { name, path } });
|
|
468
759
|
}
|
|
469
760
|
}
|
|
470
761
|
/**
|
|
471
762
|
* Attaches one child to an Application's root and walks the subtree it brings, once. The scope's
|
|
472
763
|
* registers are copies: the caller commits the owners, and the returned root holds the descriptors.
|
|
473
764
|
*/
|
|
474
|
-
export function attachToRoot(state,
|
|
475
|
-
const
|
|
476
|
-
|
|
765
|
+
export function attachToRoot(state, child, scope) {
|
|
766
|
+
const parent = parentOf(state);
|
|
767
|
+
const attached = attach(parent, child.node, child.placement);
|
|
768
|
+
joinSubtree(scope, attached, { level: parentLevel(state.name) + 1, parent });
|
|
477
769
|
return { ...state, children: [...state.children, attached], descriptors: scope.descriptors };
|
|
478
770
|
}
|
|
479
771
|
/** Every attached Command's local options against a globals table that has just grown. */
|
|
480
|
-
function checkAttachedOptions(table, children) {
|
|
772
|
+
function checkAttachedOptions(table, parent, children) {
|
|
481
773
|
for (const { name, node } of children) {
|
|
482
|
-
|
|
483
|
-
|
|
774
|
+
const path = childPath(parent, name);
|
|
775
|
+
checkLocalOptions(optionsOf(node.declared.inputs), table, localScope(name, path));
|
|
776
|
+
checkAttachedOptions(table, { name, path }, node.declared.children);
|
|
484
777
|
}
|
|
485
778
|
}
|
|
486
779
|
/**
|
|
@@ -488,42 +781,54 @@ function checkAttachedOptions(table, children) {
|
|
|
488
781
|
* grown. The table's new option reads as the other side of any collision.
|
|
489
782
|
*/
|
|
490
783
|
export function checkDeclaredOptions(state, table) {
|
|
491
|
-
|
|
492
|
-
|
|
784
|
+
const place = { name: state.name, path: pathOf(state.name) };
|
|
785
|
+
checkLocalOptions(optionsOf(state.inputs), table, localScope(place.name, place.path));
|
|
786
|
+
checkAttachedOptions(table, place, state.children);
|
|
493
787
|
}
|
|
494
788
|
/** A variadic or optional slot ends the positional list, so nothing may follow either one. */
|
|
495
|
-
function checkSlotOrder(slot, next,
|
|
789
|
+
function checkSlotOrder(slot, next, command) {
|
|
790
|
+
const { path, subject } = command;
|
|
791
|
+
const after = inputFinding(path, next.input, 'the argument after it');
|
|
496
792
|
if (slot.variadic) {
|
|
497
|
-
throw new DeclarationError(
|
|
793
|
+
throw new DeclarationError(variadicArgumentLast, {
|
|
794
|
+
correction: 'Declare the variadic argument last.',
|
|
795
|
+
findings: [inputFinding(path, slot.input, 'the variadic argument'), after],
|
|
796
|
+
sentence: `Argument "${slot.input.name}" is variadic and precedes argument "${next.input.name}" on ${subject}.`,
|
|
797
|
+
});
|
|
498
798
|
}
|
|
499
799
|
if (!slot.required) {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
800
|
+
const findings = [inputFinding(path, slot.input, 'the optional argument'), after];
|
|
801
|
+
throw new DeclarationError(optionalArgumentLast, next.required
|
|
802
|
+
? {
|
|
803
|
+
correction: 'Declare optional arguments after required ones.',
|
|
804
|
+
findings,
|
|
805
|
+
sentence: `Argument "${slot.input.name}" is optional and precedes required argument "${next.input.name}" on ${subject}.`,
|
|
806
|
+
}
|
|
807
|
+
: {
|
|
808
|
+
correction: 'Declare an optional argument last.',
|
|
809
|
+
findings,
|
|
810
|
+
sentence: `Argument "${next.input.name}" follows optional argument "${slot.input.name}" on ${subject}.`,
|
|
811
|
+
});
|
|
503
812
|
}
|
|
504
813
|
}
|
|
505
814
|
/**
|
|
506
815
|
* The positional slots one declaration holds. Every authored argument answered these rules at its
|
|
507
816
|
* own call, so they throw here only for an argument a lifecycle hook declared.
|
|
508
817
|
*/
|
|
509
|
-
function collectArguments(state,
|
|
818
|
+
function collectArguments(state, command) {
|
|
510
819
|
const slots = [];
|
|
511
|
-
const
|
|
820
|
+
const declared = [];
|
|
821
|
+
const judged = { ...command, subject: commandSubject(command.name) };
|
|
512
822
|
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);
|
|
823
|
+
checkArgumentName(judged, declared, input);
|
|
824
|
+
declared.push(input);
|
|
520
825
|
slots.push(slotOf(input));
|
|
521
826
|
}
|
|
522
827
|
for (let index = 0; index + 1 < slots.length; index += 1) {
|
|
523
828
|
const slot = slots[index];
|
|
524
829
|
const next = slots[index + 1];
|
|
525
830
|
if (slot && next) {
|
|
526
|
-
checkSlotOrder(slot, next,
|
|
831
|
+
checkSlotOrder(slot, next, judged);
|
|
527
832
|
}
|
|
528
833
|
}
|
|
529
834
|
return slots;
|
|
@@ -564,23 +869,36 @@ function recordEntries(record) {
|
|
|
564
869
|
* One `views` entry under the unit its declaration named, read back as the shape its own functions
|
|
565
870
|
* name. The two shapes are exclusive, and a row view answers a rows declaration alone.
|
|
566
871
|
*/
|
|
567
|
-
function resultView(
|
|
568
|
-
const {
|
|
872
|
+
function resultView(kind, site, entry) {
|
|
873
|
+
const { call, key, path, sentence } = site;
|
|
874
|
+
const findings = [resultFinding(path, call, { at: entryMark(call, key) })];
|
|
569
875
|
const whole = isWholeView(entry);
|
|
570
876
|
const row = isRowView(entry);
|
|
571
877
|
if (whole && row) {
|
|
572
|
-
throw new DeclarationError(
|
|
878
|
+
throw new DeclarationError(viewShape, {
|
|
879
|
+
correction: 'Supply one of the two.',
|
|
880
|
+
findings,
|
|
881
|
+
sentence: `${sentence} names view ${quoted(key)} with render and row.`,
|
|
882
|
+
});
|
|
573
883
|
}
|
|
574
884
|
if (row) {
|
|
575
885
|
if (kind === 'value') {
|
|
576
|
-
throw new DeclarationError(
|
|
886
|
+
throw new DeclarationError(rowViewOnValue, {
|
|
887
|
+
correction: 'Supply a view with render, or declare the result with rows().',
|
|
888
|
+
findings,
|
|
889
|
+
sentence: `${sentence} names row view ${quoted(key)} on a value result.`,
|
|
890
|
+
});
|
|
577
891
|
}
|
|
578
892
|
return entry;
|
|
579
893
|
}
|
|
580
894
|
if (whole) {
|
|
581
895
|
return entry;
|
|
582
896
|
}
|
|
583
|
-
throw new DeclarationError(
|
|
897
|
+
throw new DeclarationError(viewShape, {
|
|
898
|
+
correction: 'Supply a view with render or a row view with row.',
|
|
899
|
+
findings,
|
|
900
|
+
sentence: `${sentence} names view ${quoted(key)} with a value that is not a view.`,
|
|
901
|
+
});
|
|
584
902
|
}
|
|
585
903
|
/**
|
|
586
904
|
* Every rule a results-lane call can judge on its own, applied to one Command's calls in the order
|
|
@@ -588,18 +906,31 @@ function resultView(declaration, name, entry) {
|
|
|
588
906
|
* place and appends a new one, so each name keeps the position the call that first named it gave
|
|
589
907
|
* it. A `default` once named persists through later calls that name none.
|
|
590
908
|
*/
|
|
591
|
-
function mergeResult(
|
|
592
|
-
const
|
|
909
|
+
function mergeResult(command, results) {
|
|
910
|
+
const { path } = command;
|
|
911
|
+
const sentence = commandSentence(command.name);
|
|
593
912
|
const declarations = results.filter((call) => call.kind !== 'views');
|
|
594
|
-
|
|
595
|
-
|
|
913
|
+
const [declaration, second] = declarations;
|
|
914
|
+
if (declaration && second) {
|
|
915
|
+
throw new DeclarationError(multipleResults, {
|
|
916
|
+
correction: 'Declare one result() or rows() call.',
|
|
917
|
+
findings: [
|
|
918
|
+
resultFinding(path, declaration, { at: '0', note: 'the first result' }),
|
|
919
|
+
resultFinding(path, second, { at: '0', note: 'the second result' }),
|
|
920
|
+
],
|
|
921
|
+
sentence: `${sentence} declares two results.`,
|
|
922
|
+
});
|
|
596
923
|
}
|
|
597
|
-
const declaration = declarations[0];
|
|
598
924
|
// A `views()` call reshapes a result's views, so one with no result reshapes nothing.
|
|
599
925
|
// The types publish the call where a result is carried, so this reaches a JavaScript author.
|
|
600
926
|
if (!declaration) {
|
|
601
|
-
|
|
602
|
-
|
|
927
|
+
const [reshape] = results;
|
|
928
|
+
if (reshape) {
|
|
929
|
+
throw new DeclarationError(viewsWithoutResult, {
|
|
930
|
+
correction: 'Declare result() or rows() before action().',
|
|
931
|
+
findings: [resultFinding(path, reshape, { at: '0' })],
|
|
932
|
+
sentence: `${sentence} reshapes its views and declares no result.`,
|
|
933
|
+
});
|
|
603
934
|
}
|
|
604
935
|
return undefined;
|
|
605
936
|
}
|
|
@@ -607,40 +938,57 @@ function mergeResult(name, results) {
|
|
|
607
938
|
let selected = undefined;
|
|
608
939
|
for (const call of results) {
|
|
609
940
|
for (const [key, entry] of recordEntries(call.views)) {
|
|
610
|
-
const view = resultView(
|
|
941
|
+
const view = resultView(declaration.kind, { call, key, path, sentence }, entry);
|
|
611
942
|
if (!isViewName(key)) {
|
|
612
|
-
throw new DeclarationError(
|
|
943
|
+
throw new DeclarationError(viewName, {
|
|
944
|
+
correction: 'Use a nonempty name without whitespace, a leading hyphen, or "=", and not a number.',
|
|
945
|
+
findings: [resultFinding(path, call, { at: entryMark(call, key) })],
|
|
946
|
+
sentence: `${sentence} names view ${quoted(key)}.`,
|
|
947
|
+
});
|
|
613
948
|
}
|
|
614
949
|
views.set(key, view);
|
|
615
950
|
}
|
|
616
|
-
|
|
617
|
-
|
|
951
|
+
const key = call.kind === 'views' ? selectedKey(call.default) : undefined;
|
|
952
|
+
if (key !== undefined) {
|
|
953
|
+
selected = { call, key };
|
|
618
954
|
}
|
|
619
955
|
}
|
|
620
|
-
return { kind: declaration.kind, selected, views };
|
|
956
|
+
return { declaration, kind: declaration.kind, selected, views };
|
|
621
957
|
}
|
|
622
958
|
/**
|
|
623
959
|
* The finished result: the merged record, which needs an action, at least one view, and a default
|
|
624
960
|
* that names one of them. The first key answers until one is named.
|
|
625
961
|
*/
|
|
626
|
-
function buildResult(declared, hasAction) {
|
|
627
|
-
const merged = mergeResult(declared.name, declared.results);
|
|
962
|
+
function buildResult(declared, hasAction, path) {
|
|
963
|
+
const merged = mergeResult({ name: declared.name, path }, declared.results);
|
|
628
964
|
if (!merged) {
|
|
629
965
|
return undefined;
|
|
630
966
|
}
|
|
631
967
|
const sentence = commandSentence(declared.name);
|
|
968
|
+
const { declaration, selected, views } = merged;
|
|
632
969
|
if (!hasAction) {
|
|
633
|
-
throw new DeclarationError(
|
|
970
|
+
throw new DeclarationError(resultWithoutAction, {
|
|
971
|
+
correction: 'Register an action or remove the result.',
|
|
972
|
+
findings: [resultFinding(path, declaration, { at: '0' })],
|
|
973
|
+
sentence: `${sentence} declares a result and no action.`,
|
|
974
|
+
});
|
|
634
975
|
}
|
|
635
|
-
const { selected, views } = merged;
|
|
636
976
|
const first = views.keys().next();
|
|
637
977
|
if (first.done === true) {
|
|
638
|
-
throw new DeclarationError(
|
|
978
|
+
throw new DeclarationError(resultWithoutViews, {
|
|
979
|
+
correction: 'Name at least one view.',
|
|
980
|
+
findings: [resultFinding(path, declaration, { at: '0.views' })],
|
|
981
|
+
sentence: `${sentence} declares a result with no views.`,
|
|
982
|
+
});
|
|
639
983
|
}
|
|
640
|
-
if (selected !== undefined && !views.has(selected)) {
|
|
641
|
-
throw new DeclarationError(
|
|
984
|
+
if (selected !== undefined && !views.has(selected.key)) {
|
|
985
|
+
throw new DeclarationError(unknownDefaultView, {
|
|
986
|
+
correction: 'Name the view or select a named one.',
|
|
987
|
+
findings: [resultFinding(path, selected.call, { at: '1.default' })],
|
|
988
|
+
sentence: `${sentence} selects default view ${quoted(selected.key)}, which it does not name.`,
|
|
989
|
+
});
|
|
642
990
|
}
|
|
643
|
-
return { default: selected ?? first.value, kind: merged.kind, views };
|
|
991
|
+
return { default: selected?.key ?? first.value, kind: merged.kind, views };
|
|
644
992
|
}
|
|
645
993
|
/** The values one build made, so a value a hook returns is one of them and never a forged shape. */
|
|
646
994
|
const attachments = new WeakMap();
|
|
@@ -663,7 +1011,7 @@ class AttachedCommandValue {
|
|
|
663
1011
|
#result;
|
|
664
1012
|
constructor(state) {
|
|
665
1013
|
this.#state = state;
|
|
666
|
-
this.#result = resultNode(buildResult(state.declared, state.hasAction));
|
|
1014
|
+
this.#result = resultNode(buildResult(state.declared, state.hasAction, state.path));
|
|
667
1015
|
attachments.set(this, state);
|
|
668
1016
|
Object.freeze(this);
|
|
669
1017
|
}
|
|
@@ -690,17 +1038,13 @@ class AttachedCommandValue {
|
|
|
690
1038
|
return publishStore(this.#state.extensions);
|
|
691
1039
|
}
|
|
692
1040
|
argument(name, config) {
|
|
693
|
-
return this.#declare({ config
|
|
1041
|
+
return this.#declare({ config, kind: 'argument', name });
|
|
694
1042
|
}
|
|
695
1043
|
option(name, config) {
|
|
696
|
-
return this.#declare({ config
|
|
1044
|
+
return this.#declare({ config, kind: 'option', name });
|
|
697
1045
|
}
|
|
698
1046
|
views(replacements, options) {
|
|
699
|
-
const call =
|
|
700
|
-
default: recordOf(options, 'default'),
|
|
701
|
-
kind: 'views',
|
|
702
|
-
views: replacements,
|
|
703
|
-
};
|
|
1047
|
+
const call = viewsCall(replacements, options);
|
|
704
1048
|
const { declared } = this.#state;
|
|
705
1049
|
return this.#derive({ call, kind: 'views' }, { ...declared, results: [...declared.results, call] });
|
|
706
1050
|
}
|
|
@@ -714,6 +1058,7 @@ class AttachedCommandValue {
|
|
|
714
1058
|
const layer = validateLayer({
|
|
715
1059
|
declared: values,
|
|
716
1060
|
descriptors: registry,
|
|
1061
|
+
site: { at: '', declaration: { arguments: values, call: 'extend', path: state.path } },
|
|
717
1062
|
subject: state.subject,
|
|
718
1063
|
target: 'command',
|
|
719
1064
|
});
|
|
@@ -724,8 +1069,22 @@ class AttachedCommandValue {
|
|
|
724
1069
|
});
|
|
725
1070
|
}
|
|
726
1071
|
/** One input the running hook declared, which the Command's own names now hold. */
|
|
727
|
-
#declare(
|
|
1072
|
+
#declare(raw) {
|
|
728
1073
|
const { declared, identity } = this.#state;
|
|
1074
|
+
const { path } = this.#state;
|
|
1075
|
+
// The name is judged before the config, as Command and Application judge it.
|
|
1076
|
+
if (raw.kind === 'argument') {
|
|
1077
|
+
const { name } = declared;
|
|
1078
|
+
checkArgumentName({ name, path, subject: commandSubject(name) }, [], raw);
|
|
1079
|
+
}
|
|
1080
|
+
else {
|
|
1081
|
+
checkOptionName(raw.name, configUnread(inputSite(declared.name, path, raw)));
|
|
1082
|
+
}
|
|
1083
|
+
// Each kind captures its own config, so the input keeps the pairing its kind declares.
|
|
1084
|
+
const place = { call: raw.kind, path };
|
|
1085
|
+
const input = raw.kind === 'argument'
|
|
1086
|
+
? { ...raw, config: captureInputConfig(raw, place) }
|
|
1087
|
+
: { ...raw, config: captureInputConfig(raw, place) };
|
|
729
1088
|
return this.#derive({ identity, input, kind: 'input' }, { ...declared, inputs: [...declared.inputs, input] });
|
|
730
1089
|
}
|
|
731
1090
|
#derive(call, declared) {
|
|
@@ -734,9 +1093,13 @@ class AttachedCommandValue {
|
|
|
734
1093
|
}
|
|
735
1094
|
}
|
|
736
1095
|
_a = AttachedCommandValue;
|
|
737
|
-
/** The
|
|
738
|
-
function
|
|
739
|
-
return
|
|
1096
|
+
/** The finding for the hook one plugin declared, as `plugin(identity, { onCommandAttach })`. */
|
|
1097
|
+
function hookFinding(identity, hook) {
|
|
1098
|
+
return {
|
|
1099
|
+
arguments: [identity, { onCommandAttach: hook }],
|
|
1100
|
+
call: 'plugin',
|
|
1101
|
+
mark: '1.onCommandAttach',
|
|
1102
|
+
};
|
|
740
1103
|
}
|
|
741
1104
|
/** One hook's own call, whose failure is the plugin's, and whose own report is its own. */
|
|
742
1105
|
function callHook(value, hook, named) {
|
|
@@ -748,7 +1111,11 @@ function callHook(value, hook, named) {
|
|
|
748
1111
|
if (error instanceof DeclarationError) {
|
|
749
1112
|
throw error;
|
|
750
1113
|
}
|
|
751
|
-
throw new DeclarationError(
|
|
1114
|
+
throw new DeclarationError(brokenAttachHook, {
|
|
1115
|
+
correction: 'Return the value the hook received or a value derived from it, and throw only a DeclarationError from the hook.',
|
|
1116
|
+
findings: [hookFinding(named.identity, hook)],
|
|
1117
|
+
sentence: `Plugin ${quoted(named.identity)} failed in onCommandAttach for ${named.subject}: ${asSentence(reasonOf(error))}`,
|
|
1118
|
+
}, { cause: error });
|
|
752
1119
|
}
|
|
753
1120
|
}
|
|
754
1121
|
/**
|
|
@@ -759,7 +1126,11 @@ function callHook(value, hook, named) {
|
|
|
759
1126
|
function attachOnce(value, hook, named) {
|
|
760
1127
|
const state = attachments.get(callHook(value, hook, named));
|
|
761
1128
|
if (!state || state.lineage !== named.lineage) {
|
|
762
|
-
throw new DeclarationError(
|
|
1129
|
+
throw new DeclarationError(brokenAttachHook, {
|
|
1130
|
+
correction: 'Return the value it received or a value derived from it.',
|
|
1131
|
+
findings: [hookFinding(named.identity, hook)],
|
|
1132
|
+
sentence: `Plugin ${quoted(named.identity)} returned a value that is not the attached Command from onCommandAttach for ${named.subject}.`,
|
|
1133
|
+
});
|
|
763
1134
|
}
|
|
764
1135
|
return state;
|
|
765
1136
|
}
|
|
@@ -817,10 +1188,24 @@ function applyAttachCalls(state, calls) {
|
|
|
817
1188
|
}
|
|
818
1189
|
return next;
|
|
819
1190
|
}
|
|
1191
|
+
/**
|
|
1192
|
+
* The reason rule one name collision breaks.
|
|
1193
|
+
* Two options or two arguments break the rule the application's own collision of the pair breaks.
|
|
1194
|
+
* An argument and an option under one name break name-shared-across-kinds, which only a hook reaches.
|
|
1195
|
+
*/
|
|
1196
|
+
function nameCollisionRule(declared, held) {
|
|
1197
|
+
if (declared !== held) {
|
|
1198
|
+
return nameSharedAcrossKinds;
|
|
1199
|
+
}
|
|
1200
|
+
return declared === 'option' ? optionDeclaredTwice : argumentDeclaredTwice;
|
|
1201
|
+
}
|
|
820
1202
|
/** The clause and the remedy an input another plugin's hook already declared earns. */
|
|
821
|
-
function hookClause(
|
|
1203
|
+
function hookClause(earlier, path) {
|
|
1204
|
+
const { identity, input } = earlier;
|
|
822
1205
|
return {
|
|
823
|
-
clause: `an ${kind} plugin
|
|
1206
|
+
clause: `an ${input.kind} plugin ${quoted(identity)} declared through onCommandAttach`,
|
|
1207
|
+
held: inputFinding(path, input, declarerNote(identity)),
|
|
1208
|
+
kind: input.kind,
|
|
824
1209
|
remedy: 'Install one of them.',
|
|
825
1210
|
};
|
|
826
1211
|
}
|
|
@@ -842,6 +1227,25 @@ function attachedRemedy(target) {
|
|
|
842
1227
|
}
|
|
843
1228
|
return 'Install one of them.';
|
|
844
1229
|
}
|
|
1230
|
+
/** The collision with one option the globals table holds, a global or a plugin's own option. */
|
|
1231
|
+
function tableCollision(entry) {
|
|
1232
|
+
const { owner, site } = entry;
|
|
1233
|
+
if (owner.kind === 'plugin') {
|
|
1234
|
+
const clause = `an option of plugin ${quoted(owner.identity)}`;
|
|
1235
|
+
return {
|
|
1236
|
+
clause,
|
|
1237
|
+
held: siteFinding(site, site.named, clause),
|
|
1238
|
+
kind: 'option',
|
|
1239
|
+
remedy: attachedRemedy('plugin'),
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
return {
|
|
1243
|
+
clause: 'a global option',
|
|
1244
|
+
held: siteFinding(site, site.named, 'the global option'),
|
|
1245
|
+
kind: 'option',
|
|
1246
|
+
remedy: attachedRemedy('global'),
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
845
1249
|
/**
|
|
846
1250
|
* What one name a hook-declared input of either kind collides with, in the order the scopes are
|
|
847
1251
|
* reported: an earlier hook's option, an earlier hook's argument, a local option, a global or
|
|
@@ -850,32 +1254,36 @@ function attachedRemedy(target) {
|
|
|
850
1254
|
*/
|
|
851
1255
|
function attachedCollision(input, held) {
|
|
852
1256
|
const { name } = input;
|
|
853
|
-
const
|
|
1257
|
+
const { path } = held;
|
|
1258
|
+
const hook = held.hooks.get(name) ?? held.hookArguments.get(name);
|
|
854
1259
|
if (hook !== undefined) {
|
|
855
|
-
return hookClause(
|
|
856
|
-
}
|
|
857
|
-
const hooked = held.hookArguments.get(name);
|
|
858
|
-
if (hooked !== undefined) {
|
|
859
|
-
return hookClause('argument', hooked);
|
|
1260
|
+
return hookClause(hook, path);
|
|
860
1261
|
}
|
|
861
|
-
|
|
862
|
-
|
|
1262
|
+
const local = held.locals.get(name);
|
|
1263
|
+
if (local !== undefined) {
|
|
1264
|
+
return {
|
|
1265
|
+
clause: 'a local option',
|
|
1266
|
+
held: inputFinding(path, local, 'the local option'),
|
|
1267
|
+
kind: 'option',
|
|
1268
|
+
remedy: attachedRemedy('local'),
|
|
1269
|
+
};
|
|
863
1270
|
}
|
|
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) };
|
|
1271
|
+
const entry = held.globals.names.get(name);
|
|
1272
|
+
if (entry) {
|
|
1273
|
+
return tableCollision(entry);
|
|
869
1274
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
1275
|
+
const argument = held.arguments.get(name);
|
|
1276
|
+
return argument === undefined
|
|
1277
|
+
? undefined
|
|
1278
|
+
: {
|
|
1279
|
+
clause: 'an argument',
|
|
1280
|
+
held: inputFinding(path, argument, 'the argument'),
|
|
1281
|
+
kind: 'argument',
|
|
1282
|
+
remedy: attachedRemedy('argument'),
|
|
1283
|
+
};
|
|
873
1284
|
}
|
|
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) {
|
|
1285
|
+
/** The spellings one compiled table holds, each with its form and the place that declared it. */
|
|
1286
|
+
function readSpellings(table, claimed, placeOf) {
|
|
879
1287
|
const longs = new Map();
|
|
880
1288
|
for (const [spelling, option] of table) {
|
|
881
1289
|
if (option.role === 'long') {
|
|
@@ -884,53 +1292,107 @@ function readSpellings(table, claimed) {
|
|
|
884
1292
|
}
|
|
885
1293
|
for (const [spelling, option] of table) {
|
|
886
1294
|
if (!claimed.has(spelling)) {
|
|
887
|
-
|
|
1295
|
+
const form = longs.get(option.name) ?? spelling;
|
|
1296
|
+
claimed.set(spelling, { finding: placeOf(option.name, option.role), form });
|
|
888
1297
|
}
|
|
889
1298
|
}
|
|
890
1299
|
}
|
|
1300
|
+
/** The place one input's spelling sits: the key of its call that yields the spelling. */
|
|
1301
|
+
function spellingPlace(site, role, note) {
|
|
1302
|
+
return siteFinding(site, spellingMark(site, role), note);
|
|
1303
|
+
}
|
|
891
1304
|
/** One hook-declared option's spellings, against every spelling the table already claims. */
|
|
892
1305
|
function checkAttachedSpelling(declared, named) {
|
|
893
|
-
const { claimed,
|
|
894
|
-
const
|
|
895
|
-
|
|
1306
|
+
const { claimed, scope } = named;
|
|
1307
|
+
const { identity, input } = declared;
|
|
1308
|
+
const { subject } = scope;
|
|
1309
|
+
const site = scope.siteOf(input);
|
|
1310
|
+
const table = compileOptions([input], scope);
|
|
1311
|
+
for (const [spelling, option] of table) {
|
|
896
1312
|
const used = claimed.get(spelling);
|
|
897
1313
|
if (used !== undefined) {
|
|
898
|
-
throw new DeclarationError(
|
|
1314
|
+
throw new DeclarationError(spellingTaken, {
|
|
1315
|
+
correction: 'Change one of the two spellings or omit the plugin.',
|
|
1316
|
+
findings: [
|
|
1317
|
+
spellingPlace(site, option.role, declarerNote(identity)),
|
|
1318
|
+
...(used.finding === undefined ? [] : [used.finding]),
|
|
1319
|
+
],
|
|
1320
|
+
sentence: `Plugin ${quoted(identity)} declares option ${quoted(input.name)} with spelling ${quoted(spelling)} on ${subject}, which ${quoted(used.form)} already uses.`,
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
readSpellings(table, claimed, (_name, role) => spellingPlace(site, role, declarerNote(identity)));
|
|
1325
|
+
}
|
|
1326
|
+
/** The declarations of one kind, keyed by name, the first of each name winning. */
|
|
1327
|
+
function byName(inputs) {
|
|
1328
|
+
const named = new Map();
|
|
1329
|
+
for (const input of inputs) {
|
|
1330
|
+
if (!named.has(input.name)) {
|
|
1331
|
+
named.set(input.name, input);
|
|
899
1332
|
}
|
|
900
1333
|
}
|
|
901
|
-
|
|
1334
|
+
return named;
|
|
1335
|
+
}
|
|
1336
|
+
/** Where the globals table's options declared their spellings, a global or a plugin's option. */
|
|
1337
|
+
function tableSpellings(globals) {
|
|
1338
|
+
return (name, role) => {
|
|
1339
|
+
const entry = globals.names.get(name);
|
|
1340
|
+
if (!entry) {
|
|
1341
|
+
return undefined;
|
|
1342
|
+
}
|
|
1343
|
+
const { owner, site } = entry;
|
|
1344
|
+
const note = owner.kind === 'plugin'
|
|
1345
|
+
? `an option of plugin ${quoted(owner.identity)}`
|
|
1346
|
+
: `the global option ${quoted(name)}`;
|
|
1347
|
+
return spellingPlace(site, role, note);
|
|
1348
|
+
};
|
|
902
1349
|
}
|
|
903
1350
|
/**
|
|
904
1351
|
* Every input a hook declared, against the names and spellings the Command, the globals table, and
|
|
905
1352
|
* 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.
|
|
1353
|
+
* input reports as the plugin's fault and never as the author's. A collision carries a finding for
|
|
1354
|
+
* the hook's input and one for the declaration that already holds the name or spelling.
|
|
907
1355
|
*/
|
|
908
|
-
function checkAttachedInputs(declared, attached,
|
|
909
|
-
const
|
|
1356
|
+
function checkAttachedInputs(declared, attached, place) {
|
|
1357
|
+
const { globals, path } = place;
|
|
1358
|
+
const scope = localScope(declared.name, path);
|
|
1359
|
+
const { subject } = scope;
|
|
910
1360
|
const hooked = new Set(attached.map((entry) => entry.input));
|
|
911
1361
|
const authored = declared.inputs.filter((input) => !hooked.has(input));
|
|
912
1362
|
const options = authored.filter((input) => input.kind === 'option');
|
|
1363
|
+
const locals = byName(options);
|
|
913
1364
|
const held = {
|
|
914
|
-
arguments:
|
|
1365
|
+
arguments: byName(authored.filter((input) => input.kind === 'argument')),
|
|
915
1366
|
globals,
|
|
916
1367
|
hookArguments: new Map(),
|
|
917
1368
|
hooks: new Map(),
|
|
918
|
-
locals
|
|
1369
|
+
locals,
|
|
1370
|
+
path,
|
|
919
1371
|
};
|
|
920
1372
|
const claimed = new Map();
|
|
921
|
-
readSpellings(globals.options, claimed);
|
|
922
|
-
readSpellings(compileOptions(options,
|
|
923
|
-
|
|
1373
|
+
readSpellings(globals.options, claimed, tableSpellings(globals));
|
|
1374
|
+
readSpellings(compileOptions(options, scope), claimed, (name, role) => {
|
|
1375
|
+
const local = locals.get(name);
|
|
1376
|
+
return local === undefined || local.kind !== 'option'
|
|
1377
|
+
? undefined
|
|
1378
|
+
: spellingPlace(scope.siteOf(local), role, `the local option ${quoted(name)}`);
|
|
1379
|
+
});
|
|
1380
|
+
for (const entry of attached) {
|
|
1381
|
+
const { identity, input } = entry;
|
|
924
1382
|
const collision = attachedCollision(input, held);
|
|
925
1383
|
if (collision) {
|
|
926
|
-
throw new DeclarationError(
|
|
1384
|
+
throw new DeclarationError(nameCollisionRule(input.kind, collision.kind), {
|
|
1385
|
+
correction: collision.remedy,
|
|
1386
|
+
findings: [inputFinding(path, input, declarerNote(identity)), collision.held],
|
|
1387
|
+
sentence: `Plugin ${quoted(identity)} declares ${input.kind} ${quoted(input.name)} on ${subject}, which is already declared as ${collision.clause}.`,
|
|
1388
|
+
});
|
|
927
1389
|
}
|
|
928
1390
|
if (input.kind === 'option') {
|
|
929
|
-
checkAttachedSpelling({ identity, input }, { claimed,
|
|
930
|
-
held.hooks.set(input.name,
|
|
1391
|
+
checkAttachedSpelling({ identity, input }, { claimed, scope });
|
|
1392
|
+
held.hooks.set(input.name, entry);
|
|
931
1393
|
}
|
|
932
1394
|
else {
|
|
933
|
-
held.hookArguments.set(input.name,
|
|
1395
|
+
held.hookArguments.set(input.name, entry);
|
|
934
1396
|
}
|
|
935
1397
|
}
|
|
936
1398
|
}
|
|
@@ -969,29 +1431,31 @@ function bindDispatch(state, action, globals) {
|
|
|
969
1431
|
function checkInputFacts(inputs, command, context) {
|
|
970
1432
|
const { name, subject } = command;
|
|
971
1433
|
for (const input of inputs) {
|
|
972
|
-
const
|
|
973
|
-
|
|
1434
|
+
const site = inputSite(name, context.path, input);
|
|
1435
|
+
const sentence = site.subject;
|
|
1436
|
+
checkDescription(site, input.config.description);
|
|
974
1437
|
if (input.kind === 'argument') {
|
|
975
|
-
checkNoListingFacts(
|
|
976
|
-
checkNoArgumentBinding(
|
|
1438
|
+
checkNoListingFacts(site, input.config);
|
|
1439
|
+
checkNoArgumentBinding(site, input.config);
|
|
977
1440
|
}
|
|
978
1441
|
else {
|
|
979
|
-
checkHidden(
|
|
980
|
-
checkDeprecated(
|
|
981
|
-
checkEnvBinding(
|
|
1442
|
+
checkHidden(site, input.config.hidden);
|
|
1443
|
+
checkDeprecated(site, input.config.deprecated);
|
|
1444
|
+
checkEnvBinding(site, input.config);
|
|
982
1445
|
}
|
|
983
1446
|
context.extensions.set(input, buildExtensions({
|
|
984
1447
|
declared: input.config.extensions,
|
|
985
1448
|
descriptors: context.descriptors,
|
|
1449
|
+
site: { ...site, at: '1.extensions' },
|
|
986
1450
|
subject: { phrase: `on ${subject} ${input.kind} "${input.name}"`, sentence },
|
|
987
1451
|
target: input.kind,
|
|
988
1452
|
}));
|
|
989
1453
|
}
|
|
990
1454
|
}
|
|
991
1455
|
/** One Command declares arguments or attaches children, whichever declaration made each of them. */
|
|
992
|
-
function checkArgumentPlacement(
|
|
1456
|
+
function checkArgumentPlacement(command, slot, child) {
|
|
993
1457
|
if (slot && child) {
|
|
994
|
-
throw placementFault(
|
|
1458
|
+
throw placementFault(command, slot.input, child);
|
|
995
1459
|
}
|
|
996
1460
|
}
|
|
997
1461
|
/**
|
|
@@ -1008,7 +1472,8 @@ export function buildCommand(state, context) {
|
|
|
1008
1472
|
for (const [input, record] of state.records) {
|
|
1009
1473
|
context.extensions.set(input, record);
|
|
1010
1474
|
}
|
|
1011
|
-
const
|
|
1475
|
+
const place = { name, path: context.path };
|
|
1476
|
+
const declaredSlots = collectArguments(state, place);
|
|
1012
1477
|
const hasAction = action !== undefined;
|
|
1013
1478
|
/**
|
|
1014
1479
|
* The hooks run once the author's declaration is complete, and each value a hook receives resolves
|
|
@@ -1027,15 +1492,15 @@ export function buildCommand(state, context) {
|
|
|
1027
1492
|
checkInputFacts(hookInputs.map((call) => call.input), { name, subject }, context);
|
|
1028
1493
|
// The hook-declared names are checked first, so a collision reports in the plugin's voice.
|
|
1029
1494
|
// A rule the author's own declaration voices never speaks for a name a hook declared.
|
|
1030
|
-
checkAttachedInputs(hooked, hookInputs, globals);
|
|
1495
|
+
checkAttachedInputs(hooked, hookInputs, { globals, path: context.path });
|
|
1031
1496
|
// Every value was validated once, the author's at each call and each hook's at its `extend()`.
|
|
1032
1497
|
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,
|
|
1498
|
+
const slots = hookInputs.length > 0 ? collectArguments(hooked, place) : declaredSlots;
|
|
1499
|
+
checkArgumentPlacement(place, slots[0], state.children[0]);
|
|
1500
|
+
const result = checkFinished(hooked, hasAction, { path: context.path });
|
|
1501
|
+
const options = checkLocalOptions(optionsOf(hooked.inputs), globals, localScope(name, context.path));
|
|
1037
1502
|
// A hook's erased calls answer the declaration rules an authored call answers at the call.
|
|
1038
|
-
checkDeclarations(hookInputs.map((
|
|
1503
|
+
checkDeclarations(hookInputs.map(({ input }) => ({ input, site: inputSite(name, context.path, input) })));
|
|
1039
1504
|
const children = new Map();
|
|
1040
1505
|
const routes = new Map();
|
|
1041
1506
|
for (const child of state.children) {
|
|
@@ -1093,7 +1558,7 @@ export class CommandBuilder {
|
|
|
1093
1558
|
}
|
|
1094
1559
|
argument(name, config) {
|
|
1095
1560
|
const input = {
|
|
1096
|
-
config
|
|
1561
|
+
config,
|
|
1097
1562
|
kind: 'argument',
|
|
1098
1563
|
name,
|
|
1099
1564
|
};
|
|
@@ -1101,7 +1566,7 @@ export class CommandBuilder {
|
|
|
1101
1566
|
}
|
|
1102
1567
|
option(name, config) {
|
|
1103
1568
|
const input = {
|
|
1104
|
-
config
|
|
1569
|
+
config,
|
|
1105
1570
|
kind: 'option',
|
|
1106
1571
|
name,
|
|
1107
1572
|
};
|
|
@@ -1158,7 +1623,7 @@ _b = CommandBuilder;
|
|
|
1158
1623
|
*/
|
|
1159
1624
|
class CommandDeclaration extends CommandBuilder {
|
|
1160
1625
|
constructor(name, options) {
|
|
1161
|
-
const declared = namedState(name, options);
|
|
1626
|
+
const declared = declaring(() => namedState(name, options));
|
|
1162
1627
|
super(declared.name, declared.state);
|
|
1163
1628
|
}
|
|
1164
1629
|
}
|
|
@@ -1172,12 +1637,35 @@ export function collectInputs(command) {
|
|
|
1172
1637
|
];
|
|
1173
1638
|
}
|
|
1174
1639
|
/**
|
|
1175
|
-
*
|
|
1176
|
-
*
|
|
1177
|
-
|
|
1640
|
+
* Where every input of one graph was declared: each global option at its `globalOption()` call,
|
|
1641
|
+
* and each Command's own inputs at their calls on the Command, under its path from the root.
|
|
1642
|
+
*/
|
|
1643
|
+
export function inputPlaces(graph) {
|
|
1644
|
+
const places = new Map();
|
|
1645
|
+
for (const input of graph.globals.inputs) {
|
|
1646
|
+
places.set(input, inputPlace(input, { global: true, path: [] }));
|
|
1647
|
+
}
|
|
1648
|
+
const walk = (command, path) => {
|
|
1649
|
+
for (const input of command.inputs) {
|
|
1650
|
+
places.set(input, inputPlace(input, { global: false, path }));
|
|
1651
|
+
}
|
|
1652
|
+
for (const [name, child] of command.children) {
|
|
1653
|
+
walk(child, [...path, name]);
|
|
1654
|
+
}
|
|
1655
|
+
};
|
|
1656
|
+
walk(graph.root, []);
|
|
1657
|
+
return places;
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* The names a routing failure offers: the canonical names of the visible, current children, in
|
|
1661
|
+
* authoring order. A candidate list is a listing, so a hidden or a deprecated child is absent from
|
|
1662
|
+
* it, as completion leaves them out, and a parent whose children are all hidden or deprecated
|
|
1663
|
+
* offers none. A deprecated child typed in full still routes.
|
|
1178
1664
|
*/
|
|
1179
1665
|
function candidatesOf(command) {
|
|
1180
|
-
return [...command.children]
|
|
1666
|
+
return [...command.children]
|
|
1667
|
+
.filter(([, child]) => !child.hidden && child.deprecated === undefined)
|
|
1668
|
+
.map(([name]) => name);
|
|
1181
1669
|
}
|
|
1182
1670
|
/**
|
|
1183
1671
|
* Whether a token names one of the Command's children: the Command has children and the token is
|
|
@@ -1186,8 +1674,12 @@ function candidatesOf(command) {
|
|
|
1186
1674
|
export function readsAsChild(command, token) {
|
|
1187
1675
|
return command.children.size > 0 && !isOptionToken(token);
|
|
1188
1676
|
}
|
|
1189
|
-
/**
|
|
1190
|
-
|
|
1677
|
+
/**
|
|
1678
|
+
* Bare tokens, names or aliases, select children until a Command has none; a hyphen commits.
|
|
1679
|
+
* `walked` receives the path after each name routes, so an unknown Command leaves its caller
|
|
1680
|
+
* holding the partial path walked before it.
|
|
1681
|
+
*/
|
|
1682
|
+
export function route(root, tokens, walked) {
|
|
1191
1683
|
let command = root;
|
|
1192
1684
|
const path = [];
|
|
1193
1685
|
let index = 0;
|
|
@@ -1202,6 +1694,7 @@ export function route(root, tokens) {
|
|
|
1202
1694
|
// An alias routes like the canonical name, and the path it walks reports that name alone.
|
|
1203
1695
|
command = child.command;
|
|
1204
1696
|
path.push(child.name);
|
|
1697
|
+
walked?.(Object.freeze([...path]));
|
|
1205
1698
|
index += 1;
|
|
1206
1699
|
}
|
|
1207
1700
|
return { command, path, tokens: tokens.slice(index) };
|
|
@@ -1240,10 +1733,13 @@ export function argumentSlot(slots, position) {
|
|
|
1240
1733
|
const last = slots.at(-1);
|
|
1241
1734
|
return slots[position] ?? (last?.variadic ? last : undefined);
|
|
1242
1735
|
}
|
|
1243
|
-
/**
|
|
1244
|
-
|
|
1736
|
+
/**
|
|
1737
|
+
* Consumes the globals table, then routes the remaining bare tokens to a Command. `walked`
|
|
1738
|
+
* receives the path as routing extends it, the partial path of an unknown Command included.
|
|
1739
|
+
*/
|
|
1740
|
+
export function routeInvocation(graph, argv, walked) {
|
|
1245
1741
|
const scan = extractGlobals(graph.globals.options, [...argv]);
|
|
1246
|
-
const routed = route(graph.root, scan.rest);
|
|
1742
|
+
const routed = route(graph.root, scan.rest, walked);
|
|
1247
1743
|
return {
|
|
1248
1744
|
command: routed.command,
|
|
1249
1745
|
path: routed.path,
|
|
@@ -1306,7 +1802,7 @@ function requestNode(graph, path, place) {
|
|
|
1306
1802
|
const options = place.global ? graph.globals : nodeAt(graph, path).options;
|
|
1307
1803
|
const node = options.find((option) => option.name === place.name);
|
|
1308
1804
|
if (!node) {
|
|
1309
|
-
throw
|
|
1805
|
+
throw graphMismatch(`Option "${place.name}" is not in the inspected graph.`);
|
|
1310
1806
|
}
|
|
1311
1807
|
return node;
|
|
1312
1808
|
}
|
|
@@ -1330,6 +1826,7 @@ async function fillScope({ graph, invocation, routed }, local) {
|
|
|
1330
1826
|
locals: local.kind === 'parsed'
|
|
1331
1827
|
? { global: false, inputs: optionsOf(routed.command.inputs), values: locals }
|
|
1332
1828
|
: undefined,
|
|
1829
|
+
offer: invocation.offer,
|
|
1333
1830
|
out: invocation.sourceOut,
|
|
1334
1831
|
plugins: graph.globals.plugins,
|
|
1335
1832
|
request: (input, global) => requestNode(invocation.inspected(), routed.path, { global, name: input.name }),
|
|
@@ -1426,6 +1923,7 @@ export async function prepareDispatch(graph, routed, invocation) {
|
|
|
1426
1923
|
return { ...ready, globals, kind: 'ready', result };
|
|
1427
1924
|
}
|
|
1428
1925
|
catch (error) {
|
|
1429
|
-
|
|
1926
|
+
// The fault is held as a failure, so a throw from reading a validator's output is never offered.
|
|
1927
|
+
return held(toFailure(error));
|
|
1430
1928
|
}
|
|
1431
1929
|
}
|