| // 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. |
| |
| //go:build go1.19 |
| |
| // 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/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 obj.Type().(*types.Signature).Recv() != nil { |
| kind = "method" |
| } |
| origin = obj.Origin() |
| |
| case *types.TypeName: |
| if obj.IsAlias() { |
| kind = "type alias" |
| } |
| if named, ok := 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 |
| } |
| } |