src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

report.go (6621B)


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