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

     1  package sa4032
     2  
     3  import (
     4  	"fmt"
     5  	"go/ast"
     6  	"go/build/constraint"
     7  	"go/constant"
     8  
     9  	"github.com/amarpal/go-tools/analysis/code"
    10  	"github.com/amarpal/go-tools/analysis/lint"
    11  	"github.com/amarpal/go-tools/analysis/report"
    12  	"github.com/amarpal/go-tools/knowledge"
    13  	"github.com/amarpal/go-tools/pattern"
    14  	"golang.org/x/tools/go/analysis"
    15  	"golang.org/x/tools/go/analysis/passes/inspect"
    16  )
    17  
    18  var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
    19  	Analyzer: &analysis.Analyzer{
    20  		Name:     "SA4032",
    21  		Run:      CheckImpossibleGOOSGOARCH,
    22  		Requires: []*analysis.Analyzer{inspect.Analyzer},
    23  	},
    24  	Doc: &lint.Documentation{
    25  		Title:    `Comparing \'runtime.GOOS\' or \'runtime.GOARCH\' against impossible value`,
    26  		Since:    "Unreleased",
    27  		Severity: lint.SeverityWarning,
    28  		MergeIf:  lint.MergeIfAny,
    29  	},
    30  })
    31  
    32  var Analyzer = SCAnalyzer.Analyzer
    33  
    34  var (
    35  	goosComparisonQ   = pattern.MustParse(`(BinaryExpr (Symbol "runtime.GOOS") op@(Or "==" "!=") lit@(BasicLit "STRING" _))`)
    36  	goarchComparisonQ = pattern.MustParse(`(BinaryExpr (Symbol "runtime.GOARCH") op@(Or "==" "!=") lit@(BasicLit "STRING" _))`)
    37  )
    38  
    39  func CheckImpossibleGOOSGOARCH(pass *analysis.Pass) (any, error) {
    40  	// TODO(dh): validate GOOS and GOARCH together. that is,
    41  	// given '(linux && amd64) || (windows && mips)',
    42  	// flag 'if runtime.GOOS == "linux" && runtime.GOARCH == "mips"'
    43  	//
    44  	// We can't use our IR for the control flow graph, because go/types constant folds constant comparisons, so
    45  	// 'runtime.GOOS == "windows"' will just become 'false'. We can't use the AST-based CFG builder from x/tools,
    46  	// because it doesn't model branch conditions.
    47  
    48  	for _, f := range pass.Files {
    49  		expr, ok := code.BuildConstraints(pass, f)
    50  		if !ok {
    51  			continue
    52  		}
    53  
    54  		ast.Inspect(f, func(node ast.Node) bool {
    55  			if m, ok := code.Match(pass, goosComparisonQ, node); ok {
    56  				tv := pass.TypesInfo.Types[m.State["lit"].(ast.Expr)]
    57  				goos := constant.StringVal(tv.Value)
    58  
    59  				if _, ok := knowledge.KnownGOOS[goos]; !ok {
    60  					// Don't try to reason about GOOS values we don't know about. Maybe the user is using a newer
    61  					// version of Go that supports a new target, or maybe they run a fork of Go.
    62  					return true
    63  				}
    64  				sat, ok := validateGOOSComparison(expr, goos)
    65  				if !ok {
    66  					return true
    67  				}
    68  				if !sat {
    69  					// Note that we do not have to worry about constraints that can never be satisfied, such as 'linux
    70  					// && windows'. Packages with such files will not be passed to Staticcheck in the first place,
    71  					// precisely because the constraints aren't satisfiable.
    72  					report.Report(pass, node,
    73  						fmt.Sprintf("due to the file's build constraints, runtime.GOOS will never equal %q", goos))
    74  				}
    75  			} else if m, ok := code.Match(pass, goarchComparisonQ, node); ok {
    76  				tv := pass.TypesInfo.Types[m.State["lit"].(ast.Expr)]
    77  				goarch := constant.StringVal(tv.Value)
    78  
    79  				if _, ok := knowledge.KnownGOARCH[goarch]; !ok {
    80  					// Don't try to reason about GOARCH values we don't know about. Maybe the user is using a newer
    81  					// version of Go that supports a new target, or maybe they run a fork of Go.
    82  					return true
    83  				}
    84  				sat, ok := validateGOARCHComparison(expr, goarch)
    85  				if !ok {
    86  					return true
    87  				}
    88  				if !sat {
    89  					// Note that we do not have to worry about constraints that can never be satisfied, such as 'amd64
    90  					// && mips'. Packages with such files will not be passed to Staticcheck in the first place,
    91  					// precisely because the constraints aren't satisfiable.
    92  					report.Report(pass, node,
    93  						fmt.Sprintf("due to the file's build constraints, runtime.GOARCH will never equal %q", goarch))
    94  				}
    95  			}
    96  			return true
    97  		})
    98  	}
    99  
   100  	return nil, nil
   101  }
   102  func validateGOOSComparison(expr constraint.Expr, goos string) (sat bool, didCheck bool) {
   103  	matchGoosTag := func(tag string, goos string) (ok bool, goosTag bool) {
   104  		switch tag {
   105  		case "aix",
   106  			"android",
   107  			"dragonfly",
   108  			"freebsd",
   109  			"hurd",
   110  			"illumos",
   111  			"ios",
   112  			"js",
   113  			"netbsd",
   114  			"openbsd",
   115  			"plan9",
   116  			"wasip1",
   117  			"windows":
   118  			return goos == tag, true
   119  		case "darwin":
   120  			return (goos == "darwin" || goos == "ios"), true
   121  		case "linux":
   122  			return (goos == "linux" || goos == "android"), true
   123  		case "solaris":
   124  			return (goos == "solaris" || goos == "illumos"), true
   125  		case "unix":
   126  			return (goos == "aix" ||
   127  				goos == "android" ||
   128  				goos == "darwin" ||
   129  				goos == "dragonfly" ||
   130  				goos == "freebsd" ||
   131  				goos == "hurd" ||
   132  				goos == "illumos" ||
   133  				goos == "ios" ||
   134  				goos == "linux" ||
   135  				goos == "netbsd" ||
   136  				goos == "openbsd" ||
   137  				goos == "solaris"), true
   138  		default:
   139  			return false, false
   140  		}
   141  	}
   142  
   143  	return validateTagComparison(expr, func(tag string) (matched bool, special bool) {
   144  		return matchGoosTag(tag, goos)
   145  	})
   146  }
   147  
   148  func validateGOARCHComparison(expr constraint.Expr, goarch string) (sat bool, didCheck bool) {
   149  	matchGoarchTag := func(tag string, goarch string) (ok bool, goosTag bool) {
   150  		switch tag {
   151  		case "386",
   152  			"amd64",
   153  			"arm",
   154  			"arm64",
   155  			"loong64",
   156  			"mips",
   157  			"mipsle",
   158  			"mips64",
   159  			"mips64le",
   160  			"ppc64",
   161  			"ppc64le",
   162  			"riscv64",
   163  			"s390x",
   164  			"sparc64",
   165  			"wasm":
   166  			return goarch == tag, true
   167  		default:
   168  			return false, false
   169  		}
   170  	}
   171  
   172  	return validateTagComparison(expr, func(tag string) (matched bool, special bool) {
   173  		return matchGoarchTag(tag, goarch)
   174  	})
   175  }
   176  
   177  func validateTagComparison(expr constraint.Expr, matchSpecialTag func(tag string) (matched bool, special bool)) (sat bool, didCheck bool) {
   178  	otherTags := map[string]int{}
   179  	// Collect all tags that aren't known architecture-based tags
   180  	b := expr.Eval(func(tag string) bool {
   181  		ok, special := matchSpecialTag(tag)
   182  		if !special {
   183  			// Assign an ID to this tag, but only if we haven't seen it before. For the expression 'foo && foo', this
   184  			// callback will be called twice for the 'foo' tag.
   185  			if _, ok := otherTags[tag]; !ok {
   186  				otherTags[tag] = len(otherTags)
   187  			}
   188  		}
   189  		return ok
   190  	})
   191  
   192  	if b || len(otherTags) == 0 {
   193  		// We're done. Either the formula can be satisfied regardless of the values of non-special tags, if any,
   194  		// or there aren't any non-special tags and the formula cannot be satisfied.
   195  		return b, true
   196  	}
   197  
   198  	if len(otherTags) > 10 {
   199  		// We have to try 2**len(otherTags) combinations of tags. 2**10 is about the worst we're willing to try.
   200  		return false, false
   201  	}
   202  
   203  	// Try all permutations of otherTags. If any evaluates to true, then the expression is satisfiable.
   204  	for bits := 0; bits < 1<<len(otherTags); bits++ {
   205  		b := expr.Eval(func(tag string) bool {
   206  			ok, special := matchSpecialTag(tag)
   207  			if special {
   208  				return ok
   209  			}
   210  			return bits&(1<<otherTags[tag]) != 0
   211  		})
   212  		if b {
   213  			return true, true
   214  		}
   215  	}
   216  
   217  	return false, true
   218  }