github.com/v2fly/tools@v0.100.0/internal/imports/sortimports.go (about) 1 // Copyright 2013 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Hacked up copy of go/ast/import.go 6 7 package imports 8 9 import ( 10 "go/ast" 11 "go/token" 12 "sort" 13 "strconv" 14 ) 15 16 // sortImports sorts runs of consecutive import lines in import blocks in f. 17 // It also removes duplicate imports when it is possible to do so without data loss. 18 func sortImports(localPrefix string, fset *token.FileSet, f *ast.File) { 19 for i, d := range f.Decls { 20 d, ok := d.(*ast.GenDecl) 21 if !ok || d.Tok != token.IMPORT { 22 // Not an import declaration, so we're done. 23 // Imports are always first. 24 break 25 } 26 27 if len(d.Specs) == 0 { 28 // Empty import block, remove it. 29 f.Decls = append(f.Decls[:i], f.Decls[i+1:]...) 30 } 31 32 if !d.Lparen.IsValid() { 33 // Not a block: sorted by default. 34 continue 35 } 36 37 // Identify and sort runs of specs on successive lines. 38 i := 0 39 specs := d.Specs[:0] 40 for j, s := range d.Specs { 41 if j > i && fset.Position(s.Pos()).Line > 1+fset.Position(d.Specs[j-1].End()).Line { 42 // j begins a new run. End this one. 43 specs = append(specs, sortSpecs(localPrefix, fset, f, d.Specs[i:j])...) 44 i = j 45 } 46 } 47 specs = append(specs, sortSpecs(localPrefix, fset, f, d.Specs[i:])...) 48 d.Specs = specs 49 50 // Deduping can leave a blank line before the rparen; clean that up. 51 if len(d.Specs) > 0 { 52 lastSpec := d.Specs[len(d.Specs)-1] 53 lastLine := fset.Position(lastSpec.Pos()).Line 54 if rParenLine := fset.Position(d.Rparen).Line; rParenLine > lastLine+1 { 55 fset.File(d.Rparen).MergeLine(rParenLine - 1) 56 } 57 } 58 } 59 } 60 61 // mergeImports merges all the import declarations into the first one. 62 // Taken from github.com/v2fly/tools/ast/astutil. 63 func mergeImports(fset *token.FileSet, f *ast.File) { 64 if len(f.Decls) <= 1 { 65 return 66 } 67 68 // Merge all the import declarations into the first one. 69 var first *ast.GenDecl 70 for i := 0; i < len(f.Decls); i++ { 71 decl := f.Decls[i] 72 gen, ok := decl.(*ast.GenDecl) 73 if !ok || gen.Tok != token.IMPORT || declImports(gen, "C") { 74 continue 75 } 76 if first == nil { 77 first = gen 78 continue // Don't touch the first one. 79 } 80 // We now know there is more than one package in this import 81 // declaration. Ensure that it ends up parenthesized. 82 first.Lparen = first.Pos() 83 // Move the imports of the other import declaration to the first one. 84 for _, spec := range gen.Specs { 85 spec.(*ast.ImportSpec).Path.ValuePos = first.Pos() 86 first.Specs = append(first.Specs, spec) 87 } 88 f.Decls = append(f.Decls[:i], f.Decls[i+1:]...) 89 i-- 90 } 91 } 92 93 // declImports reports whether gen contains an import of path. 94 // Taken from github.com/v2fly/tools/ast/astutil. 95 func declImports(gen *ast.GenDecl, path string) bool { 96 if gen.Tok != token.IMPORT { 97 return false 98 } 99 for _, spec := range gen.Specs { 100 impspec := spec.(*ast.ImportSpec) 101 if importPath(impspec) == path { 102 return true 103 } 104 } 105 return false 106 } 107 108 func importPath(s ast.Spec) string { 109 t, err := strconv.Unquote(s.(*ast.ImportSpec).Path.Value) 110 if err == nil { 111 return t 112 } 113 return "" 114 } 115 116 func importName(s ast.Spec) string { 117 n := s.(*ast.ImportSpec).Name 118 if n == nil { 119 return "" 120 } 121 return n.Name 122 } 123 124 func importComment(s ast.Spec) string { 125 c := s.(*ast.ImportSpec).Comment 126 if c == nil { 127 return "" 128 } 129 return c.Text() 130 } 131 132 // collapse indicates whether prev may be removed, leaving only next. 133 func collapse(prev, next ast.Spec) bool { 134 if importPath(next) != importPath(prev) || importName(next) != importName(prev) { 135 return false 136 } 137 return prev.(*ast.ImportSpec).Comment == nil 138 } 139 140 type posSpan struct { 141 Start token.Pos 142 End token.Pos 143 } 144 145 func sortSpecs(localPrefix string, fset *token.FileSet, f *ast.File, specs []ast.Spec) []ast.Spec { 146 // Can't short-circuit here even if specs are already sorted, 147 // since they might yet need deduplication. 148 // A lone import, however, may be safely ignored. 149 if len(specs) <= 1 { 150 return specs 151 } 152 153 // Record positions for specs. 154 pos := make([]posSpan, len(specs)) 155 for i, s := range specs { 156 pos[i] = posSpan{s.Pos(), s.End()} 157 } 158 159 // Identify comments in this range. 160 // Any comment from pos[0].Start to the final line counts. 161 lastLine := fset.Position(pos[len(pos)-1].End).Line 162 cstart := len(f.Comments) 163 cend := len(f.Comments) 164 for i, g := range f.Comments { 165 if g.Pos() < pos[0].Start { 166 continue 167 } 168 if i < cstart { 169 cstart = i 170 } 171 if fset.Position(g.End()).Line > lastLine { 172 cend = i 173 break 174 } 175 } 176 comments := f.Comments[cstart:cend] 177 178 // Assign each comment to the import spec preceding it. 179 importComment := map[*ast.ImportSpec][]*ast.CommentGroup{} 180 specIndex := 0 181 for _, g := range comments { 182 for specIndex+1 < len(specs) && pos[specIndex+1].Start <= g.Pos() { 183 specIndex++ 184 } 185 s := specs[specIndex].(*ast.ImportSpec) 186 importComment[s] = append(importComment[s], g) 187 } 188 189 // Sort the import specs by import path. 190 // Remove duplicates, when possible without data loss. 191 // Reassign the import paths to have the same position sequence. 192 // Reassign each comment to abut the end of its spec. 193 // Sort the comments by new position. 194 sort.Sort(byImportSpec{localPrefix, specs}) 195 196 // Dedup. Thanks to our sorting, we can just consider 197 // adjacent pairs of imports. 198 deduped := specs[:0] 199 for i, s := range specs { 200 if i == len(specs)-1 || !collapse(s, specs[i+1]) { 201 deduped = append(deduped, s) 202 } else { 203 p := s.Pos() 204 fset.File(p).MergeLine(fset.Position(p).Line) 205 } 206 } 207 specs = deduped 208 209 // Fix up comment positions 210 for i, s := range specs { 211 s := s.(*ast.ImportSpec) 212 if s.Name != nil { 213 s.Name.NamePos = pos[i].Start 214 } 215 s.Path.ValuePos = pos[i].Start 216 s.EndPos = pos[i].End 217 nextSpecPos := pos[i].End 218 219 for _, g := range importComment[s] { 220 for _, c := range g.List { 221 c.Slash = pos[i].End 222 nextSpecPos = c.End() 223 } 224 } 225 if i < len(specs)-1 { 226 pos[i+1].Start = nextSpecPos 227 pos[i+1].End = nextSpecPos 228 } 229 } 230 231 sort.Sort(byCommentPos(comments)) 232 233 // Fixup comments can insert blank lines, because import specs are on different lines. 234 // We remove those blank lines here by merging import spec to the first import spec line. 235 firstSpecLine := fset.Position(specs[0].Pos()).Line 236 for _, s := range specs[1:] { 237 p := s.Pos() 238 line := fset.File(p).Line(p) 239 for previousLine := line - 1; previousLine >= firstSpecLine; { 240 fset.File(p).MergeLine(previousLine) 241 previousLine-- 242 } 243 } 244 return specs 245 } 246 247 type byImportSpec struct { 248 localPrefix string 249 specs []ast.Spec // slice of *ast.ImportSpec 250 } 251 252 func (x byImportSpec) Len() int { return len(x.specs) } 253 func (x byImportSpec) Swap(i, j int) { x.specs[i], x.specs[j] = x.specs[j], x.specs[i] } 254 func (x byImportSpec) Less(i, j int) bool { 255 ipath := importPath(x.specs[i]) 256 jpath := importPath(x.specs[j]) 257 258 igroup := importGroup(x.localPrefix, ipath) 259 jgroup := importGroup(x.localPrefix, jpath) 260 if igroup != jgroup { 261 return igroup < jgroup 262 } 263 264 if ipath != jpath { 265 return ipath < jpath 266 } 267 iname := importName(x.specs[i]) 268 jname := importName(x.specs[j]) 269 270 if iname != jname { 271 return iname < jname 272 } 273 return importComment(x.specs[i]) < importComment(x.specs[j]) 274 } 275 276 type byCommentPos []*ast.CommentGroup 277 278 func (x byCommentPos) Len() int { return len(x) } 279 func (x byCommentPos) Swap(i, j int) { x[i], x[j] = x[j], x[i] } 280 func (x byCommentPos) Less(i, j int) bool { return x[i].Pos() < x[j].Pos() }