From 9566aff06c7e994d3d3632365dc359b9e22c9ba7 Mon Sep 17 00:00:00 2001 From: Shayegan Hooshyari Date: Sat, 24 Apr 2021 16:46:36 +0430 Subject: [PATCH] Add fix option for messagefmt and complete importalias fix option Resolves: #6 Signed-off-by: Shayegan Hooshyari --- pkg/analysis/importalias/analyzer.go | 70 ++++++++++++++++++++-------- pkg/analysis/messagefmt/analyzer.go | 68 +++++++++++++++++++++++---- 2 files changed, 110 insertions(+), 28 deletions(-) diff --git a/pkg/analysis/importalias/analyzer.go b/pkg/analysis/importalias/analyzer.go index 1debf14..e605cc5 100644 --- a/pkg/analysis/importalias/analyzer.go +++ b/pkg/analysis/importalias/analyzer.go @@ -16,7 +16,10 @@ package importalias import ( "fmt" "go/ast" + "go/types" + "regexp" + "strconv" "strings" "golang.org/x/tools/go/analysis" @@ -50,12 +53,16 @@ func run(pass *analysis.Pass) (interface{}, error) { return } + if strings.HasPrefix(alias, "_") { + return // Used by go test and for auto-includes, not a conflict. + } + aliasSlice := strings.Split(alias, "_") - path := strings.ReplaceAll(importStmt.Path.Value, "\"", "") + + originalImportPath, _ := strconv.Unquote(importStmt.Path.Value) // replace all separators with `/` for normalization - path = strings.ReplaceAll(path, "_", "/") - path = strings.ReplaceAll(path, ".", "/") - path = strings.ReplaceAll(path, "-", "") + replacer := strings.NewReplacer("_", "/", ".", "/", "-", "") + path := replacer.Replace(originalImportPath) // omit the domain name in path pathSlice := strings.Split(path, "/")[1:] @@ -64,15 +71,11 @@ func run(pass *analysis.Pass) (interface{}, error) { _, versionIndex := packageVersion(pathSlice) pass.Report(analysis.Diagnostic{ Pos: node.Pos(), - Message: fmt.Sprintf("version %q not specified in alias %q for import path %q", - pathSlice[versionIndex], alias, path), + Message: fmt.Sprintf("version %q not specified in alias %q for import path %q may replace %q with %q", + pathSlice[versionIndex], alias, path, alias, applicableAlias), SuggestedFixes: []analysis.SuggestedFix{{ - Message: fmt.Sprintf("should replace %q with %q", alias, applicableAlias), - TextEdits: []analysis.TextEdit{{ - Pos: importStmt.Pos(), - End: importStmt.Name.End(), - NewText: []byte(applicableAlias), - }}, + Message: fmt.Sprintf("may replace %q with %q", alias, applicableAlias), + TextEdits: findEdits(node, pass.TypesInfo.Uses, originalImportPath, alias, applicableAlias), }}, }) @@ -83,14 +86,10 @@ func run(pass *analysis.Pass) (interface{}, error) { applicableAlias := getAliasFix(pathSlice) pass.Report(analysis.Diagnostic{ Pos: node.Pos(), - Message: err.Error(), + Message: fmt.Sprintf("%q may replace %q with %q", err.Error(), alias, applicableAlias), SuggestedFixes: []analysis.SuggestedFix{{ - Message: fmt.Sprintf("should replace %q with %q", alias, applicableAlias), - TextEdits: []analysis.TextEdit{{ - Pos: importStmt.Pos(), - End: importStmt.Name.End(), - NewText: []byte(applicableAlias), - }}, + Message: fmt.Sprintf("may replace %q with %q", alias, applicableAlias), + TextEdits: findEdits(node, pass.TypesInfo.Uses, originalImportPath, alias, applicableAlias), }}, }) @@ -171,7 +170,7 @@ func packageVersion(pathSlice []string) (bool, int) { func searchString(slice []string, word string) int { for pos, value := range slice { - r, _ := regexp.Compile(word + "(s)?") + r, _ := regexp.Compile("^" + word + "(s)?$") if r.MatchString(value) { return pos } @@ -179,3 +178,34 @@ func searchString(slice []string, word string) int { return len(slice) } + +func findEdits(node ast.Node, uses map[*ast.Ident]types.Object, importPath, original, required string) []analysis.TextEdit { + // Edit the actual import line. + result := []analysis.TextEdit{{ + Pos: node.Pos(), + End: node.End(), + NewText: []byte(required + " " + strconv.Quote(importPath)), + }} + + // Edit all the uses of the alias in the code. + for use, pkg := range uses { + pkgName, ok := pkg.(*types.PkgName) + if !ok { + // skip identifiers that aren't pointing at a PkgName. + continue + } + + if pkgName.Pos() != node.Pos() { + // skip identifiers pointing to a different import statement. + continue + } + if original == pkgName.Name() { + result = append(result, analysis.TextEdit{ + Pos: use.Pos(), + End: use.End(), + NewText: []byte(required), + }) + } + } + return result +} diff --git a/pkg/analysis/messagefmt/analyzer.go b/pkg/analysis/messagefmt/analyzer.go index 658af6f..7c72412 100644 --- a/pkg/analysis/messagefmt/analyzer.go +++ b/pkg/analysis/messagefmt/analyzer.go @@ -14,6 +14,7 @@ package messagefmt import ( + "fmt" "go/ast" "go/token" "go/types" @@ -122,8 +123,8 @@ func checkInitialLower(pass *analysis.Pass, lit *ast.BasicLit) { if lit == nil { return } - - words := strings.Fields(strings.Trim(lit.Value, "\"`")) + value := strings.Trim(lit.Value, "\"`") + words := strings.Fields(value) first := words[0] // If the first word is all uppercase, it's an @@ -137,7 +138,21 @@ func checkInitialLower(pass *analysis.Pass, lit *ast.BasicLit) { // `lit.Value` will be the string literal quote. firstRune, _ := utf8.DecodeRuneInString(first) if unicode.IsUpper(firstRune) && !isException(first) { - pass.Reportf(lit.Pos(), "message starts with uppercase: %s", lit.Value) + validMessage := []rune(value) + validMessage[0] = unicode.ToLower(validMessage[0]) + fix := string(validMessage) + pass.Report(analysis.Diagnostic{ + Pos: lit.Pos(), + Message: fmt.Sprintf("message starts with uppercase: %s", lit.Value), + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("message starts with uppercase: %s", lit.Value), + TextEdits: []analysis.TextEdit{{ + Pos: lit.Pos()+1, + End: lit.End()-1, + NewText: []byte(fix), + }}, + }}, + }) } } @@ -146,7 +161,8 @@ func checkInitialUpper(pass *analysis.Pass, lit *ast.BasicLit) { return } - words := strings.Fields(strings.Trim(lit.Value, "\"`")) + value := strings.Trim(lit.Value, "\"`") + words := strings.Fields(value) first := words[0] // If the first word is all uppercase, it's an @@ -160,7 +176,21 @@ func checkInitialUpper(pass *analysis.Pass, lit *ast.BasicLit) { // `lit.Value` will be the string literal quote. firstRune, _ := utf8.DecodeRuneInString(first) if unicode.IsLower(firstRune) && !isException(first) { - pass.Reportf(lit.Pos(), "message starts with lowercase: %s", lit.Value) + validMessage := []rune(value) + validMessage[0] = unicode.ToUpper(validMessage[0]) + fix := string(validMessage) + pass.Report(analysis.Diagnostic{ + Pos: lit.Pos(), + Message: fmt.Sprintf("message starts with lowercase: %s", lit.Value), + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("message starts with lowercase: %s", lit.Value), + TextEdits: []analysis.TextEdit{{ + Pos: lit.Pos()+1, + End: lit.End()-1, + NewText: []byte(fix), + }}, + }}, + }) } } @@ -172,7 +202,18 @@ func checkEndsWithoutPeriod(pass *analysis.Pass, lit *ast.BasicLit) { value := strings.Trim(lit.Value, "\"`") if len(value) > 0 && value[len(value)-1] == '.' { - pass.Reportf(lit.Pos(), "message must not end with a period: %s", lit.Value) + pass.Report(analysis.Diagnostic{ + Pos: lit.Pos(), + Message: fmt.Sprintf("message must not end with a period: %s", lit.Value), + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("message must not end with a period: %s", lit.Value), + TextEdits: []analysis.TextEdit{{ + Pos: lit.Pos()+1, + End: lit.End()-1, + NewText: []byte(value[:len(value)-1]), + }}, + }}, + }) } } @@ -183,8 +224,19 @@ func checkEndsWithPeriod(pass *analysis.Pass, lit *ast.BasicLit) { value := strings.Trim(lit.Value, "\"`") - if len(value) == 0 || value[len(value)-1] != '.' { - pass.Reportf(lit.Pos(), "message must end with a period: %s", lit.Value) + if len(value) > 0 && value[len(value)-1] != '.' { + pass.Report(analysis.Diagnostic{ + Pos: lit.Pos(), + Message: fmt.Sprintf("message must end with a period: %s", lit.Value), + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("message must end with a period: %s", lit.Value), + TextEdits: []analysis.TextEdit{{ + Pos: lit.Pos()+1, + End: lit.End()-1, + NewText: []byte(value + "."), + }}, + }}, + }) } }