github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/analysis/report/report.go (about)

     1  package report
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"go/ast"
     7  	"go/format"
     8  	"go/token"
     9  	"path/filepath"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"github.com/amarpal/go-tools/analysis/code"
    14  	"github.com/amarpal/go-tools/analysis/facts/generated"
    15  	"github.com/amarpal/go-tools/go/ast/astutil"
    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 int
    26  	MaximumLanguageVersion int
    27  	MinimumStdlibVersion   int
    28  	MaximumStdlibVersion   int
    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 int) Option {
    67  	return func(opts *Options) { opts.MinimumLanguageVersion = vers }
    68  }
    69  func MaximumLanguageVersion(vers int) Option {
    70  	return func(opts *Options) { opts.MinimumLanguageVersion = vers }
    71  }
    72  func MinimumStdlibVersion(vers int) Option {
    73  	return func(opts *Options) { opts.MinimumStdlibVersion = vers }
    74  }
    75  func MaximumStdlibVersion(vers int) 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 := astutil.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 != 0 && n < langVersion {
   194  		return
   195  	}
   196  	if n := cfg.MaximumStdlibVersion; n != 0 && n < stdlibVersion {
   197  		return
   198  	}
   199  	if n := cfg.MinimumLanguageVersion; n != 0 && n > langVersion {
   200  		return
   201  	}
   202  	if n := cfg.MinimumStdlibVersion; n != 0 && n > stdlibVersion {
   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 interface{}) 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  }