...

Source file src/go/parser/resolver_test.go

Documentation: go/parser

		 1  // Copyright 2021 The Go Authors. All rights reserved.
		 2  // Use of this source code is governed by a BSD-style
		 3  // license that can be found in the LICENSE file.
		 4  
		 5  package parser
		 6  
		 7  import (
		 8  	"fmt"
		 9  	"go/ast"
		10  	"go/internal/typeparams"
		11  	"go/scanner"
		12  	"go/token"
		13  	"os"
		14  	"path/filepath"
		15  	"strings"
		16  	"testing"
		17  )
		18  
		19  // TestResolution checks that identifiers are resolved to the declarations
		20  // annotated in the source, by comparing the positions of the resulting
		21  // Ident.Obj.Decl to positions marked in the source via special comments.
		22  //
		23  // In the test source, any comment prefixed with '=' or '@' (or both) marks the
		24  // previous token position as the declaration ('=') or a use ('@') of an
		25  // identifier. The text following '=' and '@' in the comment string is the
		26  // label to use for the location.	Declaration labels must be unique within the
		27  // file, and use labels must refer to an existing declaration label. It's OK
		28  // for a comment to denote both the declaration and use of a label (e.g.
		29  // '=@foo'). Leading and trailing whitespace is ignored. Any comment not
		30  // beginning with '=' or '@' is ignored.
		31  func TestResolution(t *testing.T) {
		32  	dir := filepath.Join("testdata", "resolution")
		33  	fis, err := os.ReadDir(dir)
		34  	if err != nil {
		35  		t.Fatal(err)
		36  	}
		37  
		38  	for _, fi := range fis {
		39  		t.Run(fi.Name(), func(t *testing.T) {
		40  			fset := token.NewFileSet()
		41  			path := filepath.Join(dir, fi.Name())
		42  			src := readFile(path) // panics on failure
		43  			var mode Mode
		44  			if strings.HasSuffix(path, ".go2") {
		45  				if !typeparams.Enabled {
		46  					t.Skip("type params are not enabled")
		47  				}
		48  			} else {
		49  				mode |= typeparams.DisallowParsing
		50  			}
		51  			file, err := ParseFile(fset, path, src, mode)
		52  			if err != nil {
		53  				t.Fatal(err)
		54  			}
		55  
		56  			// Compare the positions of objects resolved during parsing (fromParser)
		57  			// to those annotated in source comments (fromComments).
		58  
		59  			handle := fset.File(file.Package)
		60  			fromParser := declsFromParser(file)
		61  			fromComments := declsFromComments(handle, src)
		62  
		63  			pos := func(pos token.Pos) token.Position {
		64  				p := handle.Position(pos)
		65  				// The file name is implied by the subtest, so remove it to avoid
		66  				// clutter in error messages.
		67  				p.Filename = ""
		68  				return p
		69  			}
		70  			for k, want := range fromComments {
		71  				if got := fromParser[k]; got != want {
		72  					t.Errorf("%s resolved to %s, want %s", pos(k), pos(got), pos(want))
		73  				}
		74  				delete(fromParser, k)
		75  			}
		76  			// What remains in fromParser are unexpected resolutions.
		77  			for k, got := range fromParser {
		78  				t.Errorf("%s resolved to %s, want no object", pos(k), pos(got))
		79  			}
		80  		})
		81  	}
		82  }
		83  
		84  // declsFromParser walks the file and collects the map associating an
		85  // identifier position with its declaration position.
		86  func declsFromParser(file *ast.File) map[token.Pos]token.Pos {
		87  	objmap := map[token.Pos]token.Pos{}
		88  	ast.Inspect(file, func(node ast.Node) bool {
		89  		// Ignore blank identifiers to reduce noise.
		90  		if ident, _ := node.(*ast.Ident); ident != nil && ident.Obj != nil && ident.Name != "_" {
		91  			objmap[ident.Pos()] = ident.Obj.Pos()
		92  		}
		93  		return true
		94  	})
		95  	return objmap
		96  }
		97  
		98  // declsFromComments looks at comments annotating uses and declarations, and
		99  // maps each identifier use to its corresponding declaration. See the
	 100  // description of these annotations in the documentation for TestResolution.
	 101  func declsFromComments(handle *token.File, src []byte) map[token.Pos]token.Pos {
	 102  	decls, uses := positionMarkers(handle, src)
	 103  
	 104  	objmap := make(map[token.Pos]token.Pos)
	 105  	// Join decls and uses on name, to build the map of use->decl.
	 106  	for name, posns := range uses {
	 107  		declpos, ok := decls[name]
	 108  		if !ok {
	 109  			panic(fmt.Sprintf("missing declaration for %s", name))
	 110  		}
	 111  		for _, pos := range posns {
	 112  			objmap[pos] = declpos
	 113  		}
	 114  	}
	 115  	return objmap
	 116  }
	 117  
	 118  // positionMarkers extracts named positions from the source denoted by comments
	 119  // prefixed with '=' (declarations) and '@' (uses): for example '@foo' or
	 120  // '=@bar'. It returns a map of name->position for declarations, and
	 121  // name->position(s) for uses.
	 122  func positionMarkers(handle *token.File, src []byte) (decls map[string]token.Pos, uses map[string][]token.Pos) {
	 123  	var s scanner.Scanner
	 124  	s.Init(handle, src, nil, scanner.ScanComments)
	 125  	decls = make(map[string]token.Pos)
	 126  	uses = make(map[string][]token.Pos)
	 127  	var prev token.Pos // position of last non-comment, non-semicolon token
	 128  
	 129  scanFile:
	 130  	for {
	 131  		pos, tok, lit := s.Scan()
	 132  		switch tok {
	 133  		case token.EOF:
	 134  			break scanFile
	 135  		case token.COMMENT:
	 136  			name, decl, use := annotatedObj(lit)
	 137  			if len(name) > 0 {
	 138  				if decl {
	 139  					if _, ok := decls[name]; ok {
	 140  						panic(fmt.Sprintf("duplicate declaration markers for %s", name))
	 141  					}
	 142  					decls[name] = prev
	 143  				}
	 144  				if use {
	 145  					uses[name] = append(uses[name], prev)
	 146  				}
	 147  			}
	 148  		case token.SEMICOLON:
	 149  			// ignore automatically inserted semicolon
	 150  			if lit == "\n" {
	 151  				continue scanFile
	 152  			}
	 153  			fallthrough
	 154  		default:
	 155  			prev = pos
	 156  		}
	 157  	}
	 158  	return decls, uses
	 159  }
	 160  
	 161  func annotatedObj(lit string) (name string, decl, use bool) {
	 162  	if lit[1] == '*' {
	 163  		lit = lit[:len(lit)-2] // strip trailing */
	 164  	}
	 165  	lit = strings.TrimSpace(lit[2:])
	 166  
	 167  scanLit:
	 168  	for idx, r := range lit {
	 169  		switch r {
	 170  		case '=':
	 171  			decl = true
	 172  		case '@':
	 173  			use = true
	 174  		default:
	 175  			name = lit[idx:]
	 176  			break scanLit
	 177  		}
	 178  	}
	 179  	return
	 180  }
	 181  

View as plain text