@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.
- 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 +62 -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,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
|
+
}
|