Add generated file
This PR adds generated files under pkg/client and vendor folder.
This commit is contained in:
111
vendor/golang.org/x/tools/godoc/analysis/README
generated
vendored
Normal file
111
vendor/golang.org/x/tools/godoc/analysis/README
generated
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
|
||||
Type and Pointer Analysis to-do list
|
||||
====================================
|
||||
|
||||
Alan Donovan <adonovan@google.com>
|
||||
|
||||
|
||||
Overall design
|
||||
--------------
|
||||
|
||||
We should re-run the type and pointer analyses periodically,
|
||||
as we do with the indexer.
|
||||
|
||||
Version skew: how to mitigate the bad effects of stale URLs in old pages?
|
||||
We could record the file's length/CRC32/mtime in the go/loader, and
|
||||
refuse to decorate it with links unless they match at serving time.
|
||||
|
||||
Use the VFS mechanism when (a) enumerating packages and (b) loading
|
||||
them. (Requires planned changes to go/loader.)
|
||||
|
||||
Future work: shard this using map/reduce for larger corpora.
|
||||
|
||||
Testing: how does one test that a web page "looks right"?
|
||||
|
||||
|
||||
Bugs
|
||||
----
|
||||
|
||||
(*ssa.Program).Create requires transitively error-free packages. We
|
||||
can make this more robust by making the requirement transitively free
|
||||
of "hard" errors; soft errors are fine.
|
||||
|
||||
Markup of compiler errors is slightly buggy because they overlap with
|
||||
other selections (e.g. Idents). Fix.
|
||||
|
||||
|
||||
User Interface
|
||||
--------------
|
||||
|
||||
CALLGRAPH:
|
||||
- Add a search box: given a search node, expand path from each entry
|
||||
point to it.
|
||||
- Cause hovering over a given node to highlight that node, and all
|
||||
nodes that are logically identical to it.
|
||||
- Initially expand the callgraph trees (but not their toggle divs).
|
||||
|
||||
CALLEES:
|
||||
- The '(' links are not very discoverable. Highlight them?
|
||||
|
||||
Type info:
|
||||
- In the source viewer's lower pane, use a toggle div around the
|
||||
IMPLEMENTS and METHODSETS lists, like we do in the pacakge view.
|
||||
Only expand them initially if short.
|
||||
- Include IMPLEMENTS and METHOD SETS information in search index.
|
||||
- URLs in IMPLEMENTS/METHOD SETS always link to source, even from the
|
||||
package docs view. This makes sense for links to non-exported
|
||||
types, but links to exported types and funcs should probably go to
|
||||
other package docs.
|
||||
- Suppress toggle divs for empty method sets.
|
||||
|
||||
Misc:
|
||||
- The [X] button in the lower pane is subject to scrolling.
|
||||
- Should the lower pane be floating? An iframe?
|
||||
When we change document.location by clicking on a link, it will go away.
|
||||
How do we prevent that (a la Gmail's chat windows)?
|
||||
- Progress/status: for each file, display its analysis status, one of:
|
||||
- not in analysis scope
|
||||
- type analysis running...
|
||||
- type analysis complete
|
||||
(+ optionally: there were type errors in this file)
|
||||
And if PTA requested:
|
||||
- type analysis complete; PTA not attempted due to type errors
|
||||
- PTA running...
|
||||
- PTA complete
|
||||
- Scroll the selection into view, e.g. the vertical center, or better
|
||||
still, under the pointer (assuming we have a mouse).
|
||||
|
||||
|
||||
More features
|
||||
-------------
|
||||
|
||||
Display the REFERRERS relation? (Useful but potentially large.)
|
||||
|
||||
Display the INSTANTIATIONS relation? i.e. given a type T, show the set of
|
||||
syntactic constructs that can instantiate it:
|
||||
var x T
|
||||
x := T{...}
|
||||
x = new(T)
|
||||
x = make([]T, n)
|
||||
etc
|
||||
+ all INSTANTIATIONS of all S defined as struct{t T} or [n]T
|
||||
(Potentially a lot of information.)
|
||||
(Add this to guru too.)
|
||||
|
||||
|
||||
Optimisations
|
||||
-------------
|
||||
|
||||
Each call to addLink takes a (per-file) lock. The locking is
|
||||
fine-grained so server latency isn't terrible, but overall it makes
|
||||
the link computation quite slow. Batch update might be better.
|
||||
|
||||
Memory usage is now about 1.5GB for GOROOT + go.tools. It used to be 700MB.
|
||||
|
||||
Optimize for time and space. The main slowdown is the network I/O
|
||||
time caused by an increase in page size of about 3x: about 2x from
|
||||
HTML, and 0.7--2.1x from JSON (unindented vs indented). The JSON
|
||||
contains a lot of filenames (e.g. 820 copies of 16 distinct
|
||||
filenames). 20% of the HTML is L%d spans (now disabled). The HTML
|
||||
also contains lots of tooltips for long struct/interface types.
|
||||
De-dup or just abbreviate? The actual formatting is very fast.
|
613
vendor/golang.org/x/tools/godoc/analysis/analysis.go
generated
vendored
Normal file
613
vendor/golang.org/x/tools/godoc/analysis/analysis.go
generated
vendored
Normal file
@@ -0,0 +1,613 @@
|
||||
// Copyright 2014 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 analysis performs type and pointer analysis
|
||||
// and generates mark-up for the Go source view.
|
||||
//
|
||||
// The Run method populates a Result object by running type and
|
||||
// (optionally) pointer analysis. The Result object is thread-safe
|
||||
// and at all times may be accessed by a serving thread, even as it is
|
||||
// progressively populated as analysis facts are derived.
|
||||
//
|
||||
// The Result is a mapping from each godoc file URL
|
||||
// (e.g. /src/fmt/print.go) to information about that file. The
|
||||
// information is a list of HTML markup links and a JSON array of
|
||||
// structured data values. Some of the links call client-side
|
||||
// JavaScript functions that index this array.
|
||||
//
|
||||
// The analysis computes mark-up for the following relations:
|
||||
//
|
||||
// IMPORTS: for each ast.ImportSpec, the package that it denotes.
|
||||
//
|
||||
// RESOLUTION: for each ast.Ident, its kind and type, and the location
|
||||
// of its definition.
|
||||
//
|
||||
// METHOD SETS, IMPLEMENTS: for each ast.Ident defining a named type,
|
||||
// its method-set, the set of interfaces it implements or is
|
||||
// implemented by, and its size/align values.
|
||||
//
|
||||
// CALLERS, CALLEES: for each function declaration ('func' token), its
|
||||
// callers, and for each call-site ('(' token), its callees.
|
||||
//
|
||||
// CALLGRAPH: the package docs include an interactive viewer for the
|
||||
// intra-package call graph of "fmt".
|
||||
//
|
||||
// CHANNEL PEERS: for each channel operation make/<-/close, the set of
|
||||
// other channel ops that alias the same channel(s).
|
||||
//
|
||||
// ERRORS: for each locus of a frontend (scanner/parser/type) error, the
|
||||
// location is highlighted in red and hover text provides the compiler
|
||||
// error message.
|
||||
//
|
||||
package analysis // import "golang.org/x/tools/godoc/analysis"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/build"
|
||||
"go/scanner"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"html"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/tools/go/loader"
|
||||
"golang.org/x/tools/go/pointer"
|
||||
"golang.org/x/tools/go/ssa"
|
||||
"golang.org/x/tools/go/ssa/ssautil"
|
||||
)
|
||||
|
||||
// -- links ------------------------------------------------------------
|
||||
|
||||
// A Link is an HTML decoration of the bytes [Start, End) of a file.
|
||||
// Write is called before/after those bytes to emit the mark-up.
|
||||
type Link interface {
|
||||
Start() int
|
||||
End() int
|
||||
Write(w io.Writer, _ int, start bool) // the godoc.LinkWriter signature
|
||||
}
|
||||
|
||||
// An <a> element.
|
||||
type aLink struct {
|
||||
start, end int // =godoc.Segment
|
||||
title string // hover text
|
||||
onclick string // JS code (NB: trusted)
|
||||
href string // URL (NB: trusted)
|
||||
}
|
||||
|
||||
func (a aLink) Start() int { return a.start }
|
||||
func (a aLink) End() int { return a.end }
|
||||
func (a aLink) Write(w io.Writer, _ int, start bool) {
|
||||
if start {
|
||||
fmt.Fprintf(w, `<a title='%s'`, html.EscapeString(a.title))
|
||||
if a.onclick != "" {
|
||||
fmt.Fprintf(w, ` onclick='%s'`, html.EscapeString(a.onclick))
|
||||
}
|
||||
if a.href != "" {
|
||||
// TODO(adonovan): I think that in principle, a.href must first be
|
||||
// url.QueryEscape'd, but if I do that, a leading slash becomes "%2F",
|
||||
// which causes the browser to treat the path as relative, not absolute.
|
||||
// WTF?
|
||||
fmt.Fprintf(w, ` href='%s'`, html.EscapeString(a.href))
|
||||
}
|
||||
fmt.Fprintf(w, ">")
|
||||
} else {
|
||||
fmt.Fprintf(w, "</a>")
|
||||
}
|
||||
}
|
||||
|
||||
// An <a class='error'> element.
|
||||
type errorLink struct {
|
||||
start int
|
||||
msg string
|
||||
}
|
||||
|
||||
func (e errorLink) Start() int { return e.start }
|
||||
func (e errorLink) End() int { return e.start + 1 }
|
||||
|
||||
func (e errorLink) Write(w io.Writer, _ int, start bool) {
|
||||
// <span> causes havoc, not sure why, so use <a>.
|
||||
if start {
|
||||
fmt.Fprintf(w, `<a class='error' title='%s'>`, html.EscapeString(e.msg))
|
||||
} else {
|
||||
fmt.Fprintf(w, "</a>")
|
||||
}
|
||||
}
|
||||
|
||||
// -- fileInfo ---------------------------------------------------------
|
||||
|
||||
// FileInfo holds analysis information for the source file view.
|
||||
// Clients must not mutate it.
|
||||
type FileInfo struct {
|
||||
Data []interface{} // JSON serializable values
|
||||
Links []Link // HTML link markup
|
||||
}
|
||||
|
||||
// A fileInfo is the server's store of hyperlinks and JSON data for a
|
||||
// particular file.
|
||||
type fileInfo struct {
|
||||
mu sync.Mutex
|
||||
data []interface{} // JSON objects
|
||||
links []Link
|
||||
sorted bool
|
||||
hasErrors bool // TODO(adonovan): surface this in the UI
|
||||
}
|
||||
|
||||
// addLink adds a link to the Go source file fi.
|
||||
func (fi *fileInfo) addLink(link Link) {
|
||||
fi.mu.Lock()
|
||||
fi.links = append(fi.links, link)
|
||||
fi.sorted = false
|
||||
if _, ok := link.(errorLink); ok {
|
||||
fi.hasErrors = true
|
||||
}
|
||||
fi.mu.Unlock()
|
||||
}
|
||||
|
||||
// addData adds the structured value x to the JSON data for the Go
|
||||
// source file fi. Its index is returned.
|
||||
func (fi *fileInfo) addData(x interface{}) int {
|
||||
fi.mu.Lock()
|
||||
index := len(fi.data)
|
||||
fi.data = append(fi.data, x)
|
||||
fi.mu.Unlock()
|
||||
return index
|
||||
}
|
||||
|
||||
// get returns the file info in external form.
|
||||
// Callers must not mutate its fields.
|
||||
func (fi *fileInfo) get() FileInfo {
|
||||
var r FileInfo
|
||||
// Copy slices, to avoid races.
|
||||
fi.mu.Lock()
|
||||
r.Data = append(r.Data, fi.data...)
|
||||
if !fi.sorted {
|
||||
sort.Sort(linksByStart(fi.links))
|
||||
fi.sorted = true
|
||||
}
|
||||
r.Links = append(r.Links, fi.links...)
|
||||
fi.mu.Unlock()
|
||||
return r
|
||||
}
|
||||
|
||||
// PackageInfo holds analysis information for the package view.
|
||||
// Clients must not mutate it.
|
||||
type PackageInfo struct {
|
||||
CallGraph []*PCGNodeJSON
|
||||
CallGraphIndex map[string]int
|
||||
Types []*TypeInfoJSON
|
||||
}
|
||||
|
||||
type pkgInfo struct {
|
||||
mu sync.Mutex
|
||||
callGraph []*PCGNodeJSON
|
||||
callGraphIndex map[string]int // keys are (*ssa.Function).RelString()
|
||||
types []*TypeInfoJSON // type info for exported types
|
||||
}
|
||||
|
||||
func (pi *pkgInfo) setCallGraph(callGraph []*PCGNodeJSON, callGraphIndex map[string]int) {
|
||||
pi.mu.Lock()
|
||||
pi.callGraph = callGraph
|
||||
pi.callGraphIndex = callGraphIndex
|
||||
pi.mu.Unlock()
|
||||
}
|
||||
|
||||
func (pi *pkgInfo) addType(t *TypeInfoJSON) {
|
||||
pi.mu.Lock()
|
||||
pi.types = append(pi.types, t)
|
||||
pi.mu.Unlock()
|
||||
}
|
||||
|
||||
// get returns the package info in external form.
|
||||
// Callers must not mutate its fields.
|
||||
func (pi *pkgInfo) get() PackageInfo {
|
||||
var r PackageInfo
|
||||
// Copy slices, to avoid races.
|
||||
pi.mu.Lock()
|
||||
r.CallGraph = append(r.CallGraph, pi.callGraph...)
|
||||
r.CallGraphIndex = pi.callGraphIndex
|
||||
r.Types = append(r.Types, pi.types...)
|
||||
pi.mu.Unlock()
|
||||
return r
|
||||
}
|
||||
|
||||
// -- Result -----------------------------------------------------------
|
||||
|
||||
// Result contains the results of analysis.
|
||||
// The result contains a mapping from filenames to a set of HTML links
|
||||
// and JavaScript data referenced by the links.
|
||||
type Result struct {
|
||||
mu sync.Mutex // guards maps (but not their contents)
|
||||
status string // global analysis status
|
||||
fileInfos map[string]*fileInfo // keys are godoc file URLs
|
||||
pkgInfos map[string]*pkgInfo // keys are import paths
|
||||
}
|
||||
|
||||
// fileInfo returns the fileInfo for the specified godoc file URL,
|
||||
// constructing it as needed. Thread-safe.
|
||||
func (res *Result) fileInfo(url string) *fileInfo {
|
||||
res.mu.Lock()
|
||||
fi, ok := res.fileInfos[url]
|
||||
if !ok {
|
||||
if res.fileInfos == nil {
|
||||
res.fileInfos = make(map[string]*fileInfo)
|
||||
}
|
||||
fi = new(fileInfo)
|
||||
res.fileInfos[url] = fi
|
||||
}
|
||||
res.mu.Unlock()
|
||||
return fi
|
||||
}
|
||||
|
||||
// Status returns a human-readable description of the current analysis status.
|
||||
func (res *Result) Status() string {
|
||||
res.mu.Lock()
|
||||
defer res.mu.Unlock()
|
||||
return res.status
|
||||
}
|
||||
|
||||
func (res *Result) setStatusf(format string, args ...interface{}) {
|
||||
res.mu.Lock()
|
||||
res.status = fmt.Sprintf(format, args...)
|
||||
log.Printf(format, args...)
|
||||
res.mu.Unlock()
|
||||
}
|
||||
|
||||
// FileInfo returns new slices containing opaque JSON values and the
|
||||
// HTML link markup for the specified godoc file URL. Thread-safe.
|
||||
// Callers must not mutate the elements.
|
||||
// It returns "zero" if no data is available.
|
||||
//
|
||||
func (res *Result) FileInfo(url string) (fi FileInfo) {
|
||||
return res.fileInfo(url).get()
|
||||
}
|
||||
|
||||
// pkgInfo returns the pkgInfo for the specified import path,
|
||||
// constructing it as needed. Thread-safe.
|
||||
func (res *Result) pkgInfo(importPath string) *pkgInfo {
|
||||
res.mu.Lock()
|
||||
pi, ok := res.pkgInfos[importPath]
|
||||
if !ok {
|
||||
if res.pkgInfos == nil {
|
||||
res.pkgInfos = make(map[string]*pkgInfo)
|
||||
}
|
||||
pi = new(pkgInfo)
|
||||
res.pkgInfos[importPath] = pi
|
||||
}
|
||||
res.mu.Unlock()
|
||||
return pi
|
||||
}
|
||||
|
||||
// PackageInfo returns new slices of JSON values for the callgraph and
|
||||
// type info for the specified package. Thread-safe.
|
||||
// Callers must not mutate its fields.
|
||||
// PackageInfo returns "zero" if no data is available.
|
||||
//
|
||||
func (res *Result) PackageInfo(importPath string) PackageInfo {
|
||||
return res.pkgInfo(importPath).get()
|
||||
}
|
||||
|
||||
// -- analysis ---------------------------------------------------------
|
||||
|
||||
type analysis struct {
|
||||
result *Result
|
||||
prog *ssa.Program
|
||||
ops []chanOp // all channel ops in program
|
||||
allNamed []*types.Named // all "defined" (formerly "named") types in the program
|
||||
ptaConfig pointer.Config
|
||||
path2url map[string]string // maps openable path to godoc file URL (/src/fmt/print.go)
|
||||
pcgs map[*ssa.Package]*packageCallGraph
|
||||
}
|
||||
|
||||
// fileAndOffset returns the file and offset for a given pos.
|
||||
func (a *analysis) fileAndOffset(pos token.Pos) (fi *fileInfo, offset int) {
|
||||
return a.fileAndOffsetPosn(a.prog.Fset.Position(pos))
|
||||
}
|
||||
|
||||
// fileAndOffsetPosn returns the file and offset for a given position.
|
||||
func (a *analysis) fileAndOffsetPosn(posn token.Position) (fi *fileInfo, offset int) {
|
||||
url := a.path2url[posn.Filename]
|
||||
return a.result.fileInfo(url), posn.Offset
|
||||
}
|
||||
|
||||
// posURL returns the URL of the source extent [pos, pos+len).
|
||||
func (a *analysis) posURL(pos token.Pos, len int) string {
|
||||
if pos == token.NoPos {
|
||||
return ""
|
||||
}
|
||||
posn := a.prog.Fset.Position(pos)
|
||||
url := a.path2url[posn.Filename]
|
||||
return fmt.Sprintf("%s?s=%d:%d#L%d",
|
||||
url, posn.Offset, posn.Offset+len, posn.Line)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
// Run runs program analysis and computes the resulting markup,
|
||||
// populating *result in a thread-safe manner, first with type
|
||||
// information then later with pointer analysis information if
|
||||
// enabled by the pta flag.
|
||||
//
|
||||
func Run(pta bool, result *Result) {
|
||||
conf := loader.Config{
|
||||
AllowErrors: true,
|
||||
}
|
||||
|
||||
// Silence the default error handler.
|
||||
// Don't print all errors; we'll report just
|
||||
// one per errant package later.
|
||||
conf.TypeChecker.Error = func(e error) {}
|
||||
|
||||
var roots, args []string // roots[i] ends with os.PathSeparator
|
||||
|
||||
// Enumerate packages in $GOROOT.
|
||||
root := filepath.Join(build.Default.GOROOT, "src") + string(os.PathSeparator)
|
||||
roots = append(roots, root)
|
||||
args = allPackages(root)
|
||||
log.Printf("GOROOT=%s: %s\n", root, args)
|
||||
|
||||
// Enumerate packages in $GOPATH.
|
||||
for i, dir := range filepath.SplitList(build.Default.GOPATH) {
|
||||
root := filepath.Join(dir, "src") + string(os.PathSeparator)
|
||||
roots = append(roots, root)
|
||||
pkgs := allPackages(root)
|
||||
log.Printf("GOPATH[%d]=%s: %s\n", i, root, pkgs)
|
||||
args = append(args, pkgs...)
|
||||
}
|
||||
|
||||
// Uncomment to make startup quicker during debugging.
|
||||
//args = []string{"golang.org/x/tools/cmd/godoc"}
|
||||
//args = []string{"fmt"}
|
||||
|
||||
if _, err := conf.FromArgs(args, true); err != nil {
|
||||
// TODO(adonovan): degrade gracefully, not fail totally.
|
||||
// (The crippling case is a parse error in an external test file.)
|
||||
result.setStatusf("Analysis failed: %s.", err) // import error
|
||||
return
|
||||
}
|
||||
|
||||
result.setStatusf("Loading and type-checking packages...")
|
||||
iprog, err := conf.Load()
|
||||
if iprog != nil {
|
||||
// Report only the first error of each package.
|
||||
for _, info := range iprog.AllPackages {
|
||||
for _, err := range info.Errors {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
break
|
||||
}
|
||||
}
|
||||
log.Printf("Loaded %d packages.", len(iprog.AllPackages))
|
||||
}
|
||||
if err != nil {
|
||||
result.setStatusf("Loading failed: %s.\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create SSA-form program representation.
|
||||
// Only the transitively error-free packages are used.
|
||||
prog := ssautil.CreateProgram(iprog, ssa.GlobalDebug)
|
||||
|
||||
// Create a "testmain" package for each package with tests.
|
||||
for _, pkg := range prog.AllPackages() {
|
||||
if testmain := prog.CreateTestMainPackage(pkg); testmain != nil {
|
||||
log.Printf("Adding tests for %s", pkg.Pkg.Path())
|
||||
}
|
||||
}
|
||||
|
||||
// Build SSA code for bodies of all functions in the whole program.
|
||||
result.setStatusf("Constructing SSA form...")
|
||||
prog.Build()
|
||||
log.Print("SSA construction complete")
|
||||
|
||||
a := analysis{
|
||||
result: result,
|
||||
prog: prog,
|
||||
pcgs: make(map[*ssa.Package]*packageCallGraph),
|
||||
}
|
||||
|
||||
// Build a mapping from openable filenames to godoc file URLs,
|
||||
// i.e. "/src/" plus path relative to GOROOT/src or GOPATH[i]/src.
|
||||
a.path2url = make(map[string]string)
|
||||
for _, info := range iprog.AllPackages {
|
||||
nextfile:
|
||||
for _, f := range info.Files {
|
||||
if f.Pos() == 0 {
|
||||
continue // e.g. files generated by cgo
|
||||
}
|
||||
abs := iprog.Fset.File(f.Pos()).Name()
|
||||
// Find the root to which this file belongs.
|
||||
for _, root := range roots {
|
||||
rel := strings.TrimPrefix(abs, root)
|
||||
if len(rel) < len(abs) {
|
||||
a.path2url[abs] = "/src/" + filepath.ToSlash(rel)
|
||||
continue nextfile
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Can't locate file %s (package %q) beneath any root",
|
||||
abs, info.Pkg.Path())
|
||||
}
|
||||
}
|
||||
|
||||
// Add links for scanner, parser, type-checker errors.
|
||||
// TODO(adonovan): fix: these links can overlap with
|
||||
// identifier markup, causing the renderer to emit some
|
||||
// characters twice.
|
||||
errors := make(map[token.Position][]string)
|
||||
for _, info := range iprog.AllPackages {
|
||||
for _, err := range info.Errors {
|
||||
switch err := err.(type) {
|
||||
case types.Error:
|
||||
posn := a.prog.Fset.Position(err.Pos)
|
||||
errors[posn] = append(errors[posn], err.Msg)
|
||||
case scanner.ErrorList:
|
||||
for _, e := range err {
|
||||
errors[e.Pos] = append(errors[e.Pos], e.Msg)
|
||||
}
|
||||
default:
|
||||
log.Printf("Package %q has error (%T) without position: %v\n",
|
||||
info.Pkg.Path(), err, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
for posn, errs := range errors {
|
||||
fi, offset := a.fileAndOffsetPosn(posn)
|
||||
fi.addLink(errorLink{
|
||||
start: offset,
|
||||
msg: strings.Join(errs, "\n"),
|
||||
})
|
||||
}
|
||||
|
||||
// ---------- type-based analyses ----------
|
||||
|
||||
// Compute the all-pairs IMPLEMENTS relation.
|
||||
// Collect all named types, even local types
|
||||
// (which can have methods via promotion)
|
||||
// and the built-in "error".
|
||||
errorType := types.Universe.Lookup("error").Type().(*types.Named)
|
||||
a.allNamed = append(a.allNamed, errorType)
|
||||
for _, info := range iprog.AllPackages {
|
||||
for _, obj := range info.Defs {
|
||||
if obj, ok := obj.(*types.TypeName); ok {
|
||||
if named, ok := obj.Type().(*types.Named); ok {
|
||||
a.allNamed = append(a.allNamed, named)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Print("Computing implements relation...")
|
||||
facts := computeImplements(&a.prog.MethodSets, a.allNamed)
|
||||
|
||||
// Add the type-based analysis results.
|
||||
log.Print("Extracting type info...")
|
||||
for _, info := range iprog.AllPackages {
|
||||
a.doTypeInfo(info, facts)
|
||||
}
|
||||
|
||||
a.visitInstrs(pta)
|
||||
|
||||
result.setStatusf("Type analysis complete.")
|
||||
|
||||
if pta {
|
||||
mainPkgs := ssautil.MainPackages(prog.AllPackages())
|
||||
log.Print("Transitively error-free main packages: ", mainPkgs)
|
||||
a.pointer(mainPkgs)
|
||||
}
|
||||
}
|
||||
|
||||
// visitInstrs visits all SSA instructions in the program.
|
||||
func (a *analysis) visitInstrs(pta bool) {
|
||||
log.Print("Visit instructions...")
|
||||
for fn := range ssautil.AllFunctions(a.prog) {
|
||||
for _, b := range fn.Blocks {
|
||||
for _, instr := range b.Instrs {
|
||||
// CALLEES (static)
|
||||
// (Dynamic calls require pointer analysis.)
|
||||
//
|
||||
// We use the SSA representation to find the static callee,
|
||||
// since in many cases it does better than the
|
||||
// types.Info.{Refs,Selection} information. For example:
|
||||
//
|
||||
// defer func(){}() // static call to anon function
|
||||
// f := func(){}; f() // static call to anon function
|
||||
// f := fmt.Println; f() // static call to named function
|
||||
//
|
||||
// The downside is that we get no static callee information
|
||||
// for packages that (transitively) contain errors.
|
||||
if site, ok := instr.(ssa.CallInstruction); ok {
|
||||
if callee := site.Common().StaticCallee(); callee != nil {
|
||||
// TODO(adonovan): callgraph: elide wrappers.
|
||||
// (Do static calls ever go to wrappers?)
|
||||
if site.Common().Pos() != token.NoPos {
|
||||
a.addCallees(site, []*ssa.Function{callee})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !pta {
|
||||
continue
|
||||
}
|
||||
|
||||
// CHANNEL PEERS
|
||||
// Collect send/receive/close instructions in the whole ssa.Program.
|
||||
for _, op := range chanOps(instr) {
|
||||
a.ops = append(a.ops, op)
|
||||
a.ptaConfig.AddQuery(op.ch) // add channel ssa.Value to PTA query
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Print("Visit instructions complete")
|
||||
}
|
||||
|
||||
// pointer runs the pointer analysis.
|
||||
func (a *analysis) pointer(mainPkgs []*ssa.Package) {
|
||||
// Run the pointer analysis and build the complete callgraph.
|
||||
a.ptaConfig.Mains = mainPkgs
|
||||
a.ptaConfig.BuildCallGraph = true
|
||||
a.ptaConfig.Reflection = false // (for now)
|
||||
|
||||
a.result.setStatusf("Pointer analysis running...")
|
||||
|
||||
ptares, err := pointer.Analyze(&a.ptaConfig)
|
||||
if err != nil {
|
||||
// If this happens, it indicates a bug.
|
||||
a.result.setStatusf("Pointer analysis failed: %s.", err)
|
||||
return
|
||||
}
|
||||
log.Print("Pointer analysis complete.")
|
||||
|
||||
// Add the results of pointer analysis.
|
||||
|
||||
a.result.setStatusf("Computing channel peers...")
|
||||
a.doChannelPeers(ptares.Queries)
|
||||
a.result.setStatusf("Computing dynamic call graph edges...")
|
||||
a.doCallgraph(ptares.CallGraph)
|
||||
|
||||
a.result.setStatusf("Analysis complete.")
|
||||
}
|
||||
|
||||
type linksByStart []Link
|
||||
|
||||
func (a linksByStart) Less(i, j int) bool { return a[i].Start() < a[j].Start() }
|
||||
func (a linksByStart) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a linksByStart) Len() int { return len(a) }
|
||||
|
||||
// allPackages returns a new sorted slice of all packages beneath the
|
||||
// specified package root directory, e.g. $GOROOT/src or $GOPATH/src.
|
||||
// Derived from from go/ssa/stdlib_test.go
|
||||
// root must end with os.PathSeparator.
|
||||
//
|
||||
// TODO(adonovan): use buildutil.AllPackages when the tree thaws.
|
||||
func allPackages(root string) []string {
|
||||
var pkgs []string
|
||||
filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
if info == nil {
|
||||
return nil // non-existent root directory?
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return nil // not a directory
|
||||
}
|
||||
// Prune the search if we encounter any of these names:
|
||||
base := filepath.Base(path)
|
||||
if base == "testdata" || strings.HasPrefix(base, ".") {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
pkg := filepath.ToSlash(strings.TrimPrefix(path, root))
|
||||
switch pkg {
|
||||
case "builtin":
|
||||
return filepath.SkipDir
|
||||
case "":
|
||||
return nil // ignore root of tree
|
||||
}
|
||||
pkgs = append(pkgs, pkg)
|
||||
return nil
|
||||
})
|
||||
return pkgs
|
||||
}
|
351
vendor/golang.org/x/tools/godoc/analysis/callgraph.go
generated
vendored
Normal file
351
vendor/golang.org/x/tools/godoc/analysis/callgraph.go
generated
vendored
Normal file
@@ -0,0 +1,351 @@
|
||||
// Copyright 2014 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 analysis
|
||||
|
||||
// This file computes the CALLERS and CALLEES relations from the call
|
||||
// graph. CALLERS/CALLEES information is displayed in the lower pane
|
||||
// when a "func" token or ast.CallExpr.Lparen is clicked, respectively.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"log"
|
||||
"math/big"
|
||||
"sort"
|
||||
|
||||
"golang.org/x/tools/go/callgraph"
|
||||
"golang.org/x/tools/go/ssa"
|
||||
)
|
||||
|
||||
// doCallgraph computes the CALLEES and CALLERS relations.
|
||||
func (a *analysis) doCallgraph(cg *callgraph.Graph) {
|
||||
log.Print("Deleting synthetic nodes...")
|
||||
// TODO(adonovan): opt: DeleteSyntheticNodes is asymptotically
|
||||
// inefficient and can be (unpredictably) slow.
|
||||
cg.DeleteSyntheticNodes()
|
||||
log.Print("Synthetic nodes deleted")
|
||||
|
||||
// Populate nodes of package call graphs (PCGs).
|
||||
for _, n := range cg.Nodes {
|
||||
a.pcgAddNode(n.Func)
|
||||
}
|
||||
// Within each PCG, sort funcs by name.
|
||||
for _, pcg := range a.pcgs {
|
||||
pcg.sortNodes()
|
||||
}
|
||||
|
||||
calledFuncs := make(map[ssa.CallInstruction]map[*ssa.Function]bool)
|
||||
callingSites := make(map[*ssa.Function]map[ssa.CallInstruction]bool)
|
||||
for _, n := range cg.Nodes {
|
||||
for _, e := range n.Out {
|
||||
if e.Site == nil {
|
||||
continue // a call from a synthetic node such as <root>
|
||||
}
|
||||
|
||||
// Add (site pos, callee) to calledFuncs.
|
||||
// (Dynamic calls only.)
|
||||
callee := e.Callee.Func
|
||||
|
||||
a.pcgAddEdge(n.Func, callee)
|
||||
|
||||
if callee.Synthetic != "" {
|
||||
continue // call of a package initializer
|
||||
}
|
||||
|
||||
if e.Site.Common().StaticCallee() == nil {
|
||||
// dynamic call
|
||||
// (CALLEES information for static calls
|
||||
// is computed using SSA information.)
|
||||
lparen := e.Site.Common().Pos()
|
||||
if lparen != token.NoPos {
|
||||
fns := calledFuncs[e.Site]
|
||||
if fns == nil {
|
||||
fns = make(map[*ssa.Function]bool)
|
||||
calledFuncs[e.Site] = fns
|
||||
}
|
||||
fns[callee] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Add (callee, site) to callingSites.
|
||||
fns := callingSites[callee]
|
||||
if fns == nil {
|
||||
fns = make(map[ssa.CallInstruction]bool)
|
||||
callingSites[callee] = fns
|
||||
}
|
||||
fns[e.Site] = true
|
||||
}
|
||||
}
|
||||
|
||||
// CALLEES.
|
||||
log.Print("Callees...")
|
||||
for site, fns := range calledFuncs {
|
||||
var funcs funcsByPos
|
||||
for fn := range fns {
|
||||
funcs = append(funcs, fn)
|
||||
}
|
||||
sort.Sort(funcs)
|
||||
|
||||
a.addCallees(site, funcs)
|
||||
}
|
||||
|
||||
// CALLERS
|
||||
log.Print("Callers...")
|
||||
for callee, sites := range callingSites {
|
||||
pos := funcToken(callee)
|
||||
if pos == token.NoPos {
|
||||
log.Printf("CALLERS: skipping %s: no pos", callee)
|
||||
continue
|
||||
}
|
||||
|
||||
var this *types.Package // for relativizing names
|
||||
if callee.Pkg != nil {
|
||||
this = callee.Pkg.Pkg
|
||||
}
|
||||
|
||||
// Compute sites grouped by parent, with text and URLs.
|
||||
sitesByParent := make(map[*ssa.Function]sitesByPos)
|
||||
for site := range sites {
|
||||
fn := site.Parent()
|
||||
sitesByParent[fn] = append(sitesByParent[fn], site)
|
||||
}
|
||||
var funcs funcsByPos
|
||||
for fn := range sitesByParent {
|
||||
funcs = append(funcs, fn)
|
||||
}
|
||||
sort.Sort(funcs)
|
||||
|
||||
v := callersJSON{
|
||||
Callee: callee.String(),
|
||||
Callers: []callerJSON{}, // (JS wants non-nil)
|
||||
}
|
||||
for _, fn := range funcs {
|
||||
caller := callerJSON{
|
||||
Func: prettyFunc(this, fn),
|
||||
Sites: []anchorJSON{}, // (JS wants non-nil)
|
||||
}
|
||||
sites := sitesByParent[fn]
|
||||
sort.Sort(sites)
|
||||
for _, site := range sites {
|
||||
pos := site.Common().Pos()
|
||||
if pos != token.NoPos {
|
||||
caller.Sites = append(caller.Sites, anchorJSON{
|
||||
Text: fmt.Sprintf("%d", a.prog.Fset.Position(pos).Line),
|
||||
Href: a.posURL(pos, len("(")),
|
||||
})
|
||||
}
|
||||
}
|
||||
v.Callers = append(v.Callers, caller)
|
||||
}
|
||||
|
||||
fi, offset := a.fileAndOffset(pos)
|
||||
fi.addLink(aLink{
|
||||
start: offset,
|
||||
end: offset + len("func"),
|
||||
title: fmt.Sprintf("%d callers", len(sites)),
|
||||
onclick: fmt.Sprintf("onClickCallers(%d)", fi.addData(v)),
|
||||
})
|
||||
}
|
||||
|
||||
// PACKAGE CALLGRAPH
|
||||
log.Print("Package call graph...")
|
||||
for pkg, pcg := range a.pcgs {
|
||||
// Maps (*ssa.Function).RelString() to index in JSON CALLGRAPH array.
|
||||
index := make(map[string]int)
|
||||
|
||||
// Treat exported functions (and exported methods of
|
||||
// exported named types) as roots even if they aren't
|
||||
// actually called from outside the package.
|
||||
for i, n := range pcg.nodes {
|
||||
if i == 0 || n.fn.Object() == nil || !n.fn.Object().Exported() {
|
||||
continue
|
||||
}
|
||||
recv := n.fn.Signature.Recv()
|
||||
if recv == nil || deref(recv.Type()).(*types.Named).Obj().Exported() {
|
||||
roots := &pcg.nodes[0].edges
|
||||
roots.SetBit(roots, i, 1)
|
||||
}
|
||||
index[n.fn.RelString(pkg.Pkg)] = i
|
||||
}
|
||||
|
||||
json := a.pcgJSON(pcg)
|
||||
|
||||
// TODO(adonovan): pkg.Path() is not unique!
|
||||
// It is possible to declare a non-test package called x_test.
|
||||
a.result.pkgInfo(pkg.Pkg.Path()).setCallGraph(json, index)
|
||||
}
|
||||
}
|
||||
|
||||
// addCallees adds client data and links for the facts that site calls fns.
|
||||
func (a *analysis) addCallees(site ssa.CallInstruction, fns []*ssa.Function) {
|
||||
v := calleesJSON{
|
||||
Descr: site.Common().Description(),
|
||||
Callees: []anchorJSON{}, // (JS wants non-nil)
|
||||
}
|
||||
var this *types.Package // for relativizing names
|
||||
if p := site.Parent().Package(); p != nil {
|
||||
this = p.Pkg
|
||||
}
|
||||
|
||||
for _, fn := range fns {
|
||||
v.Callees = append(v.Callees, anchorJSON{
|
||||
Text: prettyFunc(this, fn),
|
||||
Href: a.posURL(funcToken(fn), len("func")),
|
||||
})
|
||||
}
|
||||
|
||||
fi, offset := a.fileAndOffset(site.Common().Pos())
|
||||
fi.addLink(aLink{
|
||||
start: offset,
|
||||
end: offset + len("("),
|
||||
title: fmt.Sprintf("%d callees", len(v.Callees)),
|
||||
onclick: fmt.Sprintf("onClickCallees(%d)", fi.addData(v)),
|
||||
})
|
||||
}
|
||||
|
||||
// -- utilities --------------------------------------------------------
|
||||
|
||||
// stable order within packages but undefined across packages.
|
||||
type funcsByPos []*ssa.Function
|
||||
|
||||
func (a funcsByPos) Less(i, j int) bool { return a[i].Pos() < a[j].Pos() }
|
||||
func (a funcsByPos) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a funcsByPos) Len() int { return len(a) }
|
||||
|
||||
type sitesByPos []ssa.CallInstruction
|
||||
|
||||
func (a sitesByPos) Less(i, j int) bool { return a[i].Common().Pos() < a[j].Common().Pos() }
|
||||
func (a sitesByPos) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a sitesByPos) Len() int { return len(a) }
|
||||
|
||||
func funcToken(fn *ssa.Function) token.Pos {
|
||||
switch syntax := fn.Syntax().(type) {
|
||||
case *ast.FuncLit:
|
||||
return syntax.Type.Func
|
||||
case *ast.FuncDecl:
|
||||
return syntax.Type.Func
|
||||
}
|
||||
return token.NoPos
|
||||
}
|
||||
|
||||
// prettyFunc pretty-prints fn for the user interface.
|
||||
// TODO(adonovan): return HTML so we have more markup freedom.
|
||||
func prettyFunc(this *types.Package, fn *ssa.Function) string {
|
||||
if fn.Parent() != nil {
|
||||
return fmt.Sprintf("%s in %s",
|
||||
types.TypeString(fn.Signature, types.RelativeTo(this)),
|
||||
prettyFunc(this, fn.Parent()))
|
||||
}
|
||||
if fn.Synthetic != "" && fn.Name() == "init" {
|
||||
// (This is the actual initializer, not a declared 'func init').
|
||||
if fn.Pkg.Pkg == this {
|
||||
return "package initializer"
|
||||
}
|
||||
return fmt.Sprintf("%q package initializer", fn.Pkg.Pkg.Path())
|
||||
}
|
||||
return fn.RelString(this)
|
||||
}
|
||||
|
||||
// -- intra-package callgraph ------------------------------------------
|
||||
|
||||
// pcgNode represents a node in the package call graph (PCG).
|
||||
type pcgNode struct {
|
||||
fn *ssa.Function
|
||||
pretty string // cache of prettyFunc(fn)
|
||||
edges big.Int // set of callee func indices
|
||||
}
|
||||
|
||||
// A packageCallGraph represents the intra-package edges of the global call graph.
|
||||
// The zeroth node indicates "all external functions".
|
||||
type packageCallGraph struct {
|
||||
nodeIndex map[*ssa.Function]int // maps func to node index (a small int)
|
||||
nodes []*pcgNode // maps node index to node
|
||||
}
|
||||
|
||||
// sortNodes populates pcg.nodes in name order and updates the nodeIndex.
|
||||
func (pcg *packageCallGraph) sortNodes() {
|
||||
nodes := make([]*pcgNode, 0, len(pcg.nodeIndex))
|
||||
nodes = append(nodes, &pcgNode{fn: nil, pretty: "<external>"})
|
||||
for fn := range pcg.nodeIndex {
|
||||
nodes = append(nodes, &pcgNode{
|
||||
fn: fn,
|
||||
pretty: prettyFunc(fn.Pkg.Pkg, fn),
|
||||
})
|
||||
}
|
||||
sort.Sort(pcgNodesByPretty(nodes[1:]))
|
||||
for i, n := range nodes {
|
||||
pcg.nodeIndex[n.fn] = i
|
||||
}
|
||||
pcg.nodes = nodes
|
||||
}
|
||||
|
||||
func (pcg *packageCallGraph) addEdge(caller, callee *ssa.Function) {
|
||||
var callerIndex int
|
||||
if caller.Pkg == callee.Pkg {
|
||||
// intra-package edge
|
||||
callerIndex = pcg.nodeIndex[caller]
|
||||
if callerIndex < 1 {
|
||||
panic(caller)
|
||||
}
|
||||
}
|
||||
edges := &pcg.nodes[callerIndex].edges
|
||||
edges.SetBit(edges, pcg.nodeIndex[callee], 1)
|
||||
}
|
||||
|
||||
func (a *analysis) pcgAddNode(fn *ssa.Function) {
|
||||
if fn.Pkg == nil {
|
||||
return
|
||||
}
|
||||
pcg, ok := a.pcgs[fn.Pkg]
|
||||
if !ok {
|
||||
pcg = &packageCallGraph{nodeIndex: make(map[*ssa.Function]int)}
|
||||
a.pcgs[fn.Pkg] = pcg
|
||||
}
|
||||
pcg.nodeIndex[fn] = -1
|
||||
}
|
||||
|
||||
func (a *analysis) pcgAddEdge(caller, callee *ssa.Function) {
|
||||
if callee.Pkg != nil {
|
||||
a.pcgs[callee.Pkg].addEdge(caller, callee)
|
||||
}
|
||||
}
|
||||
|
||||
// pcgJSON returns a new slice of callgraph JSON values.
|
||||
func (a *analysis) pcgJSON(pcg *packageCallGraph) []*PCGNodeJSON {
|
||||
var nodes []*PCGNodeJSON
|
||||
for _, n := range pcg.nodes {
|
||||
|
||||
// TODO(adonovan): why is there no good way to iterate
|
||||
// over the set bits of a big.Int?
|
||||
var callees []int
|
||||
nbits := n.edges.BitLen()
|
||||
for j := 0; j < nbits; j++ {
|
||||
if n.edges.Bit(j) == 1 {
|
||||
callees = append(callees, j)
|
||||
}
|
||||
}
|
||||
|
||||
var pos token.Pos
|
||||
if n.fn != nil {
|
||||
pos = funcToken(n.fn)
|
||||
}
|
||||
nodes = append(nodes, &PCGNodeJSON{
|
||||
Func: anchorJSON{
|
||||
Text: n.pretty,
|
||||
Href: a.posURL(pos, len("func")),
|
||||
},
|
||||
Callees: callees,
|
||||
})
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
type pcgNodesByPretty []*pcgNode
|
||||
|
||||
func (a pcgNodesByPretty) Less(i, j int) bool { return a[i].pretty < a[j].pretty }
|
||||
func (a pcgNodesByPretty) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a pcgNodesByPretty) Len() int { return len(a) }
|
195
vendor/golang.org/x/tools/godoc/analysis/implements.go
generated
vendored
Normal file
195
vendor/golang.org/x/tools/godoc/analysis/implements.go
generated
vendored
Normal file
@@ -0,0 +1,195 @@
|
||||
// Copyright 2014 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 analysis
|
||||
|
||||
// This file computes the "implements" relation over all pairs of
|
||||
// named types in the program. (The mark-up is done by typeinfo.go.)
|
||||
|
||||
// TODO(adonovan): do we want to report implements(C, I) where C and I
|
||||
// belong to different packages and at least one is not exported?
|
||||
|
||||
import (
|
||||
"go/types"
|
||||
"sort"
|
||||
|
||||
"golang.org/x/tools/go/types/typeutil"
|
||||
)
|
||||
|
||||
// computeImplements computes the "implements" relation over all pairs
|
||||
// of named types in allNamed.
|
||||
func computeImplements(cache *typeutil.MethodSetCache, allNamed []*types.Named) map[*types.Named]implementsFacts {
|
||||
// Information about a single type's method set.
|
||||
type msetInfo struct {
|
||||
typ types.Type
|
||||
mset *types.MethodSet
|
||||
mask1, mask2 uint64
|
||||
}
|
||||
|
||||
initMsetInfo := func(info *msetInfo, typ types.Type) {
|
||||
info.typ = typ
|
||||
info.mset = cache.MethodSet(typ)
|
||||
for i := 0; i < info.mset.Len(); i++ {
|
||||
name := info.mset.At(i).Obj().Name()
|
||||
info.mask1 |= 1 << methodBit(name[0])
|
||||
info.mask2 |= 1 << methodBit(name[len(name)-1])
|
||||
}
|
||||
}
|
||||
|
||||
// satisfies(T, U) reports whether type T satisfies type U.
|
||||
// U must be an interface.
|
||||
//
|
||||
// Since there are thousands of types (and thus millions of
|
||||
// pairs of types) and types.Assignable(T, U) is relatively
|
||||
// expensive, we compute assignability directly from the
|
||||
// method sets. (At least one of T and U must be an
|
||||
// interface.)
|
||||
//
|
||||
// We use a trick (thanks gri!) related to a Bloom filter to
|
||||
// quickly reject most tests, which are false. For each
|
||||
// method set, we precompute a mask, a set of bits, one per
|
||||
// distinct initial byte of each method name. Thus the mask
|
||||
// for io.ReadWriter would be {'R','W'}. AssignableTo(T, U)
|
||||
// cannot be true unless mask(T)&mask(U)==mask(U).
|
||||
//
|
||||
// As with a Bloom filter, we can improve precision by testing
|
||||
// additional hashes, e.g. using the last letter of each
|
||||
// method name, so long as the subset mask property holds.
|
||||
//
|
||||
// When analyzing the standard library, there are about 1e6
|
||||
// calls to satisfies(), of which 0.6% return true. With a
|
||||
// 1-hash filter, 95% of calls avoid the expensive check; with
|
||||
// a 2-hash filter, this grows to 98.2%.
|
||||
satisfies := func(T, U *msetInfo) bool {
|
||||
return T.mask1&U.mask1 == U.mask1 &&
|
||||
T.mask2&U.mask2 == U.mask2 &&
|
||||
containsAllIdsOf(T.mset, U.mset)
|
||||
}
|
||||
|
||||
// Information about a named type N, and perhaps also *N.
|
||||
type namedInfo struct {
|
||||
isInterface bool
|
||||
base msetInfo // N
|
||||
ptr msetInfo // *N, iff N !isInterface
|
||||
}
|
||||
|
||||
var infos []namedInfo
|
||||
|
||||
// Precompute the method sets and their masks.
|
||||
for _, N := range allNamed {
|
||||
var info namedInfo
|
||||
initMsetInfo(&info.base, N)
|
||||
_, info.isInterface = N.Underlying().(*types.Interface)
|
||||
if !info.isInterface {
|
||||
initMsetInfo(&info.ptr, types.NewPointer(N))
|
||||
}
|
||||
|
||||
if info.base.mask1|info.ptr.mask1 == 0 {
|
||||
continue // neither N nor *N has methods
|
||||
}
|
||||
|
||||
infos = append(infos, info)
|
||||
}
|
||||
|
||||
facts := make(map[*types.Named]implementsFacts)
|
||||
|
||||
// Test all pairs of distinct named types (T, U).
|
||||
// TODO(adonovan): opt: compute (U, T) at the same time.
|
||||
for t := range infos {
|
||||
T := &infos[t]
|
||||
var to, from, fromPtr []types.Type
|
||||
for u := range infos {
|
||||
if t == u {
|
||||
continue
|
||||
}
|
||||
U := &infos[u]
|
||||
switch {
|
||||
case T.isInterface && U.isInterface:
|
||||
if satisfies(&U.base, &T.base) {
|
||||
to = append(to, U.base.typ)
|
||||
}
|
||||
if satisfies(&T.base, &U.base) {
|
||||
from = append(from, U.base.typ)
|
||||
}
|
||||
case T.isInterface: // U concrete
|
||||
if satisfies(&U.base, &T.base) {
|
||||
to = append(to, U.base.typ)
|
||||
} else if satisfies(&U.ptr, &T.base) {
|
||||
to = append(to, U.ptr.typ)
|
||||
}
|
||||
case U.isInterface: // T concrete
|
||||
if satisfies(&T.base, &U.base) {
|
||||
from = append(from, U.base.typ)
|
||||
} else if satisfies(&T.ptr, &U.base) {
|
||||
fromPtr = append(fromPtr, U.base.typ)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort types (arbitrarily) to avoid nondeterminism.
|
||||
sort.Sort(typesByString(to))
|
||||
sort.Sort(typesByString(from))
|
||||
sort.Sort(typesByString(fromPtr))
|
||||
|
||||
facts[T.base.typ.(*types.Named)] = implementsFacts{to, from, fromPtr}
|
||||
}
|
||||
|
||||
return facts
|
||||
}
|
||||
|
||||
type implementsFacts struct {
|
||||
to []types.Type // named or ptr-to-named types assignable to interface T
|
||||
from []types.Type // named interfaces assignable from T
|
||||
fromPtr []types.Type // named interfaces assignable only from *T
|
||||
}
|
||||
|
||||
type typesByString []types.Type
|
||||
|
||||
func (p typesByString) Len() int { return len(p) }
|
||||
func (p typesByString) Less(i, j int) bool { return p[i].String() < p[j].String() }
|
||||
func (p typesByString) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||
|
||||
// methodBit returns the index of x in [a-zA-Z], or 52 if not found.
|
||||
func methodBit(x byte) uint64 {
|
||||
switch {
|
||||
case 'a' <= x && x <= 'z':
|
||||
return uint64(x - 'a')
|
||||
case 'A' <= x && x <= 'Z':
|
||||
return uint64(26 + x - 'A')
|
||||
}
|
||||
return 52 // all other bytes
|
||||
}
|
||||
|
||||
// containsAllIdsOf reports whether the method identifiers of T are a
|
||||
// superset of those in U. If U belongs to an interface type, the
|
||||
// result is equal to types.Assignable(T, U), but is cheaper to compute.
|
||||
//
|
||||
// TODO(gri): make this a method of *types.MethodSet.
|
||||
//
|
||||
func containsAllIdsOf(T, U *types.MethodSet) bool {
|
||||
t, tlen := 0, T.Len()
|
||||
u, ulen := 0, U.Len()
|
||||
for t < tlen && u < ulen {
|
||||
tMeth := T.At(t).Obj()
|
||||
uMeth := U.At(u).Obj()
|
||||
tId := tMeth.Id()
|
||||
uId := uMeth.Id()
|
||||
if tId > uId {
|
||||
// U has a method T lacks: fail.
|
||||
return false
|
||||
}
|
||||
if tId < uId {
|
||||
// T has a method U lacks: ignore it.
|
||||
t++
|
||||
continue
|
||||
}
|
||||
// U and T both have a method of this Id. Check types.
|
||||
if !types.Identical(tMeth.Type(), uMeth.Type()) {
|
||||
return false // type mismatch
|
||||
}
|
||||
u++
|
||||
t++
|
||||
}
|
||||
return u == ulen
|
||||
}
|
69
vendor/golang.org/x/tools/godoc/analysis/json.go
generated
vendored
Normal file
69
vendor/golang.org/x/tools/godoc/analysis/json.go
generated
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright 2014 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 analysis
|
||||
|
||||
// This file defines types used by client-side JavaScript.
|
||||
|
||||
type anchorJSON struct {
|
||||
Text string // HTML
|
||||
Href string // URL
|
||||
}
|
||||
|
||||
type commOpJSON struct {
|
||||
Op anchorJSON
|
||||
Fn string
|
||||
}
|
||||
|
||||
// JavaScript's onClickComm() expects a commJSON.
|
||||
type commJSON struct {
|
||||
Ops []commOpJSON
|
||||
}
|
||||
|
||||
// Indicates one of these forms of fact about a type T:
|
||||
// T "is implemented by <ByKind> type <Other>" (ByKind != "", e.g. "array")
|
||||
// T "implements <Other>" (ByKind == "")
|
||||
type implFactJSON struct {
|
||||
ByKind string `json:",omitempty"`
|
||||
Other anchorJSON
|
||||
}
|
||||
|
||||
// Implements facts are grouped by form, for ease of reading.
|
||||
type implGroupJSON struct {
|
||||
Descr string
|
||||
Facts []implFactJSON
|
||||
}
|
||||
|
||||
// JavaScript's onClickIdent() expects a TypeInfoJSON.
|
||||
type TypeInfoJSON struct {
|
||||
Name string // type name
|
||||
Size, Align int64
|
||||
Methods []anchorJSON
|
||||
ImplGroups []implGroupJSON
|
||||
}
|
||||
|
||||
// JavaScript's onClickCallees() expects a calleesJSON.
|
||||
type calleesJSON struct {
|
||||
Descr string
|
||||
Callees []anchorJSON // markup for called function
|
||||
}
|
||||
|
||||
type callerJSON struct {
|
||||
Func string
|
||||
Sites []anchorJSON
|
||||
}
|
||||
|
||||
// JavaScript's onClickCallers() expects a callersJSON.
|
||||
type callersJSON struct {
|
||||
Callee string
|
||||
Callers []callerJSON
|
||||
}
|
||||
|
||||
// JavaScript's cgAddChild requires a global array of PCGNodeJSON
|
||||
// called CALLGRAPH, representing the intra-package call graph.
|
||||
// The first element is special and represents "all external callers".
|
||||
type PCGNodeJSON struct {
|
||||
Func anchorJSON
|
||||
Callees []int // indices within CALLGRAPH of nodes called by this one
|
||||
}
|
154
vendor/golang.org/x/tools/godoc/analysis/peers.go
generated
vendored
Normal file
154
vendor/golang.org/x/tools/godoc/analysis/peers.go
generated
vendored
Normal file
@@ -0,0 +1,154 @@
|
||||
// Copyright 2014 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 analysis
|
||||
|
||||
// This file computes the channel "peers" relation over all pairs of
|
||||
// channel operations in the program. The peers are displayed in the
|
||||
// lower pane when a channel operation (make, <-, close) is clicked.
|
||||
|
||||
// TODO(adonovan): handle calls to reflect.{Select,Recv,Send,Close} too,
|
||||
// then enable reflection in PTA.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/token"
|
||||
"go/types"
|
||||
|
||||
"golang.org/x/tools/go/pointer"
|
||||
"golang.org/x/tools/go/ssa"
|
||||
)
|
||||
|
||||
func (a *analysis) doChannelPeers(ptsets map[ssa.Value]pointer.Pointer) {
|
||||
addSendRecv := func(j *commJSON, op chanOp) {
|
||||
j.Ops = append(j.Ops, commOpJSON{
|
||||
Op: anchorJSON{
|
||||
Text: op.mode,
|
||||
Href: a.posURL(op.pos, op.len),
|
||||
},
|
||||
Fn: prettyFunc(nil, op.fn),
|
||||
})
|
||||
}
|
||||
|
||||
// Build an undirected bipartite multigraph (binary relation)
|
||||
// of MakeChan ops and send/recv/close ops.
|
||||
//
|
||||
// TODO(adonovan): opt: use channel element types to partition
|
||||
// the O(n^2) problem into subproblems.
|
||||
aliasedOps := make(map[*ssa.MakeChan][]chanOp)
|
||||
opToMakes := make(map[chanOp][]*ssa.MakeChan)
|
||||
for _, op := range a.ops {
|
||||
// Combine the PT sets from all contexts.
|
||||
var makes []*ssa.MakeChan // aliased ops
|
||||
ptr, ok := ptsets[op.ch]
|
||||
if !ok {
|
||||
continue // e.g. channel op in dead code
|
||||
}
|
||||
for _, label := range ptr.PointsTo().Labels() {
|
||||
makechan, ok := label.Value().(*ssa.MakeChan)
|
||||
if !ok {
|
||||
continue // skip intrinsically-created channels for now
|
||||
}
|
||||
if makechan.Pos() == token.NoPos {
|
||||
continue // not possible?
|
||||
}
|
||||
makes = append(makes, makechan)
|
||||
aliasedOps[makechan] = append(aliasedOps[makechan], op)
|
||||
}
|
||||
opToMakes[op] = makes
|
||||
}
|
||||
|
||||
// Now that complete relation is built, build links for ops.
|
||||
for _, op := range a.ops {
|
||||
v := commJSON{
|
||||
Ops: []commOpJSON{}, // (JS wants non-nil)
|
||||
}
|
||||
ops := make(map[chanOp]bool)
|
||||
for _, makechan := range opToMakes[op] {
|
||||
v.Ops = append(v.Ops, commOpJSON{
|
||||
Op: anchorJSON{
|
||||
Text: "made",
|
||||
Href: a.posURL(makechan.Pos()-token.Pos(len("make")),
|
||||
len("make")),
|
||||
},
|
||||
Fn: makechan.Parent().RelString(op.fn.Package().Pkg),
|
||||
})
|
||||
for _, op := range aliasedOps[makechan] {
|
||||
ops[op] = true
|
||||
}
|
||||
}
|
||||
for op := range ops {
|
||||
addSendRecv(&v, op)
|
||||
}
|
||||
|
||||
// Add links for each aliased op.
|
||||
fi, offset := a.fileAndOffset(op.pos)
|
||||
fi.addLink(aLink{
|
||||
start: offset,
|
||||
end: offset + op.len,
|
||||
title: "show channel ops",
|
||||
onclick: fmt.Sprintf("onClickComm(%d)", fi.addData(v)),
|
||||
})
|
||||
}
|
||||
// Add links for makechan ops themselves.
|
||||
for makechan, ops := range aliasedOps {
|
||||
v := commJSON{
|
||||
Ops: []commOpJSON{}, // (JS wants non-nil)
|
||||
}
|
||||
for _, op := range ops {
|
||||
addSendRecv(&v, op)
|
||||
}
|
||||
|
||||
fi, offset := a.fileAndOffset(makechan.Pos())
|
||||
fi.addLink(aLink{
|
||||
start: offset - len("make"),
|
||||
end: offset,
|
||||
title: "show channel ops",
|
||||
onclick: fmt.Sprintf("onClickComm(%d)", fi.addData(v)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// -- utilities --------------------------------------------------------
|
||||
|
||||
// chanOp abstracts an ssa.Send, ssa.Unop(ARROW), close(), or a SelectState.
|
||||
// Derived from cmd/guru/peers.go.
|
||||
type chanOp struct {
|
||||
ch ssa.Value
|
||||
mode string // sent|received|closed
|
||||
pos token.Pos
|
||||
len int
|
||||
fn *ssa.Function
|
||||
}
|
||||
|
||||
// chanOps returns a slice of all the channel operations in the instruction.
|
||||
// Derived from cmd/guru/peers.go.
|
||||
func chanOps(instr ssa.Instruction) []chanOp {
|
||||
fn := instr.Parent()
|
||||
var ops []chanOp
|
||||
switch instr := instr.(type) {
|
||||
case *ssa.UnOp:
|
||||
if instr.Op == token.ARROW {
|
||||
// TODO(adonovan): don't assume <-ch; could be 'range ch'.
|
||||
ops = append(ops, chanOp{instr.X, "received", instr.Pos(), len("<-"), fn})
|
||||
}
|
||||
case *ssa.Send:
|
||||
ops = append(ops, chanOp{instr.Chan, "sent", instr.Pos(), len("<-"), fn})
|
||||
case *ssa.Select:
|
||||
for _, st := range instr.States {
|
||||
mode := "received"
|
||||
if st.Dir == types.SendOnly {
|
||||
mode = "sent"
|
||||
}
|
||||
ops = append(ops, chanOp{st.Chan, mode, st.Pos, len("<-"), fn})
|
||||
}
|
||||
case ssa.CallInstruction:
|
||||
call := instr.Common()
|
||||
if blt, ok := call.Value.(*ssa.Builtin); ok && blt.Name() == "close" {
|
||||
pos := instr.Common().Pos()
|
||||
ops = append(ops, chanOp{call.Args[0], "closed", pos - token.Pos(len("close")), len("close("), fn})
|
||||
}
|
||||
}
|
||||
return ops
|
||||
}
|
234
vendor/golang.org/x/tools/godoc/analysis/typeinfo.go
generated
vendored
Normal file
234
vendor/golang.org/x/tools/godoc/analysis/typeinfo.go
generated
vendored
Normal file
@@ -0,0 +1,234 @@
|
||||
// Copyright 2014 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 analysis
|
||||
|
||||
// This file computes the markup for information from go/types:
|
||||
// IMPORTS, identifier RESOLUTION, METHOD SETS, size/alignment, and
|
||||
// the IMPLEMENTS relation.
|
||||
//
|
||||
// IMPORTS links connect import specs to the documentation for the
|
||||
// imported package.
|
||||
//
|
||||
// RESOLUTION links referring identifiers to their defining
|
||||
// identifier, and adds tooltips for kind and type.
|
||||
//
|
||||
// METHOD SETS, size/alignment, and the IMPLEMENTS relation are
|
||||
// displayed in the lower pane when a type's defining identifier is
|
||||
// clicked.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/types"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/go/loader"
|
||||
"golang.org/x/tools/go/types/typeutil"
|
||||
)
|
||||
|
||||
// TODO(adonovan): audit to make sure it's safe on ill-typed packages.
|
||||
|
||||
// TODO(adonovan): use same Sizes as loader.Config.
|
||||
var sizes = types.StdSizes{WordSize: 8, MaxAlign: 8}
|
||||
|
||||
func (a *analysis) doTypeInfo(info *loader.PackageInfo, implements map[*types.Named]implementsFacts) {
|
||||
// We must not assume the corresponding SSA packages were
|
||||
// created (i.e. were transitively error-free).
|
||||
|
||||
// IMPORTS
|
||||
for _, f := range info.Files {
|
||||
// Package decl.
|
||||
fi, offset := a.fileAndOffset(f.Name.Pos())
|
||||
fi.addLink(aLink{
|
||||
start: offset,
|
||||
end: offset + len(f.Name.Name),
|
||||
title: "Package docs for " + info.Pkg.Path(),
|
||||
// TODO(adonovan): fix: we're putting the untrusted Path()
|
||||
// into a trusted field. What's the appropriate sanitizer?
|
||||
href: "/pkg/" + info.Pkg.Path(),
|
||||
})
|
||||
|
||||
// Import specs.
|
||||
for _, imp := range f.Imports {
|
||||
// Remove quotes.
|
||||
L := int(imp.End()-imp.Path.Pos()) - len(`""`)
|
||||
path, _ := strconv.Unquote(imp.Path.Value)
|
||||
fi, offset := a.fileAndOffset(imp.Path.Pos())
|
||||
fi.addLink(aLink{
|
||||
start: offset + 1,
|
||||
end: offset + 1 + L,
|
||||
title: "Package docs for " + path,
|
||||
// TODO(adonovan): fix: we're putting the untrusted path
|
||||
// into a trusted field. What's the appropriate sanitizer?
|
||||
href: "/pkg/" + path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// RESOLUTION
|
||||
qualifier := types.RelativeTo(info.Pkg)
|
||||
for id, obj := range info.Uses {
|
||||
// Position of the object definition.
|
||||
pos := obj.Pos()
|
||||
Len := len(obj.Name())
|
||||
|
||||
// Correct the position for non-renaming import specs.
|
||||
// import "sync/atomic"
|
||||
// ^^^^^^^^^^^
|
||||
if obj, ok := obj.(*types.PkgName); ok && id.Name == obj.Imported().Name() {
|
||||
// Assume this is a non-renaming import.
|
||||
// NB: not true for degenerate renamings: `import foo "foo"`.
|
||||
pos++
|
||||
Len = len(obj.Imported().Path())
|
||||
}
|
||||
|
||||
if obj.Pkg() == nil {
|
||||
continue // don't mark up built-ins.
|
||||
}
|
||||
|
||||
fi, offset := a.fileAndOffset(id.NamePos)
|
||||
fi.addLink(aLink{
|
||||
start: offset,
|
||||
end: offset + len(id.Name),
|
||||
title: types.ObjectString(obj, qualifier),
|
||||
href: a.posURL(pos, Len),
|
||||
})
|
||||
}
|
||||
|
||||
// IMPLEMENTS & METHOD SETS
|
||||
for _, obj := range info.Defs {
|
||||
if obj, ok := obj.(*types.TypeName); ok {
|
||||
if named, ok := obj.Type().(*types.Named); ok {
|
||||
a.namedType(named, implements)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *analysis) namedType(T *types.Named, implements map[*types.Named]implementsFacts) {
|
||||
obj := T.Obj()
|
||||
qualifier := types.RelativeTo(obj.Pkg())
|
||||
v := &TypeInfoJSON{
|
||||
Name: obj.Name(),
|
||||
Size: sizes.Sizeof(T),
|
||||
Align: sizes.Alignof(T),
|
||||
Methods: []anchorJSON{}, // (JS wants non-nil)
|
||||
}
|
||||
|
||||
// addFact adds the fact "is implemented by T" (by) or
|
||||
// "implements T" (!by) to group.
|
||||
addFact := func(group *implGroupJSON, T types.Type, by bool) {
|
||||
Tobj := deref(T).(*types.Named).Obj()
|
||||
var byKind string
|
||||
if by {
|
||||
// Show underlying kind of implementing type,
|
||||
// e.g. "slice", "array", "struct".
|
||||
s := reflect.TypeOf(T.Underlying()).String()
|
||||
byKind = strings.ToLower(strings.TrimPrefix(s, "*types."))
|
||||
}
|
||||
group.Facts = append(group.Facts, implFactJSON{
|
||||
ByKind: byKind,
|
||||
Other: anchorJSON{
|
||||
Href: a.posURL(Tobj.Pos(), len(Tobj.Name())),
|
||||
Text: types.TypeString(T, qualifier),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// IMPLEMENTS
|
||||
if r, ok := implements[T]; ok {
|
||||
if isInterface(T) {
|
||||
// "T is implemented by <conc>" ...
|
||||
// "T is implemented by <iface>"...
|
||||
// "T implements <iface>"...
|
||||
group := implGroupJSON{
|
||||
Descr: types.TypeString(T, qualifier),
|
||||
}
|
||||
// Show concrete types first; use two passes.
|
||||
for _, sub := range r.to {
|
||||
if !isInterface(sub) {
|
||||
addFact(&group, sub, true)
|
||||
}
|
||||
}
|
||||
for _, sub := range r.to {
|
||||
if isInterface(sub) {
|
||||
addFact(&group, sub, true)
|
||||
}
|
||||
}
|
||||
for _, super := range r.from {
|
||||
addFact(&group, super, false)
|
||||
}
|
||||
v.ImplGroups = append(v.ImplGroups, group)
|
||||
} else {
|
||||
// T is concrete.
|
||||
if r.from != nil {
|
||||
// "T implements <iface>"...
|
||||
group := implGroupJSON{
|
||||
Descr: types.TypeString(T, qualifier),
|
||||
}
|
||||
for _, super := range r.from {
|
||||
addFact(&group, super, false)
|
||||
}
|
||||
v.ImplGroups = append(v.ImplGroups, group)
|
||||
}
|
||||
if r.fromPtr != nil {
|
||||
// "*C implements <iface>"...
|
||||
group := implGroupJSON{
|
||||
Descr: "*" + types.TypeString(T, qualifier),
|
||||
}
|
||||
for _, psuper := range r.fromPtr {
|
||||
addFact(&group, psuper, false)
|
||||
}
|
||||
v.ImplGroups = append(v.ImplGroups, group)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// METHOD SETS
|
||||
for _, sel := range typeutil.IntuitiveMethodSet(T, &a.prog.MethodSets) {
|
||||
meth := sel.Obj().(*types.Func)
|
||||
pos := meth.Pos() // may be 0 for error.Error
|
||||
v.Methods = append(v.Methods, anchorJSON{
|
||||
Href: a.posURL(pos, len(meth.Name())),
|
||||
Text: types.SelectionString(sel, qualifier),
|
||||
})
|
||||
}
|
||||
|
||||
// Since there can be many specs per decl, we
|
||||
// can't attach the link to the keyword 'type'
|
||||
// (as we do with 'func'); we use the Ident.
|
||||
fi, offset := a.fileAndOffset(obj.Pos())
|
||||
fi.addLink(aLink{
|
||||
start: offset,
|
||||
end: offset + len(obj.Name()),
|
||||
title: fmt.Sprintf("type info for %s", obj.Name()),
|
||||
onclick: fmt.Sprintf("onClickTypeInfo(%d)", fi.addData(v)),
|
||||
})
|
||||
|
||||
// Add info for exported package-level types to the package info.
|
||||
if obj.Exported() && isPackageLevel(obj) {
|
||||
// TODO(adonovan): Path is not unique!
|
||||
// It is possible to declare a non-test package called x_test.
|
||||
a.result.pkgInfo(obj.Pkg().Path()).addType(v)
|
||||
}
|
||||
}
|
||||
|
||||
// -- utilities --------------------------------------------------------
|
||||
|
||||
func isInterface(T types.Type) bool { return types.IsInterface(T) }
|
||||
|
||||
// deref returns a pointer's element type; otherwise it returns typ.
|
||||
func deref(typ types.Type) types.Type {
|
||||
if p, ok := typ.Underlying().(*types.Pointer); ok {
|
||||
return p.Elem()
|
||||
}
|
||||
return typ
|
||||
}
|
||||
|
||||
// isPackageLevel reports whether obj is a package-level object.
|
||||
func isPackageLevel(obj types.Object) bool {
|
||||
return obj.Pkg().Scope().Lookup(obj.Name()) == obj
|
||||
}
|
Reference in New Issue
Block a user