1
0
mirror of https://github.com/golang/go synced 2024-09-30 08:18:40 -06:00

go/parser: add data-driven tests for object resolution

Add new tests for object resolution driven by source files with
declarations and uses marked via special comments. This made it easier
to add test coverage while refactoring object resolution for #45104.

Tests are added to a new resolver_test.go file. In a subsequent CL the
resolver.go file will be added, making this choice of file name more
sensible.

For #45104
For #45136
For #45160

Change-Id: I240fccc0de95aa8f2d03e39c77146d4c61f1ef9e
Reviewed-on: https://go-review.googlesource.com/c/go/+/304450
Trust: Robert Findley <rfindley@google.com>
Trust: Robert Griesemer <gri@golang.org>
Run-TryBot: Robert Findley <rfindley@google.com>
Reviewed-by: Robert Griesemer <gri@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
This commit is contained in:
Rob Findley 2021-03-22 22:10:13 -04:00 committed by Robert Findley
parent 64ca7e2cb3
commit 0fdd371e6b
4 changed files with 274 additions and 0 deletions

View File

@ -0,0 +1,170 @@
// Copyright 2021 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.
package parser
import (
"fmt"
"go/ast"
"go/scanner"
"go/token"
"os"
"path/filepath"
"strings"
"testing"
)
// TestResolution checks that identifiers are resolved to the declarations
// annotated in the source, by comparing the positions of the resulting
// Ident.Obj.Decl to positions marked in the source via special comments.
//
// In the test source, any comment prefixed with '=' or '@' (or both) marks the
// previous token position as the declaration ('=') or a use ('@') of an
// identifier. The text following '=' and '@' in the comment string is the
// label to use for the location. Declaration labels must be unique within the
// file, and use labels must refer to an existing declaration label. It's OK
// for a comment to denote both the declaration and use of a label (e.g.
// '=@foo'). Leading and trailing whitespace is ignored. Any comment not
// beginning with '=' or '@' is ignored.
func TestResolution(t *testing.T) {
dir := filepath.Join("testdata", "resolution")
fis, err := os.ReadDir(dir)
if err != nil {
t.Fatal(err)
}
for _, fi := range fis {
t.Run(fi.Name(), func(t *testing.T) {
fset := token.NewFileSet()
path := filepath.Join(dir, fi.Name())
src := readFile(path) // panics on failure
file, err := ParseFile(fset, path, src, 0)
if err != nil {
t.Fatal(err)
}
// Compare the positions of objects resolved during parsing (fromParser)
// to those annotated in source comments (fromComments).
handle := fset.File(file.Package)
fromParser := declsFromParser(file)
fromComments := declsFromComments(handle, src)
pos := func(pos token.Pos) token.Position {
p := handle.Position(pos)
// The file name is implied by the subtest, so remove it to avoid
// clutter in error messages.
p.Filename = ""
return p
}
for k, want := range fromComments {
if got := fromParser[k]; got != want {
t.Errorf("%s resolved to %s, want %s", pos(k), pos(got), pos(want))
}
delete(fromParser, k)
}
// What remains in fromParser are unexpected resolutions.
for k, got := range fromParser {
t.Errorf("%s resolved to %s, want no object", pos(k), pos(got))
}
})
}
}
// declsFromParser walks the file and collects the map associating an
// identifier position with its declaration position.
func declsFromParser(file *ast.File) map[token.Pos]token.Pos {
objmap := map[token.Pos]token.Pos{}
ast.Inspect(file, func(node ast.Node) bool {
if ident, _ := node.(*ast.Ident); ident != nil && ident.Obj != nil {
objmap[ident.Pos()] = ident.Obj.Pos()
}
return true
})
return objmap
}
// declsFromComments looks at comments annotating uses and declarations, and
// maps each identifier use to its corresponding declaration. See the
// description of these annotations in the documentation for TestResolution.
func declsFromComments(handle *token.File, src []byte) map[token.Pos]token.Pos {
decls, uses := positionMarkers(handle, src)
objmap := make(map[token.Pos]token.Pos)
// Join decls and uses on name, to build the map of use->decl.
for name, posns := range uses {
declpos, ok := decls[name]
if !ok {
panic(fmt.Sprintf("missing declaration for %s", name))
}
for _, pos := range posns {
objmap[pos] = declpos
}
}
return objmap
}
// positionMarkers extracts named positions from the source denoted by comments
// prefixed with '=' (declarations) and '@' (uses): for example '@foo' or
// '=@bar'. It returns a map of name->position for declarations, and
// name->position(s) for uses.
func positionMarkers(handle *token.File, src []byte) (decls map[string]token.Pos, uses map[string][]token.Pos) {
var s scanner.Scanner
s.Init(handle, src, nil, scanner.ScanComments)
decls = make(map[string]token.Pos)
uses = make(map[string][]token.Pos)
var prev token.Pos // position of last non-comment, non-semicolon token
scanFile:
for {
pos, tok, lit := s.Scan()
switch tok {
case token.EOF:
break scanFile
case token.COMMENT:
name, decl, use := annotatedObj(lit)
if len(name) > 0 {
if decl {
if _, ok := decls[name]; ok {
panic(fmt.Sprintf("duplicate declaration markers for %s", name))
}
decls[name] = prev
}
if use {
uses[name] = append(uses[name], prev)
}
}
case token.SEMICOLON:
// ignore automatically inserted semicolon
if lit == "\n" {
continue scanFile
}
fallthrough
default:
prev = pos
}
}
return decls, uses
}
func annotatedObj(lit string) (name string, decl, use bool) {
if lit[1] == '*' {
lit = lit[:len(lit)-2] // strip trailing */
}
lit = strings.TrimSpace(lit[2:])
scanLit:
for idx, r := range lit {
switch r {
case '=':
decl = true
case '@':
use = true
default:
name = lit[idx:]
break scanLit
}
}
return
}

View File

@ -0,0 +1,29 @@
// Copyright 2021 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.
package issue45136
type obj /* =@obj */ struct {
name /*=@name */ string
}
func _ /* =@blank */ () {
var foo /* =@foo */ = "foo"
obj /* @obj */ ["foo"]
obj /* @obj */ .run()
// TODO(#45136): the next two statements are missing objects.
obj{
name: foo /* @foo */,
}
obj{
name: "bar",
}.run()
var _ /* @=blank4 */ = File{key: obj /* @obj */{}}
var _ /* @=blank3 */ = File{obj{}}
[]obj /* @obj */ {foo /* @foo */}
x /* =@x1 */ := obj /* @obj */{}
}

View File

@ -0,0 +1,25 @@
// Copyright 2021 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.
package issue45160
func mklink1 /* =@mklink1func */() {}
func _ /* =@blank */ () {
var tests /* =@tests */ = []dirLinkTest /* @dirLinkTest */ {
{
mklink1 /* @mklink1func */: func() {},
mklink2: func(link /* =@link */, target /* =@target */ string) error {
return nil
},
},
}
}
type dirLinkTest /* =@dirLinkTest */ struct {
mklink1 /* =@mklink1field */ func(string, string) error
mklink2 /* =@mklink2field */ func(string, string) error
}
func mklink2 /* =@mklink2func */() {}

View File

@ -0,0 +1,50 @@
// Copyright 2021 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.
package resolution
func f /* =@fdecl */(n /* =@narg */ ast.Node) bool {
if n /* =@ninit */, ok /* =@ok */ := n /* @narg */ .(*ast.SelectorExpr); ok /* @ok */ {
sel = n /* @ninit */
}
}
type c /* =@cdecl */ map[token.Pos]resolvedObj
func (v /* =@vdecl */ c /* @cdecl */) Visit(node /* =@nodearg */ ast.Node) (w /* =@w */ ast.Visitor) {}
const (
basic /* =@basic */ = iota
labelOk // =@labelOk
)
func _ /* =@blankFunc */ () {
x /* =@x1 */ := c /* @cdecl */{}
switch x /* =@x2 */ := x /* @x1 */; x /* =@x3 */ := x /* @x2 */.(type) {
case c /* @cdecl */:
default:
}
loop /* =@loop */:
for {
if true {
break loop /* @loop */
}
}
select {
case err /* =@err1 */ := <-_:
return err /* @err1 */
case err /* =@err2 */ := <-_:
return err /* @err2 */
}
}
var cycle /* =@cycle */ = cycle /* @cycle */ + 1
type chain /* =@chain */ struct {
next /* =@next */ *chain /* @chain */
}
func recursive /* =@recursive */() {
recursive /* @recursive */ ()
}