@ttsc/lint 0.10.2 → 0.11.0-dev.20260518
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 +172 -33
- package/lib/defineConfig.d.ts +55 -0
- package/lib/defineConfig.js +39 -0
- package/lib/defineConfig.js.map +1 -0
- package/lib/index.d.ts +25 -1
- package/lib/index.js +622 -2
- package/lib/index.js.map +1 -1
- package/lib/structures/ITtscLintPlugin.d.ts +53 -0
- package/lib/structures/ITtscLintPlugin.js +3 -0
- package/lib/structures/ITtscLintPlugin.js.map +1 -0
- package/lib/structures/ITtscLintPluginConfig.d.ts +82 -5
- package/lib/structures/ITtscLintPluginMeta.d.ts +12 -0
- package/lib/structures/ITtscLintPluginMeta.js +3 -0
- package/lib/structures/ITtscLintPluginMeta.js.map +1 -0
- package/lib/structures/PluginRuleNames.d.ts +14 -0
- package/lib/structures/PluginRuleNames.js +3 -0
- package/lib/structures/PluginRuleNames.js.map +1 -0
- package/lib/structures/TtscLintConfig.d.ts +24 -8
- package/lib/structures/TtscLintConfigEntry.d.ts +39 -0
- package/lib/structures/TtscLintConfigEntry.js +3 -0
- package/lib/structures/TtscLintConfigEntry.js.map +1 -0
- package/lib/structures/TtscLintFormatConfig.d.ts +154 -0
- package/lib/structures/TtscLintFormatConfig.js +3 -0
- package/lib/structures/TtscLintFormatConfig.js.map +1 -0
- package/lib/structures/TtscLintPlugins.d.ts +9 -0
- package/lib/structures/TtscLintPlugins.js +3 -0
- package/lib/structures/TtscLintPlugins.js.map +1 -0
- package/lib/structures/TtscLintRule.d.ts +1 -1
- package/lib/structures/TtscLintRuleEntry.d.ts +22 -0
- package/lib/structures/TtscLintRuleEntry.js +3 -0
- package/lib/structures/TtscLintRuleEntry.js.map +1 -0
- package/lib/structures/TtscLintRuleMap.d.ts +44 -0
- package/lib/structures/TtscLintRuleMap.js +3 -0
- package/lib/structures/TtscLintRuleMap.js.map +1 -0
- package/lib/structures/TtscLintRuleOptions.d.ts +149 -0
- package/lib/structures/TtscLintRuleOptions.js +3 -0
- package/lib/structures/TtscLintRuleOptions.js.map +1 -0
- package/lib/structures/defaultFormat.d.ts +31 -0
- package/lib/structures/defaultFormat.js +35 -0
- package/lib/structures/defaultFormat.js.map +1 -0
- package/lib/structures/index.d.ts +10 -0
- package/lib/structures/index.js +10 -0
- package/lib/structures/index.js.map +1 -1
- package/{plugin → linthost}/ast_helpers.go +92 -1
- package/{plugin → linthost}/compile.go +1 -1
- package/{plugin → linthost}/config.go +583 -66
- package/linthost/config_format.go +360 -0
- package/linthost/contrib_adapter.go +134 -0
- package/{plugin → linthost}/directives.go +1 -1
- package/linthost/dispatch.go +65 -0
- package/{plugin → linthost}/engine.go +156 -11
- package/{plugin → linthost}/eslint_runtime.go +76 -2
- package/linthost/fix.go +249 -0
- package/linthost/format.go +107 -0
- package/{plugin → linthost}/host.go +1 -1
- package/linthost/print_dispatch.go +118 -0
- package/linthost/print_doc.go +168 -0
- package/linthost/print_engine.go +343 -0
- package/linthost/print_nodes_array.go +46 -0
- package/linthost/print_nodes_call.go +180 -0
- package/linthost/print_nodes_imports.go +171 -0
- package/linthost/print_nodes_list.go +64 -0
- package/linthost/print_nodes_object.go +51 -0
- package/{plugin → linthost}/rules_arrays.go +1 -1
- package/{plugin → linthost}/rules_console.go +1 -1
- package/{plugin → linthost}/rules_debugger.go +1 -1
- package/{plugin → linthost}/rules_dupes.go +1 -1
- package/{plugin → linthost}/rules_empty.go +1 -1
- package/linthost/rules_escape.go +238 -0
- package/{plugin → linthost}/rules_eval.go +1 -1
- package/{plugin → linthost}/rules_finally.go +1 -1
- package/linthost/rules_format_jsdoc.go +230 -0
- package/linthost/rules_format_print_width.go +332 -0
- package/linthost/rules_format_quotes.go +219 -0
- package/linthost/rules_format_semi.go +190 -0
- package/linthost/rules_format_sort_imports.go +462 -0
- package/linthost/rules_format_trailing_comma.go +460 -0
- package/{plugin → linthost}/rules_gap.go +101 -6
- package/linthost/rules_imports.go +101 -0
- package/{plugin → linthost}/rules_logic.go +55 -3
- package/{plugin → linthost}/rules_loops.go +1 -1
- package/{plugin → linthost}/rules_misc.go +1 -1
- package/linthost/rules_params.go +64 -0
- package/{plugin → linthost}/rules_problems.go +10 -2
- package/linthost/rules_promise.go +111 -0
- package/{plugin → linthost}/rules_protos.go +1 -1
- package/{plugin → linthost}/rules_self.go +1 -1
- package/{plugin → linthost}/rules_strings.go +1 -1
- package/{plugin → linthost}/rules_suggestions.go +51 -9
- package/{plugin → linthost}/rules_throw.go +1 -1
- package/{plugin → linthost}/rules_ts.go +12 -2
- package/{plugin → linthost}/rules_ts_extra.go +90 -4
- package/{plugin → linthost}/rules_var.go +36 -5
- package/package.json +8 -6
- package/plugin/main.go +14 -27
- package/rule/astutil/astutil.go +148 -0
- package/rule/rule.go +291 -0
- package/src/defineConfig.ts +69 -0
- package/src/index.ts +807 -3
- package/src/structures/ITtscLintPlugin.ts +56 -0
- package/src/structures/ITtscLintPluginConfig.ts +86 -5
- package/src/structures/ITtscLintPluginMeta.ts +14 -0
- package/src/structures/PluginRuleNames.ts +19 -0
- package/src/structures/TtscLintConfig.ts +27 -8
- package/src/structures/TtscLintConfigEntry.ts +50 -0
- package/src/structures/TtscLintFormatConfig.ts +169 -0
- package/src/structures/TtscLintPlugins.ts +10 -0
- package/src/structures/TtscLintRule.ts +14 -1
- package/src/structures/TtscLintRuleEntry.ts +28 -0
- package/src/structures/TtscLintRuleMap.ts +52 -0
- package/src/structures/TtscLintRuleOptions.ts +162 -0
- package/src/structures/defaultFormat.ts +33 -0
- package/src/structures/index.ts +10 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
package linthost
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
5
|
+
shimscanner "github.com/microsoft/typescript-go/shim/scanner"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// format/jsdoc rewrites JSDoc blocks toward the prettier-plugin-jsdoc
|
|
9
|
+
// canonical shape. The MVP implementation handles tag-synonym
|
|
10
|
+
// normalization; future passes will fold in tag sorting, @param column
|
|
11
|
+
// alignment, and description wrapping under the same rule name so
|
|
12
|
+
// projects pick up new behaviors by upgrading rather than by enabling
|
|
13
|
+
// additional rules. The synonym table covers the same names
|
|
14
|
+
// prettier-plugin-jsdoc documents:
|
|
15
|
+
//
|
|
16
|
+
// - @return → @returns
|
|
17
|
+
// - @arg, @argument → @param
|
|
18
|
+
// - @desc → @description
|
|
19
|
+
// - @virtual → @abstract
|
|
20
|
+
// - @func, @method → @function
|
|
21
|
+
//
|
|
22
|
+
// JSDoc tags only fire when the `@` is at the start of a JSDoc line —
|
|
23
|
+
// preceded by `*`, whitespace, or a newline. Inline `@foo` references in
|
|
24
|
+
// prose text are left alone. The rule scans source bytes directly inside
|
|
25
|
+
// `/** ... */` blocks; it deliberately avoids relying on the JSDoc AST
|
|
26
|
+
// because comment attachment is a moving target across TypeScript
|
|
27
|
+
// versions.
|
|
28
|
+
type formatJSDoc struct{}
|
|
29
|
+
|
|
30
|
+
// formatJSDocOptions mirrors `TtscLintRuleOptions.JSDoc`. The
|
|
31
|
+
// `tagSynonyms` map layers on top of the built-in synonym table so
|
|
32
|
+
// projects can add custom aliases without losing the defaults.
|
|
33
|
+
type formatJSDocOptions struct {
|
|
34
|
+
TagSynonyms map[string]string `json:"tagSynonyms"`
|
|
35
|
+
SortTags bool `json:"sortTags"`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func (formatJSDoc) Name() string { return "format/jsdoc" }
|
|
39
|
+
func (formatJSDoc) IsFormat() bool { return true }
|
|
40
|
+
func (formatJSDoc) Visits() []shimast.Kind {
|
|
41
|
+
return []shimast.Kind{shimast.KindSourceFile}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
var jsdocTagSynonyms = map[string]string{
|
|
45
|
+
"return": "returns",
|
|
46
|
+
"arg": "param",
|
|
47
|
+
"argument": "param",
|
|
48
|
+
"desc": "description",
|
|
49
|
+
"virtual": "abstract",
|
|
50
|
+
"func": "function",
|
|
51
|
+
"method": "function",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func (formatJSDoc) Check(ctx *Context, node *shimast.Node) {
|
|
55
|
+
if ctx == nil || ctx.File == nil {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
var opts formatJSDocOptions
|
|
59
|
+
_ = ctx.DecodeOptions(&opts)
|
|
60
|
+
synonyms := jsdocTagSynonyms
|
|
61
|
+
if len(opts.TagSynonyms) > 0 {
|
|
62
|
+
synonyms = make(map[string]string, len(jsdocTagSynonyms)+len(opts.TagSynonyms))
|
|
63
|
+
for k, v := range jsdocTagSynonyms {
|
|
64
|
+
synonyms[k] = v
|
|
65
|
+
}
|
|
66
|
+
for k, v := range opts.TagSynonyms {
|
|
67
|
+
// Reject empty canonicals or canonicals carrying non-identifier
|
|
68
|
+
// bytes — the fixer would otherwise emit malformed JSDoc like
|
|
69
|
+
// `@` or `@my tag`. Silently dropping the bad entry is the right
|
|
70
|
+
// failure mode: the rule already runs on every file in a project
|
|
71
|
+
// and a single typo'd entry should not poison every block.
|
|
72
|
+
if v == "" || !isValidJSDocTagName(v) {
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
synonyms[k] = v
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
src := ctx.File.Text()
|
|
79
|
+
for _, block := range findJSDocBlocks(src) {
|
|
80
|
+
rewriteJSDocTags(ctx, src, block, synonyms)
|
|
81
|
+
}
|
|
82
|
+
// `sortTags` is reserved for a follow-up that pulls in the
|
|
83
|
+
// prettier-plugin-jsdoc canonical order. The flag is parsed today so
|
|
84
|
+
// the type surface freezes, but the implementation lands in a future
|
|
85
|
+
// pass — projects that opt in get a no-op until then.
|
|
86
|
+
_ = opts.SortTags
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// jsdocBlock captures one `/** ... */` block's byte span. `bodyStart`
|
|
90
|
+
// excludes the leading `/**`, `bodyEnd` excludes the trailing `*/`.
|
|
91
|
+
type jsdocBlock struct {
|
|
92
|
+
start, end int
|
|
93
|
+
bodyStart, bodyEnd int
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// findJSDocBlocks enumerates JSDoc blocks via the tsgo scanner so the
|
|
97
|
+
// rule operates on real `MultiLineCommentTrivia` ranges. A naive
|
|
98
|
+
// byte-level scan for `/**` would mistakenly match `/**` sequences that
|
|
99
|
+
// appear inside string literals or template literals (e.g.
|
|
100
|
+
// `const s = "/** @return */";`), corrupting user data when the rule
|
|
101
|
+
// rewrote the embedded "tag" bytes. Using the scanner guarantees that
|
|
102
|
+
// only real comments enter the rule.
|
|
103
|
+
func findJSDocBlocks(src string) []jsdocBlock {
|
|
104
|
+
out := make([]jsdocBlock, 0)
|
|
105
|
+
scanner := shimscanner.NewScanner()
|
|
106
|
+
scanner.SetText(src)
|
|
107
|
+
scanner.SetSkipTrivia(false)
|
|
108
|
+
for {
|
|
109
|
+
kind := scanner.Scan()
|
|
110
|
+
if kind == shimast.KindEndOfFile {
|
|
111
|
+
break
|
|
112
|
+
}
|
|
113
|
+
if kind != shimast.KindMultiLineCommentTrivia {
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
start := scanner.TokenStart()
|
|
117
|
+
end := scanner.TokenEnd()
|
|
118
|
+
if end-start < 5 {
|
|
119
|
+
// Shorter than `/** */`, can't contain tags.
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
if !(src[start] == '/' && src[start+1] == '*' && src[start+2] == '*') {
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
// Skip `/**/` — an empty doc block has no tags to rewrite.
|
|
126
|
+
if end-start == 4 && src[start+3] == '/' {
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
out = append(out, jsdocBlock{
|
|
130
|
+
start: start,
|
|
131
|
+
end: end,
|
|
132
|
+
bodyStart: start + 3,
|
|
133
|
+
bodyEnd: end - 2,
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
return out
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
func rewriteJSDocTags(ctx *Context, src string, block jsdocBlock, synonyms map[string]string) {
|
|
140
|
+
for i := block.bodyStart; i < block.bodyEnd; i++ {
|
|
141
|
+
if src[i] != '@' {
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
if i > 0 {
|
|
145
|
+
prev := src[i-1]
|
|
146
|
+
if prev != ' ' && prev != '\t' && prev != '\n' && prev != '\r' && prev != '*' {
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
tagStart := i + 1
|
|
151
|
+
tagEnd := tagStart
|
|
152
|
+
for tagEnd < block.bodyEnd && isJSDocTagByte(src[tagEnd]) {
|
|
153
|
+
tagEnd++
|
|
154
|
+
}
|
|
155
|
+
if tagEnd == tagStart {
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
tag := src[tagStart:tagEnd]
|
|
159
|
+
// `@example` opens a region of free-form sample code that may
|
|
160
|
+
// include literal `@param` / `@return` etc. as part of the demo.
|
|
161
|
+
// Rewriting those would corrupt the example. Fast-forward past
|
|
162
|
+
// the example body to the next top-level tag.
|
|
163
|
+
if tag == "example" {
|
|
164
|
+
i = endOfJSDocExampleBody(src, block, tagEnd) - 1
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
canonical, ok := synonyms[tag]
|
|
168
|
+
if !ok || canonical == tag {
|
|
169
|
+
i = tagEnd - 1
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
ctx.ReportRangeFix(
|
|
173
|
+
tagStart-1,
|
|
174
|
+
tagEnd,
|
|
175
|
+
"JSDoc tag should use the canonical name.",
|
|
176
|
+
TextEdit{Pos: tagStart, End: tagEnd, Text: canonical},
|
|
177
|
+
)
|
|
178
|
+
i = tagEnd - 1
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// endOfJSDocExampleBody returns the byte offset of the next top-level
|
|
183
|
+
// tag (an `@` at line start, optionally after `*` and whitespace) at or
|
|
184
|
+
// after `start`, or `block.bodyEnd` when none exists. Used to skip the
|
|
185
|
+
// free-form body of an `@example` block when rewriting tag synonyms.
|
|
186
|
+
func endOfJSDocExampleBody(src string, block jsdocBlock, start int) int {
|
|
187
|
+
for i := start; i < block.bodyEnd; i++ {
|
|
188
|
+
if src[i] != '\n' {
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
191
|
+
// Find the first non-whitespace, non-`*` byte on the next line.
|
|
192
|
+
j := i + 1
|
|
193
|
+
for j < block.bodyEnd {
|
|
194
|
+
c := src[j]
|
|
195
|
+
if c == ' ' || c == '\t' || c == '\r' || c == '*' {
|
|
196
|
+
j++
|
|
197
|
+
continue
|
|
198
|
+
}
|
|
199
|
+
break
|
|
200
|
+
}
|
|
201
|
+
if j < block.bodyEnd && src[j] == '@' {
|
|
202
|
+
return j
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return block.bodyEnd
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
func isJSDocTagByte(b byte) bool {
|
|
209
|
+
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// isValidJSDocTagName reports whether `name` is a non-empty sequence of
|
|
213
|
+
// JSDoc tag bytes (ASCII letters). The rule rejects user-supplied
|
|
214
|
+
// canonical names that fall outside this shape so the fix output stays
|
|
215
|
+
// well-formed.
|
|
216
|
+
func isValidJSDocTagName(name string) bool {
|
|
217
|
+
if name == "" {
|
|
218
|
+
return false
|
|
219
|
+
}
|
|
220
|
+
for i := 0; i < len(name); i++ {
|
|
221
|
+
if !isJSDocTagByte(name[i]) {
|
|
222
|
+
return false
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return true
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
func init() {
|
|
229
|
+
Register(formatJSDoc{})
|
|
230
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
package linthost
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
5
|
+
shimscanner "github.com/microsoft/typescript-go/shim/scanner"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// format/print-width reflows expressions and declarations so they fit
|
|
9
|
+
// within a `printWidth`-column budget, mirroring Prettier's headline
|
|
10
|
+
// formatting feature.
|
|
11
|
+
//
|
|
12
|
+
// Coverage in v1 is intentionally narrow: the rule activates on the
|
|
13
|
+
// node kinds whose per-node printers are registered with the
|
|
14
|
+
// dispatcher (object/array literals, call/new expressions, named
|
|
15
|
+
// import / export clauses, top-level import declarations). For every
|
|
16
|
+
// other kind, the rule abstains — it never emits an edit that would
|
|
17
|
+
// modify bytes the dispatcher does not fully control. Coverage
|
|
18
|
+
// expands by adding kinds to the dispatcher; the rule itself does not
|
|
19
|
+
// need to grow.
|
|
20
|
+
//
|
|
21
|
+
// Per-node decision flow:
|
|
22
|
+
//
|
|
23
|
+
// 1. Skip leading trivia to find the node's actual first byte.
|
|
24
|
+
// 2. Count the leading column on that line — that becomes the
|
|
25
|
+
// printer's StartingIndent so continuation lines align under the
|
|
26
|
+
// opening token and fit measurement charges the prefix against
|
|
27
|
+
// the budget.
|
|
28
|
+
// 3. Build the node's Doc via PrintNode.
|
|
29
|
+
// 4. Render with the configured printWidth / tabWidth / useTabs /
|
|
30
|
+
// endOfLine.
|
|
31
|
+
// 5. Slice the original source bytes for the node's range.
|
|
32
|
+
// 6. If the rendered output differs, emit one TextEdit replacing
|
|
33
|
+
// [start, end) with the new bytes.
|
|
34
|
+
//
|
|
35
|
+
// The "no diff → no edit" invariant is what keeps `ttsc format`
|
|
36
|
+
// idempotent: a second pass renders identical bytes, the comparison
|
|
37
|
+
// short-circuits, and the cascade converges.
|
|
38
|
+
//
|
|
39
|
+
// The rule is a format-class rule (IsFormat == true) so `ttsc format`
|
|
40
|
+
// applies its edits while `ttsc check` only emits diagnostics for
|
|
41
|
+
// configured severities. Reuses the `error` severity caveat from
|
|
42
|
+
// other format rules: only set `error` once the full reflow coverage
|
|
43
|
+
// is mature.
|
|
44
|
+
type formatPrintWidth struct{}
|
|
45
|
+
|
|
46
|
+
// formatPrintWidthOptions mirrors `TtscLintRuleOptions.PrintWidth`.
|
|
47
|
+
type formatPrintWidthOptions struct {
|
|
48
|
+
PrintWidth *int `json:"printWidth"`
|
|
49
|
+
TabWidth *int `json:"tabWidth"`
|
|
50
|
+
UseTabs *bool `json:"useTabs"`
|
|
51
|
+
EndOfLine *string `json:"endOfLine"`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func (formatPrintWidth) Name() string { return "format/print-width" }
|
|
55
|
+
func (formatPrintWidth) IsFormat() bool { return true }
|
|
56
|
+
|
|
57
|
+
func (formatPrintWidth) Visits() []shimast.Kind {
|
|
58
|
+
return []shimast.Kind{
|
|
59
|
+
shimast.KindObjectLiteralExpression,
|
|
60
|
+
shimast.KindArrayLiteralExpression,
|
|
61
|
+
shimast.KindCallExpression,
|
|
62
|
+
shimast.KindNewExpression,
|
|
63
|
+
shimast.KindNamedImports,
|
|
64
|
+
shimast.KindNamedExports,
|
|
65
|
+
shimast.KindImportDeclaration,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func (formatPrintWidth) Check(ctx *Context, node *shimast.Node) {
|
|
70
|
+
if ctx == nil || ctx.File == nil || node == nil {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
var opts formatPrintWidthOptions
|
|
74
|
+
_ = ctx.DecodeOptions(&opts)
|
|
75
|
+
printOpts := DefaultPrintOptions()
|
|
76
|
+
if opts.PrintWidth != nil && *opts.PrintWidth > 0 {
|
|
77
|
+
printOpts.PrintWidth = *opts.PrintWidth
|
|
78
|
+
}
|
|
79
|
+
if opts.TabWidth != nil && *opts.TabWidth > 0 {
|
|
80
|
+
printOpts.TabWidth = *opts.TabWidth
|
|
81
|
+
}
|
|
82
|
+
if opts.UseTabs != nil {
|
|
83
|
+
printOpts.UseTabs = *opts.UseTabs
|
|
84
|
+
}
|
|
85
|
+
if opts.EndOfLine != nil {
|
|
86
|
+
printOpts.EndOfLine = *opts.EndOfLine
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
src := ctx.File.Text()
|
|
90
|
+
start := shimscanner.SkipTrivia(src, node.Pos())
|
|
91
|
+
end := node.End()
|
|
92
|
+
if start < 0 || end <= start || end > len(src) {
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Skip nested-print fires: if the visiting node has an ancestor
|
|
97
|
+
// that also belongs to the rule's set, the outer reflow already
|
|
98
|
+
// includes us. Acting at every level would emit overlapping edits
|
|
99
|
+
// that the applier rejects and would waste cascade passes.
|
|
100
|
+
if hasReflowAncestor(node) {
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Safety: abstain when the node carries comments outside its
|
|
105
|
+
// children. The per-node printers join child docs with a fresh
|
|
106
|
+
// `, ` separator and have no path for trivia between siblings, so
|
|
107
|
+
// reflowing such a node would silently delete the comment.
|
|
108
|
+
// Conservative — false positives mean a missed reflow opportunity,
|
|
109
|
+
// not a regression. Coverage of inline comments inside lists is
|
|
110
|
+
// the next slice of work.
|
|
111
|
+
if hasNonChildComments(node, src, start, end) {
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
printOpts.StartingColumn = leadingColumn(src, start, printOpts.TabWidth)
|
|
116
|
+
printOpts.BaseIndent = lineLeadingIndent(src, start, printOpts.TabWidth)
|
|
117
|
+
|
|
118
|
+
// Fast path: if the node's existing single-line bytes already fit
|
|
119
|
+
// the printWidth budget, the reflowed output cannot differ from
|
|
120
|
+
// the source (the printer would render the same flat shape). Skip
|
|
121
|
+
// the Doc build + render entirely. This is the common case on
|
|
122
|
+
// well-formatted code — every short call, every short literal —
|
|
123
|
+
// and saves the allocations from PrintNode + Print.
|
|
124
|
+
if !sliceContainsNewline(src, start, end) &&
|
|
125
|
+
printOpts.StartingColumn+(end-start) <= printOpts.PrintWidth {
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
printCtx := NewPrintContext(ctx.File, printOpts)
|
|
130
|
+
doc, _ := PrintNode(printCtx, node)
|
|
131
|
+
if doc.IsNil() {
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
rendered := Print(doc, printOpts)
|
|
135
|
+
original := src[start:end]
|
|
136
|
+
if rendered == original {
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
ctx.ReportRangeFix(
|
|
140
|
+
start,
|
|
141
|
+
end,
|
|
142
|
+
"Reflow to fit printWidth.",
|
|
143
|
+
TextEdit{Pos: start, End: end, Text: rendered},
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// leadingColumn returns the visual column the byte at `pos` occupies on
|
|
148
|
+
// its line. Tabs expand to `tabWidth` columns; other bytes count as 1.
|
|
149
|
+
// The rule uses this to seed the printer's StartingColumn so fit
|
|
150
|
+
// measurement charges the prefix against the column budget.
|
|
151
|
+
func leadingColumn(src string, pos int, tabWidth int) int {
|
|
152
|
+
if pos <= 0 {
|
|
153
|
+
return 0
|
|
154
|
+
}
|
|
155
|
+
if tabWidth <= 0 {
|
|
156
|
+
tabWidth = 2
|
|
157
|
+
}
|
|
158
|
+
lineStart := lineStartOffset(src, pos)
|
|
159
|
+
col := 0
|
|
160
|
+
for i := lineStart; i < pos; i++ {
|
|
161
|
+
if src[i] == '\t' {
|
|
162
|
+
col += tabWidth - (col % tabWidth)
|
|
163
|
+
} else {
|
|
164
|
+
col++
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return col
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// lineLeadingIndent returns the visual column of the first non-blank
|
|
171
|
+
// byte on the line containing `pos`. That is the indent the line's
|
|
172
|
+
// content starts at, regardless of where on the line the node itself
|
|
173
|
+
// sits.
|
|
174
|
+
//
|
|
175
|
+
// Continuation lines (Hardline / broken-mode Line) emitted by the
|
|
176
|
+
// reflow doc should align relative to this value, not relative to the
|
|
177
|
+
// node's leading column — see PrintOptions.BaseIndent for the contract.
|
|
178
|
+
//
|
|
179
|
+
// When the visited node lives on a *continuation line* (e.g. an RHS
|
|
180
|
+
// expression hanging below a binary operator on the previous line),
|
|
181
|
+
// the helper still returns the continuation line's own leading
|
|
182
|
+
// indent. This matches Prettier's convention: continuation indent
|
|
183
|
+
// anchors to the visual indent of the line carrying the node, not to
|
|
184
|
+
// the original statement's indent. Callers that need the latter
|
|
185
|
+
// would have to walk back through the AST themselves.
|
|
186
|
+
//
|
|
187
|
+
// `pos` must point at or after the first non-trivia byte on its line
|
|
188
|
+
// (callers in this file always pass `shimscanner.SkipTrivia` output,
|
|
189
|
+
// so the forward walk is bounded by that invariant in practice). The
|
|
190
|
+
// `i < pos` cap below makes the bound explicit, which keeps the
|
|
191
|
+
// helper safe against future callers that forget the precondition.
|
|
192
|
+
func lineLeadingIndent(src string, pos int, tabWidth int) int {
|
|
193
|
+
if tabWidth <= 0 {
|
|
194
|
+
tabWidth = 2
|
|
195
|
+
}
|
|
196
|
+
lineStart := lineStartOffset(src, pos)
|
|
197
|
+
col := 0
|
|
198
|
+
for i := lineStart; i < len(src) && i < pos; i++ {
|
|
199
|
+
c := src[i]
|
|
200
|
+
if c == ' ' {
|
|
201
|
+
col++
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
if c == '\t' {
|
|
205
|
+
col += tabWidth - (col % tabWidth)
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
break
|
|
209
|
+
}
|
|
210
|
+
return col
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// sliceContainsNewline reports whether the byte range [start, end)
|
|
214
|
+
// contains a newline. Used by the fast path to certify that a node
|
|
215
|
+
// fits its source's single line so the rule can skip the Doc build.
|
|
216
|
+
func sliceContainsNewline(src string, start, end int) bool {
|
|
217
|
+
if start < 0 {
|
|
218
|
+
start = 0
|
|
219
|
+
}
|
|
220
|
+
if end > len(src) {
|
|
221
|
+
end = len(src)
|
|
222
|
+
}
|
|
223
|
+
for i := start; i < end; i++ {
|
|
224
|
+
if src[i] == '\n' {
|
|
225
|
+
return true
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return false
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// lineStartOffset returns the byte offset of the start of the line
|
|
232
|
+
// containing `pos`. Used by both column helpers above.
|
|
233
|
+
func lineStartOffset(src string, pos int) int {
|
|
234
|
+
if pos <= 0 {
|
|
235
|
+
return 0
|
|
236
|
+
}
|
|
237
|
+
for pos > 0 && src[pos-1] != '\n' {
|
|
238
|
+
pos--
|
|
239
|
+
}
|
|
240
|
+
return pos
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// hasReflowAncestor reports whether any ancestor of `node` would also
|
|
244
|
+
// match the format/print-width visitor. The rule uses this to suppress
|
|
245
|
+
// nested fires when an enclosing reflow target already covers the
|
|
246
|
+
// child.
|
|
247
|
+
func hasReflowAncestor(node *shimast.Node) bool {
|
|
248
|
+
if node == nil {
|
|
249
|
+
return false
|
|
250
|
+
}
|
|
251
|
+
for parent := node.Parent; parent != nil; parent = parent.Parent {
|
|
252
|
+
if isReflowKind(parent.Kind) {
|
|
253
|
+
return true
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return false
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
func isReflowKind(k shimast.Kind) bool {
|
|
260
|
+
switch k {
|
|
261
|
+
case shimast.KindObjectLiteralExpression,
|
|
262
|
+
shimast.KindArrayLiteralExpression,
|
|
263
|
+
shimast.KindCallExpression,
|
|
264
|
+
shimast.KindNewExpression,
|
|
265
|
+
shimast.KindNamedImports,
|
|
266
|
+
shimast.KindNamedExports,
|
|
267
|
+
shimast.KindImportDeclaration:
|
|
268
|
+
return true
|
|
269
|
+
}
|
|
270
|
+
return false
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// hasNonChildComments scans the byte range [start, end) and returns true
|
|
274
|
+
// if any `//` or `/*` lives outside the union of `node`'s direct child
|
|
275
|
+
// byte ranges. The rule uses this to abstain from reflows that would
|
|
276
|
+
// drop inter-child comments — the v1 list printers join children with
|
|
277
|
+
// a fresh separator that has no slot for trivia between them.
|
|
278
|
+
//
|
|
279
|
+
// The scan is byte-level for simplicity: a TS scanner would also work
|
|
280
|
+
// but costs more. `inChild` masks comment-shaped bytes that live
|
|
281
|
+
// inside complete child token ranges (string and template literals
|
|
282
|
+
// are children, so `"//"` inside them never reaches the comment
|
|
283
|
+
// check). The residual conservative case is comment-shaped bytes
|
|
284
|
+
// inside an inter-child gap that the TS grammar would never tokenize
|
|
285
|
+
// as a comment — effectively nil for valid TypeScript source.
|
|
286
|
+
//
|
|
287
|
+
// `format/sort-imports` chose the opposite path on a similar shape:
|
|
288
|
+
// it actively preserves inter-specifier comments by walking the
|
|
289
|
+
// original byte ranges between elements. `format/print-width`
|
|
290
|
+
// abstains because the printer's separator (`,` + Line) is freshly
|
|
291
|
+
// minted and has no carrier slot for trivia. Extending preservation
|
|
292
|
+
// is a future slice; abstaining is byte-safe.
|
|
293
|
+
func hasNonChildComments(node *shimast.Node, src string, start, end int) bool {
|
|
294
|
+
if node == nil {
|
|
295
|
+
return false
|
|
296
|
+
}
|
|
297
|
+
type span struct{ pos, end int }
|
|
298
|
+
var children []span
|
|
299
|
+
node.ForEachChild(func(child *shimast.Node) bool {
|
|
300
|
+
if child == nil {
|
|
301
|
+
return false
|
|
302
|
+
}
|
|
303
|
+
// child.Pos() points at the start of leading trivia, which
|
|
304
|
+
// can include comments belonging to the sibling boundary.
|
|
305
|
+
// Trim past trivia so the inChild check only covers the
|
|
306
|
+
// child's actual token bytes — comments between siblings
|
|
307
|
+
// then surface to the scanner below.
|
|
308
|
+
children = append(children, span{shimscanner.SkipTrivia(src, child.Pos()), child.End()})
|
|
309
|
+
return false
|
|
310
|
+
})
|
|
311
|
+
inChild := func(i int) bool {
|
|
312
|
+
for _, c := range children {
|
|
313
|
+
if i >= c.pos && i < c.end {
|
|
314
|
+
return true
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return false
|
|
318
|
+
}
|
|
319
|
+
for i := start; i < end-1 && i < len(src)-1; i++ {
|
|
320
|
+
if inChild(i) {
|
|
321
|
+
continue
|
|
322
|
+
}
|
|
323
|
+
if src[i] == '/' && (src[i+1] == '/' || src[i+1] == '*') {
|
|
324
|
+
return true
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return false
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
func init() {
|
|
331
|
+
Register(formatPrintWidth{})
|
|
332
|
+
}
|