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