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