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/src/format.ts
CHANGED
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
// trees: a formatter that cannot prove its output is the same document
|
|
14
14
|
// refuses rather than return it.
|
|
15
15
|
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// tree. The lawful
|
|
19
|
-
//
|
|
16
|
+
// Two tiers. The syntactic (P1): whitespace, commas, quotes, bare
|
|
17
|
+
// keys, chains and pair elements, none of which changes the parse
|
|
18
|
+
// tree. The lawful (P2), over it: repeat the prefix, and merge what
|
|
19
|
+
// repeats -- rewrites that rest on the meet, each checked by the meet
|
|
20
|
+
// in isolation and kept only where the engine agrees.
|
|
20
21
|
//
|
|
21
22
|
// The Go twin is go/format.go, function for function; the shared
|
|
22
23
|
// behaviour is test/spec/fmt.tsv, executed by both spec runners.
|
|
@@ -42,6 +43,18 @@ const MAX_DEPTH = 1000
|
|
|
42
43
|
export type FormatOptions = {
|
|
43
44
|
// The file's name, for the site of a parse failure.
|
|
44
45
|
path?: string
|
|
46
|
+
// Report the style findings of §4 -- key case, repeated shapes --
|
|
47
|
+
// beside the text. The formatter never acts on them.
|
|
48
|
+
lint?: boolean
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// A style finding (§4): what the formatter points at and never
|
|
52
|
+
// touches. `line` and `col` are 1-based, of the key or the container.
|
|
53
|
+
export type LintFinding = {
|
|
54
|
+
rule: 'style/key-case' | 'style/repeat'
|
|
55
|
+
line: number
|
|
56
|
+
col: number
|
|
57
|
+
message: string
|
|
45
58
|
}
|
|
46
59
|
|
|
47
60
|
// The self-check, injectable so the refusal it guards can be exercised
|
|
@@ -49,10 +62,11 @@ export type FormatOptions = {
|
|
|
49
62
|
// takes that arm on its own.
|
|
50
63
|
export type FormatHooks = {
|
|
51
64
|
same?: (root: any, after: string) => boolean
|
|
65
|
+
meet?: (before: string, after: string) => boolean
|
|
52
66
|
}
|
|
53
67
|
|
|
54
68
|
export type FormatReport =
|
|
55
|
-
| { verdict: 'formatted', text: string, changed: boolean }
|
|
69
|
+
| { verdict: 'formatted', text: string, changed: boolean, findings: LintFinding[] }
|
|
56
70
|
| { verdict: 'error', errors: VetFinding[] }
|
|
57
71
|
|
|
58
72
|
|
|
@@ -134,6 +148,10 @@ type Node = {
|
|
|
134
148
|
// value; spread: the value.
|
|
135
149
|
key?: string
|
|
136
150
|
opt?: boolean
|
|
151
|
+
// pair: written with `=`, the alias declaration operator, rather than
|
|
152
|
+
// a colon. The spelling is the parse's -- a colon after an alias name
|
|
153
|
+
// is a refused document, and the formatter keeps it one.
|
|
154
|
+
alias?: boolean
|
|
137
155
|
value?: Node
|
|
138
156
|
|
|
139
157
|
// map, list: the entries, and the comment on the opener's line.
|
|
@@ -155,6 +173,16 @@ type Node = {
|
|
|
155
173
|
|
|
156
174
|
// A comment on the last line of this entry.
|
|
157
175
|
trail?: string
|
|
176
|
+
|
|
177
|
+
// An argument: a comma stood before it (§3.6), rather than a space.
|
|
178
|
+
sep?: boolean
|
|
179
|
+
|
|
180
|
+
// pair: the statements this one replaces, where the lawful tier
|
|
181
|
+
// merged them, or rewrote something below them.
|
|
182
|
+
orig?: Node[]
|
|
183
|
+
|
|
184
|
+
// The source index of the node's first token: the lint's positions.
|
|
185
|
+
at?: number
|
|
158
186
|
}
|
|
159
187
|
|
|
160
188
|
const BINARY: Record<string, boolean> = { '#E&': true, '#E|': true, '#E+': true }
|
|
@@ -300,6 +328,11 @@ class Reader {
|
|
|
300
328
|
opener = false
|
|
301
329
|
gap = false
|
|
302
330
|
}
|
|
331
|
+
// A blank line before the closer is no paragraph break: nothing
|
|
332
|
+
// follows it, and the layout would drop it anyway.
|
|
333
|
+
while (0 < body.length && 'blank' === body[body.length - 1].t) {
|
|
334
|
+
body.pop()
|
|
335
|
+
}
|
|
303
336
|
return { body, open }
|
|
304
337
|
}
|
|
305
338
|
|
|
@@ -307,20 +340,22 @@ class Reader {
|
|
|
307
340
|
// at the root -- a value.
|
|
308
341
|
entry(): Node {
|
|
309
342
|
const n = this.name(0)
|
|
343
|
+
const at = this.T[this.i].sI
|
|
310
344
|
if ('#OD_multisource' === n) {
|
|
311
345
|
const text = '@' + normStr(this.T[this.i + 1].src)
|
|
312
346
|
this.i += 2
|
|
313
|
-
return { t: 'include', text }
|
|
347
|
+
return { t: 'include', text, at }
|
|
314
348
|
}
|
|
315
349
|
if ('#E&' === n && '#CL' === this.name(1)) {
|
|
316
350
|
this.i += 2
|
|
317
|
-
return { t: 'spread', value: this.value() }
|
|
351
|
+
return { t: 'spread', value: this.value(), at }
|
|
318
352
|
}
|
|
319
353
|
if (this.atKey()) {
|
|
320
354
|
const tok = this.T[this.i]
|
|
321
355
|
const opt = '#QM' === this.name(1)
|
|
356
|
+
const alias = '=' === this.T[this.i + (opt ? 2 : 1)].src
|
|
322
357
|
this.i += opt ? 3 : 2
|
|
323
|
-
return { t: 'pair', key: keyText(tok), opt, value: this.value() }
|
|
358
|
+
return { t: 'pair', key: keyText(tok), opt, alias, value: this.value(), at }
|
|
324
359
|
}
|
|
325
360
|
return this.value()
|
|
326
361
|
}
|
|
@@ -349,12 +384,13 @@ class Reader {
|
|
|
349
384
|
if (!this.open(items) && !BINARY[n] && '#LN' !== n && '#CM' !== n) {
|
|
350
385
|
break
|
|
351
386
|
}
|
|
387
|
+
const at = this.T[this.i].sI
|
|
352
388
|
if ('#E&' === n && '#CL' === this.name(1)) {
|
|
353
389
|
if (0 === items.length) {
|
|
354
390
|
// A chain through a spread, `a: &: integer`. The braces are
|
|
355
391
|
// the agreed spelling (X-7), so it is read as the map it is.
|
|
356
392
|
this.i += 2
|
|
357
|
-
return { t: 'map', body: [{ t: 'spread', value: this.value() }] }
|
|
393
|
+
return { t: 'map', body: [{ t: 'spread', value: this.value(), at }], at }
|
|
358
394
|
}
|
|
359
395
|
// A sibling spread in a list, `[1 &: 2]`: this value is complete.
|
|
360
396
|
break
|
|
@@ -374,7 +410,7 @@ class Reader {
|
|
|
374
410
|
// operator, or on a line the value continues past. Otherwise
|
|
375
411
|
// it trails the statement and the caller attaches it.
|
|
376
412
|
if (this.open(items) || BINARY[this.name(this.significant())]) {
|
|
377
|
-
items.push({ t: 'note', text: this.T[this.i].src })
|
|
413
|
+
items.push({ t: 'note', text: this.T[this.i].src, at })
|
|
378
414
|
this.i++
|
|
379
415
|
continue
|
|
380
416
|
}
|
|
@@ -383,13 +419,13 @@ class Reader {
|
|
|
383
419
|
if (BINARY[n]) {
|
|
384
420
|
items.push({
|
|
385
421
|
t: 'op', text: this.T[this.i].src,
|
|
386
|
-
brk: '#LN' === this.name(-1) || '#LN' === this.name(1),
|
|
422
|
+
brk: '#LN' === this.name(-1) || '#LN' === this.name(1), at,
|
|
387
423
|
})
|
|
388
424
|
this.i++
|
|
389
425
|
continue
|
|
390
426
|
}
|
|
391
427
|
if (PREFIX[n]) {
|
|
392
|
-
items.push({ t: 'prefix', text: this.T[this.i].src })
|
|
428
|
+
items.push({ t: 'prefix', text: this.T[this.i].src, at })
|
|
393
429
|
this.i++
|
|
394
430
|
continue
|
|
395
431
|
}
|
|
@@ -397,7 +433,7 @@ class Reader {
|
|
|
397
433
|
this.i++
|
|
398
434
|
const inner = this.seq()
|
|
399
435
|
this.i++
|
|
400
|
-
items.push({ t: 'paren', inner })
|
|
436
|
+
items.push({ t: 'paren', inner, at })
|
|
401
437
|
continue
|
|
402
438
|
}
|
|
403
439
|
if ('#TX' === n && '#E(' === this.name(1)) {
|
|
@@ -405,25 +441,25 @@ class Reader {
|
|
|
405
441
|
this.i += 2
|
|
406
442
|
const args = this.seq()
|
|
407
443
|
this.i++
|
|
408
|
-
items.push({ t: 'call', name, args })
|
|
444
|
+
items.push({ t: 'call', name, args, at })
|
|
409
445
|
continue
|
|
410
446
|
}
|
|
411
447
|
if ('#OB' === n) {
|
|
412
448
|
this.i++
|
|
413
449
|
const m = this.body('#CB', true)
|
|
414
450
|
this.i++
|
|
415
|
-
items.push({ t: 'map', body: m.body, open: m.open })
|
|
451
|
+
items.push({ t: 'map', body: m.body, open: m.open, at })
|
|
416
452
|
continue
|
|
417
453
|
}
|
|
418
454
|
if ('#OS' === n) {
|
|
419
455
|
this.i++
|
|
420
456
|
const l = this.body('#CS', true)
|
|
421
457
|
this.i++
|
|
422
|
-
items.push({ t: 'list', body: l.body, open: l.open })
|
|
458
|
+
items.push({ t: 'list', body: l.body, open: l.open, at })
|
|
423
459
|
continue
|
|
424
460
|
}
|
|
425
461
|
if ('#OD_multisource' === n) {
|
|
426
|
-
items.push({ t: 'include', text: '@' + normStr(this.T[this.i + 1].src) })
|
|
462
|
+
items.push({ t: 'include', text: '@' + normStr(this.T[this.i + 1].src), at })
|
|
427
463
|
this.i += 2
|
|
428
464
|
continue
|
|
429
465
|
}
|
|
@@ -439,7 +475,8 @@ class Reader {
|
|
|
439
475
|
'note' !== items[0].t) {
|
|
440
476
|
return items[0]
|
|
441
477
|
}
|
|
442
|
-
|
|
478
|
+
// An empty value, `a:`, is an expression with nothing in it.
|
|
479
|
+
return { t: 'expr', items, at: items[0]?.at }
|
|
443
480
|
}
|
|
444
481
|
|
|
445
482
|
// Whether the expression so far wants an operand: nothing yet, or an
|
|
@@ -454,6 +491,7 @@ class Reader {
|
|
|
454
491
|
|
|
455
492
|
// The token under the cursor, and the parts glued to it.
|
|
456
493
|
atom(): Node {
|
|
494
|
+
const at = this.T[this.i].sI
|
|
457
495
|
let text = atomText(this.T[this.i])
|
|
458
496
|
this.i++
|
|
459
497
|
while (GLUE[this.name(0)] &&
|
|
@@ -461,7 +499,7 @@ class Reader {
|
|
|
461
499
|
text += atomText(this.T[this.i])
|
|
462
500
|
this.i++
|
|
463
501
|
}
|
|
464
|
-
return { t: 'atom', text }
|
|
502
|
+
return { t: 'atom', text, at }
|
|
465
503
|
}
|
|
466
504
|
|
|
467
505
|
// A call's arguments, or a parenthesis's contents, up to the closing
|
|
@@ -470,6 +508,7 @@ class Reader {
|
|
|
470
508
|
seq(): Node[] {
|
|
471
509
|
const out: Node[] = []
|
|
472
510
|
let gap = true
|
|
511
|
+
let comma = false
|
|
473
512
|
for (;;) {
|
|
474
513
|
const n = this.name(0)
|
|
475
514
|
if ('' === n || CLOSER[n]) {
|
|
@@ -481,9 +520,10 @@ class Reader {
|
|
|
481
520
|
}
|
|
482
521
|
if ('#CA' === n) {
|
|
483
522
|
if (gap) {
|
|
484
|
-
out.push({ t: 'atom', text: 'nil' })
|
|
523
|
+
out.push({ t: 'atom', text: 'nil', sep: comma })
|
|
485
524
|
}
|
|
486
525
|
gap = true
|
|
526
|
+
comma = true
|
|
487
527
|
this.i++
|
|
488
528
|
continue
|
|
489
529
|
}
|
|
@@ -492,8 +532,11 @@ class Reader {
|
|
|
492
532
|
this.i++
|
|
493
533
|
continue
|
|
494
534
|
}
|
|
495
|
-
|
|
535
|
+
const v = this.value()
|
|
536
|
+
v.sep = comma
|
|
537
|
+
out.push(v)
|
|
496
538
|
gap = false
|
|
539
|
+
comma = false
|
|
497
540
|
}
|
|
498
541
|
return out
|
|
499
542
|
}
|
|
@@ -552,6 +595,11 @@ function width(s: string): number {
|
|
|
552
595
|
}
|
|
553
596
|
|
|
554
597
|
function pairHead(node: Node, tight: boolean): string {
|
|
598
|
+
// An alias declaration is `%name = value` at every width: the `=` is
|
|
599
|
+
// an operator, and operators are spaced (§3.2).
|
|
600
|
+
if (node.alias) {
|
|
601
|
+
return node.key! + ' = '
|
|
602
|
+
}
|
|
555
603
|
return node.key! + (node.opt ? '?' : '') + (tight ? ':' : ': ')
|
|
556
604
|
}
|
|
557
605
|
|
|
@@ -611,16 +659,23 @@ function inline(node: Node, tight: boolean): string | undefined {
|
|
|
611
659
|
}
|
|
612
660
|
}
|
|
613
661
|
|
|
662
|
+
// Arguments on one line, each after the separator the author wrote
|
|
663
|
+
// (§3.6): a comma stays a comma, and a space a space, because the
|
|
664
|
+
// parser reads `must((v) => 0 <= v, "…")` as a run of arguments too.
|
|
614
665
|
function inlineSeq(items: Node[]): string | undefined {
|
|
615
|
-
|
|
616
|
-
for (
|
|
617
|
-
const s = inline(
|
|
666
|
+
let out = ''
|
|
667
|
+
for (let k = 0; k < items.length; k++) {
|
|
668
|
+
const s = inline(items[k], true)
|
|
618
669
|
if (undefined === s) {
|
|
619
670
|
return undefined
|
|
620
671
|
}
|
|
621
|
-
|
|
672
|
+
out += (0 === k ? '' : sepOf(items[k])) + s
|
|
622
673
|
}
|
|
623
|
-
return
|
|
674
|
+
return out
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function sepOf(node: Node): string {
|
|
678
|
+
return node.sep ? ', ' : ' '
|
|
624
679
|
}
|
|
625
680
|
|
|
626
681
|
// Binary operators spaced, prefixes tight (§3.11). An operand is
|
|
@@ -679,6 +734,26 @@ class Writer {
|
|
|
679
734
|
return width(this.line)
|
|
680
735
|
}
|
|
681
736
|
|
|
737
|
+
// Where the page is, and the lines written since, the current line
|
|
738
|
+
// included: the spelling of one statement, as it stands on the page.
|
|
739
|
+
mark(): number {
|
|
740
|
+
return this.lines.length
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
since(mark: number): string {
|
|
744
|
+
return this.lines.slice(mark).concat([this.line]).map(rtrim).join('\n') + '\n'
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// The lines since a mark replaced by a text: the spelling before,
|
|
748
|
+
// where a rewrite did not pass its check.
|
|
749
|
+
replace(mark: number, text: string): void {
|
|
750
|
+
const lines = text.split('\n')
|
|
751
|
+
lines.pop()
|
|
752
|
+
this.line = lines.pop()!
|
|
753
|
+
this.lines.length = mark
|
|
754
|
+
this.lines.push(...lines)
|
|
755
|
+
}
|
|
756
|
+
|
|
682
757
|
finish(): string {
|
|
683
758
|
if (!this.started) {
|
|
684
759
|
return ''
|
|
@@ -697,8 +772,11 @@ function rtrim(s: string): string {
|
|
|
697
772
|
|
|
698
773
|
// The entries of a body, one per line at the indentation, with the
|
|
699
774
|
// blank lines the author kept between them (§3.8) -- never at the
|
|
700
|
-
// start or the end.
|
|
701
|
-
|
|
775
|
+
// start or the end. In STATEMENT position (`stmt`: the root, and the
|
|
776
|
+
// body of a plain map that is itself the value of a statement) a pair
|
|
777
|
+
// is laid out by §3.4, which may repeat its key; anywhere else -- a
|
|
778
|
+
// list, an operand, an argument -- by §3.5 alone.
|
|
779
|
+
function emitBody(w: Writer, body: Node[], indent: number, stmt: Stmt | undefined): void {
|
|
702
780
|
let pending = false
|
|
703
781
|
let count = 0
|
|
704
782
|
for (const node of body) {
|
|
@@ -713,6 +791,10 @@ function emitBody(w: Writer, body: Node[], indent: number): void {
|
|
|
713
791
|
w.text(node.text!)
|
|
714
792
|
continue
|
|
715
793
|
}
|
|
794
|
+
if (undefined !== stmt && 'pair' === node.t) {
|
|
795
|
+
emitStatement(w, node, indent, stmt, '')
|
|
796
|
+
continue
|
|
797
|
+
}
|
|
716
798
|
const e = chain(node)
|
|
717
799
|
emitValue(w, e, indent)
|
|
718
800
|
if (undefined !== e.trail) {
|
|
@@ -745,10 +827,10 @@ function emitValue(w: Writer, node: Node, indent: number): void {
|
|
|
745
827
|
emitValue(w, node.value!, indent)
|
|
746
828
|
return
|
|
747
829
|
case 'map':
|
|
748
|
-
emitBlock(w, '{', '}', node, indent)
|
|
830
|
+
emitBlock(w, '{', '}', node, indent, undefined)
|
|
749
831
|
return
|
|
750
832
|
case 'list':
|
|
751
|
-
emitBlock(w, '[', ']', node, indent)
|
|
833
|
+
emitBlock(w, '[', ']', node, indent, undefined)
|
|
752
834
|
return
|
|
753
835
|
case 'expr':
|
|
754
836
|
emitExpr(w, node.items!, indent)
|
|
@@ -763,27 +845,36 @@ function emitValue(w: Writer, node: Node, indent: number): void {
|
|
|
763
845
|
}
|
|
764
846
|
|
|
765
847
|
// A call, or a parenthesis, that has no one-line form or is too wide
|
|
766
|
-
// for the budget. Three shapes.
|
|
767
|
-
//
|
|
768
|
-
//
|
|
769
|
-
//
|
|
770
|
-
//
|
|
771
|
-
//
|
|
772
|
-
//
|
|
848
|
+
// for the budget. Three shapes. Arguments that are all FLAT -- none
|
|
849
|
+
// holds a container -- stay on the one line however wide it is: a
|
|
850
|
+
// scalar is no narrower on a line of its own, and the formatter never
|
|
851
|
+
// breaks a line. The last argument HUGS the parentheses, `hide({` ...
|
|
852
|
+
// `})`, `close($.E & {` ... `})`, when it is a container, or an
|
|
853
|
+
// expression the author did not break that ends in one, and the
|
|
854
|
+
// arguments before it fit on the opener's line: the container decides
|
|
855
|
+
// its own lines. Otherwise the parenthesis opens a block: one argument
|
|
856
|
+
// per line one level in, the closer alone at the opener's level. A
|
|
857
|
+
// call whose last argument hugs is hugged in turn, `type(close({` ...
|
|
858
|
+
// `}))`: the schema idiom.
|
|
773
859
|
function emitCall(w: Writer, node: Node, indent: number): void {
|
|
774
860
|
const items = 'call' === node.t ? node.args! : node.inner!
|
|
775
861
|
const open = ('call' === node.t ? node.name! : '') + '('
|
|
776
|
-
if (1 === items.length && ('map' === items[0].t || 'list' === items[0].t)) {
|
|
777
|
-
w.text(open)
|
|
778
|
-
emitValue(w, items[0], indent)
|
|
779
|
-
w.text(')')
|
|
780
|
-
return
|
|
781
|
-
}
|
|
782
862
|
const one = inlineSeq(items)
|
|
783
|
-
if (undefined !== one) {
|
|
863
|
+
if (undefined !== one && !items.some(holdsContainer)) {
|
|
784
864
|
w.text(open + one + ')')
|
|
785
865
|
return
|
|
786
866
|
}
|
|
867
|
+
const last = items[items.length - 1]
|
|
868
|
+
if (0 < items.length && hugs(last)) {
|
|
869
|
+
const head = inlineSeq(items.slice(0, -1))
|
|
870
|
+
const lead = '' === head ? '' : head + sepOf(last)
|
|
871
|
+
if (undefined !== head && ('' === head || w.width() + width(open + lead) <= BUDGET)) {
|
|
872
|
+
w.text(open + lead)
|
|
873
|
+
emitValue(w, last, indent)
|
|
874
|
+
w.text(')')
|
|
875
|
+
return
|
|
876
|
+
}
|
|
877
|
+
}
|
|
787
878
|
w.text(open)
|
|
788
879
|
let noted = false
|
|
789
880
|
for (let k = 0; k < items.length; k++) {
|
|
@@ -804,7 +895,8 @@ function emitCall(w: Writer, node: Node, indent: number): void {
|
|
|
804
895
|
}
|
|
805
896
|
w.open(indent + 2, false)
|
|
806
897
|
emitValue(w, it, indent + 2)
|
|
807
|
-
|
|
898
|
+
const next = items.slice(k + 1).find((x) => 'note' !== x.t)
|
|
899
|
+
if (undefined !== next && next.sep) {
|
|
808
900
|
w.text(',')
|
|
809
901
|
}
|
|
810
902
|
noted = false
|
|
@@ -813,10 +905,45 @@ function emitCall(w: Writer, node: Node, indent: number): void {
|
|
|
813
905
|
w.text(')')
|
|
814
906
|
}
|
|
815
907
|
|
|
908
|
+
// Whether a node holds a container anywhere: the argument has a
|
|
909
|
+
// several-line form of its own.
|
|
910
|
+
function holdsContainer(node: Node): boolean {
|
|
911
|
+
switch (node.t) {
|
|
912
|
+
case 'map':
|
|
913
|
+
case 'list':
|
|
914
|
+
return true
|
|
915
|
+
case 'call':
|
|
916
|
+
return node.args!.some(holdsContainer)
|
|
917
|
+
case 'paren':
|
|
918
|
+
return node.inner!.some(holdsContainer)
|
|
919
|
+
case 'expr':
|
|
920
|
+
return node.items!.some(holdsContainer)
|
|
921
|
+
default:
|
|
922
|
+
return false
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Whether a last argument hugs the parentheses: a container; an
|
|
927
|
+
// expression with no break and no comment whose last operand is one;
|
|
928
|
+
// a call whose own last argument does.
|
|
929
|
+
function hugs(node: Node): boolean {
|
|
930
|
+
if ('map' === node.t || 'list' === node.t) {
|
|
931
|
+
return true
|
|
932
|
+
}
|
|
933
|
+
if ('call' === node.t) {
|
|
934
|
+
return 0 < node.args!.length && hugs(node.args![node.args!.length - 1])
|
|
935
|
+
}
|
|
936
|
+
return 'expr' === node.t &&
|
|
937
|
+
node.items!.every((it) => 'note' !== it.t && !('op' === it.t && it.brk)) &&
|
|
938
|
+
hugs(node.items![node.items!.length - 1])
|
|
939
|
+
}
|
|
940
|
+
|
|
816
941
|
// A container on several lines (§3.5): the opener ends its line, the
|
|
817
942
|
// entries are statements one level in, the closer stands alone. An
|
|
818
943
|
// empty container is inline whatever the budget says.
|
|
819
|
-
function emitBlock(
|
|
944
|
+
function emitBlock(
|
|
945
|
+
w: Writer, open: string, close: string, node: Node, indent: number, stmt: Stmt | undefined
|
|
946
|
+
): void {
|
|
820
947
|
if (0 === node.body!.length && undefined === node.open) {
|
|
821
948
|
w.text(open + close)
|
|
822
949
|
return
|
|
@@ -825,7 +952,7 @@ function emitBlock(w: Writer, open: string, close: string, node: Node, indent: n
|
|
|
825
952
|
if (undefined !== node.open) {
|
|
826
953
|
w.text(' ' + node.open)
|
|
827
954
|
}
|
|
828
|
-
emitBody(w, node.body!, indent + 2)
|
|
955
|
+
emitBody(w, node.body!, indent + 2, stmt)
|
|
829
956
|
w.open(indent, false)
|
|
830
957
|
w.text(close)
|
|
831
958
|
}
|
|
@@ -882,13 +1009,443 @@ function emitExpr(w: Writer, items: Node[], indent: number): void {
|
|
|
882
1009
|
}
|
|
883
1010
|
}
|
|
884
1011
|
|
|
885
|
-
|
|
1012
|
+
|
|
1013
|
+
// ---------------------------------------------------------------------
|
|
1014
|
+
// The lawful tier (§3.4): repeat the prefix, and merge what repeats.
|
|
1015
|
+
//
|
|
1016
|
+
// Both rewrites rest on the meet. `s: a: 1` / `s: b: 2` is one document
|
|
1017
|
+
// with `s: { a:1 b:2 }`, because a key written twice is a meet and the
|
|
1018
|
+
// meet of two maps with disjoint keys is their union. So they apply
|
|
1019
|
+
// only to a PLAIN map in STATEMENT position -- an entry of the root, or
|
|
1020
|
+
// of a map that is itself the plain value of such an entry -- and never
|
|
1021
|
+
// to a map that is an operand, an argument or a list element, where
|
|
1022
|
+
// splitting it would change the document (`close({a:1})` /
|
|
1023
|
+
// `close({b:2})` does not evaluate at all). And every statement the
|
|
1024
|
+
// tier rewrites is checked by unification, locally (§7.3): the spelling
|
|
1025
|
+
// before and the spelling after must come to the same meet, or the
|
|
1026
|
+
// statement keeps the spelling before. The check is the engine's
|
|
1027
|
+
// agreement, not the formatter's self-check -- the engine's own repros
|
|
1028
|
+
// hold maps whose two spellings it evaluates differently -- so failing
|
|
1029
|
+
// it is no refusal.
|
|
1030
|
+
|
|
1031
|
+
// The check of one rewrite: the spelling before and the spelling after.
|
|
1032
|
+
type Meet = (before: string, after: string) => boolean
|
|
1033
|
+
|
|
1034
|
+
// Statement position: the check, and whether the statement being laid
|
|
1035
|
+
// out stands inside one that is checked as a whole, which covers it.
|
|
1036
|
+
// Undefined anywhere else -- a list, an operand, an argument.
|
|
1037
|
+
type Stmt = { meet: Meet, covered: boolean }
|
|
1038
|
+
|
|
1039
|
+
// The entries of a plain map value: a braced map, or a chain, which is
|
|
1040
|
+
// a one-entry map. A map with a comment on its opener keeps its braces
|
|
1041
|
+
// (§3.7), so it is not plain here; nor is a map holding an include,
|
|
1042
|
+
// which the local check cannot follow.
|
|
1043
|
+
function plainEntries(v: Node): Node[] | undefined {
|
|
1044
|
+
if ('pair' === v.t) {
|
|
1045
|
+
return [v]
|
|
1046
|
+
}
|
|
1047
|
+
if ('map' !== v.t || undefined !== v.open || v.body!.some((e) => 'include' === e.t)) {
|
|
1048
|
+
return undefined
|
|
1049
|
+
}
|
|
1050
|
+
return v.body
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// The entries of a statement as they stand once it is merged into a
|
|
1054
|
+
// wider map: its trailing comment sunk onto its last entry, so that it
|
|
1055
|
+
// travels with the entry it stood beside. Undefined where the value is
|
|
1056
|
+
// not a plain map, or the comment has no entry to sit on.
|
|
1057
|
+
function members(p: Node): Node[] | undefined {
|
|
1058
|
+
const entries = plainEntries(p.value!)
|
|
1059
|
+
if (undefined === entries || undefined === p.trail) {
|
|
1060
|
+
return entries
|
|
1061
|
+
}
|
|
1062
|
+
const last = entries[entries.length - 1]
|
|
1063
|
+
if (undefined === last || ('pair' !== last.t && 'spread' !== last.t)) {
|
|
1064
|
+
return undefined
|
|
1065
|
+
}
|
|
1066
|
+
const trail = undefined === last.trail ? p.trail : last.trail + ' ' + p.trail
|
|
1067
|
+
return entries.slice(0, -1).concat([{ ...last, trail }])
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// Adjacent statements naming one key, whose values are plain maps, are
|
|
1071
|
+
// one map: their entries in order, with the comments and blank lines
|
|
1072
|
+
// between the statements travelling with the statement they preceded.
|
|
1073
|
+
// Only ADJACENT statements merge -- a `server:` line, something else,
|
|
1074
|
+
// then another `server:` line stays as it is, because merging them
|
|
1075
|
+
// would move a statement, and the formatter never reorders (§3.13).
|
|
1076
|
+
// Nor do two statements merge into a map with two spreads: the engine
|
|
1077
|
+
// keeps those as a conjunction, which is not the meet of the two maps.
|
|
1078
|
+
// The tree is not changed: a merged statement is a new node that keeps
|
|
1079
|
+
// the statements it replaces as its `orig`, its spelling before, and a
|
|
1080
|
+
// statement merged somewhere below is copied the same way.
|
|
1081
|
+
function mergeRuns(body: Node[]): Node[] {
|
|
1082
|
+
const out: Node[] = []
|
|
1083
|
+
let i = 0
|
|
1084
|
+
while (i < body.length) {
|
|
1085
|
+
const first = body[i]
|
|
1086
|
+
const entries = 'pair' === first.t ? members(first) : undefined
|
|
1087
|
+
if (undefined === entries) {
|
|
1088
|
+
out.push('pair' === first.t ? mergeDeep(first) : first)
|
|
1089
|
+
i++
|
|
1090
|
+
continue
|
|
1091
|
+
}
|
|
1092
|
+
const group = [first]
|
|
1093
|
+
let merged = entries
|
|
1094
|
+
let carry: Node[] = []
|
|
1095
|
+
let j = i + 1
|
|
1096
|
+
for (; j < body.length; j++) {
|
|
1097
|
+
const n = body[j]
|
|
1098
|
+
if ('comment' === n.t || 'blank' === n.t) {
|
|
1099
|
+
carry.push(n)
|
|
1100
|
+
continue
|
|
1101
|
+
}
|
|
1102
|
+
const more = 'pair' === n.t && n.key === first.key && n.opt === first.opt
|
|
1103
|
+
? members(n) : undefined
|
|
1104
|
+
if (undefined === more || (spreads(merged) && spreads(more))) {
|
|
1105
|
+
break
|
|
1106
|
+
}
|
|
1107
|
+
group.push(...carry, n)
|
|
1108
|
+
merged = merged.concat(carry, more)
|
|
1109
|
+
carry = []
|
|
1110
|
+
}
|
|
1111
|
+
if (1 === group.length) {
|
|
1112
|
+
out.push(mergeDeep(first))
|
|
1113
|
+
i++
|
|
1114
|
+
continue
|
|
1115
|
+
}
|
|
1116
|
+
out.push({
|
|
1117
|
+
t: 'pair', key: first.key, opt: first.opt, alias: first.alias,
|
|
1118
|
+
value: { t: 'map', body: mergeRuns(merged) }, orig: group,
|
|
1119
|
+
})
|
|
1120
|
+
i = j - carry.length
|
|
1121
|
+
}
|
|
1122
|
+
return out
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function spreads(entries: Node[]): boolean {
|
|
1126
|
+
return entries.some((e) => 'spread' === e.t)
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// The merge down a statement's plain-map spine: a chain's inner pair,
|
|
1130
|
+
// or the entries of a map value, are statements of the map they are
|
|
1131
|
+
// in. The statement itself where nothing below it merged.
|
|
1132
|
+
function mergeDeep(p: Node): Node {
|
|
1133
|
+
const v = p.value!
|
|
1134
|
+
const entries = plainEntries(v)
|
|
1135
|
+
if (undefined === entries) {
|
|
1136
|
+
return p
|
|
1137
|
+
}
|
|
1138
|
+
const body = mergeRuns(entries)
|
|
1139
|
+
if (body.length === entries.length && body.every((n, k) => n === entries[k])) {
|
|
1140
|
+
return p
|
|
1141
|
+
}
|
|
1142
|
+
return { ...p, value: 'pair' === v.t ? body[0] : { ...v, body }, orig: [p] }
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// The lines of a map repeated under a prefix (§3.4, rule 2): every
|
|
1146
|
+
// entry written with the prefix in front of it as one line, or --
|
|
1147
|
+
// where an entry's value is a map that does not fit -- repeated further
|
|
1148
|
+
// under the longer prefix. Comments and blank lines are kept where
|
|
1149
|
+
// they stood. Undefined where an entry cannot be one line: a list that
|
|
1150
|
+
// does not fit, a value that spans lines, a comment closing the map
|
|
1151
|
+
// (which a repeat could not keep in the map) -- and where the map holds
|
|
1152
|
+
// two spreads, which repeated would be two maps, and a different meet.
|
|
1153
|
+
type Line = { t: 'text' | 'comment' | 'blank', text?: string }
|
|
1154
|
+
|
|
1155
|
+
function repeatLines(entries: Node[], prefix: string, indent: number): Line[] | undefined {
|
|
1156
|
+
if (0 === entries.length || 'comment' === entries[entries.length - 1].t ||
|
|
1157
|
+
1 < entries.filter((e) => 'spread' === e.t).length) {
|
|
1158
|
+
return undefined
|
|
1159
|
+
}
|
|
1160
|
+
const out: Line[] = []
|
|
1161
|
+
for (const e of entries) {
|
|
1162
|
+
if ('blank' === e.t) {
|
|
1163
|
+
out.push({ t: 'blank' })
|
|
1164
|
+
continue
|
|
1165
|
+
}
|
|
1166
|
+
if ('comment' === e.t) {
|
|
1167
|
+
out.push({ t: 'comment', text: e.text })
|
|
1168
|
+
continue
|
|
1169
|
+
}
|
|
1170
|
+
const trail = undefined === e.trail ? '' : ' ' + e.trail
|
|
1171
|
+
if ('spread' === e.t) {
|
|
1172
|
+
// The repeated spread entry is a one-entry map holding only a
|
|
1173
|
+
// spread, so by D1's exception it keeps its braces.
|
|
1174
|
+
const s = inline(e.value!, true)
|
|
1175
|
+
if (undefined === s || !fits(indent, prefix + '{ &: ' + s + ' }')) {
|
|
1176
|
+
return undefined
|
|
1177
|
+
}
|
|
1178
|
+
out.push({ t: 'text', text: prefix + '{ &: ' + s + ' }' + trail })
|
|
1179
|
+
continue
|
|
1180
|
+
}
|
|
1181
|
+
const head = prefix + pairHead(e, false)
|
|
1182
|
+
const s = inline(chain(e.value!), false)
|
|
1183
|
+
if (undefined !== s && fits(indent, head + s)) {
|
|
1184
|
+
out.push({ t: 'text', text: head + s + trail })
|
|
1185
|
+
continue
|
|
1186
|
+
}
|
|
1187
|
+
const sub = plainEntries(e.value!)
|
|
1188
|
+
if (undefined === sub) {
|
|
1189
|
+
return undefined
|
|
1190
|
+
}
|
|
1191
|
+
const lines = repeatLines(sub, head, indent)
|
|
1192
|
+
if (undefined === lines) {
|
|
1193
|
+
return undefined
|
|
1194
|
+
}
|
|
1195
|
+
if ('' !== trail) {
|
|
1196
|
+
lines[lines.length - 1].text += trail
|
|
1197
|
+
}
|
|
1198
|
+
out.push(...lines)
|
|
1199
|
+
}
|
|
1200
|
+
return out
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function fits(indent: number, text: string): boolean {
|
|
1204
|
+
return indent + width(text) <= BUDGET
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// A pair in statement position, by §3.4. `prefix` is what stands
|
|
1208
|
+
// before it on its line: the heads of the chain it hangs from, not yet
|
|
1209
|
+
// written. Its value is laid out by §3.5 unless it is a plain map, and
|
|
1210
|
+
// then in this order: a chain, when the map holds exactly one pair
|
|
1211
|
+
// (D1); one line, when that fits the budget; the key repeated over the
|
|
1212
|
+
// entries, when every entry can be one line that way; a braced block
|
|
1213
|
+
// otherwise, whose entries are statements in turn. Whether the
|
|
1214
|
+
// statement was rewritten by this tier -- merged, or repeated -- is
|
|
1215
|
+
// returned, and the outermost such statement is checked: its spelling
|
|
1216
|
+
// on the page against what the syntactic tier writes for the
|
|
1217
|
+
// statements it came from, at the same indentation, which is what
|
|
1218
|
+
// stays on the page when the check fails.
|
|
1219
|
+
function emitStatement(w: Writer, p: Node, indent: number, stmt: Stmt, prefix: string): boolean {
|
|
1220
|
+
const mark = w.mark()
|
|
1221
|
+
let rewritten = undefined !== p.orig
|
|
1222
|
+
const entries = plainEntries(p.value!)
|
|
1223
|
+
const head = prefix + pairHead(p, false)
|
|
1224
|
+
const s = undefined === entries ? undefined : inline(p.value!, false)
|
|
1225
|
+
if (undefined === entries) {
|
|
1226
|
+
w.text(prefix)
|
|
1227
|
+
emitValue(w, p, indent)
|
|
1228
|
+
}
|
|
1229
|
+
else if (1 === entries.length && 'pair' === entries[0].t) {
|
|
1230
|
+
rewritten = emitStatement(w, entries[0], indent, { meet: stmt.meet, covered: true }, head)
|
|
1231
|
+
|| rewritten
|
|
1232
|
+
}
|
|
1233
|
+
else if (undefined !== s && fits(indent, head + s)) {
|
|
1234
|
+
w.text(head + s)
|
|
1235
|
+
}
|
|
1236
|
+
else {
|
|
1237
|
+
const lines = repeatLines(entries, head, indent)
|
|
1238
|
+
if (undefined !== lines) {
|
|
1239
|
+
let pending = false
|
|
1240
|
+
let count = 0
|
|
1241
|
+
for (const line of lines) {
|
|
1242
|
+
if ('blank' === line.t) {
|
|
1243
|
+
pending = 0 < count
|
|
1244
|
+
continue
|
|
1245
|
+
}
|
|
1246
|
+
if (0 < count) {
|
|
1247
|
+
w.open(indent, pending)
|
|
1248
|
+
}
|
|
1249
|
+
pending = false
|
|
1250
|
+
count++
|
|
1251
|
+
w.text(line.text!)
|
|
1252
|
+
}
|
|
1253
|
+
rewritten = true
|
|
1254
|
+
}
|
|
1255
|
+
else {
|
|
1256
|
+
w.text(head)
|
|
1257
|
+
emitBlock(w, '{', '}', p.value!, indent, { meet: stmt.meet, covered: stmt.covered || rewritten })
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
if (undefined !== p.trail) {
|
|
1261
|
+
w.text(' ' + p.trail)
|
|
1262
|
+
}
|
|
1263
|
+
if (rewritten && !stmt.covered) {
|
|
1264
|
+
const before = emitAt(p.orig ?? [p], indent)
|
|
1265
|
+
if (!stmt.meet(before, w.since(mark))) {
|
|
1266
|
+
w.replace(mark, before)
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
return rewritten
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// The syntactic tier's spelling of some statements at an indentation:
|
|
1273
|
+
// a rewrite's spelling before.
|
|
1274
|
+
function emitAt(nodes: Node[], indent: number): string {
|
|
1275
|
+
const w = new Writer()
|
|
1276
|
+
emitBody(w, nodes, indent, undefined)
|
|
1277
|
+
return w.finish()
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// The document: by the syntactic tier alone, or with the lawful tier
|
|
1281
|
+
// over it when given its check.
|
|
1282
|
+
function emit(root: Node[], meet: Meet | undefined): string {
|
|
886
1283
|
const w = new Writer()
|
|
887
|
-
emitBody(w, root, 0
|
|
1284
|
+
emitBody(w, undefined === meet ? root : mergeRuns(root), 0,
|
|
1285
|
+
undefined === meet ? undefined : { meet, covered: false })
|
|
888
1286
|
return w.finish()
|
|
889
1287
|
}
|
|
890
1288
|
|
|
891
1289
|
|
|
1290
|
+
// ---------------------------------------------------------------------
|
|
1291
|
+
// The lint (§4): what the formatter points at and never touches. Two
|
|
1292
|
+
// rules, both advice: the formatter never renames a key (§4.1) and
|
|
1293
|
+
// never introduces an alias (§4.2), and a rule with a mechanical fix
|
|
1294
|
+
// that keeps the document would belong to §3 instead (§4.3).
|
|
1295
|
+
|
|
1296
|
+
// The shape width at which a repeat is worth an alias (§4.2): below
|
|
1297
|
+
// it, `{ a:1 }` twice is the shorter spelling. Measured over the use
|
|
1298
|
+
// cases when the lint landed (§7.10).
|
|
1299
|
+
const REPEAT_MIN_WIDTH = 40
|
|
1300
|
+
|
|
1301
|
+
function lintOf(root: Node[], text: string): LintFinding[] {
|
|
1302
|
+
const out: LintFinding[] = []
|
|
1303
|
+
const nodes = root.map(lintNode)
|
|
1304
|
+
for (const n of nodes) {
|
|
1305
|
+
keyCase(n, text, out)
|
|
1306
|
+
}
|
|
1307
|
+
repeats(nodes, text, out)
|
|
1308
|
+
out.sort((a, b) => a.line - b.line || a.col - b.col)
|
|
1309
|
+
return out
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// The tree the lint walks: a chain's inner pair as the one-entry map
|
|
1313
|
+
// it is, so that `a: {b: 1}` and `a: b: 1` -- one document to the
|
|
1314
|
+
// formatter -- are one shape to the lint.
|
|
1315
|
+
function lintNode(node: Node): Node {
|
|
1316
|
+
if ('pair' === node.t && 'pair' === node.value!.t) {
|
|
1317
|
+
return { ...node, value: { t: 'map', body: [node.value!], at: node.value!.at } }
|
|
1318
|
+
}
|
|
1319
|
+
return node
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function lintChildren(node: Node): Node[] {
|
|
1323
|
+
switch (node.t) {
|
|
1324
|
+
case 'pair':
|
|
1325
|
+
case 'spread':
|
|
1326
|
+
return [lintNode(node).value!]
|
|
1327
|
+
case 'map':
|
|
1328
|
+
case 'list':
|
|
1329
|
+
return node.body!.map(lintNode)
|
|
1330
|
+
case 'call':
|
|
1331
|
+
return node.args!
|
|
1332
|
+
case 'paren':
|
|
1333
|
+
return node.inner!
|
|
1334
|
+
case 'expr':
|
|
1335
|
+
return node.items!
|
|
1336
|
+
default:
|
|
1337
|
+
return []
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// Line and column, 1-based, of a source index.
|
|
1342
|
+
function lineCol(text: string, at: number): { line: number, col: number } {
|
|
1343
|
+
const before = text.slice(0, at)
|
|
1344
|
+
return { line: before.split('\n').length, col: at - before.lastIndexOf('\n') }
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// D4 (§4.1): keys are lower-case words, or CamelCase when a key is
|
|
1348
|
+
// several. A bare key holding `_`, or beginning with two capitals, is
|
|
1349
|
+
// reported with the spelling that would follow the form; a quoted key
|
|
1350
|
+
// is a deliberate spelling and a key of underscores alone names
|
|
1351
|
+
// nothing the rule can respell.
|
|
1352
|
+
function keyCase(node: Node, text: string, out: LintFinding[]): void {
|
|
1353
|
+
if ('pair' === node.t && BARE.test(node.key!) && /[A-Za-z]/.test(node.key!)) {
|
|
1354
|
+
const why = node.key!.includes('_') ? 'holds an underscore'
|
|
1355
|
+
: /^[A-Z][A-Z]/.test(node.key!) ? 'begins with capitals' : ''
|
|
1356
|
+
if ('' !== why) {
|
|
1357
|
+
out.push({
|
|
1358
|
+
rule: 'style/key-case', ...lineCol(text, node.at!),
|
|
1359
|
+
message: `key ${node.key} ${why}; ${camel(node.key!)} would follow the form`,
|
|
1360
|
+
})
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
for (const child of lintChildren(node)) {
|
|
1364
|
+
keyCase(child, text, out)
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// The key as lower-case words or CamelCase: `credit_cents` is
|
|
1369
|
+
// `creditCents`, `HTTP_PORT` is `httpPort`, `HTTPServer` is
|
|
1370
|
+
// `httpServer`, `ID` is `id`.
|
|
1371
|
+
function camel(key: string): string {
|
|
1372
|
+
const words = key.split('_').filter((w) => '' !== w)
|
|
1373
|
+
.map((w) => /^[A-Z]+$/.test(w) ? w.toLowerCase() : w)
|
|
1374
|
+
const head = words[0].replace(/^[A-Z]+(?=[A-Z][a-z])/, (run) => run.toLowerCase())
|
|
1375
|
+
return head.charAt(0).toLowerCase() + head.slice(1) +
|
|
1376
|
+
words.slice(1).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join('')
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// D3 (§4.2): a shape written twice can drift, and an alias names it
|
|
1380
|
+
// once. Every map or list whose shape recurs in the file, and whose
|
|
1381
|
+
// shape is REPEAT_MIN_WIDTH or wider, is reported once, at its first
|
|
1382
|
+
// site, with the count and the other sites; the naming is the
|
|
1383
|
+
// author's. A repeat inside a repeat is the outer one's: the walk does
|
|
1384
|
+
// not descend into a shape it reports.
|
|
1385
|
+
function repeats(nodes: Node[], text: string, out: LintFinding[]): void {
|
|
1386
|
+
const counts = new Map<string, number>()
|
|
1387
|
+
const tally = (node: Node): void => {
|
|
1388
|
+
if ('map' === node.t || 'list' === node.t) {
|
|
1389
|
+
const s = shape(node)
|
|
1390
|
+
counts.set(s, (counts.get(s) ?? 0) + 1)
|
|
1391
|
+
}
|
|
1392
|
+
lintChildren(node).forEach(tally)
|
|
1393
|
+
}
|
|
1394
|
+
nodes.forEach(tally)
|
|
1395
|
+
const sites = new Map<string, Node[]>()
|
|
1396
|
+
const visit = (node: Node): void => {
|
|
1397
|
+
if ('map' === node.t || 'list' === node.t) {
|
|
1398
|
+
const s = shape(node)
|
|
1399
|
+
if (2 <= counts.get(s)! && REPEAT_MIN_WIDTH <= width(s)) {
|
|
1400
|
+
sites.set(s, (sites.get(s) ?? []).concat([node]))
|
|
1401
|
+
return
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
lintChildren(node).forEach(visit)
|
|
1405
|
+
}
|
|
1406
|
+
nodes.forEach(visit)
|
|
1407
|
+
for (const found of sites.values()) {
|
|
1408
|
+
if (2 <= found.length) {
|
|
1409
|
+
const [first, ...rest] = found.map((n) => lineCol(text, n.at!))
|
|
1410
|
+
out.push({
|
|
1411
|
+
rule: 'style/repeat', ...first,
|
|
1412
|
+
message: `this ${found[0].t} is written ${found.length} times (again at ` +
|
|
1413
|
+
rest.map((p) => p.line + ':' + p.col).join(', ') +
|
|
1414
|
+
'); an alias would name it once',
|
|
1415
|
+
})
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// A node's shape: its spelling with the layout, the comments and, for
|
|
1421
|
+
// a map, the order of its entries taken out, so that two spellings of
|
|
1422
|
+
// one value are one shape, as they are one canon.
|
|
1423
|
+
function shape(node: Node): string {
|
|
1424
|
+
switch (node.t) {
|
|
1425
|
+
case 'map':
|
|
1426
|
+
return '{' + node.body!.filter(shaped).map((e) => shape(lintNode(e))).sort().join(' ') + '}'
|
|
1427
|
+
case 'list':
|
|
1428
|
+
return '[' + node.body!.filter(shaped).map((e) => shape(lintNode(e))).join(' ') + ']'
|
|
1429
|
+
case 'pair':
|
|
1430
|
+
return node.key! + (node.opt ? '?' : '') + ':' + shape(node.value!)
|
|
1431
|
+
case 'spread':
|
|
1432
|
+
return '&:' + shape(node.value!)
|
|
1433
|
+
case 'call':
|
|
1434
|
+
return node.name! + '(' + node.args!.filter(shaped).map(shape).join(',') + ')'
|
|
1435
|
+
case 'paren':
|
|
1436
|
+
return '(' + node.inner!.filter(shaped).map(shape).join(',') + ')'
|
|
1437
|
+
case 'expr':
|
|
1438
|
+
return node.items!.filter(shaped).map(shape).join('')
|
|
1439
|
+
default:
|
|
1440
|
+
return node.text!
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
function shaped(node: Node): boolean {
|
|
1445
|
+
return 'comment' !== node.t && 'blank' !== node.t && 'note' !== node.t
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
|
|
892
1449
|
// ---------------------------------------------------------------------
|
|
893
1450
|
// The verb's library surface
|
|
894
1451
|
|
|
@@ -897,13 +1454,42 @@ function lf(text: string): string {
|
|
|
897
1454
|
}
|
|
898
1455
|
|
|
899
1456
|
// The check: the output parses, and to the same tree. Pre-unification
|
|
900
|
-
// canon is that tree, positions aside, and every rewrite of
|
|
901
|
-
// leaves it unchanged (§7.3).
|
|
1457
|
+
// canon is that tree, positions aside, and every rewrite of the
|
|
1458
|
+
// syntactic tier leaves it unchanged (§7.3).
|
|
902
1459
|
function sameDocument(root: any, after: string): boolean {
|
|
903
1460
|
const p = parseDoc(after, undefined, undefined)
|
|
904
1461
|
return undefined === p.errors && root.canon === p.root.canon
|
|
905
1462
|
}
|
|
906
1463
|
|
|
1464
|
+
// The check of a lawful rewrite: the spelling before and the spelling
|
|
1465
|
+
// after, evaluated in isolation, come to the same canon, the same
|
|
1466
|
+
// kinds of failure, and the same outcome of generation (§7.3). Local,
|
|
1467
|
+
// so it needs no include and no capability, and it applies whether or
|
|
1468
|
+
// not the document as a whole evaluates. The kinds, not the count: how
|
|
1469
|
+
// often one unresolved reference is reported depends on the order the
|
|
1470
|
+
// meet took. Generation too, because the engine generates from more
|
|
1471
|
+
// than the canon: a meet of maps with a nil member has refused a key
|
|
1472
|
+
// the same map written once generates.
|
|
1473
|
+
function sameByMeet(before: string, after: string): boolean {
|
|
1474
|
+
return meetOf(before) === meetOf(after)
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
function meetOf(text: string): string {
|
|
1478
|
+
const aontu = engine()
|
|
1479
|
+
const ctx = aontu.ctx({ collect: true })
|
|
1480
|
+
const v: any = aontu.unify(text, undefined, ctx)
|
|
1481
|
+
const gen = aontu.ctx({ collect: true })
|
|
1482
|
+
const out = aontu.generate(text, undefined, gen)
|
|
1483
|
+
const outcome = undefined !== out ? 'generated'
|
|
1484
|
+
: 0 < ctx.err.length ? kinds(ctx.err) : gen.err[0].why
|
|
1485
|
+
return v.canon + '\n' + kinds(ctx.err) + '\n' + outcome
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function kinds(errs: any[]): string {
|
|
1489
|
+
const whys: string[] = errs.map((e) => e.why)
|
|
1490
|
+
return whys.filter((x, i) => i === whys.indexOf(x)).sort().join(',')
|
|
1491
|
+
}
|
|
1492
|
+
|
|
907
1493
|
function depthFinding(): VetFinding {
|
|
908
1494
|
return {
|
|
909
1495
|
code: 'max_depth',
|
|
@@ -941,19 +1527,25 @@ export function format(src: string, opts?: FormatOptions, hooks?: FormatHooks):
|
|
|
941
1527
|
return { verdict: 'error', errors: parsed.errors }
|
|
942
1528
|
}
|
|
943
1529
|
const reader = new Reader(toks)
|
|
944
|
-
const root = reader.body('', false).body
|
|
1530
|
+
const root = unwrap(reader.body('', false).body)
|
|
945
1531
|
if (reader.deep) {
|
|
946
1532
|
return { verdict: 'error', errors: [depthFinding()] }
|
|
947
1533
|
}
|
|
948
|
-
|
|
1534
|
+
// The syntactic tier first, checked against the parse tree; then the
|
|
1535
|
+
// lawful tier over it, each rewrite checked by the meet.
|
|
1536
|
+
const plain = emit(root, undefined)
|
|
949
1537
|
const same = hooks?.same ?? sameDocument
|
|
950
|
-
if (!same(parsed.root,
|
|
1538
|
+
if (!same(parsed.root, plain)) {
|
|
951
1539
|
return {
|
|
952
1540
|
verdict: 'error',
|
|
953
|
-
errors: [checkFinding(opts?.path, parsed.root.canon,
|
|
1541
|
+
errors: [checkFinding(opts?.path, parsed.root.canon, plain)],
|
|
954
1542
|
}
|
|
955
1543
|
}
|
|
956
|
-
|
|
1544
|
+
const out = emit(root, hooks?.meet ?? sameByMeet)
|
|
1545
|
+
return {
|
|
1546
|
+
verdict: 'formatted', text: out, changed: out !== src,
|
|
1547
|
+
findings: opts?.lint ? lintOf(root, text) : [],
|
|
1548
|
+
}
|
|
957
1549
|
}
|
|
958
1550
|
|
|
959
1551
|
|