package noliteral import ( "go/ast" "go/token" astutils "git.frostfs.info/TrueCloudLab/linters/pkg/ast-utils" "github.com/mitchellh/mapstructure" "golang.org/x/tools/go/analysis" ) const LinterName = "noliteral" var LogsAnalyzer = &analysis.Analyzer{ Name: LinterName, Doc: LinterName + " is a helper tool that ensures logging messages in Go code are structured and not written as simple text.", Run: run, } type Configuration struct { TargetMethods []string `mapstructure:"target-methods"` DisablePackages []string `mapstructure:"disable-packages"` ConstantsPackage string `mapstructure:"constants-package"` Enable bool `mapstructure:"enable"` } var Config = Configuration{ TargetMethods: []string{"Debug", "Info", "Warn", "Error"}, } func New(conf any) (*analysis.Analyzer, error) { var config Configuration config.Enable = true err := mapstructure.Decode(conf, &config) if err != nil { Config.Enable = true return LogsAnalyzer, nil } Config.TargetMethods = append(Config.TargetMethods, config.TargetMethods...) Config.ConstantsPackage = config.ConstantsPackage Config.DisablePackages = config.DisablePackages Config.Enable = config.Enable return LogsAnalyzer, nil } func run(pass *analysis.Pass) (interface{}, error) { if !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 analyzeCallExpr(pass, expr, file) } } return true }) } return nil, nil } func analyzeCallExpr(pass *analysis.Pass, expr *ast.CallExpr, file *ast.File) bool { isLog, _ := astutils.IsTargetMethod(expr.Fun, Config.TargetMethods) if !isLog || len(expr.Args) == 0 || astutils.HasNoLintComment(pass, expr.Pos()) { return false } if !astutils.IsStringValue(expr.Args[0]) { alias, _ := astutils.GetAliasByPkgName(file, Config.ConstantsPackage) if Config.ConstantsPackage == "" || astutils.GetPackageName(expr.Args[0]) == alias || astutils.GetPackageName(expr.Args[0]) == "" { return false } for _, pkgName := range Config.DisablePackages { if pkgName == astutils.GetPackageName(expr.Args[0]) { 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 }