aboutsummaryrefslogtreecommitdiff
path: root/godoc/linkify.go
blob: 4a9c50604770546cd9efece706f27964f9086b3f (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
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
// 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 godoc

import (
	"fmt"
	"go/ast"
	"go/doc"
	"go/token"
	"io"
	"strconv"

	"golang.org/x/tools/internal/typeparams"
)

// 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 := linksFor(n)

	i := 0     // links index
	prev := "" // prev HTML tag
	linkWriter := func(w io.Writer, _ int, start bool) {
		// end tag
		if !start {
			if prev != "" {
				fmt.Fprintf(w, `</%s>`, prev)
				prev = ""
			}
			return
		}

		// start tag
		prev = ""
		if i < len(links) {
			switch info := links[i]; {
			case info.path != "" && info.name == "":
				// package path
				fmt.Fprintf(w, `<a href="/pkg/%s/">`, info.path)
				prev = "a"
			case info.path != "" && info.name != "":
				// qualified identifier
				fmt.Fprintf(w, `<a href="/pkg/%s/#%s">`, info.path, info.name)
				prev = "a"
			case info.path == "" && info.name != "":
				// local identifier
				if info.isVal {
					fmt.Fprintf(w, `<span id="%s">`, info.name)
					prev = "span"
				} else if ast.IsExported(info.name) {
					fmt.Fprintf(w, `<a href="#%s">`, info.name)
					prev = "a"
				}
			}
			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, name string // package path, identifier name
	isVal      bool   // identifier is defined in a const or var declaration
}

// linksFor returns the list of links for the identifiers used
// by node in the same order as they appear in the source.
//
func linksFor(node ast.Node) (links []link) {
	// linkMap tracks link information for each ast.Ident node. Entries may
	// be created out of source order (for example, when we visit a parent
	// definition node). These links are appended to the returned slice when
	// their ast.Ident nodes are visited.
	linkMap := make(map[*ast.Ident]link)

	typeParams := make(map[string]bool)

	ast.Inspect(node, func(node ast.Node) bool {
		switch n := node.(type) {
		case *ast.Field:
			for _, n := range n.Names {
				linkMap[n] = link{}
			}
		case *ast.ImportSpec:
			if name := n.Name; name != nil {
				linkMap[name] = link{}
			}
		case *ast.ValueSpec:
			for _, n := range n.Names {
				linkMap[n] = link{name: n.Name, isVal: true}
			}
		case *ast.FuncDecl:
			linkMap[n.Name] = link{}
			if n.Recv != nil {
				recv := n.Recv.List[0].Type
				if r, isstar := recv.(*ast.StarExpr); isstar {
					recv = r.X
				}
				switch x := recv.(type) {
				case *ast.IndexExpr:
					if ident, _ := x.Index.(*ast.Ident); ident != nil {
						typeParams[ident.Name] = true
					}
				case *typeparams.IndexListExpr:
					for _, index := range x.Indices {
						if ident, _ := index.(*ast.Ident); ident != nil {
							typeParams[ident.Name] = true
						}
					}
				}
			}
		case *ast.TypeSpec:
			linkMap[n.Name] = link{}
		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 {
						linkMap[n] = link{isVal: true}
					}
				}
			}
		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 {
				// Create links only if x is 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.
							linkMap[x] = link{path: path}
							linkMap[n.Sel] = link{path: path, name: n.Sel.Name}
						}
					}
				}
			}
		case *ast.CompositeLit:
			// Detect field names within composite literals. These links should
			// be prefixed by the type name.
			fieldPath := ""
			prefix := ""
			switch typ := n.Type.(type) {
			case *ast.Ident:
				prefix = typ.Name + "."
			case *ast.SelectorExpr:
				if x, _ := typ.X.(*ast.Ident); x != nil {
					// Create links only if x is 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.
								linkMap[x] = link{path: path}
								linkMap[typ.Sel] = link{path: path, name: typ.Sel.Name}
								fieldPath = path
								prefix = typ.Sel.Name + "."
							}
						}
					}
				}
			}
			for _, e := range n.Elts {
				if kv, ok := e.(*ast.KeyValueExpr); ok {
					if k, ok := kv.Key.(*ast.Ident); ok {
						// Note: there is some syntactic ambiguity here. We cannot determine
						// if this is a struct literal or a map literal without type
						// information. We assume struct literal.
						name := prefix + k.Name
						linkMap[k] = link{path: fieldPath, name: name}
					}
				}
			}
		case *ast.Ident:
			if l, ok := linkMap[n]; ok {
				links = append(links, l)
			} else {
				l := link{name: n.Name}
				if n.Obj == nil {
					if doc.IsPredeclared(n.Name) {
						l.path = builtinPkgPath
					} else {
						if typeParams[n.Name] {
							// If a type parameter was declared then do not generate a link.
							// Doing this is necessary because type parameter identifiers do not
							// have their Decl recorded sometimes, see
							// https://golang.org/issue/50956.
							l = link{}
						}
					}
				} else {
					if n.Obj.Kind == ast.Typ {
						if _, isfield := n.Obj.Decl.(*ast.Field); isfield {
							// If an identifier is a type declared in a field assume it is a type
							// parameter and do not generate a link.
							l = link{}
						}
					}
				}
				links = append(links, l)
			}
		}
		return true
	})
	return
}