report.go raw

   1  package report
   2  
   3  import (
   4  	"bytes"
   5  	"fmt"
   6  	"go/ast"
   7  	"go/format"
   8  	"go/token"
   9  	"go/version"
  10  	"path/filepath"
  11  	"strconv"
  12  	"strings"
  13  
  14  	"honnef.co/go/tools/analysis/code"
  15  	"honnef.co/go/tools/analysis/facts/generated"
  16  	"honnef.co/go/tools/go/ast/astutil"
  17  
  18  	"golang.org/x/tools/go/analysis"
  19  )
  20  
  21  type Options struct {
  22  	ShortRange             bool
  23  	FilterGenerated        bool
  24  	Fixes                  []analysis.SuggestedFix
  25  	Related                []analysis.RelatedInformation
  26  	MinimumLanguageVersion string
  27  	MaximumLanguageVersion string
  28  	MinimumStdlibVersion   string
  29  	MaximumStdlibVersion   string
  30  }
  31  
  32  type Option func(*Options)
  33  
  34  func ShortRange() Option {
  35  	return func(opts *Options) {
  36  		opts.ShortRange = true
  37  	}
  38  }
  39  
  40  func FilterGenerated() Option {
  41  	return func(opts *Options) {
  42  		opts.FilterGenerated = true
  43  	}
  44  }
  45  
  46  func Fixes(fixes ...analysis.SuggestedFix) Option {
  47  	return func(opts *Options) {
  48  		opts.Fixes = append(opts.Fixes, fixes...)
  49  	}
  50  }
  51  
  52  func Related(node Positioner, message string) Option {
  53  	return func(opts *Options) {
  54  		pos, end, ok := getRange(node, opts.ShortRange)
  55  		if !ok {
  56  			return
  57  		}
  58  		r := analysis.RelatedInformation{
  59  			Pos:     pos,
  60  			End:     end,
  61  			Message: message,
  62  		}
  63  		opts.Related = append(opts.Related, r)
  64  	}
  65  }
  66  
  67  func MinimumLanguageVersion(vers string) Option {
  68  	return func(opts *Options) { opts.MinimumLanguageVersion = vers }
  69  }
  70  func MaximumLanguageVersion(vers string) Option {
  71  	return func(opts *Options) { opts.MinimumLanguageVersion = vers }
  72  }
  73  func MinimumStdlibVersion(vers string) Option {
  74  	return func(opts *Options) { opts.MinimumStdlibVersion = vers }
  75  }
  76  func MaximumStdlibVersion(vers string) Option {
  77  	return func(opts *Options) { opts.MaximumStdlibVersion = vers }
  78  }
  79  
  80  type Positioner interface {
  81  	Pos() token.Pos
  82  }
  83  
  84  type fullPositioner interface {
  85  	Pos() token.Pos
  86  	End() token.Pos
  87  }
  88  
  89  type sourcer interface {
  90  	Source() ast.Node
  91  }
  92  
  93  // shortRange returns the position and end of the main component of an
  94  // AST node. For nodes that have no body, the short range is identical
  95  // to the node's Pos and End. For nodes that do have a body, the short
  96  // range excludes the body.
  97  func shortRange(node ast.Node) (pos, end token.Pos) {
  98  	switch node := node.(type) {
  99  	case *ast.File:
 100  		return node.Pos(), node.Name.End()
 101  	case *ast.CaseClause:
 102  		return node.Pos(), node.Colon + 1
 103  	case *ast.CommClause:
 104  		return node.Pos(), node.Colon + 1
 105  	case *ast.DeferStmt:
 106  		return node.Pos(), node.Defer + token.Pos(len("defer"))
 107  	case *ast.ExprStmt:
 108  		return shortRange(node.X)
 109  	case *ast.ForStmt:
 110  		if node.Post != nil {
 111  			return node.For, node.Post.End()
 112  		} else if node.Cond != nil {
 113  			return node.For, node.Cond.End()
 114  		} else if node.Init != nil {
 115  			// +1 to catch the semicolon, for gofmt'ed code
 116  			return node.Pos(), node.Init.End() + 1
 117  		} else {
 118  			return node.Pos(), node.For + token.Pos(len("for"))
 119  		}
 120  	case *ast.FuncDecl:
 121  		return node.Pos(), node.Type.End()
 122  	case *ast.FuncLit:
 123  		return node.Pos(), node.Type.End()
 124  	case *ast.GoStmt:
 125  		if _, ok := astutil.Unparen(node.Call.Fun).(*ast.FuncLit); ok {
 126  			return node.Pos(), node.Go + token.Pos(len("go"))
 127  		} else {
 128  			return node.Pos(), node.End()
 129  		}
 130  	case *ast.IfStmt:
 131  		return node.Pos(), node.Cond.End()
 132  	case *ast.RangeStmt:
 133  		return node.Pos(), node.X.End()
 134  	case *ast.SelectStmt:
 135  		return node.Pos(), node.Pos() + token.Pos(len("select"))
 136  	case *ast.SwitchStmt:
 137  		if node.Tag != nil {
 138  			return node.Pos(), node.Tag.End()
 139  		} else if node.Init != nil {
 140  			// +1 to catch the semicolon, for gofmt'ed code
 141  			return node.Pos(), node.Init.End() + 1
 142  		} else {
 143  			return node.Pos(), node.Pos() + token.Pos(len("switch"))
 144  		}
 145  	case *ast.TypeSwitchStmt:
 146  		return node.Pos(), node.Assign.End()
 147  	default:
 148  		return node.Pos(), node.End()
 149  	}
 150  }
 151  
 152  func HasRange(node Positioner) bool {
 153  	// we don't know if getRange will be called with shortRange set to
 154  	// true, so make sure that both work.
 155  	_, _, ok := getRange(node, false)
 156  	if !ok {
 157  		return false
 158  	}
 159  	_, _, ok = getRange(node, true)
 160  	return ok
 161  }
 162  
 163  func getRange(node Positioner, short bool) (pos, end token.Pos, ok bool) {
 164  	switch n := node.(type) {
 165  	case sourcer:
 166  		s := n.Source()
 167  		if s == nil {
 168  			return 0, 0, false
 169  		}
 170  		if short {
 171  			p, e := shortRange(s)
 172  			return p, e, true
 173  		}
 174  		return s.Pos(), s.End(), true
 175  	case fullPositioner:
 176  		if short {
 177  			p, e := shortRange(n)
 178  			return p, e, true
 179  		}
 180  		return n.Pos(), n.End(), true
 181  	default:
 182  		return n.Pos(), token.NoPos, true
 183  	}
 184  }
 185  
 186  func Report(pass *analysis.Pass, node Positioner, message string, opts ...Option) {
 187  	cfg := &Options{}
 188  	for _, opt := range opts {
 189  		opt(cfg)
 190  	}
 191  
 192  	langVersion := code.LanguageVersion(pass, node)
 193  	stdlibVersion := code.StdlibVersion(pass, node)
 194  	if n := cfg.MaximumLanguageVersion; n != "" && version.Compare(n, langVersion) == -1 {
 195  		return
 196  	}
 197  	if n := cfg.MaximumStdlibVersion; n != "" && version.Compare(n, stdlibVersion) == -1 {
 198  		return
 199  	}
 200  	if n := cfg.MinimumLanguageVersion; n != "" && version.Compare(n, langVersion) == 1 {
 201  		return
 202  	}
 203  	if n := cfg.MinimumStdlibVersion; n != "" && version.Compare(n, stdlibVersion) == 1 {
 204  		return
 205  	}
 206  
 207  	file := DisplayPosition(pass.Fset, node.Pos()).Filename
 208  	if cfg.FilterGenerated {
 209  		m := pass.ResultOf[generated.Analyzer].(map[string]generated.Generator)
 210  		if _, ok := m[file]; ok {
 211  			return
 212  		}
 213  	}
 214  
 215  	pos, end, ok := getRange(node, cfg.ShortRange)
 216  	if !ok {
 217  		panic(fmt.Sprintf("no valid position for reporting node %v", node))
 218  	}
 219  	d := analysis.Diagnostic{
 220  		Pos:            pos,
 221  		End:            end,
 222  		Message:        message,
 223  		SuggestedFixes: cfg.Fixes,
 224  		Related:        cfg.Related,
 225  	}
 226  	pass.Report(d)
 227  }
 228  
 229  func Render(pass *analysis.Pass, x interface{}) string {
 230  	var buf bytes.Buffer
 231  	if err := format.Node(&buf, pass.Fset, x); err != nil {
 232  		panic(err)
 233  	}
 234  	return buf.String()
 235  }
 236  
 237  func RenderArgs(pass *analysis.Pass, args []ast.Expr) string {
 238  	var ss []string
 239  	for _, arg := range args {
 240  		ss = append(ss, Render(pass, arg))
 241  	}
 242  	return strings.Join(ss, ", ")
 243  }
 244  
 245  func DisplayPosition(fset *token.FileSet, p token.Pos) token.Position {
 246  	if p == token.NoPos {
 247  		return token.Position{}
 248  	}
 249  
 250  	// Only use the adjusted position if it points to another Go file.
 251  	// This means we'll point to the original file for cgo files, but
 252  	// we won't point to a YACC grammar file.
 253  	pos := fset.PositionFor(p, false)
 254  	adjPos := fset.PositionFor(p, true)
 255  
 256  	if filepath.Ext(adjPos.Filename) == ".go" {
 257  		return adjPos
 258  	}
 259  
 260  	return pos
 261  }
 262  
 263  func Ordinal(n int) string {
 264  	suffix := "th"
 265  	if n < 10 || n > 20 {
 266  		switch n % 10 {
 267  		case 0:
 268  			suffix = "th"
 269  		case 1:
 270  			suffix = "st"
 271  		case 2:
 272  			suffix = "nd"
 273  		case 3:
 274  			suffix = "rd"
 275  		default:
 276  			suffix = "th"
 277  		}
 278  	}
 279  
 280  	return strconv.Itoa(n) + suffix
 281  }
 282