package noliteral import ( "fmt" "go/ast" "go/token" "slices" astutils "git.frostfs.info/TrueCloudLab/linters/pkg/ast-utils" "github.com/mitchellh/mapstructure" "golang.org/x/tools/go/analysis" ) const linterName = "noliteral" const linterDoc = linterName + " is a helper tool that ensures logging messages in Go code are structured and not written as simple text." type noliteral struct { analyzer analysis.Analyzer config Configuration } type Configuration struct { TargetMethods []string `mapstructure:"target-methods"` DisablePackages []string `mapstructure:"disable-packages"` ConstantsPackage string `mapstructure:"constants-package"` Enable bool `mapstructure:"enable"` Position *int `mapstructure:"position"` } var defaultTargetMethods = []string{"Debug", "Info", "Warn", "Error"} func New(conf any) (*analysis.Analyzer, error) { var linter noliteral linter.analyzer = analysis.Analyzer{ Name: linterName, Doc: linterDoc, Run: linter.run, } var config Configuration config.Enable = true err := mapstructure.Decode(conf, &linter.config) if err != nil { return nil, err } linter.config.TargetMethods = append(linter.config.TargetMethods, defaultTargetMethods...) if linter.config.Position == nil { linter.config.Position = new(int) } if *linter.config.Position < 0 { return nil, fmt.Errorf("position contains negative value: %d", *linter.config.Position) } return &linter.analyzer, nil } func (l *noliteral) run(pass *analysis.Pass) (interface{}, error) { if !l.config.Enable { return nil, nil } for _, file := range pass.Files { ast.Inspect(file, func(n ast.Node) bool { switch v := n.(type) { // a := zap.Error() case *ast.AssignStmt: if _, ok := v.Rhs[0].(*ast.CallExpr); ok { return false } // a := &log.Error() case *ast.UnaryExpr: if _, ok := v.X.(*ast.CallExpr); ok && v.Op == token.AND { return false } // log.Error() case *ast.CallExpr: if expr, ok := n.(*ast.CallExpr); ok { return l.analyzeCallExpr(pass, expr, file) } } return true }) } return nil, nil } func (l *noliteral) analyzeCallExpr(pass *analysis.Pass, expr *ast.CallExpr, file *ast.File) bool { isLog, _ := astutils.IsTargetMethod(expr.Fun, l.config.TargetMethods) pos := *l.config.Position if !isLog || len(expr.Args) == 0 || pos >= len(expr.Args) || astutils.HasNoLintComment(pass, expr.Pos()) { return false } if !astutils.IsStringValue(expr.Args[pos]) { alias, _ := astutils.GetAliasByPkgName(file, l.config.ConstantsPackage) pkgName := astutils.GetPackageName(expr.Args[pos]) if l.config.ConstantsPackage == "" || pkgName == alias || pkgName == "" || slices.Contains(l.config.DisablePackages, pkgName) { return false } pass.Report(analysis.Diagnostic{ Pos: expr.Pos(), End: expr.End(), Category: linterName, Message: "Wrong package for constants", SuggestedFixes: nil, }) return false } pass.Report(analysis.Diagnostic{ Pos: expr.Pos(), End: expr.End(), Category: linterName, Message: "Literals are not allowed in the body of the logger", SuggestedFixes: nil, }) return false }