@ttsc/lint 0.25.0 → 0.26.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/structures/rules/ITtscLintFunctionalRuleOptions.d.ts +73 -16
- package/linthost/config_format.go +7 -0
- package/linthost/directives.go +4 -7
- package/linthost/fix.go +6 -4
- package/linthost/rule_codes.json +1 -0
- package/linthost/rules_format_brace_continuation.go +267 -0
- package/linthost/rules_format_clause_join.go +289 -40
- package/linthost/rules_format_parameter_properties.go +9 -21
- package/linthost/rules_format_whitespace.go +5 -1
- package/linthost/rules_functional.go +248 -25
- package/linthost/rules_ts_extra.go +7 -5
- package/package.json +2 -2
- package/rule/rule.go +21 -9
- package/src/structures/rules/ITtscLintFunctionalRuleOptions.ts +73 -16
package/README.md
CHANGED
|
@@ -138,7 +138,7 @@ Each `format` key controls one behavior:
|
|
|
138
138
|
| `sortImports` (opt-in) | Sort named specifiers and erased type-only imports. Runtime declaration sorting requires `unsafeSortRuntimeImports`. |
|
|
139
139
|
| `jsDoc` (on by default) | Normalize JSDoc blocks toward [prettier-plugin-jsdoc](https://github.com/hosseinmd/prettier-plugin-jsdoc). |
|
|
140
140
|
|
|
141
|
-
`sortImports` is **opt-in** — it takes effect only when you set it. Every other key takes effect as soon as the `format` block is present (JSDoc normalization included; set `jsDoc: false` to opt out), which also applies several keyless layout behaviors (statement splitting, indentation, whitespace normalization, clause joining, declaration-header reflow, ternary-nullish parens, leading-semicolon merging, and parameter-property breaking).
|
|
141
|
+
`sortImports` is **opt-in** — it takes effect only when you set it. Every other key takes effect as soon as the `format` block is present (JSDoc normalization included; set `jsDoc: false` to opt out), which also applies several keyless layout behaviors (statement splitting, indentation, whitespace normalization, clause joining, continuation-keyword placement, declaration-header reflow, ternary-nullish parens, leading-semicolon merging, and parameter-property breaking).
|
|
142
142
|
|
|
143
143
|
The safe default preserves the source order of every runtime-bearing import, including default, namespace, named, and bare imports, because each form can evaluate its dependency module. It still alphabetizes named specifiers within one declaration and can sort or merge a block made entirely of erased `import type` declarations. Set `unsafeSortRuntimeImports: true` only when every dependency in the block is order-independent. `combineTypeAndValue` affects a mixed type/value block only under that unsafe opt-in.
|
|
144
144
|
|
|
@@ -67,52 +67,105 @@ export interface ITtscLintFunctionalNoThrowStatementsRuleOptions {
|
|
|
67
67
|
}
|
|
68
68
|
/** `functional/no-mixed-types` rule options. */
|
|
69
69
|
export interface ITtscLintFunctionalNoMixedTypesRuleOptions {
|
|
70
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* Check interface member kinds.
|
|
72
|
+
*
|
|
73
|
+
* @default true
|
|
74
|
+
*/
|
|
71
75
|
checkInterfaces?: boolean;
|
|
72
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Check type-literal member kinds.
|
|
78
|
+
*
|
|
79
|
+
* @default true
|
|
80
|
+
*/
|
|
73
81
|
checkTypeLiterals?: boolean;
|
|
74
82
|
}
|
|
75
83
|
/** `functional/no-return-void` rule options. */
|
|
76
84
|
export interface ITtscLintFunctionalNoReturnVoidRuleOptions {
|
|
77
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* Permit a function whose declared return type is `null`. Set `false` to
|
|
87
|
+
* reject it the way a declared `void` is rejected.
|
|
88
|
+
*
|
|
89
|
+
* @default true
|
|
90
|
+
*/
|
|
78
91
|
allowNull?: boolean;
|
|
79
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* Permit a function whose declared return type is `undefined`. Set `false` to
|
|
94
|
+
* reject it the way a declared `void` is rejected.
|
|
95
|
+
*
|
|
96
|
+
* @default true
|
|
97
|
+
*/
|
|
80
98
|
allowUndefined?: boolean;
|
|
81
99
|
/**
|
|
82
|
-
* Skip
|
|
83
|
-
*
|
|
100
|
+
* Skip a bare `return;` inside a function that declares no return type. That
|
|
101
|
+
* statement is the one place the rule rejects a void-ness it inferred rather
|
|
102
|
+
* than read from an annotation.
|
|
103
|
+
*
|
|
104
|
+
* @default false
|
|
84
105
|
*/
|
|
85
106
|
ignoreInferredTypes?: boolean;
|
|
86
107
|
}
|
|
87
108
|
/** `functional/prefer-immutable-types` rule options. */
|
|
88
109
|
export interface ITtscLintFunctionalPreferImmutableTypesRuleOptions extends ITtscLintFunctionalPatternOptions {
|
|
89
110
|
/**
|
|
90
|
-
* Minimum accepted immutability.
|
|
111
|
+
* Minimum accepted immutability. Reserved for upstream-compatible configs;
|
|
112
|
+
* the native subset computes no immutability level and treats any configured
|
|
91
113
|
* value as readonly-required.
|
|
92
114
|
*/
|
|
93
115
|
enforcement?: "ReadonlyShallow" | "ReadonlyDeep" | "Immutable" | "None" | false;
|
|
94
116
|
}
|
|
95
117
|
/** `functional/prefer-readonly-type` rule options. */
|
|
96
118
|
export interface ITtscLintFunctionalPreferReadonlyTypeRuleOptions extends ITtscLintFunctionalPatternOptions {
|
|
97
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* Permit mutation of locals while still policing exported types. Reserved for
|
|
121
|
+
* upstream-compatible configs; the native subset reads type annotations and
|
|
122
|
+
* models no local-versus-exported distinction.
|
|
123
|
+
*/
|
|
98
124
|
allowLocalMutation?: boolean;
|
|
99
|
-
/**
|
|
125
|
+
/**
|
|
126
|
+
* Permit a mutable return type even when parameters must be readonly. Covers
|
|
127
|
+
* every signature that can declare one, including call and construct
|
|
128
|
+
* signatures, constructor types, and a get accessor.
|
|
129
|
+
*
|
|
130
|
+
* @default false
|
|
131
|
+
*/
|
|
100
132
|
allowMutableReturnType?: boolean;
|
|
101
|
-
/**
|
|
133
|
+
/**
|
|
134
|
+
* Also check property positions that have no explicit type annotation.
|
|
135
|
+
* Reserved for upstream-compatible configs; judging an unannotated position
|
|
136
|
+
* needs the type checker, which this rule does not use.
|
|
137
|
+
*/
|
|
102
138
|
checkImplicit?: boolean;
|
|
103
|
-
/**
|
|
139
|
+
/**
|
|
140
|
+
* Skip array / tuple / `Map` / `Set` types.
|
|
141
|
+
*
|
|
142
|
+
* @default false
|
|
143
|
+
*/
|
|
104
144
|
ignoreCollections?: boolean;
|
|
105
145
|
/**
|
|
106
|
-
* Skip class
|
|
107
|
-
*
|
|
146
|
+
* Skip class members. `true` skips anything under a class, its heritage
|
|
147
|
+
* clause and type parameters included; `"fieldsOnly"` narrows that to field
|
|
148
|
+
* declarations and keeps methods, accessors, and constructor parameters
|
|
149
|
+
* checked.
|
|
150
|
+
*
|
|
151
|
+
* @default false
|
|
108
152
|
*/
|
|
109
153
|
ignoreClass?: boolean | "fieldsOnly";
|
|
110
|
-
/**
|
|
154
|
+
/**
|
|
155
|
+
* Skip interface members entirely.
|
|
156
|
+
*
|
|
157
|
+
* @default false
|
|
158
|
+
*/
|
|
111
159
|
ignoreInterface?: boolean;
|
|
112
160
|
}
|
|
113
161
|
/** `functional/prefer-tacit` rule options. */
|
|
114
162
|
export interface ITtscLintFunctionalPreferTacitRuleOptions {
|
|
115
|
-
/**
|
|
163
|
+
/**
|
|
164
|
+
* Check member expressions such as `x => service.map(x)`. Set `false` to keep
|
|
165
|
+
* the rule on bare-identifier callees only.
|
|
166
|
+
*
|
|
167
|
+
* @default true
|
|
168
|
+
*/
|
|
116
169
|
checkMemberExpressions?: boolean;
|
|
117
170
|
}
|
|
118
171
|
/** `functional/readonly-type` rule options. */
|
|
@@ -133,7 +186,11 @@ export interface ITtscLintFunctionalTypeDeclarationImmutabilityRule {
|
|
|
133
186
|
* every value as readonly-required.
|
|
134
187
|
*/
|
|
135
188
|
immutability?: "ReadonlyShallow" | "ReadonlyDeep" | "Immutable" | "Mutable";
|
|
136
|
-
/**
|
|
189
|
+
/**
|
|
190
|
+
* Comparator applied to the immutability level above. Reserved for
|
|
191
|
+
* upstream-compatible configs alongside `immutability`: the native subset
|
|
192
|
+
* computes no immutability level, so there is nothing to compare.
|
|
193
|
+
*/
|
|
137
194
|
comparator?: "Less" | "AtMost" | "Exactly" | "AtLeast" | "More" | -2 | -1 | 0 | 1 | 2;
|
|
138
195
|
}
|
|
139
196
|
/** `functional/type-declaration-immutability` rule options. */
|
|
@@ -29,6 +29,7 @@ import (
|
|
|
29
29
|
// - `format/trailing-comma`, always on with the requested mode.
|
|
30
30
|
// - `format/print-width`, always on, driven by printWidth/tabWidth/useTabs/endOfLine.
|
|
31
31
|
// - `format/clause-join`, always on, joins a single-statement clause body that fits printWidth.
|
|
32
|
+
// - `format/brace-continuation`, always on, places `else`/`catch`/`finally`/do-`while` against the clause they continue.
|
|
32
33
|
// - `format/declaration-header`, always on, reflows a class/interface header's type params and heritage clauses.
|
|
33
34
|
// - `format/ternary-nullish-parens`, always on, parenthesizes a `??` operand of a conditional expression.
|
|
34
35
|
// - `format/orphan-semi`, always on; under semi:false, merges a leading-semicolon ASI guard onto its statement.
|
|
@@ -220,6 +221,12 @@ func expandFormatBlock(raw map[string]any) (map[string]any, error) {
|
|
|
220
221
|
}
|
|
221
222
|
out["format/whitespace"] = ruleEntry(wsOpts)
|
|
222
223
|
|
|
224
|
+
// formatBraceContinuation, always on. Needs only endOfLine: the pushed-down
|
|
225
|
+
// direction copies the owning statement's own indent verbatim rather than
|
|
226
|
+
// synthesizing one, so it shares the whitespace rule's trimmed surface rather
|
|
227
|
+
// than the layout one.
|
|
228
|
+
out["format/brace-continuation"] = ruleEntry(cloneStringAnyMap(wsOpts))
|
|
229
|
+
|
|
223
230
|
// formatSortImports, opt-in by `sortImports` (a boolean or options object).
|
|
224
231
|
// The top-level `endOfLine` is threaded in (like the other layout rules)
|
|
225
232
|
// so the rebuilt import block joins declarations with the file's line
|
package/linthost/directives.go
CHANGED
|
@@ -337,18 +337,15 @@ func directivePayload(text, marker string) (string, bool) {
|
|
|
337
337
|
return strings.TrimSpace(rest), true
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
-
// parseDirectiveRules converts the payload (the text after the directive
|
|
341
|
-
// marker) into a lintDirectiveRules value. The `--` separator strips an
|
|
342
|
-
// optional human-readable description. An empty rule list means "all rules".
|
|
343
|
-
func parseDirectiveRules(payload string) lintDirectiveRules {
|
|
344
|
-
return parseDirectivePayload(payload).rules
|
|
345
|
-
}
|
|
346
|
-
|
|
347
340
|
type parsedDirectivePayload struct {
|
|
348
341
|
rules lintDirectiveRules
|
|
349
342
|
ruleList []string
|
|
350
343
|
}
|
|
351
344
|
|
|
345
|
+
// parseDirectivePayload converts the payload (the text after the directive
|
|
346
|
+
// marker) into the directive's rule scope plus the raw list the engine reports
|
|
347
|
+
// unknown names from. The `--` separator strips an optional human-readable
|
|
348
|
+
// description. An empty rule list means "all rules".
|
|
352
349
|
func parseDirectivePayload(payload string) parsedDirectivePayload {
|
|
353
350
|
ruleText := stripDirectiveDescription(payload)
|
|
354
351
|
ruleText = strings.ReplaceAll(ruleText, ",", " ")
|
package/linthost/fix.go
CHANGED
|
@@ -289,10 +289,12 @@ func selectTextEdits(sourceLen int, edits []TextEdit) []TextEdit {
|
|
|
289
289
|
// insert at that same offset must be dropped: two coincident inserts both
|
|
290
290
|
// pass the `edit.Pos < lastEnd` gate, then apply in reverse sort order and
|
|
291
291
|
// concatenate at one point — silently corrupting the source (e.g. a `;`
|
|
292
|
-
// insert and a `\n` insert at EOF yielding `\n;`).
|
|
293
|
-
// one winner and drops the rest
|
|
294
|
-
//
|
|
295
|
-
//
|
|
292
|
+
// insert and a `\n` insert at EOF yielding `\n;`). This edit-level selector
|
|
293
|
+
// keeps one winner and drops the rest; selectTextEditGroups turns that drop
|
|
294
|
+
// into a whole-finding skip, which is the contract rule.TextEdit states. A
|
|
295
|
+
// zero-width insert sitting at the end of a prior NON-empty edit is left
|
|
296
|
+
// alone: it applies cleanly after the replacement and is a legitimate
|
|
297
|
+
// adjacency.
|
|
296
298
|
lastInsertAt := -1
|
|
297
299
|
for _, edit := range sorted {
|
|
298
300
|
if edit.Pos < lastEnd {
|
package/linthost/rule_codes.json
CHANGED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
package linthost
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
|
|
6
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
7
|
+
shimscanner "github.com/microsoft/typescript-go/shim/scanner"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// formatBraceContinuation places a continuation keyword against the clause it
|
|
11
|
+
// continues, mirroring Prettier. The rule is one decision read in two
|
|
12
|
+
// directions, taken from the shape of the preceding clause:
|
|
13
|
+
//
|
|
14
|
+
// - the clause is a block, so the keyword shares its closing brace's line:
|
|
15
|
+
// `} else {`, `} catch (e) {`, `} finally {`, `} while (ready);`
|
|
16
|
+
// - the clause is not a block, so the keyword starts its own line:
|
|
17
|
+
// `if (a) x();` then `else y();`, and `do tick();` then `while (ready);`
|
|
18
|
+
//
|
|
19
|
+
// Both halves belong to one rule because they are the same decision. Splitting
|
|
20
|
+
// them across two owners lets a source satisfy neither, which is what the
|
|
21
|
+
// formatter did before: it never touched this boundary at all, so an
|
|
22
|
+
// Allman-braced file survived `ttsc format` unchanged and a one-line
|
|
23
|
+
// `if (a) x(); else y();` stayed on one line.
|
|
24
|
+
//
|
|
25
|
+
// The rule rewrites only the gap between the preceding clause's last byte and
|
|
26
|
+
// the continuation keyword's first byte. No structural format rule contends for
|
|
27
|
+
// it: `format/clause-join` rewrites the gap AFTER a header token,
|
|
28
|
+
// `format/statement-split` splits statement-list members, and `format/indent`
|
|
29
|
+
// visits statement-list members, closing-brace lines, and member headers.
|
|
30
|
+
// `format/whitespace`'s trailing-whitespace trim does land inside the gap when
|
|
31
|
+
// the clause line ends in spaces; the host drops one of the two findings and the
|
|
32
|
+
// survivor re-fires on the next cascade pass, so the file still converges.
|
|
33
|
+
//
|
|
34
|
+
// A comment in the gap makes the rule abstain rather than relocate it, the same
|
|
35
|
+
// abstention `format/clause-join` and `format/statement-split` apply. Idempotent
|
|
36
|
+
// in both directions: once the keyword sits where it belongs the gap already
|
|
37
|
+
// holds the target text and the rule emits nothing.
|
|
38
|
+
type formatBraceContinuation struct{ optionsRule }
|
|
39
|
+
|
|
40
|
+
// formatBraceContinuationOptions carries only the EOL setting. The push-down
|
|
41
|
+
// direction copies the owning statement's own indent verbatim rather than
|
|
42
|
+
// synthesizing one, so tabWidth and useTabs would decide nothing here and the
|
|
43
|
+
// rule takes the same trimmed option surface `format/whitespace` does. The JSON
|
|
44
|
+
// tag matches the `format` block key the config layer mirrors in (see
|
|
45
|
+
// expandFormatBlock).
|
|
46
|
+
type formatBraceContinuationOptions struct {
|
|
47
|
+
EndOfLine *string `json:"endOfLine"`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func (formatBraceContinuation) Name() string { return "format/brace-continuation" }
|
|
51
|
+
func (formatBraceContinuation) IsFormat() bool { return true }
|
|
52
|
+
|
|
53
|
+
func (formatBraceContinuation) Visits() []shimast.Kind {
|
|
54
|
+
return []shimast.Kind{
|
|
55
|
+
shimast.KindIfStatement,
|
|
56
|
+
shimast.KindTryStatement,
|
|
57
|
+
shimast.KindDoStatement,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// braceContinuation is one keyword placement: the clause that precedes it and
|
|
62
|
+
// the keyword that continues it.
|
|
63
|
+
type braceContinuation struct {
|
|
64
|
+
// previous is the clause the keyword continues. Its `End()` is the left edge
|
|
65
|
+
// of the gap, and whether it is a block decides the direction.
|
|
66
|
+
previous *shimast.Node
|
|
67
|
+
// keyword is the exact lexeme expected at the right edge of the gap. The
|
|
68
|
+
// following node cannot bound the scan: a catch clause and a finally block
|
|
69
|
+
// both start at the preceding clause's end, so their Pos() is the gap's left
|
|
70
|
+
// edge, not its right. Matching the lexeme instead is also what makes a
|
|
71
|
+
// comment in the gap detectable, since the first non-whitespace byte is then
|
|
72
|
+
// the comment rather than the keyword.
|
|
73
|
+
keyword string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
func (formatBraceContinuation) Check(ctx *Context, node *shimast.Node) {
|
|
77
|
+
if ctx == nil || ctx.File == nil || node == nil {
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
var opts formatBraceContinuationOptions
|
|
81
|
+
_ = ctx.DecodeOptions(&opts)
|
|
82
|
+
eol := "\n"
|
|
83
|
+
if opts.EndOfLine != nil && *opts.EndOfLine == "crlf" {
|
|
84
|
+
eol = "\r\n"
|
|
85
|
+
}
|
|
86
|
+
src := ctx.File.Text()
|
|
87
|
+
for _, continuation := range braceContinuations(node) {
|
|
88
|
+
placeBraceContinuation(ctx, src, node, continuation, eol)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// braceContinuations returns every continuation keyword `node` carries, paired
|
|
93
|
+
// with the clause it continues.
|
|
94
|
+
func braceContinuations(node *shimast.Node) []braceContinuation {
|
|
95
|
+
switch node.Kind {
|
|
96
|
+
case shimast.KindIfStatement:
|
|
97
|
+
stmt := node.AsIfStatement()
|
|
98
|
+
if stmt == nil || stmt.ElseStatement == nil || stmt.ThenStatement == nil {
|
|
99
|
+
return nil
|
|
100
|
+
}
|
|
101
|
+
return []braceContinuation{{previous: stmt.ThenStatement, keyword: "else"}}
|
|
102
|
+
case shimast.KindTryStatement:
|
|
103
|
+
stmt := node.AsTryStatement()
|
|
104
|
+
if stmt == nil || stmt.TryBlock == nil {
|
|
105
|
+
return nil
|
|
106
|
+
}
|
|
107
|
+
out := make([]braceContinuation, 0, 2)
|
|
108
|
+
previous := stmt.TryBlock.AsNode()
|
|
109
|
+
if stmt.CatchClause != nil {
|
|
110
|
+
out = append(out, braceContinuation{previous: previous, keyword: "catch"})
|
|
111
|
+
previous = stmt.CatchClause
|
|
112
|
+
}
|
|
113
|
+
if stmt.FinallyBlock != nil {
|
|
114
|
+
out = append(out, braceContinuation{previous: previous, keyword: "finally"})
|
|
115
|
+
}
|
|
116
|
+
return out
|
|
117
|
+
case shimast.KindDoStatement:
|
|
118
|
+
stmt := node.AsDoStatement()
|
|
119
|
+
if stmt == nil || stmt.Statement == nil || stmt.Expression == nil {
|
|
120
|
+
return nil
|
|
121
|
+
}
|
|
122
|
+
return []braceContinuation{{previous: stmt.Statement, keyword: "while"}}
|
|
123
|
+
}
|
|
124
|
+
return nil
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
func placeBraceContinuation(
|
|
128
|
+
ctx *Context,
|
|
129
|
+
src string,
|
|
130
|
+
node *shimast.Node,
|
|
131
|
+
continuation braceContinuation,
|
|
132
|
+
eol string,
|
|
133
|
+
) {
|
|
134
|
+
previous := continuation.previous
|
|
135
|
+
if previous == nil {
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
gapStart := previous.End()
|
|
139
|
+
if gapStart < 0 || gapStart > len(src) {
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
// The gap is the whitespace run after the preceding clause. The bytes that
|
|
143
|
+
// follow it must be the continuation keyword itself: a comment there is
|
|
144
|
+
// content the rewrite would delete, and it shows up as a first non-whitespace
|
|
145
|
+
// byte that is not the keyword.
|
|
146
|
+
keywordStart := gapStart
|
|
147
|
+
for keywordStart < len(src) && isBraceContinuationGapByte(src[keywordStart]) {
|
|
148
|
+
keywordStart++
|
|
149
|
+
}
|
|
150
|
+
if !braceContinuationKeywordAt(src, keywordStart, continuation.keyword) {
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
want := " "
|
|
155
|
+
if !braceContinuationEndsInBlock(previous) {
|
|
156
|
+
// A non-block clause pushes the keyword onto its own line, indented to the
|
|
157
|
+
// statement that owns it. The whole gap is the target, so a keyword already
|
|
158
|
+
// on its own line at the wrong column is corrected by the same comparison
|
|
159
|
+
// instead of being ceded to a rule that never visits that line.
|
|
160
|
+
indent, ok := braceContinuationIndent(src, node)
|
|
161
|
+
if !ok {
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
want = eol + indent
|
|
165
|
+
}
|
|
166
|
+
if src[gapStart:keywordStart] == want {
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
ctx.ReportRangeFix(
|
|
170
|
+
gapStart,
|
|
171
|
+
keywordStart,
|
|
172
|
+
"Continuation keyword should sit against the clause it continues.",
|
|
173
|
+
TextEdit{Pos: gapStart, End: keywordStart, Text: want},
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// braceContinuationIndent returns the leading whitespace of the line the
|
|
178
|
+
// statement starts on, which is the column Prettier gives the pushed-down
|
|
179
|
+
// keyword, and reports whether that column is knowable yet.
|
|
180
|
+
//
|
|
181
|
+
// A statement sharing its line with something else has no column of its own,
|
|
182
|
+
// and nothing would repair a keyword pushed to column zero: `format/indent`
|
|
183
|
+
// visits statement-list members, closing-brace lines, and member headers, and a
|
|
184
|
+
// continuation-keyword line is none of the three. So the rule abstains, lets
|
|
185
|
+
// `format/statement-split` give the statement its own line first, and reads the
|
|
186
|
+
// real column on the next cascade pass. `format/indent` takes the same
|
|
187
|
+
// not-first-on-its-line abstention for the same reason.
|
|
188
|
+
func braceContinuationIndent(src string, node *shimast.Node) (string, bool) {
|
|
189
|
+
start := shimscanner.SkipTrivia(src, node.Pos())
|
|
190
|
+
if start < 0 || start > len(src) {
|
|
191
|
+
return "", false
|
|
192
|
+
}
|
|
193
|
+
lineStart := lineStartOffset(src, start)
|
|
194
|
+
indentEnd := lineStart
|
|
195
|
+
for indentEnd < start && (src[indentEnd] == ' ' || src[indentEnd] == '\t') {
|
|
196
|
+
indentEnd++
|
|
197
|
+
}
|
|
198
|
+
// A label prefix is part of the statement's own line rather than another
|
|
199
|
+
// statement sharing it, and nothing downstream would ever give a labeled body
|
|
200
|
+
// its own line, so refusing here would strand the keyword permanently.
|
|
201
|
+
if !isBraceContinuationLabelPrefix(src[indentEnd:start]) {
|
|
202
|
+
return "", false
|
|
203
|
+
}
|
|
204
|
+
return src[lineStart:indentEnd], true
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// isBraceContinuationLabelPrefix reports whether `prefix` is empty or a run of
|
|
208
|
+
// `identifier :` labels, the only thing allowed to precede a statement on the
|
|
209
|
+
// line whose indent the pushed-down keyword adopts.
|
|
210
|
+
func isBraceContinuationLabelPrefix(prefix string) bool {
|
|
211
|
+
for {
|
|
212
|
+
prefix = strings.TrimLeft(prefix, " \t")
|
|
213
|
+
if prefix == "" {
|
|
214
|
+
return true
|
|
215
|
+
}
|
|
216
|
+
name := 0
|
|
217
|
+
for name < len(prefix) && isBraceContinuationIdentifierByte(prefix[name]) {
|
|
218
|
+
name++
|
|
219
|
+
}
|
|
220
|
+
if name == 0 {
|
|
221
|
+
return false
|
|
222
|
+
}
|
|
223
|
+
rest := strings.TrimLeft(prefix[name:], " \t")
|
|
224
|
+
if !strings.HasPrefix(rest, ":") {
|
|
225
|
+
return false
|
|
226
|
+
}
|
|
227
|
+
prefix = rest[1:]
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// braceContinuationEndsInBlock reports whether the keyword shares the preceding
|
|
232
|
+
// clause's last line, which is what decides the direction. The predicate is the
|
|
233
|
+
// clause's kind, not its last byte: a `switch` consequent and an Annex-B
|
|
234
|
+
// `function` consequent both end in `}` and Prettier still pushes `else` onto
|
|
235
|
+
// its own line after them. A catch clause is the one non-block kind that shares
|
|
236
|
+
// its line, because the try printer always spaces `} finally`.
|
|
237
|
+
func braceContinuationEndsInBlock(previous *shimast.Node) bool {
|
|
238
|
+
return previous.Kind == shimast.KindBlock || previous.Kind == shimast.KindCatchClause
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// braceContinuationKeywordAt reports whether `keyword` starts at `offset` as a
|
|
242
|
+
// whole token. The trailing boundary matters: `finally` must not match the
|
|
243
|
+
// `final` prefix of an identifier a comment left exposed.
|
|
244
|
+
func braceContinuationKeywordAt(src string, offset int, keyword string) bool {
|
|
245
|
+
end := offset + len(keyword)
|
|
246
|
+
if offset < 0 || end > len(src) || src[offset:end] != keyword {
|
|
247
|
+
return false
|
|
248
|
+
}
|
|
249
|
+
return end == len(src) || !isBraceContinuationIdentifierByte(src[end])
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// isBraceContinuationIdentifierByte reports whether `c` can appear inside an
|
|
253
|
+
// identifier, so a keyword is only accepted as a whole token.
|
|
254
|
+
func isBraceContinuationIdentifierByte(c byte) bool {
|
|
255
|
+
return c == 0x5f || c == 0x24 ||
|
|
256
|
+
(c >= 0x61 && c <= 0x7a) || (c >= 0x41 && c <= 0x5a) || (c >= 0x30 && c <= 0x39)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// isBraceContinuationGapByte reports whether `c` is whitespace that may appear
|
|
260
|
+
// between a clause and the keyword continuing it.
|
|
261
|
+
func isBraceContinuationGapByte(c byte) bool {
|
|
262
|
+
return c == ' ' || c == '\t' || c == '\r' || c == '\n'
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
func init() {
|
|
266
|
+
Register(formatBraceContinuation{})
|
|
267
|
+
}
|