package noliteral import ( "go/ast" "go/token" "strings" "sync" "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, } var ( aliasCache = sync.Map{} ) type Configuration struct { TargetMethods []string `mapstructure:"target-methods"` DisablePackages []string `mapstructure:"disable-packages"` ConstantsPackage string `mapstructure:"constants-package"` } var Config = Configuration{ TargetMethods: []string{"Debug", "Info", "Warn", "Error"}, } func run(pass *analysis.Pass) (interface{}, error) { for _, file := range pass.Files { ast.Inspect(file, func(n ast.Node) bool { expr, ok := n.(*ast.CallExpr) if !ok { return true } isLog, _ := isLogDot(expr.Fun) if !isLog || len(expr.Args) == 0 { return true } if !isStringValue(expr.Args[0]) { alias, _ := getAliasByPkgName(file, Config.ConstantsPackage) if Config.ConstantsPackage == "" || getPackageName(expr.Args[0]) == alias || getPackageName(expr.Args[0]) == "" { return true } for _, pkgName := range Config.DisablePackages { if pkgName == getPackageName(expr.Args[0]) { return true } } pass.Report(analysis.Diagnostic{ Pos: expr.Pos(), End: expr.End(), Category: LinterName, Message: "Wrong package for constants", SuggestedFixes: nil, }) return true } 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 }) } return nil, nil } func isLogDot(expr ast.Expr) (bool, string) { sel, ok := expr.(*ast.SelectorExpr) if !ok { return false, "" } for _, method := range Config.TargetMethods { if isIdent(sel.Sel, method) { return true, method } } return false, "" } func isIdent(expr ast.Expr, ident string) bool { id, ok := expr.(*ast.Ident) if ok && id.Name == ident { return true } return false } func isStringValue(expr ast.Expr) bool { basicLit, ok := expr.(*ast.BasicLit) return ok && basicLit.Kind == token.STRING } func getAliasByPkgName(file *ast.File, pkgName string) (string, error) { if alias, ok := aliasCache.Load(file); ok { return alias.(string), nil } var alias string specs := file.Imports for _, spec := range specs { alias = getAliasFromImportSpec(spec, pkgName) if alias != "" { break } } aliasCache.Store(file, alias) return alias, nil } func getAliasFromImportSpec(spec *ast.ImportSpec, pkgName string) string { if spec == nil { return "" } importName := strings.Replace(spec.Path.Value, "\"", "", -1) if importName != pkgName { return "" } split := strings.Split(importName, "/") if len(split) == 0 { return "" } alias := split[len(split)-1] if spec.Name != nil { alias = spec.Name.Name } return alias } func getPackageName(expr ast.Expr) string { if selectorExpr, ok := expr.(*ast.SelectorExpr); ok { if ident, ok := selectorExpr.X.(*ast.Ident); ok { return ident.Name } } return "" }