diff --git a/apps/okgo/checks.yml b/apps/okgo/checks.yml index e5391943..78ead681 100644 --- a/apps/okgo/checks.yml +++ b/apps/okgo/checks.yml @@ -12,6 +12,8 @@ packages: distance-to-project-pkg: 1 govet: main: github.com/nmiyake/govet + importalias: + main: github.com/palantir/checks/importalias ineffassign: main: github.com/gordonklaus/ineffassign novendor: diff --git a/apps/okgo/checks/definitions.go b/apps/okgo/checks/definitions.go index e7e934e6..9ab8600a 100644 --- a/apps/okgo/checks/definitions.go +++ b/apps/okgo/checks/definitions.go @@ -76,6 +76,10 @@ func checkers() []Checker { return line == "exit status 1" }, }, + &checkerDefinition{ + cmd: cmdlib.Instance().MustNewCmd("importalias"), + lineParser: checkoutput.DefaultParser(pkgpath.Relative), + }, &checkerDefinition{ cmd: cmdlib.Instance().MustNewCmd("ineffassign"), lineParser: checkoutput.DefaultParser(pkgpath.Absolute), diff --git a/apps/okgo/generated_src/amalgomatedchecks.go b/apps/okgo/generated_src/amalgomatedchecks.go index 0d61a5c8..18821680 100644 --- a/apps/okgo/generated_src/amalgomatedchecks.go +++ b/apps/okgo/generated_src/amalgomatedchecks.go @@ -13,6 +13,7 @@ import ( varcheck "github.com/palantir/godel/apps/okgo/generated_src/internal/github.com/opennota/check/cmd/varcheck" compiles "github.com/palantir/godel/apps/okgo/generated_src/internal/github.com/palantir/checks/compiles" extimport "github.com/palantir/godel/apps/okgo/generated_src/internal/github.com/palantir/checks/extimport" + importalias "github.com/palantir/godel/apps/okgo/generated_src/internal/github.com/palantir/checks/importalias" novendor "github.com/palantir/godel/apps/okgo/generated_src/internal/github.com/palantir/checks/novendor" outparamcheck "github.com/palantir/godel/apps/okgo/generated_src/internal/github.com/palantir/checks/outparamcheck" deadcode "github.com/palantir/godel/apps/okgo/generated_src/internal/github.com/remyoudompheng/go-misc/deadcode" @@ -30,6 +31,8 @@ var programs = map[string]func(){"compiles": func() { golint.AmalgomatedMain() }, "govet": func() { govet.AmalgomatedMain() +}, "importalias": func() { + importalias.AmalgomatedMain() }, "ineffassign": func() { ineffassign.AmalgomatedMain() }, "novendor": func() { diff --git a/apps/okgo/generated_src/internal/github.com/palantir/checks/extimport/extimport.go b/apps/okgo/generated_src/internal/github.com/palantir/checks/extimport/extimport.go index a7c27f44..acd99cca 100644 --- a/apps/okgo/generated_src/internal/github.com/palantir/checks/extimport/extimport.go +++ b/apps/okgo/generated_src/internal/github.com/palantir/checks/extimport/extimport.go @@ -42,7 +42,7 @@ const ( var ( pkgsFlag = flag.StringSlice{ Name: pkgsFlagName, - Usage: "paths to the pacakges to check", + Usage: "paths to the packages to check", } listFlag = flag.BoolFlag{ Name: listFlagName, diff --git a/apps/okgo/generated_src/internal/github.com/palantir/checks/importalias/importalias.go b/apps/okgo/generated_src/internal/github.com/palantir/checks/importalias/importalias.go new file mode 100644 index 00000000..d20a0542 --- /dev/null +++ b/apps/okgo/generated_src/internal/github.com/palantir/checks/importalias/importalias.go @@ -0,0 +1,188 @@ +// Copyright 2016 Palantir Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package amalgomated + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "path" + "path/filepath" + "sort" + "strings" + + "github.com/nmiyake/pkg/dirs" + "github.com/nmiyake/pkg/errorstringer" + "github.com/palantir/pkg/cli" + "github.com/palantir/pkg/cli/flag" + "github.com/palantir/pkg/pkgpath" + "github.com/pkg/errors" +) + +const ( + pkgsFlagName = "pkgs" + verboseFlagName = "verbose" +) + +var ( + pkgsFlag = flag.StringSlice{ + Name: pkgsFlagName, + Usage: "paths to the packages to check", + Optional: true, + } + verboseFlag = flag.BoolFlag{ + Name: verboseFlagName, + Usage: "print verbose analysis of all imports that have multiple aliases", + Alias: "v", + } +) + +func AmalgomatedMain() { + app := cli.NewApp(cli.DebugHandler(errorstringer.SingleStack)) + app.Flags = append(app.Flags, + pkgsFlag, + verboseFlag, + ) + app.Action = func(ctx cli.Context) error { + wd, err := dirs.GetwdEvalSymLinks() + if err != nil { + return errors.Wrapf(err, "Failed to get working directory") + } + return doImportAlias(wd, ctx.Slice(pkgsFlagName), ctx.Bool(verboseFlagName), ctx.App.Stdout) + } + os.Exit(app.Run(os.Args)) +} + +func doImportAlias(projectDir string, pkgPaths []string, verbose bool, w io.Writer) error { + if !path.IsAbs(projectDir) { + return errors.Errorf("projectDir %s must be an absolute path", projectDir) + } + + gopath := os.Getenv("GOPATH") + if gopath == "" { + return errors.Errorf("GOPATH environment variable must be set") + } + + if relPath, err := filepath.Rel(path.Join(gopath, "src"), projectDir); err != nil || strings.HasPrefix(relPath, "../") { + return errors.Wrapf(err, "Project directory %s must be a subdirectory of $GOPATH/src (%s)", projectDir, path.Join(gopath, "src")) + } + + if len(pkgPaths) == 0 { + pkgs, err := pkgpath.PackagesInDir(projectDir, pkgpath.DefaultGoPkgExcludeMatcher()) + if err != nil { + return errors.Wrapf(err, "Failed to list packages") + } + + pkgPaths, err = pkgs.Paths(pkgpath.Relative) + if err != nil { + return errors.Wrapf(err, "Failed to convert package paths") + } + } + + projectImportInfo := NewProjectImportInfo() + for _, pkgPath := range pkgPaths { + currPath := path.Join(projectDir, pkgPath) + fis, err := ioutil.ReadDir(currPath) + if err != nil { + return errors.Wrapf(err, "Failed to list contents of directory %s", currPath) + } + for _, fi := range fis { + if !fi.IsDir() && strings.HasSuffix(fi.Name(), ".go") { + currFile := path.Join(currPath, fi.Name()) + if err := projectImportInfo.AddImportAliasesFromFile(currFile); err != nil { + return errors.Wrapf(err, "failed to determine imports in file %s", currFile) + } + } + } + } + + importsToAliases := projectImportInfo.ImportsToAliases() + var pkgsWithMultipleAliases []string + pkgsWithMultipleAliasesMap := make(map[string]struct{}) + for k, v := range importsToAliases { + if len(v) > 1 { + // package is imported using more than 1 alias + pkgsWithMultipleAliases = append(pkgsWithMultipleAliases, k) + pkgsWithMultipleAliasesMap[k] = struct{}{} + } + } + sort.Strings(pkgsWithMultipleAliases) + if len(pkgsWithMultipleAliases) > 0 { + var output []string + if verbose { + for _, k := range pkgsWithMultipleAliases { + output = append(output, fmt.Sprintf("%s is imported using multiple different aliases:", k)) + for _, currAliasInfo := range importsToAliases[k] { + var files []string + for k, v := range currAliasInfo.Occurrences { + relPkgPath, err := pkgpath.NewAbsPkgPath(k).Rel(projectDir) + if err != nil { + return errors.Wrapf(err, "failed to get package path") + } + relPkgPath = strings.TrimLeft(relPkgPath, "./") + files = append(files, fmt.Sprintf("%s:%d:%d", relPkgPath, v.Line, v.Column)) + } + sort.Strings(files) + + var numFilesMsg string + if len(currAliasInfo.Occurrences) == 1 { + numFilesMsg = "(1 file)" + } else { + numFilesMsg = fmt.Sprintf("(%d files)", len(currAliasInfo.Occurrences)) + } + output = append(output, fmt.Sprintf("\t%s %s:\n\t\t%s", currAliasInfo.Alias, numFilesMsg, strings.Join(files, "\n\t\t"))) + } + } + } else { + filesToAliases := projectImportInfo.FilesToImportAliases() + + var relPkgPaths []string + relPkgPathToFile := make(map[string]string) + for file := range filesToAliases { + relPkgPath, err := pkgpath.NewAbsPkgPath(file).GoPathSrcRel() + if err != nil { + return errors.Wrapf(err, "failed to get package path") + } + relPkgPaths = append(relPkgPaths, relPkgPath) + relPkgPathToFile[relPkgPath] = file + } + sort.Strings(relPkgPaths) + + for _, relPkgPath := range relPkgPaths { + file := relPkgPathToFile[relPkgPath] + for _, alias := range filesToAliases[file] { + if _, ok := pkgsWithMultipleAliasesMap[alias.ImportPath]; !ok { + continue + } + status := projectImportInfo.GetAliasStatus(alias.Alias, alias.ImportPath) + if status.OK { + continue + } + + relPkgPath, err := pkgpath.NewAbsPkgPath(file).Rel(projectDir) + if err != nil { + return errors.Wrapf(err, "failed to get package path") + } + relPkgPath = strings.TrimLeft(relPkgPath, "./") + msg := fmt.Sprintf("%s:%d:%d: uses alias %q to import package %s. %s.", relPkgPath, alias.Pos.Line, alias.Pos.Column, alias.Alias, alias.ImportPath, status.Recommendation) + output = append(output, msg) + } + } + } + return errors.New(strings.Join(output, "\n")) + } + return nil +} diff --git a/apps/okgo/generated_src/internal/github.com/palantir/checks/importalias/importaliasinfo.go b/apps/okgo/generated_src/internal/github.com/palantir/checks/importalias/importaliasinfo.go new file mode 100644 index 00000000..5b2f61db --- /dev/null +++ b/apps/okgo/generated_src/internal/github.com/palantir/checks/importalias/importaliasinfo.go @@ -0,0 +1,223 @@ +// Copyright 2016 Palantir Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package amalgomated + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "io/ioutil" + "sort" + "strings" +) + +type ImportAliasInfo struct { + ImportPath string + Alias string + // file -> line information for import in the file + Occurrences map[string]token.Position +} + +type ImportAlias struct { + ImportPath string + Alias string + Pos token.Position +} + +type projectImportAliasInfo struct { + importInfos map[string]map[string]ImportAliasInfo +} + +type ProjectImportInfo interface { + // AddImportAliasesFromFile adds all of the import alias information from the given file. + AddImportAliasesFromFile(filename string) error + + // ImportsWithMultipleAliases returns a map from an imported package path to all of the aliases to import the package. + // The aliases are sorted by the number of uses of that alias. + ImportsToAliases() map[string][]ImportAliasInfo + + // FilesToImportAliases returns a map from each file in the project to all of the alias imports in the file. + FilesToImportAliases() map[string][]ImportAlias + + // GetAliasStatus returns the AliasStatus for the given alias used to import the package with the provided path. + GetAliasStatus(alias, importPath string) AliasStatus +} + +type AliasStatus struct { + // true if this alias is the only alias used for a package or is the most common alias used for a package. + OK bool + // recommendation for how to fix the issue if OK is false. + Recommendation string +} + +func NewProjectImportInfo() ProjectImportInfo { + return &projectImportAliasInfo{ + importInfos: make(map[string]map[string]ImportAliasInfo), + } +} + +func (p *projectImportAliasInfo) AddImportAliasesFromFile(filename string) error { + src, err := ioutil.ReadFile(filename) + if err != nil { + return err + } + + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, filename, src, parser.ParseComments) + if err != nil { + return fmt.Errorf("failed to parse file %s: %v", filename, err) + } + + var visitor visitFn + visitor = visitFn(func(node ast.Node) ast.Visitor { + if node == nil { + return visitor + } + switch v := node.(type) { + case *ast.ImportSpec: + if v.Name != nil && v.Name.Name != "." && v.Name.Name != "_" { + + p.addImportAlias(filename, v.Name.Name, v.Path.Value, fset.Position(v.Pos())) + break + } + } + return visitor + }) + ast.Walk(visitor, file) + return nil +} + +type visitFn func(node ast.Node) ast.Visitor + +func (fn visitFn) Visit(node ast.Node) ast.Visitor { + return fn(node) +} + +func (p *projectImportAliasInfo) addImportAlias(file, alias, importPath string, pos token.Position) { + if _, ok := p.importInfos[importPath]; !ok { + p.importInfos[importPath] = make(map[string]ImportAliasInfo) + } + if _, ok := p.importInfos[importPath][alias]; !ok { + p.importInfos[importPath][alias] = ImportAliasInfo{ + ImportPath: importPath, + Alias: alias, + Occurrences: make(map[string]token.Position), + } + } + p.importInfos[importPath][alias].Occurrences[file] = pos +} + +func (p *projectImportAliasInfo) ImportsToAliases() map[string][]ImportAliasInfo { + m := make(map[string][]ImportAliasInfo) + for importPath, aliases := range p.importInfos { + for _, aliasInfo := range aliases { + m[importPath] = append(m[importPath], aliasInfo) + } + } + for _, v := range m { + sort.Sort(byNumOccurrencesDesc(v)) + } + return m +} + +func (p *projectImportAliasInfo) FilesToImportAliases() map[string][]ImportAlias { + m := make(map[string][]ImportAlias) + for importPath, aliases := range p.importInfos { + for _, currAliasInfo := range aliases { + for file, pos := range currAliasInfo.Occurrences { + m[file] = append(m[file], ImportAlias{ + ImportPath: importPath, + Alias: currAliasInfo.Alias, + Pos: pos, + }) + } + } + } + for _, v := range m { + sort.Sort(byPos(v)) + } + return m +} + +func (p *projectImportAliasInfo) GetAliasStatus(alias, importPath string) AliasStatus { + importsToAliases := p.ImportsToAliases() + if aliases, ok := importsToAliases[importPath]; ok && len(aliases) > 1 { + var mostCommonAliases []string + for _, currAlias := range aliases { + if len(currAlias.Occurrences) != len(aliases[0].Occurrences) { + break + } + mostCommonAliases = append(mostCommonAliases, currAlias.Alias) + } + switch { + case len(mostCommonAliases) > 1: + var aliasesUsed string + if len(mostCommonAliases) == 2 { + aliasesUsed = fmt.Sprintf("%q and %q are both", mostCommonAliases[0], mostCommonAliases[1]) + } else { + var quoted []string + for _, curr := range mostCommonAliases { + quoted = append(quoted, fmt.Sprintf("%q", curr)) + } + aliasesUsed = strings.Join(quoted[:len(quoted)-1], ", ") + aliasesUsed += " and " + quoted[len(quoted)-1] + " are all" + } + + var timesUsed string + if len(aliases[0].Occurrences) == 1 { + timesUsed = "once" + } else { + timesUsed = fmt.Sprintf("%d times", len(aliases[0].Occurrences)) + } + + // there is not a single most common alias + return AliasStatus{ + OK: false, + Recommendation: fmt.Sprintf("No consensus alias exists for this import in the project (%s used %s each)", aliasesUsed, timesUsed), + } + case alias != mostCommonAliases[0]: + // this is not the most common alias + return AliasStatus{ + OK: false, + Recommendation: fmt.Sprintf("Use alias %q instead", mostCommonAliases[0]), + } + } + } + return AliasStatus{ + OK: true, + } +} + +type byNumOccurrencesDesc []ImportAliasInfo + +func (a byNumOccurrencesDesc) Len() int { return len(a) } +func (a byNumOccurrencesDesc) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a byNumOccurrencesDesc) Less(i, j int) bool { + if len(a[i].Occurrences) == len(a[j].Occurrences) { + // if number of occurrences are the same, do secondary sort based on name of alias + return strings.Compare(a[i].Alias, a[j].Alias) < 0 + } + // sort occurrences by descending order + return len(a[i].Occurrences) > len(a[j].Occurrences) +} + +type byPos []ImportAlias + +func (a byPos) Len() int { return len(a) } +func (a byPos) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a byPos) Less(i, j int) bool { + return a[i].Pos.Line < a[j].Pos.Line +} diff --git a/apps/okgo/integration_test/integration_test.go b/apps/okgo/integration_test/integration_test.go index 30f4f553..6acdd219 100644 --- a/apps/okgo/integration_test/integration_test.go +++ b/apps/okgo/integration_test/integration_test.go @@ -74,6 +74,13 @@ func TestCheckers(t *testing.T) { "pkg2/bad2.go:23: self-assignment of foo to foo", }, }, + { + check: cmdlib.Instance().MustNewCmd("importalias"), + want: []string{ + `pkg1/bad.go:3:8: uses alias "myjson" to import package "encoding/json". No consensus alias exists for this import in the project ("ejson" and "myjson" are both used once each).`, + `pkg2/bad2.go:3:8: uses alias "ejson" to import package "encoding/json". No consensus alias exists for this import in the project ("ejson" and "myjson" are both used once each).`, + }, + }, { check: cmdlib.Instance().MustNewCmd("ineffassign"), want: []string{ @@ -86,8 +93,8 @@ func TestCheckers(t *testing.T) { { check: cmdlib.Instance().MustNewCmd("outparamcheck"), want: []string{ - `github.com/palantir/godel/apps/okgo/integration_test/testdata/standard/pkg1/bad.go:16:26: _ = json.Unmarshal(nil, "") // 2nd argument of 'Unmarshal' requires '&'`, - `github.com/palantir/godel/apps/okgo/integration_test/testdata/standard/pkg2/bad2.go:16:26: _ = json.Unmarshal(nil, "") // 2nd argument of 'Unmarshal' requires '&'`, + `github.com/palantir/godel/apps/okgo/integration_test/testdata/standard/pkg1/bad.go:16:28: _ = myjson.Unmarshal(nil, "") // 2nd argument of 'Unmarshal' requires '&'`, + `github.com/palantir/godel/apps/okgo/integration_test/testdata/standard/pkg2/bad2.go:16:27: _ = ejson.Unmarshal(nil, "") // 2nd argument of 'Unmarshal' requires '&'`, }, }, { diff --git a/apps/okgo/integration_test/testdata/standard/pkg1/bad.go b/apps/okgo/integration_test/testdata/standard/pkg1/bad.go index 21feb2eb..237a8f43 100644 --- a/apps/okgo/integration_test/testdata/standard/pkg1/bad.go +++ b/apps/okgo/integration_test/testdata/standard/pkg1/bad.go @@ -1,6 +1,6 @@ package pkg1 -import "encoding/json" +import myjson "encoding/json" // Errcheck function. func Errcheck() { @@ -13,7 +13,7 @@ func Errcheck() { // Outparamcheck function. func Outparamcheck() { - _ = json.Unmarshal(nil, "") + _ = myjson.Unmarshal(nil, "") } // Vet function. diff --git a/apps/okgo/integration_test/testdata/standard/pkg2/bad2.go b/apps/okgo/integration_test/testdata/standard/pkg2/bad2.go index 217ae09a..d80eb535 100644 --- a/apps/okgo/integration_test/testdata/standard/pkg2/bad2.go +++ b/apps/okgo/integration_test/testdata/standard/pkg2/bad2.go @@ -1,6 +1,6 @@ package pkg2 -import "encoding/json" +import ejson "encoding/json" // Errcheck function. func Errcheck() { @@ -13,7 +13,7 @@ func Errcheck() { // Outparamcheck function. func Outparamcheck() { - _ = json.Unmarshal(nil, "") + _ = ejson.Unmarshal(nil, "") } // Vet function. diff --git a/vendor/github.com/palantir/checks/extimport/extimport.go b/vendor/github.com/palantir/checks/extimport/extimport.go index aaca26d7..15fb01be 100644 --- a/vendor/github.com/palantir/checks/extimport/extimport.go +++ b/vendor/github.com/palantir/checks/extimport/extimport.go @@ -42,7 +42,7 @@ const ( var ( pkgsFlag = flag.StringSlice{ Name: pkgsFlagName, - Usage: "paths to the pacakges to check", + Usage: "paths to the packages to check", } listFlag = flag.BoolFlag{ Name: listFlagName, diff --git a/vendor/github.com/palantir/checks/importalias/importalias.go b/vendor/github.com/palantir/checks/importalias/importalias.go new file mode 100644 index 00000000..2f8061c8 --- /dev/null +++ b/vendor/github.com/palantir/checks/importalias/importalias.go @@ -0,0 +1,188 @@ +// Copyright 2016 Palantir Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "path" + "path/filepath" + "sort" + "strings" + + "github.com/nmiyake/pkg/dirs" + "github.com/nmiyake/pkg/errorstringer" + "github.com/palantir/pkg/cli" + "github.com/palantir/pkg/cli/flag" + "github.com/palantir/pkg/pkgpath" + "github.com/pkg/errors" +) + +const ( + pkgsFlagName = "pkgs" + verboseFlagName = "verbose" +) + +var ( + pkgsFlag = flag.StringSlice{ + Name: pkgsFlagName, + Usage: "paths to the packages to check", + Optional: true, + } + verboseFlag = flag.BoolFlag{ + Name: verboseFlagName, + Usage: "print verbose analysis of all imports that have multiple aliases", + Alias: "v", + } +) + +func main() { + app := cli.NewApp(cli.DebugHandler(errorstringer.SingleStack)) + app.Flags = append(app.Flags, + pkgsFlag, + verboseFlag, + ) + app.Action = func(ctx cli.Context) error { + wd, err := dirs.GetwdEvalSymLinks() + if err != nil { + return errors.Wrapf(err, "Failed to get working directory") + } + return doImportAlias(wd, ctx.Slice(pkgsFlagName), ctx.Bool(verboseFlagName), ctx.App.Stdout) + } + os.Exit(app.Run(os.Args)) +} + +func doImportAlias(projectDir string, pkgPaths []string, verbose bool, w io.Writer) error { + if !path.IsAbs(projectDir) { + return errors.Errorf("projectDir %s must be an absolute path", projectDir) + } + + gopath := os.Getenv("GOPATH") + if gopath == "" { + return errors.Errorf("GOPATH environment variable must be set") + } + + if relPath, err := filepath.Rel(path.Join(gopath, "src"), projectDir); err != nil || strings.HasPrefix(relPath, "../") { + return errors.Wrapf(err, "Project directory %s must be a subdirectory of $GOPATH/src (%s)", projectDir, path.Join(gopath, "src")) + } + + if len(pkgPaths) == 0 { + pkgs, err := pkgpath.PackagesInDir(projectDir, pkgpath.DefaultGoPkgExcludeMatcher()) + if err != nil { + return errors.Wrapf(err, "Failed to list packages") + } + + pkgPaths, err = pkgs.Paths(pkgpath.Relative) + if err != nil { + return errors.Wrapf(err, "Failed to convert package paths") + } + } + + projectImportInfo := NewProjectImportInfo() + for _, pkgPath := range pkgPaths { + currPath := path.Join(projectDir, pkgPath) + fis, err := ioutil.ReadDir(currPath) + if err != nil { + return errors.Wrapf(err, "Failed to list contents of directory %s", currPath) + } + for _, fi := range fis { + if !fi.IsDir() && strings.HasSuffix(fi.Name(), ".go") { + currFile := path.Join(currPath, fi.Name()) + if err := projectImportInfo.AddImportAliasesFromFile(currFile); err != nil { + return errors.Wrapf(err, "failed to determine imports in file %s", currFile) + } + } + } + } + + importsToAliases := projectImportInfo.ImportsToAliases() + var pkgsWithMultipleAliases []string + pkgsWithMultipleAliasesMap := make(map[string]struct{}) + for k, v := range importsToAliases { + if len(v) > 1 { + // package is imported using more than 1 alias + pkgsWithMultipleAliases = append(pkgsWithMultipleAliases, k) + pkgsWithMultipleAliasesMap[k] = struct{}{} + } + } + sort.Strings(pkgsWithMultipleAliases) + if len(pkgsWithMultipleAliases) > 0 { + var output []string + if verbose { + for _, k := range pkgsWithMultipleAliases { + output = append(output, fmt.Sprintf("%s is imported using multiple different aliases:", k)) + for _, currAliasInfo := range importsToAliases[k] { + var files []string + for k, v := range currAliasInfo.Occurrences { + relPkgPath, err := pkgpath.NewAbsPkgPath(k).Rel(projectDir) + if err != nil { + return errors.Wrapf(err, "failed to get package path") + } + relPkgPath = strings.TrimLeft(relPkgPath, "./") + files = append(files, fmt.Sprintf("%s:%d:%d", relPkgPath, v.Line, v.Column)) + } + sort.Strings(files) + + var numFilesMsg string + if len(currAliasInfo.Occurrences) == 1 { + numFilesMsg = "(1 file)" + } else { + numFilesMsg = fmt.Sprintf("(%d files)", len(currAliasInfo.Occurrences)) + } + output = append(output, fmt.Sprintf("\t%s %s:\n\t\t%s", currAliasInfo.Alias, numFilesMsg, strings.Join(files, "\n\t\t"))) + } + } + } else { + filesToAliases := projectImportInfo.FilesToImportAliases() + + var relPkgPaths []string + relPkgPathToFile := make(map[string]string) + for file := range filesToAliases { + relPkgPath, err := pkgpath.NewAbsPkgPath(file).GoPathSrcRel() + if err != nil { + return errors.Wrapf(err, "failed to get package path") + } + relPkgPaths = append(relPkgPaths, relPkgPath) + relPkgPathToFile[relPkgPath] = file + } + sort.Strings(relPkgPaths) + + for _, relPkgPath := range relPkgPaths { + file := relPkgPathToFile[relPkgPath] + for _, alias := range filesToAliases[file] { + if _, ok := pkgsWithMultipleAliasesMap[alias.ImportPath]; !ok { + continue + } + status := projectImportInfo.GetAliasStatus(alias.Alias, alias.ImportPath) + if status.OK { + continue + } + + relPkgPath, err := pkgpath.NewAbsPkgPath(file).Rel(projectDir) + if err != nil { + return errors.Wrapf(err, "failed to get package path") + } + relPkgPath = strings.TrimLeft(relPkgPath, "./") + msg := fmt.Sprintf("%s:%d:%d: uses alias %q to import package %s. %s.", relPkgPath, alias.Pos.Line, alias.Pos.Column, alias.Alias, alias.ImportPath, status.Recommendation) + output = append(output, msg) + } + } + } + return errors.New(strings.Join(output, "\n")) + } + return nil +} diff --git a/vendor/github.com/palantir/checks/importalias/importaliasinfo.go b/vendor/github.com/palantir/checks/importalias/importaliasinfo.go new file mode 100644 index 00000000..ec4bacc3 --- /dev/null +++ b/vendor/github.com/palantir/checks/importalias/importaliasinfo.go @@ -0,0 +1,224 @@ +// Copyright 2016 Palantir Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "io/ioutil" + "sort" + "strings" +) + +type ImportAliasInfo struct { + ImportPath string + Alias string + // file -> line information for import in the file + Occurrences map[string]token.Position +} + +type ImportAlias struct { + ImportPath string + Alias string + Pos token.Position +} + +type projectImportAliasInfo struct { + // import path -> alias -> all aliases for the import + importInfos map[string]map[string]ImportAliasInfo +} + +type ProjectImportInfo interface { + // AddImportAliasesFromFile adds all of the import alias information from the given file. + AddImportAliasesFromFile(filename string) error + + // ImportsWithMultipleAliases returns a map from an imported package path to all of the aliases to import the package. + // The aliases are sorted by the number of uses of that alias. + ImportsToAliases() map[string][]ImportAliasInfo + + // FilesToImportAliases returns a map from each file in the project to all of the alias imports in the file. + FilesToImportAliases() map[string][]ImportAlias + + // GetAliasStatus returns the AliasStatus for the given alias used to import the package with the provided path. + GetAliasStatus(alias, importPath string) AliasStatus +} + +type AliasStatus struct { + // true if this alias is the only alias used for a package or is the most common alias used for a package. + OK bool + // recommendation for how to fix the issue if OK is false. + Recommendation string +} + +func NewProjectImportInfo() ProjectImportInfo { + return &projectImportAliasInfo{ + importInfos: make(map[string]map[string]ImportAliasInfo), + } +} + +func (p *projectImportAliasInfo) AddImportAliasesFromFile(filename string) error { + src, err := ioutil.ReadFile(filename) + if err != nil { + return err + } + + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, filename, src, parser.ParseComments) + if err != nil { + return fmt.Errorf("failed to parse file %s: %v", filename, err) + } + + var visitor visitFn + visitor = visitFn(func(node ast.Node) ast.Visitor { + if node == nil { + return visitor + } + switch v := node.(type) { + case *ast.ImportSpec: + if v.Name != nil && v.Name.Name != "." && v.Name.Name != "_" { + // import has alias: record + p.addImportAlias(filename, v.Name.Name, v.Path.Value, fset.Position(v.Pos())) + break + } + } + return visitor + }) + ast.Walk(visitor, file) + return nil +} + +type visitFn func(node ast.Node) ast.Visitor + +func (fn visitFn) Visit(node ast.Node) ast.Visitor { + return fn(node) +} + +func (p *projectImportAliasInfo) addImportAlias(file, alias, importPath string, pos token.Position) { + if _, ok := p.importInfos[importPath]; !ok { + p.importInfos[importPath] = make(map[string]ImportAliasInfo) + } + if _, ok := p.importInfos[importPath][alias]; !ok { + p.importInfos[importPath][alias] = ImportAliasInfo{ + ImportPath: importPath, + Alias: alias, + Occurrences: make(map[string]token.Position), + } + } + p.importInfos[importPath][alias].Occurrences[file] = pos +} + +func (p *projectImportAliasInfo) ImportsToAliases() map[string][]ImportAliasInfo { + m := make(map[string][]ImportAliasInfo) + for importPath, aliases := range p.importInfos { + for _, aliasInfo := range aliases { + m[importPath] = append(m[importPath], aliasInfo) + } + } + for _, v := range m { + sort.Sort(byNumOccurrencesDesc(v)) + } + return m +} + +func (p *projectImportAliasInfo) FilesToImportAliases() map[string][]ImportAlias { + m := make(map[string][]ImportAlias) + for importPath, aliases := range p.importInfos { + for _, currAliasInfo := range aliases { + for file, pos := range currAliasInfo.Occurrences { + m[file] = append(m[file], ImportAlias{ + ImportPath: importPath, + Alias: currAliasInfo.Alias, + Pos: pos, + }) + } + } + } + for _, v := range m { + sort.Sort(byPos(v)) + } + return m +} + +func (p *projectImportAliasInfo) GetAliasStatus(alias, importPath string) AliasStatus { + importsToAliases := p.ImportsToAliases() + if aliases, ok := importsToAliases[importPath]; ok && len(aliases) > 1 { + var mostCommonAliases []string + for _, currAlias := range aliases { + if len(currAlias.Occurrences) != len(aliases[0].Occurrences) { + break + } + mostCommonAliases = append(mostCommonAliases, currAlias.Alias) + } + switch { + case len(mostCommonAliases) > 1: + var aliasesUsed string + if len(mostCommonAliases) == 2 { + aliasesUsed = fmt.Sprintf("%q and %q are both", mostCommonAliases[0], mostCommonAliases[1]) + } else { + var quoted []string + for _, curr := range mostCommonAliases { + quoted = append(quoted, fmt.Sprintf("%q", curr)) + } + aliasesUsed = strings.Join(quoted[:len(quoted)-1], ", ") + aliasesUsed += " and " + quoted[len(quoted)-1] + " are all" + } + + var timesUsed string + if len(aliases[0].Occurrences) == 1 { + timesUsed = "once" + } else { + timesUsed = fmt.Sprintf("%d times", len(aliases[0].Occurrences)) + } + + // there is not a single most common alias + return AliasStatus{ + OK: false, + Recommendation: fmt.Sprintf("No consensus alias exists for this import in the project (%s used %s each)", aliasesUsed, timesUsed), + } + case alias != mostCommonAliases[0]: + // this is not the most common alias + return AliasStatus{ + OK: false, + Recommendation: fmt.Sprintf("Use alias %q instead", mostCommonAliases[0]), + } + } + } + return AliasStatus{ + OK: true, + } +} + +type byNumOccurrencesDesc []ImportAliasInfo + +func (a byNumOccurrencesDesc) Len() int { return len(a) } +func (a byNumOccurrencesDesc) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a byNumOccurrencesDesc) Less(i, j int) bool { + if len(a[i].Occurrences) == len(a[j].Occurrences) { + // if number of occurrences are the same, do secondary sort based on name of alias + return strings.Compare(a[i].Alias, a[j].Alias) < 0 + } + // sort occurrences by descending order + return len(a[i].Occurrences) > len(a[j].Occurrences) +} + +type byPos []ImportAlias + +func (a byPos) Len() int { return len(a) } +func (a byPos) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a byPos) Less(i, j int) bool { + return a[i].Pos.Line < a[j].Pos.Line +} diff --git a/vendor/vendor.json b/vendor/vendor.json index cb5be7c7..36a79a92 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -193,98 +193,104 @@ { "checksumSHA1": "xv/NocKXoHIcV3oEn4CD54+BuCE=", "path": "github.com/palantir/checks/compiles", - "revision": "82f0aa32d4718aaebd9a96b9235be17ddbdb6b58", - "revisionTime": "2016-12-15T00:25:33Z" + "revision": "10da8023aca66638ce70ae91d94e565b802f759d", + "revisionTime": "2017-02-05T21:14:14Z" }, { - "checksumSHA1": "eZTii1hynAYJp1Q7m5wNoxqXxZE=", + "checksumSHA1": "+W4pDqs5jj+pxNdjaI9pMqtqlSA=", "path": "github.com/palantir/checks/extimport", - "revision": "82f0aa32d4718aaebd9a96b9235be17ddbdb6b58", - "revisionTime": "2016-12-15T00:25:33Z" + "revision": "10da8023aca66638ce70ae91d94e565b802f759d", + "revisionTime": "2017-02-05T21:14:14Z" }, { "checksumSHA1": "1r3QmWFyIFamo75wMIWAdMjQ7L0=", "path": "github.com/palantir/checks/gocd/cmd", - "revision": "82f0aa32d4718aaebd9a96b9235be17ddbdb6b58", - "revisionTime": "2016-12-15T00:25:33Z" + "revision": "10da8023aca66638ce70ae91d94e565b802f759d", + "revisionTime": "2017-02-05T21:14:14Z" }, { "checksumSHA1": "4RPr1kGPVZvuE492gPw7CRXnJKQ=", "path": "github.com/palantir/checks/gocd/cmd/gocd", - "revision": "82f0aa32d4718aaebd9a96b9235be17ddbdb6b58", - "revisionTime": "2016-12-15T00:25:33Z" + "revision": "10da8023aca66638ce70ae91d94e565b802f759d", + "revisionTime": "2017-02-05T21:14:14Z" }, { "checksumSHA1": "kyaKgoYMv9BAyQSVVuOlHc5nHBI=", "path": "github.com/palantir/checks/gocd/config", - "revision": "82f0aa32d4718aaebd9a96b9235be17ddbdb6b58", - "revisionTime": "2016-12-15T00:25:33Z" + "revision": "10da8023aca66638ce70ae91d94e565b802f759d", + "revisionTime": "2017-02-05T21:14:14Z" }, { "checksumSHA1": "XRLjye5JRGKZ/bQOdqW27H4HdPE=", "path": "github.com/palantir/checks/gocd/gocd", - "revision": "82f0aa32d4718aaebd9a96b9235be17ddbdb6b58", - "revisionTime": "2016-12-15T00:25:33Z" + "revision": "10da8023aca66638ce70ae91d94e565b802f759d", + "revisionTime": "2017-02-05T21:14:14Z" }, { "checksumSHA1": "CkZ462LKZ0w0bC41A2y9E0CObuI=", "path": "github.com/palantir/checks/golicense/cmd", - "revision": "82f0aa32d4718aaebd9a96b9235be17ddbdb6b58", - "revisionTime": "2016-12-15T00:25:33Z" + "revision": "10da8023aca66638ce70ae91d94e565b802f759d", + "revisionTime": "2017-02-05T21:14:14Z" }, { "checksumSHA1": "6ahBfVKtEOJCMdVsazwEZqfm2f8=", "path": "github.com/palantir/checks/golicense/cmd/golicense", - "revision": "82f0aa32d4718aaebd9a96b9235be17ddbdb6b58", - "revisionTime": "2016-12-15T00:25:33Z" + "revision": "10da8023aca66638ce70ae91d94e565b802f759d", + "revisionTime": "2017-02-05T21:14:14Z" }, { "checksumSHA1": "wOHJVm9MVhp8LhmW21gGNw9hTjg=", "path": "github.com/palantir/checks/golicense/config", - "revision": "82f0aa32d4718aaebd9a96b9235be17ddbdb6b58", - "revisionTime": "2016-12-15T00:25:33Z" + "revision": "10da8023aca66638ce70ae91d94e565b802f759d", + "revisionTime": "2017-02-05T21:14:14Z" }, { "checksumSHA1": "0zZSJ5hzQeXFJNhc0SzhpFzSk6o=", "path": "github.com/palantir/checks/golicense/golicense", - "revision": "82f0aa32d4718aaebd9a96b9235be17ddbdb6b58", - "revisionTime": "2016-12-15T00:25:33Z" + "revision": "10da8023aca66638ce70ae91d94e565b802f759d", + "revisionTime": "2017-02-05T21:14:14Z" + }, + { + "checksumSHA1": "E1Tg4QpAvzZHfXY9htAVhNm9lno=", + "path": "github.com/palantir/checks/importalias", + "revision": "10da8023aca66638ce70ae91d94e565b802f759d", + "revisionTime": "2017-02-05T21:14:14Z" }, { "checksumSHA1": "1WiAS7qLQ4Lf41pHDBJZ5AcxyLQ=", "path": "github.com/palantir/checks/novendor", - "revision": "82f0aa32d4718aaebd9a96b9235be17ddbdb6b58", - "revisionTime": "2016-12-15T00:25:33Z" + "revision": "10da8023aca66638ce70ae91d94e565b802f759d", + "revisionTime": "2017-02-05T21:14:14Z" }, { "checksumSHA1": "C7LiTOVSBxQMEt+YH1yHVt3VXug=", "path": "github.com/palantir/checks/outparamcheck", - "revision": "82f0aa32d4718aaebd9a96b9235be17ddbdb6b58", - "revisionTime": "2016-12-15T00:25:33Z" + "revision": "10da8023aca66638ce70ae91d94e565b802f759d", + "revisionTime": "2017-02-05T21:14:14Z" }, { "checksumSHA1": "IzXhbgz+1UBh9+7w9J+nWYbguVo=", "path": "github.com/palantir/checks/outparamcheck/exprs", - "revision": "82f0aa32d4718aaebd9a96b9235be17ddbdb6b58", - "revisionTime": "2016-12-15T00:25:33Z" + "revision": "10da8023aca66638ce70ae91d94e565b802f759d", + "revisionTime": "2017-02-05T21:14:14Z" }, { "checksumSHA1": "7v1AuQWvLeij4+YRVCRqRfoIkCE=", "path": "github.com/palantir/checks/outparamcheck/outparamcheck", - "revision": "82f0aa32d4718aaebd9a96b9235be17ddbdb6b58", - "revisionTime": "2016-12-15T00:25:33Z" + "revision": "10da8023aca66638ce70ae91d94e565b802f759d", + "revisionTime": "2017-02-05T21:14:14Z" }, { "checksumSHA1": "UApv/jY/w3k0I8UBpp8ALwvr+ZY=", "path": "github.com/palantir/checks/ptimports", - "revision": "82f0aa32d4718aaebd9a96b9235be17ddbdb6b58", - "revisionTime": "2016-12-15T00:25:33Z" + "revision": "10da8023aca66638ce70ae91d94e565b802f759d", + "revisionTime": "2017-02-05T21:14:14Z" }, { "checksumSHA1": "ceNbjVr1hzaH2t/XUzp2LPFOsOo=", "path": "github.com/palantir/checks/ptimports/ptimports", - "revision": "82f0aa32d4718aaebd9a96b9235be17ddbdb6b58", - "revisionTime": "2016-12-15T00:25:33Z" + "revision": "10da8023aca66638ce70ae91d94e565b802f759d", + "revisionTime": "2017-02-05T21:14:14Z" }, { "checksumSHA1": "csaM9q2Qqj12DpxMSXS3V+a5qdY=",