diff --git a/cmd/stringer/endtoend_test.go b/cmd/stringer/endtoend_test.go index f8530a6cbd..c6a2b0008e 100644 --- a/cmd/stringer/endtoend_test.go +++ b/cmd/stringer/endtoend_test.go @@ -75,7 +75,12 @@ func TestTags(t *testing.T) { } } - err := run(stringer, "-type", "Const", dir) + // Run stringer in the directory that contains the package files. + // We cannot run stringer in the current directory for the following reasons: + // - Versions of Go earlier than Go 1.11, do not support absolute directories as a pattern. + // - When the current directory is inside a go module, the path will not be considered + // a valid path to a package. + err := runInDir(dir, stringer, "-type", "Const", ".") if err != nil { t.Fatal(err) } @@ -90,7 +95,7 @@ func TestTags(t *testing.T) { if err != nil { t.Fatal(err) } - err = run(stringer, "-type", "Const", "-tags", "tag", dir) + err = runInDir(dir, stringer, "-type", "Const", "-tags", "tag", ".") if err != nil { t.Fatal(err) } @@ -160,7 +165,14 @@ func copy(to, from string) error { // run runs a single command and returns an error if it does not succeed. // os/exec should have this function, to be honest. func run(name string, arg ...string) error { + return runInDir(".", name, arg...) +} + +// runInDir runs a single command in directory dir and returns an error if +// it does not succeed. +func runInDir(dir, name string, arg ...string) error { cmd := exec.Command(name, arg...) + cmd.Dir = dir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() diff --git a/cmd/stringer/golden_test.go b/cmd/stringer/golden_test.go index 4ba77881ce..fd8e79419c 100644 --- a/cmd/stringer/golden_test.go +++ b/cmd/stringer/golden_test.go @@ -10,6 +10,9 @@ package main import ( + "io/ioutil" + "os" + "path/filepath" "strings" "testing" ) @@ -297,6 +300,12 @@ func (i Token) String() string { ` func TestGolden(t *testing.T) { + dir, err := ioutil.TempDir("", "stringer") + if err != nil { + t.Error(err) + } + defer os.RemoveAll(dir) + for _, test := range golden { g := Generator{ trimPrefix: test.trimPrefix, @@ -304,7 +313,13 @@ func TestGolden(t *testing.T) { } input := "package test\n" + test.input file := test.name + ".go" - g.parsePackage(".", []string{file}, input) + absFile := filepath.Join(dir, file) + err := ioutil.WriteFile(absFile, []byte(input), 0644) + if err != nil { + t.Error(err) + } + + g.parsePackage([]string{absFile}, nil) // Extract the name and type of the constant from the first line. tokens := strings.SplitN(test.input, " ", 3) if len(tokens) != 3 { diff --git a/cmd/stringer/importer18.go b/cmd/stringer/importer18.go deleted file mode 100644 index fd21873cda..0000000000 --- a/cmd/stringer/importer18.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2017 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build !go1.9 - -package main - -import ( - "go/importer" - "go/types" -) - -func defaultImporter() types.Importer { - return importer.Default() -} diff --git a/cmd/stringer/importer19.go b/cmd/stringer/importer19.go deleted file mode 100644 index deddadb1f1..0000000000 --- a/cmd/stringer/importer19.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2017 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build go1.9 - -package main - -import ( - "go/importer" - "go/types" -) - -func defaultImporter() types.Importer { - return importer.For("source", nil) -} diff --git a/cmd/stringer/stringer.go b/cmd/stringer/stringer.go index 57d3a723f9..3d6f5d8350 100644 --- a/cmd/stringer/stringer.go +++ b/cmd/stringer/stringer.go @@ -63,10 +63,8 @@ import ( "flag" "fmt" "go/ast" - "go/build" "go/constant" "go/format" - "go/parser" "go/token" "go/types" "io/ioutil" @@ -75,6 +73,8 @@ import ( "path/filepath" "sort" "strings" + + "golang.org/x/tools/go/packages" ) var ( @@ -124,17 +124,18 @@ func main() { trimPrefix: *trimprefix, lineComment: *linecomment, } + // TODO(suzmue): accept other patterns for packages (directories, list of files, import paths, etc). if len(args) == 1 && isDirectory(args[0]) { dir = args[0] - g.parsePackageDir(args[0], tags) } else { if len(tags) != 0 { log.Fatal("-tags option applies only to directories, not when files are specified") } dir = filepath.Dir(args[0]) - g.parsePackageFiles(args) } + g.parsePackage(args, tags) + // Print the header and package clause. g.Printf("// Code generated by \"stringer %s\"; DO NOT EDIT.\n", strings.Join(os.Args[1:], " ")) g.Printf("\n") @@ -198,102 +199,47 @@ type File struct { } type Package struct { - dir string - name string - defs map[*ast.Ident]types.Object - files []*File - typesPkg *types.Package + name string + defs map[*ast.Ident]types.Object + files []*File } -func buildContext(tags []string) *build.Context { - ctx := build.Default - ctx.BuildTags = tags - return &ctx -} - -// parsePackageDir parses the package residing in the directory. -func (g *Generator) parsePackageDir(directory string, tags []string) { - pkg, err := buildContext(tags).ImportDir(directory, 0) +// parsePackage analyzes the single package constructed from the patterns and tags. +// parsePackage exits if there is an error. +func (g *Generator) parsePackage(patterns []string, tags []string) { + cfg := &packages.Config{ + Mode: packages.LoadSyntax, + // TODO: Need to think about constants in test files. Maybe write type_string_test.go + // in a separate pass? For later. + Tests: false, + BuildFlags: []string{fmt.Sprintf("-tags=%s", strings.Join(tags, " "))}, + } + pkgs, err := packages.Load(cfg, patterns...) if err != nil { - log.Fatalf("cannot process directory %s: %s", directory, err) + log.Fatal(err) } - var names []string - names = append(names, pkg.GoFiles...) - names = append(names, pkg.CgoFiles...) - // TODO: Need to think about constants in test files. Maybe write type_string_test.go - // in a separate pass? For later. - // names = append(names, pkg.TestGoFiles...) // These are also in the "foo" package. - names = append(names, pkg.SFiles...) - names = prefixDirectory(directory, names) - g.parsePackage(directory, names, nil) + if len(pkgs) != 1 { + log.Fatalf("error: %d packages found", len(pkgs)) + } + g.addPackage(pkgs[0]) } -// parsePackageFiles parses the package occupying the named files. -func (g *Generator) parsePackageFiles(names []string) { - g.parsePackage(".", names, nil) -} - -// prefixDirectory places the directory name on the beginning of each name in the list. -func prefixDirectory(directory string, names []string) []string { - if directory == "." { - return names +// addPackage adds a type checked Package and its syntax files to the generator. +func (g *Generator) addPackage(pkg *packages.Package) { + g.pkg = &Package{ + name: pkg.Name, + defs: pkg.TypesInfo.Defs, + files: make([]*File, len(pkg.Syntax)), } - ret := make([]string, len(names)) - for i, name := range names { - ret[i] = filepath.Join(directory, name) - } - return ret -} -// parsePackage analyzes the single package constructed from the named files. -// If text is non-nil, it is a string to be used instead of the content of the file, -// to be used for testing. parsePackage exits if there is an error. -func (g *Generator) parsePackage(directory string, names []string, text interface{}) { - var files []*File - var astFiles []*ast.File - g.pkg = new(Package) - fs := token.NewFileSet() - for _, name := range names { - if !strings.HasSuffix(name, ".go") { - continue - } - parsedFile, err := parser.ParseFile(fs, name, text, parser.ParseComments) - if err != nil { - log.Fatalf("parsing package: %s: %s", name, err) - } - astFiles = append(astFiles, parsedFile) - files = append(files, &File{ - file: parsedFile, + for i, file := range pkg.Syntax { + g.pkg.files[i] = &File{ + file: file, pkg: g.pkg, trimPrefix: g.trimPrefix, lineComment: g.lineComment, - }) + } } - if len(astFiles) == 0 { - log.Fatalf("%s: no buildable Go files", directory) - } - g.pkg.name = astFiles[0].Name.Name - g.pkg.files = files - g.pkg.dir = directory - g.pkg.typeCheck(fs, astFiles) -} - -// check type-checks the package so we can evaluate contants whose values we are printing. -func (pkg *Package) typeCheck(fs *token.FileSet, astFiles []*ast.File) { - pkg.defs = make(map[*ast.Ident]types.Object) - config := types.Config{ - IgnoreFuncBodies: true, // We only need to evaluate constants. - Importer: defaultImporter(), - FakeImportC: true, - } - info := &types.Info{ - Defs: pkg.defs, - } - typesPkg, err := config.Check(pkg.dir, fs, astFiles, info) - if err != nil { - log.Fatalf("checking package: %s", err) - } - pkg.typesPkg = typesPkg } // generate produces the String method for the named type.