summaryrefslogtreecommitdiffstats
path: root/src/go/parser/resolver_test.go
blob: 625c009c914d8342eebcbfbe2649f2ed786ac44d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
// 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/internal/typeparams"
	"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
			var mode Mode
			if strings.HasSuffix(path, ".go2") {
				if !typeparams.Enabled {
					t.Skip("type params are not enabled")
				}
			} else {
				mode |= typeparams.DisallowParsing
			}
			file, err := ParseFile(fset, path, src, mode)
			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 {
		// Ignore blank identifiers to reduce noise.
		if ident, _ := node.(*ast.Ident); ident != nil && ident.Obj != nil && ident.Name != "_" {
			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
}