github.com/goplus/gop@v1.2.6/ast/import.go (about) 1 /* 2 * Copyright (c) 2021 The GoPlus Authors (goplus.org). All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package ast 18 19 import ( 20 "sort" 21 "strconv" 22 23 "github.com/goplus/gop/token" 24 ) 25 26 // SortImports sorts runs of consecutive import lines in import blocks in f. 27 // It also removes duplicate imports when it is possible to do so without data loss. 28 func SortImports(fset *token.FileSet, f *File) { 29 for _, d := range f.Decls { 30 d, ok := d.(*GenDecl) 31 if !ok || d.Tok != token.IMPORT { 32 // Not an import declaration, so we're done. 33 // Imports are always first. 34 break 35 } 36 37 if !d.Lparen.IsValid() { 38 // Not a block: sorted by default. 39 continue 40 } 41 42 // Identify and sort runs of specs on successive lines. 43 i := 0 44 specs := d.Specs[:0] 45 for j, s := range d.Specs { 46 if j > i && lineAt(fset, s.Pos()) > 1+lineAt(fset, d.Specs[j-1].End()) { 47 // j begins a new run. End this one. 48 specs = append(specs, sortSpecs(fset, f, d.Specs[i:j])...) 49 i = j 50 } 51 } 52 specs = append(specs, sortSpecs(fset, f, d.Specs[i:])...) 53 d.Specs = specs 54 55 // Deduping can leave a blank line before the rparen; clean that up. 56 if len(d.Specs) > 0 { 57 lastSpec := d.Specs[len(d.Specs)-1] 58 lastLine := lineAt(fset, lastSpec.Pos()) 59 rParenLine := lineAt(fset, d.Rparen) 60 for rParenLine > lastLine+1 { 61 rParenLine-- 62 fset.File(d.Rparen).MergeLine(rParenLine) 63 } 64 } 65 } 66 } 67 68 func lineAt(fset *token.FileSet, pos token.Pos) int { 69 return fset.PositionFor(pos, false).Line 70 } 71 72 func importPath(s Spec) string { 73 t, err := strconv.Unquote(s.(*ImportSpec).Path.Value) 74 if err == nil { 75 return t 76 } 77 return "" 78 } 79 80 func importName(s Spec) string { 81 n := s.(*ImportSpec).Name 82 if n == nil { 83 return "" 84 } 85 return n.Name 86 } 87 88 func importComment(s Spec) string { 89 c := s.(*ImportSpec).Comment 90 if c == nil { 91 return "" 92 } 93 return c.Text() 94 } 95 96 // collapse indicates whether prev may be removed, leaving only next. 97 func collapse(prev, next Spec) bool { 98 if importPath(next) != importPath(prev) || importName(next) != importName(prev) { 99 return false 100 } 101 return prev.(*ImportSpec).Comment == nil 102 } 103 104 type posSpan struct { 105 Start token.Pos 106 End token.Pos 107 } 108 109 type cgPos struct { 110 left bool // true if comment is to the left of the spec, false otherwise. 111 cg *CommentGroup 112 } 113 114 func sortSpecs(fset *token.FileSet, f *File, specs []Spec) []Spec { 115 // Can't short-circuit here even if specs are already sorted, 116 // since they might yet need deduplication. 117 // A lone import, however, may be safely ignored. 118 if len(specs) <= 1 { 119 return specs 120 } 121 122 // Record positions for specs. 123 pos := make([]posSpan, len(specs)) 124 for i, s := range specs { 125 pos[i] = posSpan{s.Pos(), s.End()} 126 } 127 128 // Identify comments in this range. 129 begSpecs := pos[0].Start 130 endSpecs := pos[len(pos)-1].End 131 beg := fset.File(begSpecs).LineStart(lineAt(fset, begSpecs)) 132 endLine := lineAt(fset, endSpecs) 133 endFile := fset.File(endSpecs) 134 var end token.Pos 135 if endLine == endFile.LineCount() { 136 end = endSpecs 137 } else { 138 end = endFile.LineStart(endLine + 1) // beginning of next line 139 } 140 first := len(f.Comments) 141 last := -1 142 for i, g := range f.Comments { 143 if g.End() >= end { 144 break 145 } 146 // g.End() < end 147 if beg <= g.Pos() { 148 // comment is within the range [beg, end[ of import declarations 149 if i < first { 150 first = i 151 } 152 if i > last { 153 last = i 154 } 155 } 156 } 157 158 var comments []*CommentGroup 159 if last >= 0 { 160 comments = f.Comments[first : last+1] 161 } 162 163 // Assign each comment to the import spec on the same line. 164 importComments := map[*ImportSpec][]cgPos{} 165 specIndex := 0 166 for _, g := range comments { 167 for specIndex+1 < len(specs) && pos[specIndex+1].Start <= g.Pos() { 168 specIndex++ 169 } 170 var left bool 171 // A block comment can appear before the first import spec. 172 if specIndex == 0 && pos[specIndex].Start > g.Pos() { 173 left = true 174 } else if specIndex+1 < len(specs) && // Or it can appear on the left of an import spec. 175 lineAt(fset, pos[specIndex].Start)+1 == lineAt(fset, g.Pos()) { 176 specIndex++ 177 left = true 178 } 179 s := specs[specIndex].(*ImportSpec) 180 importComments[s] = append(importComments[s], cgPos{left: left, cg: g}) 181 } 182 183 // Sort the import specs by import path. 184 // Remove duplicates, when possible without data loss. 185 // Reassign the import paths to have the same position sequence. 186 // Reassign each comment to the spec on the same line. 187 // Sort the comments by new position. 188 sort.Slice(specs, func(i, j int) bool { 189 ipath := importPath(specs[i]) 190 jpath := importPath(specs[j]) 191 if ipath != jpath { 192 return ipath < jpath 193 } 194 iname := importName(specs[i]) 195 jname := importName(specs[j]) 196 if iname != jname { 197 return iname < jname 198 } 199 return importComment(specs[i]) < importComment(specs[j]) 200 }) 201 202 // Dedup. Thanks to our sorting, we can just consider 203 // adjacent pairs of imports. 204 deduped := specs[:0] 205 for i, s := range specs { 206 if i == len(specs)-1 || !collapse(s, specs[i+1]) { 207 deduped = append(deduped, s) 208 } else { 209 p := s.Pos() 210 fset.File(p).MergeLine(lineAt(fset, p)) 211 } 212 } 213 specs = deduped 214 215 // Fix up comment positions 216 for i, s := range specs { 217 s := s.(*ImportSpec) 218 if s.Name != nil { 219 s.Name.NamePos = pos[i].Start 220 } 221 s.Path.ValuePos = pos[i].Start 222 s.EndPos = pos[i].End 223 for _, g := range importComments[s] { 224 for _, c := range g.cg.List { 225 if g.left { 226 c.Slash = pos[i].Start - 1 227 } else { 228 // An import spec can have both block comment and a line comment 229 // to its right. In that case, both of them will have the same pos. 230 // But while formatting the AST, the line comment gets moved to 231 // after the block comment. 232 c.Slash = pos[i].End 233 } 234 } 235 } 236 } 237 238 sort.Slice(comments, func(i, j int) bool { 239 return comments[i].Pos() < comments[j].Pos() 240 }) 241 242 return specs 243 }