mirror of
https://github.com/mgechev/revive.git
synced 2025-11-25 22:12:38 +02:00
238 lines
6.0 KiB
Go
238 lines
6.0 KiB
Go
|
|
package rule
|
||
|
|
|
||
|
|
import (
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"go/ast"
|
||
|
|
"go/token"
|
||
|
|
"strconv"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"github.com/mgechev/revive/lint"
|
||
|
|
"github.com/mgechev/revive/logging"
|
||
|
|
)
|
||
|
|
|
||
|
|
// TimeDateRule lints the way time.Date is used.
|
||
|
|
type TimeDateRule struct{}
|
||
|
|
|
||
|
|
// Apply applies the rule to given file.
|
||
|
|
func (*TimeDateRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
|
||
|
|
var failures []lint.Failure
|
||
|
|
|
||
|
|
onFailure := func(failure lint.Failure) {
|
||
|
|
failures = append(failures, failure)
|
||
|
|
}
|
||
|
|
|
||
|
|
w := &lintTimeDate{file, onFailure}
|
||
|
|
|
||
|
|
ast.Walk(w, file.AST)
|
||
|
|
return failures
|
||
|
|
}
|
||
|
|
|
||
|
|
// Name returns the rule name.
|
||
|
|
func (*TimeDateRule) Name() string {
|
||
|
|
return "time-date"
|
||
|
|
}
|
||
|
|
|
||
|
|
type lintTimeDate struct {
|
||
|
|
file *lint.File
|
||
|
|
onFailure func(lint.Failure)
|
||
|
|
}
|
||
|
|
|
||
|
|
var (
|
||
|
|
// timeDateArgumentNames are the names of the arguments of time.Date
|
||
|
|
timeDateArgumentNames = []string{
|
||
|
|
"year",
|
||
|
|
"month",
|
||
|
|
"day",
|
||
|
|
"hour",
|
||
|
|
"minute",
|
||
|
|
"second",
|
||
|
|
"nanosecond",
|
||
|
|
"timezone",
|
||
|
|
}
|
||
|
|
|
||
|
|
// timeDateArity is the number of arguments of time.Date
|
||
|
|
timeDateArity = len(timeDateArgumentNames)
|
||
|
|
)
|
||
|
|
|
||
|
|
func (w lintTimeDate) Visit(n ast.Node) ast.Visitor {
|
||
|
|
ce, ok := n.(*ast.CallExpr)
|
||
|
|
if !ok || len(ce.Args) != timeDateArity {
|
||
|
|
return w
|
||
|
|
}
|
||
|
|
if !isPkgDot(ce.Fun, "time", "Date") {
|
||
|
|
return w
|
||
|
|
}
|
||
|
|
|
||
|
|
// The last argument is a timezone, there is no need to check it, also it has a different type
|
||
|
|
for pos, arg := range ce.Args[:timeDateArity-1] {
|
||
|
|
bl, ok := arg.(*ast.BasicLit)
|
||
|
|
if !ok {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
replacedValue, err := parseDecimalInteger(bl)
|
||
|
|
if err == nil {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
if errors.Is(err, errParsedInvalid) {
|
||
|
|
// This is not supposed to happen, let's be defensive
|
||
|
|
// log the error, but continue
|
||
|
|
|
||
|
|
logger, errLogger := logging.GetLogger()
|
||
|
|
if errLogger != nil {
|
||
|
|
// This is not supposed to happen, discard both errors
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
logger.With(
|
||
|
|
"value", bl.Value,
|
||
|
|
"kind", bl.Kind,
|
||
|
|
"error", err.Error(),
|
||
|
|
).Error("failed to parse time.Date argument")
|
||
|
|
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
confidence := 0.8 // default confidence
|
||
|
|
errMessage := err.Error()
|
||
|
|
instructions := fmt.Sprintf("use %s instead of %s", replacedValue, bl.Value)
|
||
|
|
switch {
|
||
|
|
case errors.Is(err, errParsedOctalWithZero):
|
||
|
|
// people can use 00, 01, 02, 03, 04, 05, 06, and 07 if they want.
|
||
|
|
confidence = 0.5
|
||
|
|
|
||
|
|
case errors.Is(err, errParsedOctalWithPaddingZeroes):
|
||
|
|
// This is a clear mistake.
|
||
|
|
// example with 000123456 (octal) is about 123456 or 42798 ?
|
||
|
|
confidence = 1
|
||
|
|
|
||
|
|
strippedValue := strings.TrimLeft(bl.Value, "0")
|
||
|
|
if strippedValue == "" {
|
||
|
|
// avoid issue with 00000000
|
||
|
|
strippedValue = "0"
|
||
|
|
}
|
||
|
|
|
||
|
|
if strippedValue != replacedValue {
|
||
|
|
instructions = fmt.Sprintf(
|
||
|
|
"choose between %s and %s (decimal value of %s octal value)",
|
||
|
|
strippedValue, replacedValue, strippedValue,
|
||
|
|
)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
w.onFailure(lint.Failure{
|
||
|
|
Category: "time",
|
||
|
|
Node: bl,
|
||
|
|
Confidence: confidence,
|
||
|
|
Failure: fmt.Sprintf(
|
||
|
|
"use decimal digits for time.Date %s argument: %s found: %s",
|
||
|
|
timeDateArgumentNames[pos], errMessage, instructions),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
return w
|
||
|
|
}
|
||
|
|
|
||
|
|
var (
|
||
|
|
errParsedOctal = errors.New("octal notation")
|
||
|
|
errParsedOctalWithZero = errors.New("octal notation with leading zero")
|
||
|
|
errParsedOctalWithPaddingZeroes = errors.New("octal notation with padding zeroes")
|
||
|
|
errParsedHexadecimal = errors.New("hexadecimal notation")
|
||
|
|
errParseBinary = errors.New("binary notation")
|
||
|
|
errParsedFloat = errors.New("float literal")
|
||
|
|
errParsedExponential = errors.New("exponential notation")
|
||
|
|
errParsedAlternative = errors.New("alternative notation")
|
||
|
|
errParsedInvalid = errors.New("invalid notation")
|
||
|
|
)
|
||
|
|
|
||
|
|
func parseDecimalInteger(bl *ast.BasicLit) (string, error) {
|
||
|
|
currentValue := strings.ToLower(bl.Value)
|
||
|
|
|
||
|
|
switch currentValue {
|
||
|
|
case "0":
|
||
|
|
// skip 0 as it is a valid value for all the arguments
|
||
|
|
return bl.Value, nil
|
||
|
|
case "00", "01", "02", "03", "04", "05", "06", "07":
|
||
|
|
// people can use 00, 01, 02, 03, 04, 05, 06, 07, if they want
|
||
|
|
return bl.Value[1:], errParsedOctalWithZero
|
||
|
|
}
|
||
|
|
|
||
|
|
switch bl.Kind {
|
||
|
|
case token.FLOAT:
|
||
|
|
// someone used a float literal, while they should have used an integer literal.
|
||
|
|
parsedValue, err := strconv.ParseFloat(currentValue, 64)
|
||
|
|
if err != nil {
|
||
|
|
// This is not supposed to happen
|
||
|
|
return bl.Value, fmt.Errorf(
|
||
|
|
"%w: %s: %w",
|
||
|
|
errParsedInvalid,
|
||
|
|
"failed to parse number as float",
|
||
|
|
err,
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// this will convert back the number to a string
|
||
|
|
cleanedValue := strconv.FormatFloat(parsedValue, 'f', -1, 64)
|
||
|
|
if strings.Contains(currentValue, "e") {
|
||
|
|
return cleanedValue, errParsedExponential
|
||
|
|
}
|
||
|
|
|
||
|
|
return cleanedValue, errParsedFloat
|
||
|
|
|
||
|
|
case token.INT:
|
||
|
|
// we expect this format
|
||
|
|
|
||
|
|
default:
|
||
|
|
// This is not supposed to happen
|
||
|
|
return bl.Value, fmt.Errorf(
|
||
|
|
"%w: %s",
|
||
|
|
errParsedInvalid,
|
||
|
|
"unexpected kind of literal",
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Parse the number with base=0 that allows to accept all number formats and base
|
||
|
|
parsedValue, err := strconv.ParseInt(currentValue, 0, 64)
|
||
|
|
if err != nil {
|
||
|
|
// This is not supposed to happen
|
||
|
|
return bl.Value, fmt.Errorf(
|
||
|
|
"%w: %s: %w",
|
||
|
|
errParsedInvalid,
|
||
|
|
"failed to parse number as integer",
|
||
|
|
err,
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
cleanedValue := strconv.FormatInt(parsedValue, 10)
|
||
|
|
|
||
|
|
// Let's figure out the notation to return an error
|
||
|
|
switch {
|
||
|
|
case strings.HasPrefix(currentValue, "0b"):
|
||
|
|
return cleanedValue, errParseBinary
|
||
|
|
case strings.HasPrefix(currentValue, "0x"):
|
||
|
|
return cleanedValue, errParsedHexadecimal
|
||
|
|
case strings.HasPrefix(currentValue, "0"):
|
||
|
|
// this matches both "0" and "0o" octal notation.
|
||
|
|
|
||
|
|
if strings.HasPrefix(currentValue, "00") {
|
||
|
|
// 00123456 (octal) is about 123456 or 42798 ?
|
||
|
|
return cleanedValue, errParsedOctalWithPaddingZeroes
|
||
|
|
}
|
||
|
|
|
||
|
|
// 0006 is a valid octal notation, but we can use 6 instead.
|
||
|
|
return cleanedValue, errParsedOctal
|
||
|
|
}
|
||
|
|
|
||
|
|
// Convert back the number to a string, and compare it with the original one
|
||
|
|
formattedValue := strconv.FormatInt(parsedValue, 10)
|
||
|
|
if formattedValue != currentValue {
|
||
|
|
// This can catch some edge cases like: 1_0 ...
|
||
|
|
return cleanedValue, errParsedAlternative
|
||
|
|
}
|
||
|
|
|
||
|
|
// The number is a decimal integer, we can use it as is.
|
||
|
|
return bl.Value, nil
|
||
|
|
}
|