From 7cfebf7b1d0e02663a18225c04ca02f28e4fd6df Mon Sep 17 00:00:00 2001 From: Robert Griesemer Date: Tue, 26 Mar 2013 11:14:30 -0700 Subject: [PATCH] godoc: link identifiers to declarations The changes are almost completely self-contained in the new file linkify.go. The other changes are minimal and should not disturb the currently working godoc, in anticipation of Go 1.1. To disable the feature in case of problems, set -links=false. Fixes #2063. R=adg, r CC=golang-dev https://golang.org/cl/7883044 --- src/cmd/godoc/format.go | 10 +- src/cmd/godoc/godoc.go | 39 +++++++- src/cmd/godoc/linkify.go | 210 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 251 insertions(+), 8 deletions(-) create mode 100644 src/cmd/godoc/linkify.go diff --git a/src/cmd/godoc/format.go b/src/cmd/godoc/format.go index 122ddc7d62..5245409369 100644 --- a/src/cmd/godoc/format.go +++ b/src/cmd/godoc/format.go @@ -226,10 +226,10 @@ func lineSelection(text []byte) Selection { } } -// commentSelection returns the sequence of consecutive comments -// in the Go src text as a Selection. +// tokenSelection returns, as a selection, the sequence of +// consecutive occurrences of token sel in the Go src text. // -func commentSelection(src []byte) Selection { +func tokenSelection(src []byte, sel token.Token) Selection { var s scanner.Scanner fset := token.NewFileSet() file := fset.AddFile("", fset.Base(), len(src)) @@ -241,7 +241,7 @@ func commentSelection(src []byte) Selection { break } offs := file.Offset(pos) - if tok == token.COMMENT { + if tok == sel { seg = []int{offs, offs + len(lit)} break } @@ -338,7 +338,7 @@ func selectionTag(w io.Writer, text []byte, selections int) { func FormatText(w io.Writer, text []byte, line int, goSource bool, pattern string, selection Selection) { var comments, highlights Selection if goSource { - comments = commentSelection(text) + comments = tokenSelection(text, token.COMMENT) } if pattern != "" { highlights = regexpSelection(text, pattern) diff --git a/src/cmd/godoc/godoc.go b/src/cmd/godoc/godoc.go index 5774321130..b5282b863d 100644 --- a/src/cmd/godoc/godoc.go +++ b/src/cmd/godoc/godoc.go @@ -66,6 +66,7 @@ var ( templateDir = flag.String("templates", "", "directory containing alternate template files") showPlayground = flag.Bool("play", false, "enable playground in web interface") showExamples = flag.Bool("ex", false, "show examples in command line mode") + declLinks = flag.Bool("links", true, "link identifiers to their declarations") // search index indexEnabled = flag.Bool("index", false, "enable search index") @@ -281,8 +282,21 @@ func nodeFunc(node interface{}, fset *token.FileSet) string { func node_htmlFunc(node interface{}, fset *token.FileSet) string { var buf1 bytes.Buffer writeNode(&buf1, fset, node) + var buf2 bytes.Buffer - FormatText(&buf2, buf1.Bytes(), -1, true, "", nil) + // BUG(gri): When showing full source text (?m=src), + // identifier links are incorrect. + // TODO(gri): Only linkify exported code snippets, not the + // full source text: identifier resolution is + // not sufficiently strong w/o type checking. + // Need to check if info.PAst != nil - requires + // to pass *PageInfo around instead of fset. + if n, _ := node.(ast.Node); n != nil && *declLinks { + LinkifyText(&buf2, buf1.Bytes(), n) + } else { + FormatText(&buf2, buf1.Bytes(), -1, true, "", nil) + } + return buf2.String() } @@ -521,7 +535,7 @@ var fmap = template.FuncMap{ "filename": filenameFunc, "repeat": strings.Repeat, - // accss to FileInfos (directory listings) + // access to FileInfos (directory listings) "fileInfoName": fileInfoNameFunc, "fileInfoTime": fileInfoTimeFunc, @@ -1020,6 +1034,23 @@ func collectExamples(pkg *ast.Package, testfiles map[string]*ast.File) []*doc.Ex return examples } +// poorMansImporter returns a (dummy) package object named +// by the last path component of the provided package path +// (as is the convention for packages). This is sufficient +// to resolve package identifiers without doing an actual +// import. It never returns an error. +// +func poorMansImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) { + pkg := imports[path] + if pkg == nil { + // note that strings.LastIndex returns -1 if there is no "/" + pkg = ast.NewObj(ast.Pkg, path[strings.LastIndex(path, "/")+1:]) + pkg.Data = ast.NewScope(nil) // required by ast.NewPackage for dot-import + imports[path] = pkg + } + return pkg, nil +} + // getPageInfo returns the PageInfo for a package directory abspath. If the // parameter genAST is set, an AST containing only the package exports is // computed (PageInfo.PAst), otherwise package documentation (PageInfo.Doc) @@ -1071,7 +1102,9 @@ func (h *docServer) getPageInfo(abspath, relpath string, mode PageInfoMode) (inf info.Err = err return } - pkg := &ast.Package{Name: pkgname, Files: files} + + // ignore any errors - they are due to unresolved identifiers + pkg, _ := ast.NewPackage(fset, files, poorMansImporter, nil) // extract package documentation info.FSet = fset diff --git a/src/cmd/godoc/linkify.go b/src/cmd/godoc/linkify.go new file mode 100644 index 0000000000..1f976951b5 --- /dev/null +++ b/src/cmd/godoc/linkify.go @@ -0,0 +1,210 @@ +// Copyright 2013 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. + +// This file implements LinkifyText which introduces +// links for identifiers pointing to their declarations. +// The approach does not cover all cases because godoc +// doesn't have complete type information, but it's +// reasonably good for browsing. + +package main + +import ( + "fmt" + "go/ast" + "go/token" + "io" + "strconv" +) + +// LinkifyText HTML-escapes source text and writes it to w. +// Identifiers that are in a "use" position (i.e., that are +// not being declared), are wrapped with HTML links pointing +// to the respective declaration, if possible. Comments are +// formatted the same way as with FormatText. +// +func LinkifyText(w io.Writer, text []byte, n ast.Node) { + links := links(n) + + i := 0 // links index + open := false // status of html tag + linkWriter := func(w io.Writer, _ int, start bool) { + // end tag + if !start { + if open { + fmt.Fprintf(w, ``) + open = false + } + return + } + + // start tag + open = false + if i < len(links) { + switch info := links[i]; { + case info.path != "" && info.ident == nil: + // package path + fmt.Fprintf(w, ``, info.path) + open = true + case info.path != "" && info.ident != nil: + // qualified identifier + fmt.Fprintf(w, ``, info.path, info.ident.Name) + open = true + case info.path == "" && info.ident != nil: + // locally declared identifier + fmt.Fprintf(w, ``, info.ident.Name) + open = true + } + i++ + } + } + + idents := tokenSelection(text, token.IDENT) + comments := tokenSelection(text, token.COMMENT) + FormatSelections(w, text, linkWriter, idents, selectionTag, comments) +} + +// A link describes the (HTML) link information for an identifier. +// The zero value of a link represents "no link". +// +type link struct { + path string + ident *ast.Ident +} + +// links returns the list of links for the identifiers used +// by node in the same order as they appear in the source. +// +func links(node ast.Node) (list []link) { + defs := defs(node) + + // NOTE: We are expecting ast.Inspect to call the + // callback function in source text order. + ast.Inspect(node, func(node ast.Node) bool { + switch n := node.(type) { + case *ast.Ident: + info := link{} + if !defs[n] { + if n.Obj == nil && predeclared[n.Name] { + info.path = builtinPkgPath + } + info.ident = n + } + list = append(list, info) + return false + case *ast.SelectorExpr: + // Detect qualified identifiers of the form pkg.ident. + // If anything fails we return true and collect individual + // identifiers instead. + if x, _ := n.X.(*ast.Ident); x != nil { + // x must be a package for a qualified identifier + if obj := x.Obj; obj != nil && obj.Kind == ast.Pkg { + if spec, _ := obj.Decl.(*ast.ImportSpec); spec != nil { + // spec.Path.Value is the import path + if path, err := strconv.Unquote(spec.Path.Value); err == nil { + // Register two links, one for the package + // and one for the qualified identifier. + info := link{path: path} + list = append(list, info) + info.ident = n.Sel + list = append(list, info) + return false + } + } + } + } + } + return true + }) + + return +} + +// defs returns the set of identifiers that are declared ("defined") by node. +func defs(node ast.Node) map[*ast.Ident]bool { + m := make(map[*ast.Ident]bool) + + ast.Inspect(node, func(node ast.Node) bool { + switch n := node.(type) { + case *ast.Field: + for _, n := range n.Names { + m[n] = true + } + case *ast.ImportSpec: + if name := n.Name; name != nil { + m[name] = true + } + case *ast.ValueSpec: + for _, n := range n.Names { + m[n] = true + } + case *ast.TypeSpec: + m[n.Name] = true + case *ast.FuncDecl: + m[n.Name] = true + case *ast.AssignStmt: + // Short variable declarations only show up if we apply + // this code to all source code (as opposed to exported + // declarations only). + if n.Tok == token.DEFINE { + // Some of the lhs variables may be re-declared, + // so technically they are not defs. We don't + // care for now. + for _, x := range n.Lhs { + // Each lhs expression should be an + // ident, but we are conservative and check. + if n, _ := x.(*ast.Ident); n != nil { + m[n] = true + } + } + } + } + return true + }) + + return m +} + +// The predeclared map represents the set of all predeclared identifiers. +var predeclared = map[string]bool{ + "bool": true, + "byte": true, + "complex64": true, + "complex128": true, + "error": true, + "float32": true, + "float64": true, + "int": true, + "int8": true, + "int16": true, + "int32": true, + "int64": true, + "rune": true, + "string": true, + "uint": true, + "uint8": true, + "uint16": true, + "uint32": true, + "uint64": true, + "uintptr": true, + "true": true, + "false": true, + "iota": true, + "nil": true, + "append": true, + "cap": true, + "close": true, + "complex": true, + "copy": true, + "delete": true, + "imag": true, + "len": true, + "make": true, + "new": true, + "panic": true, + "print": true, + "println": true, + "real": true, + "recover": true, +} -- 2.48.1