mirror of
https://github.com/dutchcoders/transfer.sh.git
synced 2024-12-02 17:10:18 +01:00
cb6e5cb0c7
* use dep for vendoring * lets encrypt * moved web to transfer.sh-web repo * single command install * added first tests
656 lines
15 KiB
Go
656 lines
15 KiB
Go
// 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 or at
|
|
// https://developers.google.com/open-source/licenses/bsd.
|
|
|
|
package doc
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"go/ast"
|
|
"go/build"
|
|
"go/doc"
|
|
"go/format"
|
|
"go/parser"
|
|
"go/token"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"github.com/golang/gddo/gosrc"
|
|
)
|
|
|
|
func startsWithUppercase(s string) bool {
|
|
r, _ := utf8.DecodeRuneInString(s)
|
|
return unicode.IsUpper(r)
|
|
}
|
|
|
|
var badSynopsisPrefixes = []string{
|
|
"Autogenerated by Thrift Compiler",
|
|
"Automatically generated ",
|
|
"Auto-generated by ",
|
|
"Copyright ",
|
|
"COPYRIGHT ",
|
|
`THE SOFTWARE IS PROVIDED "AS IS"`,
|
|
"TODO: ",
|
|
"vim:",
|
|
}
|
|
|
|
// synopsis extracts the first sentence from s. All runs of whitespace are
|
|
// replaced by a single space.
|
|
func synopsis(s string) string {
|
|
|
|
parts := strings.SplitN(s, "\n\n", 2)
|
|
s = parts[0]
|
|
|
|
var buf []byte
|
|
const (
|
|
other = iota
|
|
period
|
|
space
|
|
)
|
|
last := space
|
|
Loop:
|
|
for i := 0; i < len(s); i++ {
|
|
b := s[i]
|
|
switch b {
|
|
case ' ', '\t', '\r', '\n':
|
|
switch last {
|
|
case period:
|
|
break Loop
|
|
case other:
|
|
buf = append(buf, ' ')
|
|
last = space
|
|
}
|
|
case '.':
|
|
last = period
|
|
buf = append(buf, b)
|
|
default:
|
|
last = other
|
|
buf = append(buf, b)
|
|
}
|
|
}
|
|
|
|
// Ensure that synopsis fits an App Engine datastore text property.
|
|
const m = 400
|
|
if len(buf) > m {
|
|
buf = buf[:m]
|
|
if i := bytes.LastIndex(buf, []byte{' '}); i >= 0 {
|
|
buf = buf[:i]
|
|
}
|
|
buf = append(buf, " ..."...)
|
|
}
|
|
|
|
s = string(buf)
|
|
|
|
r, n := utf8.DecodeRuneInString(s)
|
|
if n < 0 || unicode.IsPunct(r) || unicode.IsSymbol(r) {
|
|
// ignore Markdown headings, editor settings, Go build constraints, and * in poorly formatted block comments.
|
|
s = ""
|
|
} else {
|
|
for _, prefix := range badSynopsisPrefixes {
|
|
if strings.HasPrefix(s, prefix) {
|
|
s = ""
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
var referencesPats = []*regexp.Regexp{
|
|
regexp.MustCompile(`"([-a-zA-Z0-9~+_./]+)"`), // quoted path
|
|
regexp.MustCompile(`https://drone\.io/([-a-zA-Z0-9~+_./]+)/status\.png`),
|
|
regexp.MustCompile(`\b(?:` + strings.Join([]string{
|
|
`go\s+get\s+`,
|
|
`goinstall\s+`,
|
|
regexp.QuoteMeta("http://godoc.org/"),
|
|
regexp.QuoteMeta("http://gopkgdoc.appspot.com/pkg/"),
|
|
regexp.QuoteMeta("http://go.pkgdoc.org/"),
|
|
regexp.QuoteMeta("http://gowalker.org/"),
|
|
}, "|") + `)([-a-zA-Z0-9~+_./]+)`),
|
|
}
|
|
|
|
// addReferences adds packages referenced in plain text s.
|
|
func addReferences(references map[string]bool, s []byte) {
|
|
for _, pat := range referencesPats {
|
|
for _, m := range pat.FindAllSubmatch(s, -1) {
|
|
p := string(m[1])
|
|
if gosrc.IsValidRemotePath(p) {
|
|
references[p] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type byFuncName []*doc.Func
|
|
|
|
func (s byFuncName) Len() int { return len(s) }
|
|
func (s byFuncName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
|
func (s byFuncName) Less(i, j int) bool { return s[i].Name < s[j].Name }
|
|
|
|
func removeAssociations(dpkg *doc.Package) {
|
|
for _, t := range dpkg.Types {
|
|
dpkg.Funcs = append(dpkg.Funcs, t.Funcs...)
|
|
t.Funcs = nil
|
|
}
|
|
sort.Sort(byFuncName(dpkg.Funcs))
|
|
}
|
|
|
|
// builder holds the state used when building the documentation.
|
|
type builder struct {
|
|
srcs map[string]*source
|
|
fset *token.FileSet
|
|
examples []*doc.Example
|
|
buf []byte // scratch space for printNode method.
|
|
}
|
|
|
|
type Value struct {
|
|
Decl Code
|
|
Pos Pos
|
|
Doc string
|
|
}
|
|
|
|
func (b *builder) values(vdocs []*doc.Value) []*Value {
|
|
var result []*Value
|
|
for _, d := range vdocs {
|
|
result = append(result, &Value{
|
|
Decl: b.printDecl(d.Decl),
|
|
Pos: b.position(d.Decl),
|
|
Doc: d.Doc,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
type Note struct {
|
|
Pos Pos
|
|
UID string
|
|
Body string
|
|
}
|
|
|
|
type posNode token.Pos
|
|
|
|
func (p posNode) Pos() token.Pos { return token.Pos(p) }
|
|
func (p posNode) End() token.Pos { return token.Pos(p) }
|
|
|
|
func (b *builder) notes(gnotes map[string][]*doc.Note) map[string][]*Note {
|
|
if len(gnotes) == 0 {
|
|
return nil
|
|
}
|
|
notes := make(map[string][]*Note)
|
|
for tag, gvalues := range gnotes {
|
|
values := make([]*Note, len(gvalues))
|
|
for i := range gvalues {
|
|
values[i] = &Note{
|
|
Pos: b.position(posNode(gvalues[i].Pos)),
|
|
UID: gvalues[i].UID,
|
|
Body: strings.TrimSpace(gvalues[i].Body),
|
|
}
|
|
}
|
|
notes[tag] = values
|
|
}
|
|
return notes
|
|
}
|
|
|
|
type Example struct {
|
|
Name string
|
|
Doc string
|
|
Code Code
|
|
Play string
|
|
Output string
|
|
}
|
|
|
|
var exampleOutputRx = regexp.MustCompile(`(?i)//[[:space:]]*output:`)
|
|
|
|
func (b *builder) getExamples(name string) []*Example {
|
|
var docs []*Example
|
|
for _, e := range b.examples {
|
|
if !strings.HasPrefix(e.Name, name) {
|
|
continue
|
|
}
|
|
n := e.Name[len(name):]
|
|
if n != "" {
|
|
if i := strings.LastIndex(n, "_"); i != 0 {
|
|
continue
|
|
}
|
|
n = n[1:]
|
|
if startsWithUppercase(n) {
|
|
continue
|
|
}
|
|
n = strings.Title(n)
|
|
}
|
|
|
|
code, output := b.printExample(e)
|
|
|
|
play := ""
|
|
if e.Play != nil {
|
|
b.buf = b.buf[:0]
|
|
if err := format.Node(sliceWriter{&b.buf}, b.fset, e.Play); err != nil {
|
|
play = err.Error()
|
|
} else {
|
|
play = string(b.buf)
|
|
}
|
|
}
|
|
|
|
docs = append(docs, &Example{
|
|
Name: n,
|
|
Doc: e.Doc,
|
|
Code: code,
|
|
Output: output,
|
|
Play: play})
|
|
}
|
|
return docs
|
|
}
|
|
|
|
type Func struct {
|
|
Decl Code
|
|
Pos Pos
|
|
Doc string
|
|
Name string
|
|
Recv string // Actual receiver "T" or "*T".
|
|
Orig string // Original receiver "T" or "*T". This can be different from Recv due to embedding.
|
|
Examples []*Example
|
|
}
|
|
|
|
func (b *builder) funcs(fdocs []*doc.Func) []*Func {
|
|
var result []*Func
|
|
for _, d := range fdocs {
|
|
var exampleName string
|
|
switch {
|
|
case d.Recv == "":
|
|
exampleName = d.Name
|
|
case d.Recv[0] == '*':
|
|
exampleName = d.Recv[1:] + "_" + d.Name
|
|
default:
|
|
exampleName = d.Recv + "_" + d.Name
|
|
}
|
|
result = append(result, &Func{
|
|
Decl: b.printDecl(d.Decl),
|
|
Pos: b.position(d.Decl),
|
|
Doc: d.Doc,
|
|
Name: d.Name,
|
|
Recv: d.Recv,
|
|
Orig: d.Orig,
|
|
Examples: b.getExamples(exampleName),
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
type Type struct {
|
|
Doc string
|
|
Name string
|
|
Decl Code
|
|
Pos Pos
|
|
Consts []*Value
|
|
Vars []*Value
|
|
Funcs []*Func
|
|
Methods []*Func
|
|
Examples []*Example
|
|
}
|
|
|
|
func (b *builder) types(tdocs []*doc.Type) []*Type {
|
|
var result []*Type
|
|
for _, d := range tdocs {
|
|
result = append(result, &Type{
|
|
Doc: d.Doc,
|
|
Name: d.Name,
|
|
Decl: b.printDecl(d.Decl),
|
|
Pos: b.position(d.Decl),
|
|
Consts: b.values(d.Consts),
|
|
Vars: b.values(d.Vars),
|
|
Funcs: b.funcs(d.Funcs),
|
|
Methods: b.funcs(d.Methods),
|
|
Examples: b.getExamples(d.Name),
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
var packageNamePats = []*regexp.Regexp{
|
|
// Last element with .suffix removed.
|
|
regexp.MustCompile(`/([^-./]+)[-.](?:git|svn|hg|bzr|v\d+)$`),
|
|
|
|
// Last element with "go" prefix or suffix removed.
|
|
regexp.MustCompile(`/([^-./]+)[-.]go$`),
|
|
regexp.MustCompile(`/go[-.]([^-./]+)$`),
|
|
|
|
// Special cases for popular repos.
|
|
regexp.MustCompile(`^code\.google\.com/p/google-api-go-client/([^/]+)/v[^/]+$`),
|
|
regexp.MustCompile(`^code\.google\.com/p/biogo\.([^/]+)$`),
|
|
|
|
// It's also common for the last element of the path to contain an
|
|
// extra "go" prefix, but not always. TODO: examine unresolved ids to
|
|
// detect when trimming the "go" prefix is appropriate.
|
|
|
|
// Last component of path.
|
|
regexp.MustCompile(`([^/]+)$`),
|
|
}
|
|
|
|
func simpleImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) {
|
|
pkg := imports[path]
|
|
if pkg != nil {
|
|
return pkg, nil
|
|
}
|
|
|
|
// Guess the package name without importing it.
|
|
for _, pat := range packageNamePats {
|
|
m := pat.FindStringSubmatch(path)
|
|
if m != nil {
|
|
pkg = ast.NewObj(ast.Pkg, m[1])
|
|
pkg.Data = ast.NewScope(nil)
|
|
imports[path] = pkg
|
|
return pkg, nil
|
|
}
|
|
}
|
|
|
|
return nil, errors.New("package not found")
|
|
}
|
|
|
|
type File struct {
|
|
Name string
|
|
URL string
|
|
}
|
|
|
|
type Pos struct {
|
|
Line int32 // 0 if not valid.
|
|
N uint16 // number of lines - 1
|
|
File int16 // index in Package.Files
|
|
}
|
|
|
|
type source struct {
|
|
name string
|
|
browseURL string
|
|
data []byte
|
|
index int
|
|
}
|
|
|
|
// PackageVersion is modified when previously stored packages are invalid.
|
|
const PackageVersion = "8"
|
|
|
|
type Package struct {
|
|
// The import path for this package.
|
|
ImportPath string
|
|
|
|
// Import path prefix for all packages in the project.
|
|
ProjectRoot string
|
|
|
|
// Name of the project.
|
|
ProjectName string
|
|
|
|
// Project home page.
|
|
ProjectURL string
|
|
|
|
// Errors found when fetching or parsing this package.
|
|
Errors []string
|
|
|
|
// Packages referenced in README files.
|
|
References []string
|
|
|
|
// Version control system: git, hg, bzr, ...
|
|
VCS string
|
|
|
|
// Version control: active or suppressed.
|
|
Status gosrc.DirectoryStatus
|
|
|
|
// Whether the package is a fork of another one.
|
|
Fork bool
|
|
|
|
// How many stars (for a GitHub project) or followers (for a BitBucket
|
|
// project) the repository of this package has.
|
|
Stars int
|
|
|
|
// The time this object was created.
|
|
Updated time.Time
|
|
|
|
// Cache validation tag. This tag is not necessarily an HTTP entity tag.
|
|
// The tag is "" if there is no meaningful cache validation for the VCS.
|
|
Etag string
|
|
|
|
// Subdirectories, possibly containing Go code.
|
|
Subdirectories []string
|
|
|
|
// Package name or "" if no package for this import path. The proceeding
|
|
// fields are set even if a package is not found for the import path.
|
|
Name string
|
|
|
|
// Synopsis and full documentation for the package.
|
|
Synopsis string
|
|
Doc string
|
|
|
|
// Format this package as a command.
|
|
IsCmd bool
|
|
|
|
// True if package documentation is incomplete.
|
|
Truncated bool
|
|
|
|
// Environment
|
|
GOOS, GOARCH string
|
|
|
|
// Top-level declarations.
|
|
Consts []*Value
|
|
Funcs []*Func
|
|
Types []*Type
|
|
Vars []*Value
|
|
|
|
// Package examples
|
|
Examples []*Example
|
|
|
|
Notes map[string][]*Note
|
|
|
|
// Source.
|
|
LineFmt string
|
|
BrowseURL string
|
|
Files []*File
|
|
TestFiles []*File
|
|
|
|
// Source size in bytes.
|
|
SourceSize int
|
|
TestSourceSize int
|
|
|
|
// Imports
|
|
Imports []string
|
|
TestImports []string
|
|
XTestImports []string
|
|
}
|
|
|
|
var goEnvs = []struct{ GOOS, GOARCH string }{
|
|
{"linux", "amd64"},
|
|
{"darwin", "amd64"},
|
|
{"windows", "amd64"},
|
|
{"linux", "js"},
|
|
}
|
|
|
|
// SetDefaultGOOS sets given GOOS value as default one to use when building
|
|
// package documents. SetDefaultGOOS has no effect on some windows-only
|
|
// packages.
|
|
func SetDefaultGOOS(goos string) {
|
|
if goos == "" {
|
|
return
|
|
}
|
|
var i int
|
|
for ; i < len(goEnvs); i++ {
|
|
if goEnvs[i].GOOS == goos {
|
|
break
|
|
}
|
|
}
|
|
switch i {
|
|
case 0:
|
|
return
|
|
case len(goEnvs):
|
|
env := goEnvs[0]
|
|
env.GOOS = goos
|
|
goEnvs = append(goEnvs, env)
|
|
}
|
|
goEnvs[0], goEnvs[i] = goEnvs[i], goEnvs[0]
|
|
}
|
|
|
|
var windowsOnlyPackages = map[string]bool{
|
|
"internal/syscall/windows": true,
|
|
"internal/syscall/windows/registry": true,
|
|
"golang.org/x/exp/shiny/driver/internal/win32": true,
|
|
"golang.org/x/exp/shiny/driver/windriver": true,
|
|
"golang.org/x/sys/windows": true,
|
|
"golang.org/x/sys/windows/registry": true,
|
|
}
|
|
|
|
func newPackage(dir *gosrc.Directory) (*Package, error) {
|
|
|
|
pkg := &Package{
|
|
Updated: time.Now().UTC(),
|
|
LineFmt: dir.LineFmt,
|
|
ImportPath: dir.ImportPath,
|
|
ProjectRoot: dir.ProjectRoot,
|
|
ProjectName: dir.ProjectName,
|
|
ProjectURL: dir.ProjectURL,
|
|
BrowseURL: dir.BrowseURL,
|
|
Etag: PackageVersion + "-" + dir.Etag,
|
|
VCS: dir.VCS,
|
|
Status: dir.Status,
|
|
Subdirectories: dir.Subdirectories,
|
|
Fork: dir.Fork,
|
|
Stars: dir.Stars,
|
|
}
|
|
|
|
var b builder
|
|
b.srcs = make(map[string]*source)
|
|
references := make(map[string]bool)
|
|
for _, file := range dir.Files {
|
|
if strings.HasSuffix(file.Name, ".go") {
|
|
gosrc.OverwriteLineComments(file.Data)
|
|
b.srcs[file.Name] = &source{name: file.Name, browseURL: file.BrowseURL, data: file.Data}
|
|
} else {
|
|
addReferences(references, file.Data)
|
|
}
|
|
}
|
|
|
|
for r := range references {
|
|
pkg.References = append(pkg.References, r)
|
|
}
|
|
|
|
if len(b.srcs) == 0 {
|
|
return pkg, nil
|
|
}
|
|
|
|
b.fset = token.NewFileSet()
|
|
|
|
// Find the package and associated files.
|
|
|
|
ctxt := build.Context{
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
CgoEnabled: true,
|
|
ReleaseTags: build.Default.ReleaseTags,
|
|
BuildTags: build.Default.BuildTags,
|
|
Compiler: "gc",
|
|
}
|
|
|
|
var err error
|
|
var bpkg *build.Package
|
|
|
|
for _, env := range goEnvs {
|
|
// Some packages should be always displayed as GOOS=windows (see issue #16509 for details).
|
|
// TODO: remove this once issue #16509 is resolved.
|
|
if windowsOnlyPackages[dir.ImportPath] && env.GOOS != "windows" {
|
|
continue
|
|
}
|
|
|
|
ctxt.GOOS = env.GOOS
|
|
ctxt.GOARCH = env.GOARCH
|
|
bpkg, err = dir.Import(&ctxt, build.ImportComment)
|
|
if _, ok := err.(*build.NoGoError); !ok {
|
|
break
|
|
}
|
|
}
|
|
if err != nil {
|
|
if _, ok := err.(*build.NoGoError); !ok {
|
|
pkg.Errors = append(pkg.Errors, err.Error())
|
|
}
|
|
return pkg, nil
|
|
}
|
|
|
|
if bpkg.ImportComment != "" && bpkg.ImportComment != dir.ImportPath {
|
|
return nil, gosrc.NotFoundError{
|
|
Message: "not at canonical import path",
|
|
Redirect: bpkg.ImportComment,
|
|
}
|
|
}
|
|
|
|
// Parse the Go files
|
|
|
|
files := make(map[string]*ast.File)
|
|
names := append(bpkg.GoFiles, bpkg.CgoFiles...)
|
|
sort.Strings(names)
|
|
pkg.Files = make([]*File, len(names))
|
|
for i, name := range names {
|
|
file, err := parser.ParseFile(b.fset, name, b.srcs[name].data, parser.ParseComments)
|
|
if err != nil {
|
|
pkg.Errors = append(pkg.Errors, err.Error())
|
|
} else {
|
|
files[name] = file
|
|
}
|
|
src := b.srcs[name]
|
|
src.index = i
|
|
pkg.Files[i] = &File{Name: name, URL: src.browseURL}
|
|
pkg.SourceSize += len(src.data)
|
|
}
|
|
|
|
apkg, _ := ast.NewPackage(b.fset, files, simpleImporter, nil)
|
|
|
|
// Find examples in the test files.
|
|
|
|
names = append(bpkg.TestGoFiles, bpkg.XTestGoFiles...)
|
|
sort.Strings(names)
|
|
pkg.TestFiles = make([]*File, len(names))
|
|
for i, name := range names {
|
|
file, err := parser.ParseFile(b.fset, name, b.srcs[name].data, parser.ParseComments)
|
|
if err != nil {
|
|
pkg.Errors = append(pkg.Errors, err.Error())
|
|
} else {
|
|
b.examples = append(b.examples, doc.Examples(file)...)
|
|
}
|
|
pkg.TestFiles[i] = &File{Name: name, URL: b.srcs[name].browseURL}
|
|
pkg.TestSourceSize += len(b.srcs[name].data)
|
|
}
|
|
|
|
b.vetPackage(pkg, apkg)
|
|
|
|
mode := doc.Mode(0)
|
|
if pkg.ImportPath == "builtin" {
|
|
mode |= doc.AllDecls
|
|
}
|
|
|
|
dpkg := doc.New(apkg, pkg.ImportPath, mode)
|
|
|
|
if pkg.ImportPath == "builtin" {
|
|
removeAssociations(dpkg)
|
|
}
|
|
|
|
pkg.Name = dpkg.Name
|
|
pkg.Doc = strings.TrimRight(dpkg.Doc, " \t\n\r")
|
|
pkg.Synopsis = synopsis(pkg.Doc)
|
|
|
|
pkg.Examples = b.getExamples("")
|
|
pkg.IsCmd = bpkg.IsCommand()
|
|
pkg.GOOS = ctxt.GOOS
|
|
pkg.GOARCH = ctxt.GOARCH
|
|
|
|
pkg.Consts = b.values(dpkg.Consts)
|
|
pkg.Funcs = b.funcs(dpkg.Funcs)
|
|
pkg.Types = b.types(dpkg.Types)
|
|
pkg.Vars = b.values(dpkg.Vars)
|
|
pkg.Notes = b.notes(dpkg.Notes)
|
|
|
|
pkg.Imports = bpkg.Imports
|
|
pkg.TestImports = bpkg.TestImports
|
|
pkg.XTestImports = bpkg.XTestImports
|
|
|
|
return pkg, nil
|
|
}
|