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

     1  package sa4026
     2  
     3  import (
     4  	"fmt"
     5  	"go/ast"
     6  	"go/types"
     7  
     8  	"github.com/amarpal/go-tools/analysis/code"
     9  	"github.com/amarpal/go-tools/analysis/edit"
    10  	"github.com/amarpal/go-tools/analysis/lint"
    11  	"github.com/amarpal/go-tools/analysis/report"
    12  	"github.com/amarpal/go-tools/pattern"
    13  
    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:     "SA4026",
    21  		Run:      run,
    22  		Requires: []*analysis.Analyzer{inspect.Analyzer},
    23  	},
    24  	Doc: &lint.Documentation{
    25  		Title: "Go constants cannot express negative zero",
    26  		Text: `In IEEE 754 floating point math, zero has a sign and can be positive
    27  or negative. This can be useful in certain numerical code.
    28  
    29  Go constants, however, cannot express negative zero. This means that
    30  the literals \'-0.0\' and \'0.0\' have the same ideal value (zero) and
    31  will both represent positive zero at runtime.
    32  
    33  To explicitly and reliably create a negative zero, you can use the
    34  \'math.Copysign\' function: \'math.Copysign(0, -1)\'.`,
    35  		Since:    "2021.1",
    36  		Severity: lint.SeverityWarning,
    37  		MergeIf:  lint.MergeIfAny,
    38  	},
    39  })
    40  
    41  var Analyzer = SCAnalyzer.Analyzer
    42  
    43  var negativeZeroFloatQ = pattern.MustParse(`
    44  	(Or
    45  		(UnaryExpr
    46  			"-"
    47  			(BasicLit "FLOAT" "0.0"))
    48  
    49  		(UnaryExpr
    50  			"-"
    51  			(CallExpr conv@(Object (Or "float32" "float64")) lit@(Or (BasicLit "INT" "0") (BasicLit "FLOAT" "0.0"))))
    52  
    53  		(CallExpr
    54  			conv@(Object (Or "float32" "float64"))
    55  			(UnaryExpr "-" lit@(BasicLit "INT" "0"))))`)
    56  
    57  func run(pass *analysis.Pass) (interface{}, error) {
    58  	fn := func(node ast.Node) {
    59  		m, ok := code.Match(pass, negativeZeroFloatQ, node)
    60  		if !ok {
    61  			return
    62  		}
    63  
    64  		if conv, ok := m.State["conv"].(*types.TypeName); ok {
    65  			var replacement string
    66  			// TODO(dh): how does this handle type aliases?
    67  			if conv.Name() == "float32" {
    68  				replacement = `float32(math.Copysign(0, -1))`
    69  			} else {
    70  				replacement = `math.Copysign(0, -1)`
    71  			}
    72  			report.Report(pass, node,
    73  				fmt.Sprintf("in Go, the floating-point expression '%s' is the same as '%s(%s)', it does not produce a negative zero",
    74  					report.Render(pass, node),
    75  					conv.Name(),
    76  					report.Render(pass, m.State["lit"])),
    77  				report.Fixes(edit.Fix("use math.Copysign to create negative zero", edit.ReplaceWithString(node, replacement))))
    78  		} else {
    79  			const replacement = `math.Copysign(0, -1)`
    80  			report.Report(pass, node,
    81  				"in Go, the floating-point literal '-0.0' is the same as '0.0', it does not produce a negative zero",
    82  				report.Fixes(edit.Fix("use math.Copysign to create negative zero", edit.ReplaceWithString(node, replacement))))
    83  		}
    84  	}
    85  	code.Preorder(pass, fn, (*ast.UnaryExpr)(nil), (*ast.CallExpr)(nil))
    86  	return nil, nil
    87  }