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

     1  package sa4010
     2  
     3  import (
     4  	"github.com/amarpal/go-tools/analysis/lint"
     5  	"github.com/amarpal/go-tools/analysis/report"
     6  	"github.com/amarpal/go-tools/go/ir"
     7  	"github.com/amarpal/go-tools/internal/passes/buildir"
     8  
     9  	"golang.org/x/tools/go/analysis"
    10  )
    11  
    12  var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
    13  	Analyzer: &analysis.Analyzer{
    14  		Name:     "SA4010",
    15  		Run:      run,
    16  		Requires: []*analysis.Analyzer{buildir.Analyzer},
    17  	},
    18  	Doc: &lint.Documentation{
    19  		Title:    `The result of \'append\' will never be observed anywhere`,
    20  		Since:    "2017.1",
    21  		Severity: lint.SeverityWarning,
    22  		MergeIf:  lint.MergeIfAll,
    23  	},
    24  })
    25  
    26  var Analyzer = SCAnalyzer.Analyzer
    27  
    28  func run(pass *analysis.Pass) (interface{}, error) {
    29  	isAppend := func(ins ir.Value) bool {
    30  		call, ok := ins.(*ir.Call)
    31  		if !ok {
    32  			return false
    33  		}
    34  		if call.Call.IsInvoke() {
    35  			return false
    36  		}
    37  		if builtin, ok := call.Call.Value.(*ir.Builtin); !ok || builtin.Name() != "append" {
    38  			return false
    39  		}
    40  		return true
    41  	}
    42  
    43  	// We have to be careful about aliasing.
    44  	// Multiple slices may refer to the same backing array,
    45  	// making appends observable even when we don't see the result of append be used anywhere.
    46  	//
    47  	// We will have to restrict ourselves to slices that have been allocated within the function,
    48  	// haven't been sliced,
    49  	// and haven't been passed anywhere that could retain them (such as function calls or memory stores).
    50  	//
    51  	// We check whether an append should be flagged in two steps.
    52  	//
    53  	// In the first step, we look at the data flow graph, starting in reverse from the argument to append, till we reach the root.
    54  	// This graph must only consist of the following instructions:
    55  	//
    56  	// - phi
    57  	// - sigma
    58  	// - slice
    59  	// - const nil
    60  	// - MakeSlice
    61  	// - Alloc
    62  	// - calls to append
    63  	//
    64  	// If this step succeeds, we look at all referrers of the values found in the first step, recursively.
    65  	// These referrers must either be in the set of values found in the first step,
    66  	// be DebugRefs,
    67  	// or fulfill the same type requirements as step 1, with the exception of appends, which are forbidden.
    68  	//
    69  	// If both steps succeed then we know that the backing array hasn't been aliased in an observable manner.
    70  	//
    71  	// We could relax these restrictions by making use of additional information:
    72  	// - if we passed the slice to a function that doesn't retain the slice then we can still flag it
    73  	// - if a slice has been sliced but is dead afterwards, we can flag appends to the new slice
    74  
    75  	// OPT(dh): We could cache the results of both validate functions.
    76  	// However, we only use these functions on values that we otherwise want to flag, which are very few.
    77  	// Not caching values hasn't increased the runtimes for the standard library nor k8s.
    78  	var validateArgument func(v ir.Value, seen map[ir.Value]struct{}) bool
    79  	validateArgument = func(v ir.Value, seen map[ir.Value]struct{}) bool {
    80  		if _, ok := seen[v]; ok {
    81  			// break cycle
    82  			return true
    83  		}
    84  		seen[v] = struct{}{}
    85  		switch v := v.(type) {
    86  		case *ir.Phi:
    87  			for _, edge := range v.Edges {
    88  				if !validateArgument(edge, seen) {
    89  					return false
    90  				}
    91  			}
    92  			return true
    93  		case *ir.Sigma:
    94  			return validateArgument(v.X, seen)
    95  		case *ir.Slice:
    96  			return validateArgument(v.X, seen)
    97  		case *ir.Const:
    98  			return true
    99  		case *ir.MakeSlice:
   100  			return true
   101  		case *ir.Alloc:
   102  			return true
   103  		case *ir.Call:
   104  			if isAppend(v) {
   105  				return validateArgument(v.Call.Args[0], seen)
   106  			}
   107  			return false
   108  		default:
   109  			return false
   110  		}
   111  	}
   112  
   113  	var validateReferrers func(v ir.Value, seen map[ir.Instruction]struct{}) bool
   114  	validateReferrers = func(v ir.Value, seen map[ir.Instruction]struct{}) bool {
   115  		for _, ref := range *v.Referrers() {
   116  			if _, ok := seen[ref]; ok {
   117  				continue
   118  			}
   119  
   120  			seen[ref] = struct{}{}
   121  			switch ref.(type) {
   122  			case *ir.Phi:
   123  			case *ir.Sigma:
   124  			case *ir.Slice:
   125  			case *ir.Const:
   126  			case *ir.MakeSlice:
   127  			case *ir.Alloc:
   128  			case *ir.DebugRef:
   129  			default:
   130  				return false
   131  			}
   132  
   133  			if ref, ok := ref.(ir.Value); ok {
   134  				if !validateReferrers(ref, seen) {
   135  					return false
   136  				}
   137  			}
   138  		}
   139  		return true
   140  	}
   141  
   142  	for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
   143  		for _, block := range fn.Blocks {
   144  			for _, ins := range block.Instrs {
   145  				val, ok := ins.(ir.Value)
   146  				if !ok || !isAppend(val) {
   147  					continue
   148  				}
   149  
   150  				isUsed := false
   151  				visited := map[ir.Instruction]bool{}
   152  				var walkRefs func(refs []ir.Instruction)
   153  				walkRefs = func(refs []ir.Instruction) {
   154  				loop:
   155  					for _, ref := range refs {
   156  						if visited[ref] {
   157  							continue
   158  						}
   159  						visited[ref] = true
   160  						if _, ok := ref.(*ir.DebugRef); ok {
   161  							continue
   162  						}
   163  						switch ref := ref.(type) {
   164  						case *ir.Phi:
   165  							walkRefs(*ref.Referrers())
   166  						case *ir.Sigma:
   167  							walkRefs(*ref.Referrers())
   168  						case ir.Value:
   169  							if !isAppend(ref) {
   170  								isUsed = true
   171  							} else {
   172  								walkRefs(*ref.Referrers())
   173  							}
   174  						case ir.Instruction:
   175  							isUsed = true
   176  							break loop
   177  						}
   178  					}
   179  				}
   180  
   181  				refs := val.Referrers()
   182  				if refs == nil {
   183  					continue
   184  				}
   185  				walkRefs(*refs)
   186  
   187  				if isUsed {
   188  					continue
   189  				}
   190  
   191  				seen := map[ir.Value]struct{}{}
   192  				if !validateArgument(ins.(*ir.Call).Call.Args[0], seen) {
   193  					continue
   194  				}
   195  
   196  				seen2 := map[ir.Instruction]struct{}{}
   197  				for k := range seen {
   198  					// the only values we allow are also instructions, so this type assertion cannot fail
   199  					seen2[k.(ir.Instruction)] = struct{}{}
   200  				}
   201  				seen2[ins] = struct{}{}
   202  				failed := false
   203  				for v := range seen {
   204  					if !validateReferrers(v, seen2) {
   205  						failed = true
   206  						break
   207  					}
   208  				}
   209  				if !failed {
   210  					report.Report(pass, ins, "this result of append is never used, except maybe in other appends")
   211  				}
   212  			}
   213  		}
   214  	}
   215  	return nil, nil
   216  }