@ttsc/lint 0.19.2 → 0.20.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 +1 -1
- package/lib/index.js +26 -4
- package/lib/index.js.map +1 -1
- package/lib/structures/format/ITtscLintFormat.d.ts +14 -10
- package/lib/structures/rules/ITtscLintRegexpRules.d.ts +24 -9
- package/lib/structures/rules/ITtscLintSecurityRules.d.ts +18 -0
- package/lib/structures/rules/ITtscLintSolidRules.d.ts +22 -2
- package/lib/structures/rules/ITtscLintStorybookRules.d.ts +21 -0
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.d.ts +10 -1
- package/lib/structures/rules/ITtscLintTypeScriptRules.d.ts +13 -2
- package/linthost/compile.go +9 -4
- package/linthost/config.go +18 -14
- package/linthost/contrib_adapter.go +57 -0
- package/linthost/dispatch.go +5 -1
- package/linthost/display_width.go +179 -41
- package/linthost/engine.go +120 -4
- package/linthost/flags_gen.go +4 -4
- package/linthost/hints.go +207 -0
- package/linthost/host.go +68 -0
- package/linthost/lsp.go +148 -29
- package/linthost/print_dispatch.go +24 -0
- package/linthost/print_nodes_array.go +88 -15
- package/linthost/print_nodes_call.go +38 -31
- package/linthost/print_nodes_control_flow.go +319 -0
- package/linthost/print_nodes_function.go +140 -13
- package/linthost/print_nodes_list.go +22 -10
- package/linthost/project_engine.go +18 -1
- package/linthost/project_rules.go +47 -0
- package/linthost/rule_docs.go +114 -0
- package/linthost/rules_boundaries.go +80 -2
- package/linthost/rules_format_bracket_spacing.go +18 -6
- package/linthost/rules_format_clause_join.go +11 -8
- package/linthost/rules_format_indent.go +155 -4
- package/linthost/rules_format_print_width.go +12 -35
- package/linthost/rules_format_quote_props.go +88 -27
- package/linthost/rules_format_statement_split.go +81 -0
- package/linthost/rules_format_trailing_comma.go +62 -94
- package/linthost/rules_gap.go +26 -17
- package/linthost/rules_jsdoc.go +54 -0
- package/linthost/rules_logic.go +30 -15
- package/linthost/rules_no_redeclare.go +12 -3
- package/linthost/rules_promise.go +37 -15
- package/linthost/rules_regexp.go +443 -49
- package/linthost/rules_security.go +243 -4
- package/linthost/rules_solid.go +430 -10
- package/linthost/rules_storybook.go +255 -10
- package/linthost/rules_suggestions.go +60 -39
- package/linthost/rules_ts.go +7 -0
- package/linthost/rules_ts_async.go +80 -2
- package/linthost/rules_ts_extra.go +21 -7
- package/linthost/serve.go +318 -0
- package/linthost/width_tables_gen.go +219 -0
- package/package.json +2 -2
- package/rule/hint.go +142 -0
- package/rule/rule.go +197 -1
- package/src/index.ts +35 -4
- package/src/structures/format/ITtscLintFormat.ts +14 -10
- package/src/structures/rules/ITtscLintRegexpRules.ts +24 -9
- package/src/structures/rules/ITtscLintSecurityRules.ts +18 -0
- package/src/structures/rules/ITtscLintSolidRules.ts +22 -2
- package/src/structures/rules/ITtscLintStorybookRules.ts +21 -0
- package/src/structures/rules/ITtscLintTypeScriptRuleOptions.ts +10 -1
- package/src/structures/rules/ITtscLintTypeScriptRules.ts +13 -2
|
@@ -158,18 +158,21 @@ func isClauseGapByte(c byte) bool {
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
// visualWidth returns the display-column width of `s`: a tab expands to a flat
|
|
161
|
-
// `tabWidth` columns and
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
161
|
+
// `tabWidth` columns and everything else is charged by displayWidth, which is
|
|
162
|
+
// Prettier's own measurement. The only approximation left is the flat tab
|
|
163
|
+
// expansion (no tab-stop rounding), which never changes a real clause-join
|
|
164
|
+
// decision.
|
|
165
|
+
//
|
|
166
|
+
// Split on tabs rather than walked per rune, because displayWidth is not a sum
|
|
167
|
+
// over runes: an emoji sequence is measured whole, and splitting it would
|
|
168
|
+
// charge its parts.
|
|
165
169
|
func visualWidth(s string, tabWidth int) int {
|
|
166
170
|
width := 0
|
|
167
|
-
for
|
|
168
|
-
if
|
|
171
|
+
for i, segment := range strings.Split(s, "\t") {
|
|
172
|
+
if i > 0 {
|
|
169
173
|
width += tabWidth
|
|
170
|
-
continue
|
|
171
174
|
}
|
|
172
|
-
width +=
|
|
175
|
+
width += displayWidth(segment)
|
|
173
176
|
}
|
|
174
177
|
return width
|
|
175
178
|
}
|
|
@@ -150,13 +150,69 @@ func (formatIndent) Check(ctx *Context, node *shimast.Node) {
|
|
|
150
150
|
return
|
|
151
151
|
}
|
|
152
152
|
lineStart := lineStartOffset(src, closeBrace)
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
153
|
+
// A `}` sharing its line with content splits into two cases, and
|
|
154
|
+
// abstaining on both is what let the cascade converge on a malformed
|
|
155
|
+
// block. When the block opened on this same line it is a one-line block
|
|
156
|
+
// the cascade left whole (`{ x }`), and the brace is not this rule's to
|
|
157
|
+
// move. When the block opened earlier, the statement split already broke
|
|
158
|
+
// the body onto its own lines and stranded the brace at the end of the
|
|
159
|
+
// last statement — a hybrid Prettier never produces, and a fixed point,
|
|
160
|
+
// because nothing else claimed that brace. This rule takes it, so the two
|
|
161
|
+
// stay edit-disjoint within a pass instead of both abstaining.
|
|
162
|
+
stranded := false
|
|
156
163
|
for i := lineStart; i < closeBrace; i++ {
|
|
157
164
|
if src[i] != ' ' && src[i] != '\t' {
|
|
165
|
+
stranded = true
|
|
166
|
+
break
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if stranded {
|
|
170
|
+
// An empty body is `{}` in Prettier however its header wrapped, so a
|
|
171
|
+
// `}` whose only preceding content on the line is its own `{` is never
|
|
172
|
+
// this rule's to move. Checked first, and independently of the frame
|
|
173
|
+
// span below: `format/declaration-header` deliberately GLUES `{}` to
|
|
174
|
+
// the last line of a broken class or interface header, and that head
|
|
175
|
+
// starts lines above the brace, so the span test alone would unglue on
|
|
176
|
+
// the next pass exactly what that rule just gathered.
|
|
177
|
+
if bodyIsEmptyAtBrace(src, lineStart, closeBrace) {
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
// A frame that begins on the brace's own line is still whole
|
|
181
|
+
// (`{ x }`, `class C { m() {} }` before anything has split): the
|
|
182
|
+
// cascade has not broken the body out yet, so there is nothing for the
|
|
183
|
+
// brace to be consistent with. Once another pass moves the body onto
|
|
184
|
+
// its own lines the frame spans lines, this test stops holding, and
|
|
185
|
+
// the brace is claimed then.
|
|
186
|
+
//
|
|
187
|
+
// Measured on the frame node, which for a Block, ModuleBlock,
|
|
188
|
+
// CaseBlock, or type literal IS its `{`, and for a class or interface
|
|
189
|
+
// is the first byte of the declaration. The declaration start is never
|
|
190
|
+
// below its own `{`, so a body that has been broken out still reads as
|
|
191
|
+
// spanning; the one shape where the two differ — a wrapped header over
|
|
192
|
+
// a one-line body — is a shape Prettier expands as well.
|
|
193
|
+
if start := shimscanner.SkipTrivia(src, block.Pos()); start < 0 ||
|
|
194
|
+
start > len(src) || lineStartOffset(src, start) == lineStart {
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
if indentCededToReflow(block) || cededUnderBracelessBody(block) ||
|
|
198
|
+
typeLiteralIndentCeded(src, block, ownerDepth, layout) ||
|
|
199
|
+
cededUnderWrappedFunctionExpression(src, block) ||
|
|
200
|
+
cededByChainedArrowAncestor(block) {
|
|
158
201
|
return
|
|
159
202
|
}
|
|
203
|
+
// Replace the run of spaces and tabs before the brace rather than
|
|
204
|
+
// inserting beside it, so a re-run finds the brace already alone on its
|
|
205
|
+
// line and this branch never fires twice.
|
|
206
|
+
gap := closeBrace
|
|
207
|
+
for gap > lineStart && (src[gap-1] == ' ' || src[gap-1] == '\t') {
|
|
208
|
+
gap--
|
|
209
|
+
}
|
|
210
|
+
edits = append(edits, TextEdit{
|
|
211
|
+
Pos: gap,
|
|
212
|
+
End: closeBrace,
|
|
213
|
+
Text: layout.eol + layout.indent(ownerDepth),
|
|
214
|
+
})
|
|
215
|
+
return
|
|
160
216
|
}
|
|
161
217
|
// indentCededToReflow walks block.Parent upward, the same ancestor
|
|
162
218
|
// chain a body statement would, so a callback / expression-nested
|
|
@@ -186,11 +242,21 @@ func (formatIndent) Check(ctx *Context, node *shimast.Node) {
|
|
|
186
242
|
return
|
|
187
243
|
}
|
|
188
244
|
want := layout.indent(depth)
|
|
245
|
+
// A member or clause label sharing a line with the `{` that opened its
|
|
246
|
+
// body, or with the member before it, is the other half of the stranded
|
|
247
|
+
// brace: nothing else claims it, so a flat `class C { m() { … } }` or
|
|
248
|
+
// `switch (n) { case 1: … }` had no first edit and the whole cascade
|
|
249
|
+
// stalled on it. Breaking it out is what lets the statement split and the
|
|
250
|
+
// brace pass see the multi-line frame they act on.
|
|
251
|
+
pos := shimscanner.SkipTrivia(src, header.Pos())
|
|
252
|
+
if breakOntoOwnLine(src, header, pos, depth, want, layout, &edits) {
|
|
253
|
+
return
|
|
254
|
+
}
|
|
189
255
|
decorators := header.Decorators()
|
|
190
256
|
if len(decorators) == 0 {
|
|
191
257
|
// Undecorated member: header.Pos() is the declaration's first byte,
|
|
192
258
|
// so its line is the only one to align.
|
|
193
|
-
reindentHeaderLine(src,
|
|
259
|
+
reindentHeaderLine(src, pos, want, &edits)
|
|
194
260
|
return
|
|
195
261
|
}
|
|
196
262
|
// A decorated member spans multiple physical lines when its decorators
|
|
@@ -563,6 +629,91 @@ func reindentHeaderLine(src string, pos int, want string, edits *[]TextEdit) {
|
|
|
563
629
|
*edits = append(*edits, TextEdit{Pos: lineStart, End: pos, Text: want})
|
|
564
630
|
}
|
|
565
631
|
|
|
632
|
+
// bodyIsEmptyAtBrace reports whether the only thing before `closeBrace` on its
|
|
633
|
+
// line, past horizontal whitespace, is the `{` that opened the same body.
|
|
634
|
+
func bodyIsEmptyAtBrace(src string, lineStart int, closeBrace int) bool {
|
|
635
|
+
probe := closeBrace
|
|
636
|
+
for probe > lineStart && (src[probe-1] == ' ' || src[probe-1] == '\t') {
|
|
637
|
+
probe--
|
|
638
|
+
}
|
|
639
|
+
return probe > 0 && src[probe-1] == '{'
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// frameLineIsWhereDepthSays reports whether the line a frame opens on is
|
|
643
|
+
// indented to exactly the depth the block model computes for it. It is
|
|
644
|
+
// typeLiteralIndentCeded's test, generalized: when it does not hold, some other
|
|
645
|
+
// owner (a printer reflow, a wrapped initializer, a chain continuation) placed
|
|
646
|
+
// the frame, and a member indented to `layout.indent(memberDepth)` would land
|
|
647
|
+
// at a column unrelated to its own `{`.
|
|
648
|
+
func frameLineIsWhereDepthSays(
|
|
649
|
+
src string,
|
|
650
|
+
frame *shimast.Node,
|
|
651
|
+
frameDepth int,
|
|
652
|
+
layout formatLayout,
|
|
653
|
+
) bool {
|
|
654
|
+
if frame == nil {
|
|
655
|
+
return false
|
|
656
|
+
}
|
|
657
|
+
open := shimscanner.SkipTrivia(src, frame.Pos())
|
|
658
|
+
if open < 0 || open > len(src) {
|
|
659
|
+
return false
|
|
660
|
+
}
|
|
661
|
+
lineStart := lineStartOffset(src, open)
|
|
662
|
+
i := lineStart
|
|
663
|
+
for i < len(src) && (src[i] == ' ' || src[i] == '\t') {
|
|
664
|
+
i++
|
|
665
|
+
}
|
|
666
|
+
return src[lineStart:i] == layout.indent(frameDepth)
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// breakOntoOwnLine moves a member or clause header that shares its line with
|
|
670
|
+
// earlier content down to a line of its own at `want`, and reports whether it
|
|
671
|
+
// emitted that edit. It replaces the run of spaces and tabs before the header
|
|
672
|
+
// rather than inserting beside it, so a re-run finds the header already alone
|
|
673
|
+
// on its line and this never fires twice.
|
|
674
|
+
//
|
|
675
|
+
// A type-literal member is exempt. Prettier expands a class or interface body
|
|
676
|
+
// unconditionally, but an object TYPE keeps the author's shape when the source
|
|
677
|
+
// wrote no line break after `{` — `type T = { a: number }` stays one line — so
|
|
678
|
+
// breaking its members out would reformat conforming source. The type
|
|
679
|
+
// literal's own `}` is not exempt: a member already on its own line above a
|
|
680
|
+
// stranded `}` is a shape Prettier does not produce either way.
|
|
681
|
+
//
|
|
682
|
+
// A frame whose own line is not at the column the depth model computes is
|
|
683
|
+
// exempt too. `want` is derived from block nesting alone, so under a wrapped
|
|
684
|
+
// initializer (`const C: Ctor =\n class { m() {} };`) it names a column
|
|
685
|
+
// shallower than the `{` the member belongs to, and breaking there would place
|
|
686
|
+
// the member left of its own frame.
|
|
687
|
+
func breakOntoOwnLine(
|
|
688
|
+
src string,
|
|
689
|
+
header *shimast.Node,
|
|
690
|
+
pos int,
|
|
691
|
+
depth int,
|
|
692
|
+
want string,
|
|
693
|
+
layout formatLayout,
|
|
694
|
+
edits *[]TextEdit,
|
|
695
|
+
) bool {
|
|
696
|
+
if pos < 0 || pos > len(src) {
|
|
697
|
+
return false
|
|
698
|
+
}
|
|
699
|
+
if header.Parent != nil && header.Parent.Kind == shimast.KindTypeLiteral {
|
|
700
|
+
return false
|
|
701
|
+
}
|
|
702
|
+
if !frameLineIsWhereDepthSays(src, header.Parent, depth-1, layout) {
|
|
703
|
+
return false
|
|
704
|
+
}
|
|
705
|
+
lineStart := lineStartOffset(src, pos)
|
|
706
|
+
gap := pos
|
|
707
|
+
for gap > lineStart && (src[gap-1] == ' ' || src[gap-1] == '\t') {
|
|
708
|
+
gap--
|
|
709
|
+
}
|
|
710
|
+
if gap == lineStart {
|
|
711
|
+
return false
|
|
712
|
+
}
|
|
713
|
+
*edits = append(*edits, TextEdit{Pos: gap, End: pos, Text: layout.eol + want})
|
|
714
|
+
return true
|
|
715
|
+
}
|
|
716
|
+
|
|
566
717
|
// memberDeclarationStart returns the byte offset where a decorated member's
|
|
567
718
|
// actual declaration begins (just past its last leading decorator), or -1
|
|
568
719
|
// when the member has no decorators. The declaration start is the first
|
|
@@ -157,15 +157,13 @@ func (formatPrintWidth) Check(ctx *Context, node *shimast.Node) {
|
|
|
157
157
|
// the per-byte comment scan in hasNonChildComments. Charging that
|
|
158
158
|
// cost only on nodes that actually overflow keeps the hot path on
|
|
159
159
|
// well-formatted code allocation- and scan-free.
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
// byte-identical after reflow, so the fast path stands.
|
|
168
|
-
if !fastPathForcesBreak(node) &&
|
|
160
|
+
// Some layouts break regardless of width: function composition, same-kind
|
|
161
|
+
// nested arrays/objects, and any non-empty expression-position block. Such a
|
|
162
|
+
// shape can sit below a fitting call, array, conditional, object member, or
|
|
163
|
+
// parenthesized expression, so fastPathForcesBreak walks the whole subtree the
|
|
164
|
+
// structured printer can reach. Skip the fast path when it finds one; every
|
|
165
|
+
// other fitting flat node is byte-identical after reflow.
|
|
166
|
+
if !fastPathForcesBreak(node, src) &&
|
|
169
167
|
!sliceContainsNewline(src, start, end) &&
|
|
170
168
|
printOpts.StartingColumn+displayWidth(src[start:end])+trailingWidth <= printOpts.PrintWidth {
|
|
171
169
|
return
|
|
@@ -315,15 +313,7 @@ func trailingLineWidth(src string, end int, tabWidth int) int {
|
|
|
315
313
|
tabWidth = 2
|
|
316
314
|
}
|
|
317
315
|
lineEnd := trailingSuffixEnd(src, end)
|
|
318
|
-
|
|
319
|
-
for _, r := range src[end:lineEnd] {
|
|
320
|
-
if r == '\t' {
|
|
321
|
-
col += tabWidth - (col % tabWidth)
|
|
322
|
-
} else {
|
|
323
|
-
col += runeWidth(r)
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
return col
|
|
316
|
+
return displayWidthFromColumn(src[end:lineEnd], tabWidth, 0)
|
|
327
317
|
}
|
|
328
318
|
|
|
329
319
|
// trailingSuffixEnd returns the byte offset where the node's un-movable
|
|
@@ -376,14 +366,9 @@ func maxLineWidth(text string, startingColumn, trailingWidth, tabWidth int) int
|
|
|
376
366
|
lines := strings.Split(text, "\n")
|
|
377
367
|
widest := 0
|
|
378
368
|
for i, line := range lines {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
width += tabWidth - (width % tabWidth)
|
|
383
|
-
} else if r != '\r' {
|
|
384
|
-
width += runeWidth(r)
|
|
385
|
-
}
|
|
386
|
-
}
|
|
369
|
+
// A `\r` left by a CRLF split charges nothing: displayWidth reads it as the
|
|
370
|
+
// control character it is, exactly as Prettier's own loop does.
|
|
371
|
+
width := displayWidthFromColumn(line, tabWidth, 0)
|
|
387
372
|
if i == 0 {
|
|
388
373
|
width += startingColumn
|
|
389
374
|
}
|
|
@@ -409,15 +394,7 @@ func leadingColumn(src string, pos int, tabWidth int) int {
|
|
|
409
394
|
tabWidth = 2
|
|
410
395
|
}
|
|
411
396
|
lineStart := lineStartOffset(src, pos)
|
|
412
|
-
|
|
413
|
-
for _, r := range src[lineStart:pos] {
|
|
414
|
-
if r == '\t' {
|
|
415
|
-
col += tabWidth - (col % tabWidth)
|
|
416
|
-
} else {
|
|
417
|
-
col += runeWidth(r)
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
return col
|
|
397
|
+
return displayWidthFromColumn(src[lineStart:pos], tabWidth, 0)
|
|
421
398
|
}
|
|
422
399
|
|
|
423
400
|
// lineLeadingIndent returns the visual column of the first non-blank
|
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
package linthost
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"strconv"
|
|
5
|
+
|
|
4
6
|
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
5
7
|
shimscanner "github.com/microsoft/typescript-go/shim/scanner"
|
|
6
8
|
)
|
|
7
9
|
|
|
8
|
-
// formatQuoteProps normalizes quoting of object
|
|
9
|
-
// mirroring Prettier's `quoteProps
|
|
10
|
+
// formatQuoteProps normalizes quoting of object keys and method/type-member
|
|
11
|
+
// names, mirroring Prettier's `quoteProps` except for the documented semantic
|
|
12
|
+
// preservation of `__proto__` and non-ASCII identifier keys:
|
|
10
13
|
//
|
|
11
14
|
// - "as-needed" (default): drop the quotes from a string key that is a
|
|
12
15
|
// valid identifier and not a numeric-looking key. `{ "foo": 1 }` becomes
|
|
13
16
|
// `{ foo: 1 }`; `{ "bar-baz": 1 }` and `{ "123": 1 }` keep their quotes.
|
|
14
|
-
// - "consistent": if ANY key
|
|
15
|
-
//
|
|
17
|
+
// - "consistent": if ANY object key needs quotes, quote its removable
|
|
18
|
+
// identifier siblings; otherwise unquote every removable key.
|
|
16
19
|
// - "preserve": never change quoting.
|
|
17
20
|
//
|
|
18
21
|
// Only quoted keys the rule can SAFELY unquote are ever touched: a string
|
|
19
22
|
// whose content is a plain ASCII identifier (letters, `_`, `$`, digits after
|
|
20
23
|
// the first) with no escapes. A key with escapes, unicode, a leading digit
|
|
21
24
|
// (numeric-looking), or any non-identifier byte is left quoted in every
|
|
22
|
-
// mode, so the rule can never produce an invalid key.
|
|
25
|
+
// mode, so the rule can never produce an invalid key. Class fields remain
|
|
26
|
+
// outside the surface because Prettier preserves their quoted spelling.
|
|
23
27
|
type formatQuoteProps struct{ optionsRule }
|
|
24
28
|
|
|
25
29
|
type formatQuotePropsOptions struct {
|
|
@@ -30,15 +34,21 @@ func (formatQuoteProps) Name() string { return "format/quote-props" }
|
|
|
30
34
|
func (formatQuoteProps) IsFormat() bool { return true }
|
|
31
35
|
|
|
32
36
|
func (formatQuoteProps) Visits() []shimast.Kind {
|
|
33
|
-
return []shimast.Kind{
|
|
37
|
+
return []shimast.Kind{
|
|
38
|
+
shimast.KindObjectLiteralExpression,
|
|
39
|
+
shimast.KindClassDeclaration,
|
|
40
|
+
shimast.KindClassExpression,
|
|
41
|
+
shimast.KindInterfaceDeclaration,
|
|
42
|
+
shimast.KindTypeLiteral,
|
|
43
|
+
}
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
func (formatQuoteProps) Check(ctx *Context, node *shimast.Node) {
|
|
37
47
|
if ctx == nil || ctx.File == nil || node == nil {
|
|
38
48
|
return
|
|
39
49
|
}
|
|
40
|
-
|
|
41
|
-
if
|
|
50
|
+
members, isObject := quotePropsMembers(node)
|
|
51
|
+
if len(members) == 0 {
|
|
42
52
|
return
|
|
43
53
|
}
|
|
44
54
|
var opts formatQuotePropsOptions
|
|
@@ -56,27 +66,39 @@ func (formatQuoteProps) Check(ctx *Context, node *shimast.Node) {
|
|
|
56
66
|
src := ctx.File.Text()
|
|
57
67
|
|
|
58
68
|
// removable collects each quoted key that is safely unquotable, paired
|
|
59
|
-
// with its bare identifier.
|
|
60
|
-
//
|
|
69
|
+
// with its bare identifier. bare collects identifier keys that need quotes
|
|
70
|
+
// when a mixed object is normalized under `consistent`.
|
|
61
71
|
type removableKey struct {
|
|
62
72
|
start, end int
|
|
63
73
|
ident string
|
|
64
74
|
}
|
|
75
|
+
type bareKey struct {
|
|
76
|
+
start, end int
|
|
77
|
+
text string
|
|
78
|
+
}
|
|
65
79
|
var removable []removableKey
|
|
80
|
+
var bare []bareKey
|
|
66
81
|
anyMustStayQuoted := false
|
|
67
82
|
|
|
68
|
-
for _, prop := range
|
|
83
|
+
for _, prop := range members {
|
|
69
84
|
if prop == nil {
|
|
70
85
|
continue
|
|
71
86
|
}
|
|
72
87
|
name := propertyKeyName(prop)
|
|
73
|
-
if name == nil
|
|
88
|
+
if name == nil {
|
|
74
89
|
continue
|
|
75
90
|
}
|
|
76
91
|
ks := shimscanner.SkipTrivia(src, name.Pos())
|
|
77
92
|
ke := name.End()
|
|
78
93
|
if ks < 0 || ke <= ks || ke > len(src) {
|
|
79
|
-
return // give up on the whole
|
|
94
|
+
return // give up on the whole holder rather than risk a partial edit
|
|
95
|
+
}
|
|
96
|
+
if name.Kind == shimast.KindIdentifier {
|
|
97
|
+
bare = append(bare, bareKey{start: ks, end: ke, text: src[ks:ke]})
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
if name.Kind != shimast.KindStringLiteral {
|
|
101
|
+
continue
|
|
80
102
|
}
|
|
81
103
|
ident := unquotableIdentifier(src[ks:ke])
|
|
82
104
|
if ident == "" {
|
|
@@ -86,19 +108,22 @@ func (formatQuoteProps) Check(ctx *Context, node *shimast.Node) {
|
|
|
86
108
|
removable = append(removable, removableKey{start: ks, end: ke, ident: ident})
|
|
87
109
|
}
|
|
88
110
|
|
|
89
|
-
|
|
90
|
-
|
|
111
|
+
var edits []TextEdit
|
|
112
|
+
// Prettier's consistency rule applies to object literals only. Class and
|
|
113
|
+
// type members still follow the as-needed direction because they do not
|
|
114
|
+
// form an object-key group in Prettier's printer.
|
|
115
|
+
if mode == "consistent" && isObject && anyMustStayQuoted {
|
|
116
|
+
for _, key := range bare {
|
|
117
|
+
edits = append(edits, TextEdit{Pos: key.start, End: key.end, Text: strconv.Quote(key.text)})
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
for _, key := range removable {
|
|
121
|
+
edits = append(edits, TextEdit{Pos: key.start, End: key.end, Text: key.ident})
|
|
122
|
+
}
|
|
91
123
|
}
|
|
92
|
-
|
|
93
|
-
// kept quoted, so unquote nothing.
|
|
94
|
-
if mode == "consistent" && anyMustStayQuoted {
|
|
124
|
+
if len(edits) == 0 {
|
|
95
125
|
return
|
|
96
126
|
}
|
|
97
|
-
|
|
98
|
-
var edits []TextEdit
|
|
99
|
-
for _, k := range removable {
|
|
100
|
-
edits = append(edits, TextEdit{Pos: k.start, End: k.end, Text: k.ident})
|
|
101
|
-
}
|
|
102
127
|
ctx.ReportRangeFix(
|
|
103
128
|
edits[0].Pos,
|
|
104
129
|
edits[0].End,
|
|
@@ -107,8 +132,41 @@ func (formatQuoteProps) Check(ctx *Context, node *shimast.Node) {
|
|
|
107
132
|
)
|
|
108
133
|
}
|
|
109
134
|
|
|
110
|
-
//
|
|
111
|
-
//
|
|
135
|
+
// quotePropsMembers returns the members whose static names are governed by
|
|
136
|
+
// Prettier's quoteProps option. Class fields are intentionally excluded:
|
|
137
|
+
// Prettier preserves their quoted spelling, while class methods, interface
|
|
138
|
+
// members, and type-literal members follow the normal as-needed rule.
|
|
139
|
+
func quotePropsMembers(node *shimast.Node) ([]*shimast.Node, bool) {
|
|
140
|
+
if node == nil {
|
|
141
|
+
return nil, false
|
|
142
|
+
}
|
|
143
|
+
switch node.Kind {
|
|
144
|
+
case shimast.KindObjectLiteralExpression:
|
|
145
|
+
if obj := node.AsObjectLiteralExpression(); obj != nil && obj.Properties != nil {
|
|
146
|
+
return obj.Properties.Nodes, true
|
|
147
|
+
}
|
|
148
|
+
case shimast.KindClassDeclaration:
|
|
149
|
+
if decl := node.AsClassDeclaration(); decl != nil && decl.Members != nil {
|
|
150
|
+
return decl.Members.Nodes, false
|
|
151
|
+
}
|
|
152
|
+
case shimast.KindClassExpression:
|
|
153
|
+
if expr := node.AsClassExpression(); expr != nil && expr.Members != nil {
|
|
154
|
+
return expr.Members.Nodes, false
|
|
155
|
+
}
|
|
156
|
+
case shimast.KindInterfaceDeclaration:
|
|
157
|
+
if decl := node.AsInterfaceDeclaration(); decl != nil && decl.Members != nil {
|
|
158
|
+
return decl.Members.Nodes, false
|
|
159
|
+
}
|
|
160
|
+
case shimast.KindTypeLiteral:
|
|
161
|
+
if literal := node.AsTypeLiteralNode(); literal != nil && literal.Members != nil {
|
|
162
|
+
return literal.Members.Nodes, false
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return nil, false
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// propertyKeyName returns the static name node of a member governed by
|
|
169
|
+
// quoteProps, or nil when the member has no static name or is a class field.
|
|
112
170
|
func propertyKeyName(prop *shimast.Node) *shimast.Node {
|
|
113
171
|
switch prop.Kind {
|
|
114
172
|
case shimast.KindPropertyAssignment:
|
|
@@ -117,7 +175,9 @@ func propertyKeyName(prop *shimast.Node) *shimast.Node {
|
|
|
117
175
|
}
|
|
118
176
|
case shimast.KindMethodDeclaration,
|
|
119
177
|
shimast.KindGetAccessor,
|
|
120
|
-
shimast.KindSetAccessor
|
|
178
|
+
shimast.KindSetAccessor,
|
|
179
|
+
shimast.KindMethodSignature,
|
|
180
|
+
shimast.KindPropertySignature:
|
|
121
181
|
return prop.Name()
|
|
122
182
|
}
|
|
123
183
|
return nil
|
|
@@ -146,7 +206,8 @@ func unquotableIdentifier(raw string) string {
|
|
|
146
206
|
// A bare `__proto__:` key in an object literal is the spec-special
|
|
147
207
|
// prototype setter (sets [[Prototype]]), whereas a quoted `"__proto__"`
|
|
148
208
|
// key is an ordinary own data property. Unquoting would change runtime
|
|
149
|
-
// semantics, so
|
|
209
|
+
// semantics, so ttsc deliberately keeps it quoted even though Prettier
|
|
210
|
+
// does not.
|
|
150
211
|
return ""
|
|
151
212
|
}
|
|
152
213
|
for i := 0; i < len(inner); i++ {
|
|
@@ -61,12 +61,30 @@ func (formatStatementSplit) Check(ctx *Context, node *shimast.Node) {
|
|
|
61
61
|
layout := loadFormatLayout(ctx)
|
|
62
62
|
src := ctx.File.Text()
|
|
63
63
|
var edits []TextEdit
|
|
64
|
+
scannedCommentLists := map[*shimast.Node]bool{}
|
|
65
|
+
commentLists := map[*shimast.Node]bool{}
|
|
64
66
|
forEachStatementInList(ctx.File, func(stmt *shimast.Node, depth int) {
|
|
65
67
|
// Empty statements (`;`) carry no content. Splitting each one onto
|
|
66
68
|
// its own line only multiplies blank-ish noise, so abstain.
|
|
67
69
|
if stmt.Kind == shimast.KindEmptyStatement {
|
|
68
70
|
return
|
|
69
71
|
}
|
|
72
|
+
// Preflight the whole immediate statement list before splitting any one
|
|
73
|
+
// entry. Otherwise an early statement can move before a later boundary
|
|
74
|
+
// comment makes that later statement abstain, leaving a half-formatted
|
|
75
|
+
// list (`case 1:\n f(); /* keep */ break;`). The structured printer also
|
|
76
|
+
// abstains on this boundary, so statement-split must preserve the same
|
|
77
|
+
// all-or-nothing safety floor.
|
|
78
|
+
if indentCededToReflow(stmt) {
|
|
79
|
+
parent := stmt.Parent
|
|
80
|
+
if parent != nil && !scannedCommentLists[parent] {
|
|
81
|
+
scannedCommentLists[parent] = true
|
|
82
|
+
commentLists[parent] = statementListHasInterStatementComment(src, parent)
|
|
83
|
+
}
|
|
84
|
+
if parent != nil && commentLists[parent] {
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
}
|
|
70
88
|
start := shimscanner.SkipTrivia(src, stmt.Pos())
|
|
71
89
|
if start <= 0 || start > len(src) {
|
|
72
90
|
return
|
|
@@ -104,6 +122,17 @@ func (formatStatementSplit) Check(ctx *Context, node *shimast.Node) {
|
|
|
104
122
|
if stmt.Kind == shimast.KindBlock && firstStatementAfterCaseLabel(stmt) {
|
|
105
123
|
return
|
|
106
124
|
}
|
|
125
|
+
// A statement sharing its line with the `{` that opened its own block,
|
|
126
|
+
// where that block sits in expression position, has no brace owner:
|
|
127
|
+
// format/indent cedes such a block's `}` to format/print-width, so
|
|
128
|
+
// breaking the body out here would strand the brace on the last statement
|
|
129
|
+
// and make that hybrid the cascade's fixed point — the one shape neither
|
|
130
|
+
// rule can undo. Leave the block whole and let the printer decide whether
|
|
131
|
+
// it reflows. The two rules read the same predicate, so they cede the same
|
|
132
|
+
// blocks.
|
|
133
|
+
if indentCededToReflow(stmt) && sharesLineWithBlockOpenBrace(src, stmt, start) {
|
|
134
|
+
return
|
|
135
|
+
}
|
|
107
136
|
// The gap between the previous statement and this one must be pure
|
|
108
137
|
// whitespace. A `//` or `/*` anywhere from the previous statement's
|
|
109
138
|
// end to this one would be eaten by the replacement, so abstain. The
|
|
@@ -130,6 +159,58 @@ func (formatStatementSplit) Check(ctx *Context, node *shimast.Node) {
|
|
|
130
159
|
)
|
|
131
160
|
}
|
|
132
161
|
|
|
162
|
+
// statementListHasInterStatementComment reports whether a block, module,
|
|
163
|
+
// source file, or switch clause has a comment between two consecutive
|
|
164
|
+
// statements. Prefix comments remain owned by the existing per-statement gap
|
|
165
|
+
// guard; suffix comments do not affect a split. Scanning every interior gap up
|
|
166
|
+
// front prevents an edit before one boundary from partially formatting a list
|
|
167
|
+
// whose later boundary is intentionally immutable.
|
|
168
|
+
func statementListHasInterStatementComment(src string, parent *shimast.Node) bool {
|
|
169
|
+
if parent == nil {
|
|
170
|
+
return false
|
|
171
|
+
}
|
|
172
|
+
var statements []*shimast.Node
|
|
173
|
+
switch parent.Kind {
|
|
174
|
+
case shimast.KindSourceFile, shimast.KindBlock, shimast.KindModuleBlock:
|
|
175
|
+
statements = parent.Statements()
|
|
176
|
+
case shimast.KindCaseClause, shimast.KindDefaultClause:
|
|
177
|
+
clause := parent.AsCaseOrDefaultClause()
|
|
178
|
+
if clause != nil && clause.Statements != nil {
|
|
179
|
+
statements = clause.Statements.Nodes
|
|
180
|
+
}
|
|
181
|
+
default:
|
|
182
|
+
return false
|
|
183
|
+
}
|
|
184
|
+
for i := 1; i < len(statements); i++ {
|
|
185
|
+
previous, next := statements[i-1], statements[i]
|
|
186
|
+
if previous == nil || next == nil {
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
nextStart := shimscanner.SkipTrivia(src, next.Pos())
|
|
190
|
+
if gapHasComment(src, previous.End(), nextStart) {
|
|
191
|
+
return true
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return false
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// sharesLineWithBlockOpenBrace reports whether `start`, the first byte of a
|
|
198
|
+
// statement, sits on the same physical line as the `{` of the Block that
|
|
199
|
+
// directly contains it. It is the test for "this block is still written on one
|
|
200
|
+
// line", which is the state in which splitting the body out would strand the
|
|
201
|
+
// closing brace.
|
|
202
|
+
func sharesLineWithBlockOpenBrace(src string, stmt *shimast.Node, start int) bool {
|
|
203
|
+
block := stmt.Parent
|
|
204
|
+
if block == nil || block.Kind != shimast.KindBlock {
|
|
205
|
+
return false
|
|
206
|
+
}
|
|
207
|
+
open := shimscanner.SkipTrivia(src, block.Pos())
|
|
208
|
+
if open < 0 || open >= len(src) || src[open] != '{' {
|
|
209
|
+
return false
|
|
210
|
+
}
|
|
211
|
+
return lineStartOffset(src, open) == lineStartOffset(src, start)
|
|
212
|
+
}
|
|
213
|
+
|
|
133
214
|
// gapHasComment reports whether the byte range [start, end) contains the
|
|
134
215
|
// opening bytes of a `//` or `/*` comment. The split rule abstains when
|
|
135
216
|
// the inter-statement gap carries a comment so its line-break insertion
|