@ttsc/lint 0.10.1 → 0.11.0-dev.20260517

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.
Files changed (113) hide show
  1. package/README.md +172 -33
  2. package/lib/defineConfig.d.ts +55 -0
  3. package/lib/defineConfig.js +39 -0
  4. package/lib/defineConfig.js.map +1 -0
  5. package/lib/index.d.ts +25 -1
  6. package/lib/index.js +622 -2
  7. package/lib/index.js.map +1 -1
  8. package/lib/structures/ITtscLintPlugin.d.ts +53 -0
  9. package/lib/structures/ITtscLintPlugin.js +3 -0
  10. package/lib/structures/ITtscLintPlugin.js.map +1 -0
  11. package/lib/structures/ITtscLintPluginConfig.d.ts +82 -5
  12. package/lib/structures/ITtscLintPluginMeta.d.ts +12 -0
  13. package/lib/structures/ITtscLintPluginMeta.js +3 -0
  14. package/lib/structures/ITtscLintPluginMeta.js.map +1 -0
  15. package/lib/structures/PluginRuleNames.d.ts +14 -0
  16. package/lib/structures/PluginRuleNames.js +3 -0
  17. package/lib/structures/PluginRuleNames.js.map +1 -0
  18. package/lib/structures/TtscLintConfig.d.ts +24 -8
  19. package/lib/structures/TtscLintConfigEntry.d.ts +39 -0
  20. package/lib/structures/TtscLintConfigEntry.js +3 -0
  21. package/lib/structures/TtscLintConfigEntry.js.map +1 -0
  22. package/lib/structures/TtscLintFormatConfig.d.ts +154 -0
  23. package/lib/structures/TtscLintFormatConfig.js +3 -0
  24. package/lib/structures/TtscLintFormatConfig.js.map +1 -0
  25. package/lib/structures/TtscLintPlugins.d.ts +9 -0
  26. package/lib/structures/TtscLintPlugins.js +3 -0
  27. package/lib/structures/TtscLintPlugins.js.map +1 -0
  28. package/lib/structures/TtscLintRule.d.ts +1 -1
  29. package/lib/structures/TtscLintRuleEntry.d.ts +22 -0
  30. package/lib/structures/TtscLintRuleEntry.js +3 -0
  31. package/lib/structures/TtscLintRuleEntry.js.map +1 -0
  32. package/lib/structures/TtscLintRuleMap.d.ts +44 -0
  33. package/lib/structures/TtscLintRuleMap.js +3 -0
  34. package/lib/structures/TtscLintRuleMap.js.map +1 -0
  35. package/lib/structures/TtscLintRuleOptions.d.ts +149 -0
  36. package/lib/structures/TtscLintRuleOptions.js +3 -0
  37. package/lib/structures/TtscLintRuleOptions.js.map +1 -0
  38. package/lib/structures/defaultFormat.d.ts +31 -0
  39. package/lib/structures/defaultFormat.js +35 -0
  40. package/lib/structures/defaultFormat.js.map +1 -0
  41. package/lib/structures/index.d.ts +10 -0
  42. package/lib/structures/index.js +10 -0
  43. package/lib/structures/index.js.map +1 -1
  44. package/{plugin → linthost}/ast_helpers.go +92 -1
  45. package/{plugin → linthost}/compile.go +1 -1
  46. package/{plugin → linthost}/config.go +583 -66
  47. package/linthost/config_format.go +360 -0
  48. package/linthost/contrib_adapter.go +134 -0
  49. package/{plugin → linthost}/directives.go +1 -1
  50. package/linthost/dispatch.go +62 -0
  51. package/{plugin → linthost}/engine.go +156 -11
  52. package/{plugin → linthost}/eslint_runtime.go +76 -2
  53. package/linthost/fix.go +249 -0
  54. package/linthost/format.go +107 -0
  55. package/{plugin → linthost}/host.go +1 -1
  56. package/linthost/print_dispatch.go +118 -0
  57. package/linthost/print_doc.go +168 -0
  58. package/linthost/print_engine.go +343 -0
  59. package/linthost/print_nodes_array.go +46 -0
  60. package/linthost/print_nodes_call.go +180 -0
  61. package/linthost/print_nodes_imports.go +171 -0
  62. package/linthost/print_nodes_list.go +64 -0
  63. package/linthost/print_nodes_object.go +51 -0
  64. package/{plugin → linthost}/rules_arrays.go +1 -1
  65. package/{plugin → linthost}/rules_console.go +1 -1
  66. package/{plugin → linthost}/rules_debugger.go +1 -1
  67. package/{plugin → linthost}/rules_dupes.go +1 -1
  68. package/{plugin → linthost}/rules_empty.go +1 -1
  69. package/linthost/rules_escape.go +238 -0
  70. package/{plugin → linthost}/rules_eval.go +1 -1
  71. package/{plugin → linthost}/rules_finally.go +1 -1
  72. package/linthost/rules_format_jsdoc.go +230 -0
  73. package/linthost/rules_format_print_width.go +332 -0
  74. package/linthost/rules_format_quotes.go +219 -0
  75. package/linthost/rules_format_semi.go +190 -0
  76. package/linthost/rules_format_sort_imports.go +462 -0
  77. package/linthost/rules_format_trailing_comma.go +460 -0
  78. package/{plugin → linthost}/rules_gap.go +101 -6
  79. package/linthost/rules_imports.go +101 -0
  80. package/{plugin → linthost}/rules_logic.go +55 -3
  81. package/{plugin → linthost}/rules_loops.go +1 -1
  82. package/{plugin → linthost}/rules_misc.go +1 -1
  83. package/linthost/rules_params.go +64 -0
  84. package/{plugin → linthost}/rules_problems.go +10 -2
  85. package/linthost/rules_promise.go +111 -0
  86. package/{plugin → linthost}/rules_protos.go +1 -1
  87. package/{plugin → linthost}/rules_self.go +1 -1
  88. package/{plugin → linthost}/rules_strings.go +1 -1
  89. package/{plugin → linthost}/rules_suggestions.go +51 -9
  90. package/{plugin → linthost}/rules_throw.go +1 -1
  91. package/{plugin → linthost}/rules_ts.go +12 -2
  92. package/{plugin → linthost}/rules_ts_extra.go +90 -4
  93. package/{plugin → linthost}/rules_var.go +36 -5
  94. package/package.json +8 -6
  95. package/plugin/main.go +14 -27
  96. package/rule/astutil/astutil.go +148 -0
  97. package/rule/rule.go +291 -0
  98. package/src/defineConfig.ts +69 -0
  99. package/src/index.ts +807 -3
  100. package/src/structures/ITtscLintPlugin.ts +56 -0
  101. package/src/structures/ITtscLintPluginConfig.ts +86 -5
  102. package/src/structures/ITtscLintPluginMeta.ts +14 -0
  103. package/src/structures/PluginRuleNames.ts +19 -0
  104. package/src/structures/TtscLintConfig.ts +27 -8
  105. package/src/structures/TtscLintConfigEntry.ts +50 -0
  106. package/src/structures/TtscLintFormatConfig.ts +169 -0
  107. package/src/structures/TtscLintPlugins.ts +10 -0
  108. package/src/structures/TtscLintRule.ts +14 -1
  109. package/src/structures/TtscLintRuleEntry.ts +28 -0
  110. package/src/structures/TtscLintRuleMap.ts +52 -0
  111. package/src/structures/TtscLintRuleOptions.ts +162 -0
  112. package/src/structures/defaultFormat.ts +33 -0
  113. 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
+ }