Source file src/go/parser/resolver_test.go

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

View as plain text