]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/cover: preserve compiler directives in floating comments
authorJay Conrod <jayconrod@google.com>
Tue, 10 Oct 2017 18:41:57 +0000 (14:41 -0400)
committerRobert Griesemer <gri@golang.org>
Mon, 16 Oct 2017 23:38:38 +0000 (23:38 +0000)
Previously, cover printed directives (//go: comments) near the top of
the file unless they were in doc comments. However, directives
frequently apply to specific definitions, and they are not written in
doc comments to prevent godoc from printing them. Moving all
directives to the top of the file affected semantics of tests.

With this change, directives are kept together with the following
top-level declarations. Only directives that occur after all top-level
declarations are moved.

Fixes #22022

Change-Id: Ic5c61c4d3969996e4ed5abccba0989163789254c
Reviewed-on: https://go-review.googlesource.com/69630
Run-TryBot: Emmanuel Odeke <emm.odeke@gmail.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Robert Griesemer <gri@golang.org>
Reviewed-by: Alan Donovan <adonovan@google.com>
misc/nacl/testzip.proto
src/cmd/cover/cover.go
src/cmd/cover/cover_test.go
src/cmd/cover/doc.go
src/cmd/cover/testdata/directives.go [new file with mode: 0644]

index ab9abbf21e2b3037f6a27e0f1f6baebeebf6b8c5..f15a2ab224636afb3745bdecbd49167389be2767 100644 (file)
@@ -22,6 +22,9 @@ go    src=..
                                internal
                                        syntax
                                                parser.go
+                       cover
+                               testdata
+                                       +
                        doc
                                main.go
                                pkg.go
index 0d51a6ba30c3aa2ec1afbafe7272476f2b383e33..8bcdec17c8d9bf56093ce98214cbc3f5ca15ecc0 100644 (file)
@@ -154,12 +154,11 @@ type Block struct {
 // File is a wrapper for the state of a file used in the parser.
 // The basic parse tree walker is a method of this type.
 type File struct {
-       fset       *token.FileSet
-       name       string // Name of file.
-       astFile    *ast.File
-       blocks     []Block
-       atomicPkg  string                // Package name for "sync/atomic" in this file.
-       directives map[*ast.Comment]bool // Map of compiler directives to whether it's processed in ast.Visitor or not.
+       fset      *token.FileSet
+       name      string // Name of file.
+       astFile   *ast.File
+       blocks    []Block
+       atomicPkg string // Package name for "sync/atomic" in this file.
 }
 
 // Visit implements the ast.Visitor interface.
@@ -244,23 +243,74 @@ func (f *File) Visit(node ast.Node) ast.Visitor {
                        ast.Walk(f, n.Assign)
                        return nil
                }
-       case *ast.CommentGroup:
-               var list []*ast.Comment
-               // Drop all but the //go: comments, some of which are semantically important.
-               // We drop all others because they can appear in places that cause our counters
-               // to appear in syntactically incorrect places. //go: appears at the beginning of
-               // the line and is syntactically safe.
-               for _, c := range n.List {
-                       if f.isDirective(c) {
-                               list = append(list, c)
-
-                               // Mark compiler directive as handled.
-                               f.directives[c] = true
+       }
+       return f
+}
+
+// fixDirectives identifies //go: comments (known as directives or pragmas) and
+// attaches them to the documentation group of the following top-level
+// definitions. All other comments are dropped since non-documentation comments
+// tend to get printed in the wrong place after the AST is modified.
+//
+// fixDirectives returns a list of unhandled directives. These comments could
+// not be attached to a top-level declaration and should be be printed at the
+// end of the file.
+func (f *File) fixDirectives() []*ast.Comment {
+       // Scan comments in the file and collect directives. Detach all comments.
+       var directives []*ast.Comment
+       for _, cg := range f.astFile.Comments {
+               for _, c := range cg.List {
+                       // Skip directives that will be included by initialComments, i.e., those
+                       // before the package declaration but not in the file doc comment group.
+                       if f.isDirective(c) && (c.Pos() >= f.astFile.Package || cg == f.astFile.Doc) {
+                               directives = append(directives, c)
+                       }
+               }
+               cg.List = nil
+       }
+       f.astFile.Comments = nil // force printer to use node comments
+
+       // Iterate over top-level declarations and attach preceding directives.
+       di := 0
+       var prevPos token.Pos
+       for _, decl := range f.astFile.Decls {
+               // Assume (but verify) that comments are sorted by position.
+               pos := decl.Pos()
+               if pos < prevPos {
+                       log.Fatalf("comments are out of order. %s was before %s.",
+                               f.fset.Position(prevPos), f.fset.Position(pos))
+               }
+               prevPos = pos
+
+               var doc **ast.CommentGroup
+               switch d := decl.(type) {
+               case *ast.FuncDecl:
+                       doc = &d.Doc
+               case *ast.GenDecl:
+                       // Limitation: for grouped declarations, we attach directives to the decl,
+                       // not individual specs. Directives must start in the first column, so
+                       // they are lost when the group is indented.
+                       doc = &d.Doc
+               default:
+                       // *ast.BadDecls is the only other type we might see, but
+                       // we don't need to handle it here.
+                       continue
+               }
+
+               for di < len(directives) && directives[di].Pos() < pos {
+                       c := directives[di]
+                       if *doc == nil {
+                               *doc = new(ast.CommentGroup)
                        }
+                       c.Slash = pos - 1 // must be strictly less than pos
+                       (*doc).List = append((*doc).List, c)
+                       di++
                }
-               n.List = list
        }
-       return f
+
+       // Return trailing directives. These cannot apply to a specific declaration
+       // and may be printed at the end of the file.
+       return directives[di:]
 }
 
 // unquote returns the unquoted string.
@@ -369,25 +419,15 @@ func annotate(name string) {
        }
 
        file := &File{
-               fset:       fset,
-               name:       name,
-               astFile:    parsedFile,
-               directives: map[*ast.Comment]bool{},
+               fset:    fset,
+               name:    name,
+               astFile: parsedFile,
        }
        if *mode == "atomic" {
                file.atomicPkg = file.addImport(atomicPackagePath)
        }
 
-       for _, cg := range parsedFile.Comments {
-               for _, c := range cg.List {
-                       if file.isDirective(c) {
-                               file.directives[c] = false
-                       }
-               }
-       }
-       // Remove comments. Or else they interfere with new AST.
-       parsedFile.Comments = nil
-
+       unhandledDirectives := file.fixDirectives()
        ast.Walk(file, file.astFile)
        fd := os.Stdout
        if *output != "" {
@@ -399,20 +439,17 @@ func annotate(name string) {
        }
        fd.Write(initialComments(content)) // Retain '// +build' directives.
 
-       // Retain compiler directives that are not processed in ast.Visitor.
-       // Some compiler directives like "go:linkname" and "go:cgo_"
-       // can be not attached to anything in the tree and hence will not be printed by printer.
-       // So, we have to explicitly print them here.
-       for cd, handled := range file.directives {
-               if !handled {
-                       fmt.Fprintln(fd, cd.Text)
-               }
-       }
-
        file.print(fd)
        // After printing the source tree, add some declarations for the counters etc.
        // We could do this by adding to the tree, but it's easier just to print the text.
        file.addVariables(fd)
+
+       // Print directives that are not processed in fixDirectives. Some
+       // directives are not attached to anything in the tree hence will not
+       // be printed by printer. So, we have to explicitly print them here.
+       for _, c := range unhandledDirectives {
+               fmt.Fprintln(fd, c.Text)
+       }
 }
 
 func (f *File) print(w io.Writer) {
index 1584a73b591787456ebfd469e7c07e5a0a044f25..8c9acc93f6557a933318bd8569fec445a2e21423 100644 (file)
@@ -7,12 +7,16 @@ package main_test
 import (
        "bytes"
        "fmt"
+       "go/ast"
+       "go/parser"
+       "go/token"
        "internal/testenv"
        "io/ioutil"
        "os"
        "os/exec"
        "path/filepath"
        "regexp"
+       "strings"
        "testing"
 )
 
@@ -89,20 +93,138 @@ func TestCover(t *testing.T) {
        }
        // compiler directive must appear right next to function declaration.
        if got, err := regexp.MatchString(".*\n//go:nosplit\nfunc someFunction().*", string(file)); err != nil || !got {
-               t.Errorf("misplaced compiler directive: got=(%v, %v); want=(true; nil)", got, err)
+               t.Error("misplaced compiler directive")
        }
        // "go:linkname" compiler directive should be present.
        if got, err := regexp.MatchString(`.*go\:linkname some\_name some\_name.*`, string(file)); err != nil || !got {
-               t.Errorf("'go:linkname' compiler directive not found: got=(%v, %v); want=(true; nil)", got, err)
+               t.Error("'go:linkname' compiler directive not found")
        }
 
        // No other comments should be present in generated code.
        c := ".*// This comment shouldn't appear in generated go code.*"
        if got, err := regexp.MatchString(c, string(file)); err != nil || got {
-               t.Errorf("non compiler directive comment %q found. got=(%v, %v); want=(false; nil)", c, got, err)
+               t.Errorf("non compiler directive comment %q found", c)
        }
 }
 
+// TestDirectives checks that compiler directives are preserved and positioned
+// correctly. Directives that occur before top-level declarations should remain
+// above those declarations, even if they are not part of the block of
+// documentation comments.
+func TestDirectives(t *testing.T) {
+       // Read the source file and find all the directives. We'll keep
+       // track of whether each one has been seen in the output.
+       testDirectives := filepath.Join(testdata, "directives.go")
+       source, err := ioutil.ReadFile(testDirectives)
+       if err != nil {
+               t.Fatal(err)
+       }
+       sourceDirectives := findDirectives(source)
+
+       // go tool cover -mode=set ./testdata/directives.go
+       cmd := exec.Command(testenv.GoToolPath(t), "tool", "cover", "-mode=set", testDirectives)
+       cmd.Stderr = os.Stderr
+       output, err := cmd.Output()
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       // Check that all directives are present in the output.
+       outputDirectives := findDirectives(output)
+       foundDirective := make(map[string]bool)
+       for _, p := range sourceDirectives {
+               foundDirective[p.name] = false
+       }
+       for _, p := range outputDirectives {
+               if found, ok := foundDirective[p.name]; !ok {
+                       t.Errorf("unexpected directive in output: %s", p.text)
+               } else if found {
+                       t.Errorf("directive found multiple times in output: %s", p.text)
+               }
+               foundDirective[p.name] = true
+       }
+       for name, found := range foundDirective {
+               if !found {
+                       t.Errorf("missing directive: %s", name)
+               }
+       }
+
+       // Check that directives that start with the name of top-level declarations
+       // come before the beginning of the named declaration and after the end
+       // of the previous declaration.
+       fset := token.NewFileSet()
+       astFile, err := parser.ParseFile(fset, testDirectives, output, 0)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       prevEnd := 0
+       for _, decl := range astFile.Decls {
+               var name string
+               switch d := decl.(type) {
+               case *ast.FuncDecl:
+                       name = d.Name.Name
+               case *ast.GenDecl:
+                       if len(d.Specs) == 0 {
+                               // An empty group declaration. We still want to check that
+                               // directives can be associated with it, so we make up a name
+                               // to match directives in the test data.
+                               name = "_empty"
+                       } else if spec, ok := d.Specs[0].(*ast.TypeSpec); ok {
+                               name = spec.Name.Name
+                       }
+               }
+               pos := fset.Position(decl.Pos()).Offset
+               end := fset.Position(decl.End()).Offset
+               if name == "" {
+                       prevEnd = end
+                       continue
+               }
+               for _, p := range outputDirectives {
+                       if !strings.HasPrefix(p.name, name) {
+                               continue
+                       }
+                       if p.offset < prevEnd || pos < p.offset {
+                               t.Errorf("directive %s does not appear before definition %s", p.text, name)
+                       }
+               }
+               prevEnd = end
+       }
+}
+
+type directiveInfo struct {
+       text   string // full text of the comment, not including newline
+       name   string // text after //go:
+       offset int    // byte offset of first slash in comment
+}
+
+func findDirectives(source []byte) []directiveInfo {
+       var directives []directiveInfo
+       directivePrefix := []byte("\n//go:")
+       offset := 0
+       for {
+               i := bytes.Index(source[offset:], directivePrefix)
+               if i < 0 {
+                       break
+               }
+               i++ // skip newline
+               p := source[offset+i:]
+               j := bytes.IndexByte(p, '\n')
+               if j < 0 {
+                       // reached EOF
+                       j = len(p)
+               }
+               directive := directiveInfo{
+                       text:   string(p[:j]),
+                       name:   string(p[len(directivePrefix)-1 : j]),
+                       offset: offset + i,
+               }
+               directives = append(directives, directive)
+               offset += i + j
+       }
+       return directives
+}
+
 // Makes sure that `cover -func=profile.cov` reports accurate coverage.
 // Issue #20515.
 func TestCoverFunc(t *testing.T) {
index 636d7e08d9ac9026a0163f6dd17130db10802193..e2c849419abc88bba9cc226d203837b6d523f05c 100644 (file)
@@ -14,6 +14,10 @@ than binary-rewriting coverage tools, but also a little less capable.
 For instance, it does not probe inside && and || expressions, and can
 be mildly confused by single statements with multiple function literals.
 
+When computing coverage of a package that uses cgo, the cover tool
+must be applied to the output of cgo preprocessing, not the input,
+because cover deletes comments that are significant to cgo.
+
 For usage information, please see:
        go help testflag
        go tool cover -help
diff --git a/src/cmd/cover/testdata/directives.go b/src/cmd/cover/testdata/directives.go
new file mode 100644 (file)
index 0000000..dfb7b8e
--- /dev/null
@@ -0,0 +1,40 @@
+// 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.
+
+// This file is processed by the cover command, then a test verifies that
+// all compiler directives are preserved and positioned appropriately.
+
+//go:a
+
+//go:b
+package main
+
+//go:c1
+
+//go:c2
+//doc
+func c() {
+}
+
+//go:d1
+
+//doc
+//go:d2
+type d int
+
+//go:e1
+
+//doc
+//go:e2
+type (
+       e int
+       f int
+)
+
+//go:_empty1
+//doc
+//go:_empty2
+type ()
+
+//go:f