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  }