aontu 0.56.0 → 0.58.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/README.md +2 -2
- package/dist/agentsmd.js +1 -1
- package/dist/alias.d.ts +3 -0
- package/dist/alias.js +59 -0
- package/dist/alias.js.map +1 -0
- package/dist/aontu.d.ts +5 -2
- package/dist/aontu.js +11 -3
- package/dist/aontu.js.map +1 -1
- package/dist/cli.d.ts +8 -2
- package/dist/cli.js +564 -21
- package/dist/cli.js.map +1 -1
- package/dist/ctx.d.ts +2 -0
- package/dist/ctx.js +1 -0
- package/dist/ctx.js.map +1 -1
- package/dist/escape.d.ts +5 -0
- package/dist/escape.js +455 -0
- package/dist/escape.js.map +1 -0
- package/dist/format.d.ts +9 -0
- package/dist/format.js +550 -55
- package/dist/format.js.map +1 -1
- package/dist/hints.js +74 -9
- package/dist/hints.js.map +1 -1
- package/dist/lang.js +374 -58
- package/dist/lang.js.map +1 -1
- package/dist/lower.d.ts +20 -0
- package/dist/lower.js +575 -0
- package/dist/lower.js.map +1 -0
- package/dist/lsp.d.ts +1 -1
- package/dist/lsp.js +4 -4
- package/dist/lsp.js.map +1 -1
- package/dist/mcp-server.js +2 -2
- package/dist/mcp-server.js.map +1 -1
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.js +40 -3
- package/dist/mcp.js.map +1 -1
- package/dist/mod-tool.js +8 -7
- package/dist/mod-tool.js.map +1 -1
- package/dist/mod.js +6 -6
- package/dist/mod.js.map +1 -1
- package/dist/render.d.ts +53 -0
- package/dist/render.js +542 -0
- package/dist/render.js.map +1 -0
- package/dist/sigdecl.js +1 -1
- package/dist/sigdecl.js.map +1 -1
- package/dist/std.d.ts +2 -0
- package/dist/std.js +498 -2
- package/dist/std.js.map +1 -1
- package/dist/template.d.ts +5 -0
- package/dist/template.js +257 -0
- package/dist/template.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/unify.js +43 -0
- package/dist/unify.js.map +1 -1
- package/dist/val/AggFuncVal.d.ts +1 -1
- package/dist/val/AggFuncVal.js +10 -21
- package/dist/val/AggFuncVal.js.map +1 -1
- package/dist/val/BagVal.js +1 -1
- package/dist/val/BagVal.js.map +1 -1
- package/dist/val/ConstraintVal.js +1 -1
- package/dist/val/EachFuncVal.d.ts +1 -1
- package/dist/val/EachFuncVal.js +9 -15
- package/dist/val/EachFuncVal.js.map +1 -1
- package/dist/val/EmitFuncVal.d.ts +42 -0
- package/dist/val/EmitFuncVal.js +531 -0
- package/dist/val/EmitFuncVal.js.map +1 -0
- package/dist/val/FilterFuncVal.js +9 -6
- package/dist/val/FilterFuncVal.js.map +1 -1
- package/dist/val/FormFuncVal.d.ts +14 -0
- package/dist/val/FormFuncVal.js +55 -0
- package/dist/val/FormFuncVal.js.map +1 -0
- package/dist/val/FuncBaseVal.d.ts +1 -0
- package/dist/val/FuncBaseVal.js +16 -0
- package/dist/val/FuncBaseVal.js.map +1 -1
- package/dist/val/MapVal.d.ts +2 -1
- package/dist/val/MapVal.js +2 -1
- package/dist/val/MapVal.js.map +1 -1
- package/dist/val/PackFuncVal.d.ts +1 -1
- package/dist/val/PackFuncVal.js +23 -20
- package/dist/val/PackFuncVal.js.map +1 -1
- package/dist/val/PlaceVal.d.ts +3 -1
- package/dist/val/PlaceVal.js +9 -6
- package/dist/val/PlaceVal.js.map +1 -1
- package/dist/val/RefVal.d.ts +3 -0
- package/dist/val/RefVal.js +156 -17
- package/dist/val/RefVal.js.map +1 -1
- package/dist/val/StrFuncVal.d.ts +32 -0
- package/dist/val/StrFuncVal.js +292 -0
- package/dist/val/StrFuncVal.js.map +1 -0
- package/dist/val/Val.d.ts +7 -1
- package/dist/val/Val.js +12 -1
- package/dist/val/Val.js.map +1 -1
- package/dist/val/members.d.ts +9 -0
- package/dist/val/members.js +52 -0
- package/dist/val/members.js.map +1 -0
- package/grammar/aontu.abnf +4 -3
- package/grammar/aontu.gbnf +4 -3
- package/grammar/aontu.lark +4 -3
- package/grammar/aontu.tmLanguage.json +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +4 -4
- package/skill/error-codes.md +1 -1
- package/skill/examples.md +1 -1
- package/skill/grammar-card.md +1 -1
- package/src/agentsmd.ts +1 -1
- package/src/alias.ts +112 -0
- package/src/aontu.ts +20 -2
- package/src/cli.ts +630 -23
- package/src/ctx.ts +13 -0
- package/src/escape.ts +371 -0
- package/src/format.ts +648 -56
- package/src/hints.ts +94 -9
- package/src/lang.ts +430 -63
- package/src/lower.ts +636 -0
- package/src/lsp.ts +4 -4
- package/src/mcp-server.ts +3 -2
- package/src/mcp.ts +43 -4
- package/src/mod-tool.ts +9 -8
- package/src/mod.ts +6 -6
- package/src/render.ts +727 -0
- package/src/sigdecl.ts +1 -1
- package/src/std.ts +506 -1
- package/src/template.ts +291 -0
- package/src/unify.ts +47 -0
- package/src/val/AggFuncVal.ts +10 -21
- package/src/val/BagVal.ts +1 -1
- package/src/val/ConstraintVal.ts +1 -1
- package/src/val/EachFuncVal.ts +9 -19
- package/src/val/EmitFuncVal.ts +738 -0
- package/src/val/FilterFuncVal.ts +12 -7
- package/src/val/FormFuncVal.ts +119 -0
- package/src/val/FuncBaseVal.ts +18 -0
- package/src/val/MapVal.ts +3 -2
- package/src/val/PackFuncVal.ts +24 -22
- package/src/val/PlaceVal.ts +9 -6
- package/src/val/RefVal.ts +167 -18
- package/src/val/StrFuncVal.ts +334 -0
- package/src/val/Val.ts +42 -1
- package/src/val/members.ts +86 -0
package/dist/format.js
CHANGED
|
@@ -16,10 +16,11 @@ exports.unifiedDiff = unifiedDiff;
|
|
|
16
16
|
// trees: a formatter that cannot prove its output is the same document
|
|
17
17
|
// refuses rather than return it.
|
|
18
18
|
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
// tree. The lawful
|
|
22
|
-
//
|
|
19
|
+
// Two tiers. The syntactic (P1): whitespace, commas, quotes, bare
|
|
20
|
+
// keys, chains and pair elements, none of which changes the parse
|
|
21
|
+
// tree. The lawful (P2), over it: repeat the prefix, and merge what
|
|
22
|
+
// repeats -- rewrites that rest on the meet, each checked by the meet
|
|
23
|
+
// in isolation and kept only where the engine agrees.
|
|
23
24
|
//
|
|
24
25
|
// The Go twin is go/format.go, function for function; the shared
|
|
25
26
|
// behaviour is test/spec/fmt.tsv, executed by both spec runners.
|
|
@@ -212,26 +213,33 @@ class Reader {
|
|
|
212
213
|
opener = false;
|
|
213
214
|
gap = false;
|
|
214
215
|
}
|
|
216
|
+
// A blank line before the closer is no paragraph break: nothing
|
|
217
|
+
// follows it, and the layout would drop it anyway.
|
|
218
|
+
while (0 < body.length && 'blank' === body[body.length - 1].t) {
|
|
219
|
+
body.pop();
|
|
220
|
+
}
|
|
215
221
|
return { body, open };
|
|
216
222
|
}
|
|
217
223
|
// One entry: an include, a spread, a pair, or -- as a list element or
|
|
218
224
|
// at the root -- a value.
|
|
219
225
|
entry() {
|
|
220
226
|
const n = this.name(0);
|
|
227
|
+
const at = this.T[this.i].sI;
|
|
221
228
|
if ('#OD_multisource' === n) {
|
|
222
229
|
const text = '@' + normStr(this.T[this.i + 1].src);
|
|
223
230
|
this.i += 2;
|
|
224
|
-
return { t: 'include', text };
|
|
231
|
+
return { t: 'include', text, at };
|
|
225
232
|
}
|
|
226
233
|
if ('#E&' === n && '#CL' === this.name(1)) {
|
|
227
234
|
this.i += 2;
|
|
228
|
-
return { t: 'spread', value: this.value() };
|
|
235
|
+
return { t: 'spread', value: this.value(), at };
|
|
229
236
|
}
|
|
230
237
|
if (this.atKey()) {
|
|
231
238
|
const tok = this.T[this.i];
|
|
232
239
|
const opt = '#QM' === this.name(1);
|
|
240
|
+
const alias = '=' === this.T[this.i + (opt ? 2 : 1)].src;
|
|
233
241
|
this.i += opt ? 3 : 2;
|
|
234
|
-
return { t: 'pair', key: keyText(tok), opt, value: this.value() };
|
|
242
|
+
return { t: 'pair', key: keyText(tok), opt, alias, value: this.value(), at };
|
|
235
243
|
}
|
|
236
244
|
return this.value();
|
|
237
245
|
}
|
|
@@ -258,12 +266,13 @@ class Reader {
|
|
|
258
266
|
if (!this.open(items) && !BINARY[n] && '#LN' !== n && '#CM' !== n) {
|
|
259
267
|
break;
|
|
260
268
|
}
|
|
269
|
+
const at = this.T[this.i].sI;
|
|
261
270
|
if ('#E&' === n && '#CL' === this.name(1)) {
|
|
262
271
|
if (0 === items.length) {
|
|
263
272
|
// A chain through a spread, `a: &: integer`. The braces are
|
|
264
273
|
// the agreed spelling (X-7), so it is read as the map it is.
|
|
265
274
|
this.i += 2;
|
|
266
|
-
return { t: 'map', body: [{ t: 'spread', value: this.value() }] };
|
|
275
|
+
return { t: 'map', body: [{ t: 'spread', value: this.value(), at }], at };
|
|
267
276
|
}
|
|
268
277
|
// A sibling spread in a list, `[1 &: 2]`: this value is complete.
|
|
269
278
|
break;
|
|
@@ -283,7 +292,7 @@ class Reader {
|
|
|
283
292
|
// operator, or on a line the value continues past. Otherwise
|
|
284
293
|
// it trails the statement and the caller attaches it.
|
|
285
294
|
if (this.open(items) || BINARY[this.name(this.significant())]) {
|
|
286
|
-
items.push({ t: 'note', text: this.T[this.i].src });
|
|
295
|
+
items.push({ t: 'note', text: this.T[this.i].src, at });
|
|
287
296
|
this.i++;
|
|
288
297
|
continue;
|
|
289
298
|
}
|
|
@@ -292,13 +301,13 @@ class Reader {
|
|
|
292
301
|
if (BINARY[n]) {
|
|
293
302
|
items.push({
|
|
294
303
|
t: 'op', text: this.T[this.i].src,
|
|
295
|
-
brk: '#LN' === this.name(-1) || '#LN' === this.name(1),
|
|
304
|
+
brk: '#LN' === this.name(-1) || '#LN' === this.name(1), at,
|
|
296
305
|
});
|
|
297
306
|
this.i++;
|
|
298
307
|
continue;
|
|
299
308
|
}
|
|
300
309
|
if (PREFIX[n]) {
|
|
301
|
-
items.push({ t: 'prefix', text: this.T[this.i].src });
|
|
310
|
+
items.push({ t: 'prefix', text: this.T[this.i].src, at });
|
|
302
311
|
this.i++;
|
|
303
312
|
continue;
|
|
304
313
|
}
|
|
@@ -306,7 +315,7 @@ class Reader {
|
|
|
306
315
|
this.i++;
|
|
307
316
|
const inner = this.seq();
|
|
308
317
|
this.i++;
|
|
309
|
-
items.push({ t: 'paren', inner });
|
|
318
|
+
items.push({ t: 'paren', inner, at });
|
|
310
319
|
continue;
|
|
311
320
|
}
|
|
312
321
|
if ('#TX' === n && '#E(' === this.name(1)) {
|
|
@@ -314,25 +323,25 @@ class Reader {
|
|
|
314
323
|
this.i += 2;
|
|
315
324
|
const args = this.seq();
|
|
316
325
|
this.i++;
|
|
317
|
-
items.push({ t: 'call', name, args });
|
|
326
|
+
items.push({ t: 'call', name, args, at });
|
|
318
327
|
continue;
|
|
319
328
|
}
|
|
320
329
|
if ('#OB' === n) {
|
|
321
330
|
this.i++;
|
|
322
331
|
const m = this.body('#CB', true);
|
|
323
332
|
this.i++;
|
|
324
|
-
items.push({ t: 'map', body: m.body, open: m.open });
|
|
333
|
+
items.push({ t: 'map', body: m.body, open: m.open, at });
|
|
325
334
|
continue;
|
|
326
335
|
}
|
|
327
336
|
if ('#OS' === n) {
|
|
328
337
|
this.i++;
|
|
329
338
|
const l = this.body('#CS', true);
|
|
330
339
|
this.i++;
|
|
331
|
-
items.push({ t: 'list', body: l.body, open: l.open });
|
|
340
|
+
items.push({ t: 'list', body: l.body, open: l.open, at });
|
|
332
341
|
continue;
|
|
333
342
|
}
|
|
334
343
|
if ('#OD_multisource' === n) {
|
|
335
|
-
items.push({ t: 'include', text: '@' + normStr(this.T[this.i + 1].src) });
|
|
344
|
+
items.push({ t: 'include', text: '@' + normStr(this.T[this.i + 1].src), at });
|
|
336
345
|
this.i += 2;
|
|
337
346
|
continue;
|
|
338
347
|
}
|
|
@@ -348,7 +357,8 @@ class Reader {
|
|
|
348
357
|
'note' !== items[0].t) {
|
|
349
358
|
return items[0];
|
|
350
359
|
}
|
|
351
|
-
|
|
360
|
+
// An empty value, `a:`, is an expression with nothing in it.
|
|
361
|
+
return { t: 'expr', items, at: items[0]?.at };
|
|
352
362
|
}
|
|
353
363
|
// Whether the expression so far wants an operand: nothing yet, or an
|
|
354
364
|
// operator, a prefix or a comment last.
|
|
@@ -361,6 +371,7 @@ class Reader {
|
|
|
361
371
|
}
|
|
362
372
|
// The token under the cursor, and the parts glued to it.
|
|
363
373
|
atom() {
|
|
374
|
+
const at = this.T[this.i].sI;
|
|
364
375
|
let text = atomText(this.T[this.i]);
|
|
365
376
|
this.i++;
|
|
366
377
|
while (GLUE[this.name(0)] &&
|
|
@@ -368,7 +379,7 @@ class Reader {
|
|
|
368
379
|
text += atomText(this.T[this.i]);
|
|
369
380
|
this.i++;
|
|
370
381
|
}
|
|
371
|
-
return { t: 'atom', text };
|
|
382
|
+
return { t: 'atom', text, at };
|
|
372
383
|
}
|
|
373
384
|
// A call's arguments, or a parenthesis's contents, up to the closing
|
|
374
385
|
// parenthesis: values separated by commas, with a comment among them
|
|
@@ -376,6 +387,7 @@ class Reader {
|
|
|
376
387
|
seq() {
|
|
377
388
|
const out = [];
|
|
378
389
|
let gap = true;
|
|
390
|
+
let comma = false;
|
|
379
391
|
for (;;) {
|
|
380
392
|
const n = this.name(0);
|
|
381
393
|
if ('' === n || CLOSER[n]) {
|
|
@@ -387,9 +399,10 @@ class Reader {
|
|
|
387
399
|
}
|
|
388
400
|
if ('#CA' === n) {
|
|
389
401
|
if (gap) {
|
|
390
|
-
out.push({ t: 'atom', text: 'nil' });
|
|
402
|
+
out.push({ t: 'atom', text: 'nil', sep: comma });
|
|
391
403
|
}
|
|
392
404
|
gap = true;
|
|
405
|
+
comma = true;
|
|
393
406
|
this.i++;
|
|
394
407
|
continue;
|
|
395
408
|
}
|
|
@@ -398,8 +411,11 @@ class Reader {
|
|
|
398
411
|
this.i++;
|
|
399
412
|
continue;
|
|
400
413
|
}
|
|
401
|
-
|
|
414
|
+
const v = this.value();
|
|
415
|
+
v.sep = comma;
|
|
416
|
+
out.push(v);
|
|
402
417
|
gap = false;
|
|
418
|
+
comma = false;
|
|
403
419
|
}
|
|
404
420
|
return out;
|
|
405
421
|
}
|
|
@@ -451,6 +467,11 @@ function width(s) {
|
|
|
451
467
|
return Array.from(s).length;
|
|
452
468
|
}
|
|
453
469
|
function pairHead(node, tight) {
|
|
470
|
+
// An alias declaration is `%name = value` at every width: the `=` is
|
|
471
|
+
// an operator, and operators are spaced (§3.2).
|
|
472
|
+
if (node.alias) {
|
|
473
|
+
return node.key + ' = ';
|
|
474
|
+
}
|
|
454
475
|
return node.key + (node.opt ? '?' : '') + (tight ? ':' : ': ');
|
|
455
476
|
}
|
|
456
477
|
// The one-line spelling of a node, or undefined where it has none: a
|
|
@@ -508,16 +529,22 @@ function inline(node, tight) {
|
|
|
508
529
|
return undefined;
|
|
509
530
|
}
|
|
510
531
|
}
|
|
532
|
+
// Arguments on one line, each after the separator the author wrote
|
|
533
|
+
// (§3.6): a comma stays a comma, and a space a space, because the
|
|
534
|
+
// parser reads `must((v) => 0 <= v, "…")` as a run of arguments too.
|
|
511
535
|
function inlineSeq(items) {
|
|
512
|
-
|
|
513
|
-
for (
|
|
514
|
-
const s = inline(
|
|
536
|
+
let out = '';
|
|
537
|
+
for (let k = 0; k < items.length; k++) {
|
|
538
|
+
const s = inline(items[k], true);
|
|
515
539
|
if (undefined === s) {
|
|
516
540
|
return undefined;
|
|
517
541
|
}
|
|
518
|
-
|
|
542
|
+
out += (0 === k ? '' : sepOf(items[k])) + s;
|
|
519
543
|
}
|
|
520
|
-
return
|
|
544
|
+
return out;
|
|
545
|
+
}
|
|
546
|
+
function sepOf(node) {
|
|
547
|
+
return node.sep ? ', ' : ' ';
|
|
521
548
|
}
|
|
522
549
|
// Binary operators spaced, prefixes tight (§3.11). An operand is
|
|
523
550
|
// never directly after an operand: the reader ends a value there.
|
|
@@ -570,6 +597,23 @@ class Writer {
|
|
|
570
597
|
width() {
|
|
571
598
|
return width(this.line);
|
|
572
599
|
}
|
|
600
|
+
// Where the page is, and the lines written since, the current line
|
|
601
|
+
// included: the spelling of one statement, as it stands on the page.
|
|
602
|
+
mark() {
|
|
603
|
+
return this.lines.length;
|
|
604
|
+
}
|
|
605
|
+
since(mark) {
|
|
606
|
+
return this.lines.slice(mark).concat([this.line]).map(rtrim).join('\n') + '\n';
|
|
607
|
+
}
|
|
608
|
+
// The lines since a mark replaced by a text: the spelling before,
|
|
609
|
+
// where a rewrite did not pass its check.
|
|
610
|
+
replace(mark, text) {
|
|
611
|
+
const lines = text.split('\n');
|
|
612
|
+
lines.pop();
|
|
613
|
+
this.line = lines.pop();
|
|
614
|
+
this.lines.length = mark;
|
|
615
|
+
this.lines.push(...lines);
|
|
616
|
+
}
|
|
573
617
|
finish() {
|
|
574
618
|
if (!this.started) {
|
|
575
619
|
return '';
|
|
@@ -585,8 +629,11 @@ function rtrim(s) {
|
|
|
585
629
|
}
|
|
586
630
|
// The entries of a body, one per line at the indentation, with the
|
|
587
631
|
// blank lines the author kept between them (§3.8) -- never at the
|
|
588
|
-
// start or the end.
|
|
589
|
-
|
|
632
|
+
// start or the end. In STATEMENT position (`stmt`: the root, and the
|
|
633
|
+
// body of a plain map that is itself the value of a statement) a pair
|
|
634
|
+
// is laid out by §3.4, which may repeat its key; anywhere else -- a
|
|
635
|
+
// list, an operand, an argument -- by §3.5 alone.
|
|
636
|
+
function emitBody(w, body, indent, stmt) {
|
|
590
637
|
let pending = false;
|
|
591
638
|
let count = 0;
|
|
592
639
|
for (const node of body) {
|
|
@@ -601,6 +648,10 @@ function emitBody(w, body, indent) {
|
|
|
601
648
|
w.text(node.text);
|
|
602
649
|
continue;
|
|
603
650
|
}
|
|
651
|
+
if (undefined !== stmt && 'pair' === node.t) {
|
|
652
|
+
emitStatement(w, node, indent, stmt, '');
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
604
655
|
const e = chain(node);
|
|
605
656
|
emitValue(w, e, indent);
|
|
606
657
|
if (undefined !== e.trail) {
|
|
@@ -632,10 +683,10 @@ function emitValue(w, node, indent) {
|
|
|
632
683
|
emitValue(w, node.value, indent);
|
|
633
684
|
return;
|
|
634
685
|
case 'map':
|
|
635
|
-
emitBlock(w, '{', '}', node, indent);
|
|
686
|
+
emitBlock(w, '{', '}', node, indent, undefined);
|
|
636
687
|
return;
|
|
637
688
|
case 'list':
|
|
638
|
-
emitBlock(w, '[', ']', node, indent);
|
|
689
|
+
emitBlock(w, '[', ']', node, indent, undefined);
|
|
639
690
|
return;
|
|
640
691
|
case 'expr':
|
|
641
692
|
emitExpr(w, node.items, indent);
|
|
@@ -649,27 +700,36 @@ function emitValue(w, node, indent) {
|
|
|
649
700
|
}
|
|
650
701
|
}
|
|
651
702
|
// A call, or a parenthesis, that has no one-line form or is too wide
|
|
652
|
-
// for the budget. Three shapes.
|
|
653
|
-
//
|
|
654
|
-
//
|
|
655
|
-
//
|
|
656
|
-
//
|
|
657
|
-
//
|
|
658
|
-
//
|
|
703
|
+
// for the budget. Three shapes. Arguments that are all FLAT -- none
|
|
704
|
+
// holds a container -- stay on the one line however wide it is: a
|
|
705
|
+
// scalar is no narrower on a line of its own, and the formatter never
|
|
706
|
+
// breaks a line. The last argument HUGS the parentheses, `hide({` ...
|
|
707
|
+
// `})`, `close($.E & {` ... `})`, when it is a container, or an
|
|
708
|
+
// expression the author did not break that ends in one, and the
|
|
709
|
+
// arguments before it fit on the opener's line: the container decides
|
|
710
|
+
// its own lines. Otherwise the parenthesis opens a block: one argument
|
|
711
|
+
// per line one level in, the closer alone at the opener's level. A
|
|
712
|
+
// call whose last argument hugs is hugged in turn, `type(close({` ...
|
|
713
|
+
// `}))`: the schema idiom.
|
|
659
714
|
function emitCall(w, node, indent) {
|
|
660
715
|
const items = 'call' === node.t ? node.args : node.inner;
|
|
661
716
|
const open = ('call' === node.t ? node.name : '') + '(';
|
|
662
|
-
if (1 === items.length && ('map' === items[0].t || 'list' === items[0].t)) {
|
|
663
|
-
w.text(open);
|
|
664
|
-
emitValue(w, items[0], indent);
|
|
665
|
-
w.text(')');
|
|
666
|
-
return;
|
|
667
|
-
}
|
|
668
717
|
const one = inlineSeq(items);
|
|
669
|
-
if (undefined !== one) {
|
|
718
|
+
if (undefined !== one && !items.some(holdsContainer)) {
|
|
670
719
|
w.text(open + one + ')');
|
|
671
720
|
return;
|
|
672
721
|
}
|
|
722
|
+
const last = items[items.length - 1];
|
|
723
|
+
if (0 < items.length && hugs(last)) {
|
|
724
|
+
const head = inlineSeq(items.slice(0, -1));
|
|
725
|
+
const lead = '' === head ? '' : head + sepOf(last);
|
|
726
|
+
if (undefined !== head && ('' === head || w.width() + width(open + lead) <= BUDGET)) {
|
|
727
|
+
w.text(open + lead);
|
|
728
|
+
emitValue(w, last, indent);
|
|
729
|
+
w.text(')');
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
673
733
|
w.text(open);
|
|
674
734
|
let noted = false;
|
|
675
735
|
for (let k = 0; k < items.length; k++) {
|
|
@@ -690,7 +750,8 @@ function emitCall(w, node, indent) {
|
|
|
690
750
|
}
|
|
691
751
|
w.open(indent + 2, false);
|
|
692
752
|
emitValue(w, it, indent + 2);
|
|
693
|
-
|
|
753
|
+
const next = items.slice(k + 1).find((x) => 'note' !== x.t);
|
|
754
|
+
if (undefined !== next && next.sep) {
|
|
694
755
|
w.text(',');
|
|
695
756
|
}
|
|
696
757
|
noted = false;
|
|
@@ -698,10 +759,41 @@ function emitCall(w, node, indent) {
|
|
|
698
759
|
w.open(indent, false);
|
|
699
760
|
w.text(')');
|
|
700
761
|
}
|
|
762
|
+
// Whether a node holds a container anywhere: the argument has a
|
|
763
|
+
// several-line form of its own.
|
|
764
|
+
function holdsContainer(node) {
|
|
765
|
+
switch (node.t) {
|
|
766
|
+
case 'map':
|
|
767
|
+
case 'list':
|
|
768
|
+
return true;
|
|
769
|
+
case 'call':
|
|
770
|
+
return node.args.some(holdsContainer);
|
|
771
|
+
case 'paren':
|
|
772
|
+
return node.inner.some(holdsContainer);
|
|
773
|
+
case 'expr':
|
|
774
|
+
return node.items.some(holdsContainer);
|
|
775
|
+
default:
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
// Whether a last argument hugs the parentheses: a container; an
|
|
780
|
+
// expression with no break and no comment whose last operand is one;
|
|
781
|
+
// a call whose own last argument does.
|
|
782
|
+
function hugs(node) {
|
|
783
|
+
if ('map' === node.t || 'list' === node.t) {
|
|
784
|
+
return true;
|
|
785
|
+
}
|
|
786
|
+
if ('call' === node.t) {
|
|
787
|
+
return 0 < node.args.length && hugs(node.args[node.args.length - 1]);
|
|
788
|
+
}
|
|
789
|
+
return 'expr' === node.t &&
|
|
790
|
+
node.items.every((it) => 'note' !== it.t && !('op' === it.t && it.brk)) &&
|
|
791
|
+
hugs(node.items[node.items.length - 1]);
|
|
792
|
+
}
|
|
701
793
|
// A container on several lines (§3.5): the opener ends its line, the
|
|
702
794
|
// entries are statements one level in, the closer stands alone. An
|
|
703
795
|
// empty container is inline whatever the budget says.
|
|
704
|
-
function emitBlock(w, open, close, node, indent) {
|
|
796
|
+
function emitBlock(w, open, close, node, indent, stmt) {
|
|
705
797
|
if (0 === node.body.length && undefined === node.open) {
|
|
706
798
|
w.text(open + close);
|
|
707
799
|
return;
|
|
@@ -710,7 +802,7 @@ function emitBlock(w, open, close, node, indent) {
|
|
|
710
802
|
if (undefined !== node.open) {
|
|
711
803
|
w.text(' ' + node.open);
|
|
712
804
|
}
|
|
713
|
-
emitBody(w, node.body, indent + 2);
|
|
805
|
+
emitBody(w, node.body, indent + 2, stmt);
|
|
714
806
|
w.open(indent, false);
|
|
715
807
|
w.text(close);
|
|
716
808
|
}
|
|
@@ -765,23 +857,420 @@ function emitExpr(w, items, indent) {
|
|
|
765
857
|
operand = true;
|
|
766
858
|
}
|
|
767
859
|
}
|
|
768
|
-
|
|
860
|
+
// The entries of a plain map value: a braced map, or a chain, which is
|
|
861
|
+
// a one-entry map. A map with a comment on its opener keeps its braces
|
|
862
|
+
// (§3.7), so it is not plain here; nor is a map holding an include,
|
|
863
|
+
// which the local check cannot follow.
|
|
864
|
+
function plainEntries(v) {
|
|
865
|
+
if ('pair' === v.t) {
|
|
866
|
+
return [v];
|
|
867
|
+
}
|
|
868
|
+
if ('map' !== v.t || undefined !== v.open || v.body.some((e) => 'include' === e.t)) {
|
|
869
|
+
return undefined;
|
|
870
|
+
}
|
|
871
|
+
return v.body;
|
|
872
|
+
}
|
|
873
|
+
// The entries of a statement as they stand once it is merged into a
|
|
874
|
+
// wider map: its trailing comment sunk onto its last entry, so that it
|
|
875
|
+
// travels with the entry it stood beside. Undefined where the value is
|
|
876
|
+
// not a plain map, or the comment has no entry to sit on.
|
|
877
|
+
function members(p) {
|
|
878
|
+
const entries = plainEntries(p.value);
|
|
879
|
+
if (undefined === entries || undefined === p.trail) {
|
|
880
|
+
return entries;
|
|
881
|
+
}
|
|
882
|
+
const last = entries[entries.length - 1];
|
|
883
|
+
if (undefined === last || ('pair' !== last.t && 'spread' !== last.t)) {
|
|
884
|
+
return undefined;
|
|
885
|
+
}
|
|
886
|
+
const trail = undefined === last.trail ? p.trail : last.trail + ' ' + p.trail;
|
|
887
|
+
return entries.slice(0, -1).concat([{ ...last, trail }]);
|
|
888
|
+
}
|
|
889
|
+
// Adjacent statements naming one key, whose values are plain maps, are
|
|
890
|
+
// one map: their entries in order, with the comments and blank lines
|
|
891
|
+
// between the statements travelling with the statement they preceded.
|
|
892
|
+
// Only ADJACENT statements merge -- a `server:` line, something else,
|
|
893
|
+
// then another `server:` line stays as it is, because merging them
|
|
894
|
+
// would move a statement, and the formatter never reorders (§3.13).
|
|
895
|
+
// Nor do two statements merge into a map with two spreads: the engine
|
|
896
|
+
// keeps those as a conjunction, which is not the meet of the two maps.
|
|
897
|
+
// The tree is not changed: a merged statement is a new node that keeps
|
|
898
|
+
// the statements it replaces as its `orig`, its spelling before, and a
|
|
899
|
+
// statement merged somewhere below is copied the same way.
|
|
900
|
+
function mergeRuns(body) {
|
|
901
|
+
const out = [];
|
|
902
|
+
let i = 0;
|
|
903
|
+
while (i < body.length) {
|
|
904
|
+
const first = body[i];
|
|
905
|
+
const entries = 'pair' === first.t ? members(first) : undefined;
|
|
906
|
+
if (undefined === entries) {
|
|
907
|
+
out.push('pair' === first.t ? mergeDeep(first) : first);
|
|
908
|
+
i++;
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
const group = [first];
|
|
912
|
+
let merged = entries;
|
|
913
|
+
let carry = [];
|
|
914
|
+
let j = i + 1;
|
|
915
|
+
for (; j < body.length; j++) {
|
|
916
|
+
const n = body[j];
|
|
917
|
+
if ('comment' === n.t || 'blank' === n.t) {
|
|
918
|
+
carry.push(n);
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
const more = 'pair' === n.t && n.key === first.key && n.opt === first.opt
|
|
922
|
+
? members(n) : undefined;
|
|
923
|
+
if (undefined === more || (spreads(merged) && spreads(more))) {
|
|
924
|
+
break;
|
|
925
|
+
}
|
|
926
|
+
group.push(...carry, n);
|
|
927
|
+
merged = merged.concat(carry, more);
|
|
928
|
+
carry = [];
|
|
929
|
+
}
|
|
930
|
+
if (1 === group.length) {
|
|
931
|
+
out.push(mergeDeep(first));
|
|
932
|
+
i++;
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
out.push({
|
|
936
|
+
t: 'pair', key: first.key, opt: first.opt, alias: first.alias,
|
|
937
|
+
value: { t: 'map', body: mergeRuns(merged) }, orig: group,
|
|
938
|
+
});
|
|
939
|
+
i = j - carry.length;
|
|
940
|
+
}
|
|
941
|
+
return out;
|
|
942
|
+
}
|
|
943
|
+
function spreads(entries) {
|
|
944
|
+
return entries.some((e) => 'spread' === e.t);
|
|
945
|
+
}
|
|
946
|
+
// The merge down a statement's plain-map spine: a chain's inner pair,
|
|
947
|
+
// or the entries of a map value, are statements of the map they are
|
|
948
|
+
// in. The statement itself where nothing below it merged.
|
|
949
|
+
function mergeDeep(p) {
|
|
950
|
+
const v = p.value;
|
|
951
|
+
const entries = plainEntries(v);
|
|
952
|
+
if (undefined === entries) {
|
|
953
|
+
return p;
|
|
954
|
+
}
|
|
955
|
+
const body = mergeRuns(entries);
|
|
956
|
+
if (body.length === entries.length && body.every((n, k) => n === entries[k])) {
|
|
957
|
+
return p;
|
|
958
|
+
}
|
|
959
|
+
return { ...p, value: 'pair' === v.t ? body[0] : { ...v, body }, orig: [p] };
|
|
960
|
+
}
|
|
961
|
+
function repeatLines(entries, prefix, indent) {
|
|
962
|
+
if (0 === entries.length || 'comment' === entries[entries.length - 1].t ||
|
|
963
|
+
1 < entries.filter((e) => 'spread' === e.t).length) {
|
|
964
|
+
return undefined;
|
|
965
|
+
}
|
|
966
|
+
const out = [];
|
|
967
|
+
for (const e of entries) {
|
|
968
|
+
if ('blank' === e.t) {
|
|
969
|
+
out.push({ t: 'blank' });
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
if ('comment' === e.t) {
|
|
973
|
+
out.push({ t: 'comment', text: e.text });
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
const trail = undefined === e.trail ? '' : ' ' + e.trail;
|
|
977
|
+
if ('spread' === e.t) {
|
|
978
|
+
// The repeated spread entry is a one-entry map holding only a
|
|
979
|
+
// spread, so by D1's exception it keeps its braces.
|
|
980
|
+
const s = inline(e.value, true);
|
|
981
|
+
if (undefined === s || !fits(indent, prefix + '{ &: ' + s + ' }')) {
|
|
982
|
+
return undefined;
|
|
983
|
+
}
|
|
984
|
+
out.push({ t: 'text', text: prefix + '{ &: ' + s + ' }' + trail });
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
const head = prefix + pairHead(e, false);
|
|
988
|
+
const s = inline(chain(e.value), false);
|
|
989
|
+
if (undefined !== s && fits(indent, head + s)) {
|
|
990
|
+
out.push({ t: 'text', text: head + s + trail });
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
const sub = plainEntries(e.value);
|
|
994
|
+
if (undefined === sub) {
|
|
995
|
+
return undefined;
|
|
996
|
+
}
|
|
997
|
+
const lines = repeatLines(sub, head, indent);
|
|
998
|
+
if (undefined === lines) {
|
|
999
|
+
return undefined;
|
|
1000
|
+
}
|
|
1001
|
+
if ('' !== trail) {
|
|
1002
|
+
lines[lines.length - 1].text += trail;
|
|
1003
|
+
}
|
|
1004
|
+
out.push(...lines);
|
|
1005
|
+
}
|
|
1006
|
+
return out;
|
|
1007
|
+
}
|
|
1008
|
+
function fits(indent, text) {
|
|
1009
|
+
return indent + width(text) <= BUDGET;
|
|
1010
|
+
}
|
|
1011
|
+
// A pair in statement position, by §3.4. `prefix` is what stands
|
|
1012
|
+
// before it on its line: the heads of the chain it hangs from, not yet
|
|
1013
|
+
// written. Its value is laid out by §3.5 unless it is a plain map, and
|
|
1014
|
+
// then in this order: a chain, when the map holds exactly one pair
|
|
1015
|
+
// (D1); one line, when that fits the budget; the key repeated over the
|
|
1016
|
+
// entries, when every entry can be one line that way; a braced block
|
|
1017
|
+
// otherwise, whose entries are statements in turn. Whether the
|
|
1018
|
+
// statement was rewritten by this tier -- merged, or repeated -- is
|
|
1019
|
+
// returned, and the outermost such statement is checked: its spelling
|
|
1020
|
+
// on the page against what the syntactic tier writes for the
|
|
1021
|
+
// statements it came from, at the same indentation, which is what
|
|
1022
|
+
// stays on the page when the check fails.
|
|
1023
|
+
function emitStatement(w, p, indent, stmt, prefix) {
|
|
1024
|
+
const mark = w.mark();
|
|
1025
|
+
let rewritten = undefined !== p.orig;
|
|
1026
|
+
const entries = plainEntries(p.value);
|
|
1027
|
+
const head = prefix + pairHead(p, false);
|
|
1028
|
+
const s = undefined === entries ? undefined : inline(p.value, false);
|
|
1029
|
+
if (undefined === entries) {
|
|
1030
|
+
w.text(prefix);
|
|
1031
|
+
emitValue(w, p, indent);
|
|
1032
|
+
}
|
|
1033
|
+
else if (1 === entries.length && 'pair' === entries[0].t) {
|
|
1034
|
+
rewritten = emitStatement(w, entries[0], indent, { meet: stmt.meet, covered: true }, head)
|
|
1035
|
+
|| rewritten;
|
|
1036
|
+
}
|
|
1037
|
+
else if (undefined !== s && fits(indent, head + s)) {
|
|
1038
|
+
w.text(head + s);
|
|
1039
|
+
}
|
|
1040
|
+
else {
|
|
1041
|
+
const lines = repeatLines(entries, head, indent);
|
|
1042
|
+
if (undefined !== lines) {
|
|
1043
|
+
let pending = false;
|
|
1044
|
+
let count = 0;
|
|
1045
|
+
for (const line of lines) {
|
|
1046
|
+
if ('blank' === line.t) {
|
|
1047
|
+
pending = 0 < count;
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
if (0 < count) {
|
|
1051
|
+
w.open(indent, pending);
|
|
1052
|
+
}
|
|
1053
|
+
pending = false;
|
|
1054
|
+
count++;
|
|
1055
|
+
w.text(line.text);
|
|
1056
|
+
}
|
|
1057
|
+
rewritten = true;
|
|
1058
|
+
}
|
|
1059
|
+
else {
|
|
1060
|
+
w.text(head);
|
|
1061
|
+
emitBlock(w, '{', '}', p.value, indent, { meet: stmt.meet, covered: stmt.covered || rewritten });
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
if (undefined !== p.trail) {
|
|
1065
|
+
w.text(' ' + p.trail);
|
|
1066
|
+
}
|
|
1067
|
+
if (rewritten && !stmt.covered) {
|
|
1068
|
+
const before = emitAt(p.orig ?? [p], indent);
|
|
1069
|
+
if (!stmt.meet(before, w.since(mark))) {
|
|
1070
|
+
w.replace(mark, before);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
return rewritten;
|
|
1074
|
+
}
|
|
1075
|
+
// The syntactic tier's spelling of some statements at an indentation:
|
|
1076
|
+
// a rewrite's spelling before.
|
|
1077
|
+
function emitAt(nodes, indent) {
|
|
1078
|
+
const w = new Writer();
|
|
1079
|
+
emitBody(w, nodes, indent, undefined);
|
|
1080
|
+
return w.finish();
|
|
1081
|
+
}
|
|
1082
|
+
// The document: by the syntactic tier alone, or with the lawful tier
|
|
1083
|
+
// over it when given its check.
|
|
1084
|
+
function emit(root, meet) {
|
|
769
1085
|
const w = new Writer();
|
|
770
|
-
emitBody(w, root, 0);
|
|
1086
|
+
emitBody(w, undefined === meet ? root : mergeRuns(root), 0, undefined === meet ? undefined : { meet, covered: false });
|
|
771
1087
|
return w.finish();
|
|
772
1088
|
}
|
|
773
1089
|
// ---------------------------------------------------------------------
|
|
1090
|
+
// The lint (§4): what the formatter points at and never touches. Two
|
|
1091
|
+
// rules, both advice: the formatter never renames a key (§4.1) and
|
|
1092
|
+
// never introduces an alias (§4.2), and a rule with a mechanical fix
|
|
1093
|
+
// that keeps the document would belong to §3 instead (§4.3).
|
|
1094
|
+
// The shape width at which a repeat is worth an alias (§4.2): below
|
|
1095
|
+
// it, `{ a:1 }` twice is the shorter spelling. Measured over the use
|
|
1096
|
+
// cases when the lint landed (§7.10).
|
|
1097
|
+
const REPEAT_MIN_WIDTH = 40;
|
|
1098
|
+
function lintOf(root, text) {
|
|
1099
|
+
const out = [];
|
|
1100
|
+
const nodes = root.map(lintNode);
|
|
1101
|
+
for (const n of nodes) {
|
|
1102
|
+
keyCase(n, text, out);
|
|
1103
|
+
}
|
|
1104
|
+
repeats(nodes, text, out);
|
|
1105
|
+
out.sort((a, b) => a.line - b.line || a.col - b.col);
|
|
1106
|
+
return out;
|
|
1107
|
+
}
|
|
1108
|
+
// The tree the lint walks: a chain's inner pair as the one-entry map
|
|
1109
|
+
// it is, so that `a: {b: 1}` and `a: b: 1` -- one document to the
|
|
1110
|
+
// formatter -- are one shape to the lint.
|
|
1111
|
+
function lintNode(node) {
|
|
1112
|
+
if ('pair' === node.t && 'pair' === node.value.t) {
|
|
1113
|
+
return { ...node, value: { t: 'map', body: [node.value], at: node.value.at } };
|
|
1114
|
+
}
|
|
1115
|
+
return node;
|
|
1116
|
+
}
|
|
1117
|
+
function lintChildren(node) {
|
|
1118
|
+
switch (node.t) {
|
|
1119
|
+
case 'pair':
|
|
1120
|
+
case 'spread':
|
|
1121
|
+
return [lintNode(node).value];
|
|
1122
|
+
case 'map':
|
|
1123
|
+
case 'list':
|
|
1124
|
+
return node.body.map(lintNode);
|
|
1125
|
+
case 'call':
|
|
1126
|
+
return node.args;
|
|
1127
|
+
case 'paren':
|
|
1128
|
+
return node.inner;
|
|
1129
|
+
case 'expr':
|
|
1130
|
+
return node.items;
|
|
1131
|
+
default:
|
|
1132
|
+
return [];
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
// Line and column, 1-based, of a source index.
|
|
1136
|
+
function lineCol(text, at) {
|
|
1137
|
+
const before = text.slice(0, at);
|
|
1138
|
+
return { line: before.split('\n').length, col: at - before.lastIndexOf('\n') };
|
|
1139
|
+
}
|
|
1140
|
+
// D4 (§4.1): keys are lower-case words, or CamelCase when a key is
|
|
1141
|
+
// several. A bare key holding `_`, or beginning with two capitals, is
|
|
1142
|
+
// reported with the spelling that would follow the form; a quoted key
|
|
1143
|
+
// is a deliberate spelling and a key of underscores alone names
|
|
1144
|
+
// nothing the rule can respell.
|
|
1145
|
+
function keyCase(node, text, out) {
|
|
1146
|
+
if ('pair' === node.t && BARE.test(node.key) && /[A-Za-z]/.test(node.key)) {
|
|
1147
|
+
const why = node.key.includes('_') ? 'holds an underscore'
|
|
1148
|
+
: /^[A-Z][A-Z]/.test(node.key) ? 'begins with capitals' : '';
|
|
1149
|
+
if ('' !== why) {
|
|
1150
|
+
out.push({
|
|
1151
|
+
rule: 'style/key-case', ...lineCol(text, node.at),
|
|
1152
|
+
message: `key ${node.key} ${why}; ${camel(node.key)} would follow the form`,
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
for (const child of lintChildren(node)) {
|
|
1157
|
+
keyCase(child, text, out);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
// The key as lower-case words or CamelCase: `credit_cents` is
|
|
1161
|
+
// `creditCents`, `HTTP_PORT` is `httpPort`, `HTTPServer` is
|
|
1162
|
+
// `httpServer`, `ID` is `id`.
|
|
1163
|
+
function camel(key) {
|
|
1164
|
+
const words = key.split('_').filter((w) => '' !== w)
|
|
1165
|
+
.map((w) => /^[A-Z]+$/.test(w) ? w.toLowerCase() : w);
|
|
1166
|
+
const head = words[0].replace(/^[A-Z]+(?=[A-Z][a-z])/, (run) => run.toLowerCase());
|
|
1167
|
+
return head.charAt(0).toLowerCase() + head.slice(1) +
|
|
1168
|
+
words.slice(1).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join('');
|
|
1169
|
+
}
|
|
1170
|
+
// D3 (§4.2): a shape written twice can drift, and an alias names it
|
|
1171
|
+
// once. Every map or list whose shape recurs in the file, and whose
|
|
1172
|
+
// shape is REPEAT_MIN_WIDTH or wider, is reported once, at its first
|
|
1173
|
+
// site, with the count and the other sites; the naming is the
|
|
1174
|
+
// author's. A repeat inside a repeat is the outer one's: the walk does
|
|
1175
|
+
// not descend into a shape it reports.
|
|
1176
|
+
function repeats(nodes, text, out) {
|
|
1177
|
+
const counts = new Map();
|
|
1178
|
+
const tally = (node) => {
|
|
1179
|
+
if ('map' === node.t || 'list' === node.t) {
|
|
1180
|
+
const s = shape(node);
|
|
1181
|
+
counts.set(s, (counts.get(s) ?? 0) + 1);
|
|
1182
|
+
}
|
|
1183
|
+
lintChildren(node).forEach(tally);
|
|
1184
|
+
};
|
|
1185
|
+
nodes.forEach(tally);
|
|
1186
|
+
const sites = new Map();
|
|
1187
|
+
const visit = (node) => {
|
|
1188
|
+
if ('map' === node.t || 'list' === node.t) {
|
|
1189
|
+
const s = shape(node);
|
|
1190
|
+
if (2 <= counts.get(s) && REPEAT_MIN_WIDTH <= width(s)) {
|
|
1191
|
+
sites.set(s, (sites.get(s) ?? []).concat([node]));
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
lintChildren(node).forEach(visit);
|
|
1196
|
+
};
|
|
1197
|
+
nodes.forEach(visit);
|
|
1198
|
+
for (const found of sites.values()) {
|
|
1199
|
+
if (2 <= found.length) {
|
|
1200
|
+
const [first, ...rest] = found.map((n) => lineCol(text, n.at));
|
|
1201
|
+
out.push({
|
|
1202
|
+
rule: 'style/repeat', ...first,
|
|
1203
|
+
message: `this ${found[0].t} is written ${found.length} times (again at ` +
|
|
1204
|
+
rest.map((p) => p.line + ':' + p.col).join(', ') +
|
|
1205
|
+
'); an alias would name it once',
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
// A node's shape: its spelling with the layout, the comments and, for
|
|
1211
|
+
// a map, the order of its entries taken out, so that two spellings of
|
|
1212
|
+
// one value are one shape, as they are one canon.
|
|
1213
|
+
function shape(node) {
|
|
1214
|
+
switch (node.t) {
|
|
1215
|
+
case 'map':
|
|
1216
|
+
return '{' + node.body.filter(shaped).map((e) => shape(lintNode(e))).sort().join(' ') + '}';
|
|
1217
|
+
case 'list':
|
|
1218
|
+
return '[' + node.body.filter(shaped).map((e) => shape(lintNode(e))).join(' ') + ']';
|
|
1219
|
+
case 'pair':
|
|
1220
|
+
return node.key + (node.opt ? '?' : '') + ':' + shape(node.value);
|
|
1221
|
+
case 'spread':
|
|
1222
|
+
return '&:' + shape(node.value);
|
|
1223
|
+
case 'call':
|
|
1224
|
+
return node.name + '(' + node.args.filter(shaped).map(shape).join(',') + ')';
|
|
1225
|
+
case 'paren':
|
|
1226
|
+
return '(' + node.inner.filter(shaped).map(shape).join(',') + ')';
|
|
1227
|
+
case 'expr':
|
|
1228
|
+
return node.items.filter(shaped).map(shape).join('');
|
|
1229
|
+
default:
|
|
1230
|
+
return node.text;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
function shaped(node) {
|
|
1234
|
+
return 'comment' !== node.t && 'blank' !== node.t && 'note' !== node.t;
|
|
1235
|
+
}
|
|
1236
|
+
// ---------------------------------------------------------------------
|
|
774
1237
|
// The verb's library surface
|
|
775
1238
|
function lf(text) {
|
|
776
1239
|
return text.split('\r\n').join('\n');
|
|
777
1240
|
}
|
|
778
1241
|
// The check: the output parses, and to the same tree. Pre-unification
|
|
779
|
-
// canon is that tree, positions aside, and every rewrite of
|
|
780
|
-
// leaves it unchanged (§7.3).
|
|
1242
|
+
// canon is that tree, positions aside, and every rewrite of the
|
|
1243
|
+
// syntactic tier leaves it unchanged (§7.3).
|
|
781
1244
|
function sameDocument(root, after) {
|
|
782
1245
|
const p = parseDoc(after, undefined, undefined);
|
|
783
1246
|
return undefined === p.errors && root.canon === p.root.canon;
|
|
784
1247
|
}
|
|
1248
|
+
// The check of a lawful rewrite: the spelling before and the spelling
|
|
1249
|
+
// after, evaluated in isolation, come to the same canon, the same
|
|
1250
|
+
// kinds of failure, and the same outcome of generation (§7.3). Local,
|
|
1251
|
+
// so it needs no include and no capability, and it applies whether or
|
|
1252
|
+
// not the document as a whole evaluates. The kinds, not the count: how
|
|
1253
|
+
// often one unresolved reference is reported depends on the order the
|
|
1254
|
+
// meet took. Generation too, because the engine generates from more
|
|
1255
|
+
// than the canon: a meet of maps with a nil member has refused a key
|
|
1256
|
+
// the same map written once generates.
|
|
1257
|
+
function sameByMeet(before, after) {
|
|
1258
|
+
return meetOf(before) === meetOf(after);
|
|
1259
|
+
}
|
|
1260
|
+
function meetOf(text) {
|
|
1261
|
+
const aontu = engine();
|
|
1262
|
+
const ctx = aontu.ctx({ collect: true });
|
|
1263
|
+
const v = aontu.unify(text, undefined, ctx);
|
|
1264
|
+
const gen = aontu.ctx({ collect: true });
|
|
1265
|
+
const out = aontu.generate(text, undefined, gen);
|
|
1266
|
+
const outcome = undefined !== out ? 'generated'
|
|
1267
|
+
: 0 < ctx.err.length ? kinds(ctx.err) : gen.err[0].why;
|
|
1268
|
+
return v.canon + '\n' + kinds(ctx.err) + '\n' + outcome;
|
|
1269
|
+
}
|
|
1270
|
+
function kinds(errs) {
|
|
1271
|
+
const whys = errs.map((e) => e.why);
|
|
1272
|
+
return whys.filter((x, i) => i === whys.indexOf(x)).sort().join(',');
|
|
1273
|
+
}
|
|
785
1274
|
function depthFinding() {
|
|
786
1275
|
return {
|
|
787
1276
|
code: 'max_depth',
|
|
@@ -817,19 +1306,25 @@ function format(src, opts, hooks) {
|
|
|
817
1306
|
return { verdict: 'error', errors: parsed.errors };
|
|
818
1307
|
}
|
|
819
1308
|
const reader = new Reader(toks);
|
|
820
|
-
const root = reader.body('', false).body;
|
|
1309
|
+
const root = unwrap(reader.body('', false).body);
|
|
821
1310
|
if (reader.deep) {
|
|
822
1311
|
return { verdict: 'error', errors: [depthFinding()] };
|
|
823
1312
|
}
|
|
824
|
-
|
|
1313
|
+
// The syntactic tier first, checked against the parse tree; then the
|
|
1314
|
+
// lawful tier over it, each rewrite checked by the meet.
|
|
1315
|
+
const plain = emit(root, undefined);
|
|
825
1316
|
const same = hooks?.same ?? sameDocument;
|
|
826
|
-
if (!same(parsed.root,
|
|
1317
|
+
if (!same(parsed.root, plain)) {
|
|
827
1318
|
return {
|
|
828
1319
|
verdict: 'error',
|
|
829
|
-
errors: [checkFinding(opts?.path, parsed.root.canon,
|
|
1320
|
+
errors: [checkFinding(opts?.path, parsed.root.canon, plain)],
|
|
830
1321
|
};
|
|
831
1322
|
}
|
|
832
|
-
|
|
1323
|
+
const out = emit(root, hooks?.meet ?? sameByMeet);
|
|
1324
|
+
return {
|
|
1325
|
+
verdict: 'formatted', text: out, changed: out !== src,
|
|
1326
|
+
findings: opts?.lint ? lintOf(root, text) : [],
|
|
1327
|
+
};
|
|
833
1328
|
}
|
|
834
1329
|
// The lines of a text, with a marker on the last when the text does
|
|
835
1330
|
// not end in a newline: such a line never equals its
|