@ttsc/lint 0.10.2 → 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,462 @@
1
+ package linthost
2
+
3
+ import (
4
+ "regexp"
5
+ "sort"
6
+ "strings"
7
+
8
+ shimast "github.com/microsoft/typescript-go/shim/ast"
9
+ shimscanner "github.com/microsoft/typescript-go/shim/scanner"
10
+ )
11
+
12
+ // thirdPartyModulesPlaceholder is the bucket that absorbs any import
13
+ // whose specifier doesn't match an explicit `importOrder` regex. Mirrors
14
+ // `@trivago/prettier-plugin-sort-imports`.
15
+ const thirdPartyModulesPlaceholder = "<THIRD_PARTY_MODULES>"
16
+
17
+ // formatSortImportsOptions mirrors `TtscLintRuleOptions.SortImports`.
18
+ type formatSortImportsOptions struct {
19
+ ImportOrder []string `json:"importOrder"`
20
+ ImportOrderSeparation *bool `json:"importOrderSeparation"`
21
+ ImportOrderSortSpecifiers *bool `json:"importOrderSortSpecifiers"`
22
+ ImportOrderCaseInsensitive bool `json:"importOrderCaseInsensitive"`
23
+ }
24
+
25
+ // defaultImportOrder mirrors the two-group MVP scheme used when the user
26
+ // supplies no explicit `importOrder`. External modules first
27
+ // (`<THIRD_PARTY_MODULES>`), relative imports second (`^\.`). The order
28
+ // matches the implicit behavior the rule shipped with before options
29
+ // landed, so omitting `importOrder` produces the same output.
30
+ var defaultImportOrder = []string{thirdPartyModulesPlaceholder, `^\.`}
31
+
32
+ // format/sort-imports orders the file's top-level import declarations into
33
+ // canonical groups and alphabetizes each group. Compatible-by-spirit with
34
+ // `@trivago/prettier-plugin-sort-imports`. Groups are user-configurable via
35
+ // the `importOrder` option; when omitted, the rule falls back to a
36
+ // two-group scheme:
37
+ //
38
+ // 1. External modules (specifier does not start with `.`).
39
+ // 2. Relative modules (specifier starts with `.`).
40
+ //
41
+ // Groups are separated by exactly one blank line (or omitted when
42
+ // `importOrderSeparation: false`). Within each group, declarations are
43
+ // sorted by their module-specifier text (ASCII order, or case-insensitive
44
+ // when `importOrderCaseInsensitive: true`). Named specifiers inside each
45
+ // declaration are alphabetized when `importOrderSortSpecifiers` is true
46
+ // (the default).
47
+ //
48
+ // Safety policy: if any byte between the contiguous imports is not
49
+ // whitespace, the rule bails. Comments anchored to specific imports would
50
+ // otherwise move with the wrong declaration, which is a strictly worse
51
+ // outcome than declining to sort.
52
+ //
53
+ // Side-effect imports (`import "foo"`), namespace imports
54
+ // (`import * as ns from`), default imports, and `import type` all
55
+ // participate in the same group based on their module specifier.
56
+ type formatSortImports struct{}
57
+
58
+ func (formatSortImports) Name() string { return "format/sort-imports" }
59
+ func (formatSortImports) IsFormat() bool { return true }
60
+ func (formatSortImports) Visits() []shimast.Kind {
61
+ return []shimast.Kind{shimast.KindSourceFile}
62
+ }
63
+
64
+ func (formatSortImports) Check(ctx *Context, node *shimast.Node) {
65
+ if ctx == nil || ctx.File == nil {
66
+ return
67
+ }
68
+ opts := loadSortImportsOptions(ctx)
69
+ src := ctx.File.Text()
70
+ statements := ctx.File.Statements
71
+ if statements == nil {
72
+ return
73
+ }
74
+ imports := collectLeadingImports(statements.Nodes)
75
+ // Block-level reorder runs only when the rule can do it safely:
76
+ // two or more contiguous imports with no comment trivia between
77
+ // them (comments anchor to specific imports and moving them would
78
+ // mis-attach the user's intent), AND no side-effect-only imports
79
+ // in the block. A side-effect import (`import "./polyfill"`) runs
80
+ // its module's top-level code for its observable effect; sorting it
81
+ // across a sibling import that depends on the polyfill being
82
+ // initialized first would silently change runtime behavior. The
83
+ // rule conservatively refuses to sort the entire block in that
84
+ // case.
85
+ if len(imports) >= 2 &&
86
+ leadingTriviaIsAllWhitespace(src, imports) &&
87
+ !containsSideEffectImport(imports) {
88
+ first := imports[0]
89
+ last := imports[len(imports)-1]
90
+ replaceStart := shimscanner.SkipTrivia(src, first.Pos())
91
+ replaceEnd := last.End()
92
+ original := src[replaceStart:replaceEnd]
93
+ rebuilt := buildSortedImportBlock(src, imports, opts)
94
+ if rebuilt != original {
95
+ ctx.ReportRangeFix(
96
+ replaceStart,
97
+ replaceEnd,
98
+ "Imports must be sorted into canonical groups.",
99
+ TextEdit{Pos: replaceStart, End: replaceEnd, Text: rebuilt},
100
+ )
101
+ return
102
+ }
103
+ }
104
+ if !opts.sortSpecifiers {
105
+ return
106
+ }
107
+ for _, decl := range collectLeadingImports(statements.Nodes) {
108
+ reportNamedSpecifierSort(ctx, decl, opts.caseInsensitive)
109
+ }
110
+ }
111
+
112
+ // resolvedSortImportsOptions is the normalized snapshot the rule uses
113
+ // during one Check call. All option defaults are applied here so the
114
+ // rest of the rule code does not branch on nil-ness.
115
+ type resolvedSortImportsOptions struct {
116
+ groups []sortImportsGroup
117
+ separation bool
118
+ sortSpecifiers bool
119
+ caseInsensitive bool
120
+ }
121
+
122
+ type sortImportsGroup struct {
123
+ raw string
124
+ pattern *regexp.Regexp
125
+ thirdParty bool
126
+ }
127
+
128
+ func loadSortImportsOptions(ctx *Context) resolvedSortImportsOptions {
129
+ var raw formatSortImportsOptions
130
+ _ = ctx.DecodeOptions(&raw)
131
+ order := raw.ImportOrder
132
+ if len(order) == 0 {
133
+ order = defaultImportOrder
134
+ }
135
+ groups := make([]sortImportsGroup, 0, len(order))
136
+ for _, pat := range order {
137
+ if pat == thirdPartyModulesPlaceholder {
138
+ groups = append(groups, sortImportsGroup{raw: pat, thirdParty: true})
139
+ continue
140
+ }
141
+ compiled, err := regexp.Compile(pat)
142
+ if err != nil {
143
+ // Bad regex: skip the group rather than failing the entire run.
144
+ // The diagnostic surface for misconfigured rules is intentionally
145
+ // limited; users see "no match" behavior instead of a crash.
146
+ continue
147
+ }
148
+ groups = append(groups, sortImportsGroup{raw: pat, pattern: compiled})
149
+ }
150
+ if len(groups) == 0 {
151
+ // Empty `importOrder` collapses to the third-party catchall so the
152
+ // rule still produces a deterministic ordering.
153
+ groups = []sortImportsGroup{{raw: thirdPartyModulesPlaceholder, thirdParty: true}}
154
+ }
155
+ // Ensure a third-party catchall exists; if the user omitted it,
156
+ // append one so unmatched specifiers don't land in a phantom group.
157
+ if !hasThirdPartyGroup(groups) {
158
+ groups = append(groups, sortImportsGroup{raw: thirdPartyModulesPlaceholder, thirdParty: true})
159
+ }
160
+ separation := true
161
+ if raw.ImportOrderSeparation != nil {
162
+ separation = *raw.ImportOrderSeparation
163
+ }
164
+ sortSpecifiers := true
165
+ if raw.ImportOrderSortSpecifiers != nil {
166
+ sortSpecifiers = *raw.ImportOrderSortSpecifiers
167
+ }
168
+ return resolvedSortImportsOptions{
169
+ groups: groups,
170
+ separation: separation,
171
+ sortSpecifiers: sortSpecifiers,
172
+ caseInsensitive: raw.ImportOrderCaseInsensitive,
173
+ }
174
+ }
175
+
176
+ func hasThirdPartyGroup(groups []sortImportsGroup) bool {
177
+ for _, g := range groups {
178
+ if g.thirdParty {
179
+ return true
180
+ }
181
+ }
182
+ return false
183
+ }
184
+
185
+ // matchGroup returns the index of the first group that claims `specifier`.
186
+ // Non-third-party groups are checked in order; the third-party catchall
187
+ // absorbs anything that did not match an earlier explicit pattern.
188
+ func matchGroup(groups []sortImportsGroup, specifier string) int {
189
+ thirdPartyIdx := -1
190
+ for i, g := range groups {
191
+ if g.thirdParty {
192
+ if thirdPartyIdx < 0 {
193
+ thirdPartyIdx = i
194
+ }
195
+ continue
196
+ }
197
+ if g.pattern != nil && g.pattern.MatchString(specifier) {
198
+ return i
199
+ }
200
+ }
201
+ if thirdPartyIdx >= 0 {
202
+ return thirdPartyIdx
203
+ }
204
+ return len(groups) // sentinel "no match"
205
+ }
206
+
207
+ // specifierListHasCommentTrivia returns true when the source between
208
+ // adjacent specifiers contains any byte that is not whitespace,
209
+ // comma, or whitespace-equivalent. A `/* x */` or `// x` inside the
210
+ // list anchors the rule into a no-op — sorting would re-emit the
211
+ // specifier texts joined with `", "` and silently drop the comment.
212
+ func specifierListHasCommentTrivia(src string, specifiers []*shimast.Node) bool {
213
+ for i := 1; i < len(specifiers); i++ {
214
+ prev := specifiers[i-1]
215
+ curr := specifiers[i]
216
+ if prev == nil || curr == nil {
217
+ continue
218
+ }
219
+ start := prev.End()
220
+ end := shimscanner.SkipTrivia(src, curr.Pos())
221
+ for j := start; j < end; j++ {
222
+ c := src[j]
223
+ if c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == ',' {
224
+ continue
225
+ }
226
+ return true
227
+ }
228
+ }
229
+ return false
230
+ }
231
+
232
+ // containsSideEffectImport reports whether any import in the contiguous
233
+ // block has no import clause (i.e. is a side-effect-only `import "x"`).
234
+ // These imports are evaluated for their top-level effect; their order
235
+ // relative to other imports may carry meaning the rule cannot reason
236
+ // about, so the safety policy is to refuse to sort.
237
+ func containsSideEffectImport(imports []*shimast.Node) bool {
238
+ for _, decl := range imports {
239
+ if decl == nil {
240
+ continue
241
+ }
242
+ imp := decl.AsImportDeclaration()
243
+ if imp == nil {
244
+ continue
245
+ }
246
+ if imp.ImportClause == nil {
247
+ return true
248
+ }
249
+ }
250
+ return false
251
+ }
252
+
253
+ // collectLeadingImports walks the file's statement list and returns the
254
+ // contiguous run of ImportDeclaration nodes at its head. Stops at the
255
+ // first non-import statement because moving an import across an
256
+ // expression statement could change runtime evaluation order.
257
+ func collectLeadingImports(stmts []*shimast.Node) []*shimast.Node {
258
+ out := make([]*shimast.Node, 0)
259
+ for _, stmt := range stmts {
260
+ if stmt == nil || stmt.Kind != shimast.KindImportDeclaration {
261
+ break
262
+ }
263
+ out = append(out, stmt)
264
+ }
265
+ return out
266
+ }
267
+
268
+ // leadingTriviaIsAllWhitespace returns true when the source between each
269
+ // adjacent import declaration contains no comment bytes. Comments anchor
270
+ // the rule into a no-op because moving imports could mis-attach them.
271
+ func leadingTriviaIsAllWhitespace(src string, imports []*shimast.Node) bool {
272
+ for i := 1; i < len(imports); i++ {
273
+ prevEnd := imports[i-1].End()
274
+ currStart := shimscanner.SkipTrivia(src, imports[i].Pos())
275
+ for j := prevEnd; j < currStart; j++ {
276
+ c := src[j]
277
+ if c != ' ' && c != '\t' && c != '\r' && c != '\n' {
278
+ return false
279
+ }
280
+ }
281
+ }
282
+ return true
283
+ }
284
+
285
+ // buildSortedImportBlock returns the canonical group/sort representation of
286
+ // the contiguous import declarations.
287
+ func buildSortedImportBlock(src string, imports []*shimast.Node, opts resolvedSortImportsOptions) string {
288
+ type entry struct {
289
+ group int
290
+ specifier string
291
+ sortKey string
292
+ text string
293
+ }
294
+ entries := make([]entry, 0, len(imports))
295
+ for _, decl := range imports {
296
+ spec := moduleSpecifierText(decl)
297
+ text := importStatementText(src, decl)
298
+ sortKey := spec
299
+ if opts.caseInsensitive {
300
+ sortKey = strings.ToLower(spec)
301
+ }
302
+ entries = append(entries, entry{
303
+ group: matchGroup(opts.groups, spec),
304
+ specifier: spec,
305
+ sortKey: sortKey,
306
+ text: text,
307
+ })
308
+ }
309
+ sort.SliceStable(entries, func(i, j int) bool {
310
+ if entries[i].group != entries[j].group {
311
+ return entries[i].group < entries[j].group
312
+ }
313
+ return entries[i].sortKey < entries[j].sortKey
314
+ })
315
+
316
+ var b strings.Builder
317
+ prevGroup := entries[0].group
318
+ for i, e := range entries {
319
+ if i > 0 {
320
+ if e.group != prevGroup && opts.separation {
321
+ b.WriteString("\n\n")
322
+ } else {
323
+ b.WriteString("\n")
324
+ }
325
+ }
326
+ b.WriteString(e.text)
327
+ prevGroup = e.group
328
+ }
329
+ return b.String()
330
+ }
331
+
332
+ // importStatementText returns the import declaration's source text,
333
+ // stripped of any leading whitespace/comment trivia.
334
+ func importStatementText(src string, decl *shimast.Node) string {
335
+ start := shimscanner.SkipTrivia(src, decl.Pos())
336
+ end := decl.End()
337
+ if start < 0 || start > end || end > len(src) {
338
+ return ""
339
+ }
340
+ return src[start:end]
341
+ }
342
+
343
+ // moduleSpecifierText returns the unquoted module specifier of an
344
+ // ImportDeclaration, or "" when the AST shape is unexpected.
345
+ func moduleSpecifierText(decl *shimast.Node) string {
346
+ if decl == nil {
347
+ return ""
348
+ }
349
+ imp := decl.AsImportDeclaration()
350
+ if imp == nil || imp.ModuleSpecifier == nil {
351
+ return ""
352
+ }
353
+ return stringLiteralText(imp.ModuleSpecifier)
354
+ }
355
+
356
+ // specifierEntry is the sortable pair captured for each named import
357
+ // specifier: the identifier used as the sort key and the literal source
358
+ // text that gets re-emitted in canonical order.
359
+ type specifierEntry struct {
360
+ key string
361
+ text string
362
+ }
363
+
364
+ // reportNamedSpecifierSort reports a fix when an import's `{ a, b }` list
365
+ // is out of alphabetical order. Type-only specifiers participate in the
366
+ // same sort key as value specifiers — prettier's plugin-sort-imports
367
+ // matches this behavior.
368
+ //
369
+ // Safety policy: comment trivia between specifiers anchors the rule
370
+ // into a no-op. The rule rejoins specifier text with `", "`, which
371
+ // would discard any `/* x */` comment carried inside the list. The
372
+ // block-level sort applies the same policy via
373
+ // `leadingTriviaIsAllWhitespace`; this is the per-specifier analog.
374
+ func reportNamedSpecifierSort(ctx *Context, decl *shimast.Node, caseInsensitive bool) {
375
+ imp := decl.AsImportDeclaration()
376
+ if imp == nil || imp.ImportClause == nil {
377
+ return
378
+ }
379
+ clause := imp.ImportClause.AsImportClause()
380
+ if clause == nil || clause.NamedBindings == nil {
381
+ return
382
+ }
383
+ if clause.NamedBindings.Kind != shimast.KindNamedImports {
384
+ return
385
+ }
386
+ named := clause.NamedBindings.AsNamedImports()
387
+ if named == nil || named.Elements == nil || len(named.Elements.Nodes) < 2 {
388
+ return
389
+ }
390
+ src := ctx.File.Text()
391
+ specifiers := named.Elements.Nodes
392
+ if specifierListHasCommentTrivia(src, specifiers) {
393
+ return
394
+ }
395
+
396
+ entries := make([]specifierEntry, 0, len(specifiers))
397
+ for _, spec := range specifiers {
398
+ if spec == nil {
399
+ continue
400
+ }
401
+ s := spec.AsImportSpecifier()
402
+ if s == nil {
403
+ return // unexpected shape; bail
404
+ }
405
+ name := identifierText(s.Name())
406
+ if name == "" {
407
+ return
408
+ }
409
+ start := shimscanner.SkipTrivia(src, spec.Pos())
410
+ end := spec.End()
411
+ key := name
412
+ if caseInsensitive {
413
+ key = strings.ToLower(name)
414
+ }
415
+ entries = append(entries, specifierEntry{
416
+ key: key,
417
+ text: src[start:end],
418
+ })
419
+ }
420
+ if len(entries) < 2 {
421
+ return
422
+ }
423
+ sorted := make([]specifierEntry, len(entries))
424
+ copy(sorted, entries)
425
+ sort.SliceStable(sorted, func(i, j int) bool {
426
+ return sorted[i].key < sorted[j].key
427
+ })
428
+ changed := false
429
+ for i := range entries {
430
+ if entries[i].key != sorted[i].key {
431
+ changed = true
432
+ break
433
+ }
434
+ }
435
+ if !changed {
436
+ return
437
+ }
438
+ firstStart := shimscanner.SkipTrivia(src, specifiers[0].Pos())
439
+ lastEnd := specifiers[len(specifiers)-1].End()
440
+ rebuilt := joinSortedSpecifiers(sorted)
441
+ if src[firstStart:lastEnd] == rebuilt {
442
+ return
443
+ }
444
+ ctx.ReportRangeFix(
445
+ firstStart,
446
+ lastEnd,
447
+ "Named import specifiers must be sorted alphabetically.",
448
+ TextEdit{Pos: firstStart, End: lastEnd, Text: rebuilt},
449
+ )
450
+ }
451
+
452
+ func joinSortedSpecifiers(entries []specifierEntry) string {
453
+ texts := make([]string, len(entries))
454
+ for i, e := range entries {
455
+ texts[i] = e.text
456
+ }
457
+ return strings.Join(texts, ", ")
458
+ }
459
+
460
+ func init() {
461
+ Register(formatSortImports{})
462
+ }