Black Lives Matter. Support the Equal Justice Initiative.

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