File: play.go

package info (click to toggle)
golang-golang-x-tools 1%3A0.25.0%2Bds-1
  • links: PTS, VCS
  • area: main
  • in suites: experimental, forky, sid, trixie
  • size: 22,724 kB
  • sloc: javascript: 2,027; asm: 1,645; sh: 166; yacc: 155; makefile: 49; ansic: 8
file content (376 lines) | stat: -rw-r--r-- 10,209 bytes parent folder | download | duplicates (2)
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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
// Copyright 2023 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.

// The play program is a playground for go/types: a simple web-based
// text editor into which the user can enter a Go program, select a
// region, and see type information about it.
//
// It is intended for convenient exploration and debugging of
// go/types. The command and its web interface are not officially
// supported and they may be changed arbitrarily in the future.
package main

import (
	"encoding/json"
	"fmt"
	"go/ast"
	"go/format"
	"go/token"
	"go/types"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"reflect"
	"strconv"
	"strings"

	"golang.org/x/tools/go/ast/astutil"
	"golang.org/x/tools/go/packages"
	"golang.org/x/tools/go/types/typeutil"
	"golang.org/x/tools/internal/aliases"
	"golang.org/x/tools/internal/typeparams"
)

// TODO(adonovan):
// - show line numbers next to textarea.
// - show a (tree) breakdown of the representation of the expression's type.
// - mention this in the go/types tutorial.
// - display versions of go/types and go command.

func main() {
	http.HandleFunc("/", handleRoot)
	http.HandleFunc("/main.js", handleJS)
	http.HandleFunc("/main.css", handleCSS)
	http.HandleFunc("/select.json", handleSelectJSON)
	const addr = "localhost:9999"
	log.Printf("Listening on http://%s", addr)
	log.Fatal(http.ListenAndServe(addr, nil))
}

func handleSelectJSON(w http.ResponseWriter, req *http.Request) {
	// Parse request.
	if err := req.ParseForm(); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	startOffset, err := strconv.Atoi(req.Form.Get("start"))
	if err != nil {
		http.Error(w, fmt.Sprintf("start: %v", err), http.StatusBadRequest)
		return
	}
	endOffset, err := strconv.Atoi(req.Form.Get("end"))
	if err != nil {
		http.Error(w, fmt.Sprintf("end: %v", err), http.StatusBadRequest)
		return
	}

	// Write Go program to temporary file.
	f, err := os.CreateTemp("", "play-*.go")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	if _, err := io.Copy(f, req.Body); err != nil {
		f.Close() // ignore error
		http.Error(w, fmt.Sprintf("can't read body: %v", err), http.StatusInternalServerError)
		return
	}
	if err := f.Close(); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer func() {
		_ = os.Remove(f.Name()) // ignore error
	}()

	// Load and type check it.
	cfg := &packages.Config{
		Fset: token.NewFileSet(),
		Mode: packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo,
		Dir:  filepath.Dir(f.Name()),
	}
	pkgs, err := packages.Load(cfg, "file="+f.Name())
	if err != nil {
		http.Error(w, fmt.Sprintf("load: %v", err), http.StatusInternalServerError)
		return
	}
	pkg := pkgs[0]

	// -- Format the response --

	out := new(strings.Builder)

	// Parse/type error information.
	if len(pkg.Errors) > 0 {
		fmt.Fprintf(out, "Errors:\n")
		for _, err := range pkg.Errors {
			fmt.Fprintf(out, "%s: %s\n", err.Pos, err.Msg)
		}
		fmt.Fprintf(out, "\n")
	}

	fset := pkg.Fset
	file := pkg.Syntax[0]
	tokFile := fset.File(file.Pos())
	startPos := tokFile.Pos(startOffset)
	endPos := tokFile.Pos(endOffset)

	// Syntax information
	path, exact := astutil.PathEnclosingInterval(file, startPos, endPos)
	fmt.Fprintf(out, "Path enclosing interval #%d-%d [exact=%t]:\n",
		startOffset, endOffset, exact)
	var innermostExpr ast.Expr
	for i, n := range path {
		// Show set of names defined in each scope.
		scopeNames := ""
		{
			node := n
			prefix := ""

			// A function (Func{Decl.Lit}) doesn't have a scope of its
			// own, nor does its Body: only nested BlockStmts do.
			// The type parameters, parameters, and locals are all
			// in the scope associated with the FuncType; show it.
			switch n := n.(type) {
			case *ast.FuncDecl:
				node = n.Type
				prefix = "Type."
			case *ast.FuncLit:
				node = n.Type
				prefix = "Type."
			}

			if scope := pkg.TypesInfo.Scopes[node]; scope != nil {
				scopeNames = fmt.Sprintf(" %sScope={%s}",
					prefix,
					strings.Join(scope.Names(), ", "))
			}
		}

		// TODO(adonovan): turn these into links to highlight the source.
		start, end := fset.Position(n.Pos()), fset.Position(n.End())
		fmt.Fprintf(out, "[%d] %T @ %d:%d-%d:%d (#%d-%d)%s\n",
			i, n,
			start.Line, start.Column, end.Line,
			end.Column, start.Offset, end.Offset,
			scopeNames)
		if e, ok := n.(ast.Expr); ok && innermostExpr == nil {
			innermostExpr = e
		}
	}
	fmt.Fprintf(out, "\n")

	// Expression type information
	if innermostExpr != nil {
		if tv, ok := pkg.TypesInfo.Types[innermostExpr]; ok {
			var modes []string
			for _, mode := range []struct {
				name      string
				condition func(types.TypeAndValue) bool
			}{
				{"IsVoid", types.TypeAndValue.IsVoid},
				{"IsType", types.TypeAndValue.IsType},
				{"IsBuiltin", types.TypeAndValue.IsBuiltin},
				{"IsValue", types.TypeAndValue.IsValue},
				{"IsNil", types.TypeAndValue.IsNil},
				{"Addressable", types.TypeAndValue.Addressable},
				{"Assignable", types.TypeAndValue.Assignable},
				{"HasOk", types.TypeAndValue.HasOk},
			} {
				if mode.condition(tv) {
					modes = append(modes, mode.name)
				}
			}
			fmt.Fprintf(out, "%T has type %v, mode %s",
				innermostExpr, tv.Type, modes)
			if tu := tv.Type.Underlying(); tu != tv.Type {
				fmt.Fprintf(out, ", underlying type %v", tu)
			}
			if tc := typeparams.CoreType(tv.Type); tc != tv.Type {
				fmt.Fprintf(out, ", core type %v", tc)
			}
			if tv.Value != nil {
				fmt.Fprintf(out, ", and constant value %v", tv.Value)
			}
			fmt.Fprintf(out, "\n\n")
		}
	}

	// selection x.f information (if cursor is over .f)
	for _, n := range path[:min(2, len(path))] {
		if sel, ok := n.(*ast.SelectorExpr); ok {
			seln, ok := pkg.TypesInfo.Selections[sel]
			if ok {
				fmt.Fprintf(out, "Selection: %s recv=%v obj=%v type=%v indirect=%t index=%d\n\n",
					strings.Fields("FieldVal MethodVal MethodExpr")[seln.Kind()],
					seln.Recv(),
					seln.Obj(),
					seln.Type(),
					seln.Indirect(),
					seln.Index())

			} else {
				fmt.Fprintf(out, "Selector is qualified identifier.\n\n")
			}
			break
		}
	}

	// Object type information.
	switch n := path[0].(type) {
	case *ast.Ident:
		if obj, ok := pkg.TypesInfo.Defs[n]; ok {
			if obj == nil {
				fmt.Fprintf(out, "nil def") // e.g. package name, "_", type switch
			} else {
				formatObj(out, fset, "def", obj)
			}
		}
		if obj, ok := pkg.TypesInfo.Uses[n]; ok {
			formatObj(out, fset, "use", obj)
		}
	default:
		if obj, ok := pkg.TypesInfo.Implicits[n]; ok {
			formatObj(out, fset, "implicit def", obj)
		}
	}
	fmt.Fprintf(out, "\n")

	// Pretty-print of selected syntax.
	fmt.Fprintf(out, "Pretty-printed:\n")
	format.Node(out, fset, path[0])
	fmt.Fprintf(out, "\n\n")

	// Syntax debug output.
	fmt.Fprintf(out, "Syntax:\n")
	ast.Fprint(out, fset, path[0], nil) // ignore errors

	// Clean up the messy temp file name.
	outStr := strings.ReplaceAll(out.String(), f.Name(), "play.go")

	// Send response.
	var respJSON struct {
		Out string
	}
	respJSON.Out = outStr

	data, _ := json.Marshal(respJSON) // can't fail
	w.Write(data)                     // ignore error
}

func formatObj(out *strings.Builder, fset *token.FileSet, ref string, obj types.Object) {
	// e.g. *types.Func -> "func"
	kind := strings.ToLower(strings.TrimPrefix(reflect.TypeOf(obj).String(), "*types."))

	// Show origin of generics, and refine kind.
	var origin types.Object
	switch obj := obj.(type) {
	case *types.Var:
		if obj.IsField() {
			kind = "field"
		}
		origin = obj.Origin()

	case *types.Func:
		if recv := obj.Type().(*types.Signature).Recv(); recv != nil {
			kind = fmt.Sprintf("method (with recv %v)", recv.Type())
		}
		origin = obj.Origin()

	case *types.TypeName:
		if obj.IsAlias() {
			kind = "type alias"
		}
		if named, ok := aliases.Unalias(obj.Type()).(*types.Named); ok {
			origin = named.Obj()
		}
	}

	fmt.Fprintf(out, "%s of %s %s of type %v declared at %v",
		ref, kind, obj.Name(), obj.Type(), fset.Position(obj.Pos()))
	if origin != nil && origin != obj {
		fmt.Fprintf(out, " (instantiation of %v)", origin.Type())
	}
	fmt.Fprintf(out, "\n\n")

	// method set
	if methods := typeutil.IntuitiveMethodSet(obj.Type(), nil); len(methods) > 0 {
		fmt.Fprintf(out, "Methods:\n")
		for _, m := range methods {
			fmt.Fprintln(out, m)
		}
		fmt.Fprintf(out, "\n")
	}

	// scope tree
	fmt.Fprintf(out, "Scopes:\n")
	for scope := obj.Parent(); scope != nil; scope = scope.Parent() {
		var (
			start = fset.Position(scope.Pos())
			end   = fset.Position(scope.End())
		)
		fmt.Fprintf(out, "%d:%d-%d:%d: %s\n",
			start.Line, start.Column, end.Line, end.Column, scope)
	}
}

func handleRoot(w http.ResponseWriter, req *http.Request) { io.WriteString(w, mainHTML) }
func handleJS(w http.ResponseWriter, req *http.Request)   { io.WriteString(w, mainJS) }
func handleCSS(w http.ResponseWriter, req *http.Request)  { io.WriteString(w, mainCSS) }

// TODO(adonovan): avoid CSS reliance on quirks mode and enable strict mode (<!DOCTYPE html>).
const mainHTML = `<html>
<head>
<script src="/main.js"></script>
<link rel="stylesheet" href="/main.css"></link>
</head>
<body onload="onLoad()">
<h1>go/types playground</h1>
<p>Select an expression to see information about it.</p>
<textarea rows='25' id='src'>
package main

import "fmt"

func main() {
	fmt.Println("Hello, world!")
}
</textarea>
<div id='out'/>
</body>
</html>
`

const mainJS = `
function onSelectionChange() {
	var start = document.activeElement.selectionStart;
	var end = document.activeElement.selectionEnd;
	var req = new XMLHttpRequest();
	req.open("POST", "/select.json?start=" + start + "&end=" + end, false);
	req.send(document.activeElement.value);
	var resp = JSON.parse(req.responseText);
	document.getElementById('out').innerText = resp.Out;
}

function onLoad() {
	document.getElementById("src").addEventListener('select', onSelectionChange)
}
`

const mainCSS = `
textarea { width: 6in; }
body { color: gray; }
div#out { font-family: monospace; font-size: 80%; }
`

// TODO(adonovan): use go1.21 built-in.
func min(x, y int) int {
	if x < y {
		return x
	} else {
		return y
	}
}