x/website: add code and static files for website

All of the code and static assets that the website uses to run have been
copied to this repo. There was also a few lines of code added telling
the website where the doc directory, favicon.ico and robots.txt are.

go repo change-id: Ife6443c32673b38000b90dd2efb2985db37ab773
x/tools repo change-id: Ia979a8b06d1b4db47d25ffdfdf925ba8a0ac67de

Real new code additions:
- main.go
    * lines 89-95 added getFullPath method
    * lines 217-222 mapped paths to doc/, favicon.ico, robots.txt in vfs
- appinit.go
    * lines 147-153 added getFullPath method
    * lines 80-84 mapped paths to doc/, favicon.ico in vfs

Several files were copied from x/tools and go so paths (and corresponding
import paths) were changed as follows:

"x/tools/cmd/godoc/" --> "x/website/cmd/golangorg/"
"x/tools/godoc/static/" --> "x/website/content/static/"
"x/tools/godoc/" (without godoc/static/) --> "x/website/cmd/golangorg/godoc/"
"x/tools/internal/memcache" --> "x/website/internal/memcache"
"go/doc/" --> "x/website/content/doc/"
"go/favicon.ico" --> "x/website/favicon.ico"
"go/robots.txt" --> "x/website/robots.txt"

Updates golang/go#29206

Change-Id: I53985fc027f73e60c6946038f85133acf1ecb08c
Reviewed-on: https://go-review.googlesource.com/c/156321
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
This commit is contained in:
Channing Kimble-Brown 2019-01-04 16:49:43 -05:00
Родитель 001739136e
Коммит da9e5ccbe1
293 изменённых файлов: 69955 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,67 @@
# Builder
#########
FROM golang:1.11 AS build
RUN apt-get update && apt-get install -y \
zip # required for generate-index.bash
# Check out the desired version of Go, both to build the godoc binary and serve
# as the goroot for content serving.
ARG GO_REF
RUN test -n "$GO_REF" # GO_REF is required.
RUN git clone --single-branch --depth=1 -b $GO_REF https://go.googlesource.com/go /goroot
RUN cd /goroot/src && ./make.bash
ENV GOROOT /goroot
ENV PATH=/goroot/bin:$PATH
RUN go version
RUN go get -v -d \
golang.org/x/net/context \
google.golang.org/appengine \
cloud.google.com/go/datastore \
golang.org/x/build \
github.com/gomodule/redigo/redis
COPY . /go/src/golang.org/x/tools
WORKDIR /go/src/golang.org/x/website/cmd/golangorg
RUN GODOC_DOCSET=/goroot ./generate-index.bash
RUN go build -o /godoc -tags=golangorg golang.org/x/website/cmd/golangorg
# Clean up goroot for the final image.
RUN cd /goroot && git clean -xdf
# Add build metadata.
RUN cd /goroot && echo "go repo HEAD: $(git rev-parse HEAD)" >> /goroot/buildinfo
RUN echo "requested go ref: ${GO_REF}" >> /goroot/buildinfo
ARG TOOLS_HEAD
RUN echo "x/tools HEAD: ${TOOLS_HEAD}" >> /goroot/buildinfo
ARG TOOLS_CLEAN
RUN echo "x/tools clean: ${TOOLS_CLEAN}" >> /goroot/buildinfo
ARG DOCKER_TAG
RUN echo "image: ${DOCKER_TAG}" >> /goroot/buildinfo
ARG BUILD_ENV
RUN echo "build env: ${BUILD_ENV}" >> /goroot/buildinfo
RUN rm -rf /goroot/.git
# Final image
#############
FROM gcr.io/distroless/base
WORKDIR /app
COPY --from=build /godoc /app/
COPY --from=build /go/src/golang.org/x/website/cmd/golangorg/hg-git-mapping.bin /app/
COPY --from=build /goroot /goroot
ENV GOROOT /goroot
COPY --from=build /go/src/golang.org/x/website/cmd/golangorg/index.split.* /app/
ENV GODOC_INDEX_GLOB index.split.*
CMD ["/app/godoc"]

80
cmd/golangorg/Makefile Normal file
Просмотреть файл

@ -0,0 +1,80 @@
# Copyright 2018 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.
.PHONY: usage
GO_REF ?= release-branch.go1.11
TOOLS_HEAD := $(shell git rev-parse HEAD)
TOOLS_CLEAN := $(shell (git status --porcelain | grep -q .) && echo dirty || echo clean)
ifeq ($(TOOLS_CLEAN),clean)
DOCKER_VERSION ?= $(TOOLS_HEAD)
else
DOCKER_VERSION ?= $(TOOLS_HEAD)-dirty
endif
GCP_PROJECT := golang-org
DOCKER_TAG := gcr.io/$(GCP_PROJECT)/godoc:$(DOCKER_VERSION)
usage:
@echo "See Makefile and README.golangorg-app"
@exit 1
cloud-build: Dockerfile.prod
gcloud builds submit \
--project=$(GCP_PROJECT) \
--config=cloudbuild.yaml \
--substitutions=_GO_REF=$(GO_REF),_TOOLS_HEAD=$(TOOLS_HEAD),_TOOLS_CLEAN=$(TOOLS_CLEAN),_DOCKER_TAG=$(DOCKER_TAG) \
../.. # source code
docker-build: Dockerfile.prod
# NOTE(cbro): move up in directory to include entire tools repo.
# NOTE(cbro): any changes made to this command must also be made in cloudbuild.yaml.
cd ../..; docker build \
-f=cmd/godoc/Dockerfile.prod \
--build-arg=GO_REF=$(GO_REF) \
--build-arg=TOOLS_HEAD=$(TOOLS_HEAD) \
--build-arg=TOOLS_CLEAN=$(TOOLS_CLEAN) \
--build-arg=DOCKER_TAG=$(DOCKER_TAG) \
--build-arg=BUILD_ENV=local \
--tag=$(DOCKER_TAG) \
.
docker-push: docker-build
docker push $(DOCKER_TAG)
deploy:
gcloud -q app deploy app.prod.yaml \
--project=$(GCP_PROJECT) \
--no-promote \
--image-url=$(DOCKER_TAG)
get-latest-url:
@gcloud app versions list \
--service=default \
--project=$(GCP_PROJECT) \
--sort-by=~version.createTime \
--format='value(version.versionUrl)' \
--limit 1 | cut -f1 # NOTE(cbro): gcloud prints out createTime as the second field.
get-latest-id:
@gcloud app versions list \
--service=default \
--project=$(GCP_PROJECT) \
--sort-by=~version.createTime \
--format='value(version.id)' \
--limit 1 | cut -f1 # NOTE(cbro): gcloud prints out createTime as the second field.
regtest:
go test -v \
-regtest.host=$(shell make get-latest-url) \
-run=Live
publish: regtest
gcloud -q app services set-traffic default \
--splits=$(shell make get-latest-id)=1 \
--project=$(GCP_PROJECT)
@echo !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
@echo Stop and/or delete old versions:
@echo "https://console.cloud.google.com/appengine/versions?project=$(GCP_PROJECT)&serviceId=default&versionssize=50"
@echo !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

Просмотреть файл

@ -0,0 +1,94 @@
godoc on Google App Engine
==========================
Prerequisites
-------------
* Google Cloud SDK
https://cloud.google.com/sdk/
* Redis
* Go sources under $GOROOT
* Godoc sources inside $GOPATH
(go get -d golang.org/x/website/cmd/golangorg)
Running locally, in production mode
-----------------------------------
Build the app:
go build -tags golangorg
Run the app:
./golangorg
godoc should come up at http://localhost:8080
Use the PORT environment variable to change the port:
PORT=8081 ./golangorg
Running locally, in production mode, using Docker
-------------------------------------------------
Build the app's Docker container:
make docker-build
Make sure redis is running on port 6379:
$ echo PING | nc localhost 6379
+PONG
^C
Run the datastore emulator:
gcloud beta emulators datastore start --project golang-org
In another terminal window, run the container:
$(gcloud beta emulators datastore env-init)
docker run --rm \
--net host \
--env GODOC_REDIS_ADDR=localhost:6379 \
--env DATASTORE_EMULATOR_HOST=$DATASTORE_EMULATOR_HOST \
--env DATASTORE_PROJECT_ID=$DATASTORE_PROJECT_ID \
gcr.io/golang-org/godoc
godoc should come up at http://localhost:8080
Deploying to golang.org
-----------------------
Make sure you're signed in to gcloud:
gcloud auth login
Build the image, push it to gcr.io, and deploy to Flex:
make cloud-build deploy
Point the load balancer to the newly deployed version:
(This also runs regression tests)
make publish
Stop and/or delete down any very old versions. (Stopped versions can be re-started.)
Keep at least one older verson to roll back to, just in case.
You can also migrate traffic to the new version via this UI.
https://console.cloud.google.com/appengine/versions?project=golang-org&serviceId=default&versionssize=50
Troubleshooting
---------------
Ensure the Cloud SDK is on your PATH and you have the app-engine-go component
installed (gcloud components install app-engine-go) and your components are
up-to-date (gcloud components update)

Просмотреть файл

@ -0,0 +1,13 @@
runtime: go
api_version: go1
instance_class: F4_1G
handlers:
- url: /s
script: _go_app
login: admin
- url: /dl/init
script: _go_app
login: admin
- url: /.*
script: _go_app

Просмотреть файл

@ -0,0 +1,16 @@
runtime: custom
env: flex
env_variables:
GODOC_PROD: true
GODOC_ENFORCE_HOSTS: true
GODOC_REDIS_ADDR: 10.0.0.4:6379 # instance "gophercache"
GODOC_ANALYTICS: UA-11222381-2
DATASTORE_PROJECT_ID: golang-org
network:
name: golang
resources:
cpu: 4
memory_gb: 7.50

172
cmd/golangorg/appinit.go Normal file
Просмотреть файл

@ -0,0 +1,172 @@
// Copyright 2011 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.
// +build golangorg
package main
// This file replaces main.go when running godoc under app-engine.
// See README.golangorg-app for details.
import (
"archive/zip"
"context"
"go/build"
"io"
"log"
"net/http"
"os"
"path"
"regexp"
"runtime"
"strings"
"golang.org/x/website/cmd/golangorg/godoc"
"golang.org/x/website/cmd/golangorg/godoc/dl"
"golang.org/x/website/cmd/golangorg/godoc/proxy"
"golang.org/x/website/cmd/golangorg/godoc/redirect"
"golang.org/x/website/cmd/golangorg/godoc/short"
"golang.org/x/website/cmd/golangorg/godoc/vfs"
"golang.org/x/website/cmd/golangorg/godoc/vfs/gatefs"
"golang.org/x/website/cmd/golangorg/godoc/vfs/mapfs"
"golang.org/x/website/cmd/golangorg/godoc/vfs/zipfs"
"golang.org/x/website/content/static"
"cloud.google.com/go/datastore"
"golang.org/x/website/internal/memcache"
)
func main() {
log.SetFlags(log.Lshortfile | log.LstdFlags)
var (
// .zip filename
zipFilename = os.Getenv("GODOC_ZIP")
// goroot directory in .zip file
zipGoroot = os.Getenv("GODOC_ZIP_PREFIX")
// glob pattern describing search index files
// (if empty, the index is built at run-time)
indexFilenames = os.Getenv("GODOC_INDEX_GLOB")
)
playEnabled = true
log.Println("initializing godoc ...")
log.Printf(".zip file = %s", zipFilename)
log.Printf(".zip GOROOT = %s", zipGoroot)
log.Printf("index files = %s", indexFilenames)
fsGate := make(chan bool, 20)
if zipFilename != "" {
goroot := path.Join("/", zipGoroot) // fsHttp paths are relative to '/'
// read .zip file and set up file systems
rc, err := zip.OpenReader(zipFilename)
if err != nil {
log.Fatalf("%s: %s\n", zipFilename, err)
}
// rc is never closed (app running forever)
fs.Bind("/", zipfs.New(rc, zipFilename), goroot, vfs.BindReplace)
} else {
rootfs := gatefs.New(vfs.OS(runtime.GOROOT()), fsGate)
fs.Bind("/", rootfs, "/", vfs.BindReplace)
}
fs.Bind("/lib/godoc", mapfs.New(static.Files), "/", vfs.BindReplace)
docPath := getFullPath("/src/golang.org/x/website/content/doc")
fs.Bind("/doc", gatefs.New(vfs.OS(docPath), fsGate), "/", vfs.BindBefore)
webroot := getFullPath("/src/golang.org/x/website")
fs.Bind("/favicon.ico", gatefs.New(vfs.OS(webroot), fsGate), "/favicon.ico", vfs.BindBefore)
corpus := godoc.NewCorpus(fs)
corpus.Verbose = false
corpus.MaxResults = 10000 // matches flag default in main.go
corpus.IndexEnabled = true
corpus.IndexFiles = indexFilenames
if err := corpus.Init(); err != nil {
log.Fatal(err)
}
corpus.IndexDirectory = indexDirectoryDefault
corpus.InitVersionInfo()
if indexFilenames != "" {
corpus.RunIndexer()
} else {
go corpus.RunIndexer()
}
pres = godoc.NewPresentation(corpus)
pres.TabWidth = 8
pres.ShowPlayground = true
pres.DeclLinks = true
pres.NotesRx = regexp.MustCompile("BUG")
pres.GoogleAnalytics = os.Getenv("GODOC_ANALYTICS")
readTemplates(pres)
datastoreClient, memcacheClient := getClients()
// NOTE(cbro): registerHandlers registers itself against DefaultServeMux.
// The mux returned has host enforcement, so it's important to register
// against this mux and not DefaultServeMux.
mux := registerHandlers(pres)
dl.RegisterHandlers(mux, datastoreClient, memcacheClient)
short.RegisterHandlers(mux, datastoreClient, memcacheClient)
// Register /compile and /share handlers against the default serve mux
// so that other app modules can make plain HTTP requests to those
// hosts. (For reasons, HTTPS communication between modules is broken.)
proxy.RegisterHandlers(http.DefaultServeMux)
http.HandleFunc("/_ah/health", func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "ok")
})
http.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "User-agent: *\nDisallow: /search\n")
})
if err := redirect.LoadChangeMap("hg-git-mapping.bin"); err != nil {
log.Fatalf("LoadChangeMap: %v", err)
}
log.Println("godoc initialization complete")
// TODO(cbro): add instrumentation via opencensus.
port := "8080"
if p := os.Getenv("PORT"); p != "" { // PORT is set by GAE flex.
port = p
}
log.Fatal(http.ListenAndServe(":"+port, nil))
}
func getFullPath(relPath string) string {
gopath := os.Getenv("GOPATH")
if gopath == "" {
gopath = build.Default.GOPATH
}
return gopath + relPath
}
func getClients() (*datastore.Client, *memcache.Client) {
ctx := context.Background()
datastoreClient, err := datastore.NewClient(ctx, "")
if err != nil {
if strings.Contains(err.Error(), "missing project") {
log.Fatalf("Missing datastore project. Set the DATASTORE_PROJECT_ID env variable. Use `gcloud beta emulators datastore` to start a local datastore.")
}
log.Fatalf("datastore.NewClient: %v.", err)
}
redisAddr := os.Getenv("GODOC_REDIS_ADDR")
if redisAddr == "" {
log.Fatalf("Missing redis server for godoc in production mode. set GODOC_REDIS_ADDR environment variable.")
}
memcacheClient := memcache.New(redisAddr)
return datastoreClient, memcacheClient
}

88
cmd/golangorg/autocert.go Normal file
Просмотреть файл

@ -0,0 +1,88 @@
// Copyright 2016 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.
// +build autocert
// This file adds automatic TLS certificate support (using
// golang.org/x/crypto/acme/autocert), conditional on the use of the
// autocert build tag. It sets the serveAutoCertHook func variable
// non-nil. It is used by main.go.
//
// TODO: make this the default? We're in the Go 1.8 freeze now, so
// this is too invasive to be default, but we want it for
// https://beta.golang.org/
package main
import (
"crypto/tls"
"flag"
"net"
"net/http"
"time"
"golang.org/x/crypto/acme/autocert"
"golang.org/x/net/http2"
)
var (
autoCertDirFlag = flag.String("autocert_cache_dir", "/var/cache/autocert", "Directory to cache TLS certs")
autoCertHostFlag = flag.String("autocert_hostname", "", "optional hostname to require in autocert SNI requests")
)
func init() {
runHTTPS = runHTTPSAutocert
certInit = certInitAutocert
wrapHTTPMux = wrapHTTPMuxAutocert
}
var autocertManager *autocert.Manager
func certInitAutocert() {
autocertManager = &autocert.Manager{
Cache: autocert.DirCache(*autoCertDirFlag),
Prompt: autocert.AcceptTOS,
}
if *autoCertHostFlag != "" {
autocertManager.HostPolicy = autocert.HostWhitelist(*autoCertHostFlag)
}
}
func runHTTPSAutocert(h http.Handler) error {
srv := &http.Server{
Handler: h,
TLSConfig: &tls.Config{
GetCertificate: autocertManager.GetCertificate,
},
IdleTimeout: 60 * time.Second,
}
http2.ConfigureServer(srv, &http2.Server{})
ln, err := net.Listen("tcp", ":443")
if err != nil {
return err
}
return srv.Serve(tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, srv.TLSConfig))
}
func wrapHTTPMuxAutocert(h http.Handler) http.Handler {
return autocertManager.HTTPHandler(h)
}
// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted
// connections. It's used by ListenAndServe and ListenAndServeTLS so
// dead TCP connections (e.g. closing laptop mid-download) eventually
// go away.
type tcpKeepAliveListener struct {
*net.TCPListener
}
func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
tc, err := ln.AcceptTCP()
if err != nil {
return
}
tc.SetKeepAlive(true)
tc.SetKeepAlivePeriod(3 * time.Minute)
return tc, nil
}

85
cmd/golangorg/blog.go Normal file
Просмотреть файл

@ -0,0 +1,85 @@
// 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.
package main
import (
"fmt"
"go/build"
"log"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"golang.org/x/tools/blog"
"golang.org/x/website/cmd/golangorg/godoc/redirect"
)
const (
blogRepo = "golang.org/x/blog"
blogURL = "https://blog.golang.org/"
blogPath = "/blog/"
)
var (
blogServer http.Handler // set by blogInit
blogInitOnce sync.Once
playEnabled bool
)
func init() {
// Initialize blog only when first accessed.
http.HandleFunc(blogPath, func(w http.ResponseWriter, r *http.Request) {
blogInitOnce.Do(func() {
blogInit(r.Host)
})
blogServer.ServeHTTP(w, r)
})
}
func blogInit(host string) {
// Binary distributions included the blog content in "/blog".
// We stopped including this in Go 1.11.
root := filepath.Join(runtime.GOROOT(), "blog")
// Prefer content from the golang.org/x/blog repository if present.
if pkg, err := build.Import(blogRepo, "", build.FindOnly); err == nil {
root = pkg.Dir
}
// If content is not available fall back to redirect.
if fi, err := os.Stat(root); err != nil || !fi.IsDir() {
fmt.Fprintf(os.Stderr, "Blog content not available locally. "+
"To install, run \n\tgo get %v\n", blogRepo)
blogServer = http.HandlerFunc(blogRedirectHandler)
return
}
s, err := blog.NewServer(blog.Config{
BaseURL: blogPath,
BasePath: strings.TrimSuffix(blogPath, "/"),
ContentPath: filepath.Join(root, "content"),
TemplatePath: filepath.Join(root, "template"),
HomeArticles: 5,
PlayEnabled: playEnabled,
ServeLocalLinks: strings.HasPrefix(host, "localhost"),
})
if err != nil {
log.Fatal(err)
}
blogServer = s
}
func blogRedirectHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == blogPath {
http.Redirect(w, r, blogURL, http.StatusFound)
return
}
blogPrefixHandler.ServeHTTP(w, r)
}
var blogPrefixHandler = redirect.PrefixHandler(blogPath, blogURL)

Просмотреть файл

@ -0,0 +1,25 @@
# Copyright 2018 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.
# NOTE(cbro): any changes to the docker command must also be
# made in docker-build in the Makefile.
#
# Variable substitutions must have a preceding underscore. See:
# https://cloud.google.com/cloud-build/docs/configuring-builds/substitute-variable-values#using_user-defined_substitutions
steps:
- name: 'gcr.io/cloud-builders/docker'
args: [
'build',
'-f=cmd/godoc/Dockerfile.prod',
'--build-arg=GO_REF=${_GO_REF}',
'--build-arg=TOOLS_HEAD=${_TOOLS_HEAD}',
'--build-arg=TOOLS_CLEAN=${_TOOLS_CLEAN}',
'--build-arg=DOCKER_TAG=${_DOCKER_TAG}',
'--build-arg=BUILD_ENV=cloudbuild',
'--tag=${_DOCKER_TAG}',
'.',
]
images: ['${_DOCKER_TAG}']
options:
machineType: 'N1_HIGHCPU_8' # building the godoc index takes a lot of memory.

523
cmd/golangorg/codewalk.go Normal file
Просмотреть файл

@ -0,0 +1,523 @@
// Copyright 2010 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.
// The /doc/codewalk/ tree is synthesized from codewalk descriptions,
// files named $GOROOT/doc/codewalk/*.xml.
// For an example and a description of the format, see
// http://golang.org/doc/codewalk/codewalk or run godoc -http=:6060
// and see http://localhost:6060/doc/codewalk/codewalk .
// That page is itself a codewalk; the source code for it is
// $GOROOT/doc/codewalk/codewalk.xml.
package main
import (
"bytes"
"encoding/xml"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
pathpkg "path"
"regexp"
"sort"
"strconv"
"strings"
"text/template"
"unicode/utf8"
"golang.org/x/website/cmd/golangorg/godoc"
"golang.org/x/website/cmd/golangorg/godoc/vfs"
)
var codewalkHTML, codewalkdirHTML *template.Template
// Handler for /doc/codewalk/ and below.
func codewalk(w http.ResponseWriter, r *http.Request) {
relpath := r.URL.Path[len("/doc/codewalk/"):]
abspath := r.URL.Path
r.ParseForm()
if f := r.FormValue("fileprint"); f != "" {
codewalkFileprint(w, r, f)
return
}
// If directory exists, serve list of code walks.
dir, err := fs.Lstat(abspath)
if err == nil && dir.IsDir() {
codewalkDir(w, r, relpath, abspath)
return
}
// If file exists, serve using standard file server.
if err == nil {
pres.ServeFile(w, r)
return
}
// Otherwise append .xml and hope to find
// a codewalk description, but before trim
// the trailing /.
abspath = strings.TrimRight(abspath, "/")
cw, err := loadCodewalk(abspath + ".xml")
if err != nil {
log.Print(err)
pres.ServeError(w, r, relpath, err)
return
}
// Canonicalize the path and redirect if changed
if redir(w, r) {
return
}
pres.ServePage(w, godoc.Page{
Title: "Codewalk: " + cw.Title,
Tabtitle: cw.Title,
Body: applyTemplate(codewalkHTML, "codewalk", cw),
})
}
func redir(w http.ResponseWriter, r *http.Request) (redirected bool) {
canonical := pathpkg.Clean(r.URL.Path)
if !strings.HasSuffix(canonical, "/") {
canonical += "/"
}
if r.URL.Path != canonical {
url := *r.URL
url.Path = canonical
http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
redirected = true
}
return
}
func applyTemplate(t *template.Template, name string, data interface{}) []byte {
var buf bytes.Buffer
if err := t.Execute(&buf, data); err != nil {
log.Printf("%s.Execute: %s", name, err)
}
return buf.Bytes()
}
// A Codewalk represents a single codewalk read from an XML file.
type Codewalk struct {
Title string `xml:"title,attr"`
File []string `xml:"file"`
Step []*Codestep `xml:"step"`
}
// A Codestep is a single step in a codewalk.
type Codestep struct {
// Filled in from XML
Src string `xml:"src,attr"`
Title string `xml:"title,attr"`
XML string `xml:",innerxml"`
// Derived from Src; not in XML.
Err error
File string
Lo int
LoByte int
Hi int
HiByte int
Data []byte
}
// String method for printing in template.
// Formats file address nicely.
func (st *Codestep) String() string {
s := st.File
if st.Lo != 0 || st.Hi != 0 {
s += fmt.Sprintf(":%d", st.Lo)
if st.Lo != st.Hi {
s += fmt.Sprintf(",%d", st.Hi)
}
}
return s
}
// loadCodewalk reads a codewalk from the named XML file.
func loadCodewalk(filename string) (*Codewalk, error) {
f, err := fs.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
cw := new(Codewalk)
d := xml.NewDecoder(f)
d.Entity = xml.HTMLEntity
err = d.Decode(cw)
if err != nil {
return nil, &os.PathError{Op: "parsing", Path: filename, Err: err}
}
// Compute file list, evaluate line numbers for addresses.
m := make(map[string]bool)
for _, st := range cw.Step {
i := strings.Index(st.Src, ":")
if i < 0 {
i = len(st.Src)
}
filename := st.Src[0:i]
data, err := vfs.ReadFile(fs, filename)
if err != nil {
st.Err = err
continue
}
if i < len(st.Src) {
lo, hi, err := addrToByteRange(st.Src[i+1:], 0, data)
if err != nil {
st.Err = err
continue
}
// Expand match to line boundaries.
for lo > 0 && data[lo-1] != '\n' {
lo--
}
for hi < len(data) && (hi == 0 || data[hi-1] != '\n') {
hi++
}
st.Lo = byteToLine(data, lo)
st.Hi = byteToLine(data, hi-1)
}
st.Data = data
st.File = filename
m[filename] = true
}
// Make list of files
cw.File = make([]string, len(m))
i := 0
for f := range m {
cw.File[i] = f
i++
}
sort.Strings(cw.File)
return cw, nil
}
// codewalkDir serves the codewalk directory listing.
// It scans the directory for subdirectories or files named *.xml
// and prepares a table.
func codewalkDir(w http.ResponseWriter, r *http.Request, relpath, abspath string) {
type elem struct {
Name string
Title string
}
dir, err := fs.ReadDir(abspath)
if err != nil {
log.Print(err)
pres.ServeError(w, r, relpath, err)
return
}
var v []interface{}
for _, fi := range dir {
name := fi.Name()
if fi.IsDir() {
v = append(v, &elem{name + "/", ""})
} else if strings.HasSuffix(name, ".xml") {
cw, err := loadCodewalk(abspath + "/" + name)
if err != nil {
continue
}
v = append(v, &elem{name[0 : len(name)-len(".xml")], cw.Title})
}
}
pres.ServePage(w, godoc.Page{
Title: "Codewalks",
Body: applyTemplate(codewalkdirHTML, "codewalkdir", v),
})
}
// codewalkFileprint serves requests with ?fileprint=f&lo=lo&hi=hi.
// The filename f has already been retrieved and is passed as an argument.
// Lo and hi are the numbers of the first and last line to highlight
// in the response. This format is used for the middle window pane
// of the codewalk pages. It is a separate iframe and does not get
// the usual godoc HTML wrapper.
func codewalkFileprint(w http.ResponseWriter, r *http.Request, f string) {
abspath := f
data, err := vfs.ReadFile(fs, abspath)
if err != nil {
log.Print(err)
pres.ServeError(w, r, f, err)
return
}
lo, _ := strconv.Atoi(r.FormValue("lo"))
hi, _ := strconv.Atoi(r.FormValue("hi"))
if hi < lo {
hi = lo
}
lo = lineToByte(data, lo)
hi = lineToByte(data, hi+1)
// Put the mark 4 lines before lo, so that the iframe
// shows a few lines of context before the highlighted
// section.
n := 4
mark := lo
for ; mark > 0 && n > 0; mark-- {
if data[mark-1] == '\n' {
if n--; n == 0 {
break
}
}
}
io.WriteString(w, `<style type="text/css">@import "/doc/codewalk/codewalk.css";</style><pre>`)
template.HTMLEscape(w, data[0:mark])
io.WriteString(w, "<a name='mark'></a>")
template.HTMLEscape(w, data[mark:lo])
if lo < hi {
io.WriteString(w, "<div class='codewalkhighlight'>")
template.HTMLEscape(w, data[lo:hi])
io.WriteString(w, "</div>")
}
template.HTMLEscape(w, data[hi:])
io.WriteString(w, "</pre>")
}
// addrToByte evaluates the given address starting at offset start in data.
// It returns the lo and hi byte offset of the matched region within data.
// See http://plan9.bell-labs.com/sys/doc/sam/sam.html Table II
// for details on the syntax.
func addrToByteRange(addr string, start int, data []byte) (lo, hi int, err error) {
var (
dir byte
prevc byte
charOffset bool
)
lo = start
hi = start
for addr != "" && err == nil {
c := addr[0]
switch c {
default:
err = errors.New("invalid address syntax near " + string(c))
case ',':
if len(addr) == 1 {
hi = len(data)
} else {
_, hi, err = addrToByteRange(addr[1:], hi, data)
}
return
case '+', '-':
if prevc == '+' || prevc == '-' {
lo, hi, err = addrNumber(data, lo, hi, prevc, 1, charOffset)
}
dir = c
case '$':
lo = len(data)
hi = len(data)
if len(addr) > 1 {
dir = '+'
}
case '#':
charOffset = true
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
var i int
for i = 1; i < len(addr); i++ {
if addr[i] < '0' || addr[i] > '9' {
break
}
}
var n int
n, err = strconv.Atoi(addr[0:i])
if err != nil {
break
}
lo, hi, err = addrNumber(data, lo, hi, dir, n, charOffset)
dir = 0
charOffset = false
prevc = c
addr = addr[i:]
continue
case '/':
var i, j int
Regexp:
for i = 1; i < len(addr); i++ {
switch addr[i] {
case '\\':
i++
case '/':
j = i + 1
break Regexp
}
}
if j == 0 {
j = i
}
pattern := addr[1:i]
lo, hi, err = addrRegexp(data, lo, hi, dir, pattern)
prevc = c
addr = addr[j:]
continue
}
prevc = c
addr = addr[1:]
}
if err == nil && dir != 0 {
lo, hi, err = addrNumber(data, lo, hi, dir, 1, charOffset)
}
if err != nil {
return 0, 0, err
}
return lo, hi, nil
}
// addrNumber applies the given dir, n, and charOffset to the address lo, hi.
// dir is '+' or '-', n is the count, and charOffset is true if the syntax
// used was #n. Applying +n (or +#n) means to advance n lines
// (or characters) after hi. Applying -n (or -#n) means to back up n lines
// (or characters) before lo.
// The return value is the new lo, hi.
func addrNumber(data []byte, lo, hi int, dir byte, n int, charOffset bool) (int, int, error) {
switch dir {
case 0:
lo = 0
hi = 0
fallthrough
case '+':
if charOffset {
pos := hi
for ; n > 0 && pos < len(data); n-- {
_, size := utf8.DecodeRune(data[pos:])
pos += size
}
if n == 0 {
return pos, pos, nil
}
break
}
// find next beginning of line
if hi > 0 {
for hi < len(data) && data[hi-1] != '\n' {
hi++
}
}
lo = hi
if n == 0 {
return lo, hi, nil
}
for ; hi < len(data); hi++ {
if data[hi] != '\n' {
continue
}
switch n--; n {
case 1:
lo = hi + 1
case 0:
return lo, hi + 1, nil
}
}
case '-':
if charOffset {
// Scan backward for bytes that are not UTF-8 continuation bytes.
pos := lo
for ; pos > 0 && n > 0; pos-- {
if data[pos]&0xc0 != 0x80 {
n--
}
}
if n == 0 {
return pos, pos, nil
}
break
}
// find earlier beginning of line
for lo > 0 && data[lo-1] != '\n' {
lo--
}
hi = lo
if n == 0 {
return lo, hi, nil
}
for ; lo >= 0; lo-- {
if lo > 0 && data[lo-1] != '\n' {
continue
}
switch n--; n {
case 1:
hi = lo
case 0:
return lo, hi, nil
}
}
}
return 0, 0, errors.New("address out of range")
}
// addrRegexp searches for pattern in the given direction starting at lo, hi.
// The direction dir is '+' (search forward from hi) or '-' (search backward from lo).
// Backward searches are unimplemented.
func addrRegexp(data []byte, lo, hi int, dir byte, pattern string) (int, int, error) {
re, err := regexp.Compile(pattern)
if err != nil {
return 0, 0, err
}
if dir == '-' {
// Could implement reverse search using binary search
// through file, but that seems like overkill.
return 0, 0, errors.New("reverse search not implemented")
}
m := re.FindIndex(data[hi:])
if len(m) > 0 {
m[0] += hi
m[1] += hi
} else if hi > 0 {
// No match. Wrap to beginning of data.
m = re.FindIndex(data)
}
if len(m) == 0 {
return 0, 0, errors.New("no match for " + pattern)
}
return m[0], m[1], nil
}
// lineToByte returns the byte index of the first byte of line n.
// Line numbers begin at 1.
func lineToByte(data []byte, n int) int {
if n <= 1 {
return 0
}
n--
for i, c := range data {
if c == '\n' {
if n--; n == 0 {
return i + 1
}
}
}
return len(data)
}
// byteToLine returns the number of the line containing the byte at index i.
func byteToLine(data []byte, i int) int {
l := 1
for j, c := range data {
if j == i {
return l
}
if c == '\n' {
l++
}
}
return l
}

16
cmd/golangorg/dl.go Normal file
Просмотреть файл

@ -0,0 +1,16 @@
// 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.
// +build !golangorg
package main
import "net/http"
// Register a redirect handler for /dl/ to the golang.org download page.
// This file will not be included when deploying godoc to golang.org.
func init() {
http.Handle("/dl/", http.RedirectHandler("https://golang.org/dl/", http.StatusFound))
}

118
cmd/golangorg/doc.go Normal file
Просмотреть файл

@ -0,0 +1,118 @@
// Copyright 2009 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.
/*
Godoc extracts and generates documentation for Go programs.
It runs as a web server and presents the documentation as a
web page.
godoc -http=:6060
Usage:
godoc [flag]
The flags are:
-v
verbose mode
-timestamps=true
show timestamps with directory listings
-index
enable identifier and full text search index
(no search box is shown if -index is not set)
-index_files=""
glob pattern specifying index files; if not empty,
the index is read from these files in sorted order
-index_throttle=0.75
index throttle value; a value of 0 means no time is allocated
to the indexer (the indexer will never finish), a value of 1.0
means that index creation is running at full throttle (other
goroutines may get no time while the index is built)
-index_interval=0
interval of indexing; a value of 0 sets it to 5 minutes, a
negative value indexes only once at startup
-play=false
enable playground
-links=true
link identifiers to their declarations
-write_index=false
write index to a file; the file name must be specified with
-index_files
-maxresults=10000
maximum number of full text search results shown
(no full text index is built if maxresults <= 0)
-notes="BUG"
regular expression matching note markers to show
(e.g., "BUG|TODO", ".*")
-goroot=$GOROOT
Go root directory
-http=addr
HTTP service address (e.g., '127.0.0.1:6060' or just ':6060')
-analysis=type,pointer
comma-separated list of analyses to perform
"type": display identifier resolution, type info, method sets,
'implements', and static callees
"pointer": display channel peers, callers and dynamic callees
(significantly slower)
See https://golang.org/lib/godoc/analysis/help.html for details.
-templates=""
directory containing alternate template files; if set,
the directory may provide alternative template files
for the files in $GOROOT/lib/godoc
-url=path
print to standard output the data that would be served by
an HTTP request for path
-zip=""
zip file providing the file system to serve; disabled if empty
By default, godoc looks at the packages it finds via $GOROOT and $GOPATH (if set).
This behavior can be altered by providing an alternative $GOROOT with the -goroot
flag.
When the -index flag is set, a search index is maintained.
The index is created at startup.
The index contains both identifier and full text search information (searchable
via regular expressions). The maximum number of full text search results shown
can be set with the -maxresults flag; if set to 0, no full text results are
shown, and only an identifier index but no full text search index is created.
By default, godoc uses the system's GOOS/GOARCH. You can provide the URL parameters
"GOOS" and "GOARCH" to set the output on the web page for the target system.
The presentation mode of web pages served by godoc can be controlled with the
"m" URL parameter; it accepts a comma-separated list of flag names as value:
all show documentation for all declarations, not just the exported ones
methods show all embedded methods, not just those of unexported anonymous fields
src show the original source code rather then the extracted documentation
For instance, https://golang.org/pkg/math/big/?m=all shows the documentation
for all (not just the exported) declarations of package big.
By default, godoc serves files from the file system of the underlying OS.
Instead, a .zip file may be provided via the -zip flag, which contains
the file system to serve. The file paths stored in the .zip file must use
slash ('/') as path separator; and they must be unrooted. $GOROOT (or -goroot)
must be set to the .zip file directory path containing the Go root directory.
For instance, for a .zip file created by the command:
zip -r go.zip $HOME/go
one may run godoc as follows:
godoc -http=:6060 -zip=go.zip -goroot=$HOME/go
Godoc documentation is converted to HTML or to text using the go/doc package;
see https://golang.org/pkg/go/doc/#ToHTML for the exact rules.
Godoc also shows example code that is runnable by the testing package;
see https://golang.org/pkg/testing/#hdr-Examples for the conventions.
See "Godoc: documenting Go code" for how to write good comments for godoc:
https://golang.org/doc/articles/godoc_documenting_go_code.html
*/
package main // import "golang.org/x/website/cmd/golangorg"

Просмотреть файл

@ -0,0 +1,72 @@
#!/usr/bin/env bash
# Copyright 2011 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 script creates a .zip file representing the $GOROOT file system
# and computes the corresponding search index files.
#
# These are used in production (see app.prod.yaml)
set -e -u -x
ZIPFILE=godoc.zip
INDEXFILE=godoc.index
SPLITFILES=index.split.
error() {
echo "error: $1"
exit 2
}
install() {
go install
}
getArgs() {
if [ ! -v GODOC_DOCSET ]; then
GODOC_DOCSET="$(go env GOROOT)"
echo "GODOC_DOCSET not set explicitly, using GOROOT instead"
fi
# safety checks
if [ ! -d "$GODOC_DOCSET" ]; then
error "$GODOC_DOCSET is not a directory"
fi
# reporting
echo "GODOC_DOCSET = $GODOC_DOCSET"
}
makeZipfile() {
echo "*** make $ZIPFILE"
rm -f $ZIPFILE goroot
ln -s "$GODOC_DOCSET" goroot
zip -q -r $ZIPFILE goroot/* # glob to ignore dotfiles (like .git)
rm goroot
}
makeIndexfile() {
echo "*** make $INDEXFILE"
godoc=$(go env GOPATH)/bin/godoc
# NOTE: run godoc without GOPATH set. Otherwise third-party packages will end up in the index.
GOPATH= $godoc -write_index -goroot goroot -index_files=$INDEXFILE -zip=$ZIPFILE
}
splitIndexfile() {
echo "*** split $INDEXFILE"
rm -f $SPLITFILES*
split -b8m $INDEXFILE $SPLITFILES
}
cd $(dirname $0)
install
getArgs "$@"
makeZipfile
makeIndexfile
splitIndexfile
rm $INDEXFILE
echo "*** setup complete"

Просмотреть файл

@ -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 package 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.

Просмотреть файл

@ -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/website/cmd/golangorg/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/website/cmd/golangorg"}
//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
}

Просмотреть файл

@ -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) }

Просмотреть файл

@ -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
}

Просмотреть файл

@ -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
}

Просмотреть файл

@ -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
}

Просмотреть файл

@ -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
}

Просмотреть файл

@ -0,0 +1,168 @@
// 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.
package godoc
import (
"errors"
pathpkg "path"
"sync"
"time"
"golang.org/x/website/cmd/golangorg/godoc/analysis"
"golang.org/x/website/cmd/golangorg/godoc/util"
"golang.org/x/website/cmd/golangorg/godoc/vfs"
)
// A Corpus holds all the state related to serving and indexing a
// collection of Go code.
//
// Construct a new Corpus with NewCorpus, then modify options,
// then call its Init method.
type Corpus struct {
fs vfs.FileSystem
// Verbose logging.
Verbose bool
// IndexEnabled controls whether indexing is enabled.
IndexEnabled bool
// IndexFiles specifies a glob pattern specifying index files.
// If not empty, the index is read from these files in sorted
// order.
IndexFiles string
// IndexThrottle specifies the indexing throttle value
// between 0.0 and 1.0. At 0.0, the indexer always sleeps.
// At 1.0, the indexer never sleeps. Because 0.0 is useless
// and redundant with setting IndexEnabled to false, the
// zero value for IndexThrottle means 0.9.
IndexThrottle float64
// IndexInterval specifies the time to sleep between reindexing
// all the sources.
// If zero, a default is used. If negative, the index is only
// built once.
IndexInterval time.Duration
// IndexDocs enables indexing of Go documentation.
// This will produce search results for exported types, functions,
// methods, variables, and constants, and will link to the godoc
// documentation for those identifiers.
IndexDocs bool
// IndexGoCode enables indexing of Go source code.
// This will produce search results for internal and external identifiers
// and will link to both declarations and uses of those identifiers in
// source code.
IndexGoCode bool
// IndexFullText enables full-text indexing.
// This will provide search results for any matching text in any file that
// is indexed, including non-Go files (see whitelisted in index.go).
// Regexp searching is supported via full-text indexing.
IndexFullText bool
// MaxResults optionally specifies the maximum results for indexing.
MaxResults int
// SummarizePackage optionally specifies a function to
// summarize a package. It exists as an optimization to
// avoid reading files to parse package comments.
//
// If SummarizePackage returns false for ok, the caller
// ignores all return values and parses the files in the package
// as if SummarizePackage were nil.
//
// If showList is false, the package is hidden from the
// package listing.
SummarizePackage func(pkg string) (summary string, showList, ok bool)
// IndexDirectory optionally specifies a function to determine
// whether the provided directory should be indexed. The dir
// will be of the form "/src/cmd/6a", "/doc/play",
// "/src/io", etc.
// If nil, all directories are indexed if indexing is enabled.
IndexDirectory func(dir string) bool
testDir string // TODO(bradfitz,adg): migrate old godoc flag? looks unused.
// Send a value on this channel to trigger a metadata refresh.
// It is buffered so that if a signal is not lost if sent
// during a refresh.
refreshMetadataSignal chan bool
// file system information
fsTree util.RWValue // *Directory tree of packages, updated with each sync (but sync code is removed now)
fsModified util.RWValue // timestamp of last call to invalidateIndex
docMetadata util.RWValue // mapping from paths to *Metadata
// SearchIndex is the search index in use.
searchIndex util.RWValue
// Analysis is the result of type and pointer analysis.
Analysis analysis.Result
// flag to check whether a corpus is initialized or not
initMu sync.RWMutex
initDone bool
// pkgAPIInfo contains the information about which package API
// features were added in which version of Go.
pkgAPIInfo apiVersions
}
// NewCorpus returns a new Corpus from a filesystem.
// The returned corpus has all indexing enabled and MaxResults set to 1000.
// Change or set any options on Corpus before calling the Corpus.Init method.
func NewCorpus(fs vfs.FileSystem) *Corpus {
c := &Corpus{
fs: fs,
refreshMetadataSignal: make(chan bool, 1),
MaxResults: 1000,
IndexEnabled: true,
IndexDocs: true,
IndexGoCode: true,
IndexFullText: true,
}
return c
}
func (c *Corpus) CurrentIndex() (*Index, time.Time) {
v, t := c.searchIndex.Get()
idx, _ := v.(*Index)
return idx, t
}
func (c *Corpus) FSModifiedTime() time.Time {
_, ts := c.fsModified.Get()
return ts
}
// Init initializes Corpus, once options on Corpus are set.
// It must be called before any subsequent method calls.
func (c *Corpus) Init() error {
if err := c.initFSTree(); err != nil {
return err
}
c.updateMetadata()
go c.refreshMetadataLoop()
c.initMu.Lock()
c.initDone = true
c.initMu.Unlock()
return nil
}
func (c *Corpus) initFSTree() error {
dir := c.newDirectory(pathpkg.Join("/", c.testDir), -1)
if dir == nil {
return errors.New("godoc: corpus fstree is nil")
}
c.fsTree.Set(dir)
c.invalidateIndex()
return nil
}

Просмотреть файл

@ -0,0 +1,383 @@
// Copyright 2010 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 contains the code dealing with package directory trees.
package godoc
import (
"go/doc"
"go/parser"
"go/token"
"log"
"os"
pathpkg "path"
"runtime"
"sort"
"strings"
"golang.org/x/website/cmd/golangorg/godoc/vfs"
)
// Conventional name for directories containing test data.
// Excluded from directory trees.
//
const testdataDirName = "testdata"
type Directory struct {
Depth int
Path string // directory path; includes Name
Name string // directory name
HasPkg bool // true if the directory contains at least one package
Synopsis string // package documentation, if any
RootType vfs.RootType // root type of the filesystem containing the directory
Dirs []*Directory // subdirectories
}
func isGoFile(fi os.FileInfo) bool {
name := fi.Name()
return !fi.IsDir() &&
len(name) > 0 && name[0] != '.' && // ignore .files
pathpkg.Ext(name) == ".go"
}
func isPkgFile(fi os.FileInfo) bool {
return isGoFile(fi) &&
!strings.HasSuffix(fi.Name(), "_test.go") // ignore test files
}
func isPkgDir(fi os.FileInfo) bool {
name := fi.Name()
return fi.IsDir() && len(name) > 0 &&
name[0] != '_' && name[0] != '.' // ignore _files and .files
}
type treeBuilder struct {
c *Corpus
maxDepth int
}
// ioGate is a semaphore controlling VFS activity (ReadDir, parseFile, etc).
// Send before an operation and receive after.
var ioGate = make(chan struct{}, 20)
// workGate controls the number of concurrent workers. Too many concurrent
// workers and performance degrades and the race detector gets overwhelmed. If
// we cannot check out a concurrent worker, work is performed by the main thread
// instead of spinning up another goroutine.
var workGate = make(chan struct{}, runtime.NumCPU()*4)
func (b *treeBuilder) newDirTree(fset *token.FileSet, path, name string, depth int) *Directory {
if name == testdataDirName {
return nil
}
if depth >= b.maxDepth {
// return a dummy directory so that the parent directory
// doesn't get discarded just because we reached the max
// directory depth
return &Directory{
Depth: depth,
Path: path,
Name: name,
}
}
var synopses [3]string // prioritized package documentation (0 == highest priority)
show := true // show in package listing
hasPkgFiles := false
haveSummary := false
if hook := b.c.SummarizePackage; hook != nil {
if summary, show0, ok := hook(strings.TrimPrefix(path, "/src/")); ok {
hasPkgFiles = true
show = show0
synopses[0] = summary
haveSummary = true
}
}
ioGate <- struct{}{}
list, err := b.c.fs.ReadDir(path)
<-ioGate
if err != nil {
// TODO: propagate more. See golang.org/issue/14252.
// For now:
if b.c.Verbose {
log.Printf("newDirTree reading %s: %v", path, err)
}
}
// determine number of subdirectories and if there are package files
var dirchs []chan *Directory
var dirs []*Directory
for _, d := range list {
filename := pathpkg.Join(path, d.Name())
switch {
case isPkgDir(d):
name := d.Name()
select {
case workGate <- struct{}{}:
ch := make(chan *Directory, 1)
dirchs = append(dirchs, ch)
go func() {
ch <- b.newDirTree(fset, filename, name, depth+1)
<-workGate
}()
default:
// no free workers, do work synchronously
dir := b.newDirTree(fset, filename, name, depth+1)
if dir != nil {
dirs = append(dirs, dir)
}
}
case !haveSummary && isPkgFile(d):
// looks like a package file, but may just be a file ending in ".go";
// don't just count it yet (otherwise we may end up with hasPkgFiles even
// though the directory doesn't contain any real package files - was bug)
// no "optimal" package synopsis yet; continue to collect synopses
ioGate <- struct{}{}
const flags = parser.ParseComments | parser.PackageClauseOnly
file, err := b.c.parseFile(fset, filename, flags)
<-ioGate
if err != nil {
if b.c.Verbose {
log.Printf("Error parsing %v: %v", filename, err)
}
break
}
hasPkgFiles = true
if file.Doc != nil {
// prioritize documentation
i := -1
switch file.Name.Name {
case name:
i = 0 // normal case: directory name matches package name
case "main":
i = 1 // directory contains a main package
default:
i = 2 // none of the above
}
if 0 <= i && i < len(synopses) && synopses[i] == "" {
synopses[i] = doc.Synopsis(file.Doc.Text())
}
}
haveSummary = synopses[0] != ""
}
}
// create subdirectory tree
for _, ch := range dirchs {
if d := <-ch; d != nil {
dirs = append(dirs, d)
}
}
// We need to sort the dirs slice because
// it is appended again after reading from dirchs.
sort.Slice(dirs, func(i, j int) bool {
return dirs[i].Name < dirs[j].Name
})
// if there are no package files and no subdirectories
// containing package files, ignore the directory
if !hasPkgFiles && len(dirs) == 0 {
return nil
}
// select the highest-priority synopsis for the directory entry, if any
synopsis := ""
for _, synopsis = range synopses {
if synopsis != "" {
break
}
}
return &Directory{
Depth: depth,
Path: path,
Name: name,
HasPkg: hasPkgFiles && show, // TODO(bradfitz): add proper Hide field?
Synopsis: synopsis,
RootType: b.c.fs.RootType(path),
Dirs: dirs,
}
}
// newDirectory creates a new package directory tree with at most maxDepth
// levels, anchored at root. The result tree is pruned such that it only
// contains directories that contain package files or that contain
// subdirectories containing package files (transitively). If a non-nil
// pathFilter is provided, directory paths additionally must be accepted
// by the filter (i.e., pathFilter(path) must be true). If a value >= 0 is
// provided for maxDepth, nodes at larger depths are pruned as well; they
// are assumed to contain package files even if their contents are not known
// (i.e., in this case the tree may contain directories w/o any package files).
//
func (c *Corpus) newDirectory(root string, maxDepth int) *Directory {
// The root could be a symbolic link so use Stat not Lstat.
d, err := c.fs.Stat(root)
// If we fail here, report detailed error messages; otherwise
// is is hard to see why a directory tree was not built.
switch {
case err != nil:
log.Printf("newDirectory(%s): %s", root, err)
return nil
case root != "/" && !isPkgDir(d):
log.Printf("newDirectory(%s): not a package directory", root)
return nil
case root == "/" && !d.IsDir():
log.Printf("newDirectory(%s): not a directory", root)
return nil
}
if maxDepth < 0 {
maxDepth = 1e6 // "infinity"
}
b := treeBuilder{c, maxDepth}
// the file set provided is only for local parsing, no position
// information escapes and thus we don't need to save the set
return b.newDirTree(token.NewFileSet(), root, d.Name(), 0)
}
func (dir *Directory) walk(c chan<- *Directory, skipRoot bool) {
if dir != nil {
if !skipRoot {
c <- dir
}
for _, d := range dir.Dirs {
d.walk(c, false)
}
}
}
func (dir *Directory) iter(skipRoot bool) <-chan *Directory {
c := make(chan *Directory)
go func() {
dir.walk(c, skipRoot)
close(c)
}()
return c
}
func (dir *Directory) lookupLocal(name string) *Directory {
for _, d := range dir.Dirs {
if d.Name == name {
return d
}
}
return nil
}
func splitPath(p string) []string {
p = strings.TrimPrefix(p, "/")
if p == "" {
return nil
}
return strings.Split(p, "/")
}
// lookup looks for the *Directory for a given path, relative to dir.
func (dir *Directory) lookup(path string) *Directory {
d := splitPath(dir.Path)
p := splitPath(path)
i := 0
for i < len(d) {
if i >= len(p) || d[i] != p[i] {
return nil
}
i++
}
for dir != nil && i < len(p) {
dir = dir.lookupLocal(p[i])
i++
}
return dir
}
// DirEntry describes a directory entry. The Depth and Height values
// are useful for presenting an entry in an indented fashion.
//
type DirEntry struct {
Depth int // >= 0
Height int // = DirList.MaxHeight - Depth, > 0
Path string // directory path; includes Name, relative to DirList root
Name string // directory name
HasPkg bool // true if the directory contains at least one package
Synopsis string // package documentation, if any
RootType vfs.RootType // root type of the filesystem containing the direntry
}
type DirList struct {
MaxHeight int // directory tree height, > 0
List []DirEntry
}
// hasThirdParty checks whether a list of directory entries has packages outside
// the standard library or not.
func hasThirdParty(list []DirEntry) bool {
for _, entry := range list {
if entry.RootType == vfs.RootTypeGoPath {
return true
}
}
return false
}
// listing creates a (linear) directory listing from a directory tree.
// If skipRoot is set, the root directory itself is excluded from the list.
// If filter is set, only the directory entries whose paths match the filter
// are included.
//
func (root *Directory) listing(skipRoot bool, filter func(string) bool) *DirList {
if root == nil {
return nil
}
// determine number of entries n and maximum height
n := 0
minDepth := 1 << 30 // infinity
maxDepth := 0
for d := range root.iter(skipRoot) {
n++
if minDepth > d.Depth {
minDepth = d.Depth
}
if maxDepth < d.Depth {
maxDepth = d.Depth
}
}
maxHeight := maxDepth - minDepth + 1
if n == 0 {
return nil
}
// create list
list := make([]DirEntry, 0, n)
for d := range root.iter(skipRoot) {
if filter != nil && !filter(d.Path) {
continue
}
var p DirEntry
p.Depth = d.Depth - minDepth
p.Height = maxHeight - p.Depth
// the path is relative to root.Path - remove the root.Path
// prefix (the prefix should always be present but avoid
// crashes and check)
path := strings.TrimPrefix(d.Path, root.Path)
// remove leading separator if any - path must be relative
path = strings.TrimPrefix(path, "/")
p.Path = path
p.Name = d.Name
p.HasPkg = d.HasPkg
p.Synopsis = d.Synopsis
p.RootType = d.RootType
list = append(list, p)
}
return &DirList{maxHeight, list}
}

Просмотреть файл

@ -0,0 +1,64 @@
// Copyright 2018 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 godoc
import (
"go/build"
"path/filepath"
"runtime"
"sort"
"testing"
"golang.org/x/website/cmd/golangorg/godoc/vfs"
"golang.org/x/website/cmd/golangorg/godoc/vfs/gatefs"
)
func TestNewDirTree(t *testing.T) {
fsGate := make(chan bool, 20)
rootfs := gatefs.New(vfs.OS(runtime.GOROOT()), fsGate)
fs := vfs.NameSpace{}
fs.Bind("/", rootfs, "/", vfs.BindReplace)
c := NewCorpus(fs)
// 3 levels deep is enough for testing
dir := c.newDirectory("/", 3)
processDir(t, dir)
}
func processDir(t *testing.T, dir *Directory) {
var list []string
for _, d := range dir.Dirs {
list = append(list, d.Name)
// recursively process the lower level
processDir(t, d)
}
if sort.StringsAreSorted(list) == false {
t.Errorf("list: %v is not sorted\n", list)
}
}
func BenchmarkNewDirectory(b *testing.B) {
if testing.Short() {
b.Skip("not running tests requiring large file scan in short mode")
}
fsGate := make(chan bool, 20)
goroot := runtime.GOROOT()
rootfs := gatefs.New(vfs.OS(goroot), fsGate)
fs := vfs.NameSpace{}
fs.Bind("/", rootfs, "/", vfs.BindReplace)
for _, p := range filepath.SplitList(build.Default.GOPATH) {
fs.Bind("/src/golang.org", gatefs.New(vfs.OS(p), fsGate), "/src/golang.org", vfs.BindAfter)
}
b.ResetTimer()
b.ReportAllocs()
for tries := 0; tries < b.N; tries++ {
corpus := NewCorpus(fs)
corpus.newDirectory("/", -1)
}
}

Просмотреть файл

@ -0,0 +1,352 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
// Package dl implements a simple downloads frontend server.
//
// It accepts HTTP POST requests to create a new download metadata entity, and
// lists entities with sorting and filtering.
// It is designed to run only on the instance of godoc that serves golang.org.
package dl
import (
"fmt"
"html/template"
"regexp"
"sort"
"strconv"
"strings"
"time"
)
const (
downloadBaseURL = "https://dl.google.com/go/"
cacheKey = "download_list_3" // increment if listTemplateData changes
cacheDuration = time.Hour
)
// File represents a file on the golang.org downloads page.
// It should be kept in sync with the upload code in x/build/cmd/release.
type File struct {
Filename string `json:"filename"`
OS string `json:"os"`
Arch string `json:"arch"`
Version string `json:"version"`
Checksum string `json:"-" datastore:",noindex"` // SHA1; deprecated
ChecksumSHA256 string `json:"sha256" datastore:",noindex"`
Size int64 `json:"size" datastore:",noindex"`
Kind string `json:"kind"` // "archive", "installer", "source"
Uploaded time.Time `json:"-"`
}
func (f File) ChecksumType() string {
if f.ChecksumSHA256 != "" {
return "SHA256"
}
return "SHA1"
}
func (f File) PrettyChecksum() string {
if f.ChecksumSHA256 != "" {
return f.ChecksumSHA256
}
return f.Checksum
}
func (f File) PrettyOS() string {
if f.OS == "darwin" {
switch {
case strings.Contains(f.Filename, "osx10.8"):
return "OS X 10.8+"
case strings.Contains(f.Filename, "osx10.6"):
return "OS X 10.6+"
}
}
return pretty(f.OS)
}
func (f File) PrettySize() string {
const mb = 1 << 20
if f.Size == 0 {
return ""
}
if f.Size < mb {
// All Go releases are >1mb, but handle this case anyway.
return fmt.Sprintf("%v bytes", f.Size)
}
return fmt.Sprintf("%.0fMB", float64(f.Size)/mb)
}
var primaryPorts = map[string]bool{
"darwin/amd64": true,
"linux/386": true,
"linux/amd64": true,
"linux/armv6l": true,
"windows/386": true,
"windows/amd64": true,
}
func (f File) PrimaryPort() bool {
if f.Kind == "source" {
return true
}
return primaryPorts[f.OS+"/"+f.Arch]
}
func (f File) Highlight() bool {
switch {
case f.Kind == "source":
return true
case f.Arch == "amd64" && f.OS == "linux":
return true
case f.Arch == "amd64" && f.Kind == "installer":
switch f.OS {
case "windows":
return true
case "darwin":
if !strings.Contains(f.Filename, "osx10.6") {
return true
}
}
}
return false
}
func (f File) URL() string {
return downloadBaseURL + f.Filename
}
type Release struct {
Version string `json:"version"`
Stable bool `json:"stable"`
Files []File `json:"files"`
Visible bool `json:"-"` // show files on page load
SplitPortTable bool `json:"-"` // whether files should be split by primary/other ports.
}
type Feature struct {
// The File field will be filled in by the first stable File
// whose name matches the given fileRE.
File
fileRE *regexp.Regexp
Platform string // "Microsoft Windows", "Apple macOS", "Linux"
Requirements string // "Windows XP and above, 64-bit Intel Processor"
}
// featuredFiles lists the platforms and files to be featured
// at the top of the downloads page.
var featuredFiles = []Feature{
{
Platform: "Microsoft Windows",
Requirements: "Windows 7 or later, Intel 64-bit processor",
fileRE: regexp.MustCompile(`\.windows-amd64\.msi$`),
},
{
Platform: "Apple macOS",
Requirements: "macOS 10.10 or later, Intel 64-bit processor",
fileRE: regexp.MustCompile(`\.darwin-amd64(-osx10\.8)?\.pkg$`),
},
{
Platform: "Linux",
Requirements: "Linux 2.6.23 or later, Intel 64-bit processor",
fileRE: regexp.MustCompile(`\.linux-amd64\.tar\.gz$`),
},
{
Platform: "Source",
fileRE: regexp.MustCompile(`\.src\.tar\.gz$`),
},
}
// data to send to the template; increment cacheKey if you change this.
type listTemplateData struct {
Featured []Feature
Stable, Unstable, Archive []Release
}
var (
listTemplate = template.Must(template.New("").Funcs(templateFuncs).Parse(templateHTML))
templateFuncs = template.FuncMap{"pretty": pretty}
)
func filesToFeatured(fs []File) (featured []Feature) {
for _, feature := range featuredFiles {
for _, file := range fs {
if feature.fileRE.MatchString(file.Filename) {
feature.File = file
featured = append(featured, feature)
break
}
}
}
return
}
func filesToReleases(fs []File) (stable, unstable, archive []Release) {
sort.Sort(fileOrder(fs))
var r *Release
var stableMaj, stableMin int
add := func() {
if r == nil {
return
}
if !r.Stable {
if len(unstable) != 0 {
// Only show one (latest) unstable version.
return
}
maj, min, _ := parseVersion(r.Version)
if maj < stableMaj || maj == stableMaj && min <= stableMin {
// Display unstable version only if newer than the
// latest stable release.
return
}
unstable = append(unstable, *r)
}
// Reports whether the release is the most recent minor version of the
// two most recent major versions.
shouldAddStable := func() bool {
if len(stable) >= 2 {
// Show up to two stable versions.
return false
}
if len(stable) == 0 {
// Most recent stable version.
stableMaj, stableMin, _ = parseVersion(r.Version)
return true
}
if maj, _, _ := parseVersion(r.Version); maj == stableMaj {
// Older minor version of most recent major version.
return false
}
// Second most recent stable version.
return true
}
if !shouldAddStable() {
archive = append(archive, *r)
return
}
// Split the file list into primary/other ports for the stable releases.
// NOTE(cbro): This is only done for stable releases because maintaining the historical
// nature of primary/other ports for older versions is infeasible.
// If freebsd is considered primary some time in the future, we'd not want to
// mark all of the older freebsd binaries as "primary".
// It might be better if we set that as a flag when uploading.
r.SplitPortTable = true
r.Visible = true // Toggle open all stable releases.
stable = append(stable, *r)
}
for _, f := range fs {
if r == nil || f.Version != r.Version {
add()
r = &Release{
Version: f.Version,
Stable: isStable(f.Version),
}
}
r.Files = append(r.Files, f)
}
add()
return
}
// isStable reports whether the version string v is a stable version.
func isStable(v string) bool {
return !strings.Contains(v, "beta") && !strings.Contains(v, "rc")
}
type fileOrder []File
func (s fileOrder) Len() int { return len(s) }
func (s fileOrder) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s fileOrder) Less(i, j int) bool {
a, b := s[i], s[j]
if av, bv := a.Version, b.Version; av != bv {
return versionLess(av, bv)
}
if a.OS != b.OS {
return a.OS < b.OS
}
if a.Arch != b.Arch {
return a.Arch < b.Arch
}
if a.Kind != b.Kind {
return a.Kind < b.Kind
}
return a.Filename < b.Filename
}
func versionLess(a, b string) bool {
// Put stable releases first.
if isStable(a) != isStable(b) {
return isStable(a)
}
maja, mina, ta := parseVersion(a)
majb, minb, tb := parseVersion(b)
if maja == majb {
if mina == minb {
return ta >= tb
}
return mina >= minb
}
return maja >= majb
}
func parseVersion(v string) (maj, min int, tail string) {
if i := strings.Index(v, "beta"); i > 0 {
tail = v[i:]
v = v[:i]
}
if i := strings.Index(v, "rc"); i > 0 {
tail = v[i:]
v = v[:i]
}
p := strings.Split(strings.TrimPrefix(v, "go1."), ".")
maj, _ = strconv.Atoi(p[0])
if len(p) < 2 {
return
}
min, _ = strconv.Atoi(p[1])
return
}
func validUser(user string) bool {
switch user {
case "adg", "bradfitz", "cbro", "andybons", "valsorda", "dmitshur", "katiehockman":
return true
}
return false
}
var (
fileRe = regexp.MustCompile(`^go[0-9a-z.]+\.[0-9a-z.-]+\.(tar\.gz|pkg|msi|zip)$`)
goGetRe = regexp.MustCompile(`^go[0-9a-z.]+\.[0-9a-z.-]+$`)
)
// pretty returns a human-readable version of the given OS, Arch, or Kind.
func pretty(s string) string {
t, ok := prettyStrings[s]
if !ok {
return s
}
return t
}
var prettyStrings = map[string]string{
"darwin": "macOS",
"freebsd": "FreeBSD",
"linux": "Linux",
"windows": "Windows",
"386": "x86",
"amd64": "x86-64",
"armv6l": "ARMv6",
"arm64": "ARMv8",
"archive": "Archive",
"installer": "Installer",
"source": "Source",
}

Просмотреть файл

@ -0,0 +1,135 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package dl
import (
"sort"
"strings"
"testing"
)
func TestParseVersion(t *testing.T) {
for _, c := range []struct {
in string
maj, min int
tail string
}{
{"go1.5", 5, 0, ""},
{"go1.5beta1", 5, 0, "beta1"},
{"go1.5.1", 5, 1, ""},
{"go1.5.1rc1", 5, 1, "rc1"},
} {
maj, min, tail := parseVersion(c.in)
if maj != c.maj || min != c.min || tail != c.tail {
t.Errorf("parseVersion(%q) = %v, %v, %q; want %v, %v, %q",
c.in, maj, min, tail, c.maj, c.min, c.tail)
}
}
}
func TestFileOrder(t *testing.T) {
fs := []File{
{Filename: "go1.3.src.tar.gz", Version: "go1.3", OS: "", Arch: "", Kind: "source"},
{Filename: "go1.3.1.src.tar.gz", Version: "go1.3.1", OS: "", Arch: "", Kind: "source"},
{Filename: "go1.3.linux-amd64.tar.gz", Version: "go1.3", OS: "linux", Arch: "amd64", Kind: "archive"},
{Filename: "go1.3.1.linux-amd64.tar.gz", Version: "go1.3.1", OS: "linux", Arch: "amd64", Kind: "archive"},
{Filename: "go1.3.darwin-amd64.tar.gz", Version: "go1.3", OS: "darwin", Arch: "amd64", Kind: "archive"},
{Filename: "go1.3.darwin-amd64.pkg", Version: "go1.3", OS: "darwin", Arch: "amd64", Kind: "installer"},
{Filename: "go1.3.darwin-386.tar.gz", Version: "go1.3", OS: "darwin", Arch: "386", Kind: "archive"},
{Filename: "go1.3beta1.linux-amd64.tar.gz", Version: "go1.3beta1", OS: "linux", Arch: "amd64", Kind: "archive"},
{Filename: "go1.3beta2.linux-amd64.tar.gz", Version: "go1.3beta2", OS: "linux", Arch: "amd64", Kind: "archive"},
{Filename: "go1.3rc1.linux-amd64.tar.gz", Version: "go1.3rc1", OS: "linux", Arch: "amd64", Kind: "archive"},
{Filename: "go1.2.linux-amd64.tar.gz", Version: "go1.2", OS: "linux", Arch: "amd64", Kind: "archive"},
{Filename: "go1.2.2.linux-amd64.tar.gz", Version: "go1.2.2", OS: "linux", Arch: "amd64", Kind: "archive"},
}
sort.Sort(fileOrder(fs))
var s []string
for _, f := range fs {
s = append(s, f.Filename)
}
got := strings.Join(s, "\n")
want := strings.Join([]string{
"go1.3.1.src.tar.gz",
"go1.3.1.linux-amd64.tar.gz",
"go1.3.src.tar.gz",
"go1.3.darwin-386.tar.gz",
"go1.3.darwin-amd64.tar.gz",
"go1.3.darwin-amd64.pkg",
"go1.3.linux-amd64.tar.gz",
"go1.2.2.linux-amd64.tar.gz",
"go1.2.linux-amd64.tar.gz",
"go1.3rc1.linux-amd64.tar.gz",
"go1.3beta2.linux-amd64.tar.gz",
"go1.3beta1.linux-amd64.tar.gz",
}, "\n")
if got != want {
t.Errorf("sort order is\n%s\nwant:\n%s", got, want)
}
}
func TestFilesToReleases(t *testing.T) {
fs := []File{
{Version: "go1.7.4", OS: "darwin"},
{Version: "go1.7.4", OS: "windows"},
{Version: "go1.7", OS: "darwin"},
{Version: "go1.7", OS: "windows"},
{Version: "go1.6.2", OS: "darwin"},
{Version: "go1.6.2", OS: "windows"},
{Version: "go1.6", OS: "darwin"},
{Version: "go1.6", OS: "windows"},
{Version: "go1.5.2", OS: "darwin"},
{Version: "go1.5.2", OS: "windows"},
{Version: "go1.5", OS: "darwin"},
{Version: "go1.5", OS: "windows"},
{Version: "go1.5beta1", OS: "windows"},
}
stable, unstable, archive := filesToReleases(fs)
if got, want := len(stable), 2; want != got {
t.Errorf("len(stable): got %v, want %v", got, want)
} else {
if got, want := stable[0].Version, "go1.7.4"; want != got {
t.Errorf("stable[0].Version: got %v, want %v", got, want)
}
if got, want := stable[1].Version, "go1.6.2"; want != got {
t.Errorf("stable[1].Version: got %v, want %v", got, want)
}
}
if got, want := len(unstable), 0; want != got {
t.Errorf("len(unstable): got %v, want %v", got, want)
}
if got, want := len(archive), 4; want != got {
t.Errorf("len(archive): got %v, want %v", got, want)
}
}
func TestOldUnstableNotShown(t *testing.T) {
fs := []File{
{Version: "go1.7.4"},
{Version: "go1.7"},
{Version: "go1.7beta1"},
}
_, unstable, _ := filesToReleases(fs)
if len(unstable) != 0 {
t.Errorf("got unstable, want none")
}
}
func TestUnstableShown(t *testing.T) {
fs := []File{
{Version: "go1.8beta2"},
{Version: "go1.8rc1"},
{Version: "go1.7.4"},
{Version: "go1.7"},
{Version: "go1.7beta1"},
}
_, unstable, _ := filesToReleases(fs)
if got, want := len(unstable), 1; got != want {
t.Fatalf("len(unstable): got %v, want %v", got, want)
}
// show rcs ahead of betas.
if got, want := unstable[0].Version, "go1.8rc1"; got != want {
t.Fatalf("unstable[0].Version: got %v, want %v", got, want)
}
}

Просмотреть файл

@ -0,0 +1,266 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
// +build golangorg
package dl
import (
"context"
"crypto/hmac"
"crypto/md5"
"encoding/json"
"fmt"
"html"
"io"
"log"
"net/http"
"strings"
"sync"
"time"
"cloud.google.com/go/datastore"
"golang.org/x/website/cmd/golangorg/godoc/env"
"golang.org/x/website/internal/memcache"
)
type server struct {
datastore *datastore.Client
memcache *memcache.CodecClient
}
func RegisterHandlers(mux *http.ServeMux, dc *datastore.Client, mc *memcache.Client) {
s := server{dc, mc.WithCodec(memcache.Gob)}
mux.HandleFunc("/dl", s.getHandler)
mux.HandleFunc("/dl/", s.getHandler) // also serves listHandler
mux.HandleFunc("/dl/upload", s.uploadHandler)
// NOTE(cbro): this only needs to be run once per project,
// and should be behind an admin login.
// TODO(cbro): move into a locally-run program? or remove?
// mux.HandleFunc("/dl/init", initHandler)
}
// rootKey is the ancestor of all File entities.
var rootKey = datastore.NameKey("FileRoot", "root", nil)
func (h server) listHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
ctx := r.Context()
var d listTemplateData
if err := h.memcache.Get(ctx, cacheKey, &d); err != nil {
if err != memcache.ErrCacheMiss {
log.Printf("ERROR cache get error: %v", err)
// NOTE(cbro): continue to hit datastore if the memcache is down.
}
var fs []File
q := datastore.NewQuery("File").Ancestor(rootKey)
if _, err := h.datastore.GetAll(ctx, q, &fs); err != nil {
log.Printf("ERROR error listing: %v", err)
http.Error(w, "Could not get download page. Try again in a few minutes.", 500)
return
}
d.Stable, d.Unstable, d.Archive = filesToReleases(fs)
if len(d.Stable) > 0 {
d.Featured = filesToFeatured(d.Stable[0].Files)
}
item := &memcache.Item{Key: cacheKey, Object: &d, Expiration: cacheDuration}
if err := h.memcache.Set(ctx, item); err != nil {
log.Printf("ERROR cache set error: %v", err)
}
}
if r.URL.Query().Get("mode") == "json" {
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
if err := enc.Encode(d.Stable); err != nil {
log.Printf("ERROR rendering JSON for releases: %v", err)
}
return
}
if err := listTemplate.ExecuteTemplate(w, "root", d); err != nil {
log.Printf("ERROR executing template: %v", err)
}
}
func (h server) uploadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
ctx := r.Context()
// Authenticate using a user token (same as gomote).
user := r.FormValue("user")
if !validUser(user) {
http.Error(w, "bad user", http.StatusForbidden)
return
}
if r.FormValue("key") != h.userKey(ctx, user) {
http.Error(w, "bad key", http.StatusForbidden)
return
}
var f File
defer r.Body.Close()
if err := json.NewDecoder(r.Body).Decode(&f); err != nil {
log.Printf("ERROR decoding upload JSON: %v", err)
http.Error(w, "Something broke", http.StatusInternalServerError)
return
}
if f.Filename == "" {
http.Error(w, "Must provide Filename", http.StatusBadRequest)
return
}
if f.Uploaded.IsZero() {
f.Uploaded = time.Now()
}
k := datastore.NameKey("File", f.Filename, rootKey)
if _, err := h.datastore.Put(ctx, k, &f); err != nil {
log.Printf("ERROR File entity: %v", err)
http.Error(w, "could not put File entity", http.StatusInternalServerError)
return
}
if err := h.memcache.Delete(ctx, cacheKey); err != nil {
log.Printf("ERROR delete error: %v", err)
}
io.WriteString(w, "OK")
}
func (h server) getHandler(w http.ResponseWriter, r *http.Request) {
isGoGet := (r.Method == "GET" || r.Method == "HEAD") && r.FormValue("go-get") == "1"
// For go get, we need to serve the same meta tags at /dl for cmd/go to
// validate against the import path.
if r.URL.Path == "/dl" && isGoGet {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(w, `<!DOCTYPE html><html><head>
<meta name="go-import" content="golang.org/dl git https://go.googlesource.com/dl">
</head></html>`)
return
}
if r.URL.Path == "/dl" {
http.Redirect(w, r, "/dl/", http.StatusFound)
return
}
name := strings.TrimPrefix(r.URL.Path, "/dl/")
var redirectURL string
switch {
case name == "":
h.listHandler(w, r)
return
case fileRe.MatchString(name):
http.Redirect(w, r, downloadBaseURL+name, http.StatusFound)
return
case name == "gotip":
redirectURL = "https://godoc.org/golang.org/dl/gotip"
case goGetRe.MatchString(name):
redirectURL = "https://golang.org/dl/#" + name
default:
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if !isGoGet {
w.Header().Set("Location", redirectURL)
}
fmt.Fprintf(w, `<!DOCTYPE html>
<html>
<head>
<meta name="go-import" content="golang.org/dl git https://go.googlesource.com/dl">
<meta http-equiv="refresh" content="0; url=%s">
</head>
<body>
Nothing to see here; <a href="%s">move along</a>.
</body>
</html>
`, html.EscapeString(redirectURL), html.EscapeString(redirectURL))
}
func (h server) initHandler(w http.ResponseWriter, r *http.Request) {
var fileRoot struct {
Root string
}
ctx := r.Context()
k := rootKey
_, err := h.datastore.RunInTransaction(ctx, func(tx *datastore.Transaction) error {
err := tx.Get(k, &fileRoot)
if err != nil && err != datastore.ErrNoSuchEntity {
return err
}
_, err = tx.Put(k, &fileRoot)
return err
}, nil)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
io.WriteString(w, "OK")
}
func (h server) userKey(c context.Context, user string) string {
hash := hmac.New(md5.New, []byte(h.secret(c)))
hash.Write([]byte("user-" + user))
return fmt.Sprintf("%x", hash.Sum(nil))
}
// Code below copied from x/build/app/key
var theKey struct {
sync.RWMutex
builderKey
}
type builderKey struct {
Secret string
}
func (k *builderKey) Key() *datastore.Key {
return datastore.NameKey("BuilderKey", "root", nil)
}
func (h server) secret(ctx context.Context) string {
// check with rlock
theKey.RLock()
k := theKey.Secret
theKey.RUnlock()
if k != "" {
return k
}
// prepare to fill; check with lock and keep lock
theKey.Lock()
defer theKey.Unlock()
if theKey.Secret != "" {
return theKey.Secret
}
// fill
if err := h.datastore.Get(ctx, theKey.Key(), &theKey.builderKey); err != nil {
if err == datastore.ErrNoSuchEntity {
// If the key is not stored in datastore, write it.
// This only happens at the beginning of a new deployment.
// The code is left here for SDK use and in case a fresh
// deployment is ever needed. "gophers rule" is not the
// real key.
if env.IsProd() {
panic("lost key from datastore")
}
theKey.Secret = "gophers rule"
h.datastore.Put(ctx, theKey.Key(), &theKey.builderKey)
return theKey.Secret
}
panic("cannot load builder key: " + err.Error())
}
return theKey.Secret
}

Просмотреть файл

@ -0,0 +1,277 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package dl
// TODO(adg): refactor this to use the tools/godoc/static template.
const templateHTML = `
{{define "root"}}
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Downloads - The Go Programming Language</title>
<link type="text/css" rel="stylesheet" href="/lib/godoc/style.css">
<script type="text/javascript">window.initFuncs = [];</script>
<style>
table.codetable {
margin-left: 20px; margin-right: 20px;
border-collapse: collapse;
}
table.codetable tr {
background-color: #f0f0f0;
}
table.codetable tr:nth-child(2n), table.codetable tr.first {
background-color: white;
}
table.codetable td, table.codetable th {
white-space: nowrap;
padding: 6px 10px;
}
table.codetable tt {
font-size: xx-small;
}
table.codetable tr.highlight td {
font-weight: bold;
}
a.downloadBox {
display: block;
color: #222;
border: 1px solid #375EAB;
border-radius: 5px;
background: #E0EBF5;
width: 280px;
float: left;
margin-left: 10px;
margin-bottom: 10px;
padding: 10px;
}
a.downloadBox:hover {
text-decoration: none;
}
.downloadBox .platform {
font-size: large;
}
.downloadBox .filename {
color: #375EAB;
font-weight: bold;
line-height: 1.5em;
}
a.downloadBox:hover .filename {
text-decoration: underline;
}
.downloadBox .size {
font-size: small;
font-weight: normal;
}
.downloadBox .reqs {
font-size: small;
font-style: italic;
}
.downloadBox .checksum {
font-size: 5pt;
}
</style>
</head>
<body>
<div id="topbar"><div class="container">
<div class="top-heading"><a href="/">The Go Programming Language</a></div>
<form method="GET" action="/search">
<div id="menu">
<a href="/doc/">Documents</a>
<a href="/pkg/">Packages</a>
<a href="/project/">The Project</a>
<a href="/help/">Help</a>
<a href="/blog/">Blog</a>
<span class="search-box"><input type="search" id="search" name="q" placeholder="Search" aria-label="Search" required><button type="submit"><span><!-- magnifying glass: --><svg width="24" height="24" viewBox="0 0 24 24"><title>submit search</title><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/><path d="M0 0h24v24H0z" fill="none"/></svg></span></button></span>
</div>
</form>
</div></div>
<div id="page">
<div class="container">
<h1>Downloads</h1>
<p>
After downloading a binary release suitable for your system,
please follow the <a href="/doc/install">installation instructions</a>.
</p>
<p>
If you are building from source,
follow the <a href="/doc/install/source">source installation instructions</a>.
</p>
<p>
See the <a href="/doc/devel/release.html">release history</a> for more
information about Go releases.
</p>
{{with .Featured}}
<h3 id="featured">Featured downloads</h3>
{{range .}}
{{template "download" .}}
{{end}}
{{end}}
<div style="clear: both;"></div>
{{with .Stable}}
<h3 id="stable">Stable versions</h3>
{{template "releases" .}}
{{end}}
{{with .Unstable}}
<h3 id="unstable">Unstable version</h3>
{{template "releases" .}}
{{end}}
{{with .Archive}}
<div class="toggle" id="archive">
<div class="collapsed">
<h3 class="toggleButton" title="Click to show versions">Archived versions</h3>
</div>
<div class="expanded">
<h3 class="toggleButton" title="Click to hide versions">Archived versions</h3>
{{template "releases" .}}
</div>
</div>
{{end}}
<div id="footer">
<p>
Except as
<a href="https://developers.google.com/site-policies#restrictions">noted</a>,
the content of this page is licensed under the Creative Commons
Attribution 3.0 License,<br>
and code is licensed under a <a href="http://golang.org/LICENSE">BSD license</a>.<br>
<a href="http://golang.org/doc/tos.html">Terms of Service</a> |
<a href="http://www.google.com/intl/en/policies/privacy/">Privacy Policy</a>
</p>
</div><!-- #footer -->
</div><!-- .container -->
</div><!-- #page -->
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-11222381-2', 'auto');
ga('send', 'pageview');
</script>
</body>
<script src="/lib/godoc/jquery.js"></script>
<script src="/lib/godoc/godocs.js"></script>
<script>
$(document).ready(function() {
$('a.download').click(function(e) {
// Try using the link text as the file name,
// unless there's a child element of class 'filename'.
var filename = $(this).text();
var child = $(this).find('.filename');
if (child.length > 0) {
filename = child.text();
}
// This must be kept in sync with the filenameRE in godocs.js.
var filenameRE = /^go1\.\d+(\.\d+)?([a-z0-9]+)?\.([a-z0-9]+)(-[a-z0-9]+)?(-osx10\.[68])?\.([a-z.]+)$/;
var m = filenameRE.exec(filename);
if (!m) {
// Don't redirect to the download page if it won't recognize this file.
// (Should not happen.)
return;
}
var dest = "/doc/install";
if (filename.indexOf(".src.") != -1) {
dest += "/source";
}
dest += "?download=" + filename;
e.preventDefault();
e.stopPropagation();
window.location = dest;
});
});
</script>
</html>
{{end}}
{{define "releases"}}
{{range .}}
<div class="toggle{{if .Visible}}Visible{{end}}" id="{{.Version}}">
<div class="collapsed">
<h2 class="toggleButton" title="Click to show downloads for this version">{{.Version}} </h2>
</div>
<div class="expanded">
<h2 class="toggleButton" title="Click to hide downloads for this version">{{.Version}} </h2>
{{if .Stable}}{{else}}
<p>This is an <b>unstable</b> version of Go. Use with caution.</p>
<p>If you already have Go installed, you can install this version by running:</p>
<pre>
go get golang.org/dl/{{.Version}}
</pre>
<p>Then, use the <code>{{.Version}}</code> command instead of the <code>go</code> command to use {{.Version}}.</p>
{{end}}
{{template "files" .}}
</div>
</div>
{{end}}
{{end}}
{{define "files"}}
<table class="codetable">
<thead>
<tr class="first">
<th>File name</th>
<th>Kind</th>
<th>OS</th>
<th>Arch</th>
<th>Size</th>
{{/* Use the checksum type of the first file for the column heading. */}}
<th>{{(index .Files 0).ChecksumType}} Checksum</th>
</tr>
</thead>
{{if .SplitPortTable}}
{{range .Files}}{{if .PrimaryPort}}{{template "file" .}}{{end}}{{end}}
{{/* TODO(cbro): add a link to an explanatory doc page */}}
<tr class="first"><th colspan="6" class="first">Other Ports</th></tr>
{{range .Files}}{{if not .PrimaryPort}}{{template "file" .}}{{end}}{{end}}
{{else}}
{{range .Files}}{{template "file" .}}{{end}}
{{end}}
</table>
{{end}}
{{define "file"}}
<tr{{if .Highlight}} class="highlight"{{end}}>
<td class="filename"><a class="download" href="{{.URL}}">{{.Filename}}</a></td>
<td>{{pretty .Kind}}</td>
<td>{{.PrettyOS}}</td>
<td>{{pretty .Arch}}</td>
<td>{{.PrettySize}}</td>
<td><tt>{{.PrettyChecksum}}</tt></td>
</tr>
{{end}}
{{define "download"}}
<a class="download downloadBox" href="{{.URL}}">
<div class="platform">{{.Platform}}</div>
{{with .Requirements}}<div class="reqs">{{.}}</div>{{end}}
<div>
<span class="filename">{{.Filename}}</span>
{{if .Size}}<span class="size">({{.PrettySize}})</span>{{end}}
</div>
</a>
{{end}}
`

41
cmd/golangorg/godoc/env/env.go поставляемый Normal file
Просмотреть файл

@ -0,0 +1,41 @@
// Copyright 2018 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 env provides environment information for the godoc server running on
// golang.org.
package env
import (
"log"
"os"
"strconv"
)
var (
isProd = boolEnv("GODOC_PROD")
enforceHosts = boolEnv("GODOC_ENFORCE_HOSTS")
)
// IsProd reports whether the server is running in its production configuration
// on golang.org.
func IsProd() bool {
return isProd
}
// EnforceHosts reports whether host filtering should be enforced.
func EnforceHosts() bool {
return enforceHosts
}
func boolEnv(key string) bool {
v := os.Getenv(key)
if v == "" {
return false
}
b, err := strconv.ParseBool(v)
if err != nil {
log.Fatalf("environment variable %s (%q) must be a boolean", key, v)
}
return b
}

Просмотреть файл

@ -0,0 +1,371 @@
// Copyright 2011 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 FormatSelections and FormatText.
// FormatText is used to HTML-format Go and non-Go source
// text with line numbers and highlighted sections. It is
// built on top of FormatSelections, a generic formatter
// for "selected" text.
package godoc
import (
"fmt"
"go/scanner"
"go/token"
"io"
"regexp"
"strconv"
"text/template"
)
// ----------------------------------------------------------------------------
// Implementation of FormatSelections
// A Segment describes a text segment [start, end).
// The zero value of a Segment is a ready-to-use empty segment.
//
type Segment struct {
start, end int
}
func (seg *Segment) isEmpty() bool { return seg.start >= seg.end }
// A Selection is an "iterator" function returning a text segment.
// Repeated calls to a selection return consecutive, non-overlapping,
// non-empty segments, followed by an infinite sequence of empty
// segments. The first empty segment marks the end of the selection.
//
type Selection func() Segment
// A LinkWriter writes some start or end "tag" to w for the text offset offs.
// It is called by FormatSelections at the start or end of each link segment.
//
type LinkWriter func(w io.Writer, offs int, start bool)
// A SegmentWriter formats a text according to selections and writes it to w.
// The selections parameter is a bit set indicating which selections provided
// to FormatSelections overlap with the text segment: If the n'th bit is set
// in selections, the n'th selection provided to FormatSelections is overlapping
// with the text.
//
type SegmentWriter func(w io.Writer, text []byte, selections int)
// FormatSelections takes a text and writes it to w using link and segment
// writers lw and sw as follows: lw is invoked for consecutive segment starts
// and ends as specified through the links selection, and sw is invoked for
// consecutive segments of text overlapped by the same selections as specified
// by selections. The link writer lw may be nil, in which case the links
// Selection is ignored.
//
func FormatSelections(w io.Writer, text []byte, lw LinkWriter, links Selection, sw SegmentWriter, selections ...Selection) {
// If we have a link writer, make the links
// selection the last entry in selections
if lw != nil {
selections = append(selections, links)
}
// compute the sequence of consecutive segment changes
changes := newMerger(selections)
// The i'th bit in bitset indicates that the text
// at the current offset is covered by selections[i].
bitset := 0
lastOffs := 0
// Text segments are written in a delayed fashion
// such that consecutive segments belonging to the
// same selection can be combined (peephole optimization).
// last describes the last segment which has not yet been written.
var last struct {
begin, end int // valid if begin < end
bitset int
}
// flush writes the last delayed text segment
flush := func() {
if last.begin < last.end {
sw(w, text[last.begin:last.end], last.bitset)
}
last.begin = last.end // invalidate last
}
// segment runs the segment [lastOffs, end) with the selection
// indicated by bitset through the segment peephole optimizer.
segment := func(end int) {
if lastOffs < end { // ignore empty segments
if last.end != lastOffs || last.bitset != bitset {
// the last segment is not adjacent to or
// differs from the new one
flush()
// start a new segment
last.begin = lastOffs
}
last.end = end
last.bitset = bitset
}
}
for {
// get the next segment change
index, offs, start := changes.next()
if index < 0 || offs > len(text) {
// no more segment changes or the next change
// is past the end of the text - we're done
break
}
// determine the kind of segment change
if lw != nil && index == len(selections)-1 {
// we have a link segment change (see start of this function):
// format the previous selection segment, write the
// link tag and start a new selection segment
segment(offs)
flush()
lastOffs = offs
lw(w, offs, start)
} else {
// we have a selection change:
// format the previous selection segment, determine
// the new selection bitset and start a new segment
segment(offs)
lastOffs = offs
mask := 1 << uint(index)
if start {
bitset |= mask
} else {
bitset &^= mask
}
}
}
segment(len(text))
flush()
}
// A merger merges a slice of Selections and produces a sequence of
// consecutive segment change events through repeated next() calls.
//
type merger struct {
selections []Selection
segments []Segment // segments[i] is the next segment of selections[i]
}
const infinity int = 2e9
func newMerger(selections []Selection) *merger {
segments := make([]Segment, len(selections))
for i, sel := range selections {
segments[i] = Segment{infinity, infinity}
if sel != nil {
if seg := sel(); !seg.isEmpty() {
segments[i] = seg
}
}
}
return &merger{selections, segments}
}
// next returns the next segment change: index specifies the Selection
// to which the segment belongs, offs is the segment start or end offset
// as determined by the start value. If there are no more segment changes,
// next returns an index value < 0.
//
func (m *merger) next() (index, offs int, start bool) {
// find the next smallest offset where a segment starts or ends
offs = infinity
index = -1
for i, seg := range m.segments {
switch {
case seg.start < offs:
offs = seg.start
index = i
start = true
case seg.end < offs:
offs = seg.end
index = i
start = false
}
}
if index < 0 {
// no offset found => all selections merged
return
}
// offset found - it's either the start or end offset but
// either way it is ok to consume the start offset: set it
// to infinity so it won't be considered in the following
// next call
m.segments[index].start = infinity
if start {
return
}
// end offset found - consume it
m.segments[index].end = infinity
// advance to the next segment for that selection
seg := m.selections[index]()
if !seg.isEmpty() {
m.segments[index] = seg
}
return
}
// ----------------------------------------------------------------------------
// Implementation of FormatText
// lineSelection returns the line segments for text as a Selection.
func lineSelection(text []byte) Selection {
i, j := 0, 0
return func() (seg Segment) {
// find next newline, if any
for j < len(text) {
j++
if text[j-1] == '\n' {
break
}
}
if i < j {
// text[i:j] constitutes a line
seg = Segment{i, j}
i = j
}
return
}
}
// tokenSelection returns, as a selection, the sequence of
// consecutive occurrences of token sel in the Go src text.
//
func tokenSelection(src []byte, sel token.Token) Selection {
var s scanner.Scanner
fset := token.NewFileSet()
file := fset.AddFile("", fset.Base(), len(src))
s.Init(file, src, nil, scanner.ScanComments)
return func() (seg Segment) {
for {
pos, tok, lit := s.Scan()
if tok == token.EOF {
break
}
offs := file.Offset(pos)
if tok == sel {
seg = Segment{offs, offs + len(lit)}
break
}
}
return
}
}
// makeSelection is a helper function to make a Selection from a slice of pairs.
// Pairs describing empty segments are ignored.
//
func makeSelection(matches [][]int) Selection {
i := 0
return func() Segment {
for i < len(matches) {
m := matches[i]
i++
if m[0] < m[1] {
// non-empty segment
return Segment{m[0], m[1]}
}
}
return Segment{}
}
}
// regexpSelection computes the Selection for the regular expression expr in text.
func regexpSelection(text []byte, expr string) Selection {
var matches [][]int
if rx, err := regexp.Compile(expr); err == nil {
matches = rx.FindAllIndex(text, -1)
}
return makeSelection(matches)
}
var selRx = regexp.MustCompile(`^([0-9]+):([0-9]+)`)
// RangeSelection computes the Selection for a text range described
// by the argument str; the range description must match the selRx
// regular expression.
func RangeSelection(str string) Selection {
m := selRx.FindStringSubmatch(str)
if len(m) >= 2 {
from, _ := strconv.Atoi(m[1])
to, _ := strconv.Atoi(m[2])
if from < to {
return makeSelection([][]int{{from, to}})
}
}
return nil
}
// Span tags for all the possible selection combinations that may
// be generated by FormatText. Selections are indicated by a bitset,
// and the value of the bitset specifies the tag to be used.
//
// bit 0: comments
// bit 1: highlights
// bit 2: selections
//
var startTags = [][]byte{
/* 000 */ []byte(``),
/* 001 */ []byte(`<span class="comment">`),
/* 010 */ []byte(`<span class="highlight">`),
/* 011 */ []byte(`<span class="highlight-comment">`),
/* 100 */ []byte(`<span class="selection">`),
/* 101 */ []byte(`<span class="selection-comment">`),
/* 110 */ []byte(`<span class="selection-highlight">`),
/* 111 */ []byte(`<span class="selection-highlight-comment">`),
}
var endTag = []byte(`</span>`)
func selectionTag(w io.Writer, text []byte, selections int) {
if selections < len(startTags) {
if tag := startTags[selections]; len(tag) > 0 {
w.Write(tag)
template.HTMLEscape(w, text)
w.Write(endTag)
return
}
}
template.HTMLEscape(w, text)
}
// FormatText HTML-escapes text and writes it to w.
// Consecutive text segments are wrapped in HTML spans (with tags as
// defined by startTags and endTag) as follows:
//
// - if line >= 0, line number (ln) spans are inserted before each line,
// starting with the value of line
// - if the text is Go source, comments get the "comment" span class
// - each occurrence of the regular expression pattern gets the "highlight"
// span class
// - text segments covered by selection get the "selection" span class
//
// Comments, highlights, and selections may overlap arbitrarily; the respective
// HTML span classes are specified in the startTags variable.
//
func FormatText(w io.Writer, text []byte, line int, goSource bool, pattern string, selection Selection) {
var comments, highlights Selection
if goSource {
comments = tokenSelection(text, token.COMMENT)
}
if pattern != "" {
highlights = regexpSelection(text, pattern)
}
if line >= 0 || comments != nil || highlights != nil || selection != nil {
var lineTag LinkWriter
if line >= 0 {
lineTag = func(w io.Writer, _ int, start bool) {
if start {
fmt.Fprintf(w, "<span id=\"L%d\" class=\"ln\">%6d</span>\t", line, line)
line++
}
}
}
FormatSelections(w, text, lineTag, lineSelection(text), selectionTag, comments, highlights, selection)
} else {
template.HTMLEscape(w, text)
}
}

Просмотреть файл

@ -0,0 +1,959 @@
// 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.
// Package godoc is a work-in-progress (2013-07-17) package to
// begin splitting up the godoc binary into multiple pieces.
//
// This package comment will evolve over time as this package splits
// into smaller pieces.
package godoc // import "golang.org/x/website/cmd/golangorg/godoc"
import (
"bufio"
"bytes"
"fmt"
"go/ast"
"go/doc"
"go/format"
"go/printer"
"go/token"
htmltemplate "html/template"
"io"
"log"
"os"
pathpkg "path"
"regexp"
"strconv"
"strings"
"text/template"
"time"
"unicode"
"unicode/utf8"
)
// Fake relative package path for built-ins. Documentation for all globals
// (not just exported ones) will be shown for packages in this directory.
const builtinPkgPath = "builtin"
// FuncMap defines template functions used in godoc templates.
//
// Convention: template function names ending in "_html" or "_url" produce
// HTML- or URL-escaped strings; all other function results may
// require explicit escaping in the template.
func (p *Presentation) FuncMap() template.FuncMap {
p.initFuncMapOnce.Do(p.initFuncMap)
return p.funcMap
}
func (p *Presentation) TemplateFuncs() template.FuncMap {
p.initFuncMapOnce.Do(p.initFuncMap)
return p.templateFuncs
}
func (p *Presentation) initFuncMap() {
if p.Corpus == nil {
panic("nil Presentation.Corpus")
}
p.templateFuncs = template.FuncMap{
"code": p.code,
}
p.funcMap = template.FuncMap{
// various helpers
"filename": filenameFunc,
"repeat": strings.Repeat,
"since": p.Corpus.pkgAPIInfo.sinceVersionFunc,
// access to FileInfos (directory listings)
"fileInfoName": fileInfoNameFunc,
"fileInfoTime": fileInfoTimeFunc,
// access to search result information
"infoKind_html": infoKind_htmlFunc,
"infoLine": p.infoLineFunc,
"infoSnippet_html": p.infoSnippet_htmlFunc,
// formatting of AST nodes
"node": p.nodeFunc,
"node_html": p.node_htmlFunc,
"comment_html": comment_htmlFunc,
"sanitize": sanitizeFunc,
// support for URL attributes
"pkgLink": pkgLinkFunc,
"srcLink": srcLinkFunc,
"posLink_url": newPosLink_urlFunc(srcPosLinkFunc),
"docLink": docLinkFunc,
"queryLink": queryLinkFunc,
"srcBreadcrumb": srcBreadcrumbFunc,
"srcToPkgLink": srcToPkgLinkFunc,
// formatting of Examples
"example_html": p.example_htmlFunc,
"example_name": p.example_nameFunc,
"example_suffix": p.example_suffixFunc,
// formatting of analysis information
"callgraph_html": p.callgraph_htmlFunc,
"implements_html": p.implements_htmlFunc,
"methodset_html": p.methodset_htmlFunc,
// formatting of Notes
"noteTitle": noteTitle,
// Number operation
"multiply": multiply,
// formatting of PageInfoMode query string
"modeQueryString": modeQueryString,
// check whether to display third party section or not
"hasThirdParty": hasThirdParty,
// get the no. of columns to split the toc in search page
"tocColCount": tocColCount,
}
if p.URLForSrc != nil {
p.funcMap["srcLink"] = p.URLForSrc
}
if p.URLForSrcPos != nil {
p.funcMap["posLink_url"] = newPosLink_urlFunc(p.URLForSrcPos)
}
if p.URLForSrcQuery != nil {
p.funcMap["queryLink"] = p.URLForSrcQuery
}
}
func multiply(a, b int) int { return a * b }
func filenameFunc(path string) string {
_, localname := pathpkg.Split(path)
return localname
}
func fileInfoNameFunc(fi os.FileInfo) string {
name := fi.Name()
if fi.IsDir() {
name += "/"
}
return name
}
func fileInfoTimeFunc(fi os.FileInfo) string {
if t := fi.ModTime(); t.Unix() != 0 {
return t.Local().String()
}
return "" // don't return epoch if time is obviously not set
}
// The strings in infoKinds must be properly html-escaped.
var infoKinds = [nKinds]string{
PackageClause: "package&nbsp;clause",
ImportDecl: "import&nbsp;decl",
ConstDecl: "const&nbsp;decl",
TypeDecl: "type&nbsp;decl",
VarDecl: "var&nbsp;decl",
FuncDecl: "func&nbsp;decl",
MethodDecl: "method&nbsp;decl",
Use: "use",
}
func infoKind_htmlFunc(info SpotInfo) string {
return infoKinds[info.Kind()] // infoKind entries are html-escaped
}
func (p *Presentation) infoLineFunc(info SpotInfo) int {
line := info.Lori()
if info.IsIndex() {
index, _ := p.Corpus.searchIndex.Get()
if index != nil {
line = index.(*Index).Snippet(line).Line
} else {
// no line information available because
// we don't have an index - this should
// never happen; be conservative and don't
// crash
line = 0
}
}
return line
}
func (p *Presentation) infoSnippet_htmlFunc(info SpotInfo) string {
if info.IsIndex() {
index, _ := p.Corpus.searchIndex.Get()
// Snippet.Text was HTML-escaped when it was generated
return index.(*Index).Snippet(info.Lori()).Text
}
return `<span class="alert">no snippet text available</span>`
}
func (p *Presentation) nodeFunc(info *PageInfo, node interface{}) string {
var buf bytes.Buffer
p.writeNode(&buf, info, info.FSet, node)
return buf.String()
}
func (p *Presentation) node_htmlFunc(info *PageInfo, node interface{}, linkify bool) string {
var buf1 bytes.Buffer
p.writeNode(&buf1, info, info.FSet, node)
var buf2 bytes.Buffer
if n, _ := node.(ast.Node); n != nil && linkify && p.DeclLinks {
LinkifyText(&buf2, buf1.Bytes(), n)
if st, name := isStructTypeDecl(n); st != nil {
addStructFieldIDAttributes(&buf2, name, st)
}
} else {
FormatText(&buf2, buf1.Bytes(), -1, true, "", nil)
}
return buf2.String()
}
// isStructTypeDecl checks whether n is a struct declaration.
// It either returns a non-nil StructType and its name, or zero values.
func isStructTypeDecl(n ast.Node) (st *ast.StructType, name string) {
gd, ok := n.(*ast.GenDecl)
if !ok || gd.Tok != token.TYPE {
return nil, ""
}
if gd.Lparen > 0 {
// Parenthesized type. Who does that, anyway?
// TODO: Reportedly gri does. Fix this to handle that too.
return nil, ""
}
if len(gd.Specs) != 1 {
return nil, ""
}
ts, ok := gd.Specs[0].(*ast.TypeSpec)
if !ok {
return nil, ""
}
st, ok = ts.Type.(*ast.StructType)
if !ok {
return nil, ""
}
return st, ts.Name.Name
}
// addStructFieldIDAttributes modifies the contents of buf such that
// all struct fields of the named struct have <span id='name.Field'>
// in them, so people can link to /#Struct.Field.
func addStructFieldIDAttributes(buf *bytes.Buffer, name string, st *ast.StructType) {
if st.Fields == nil {
return
}
// needsLink is a set of identifiers that still need to be
// linked, where value == key, to avoid an allocation in func
// linkedField.
needsLink := make(map[string]string)
for _, f := range st.Fields.List {
if len(f.Names) == 0 {
continue
}
fieldName := f.Names[0].Name
needsLink[fieldName] = fieldName
}
var newBuf bytes.Buffer
foreachLine(buf.Bytes(), func(line []byte) {
if fieldName := linkedField(line, needsLink); fieldName != "" {
fmt.Fprintf(&newBuf, `<span id="%s.%s"></span>`, name, fieldName)
delete(needsLink, fieldName)
}
newBuf.Write(line)
})
buf.Reset()
buf.Write(newBuf.Bytes())
}
// foreachLine calls fn for each line of in, where a line includes
// the trailing "\n", except on the last line, if it doesn't exist.
func foreachLine(in []byte, fn func(line []byte)) {
for len(in) > 0 {
nl := bytes.IndexByte(in, '\n')
if nl == -1 {
fn(in)
return
}
fn(in[:nl+1])
in = in[nl+1:]
}
}
// commentPrefix is the line prefix for comments after they've been HTMLified.
var commentPrefix = []byte(`<span class="comment">// `)
// linkedField determines whether the given line starts with an
// identifer in the provided ids map (mapping from identifier to the
// same identifier). The line can start with either an identifier or
// an identifier in a comment. If one matches, it returns the
// identifier that matched. Otherwise it returns the empty string.
func linkedField(line []byte, ids map[string]string) string {
line = bytes.TrimSpace(line)
// For fields with a doc string of the
// conventional form, we put the new span into
// the comment instead of the field.
// The "conventional" form is a complete sentence
// per https://golang.org/s/style#comment-sentences like:
//
// // Foo is an optional Fooer to foo the foos.
// Foo Fooer
//
// In this case, we want the #StructName.Foo
// link to make the browser go to the comment
// line "Foo is an optional Fooer" instead of
// the "Foo Fooer" line, which could otherwise
// obscure the docs above the browser's "fold".
//
// TODO: do this better, so it works for all
// comments, including unconventional ones.
if bytes.HasPrefix(line, commentPrefix) {
line = line[len(commentPrefix):]
}
id := scanIdentifier(line)
if len(id) == 0 {
// No leading identifier. Avoid map lookup for
// somewhat common case.
return ""
}
return ids[string(id)]
}
// scanIdentifier scans a valid Go identifier off the front of v and
// either returns a subslice of v if there's a valid identifier, or
// returns a zero-length slice.
func scanIdentifier(v []byte) []byte {
var n int // number of leading bytes of v belonging to an identifier
for {
r, width := utf8.DecodeRune(v[n:])
if !(isLetter(r) || n > 0 && isDigit(r)) {
break
}
n += width
}
return v[:n]
}
func isLetter(ch rune) bool {
return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch >= utf8.RuneSelf && unicode.IsLetter(ch)
}
func isDigit(ch rune) bool {
return '0' <= ch && ch <= '9' || ch >= utf8.RuneSelf && unicode.IsDigit(ch)
}
func comment_htmlFunc(comment string) string {
var buf bytes.Buffer
// TODO(gri) Provide list of words (e.g. function parameters)
// to be emphasized by ToHTML.
doc.ToHTML(&buf, comment, nil) // does html-escaping
return buf.String()
}
// punchCardWidth is the number of columns of fixed-width
// characters to assume when wrapping text. Very few people
// use terminals or cards smaller than 80 characters, so 80 it is.
// We do not try to sniff the environment or the tty to adapt to
// the situation; instead, by using a constant we make sure that
// godoc always produces the same output regardless of context,
// a consistency that is lost otherwise. For example, if we sniffed
// the environment or tty, then http://golang.org/pkg/math/?m=text
// would depend on the width of the terminal where godoc started,
// which is clearly bogus. More generally, the Unix tools that behave
// differently when writing to a tty than when writing to a file have
// a history of causing confusion (compare `ls` and `ls | cat`), and we
// want to avoid that mistake here.
const punchCardWidth = 80
func containsOnlySpace(buf []byte) bool {
isNotSpace := func(r rune) bool { return !unicode.IsSpace(r) }
return bytes.IndexFunc(buf, isNotSpace) == -1
}
// sanitizeFunc sanitizes the argument src by replacing newlines with
// blanks, removing extra blanks, and by removing trailing whitespace
// and commas before closing parentheses.
func sanitizeFunc(src string) string {
buf := make([]byte, len(src))
j := 0 // buf index
comma := -1 // comma index if >= 0
for i := 0; i < len(src); i++ {
ch := src[i]
switch ch {
case '\t', '\n', ' ':
// ignore whitespace at the beginning, after a blank, or after opening parentheses
if j == 0 {
continue
}
if p := buf[j-1]; p == ' ' || p == '(' || p == '{' || p == '[' {
continue
}
// replace all whitespace with blanks
ch = ' '
case ',':
comma = j
case ')', '}', ']':
// remove any trailing comma
if comma >= 0 {
j = comma
}
// remove any trailing whitespace
if j > 0 && buf[j-1] == ' ' {
j--
}
default:
comma = -1
}
buf[j] = ch
j++
}
// remove trailing blank, if any
if j > 0 && buf[j-1] == ' ' {
j--
}
return string(buf[:j])
}
type PageInfo struct {
Dirname string // directory containing the package
Err error // error or nil
GoogleCN bool // page is being served from golang.google.cn
Mode PageInfoMode // display metadata from query string
// package info
FSet *token.FileSet // nil if no package documentation
PDoc *doc.Package // nil if no package documentation
Examples []*doc.Example // nil if no example code
Notes map[string][]*doc.Note // nil if no package Notes
PAst map[string]*ast.File // nil if no AST with package exports
IsMain bool // true for package main
IsFiltered bool // true if results were filtered
// analysis info
TypeInfoIndex map[string]int // index of JSON datum for type T (if -analysis=type)
AnalysisData htmltemplate.JS // array of TypeInfoJSON values
CallGraph htmltemplate.JS // array of PCGNodeJSON values (if -analysis=pointer)
CallGraphIndex map[string]int // maps func name to index in CallGraph
// directory info
Dirs *DirList // nil if no directory information
DirTime time.Time // directory time stamp
DirFlat bool // if set, show directory in a flat (non-indented) manner
}
func (info *PageInfo) IsEmpty() bool {
return info.Err != nil || info.PAst == nil && info.PDoc == nil && info.Dirs == nil
}
func pkgLinkFunc(path string) string {
// because of the irregular mapping under goroot
// we need to correct certain relative paths
path = strings.TrimPrefix(path, "/")
path = strings.TrimPrefix(path, "src/")
path = strings.TrimPrefix(path, "pkg/")
return "pkg/" + path
}
// srcToPkgLinkFunc builds an <a> tag linking to the package
// documentation of relpath.
func srcToPkgLinkFunc(relpath string) string {
relpath = pkgLinkFunc(relpath)
relpath = pathpkg.Dir(relpath)
if relpath == "pkg" {
return `<a href="/pkg">Index</a>`
}
return fmt.Sprintf(`<a href="/%s">%s</a>`, relpath, relpath[len("pkg/"):])
}
// srcBreadcrumbFun converts each segment of relpath to a HTML <a>.
// Each segment links to its corresponding src directories.
func srcBreadcrumbFunc(relpath string) string {
segments := strings.Split(relpath, "/")
var buf bytes.Buffer
var selectedSegment string
var selectedIndex int
if strings.HasSuffix(relpath, "/") {
// relpath is a directory ending with a "/".
// Selected segment is the segment before the last slash.
selectedIndex = len(segments) - 2
selectedSegment = segments[selectedIndex] + "/"
} else {
selectedIndex = len(segments) - 1
selectedSegment = segments[selectedIndex]
}
for i := range segments[:selectedIndex] {
buf.WriteString(fmt.Sprintf(`<a href="/%s">%s</a>/`,
strings.Join(segments[:i+1], "/"),
segments[i],
))
}
buf.WriteString(`<span class="text-muted">`)
buf.WriteString(selectedSegment)
buf.WriteString(`</span>`)
return buf.String()
}
func newPosLink_urlFunc(srcPosLinkFunc func(s string, line, low, high int) string) func(info *PageInfo, n interface{}) string {
// n must be an ast.Node or a *doc.Note
return func(info *PageInfo, n interface{}) string {
var pos, end token.Pos
switch n := n.(type) {
case ast.Node:
pos = n.Pos()
end = n.End()
case *doc.Note:
pos = n.Pos
end = n.End
default:
panic(fmt.Sprintf("wrong type for posLink_url template formatter: %T", n))
}
var relpath string
var line int
var low, high int // selection offset range
if pos.IsValid() {
p := info.FSet.Position(pos)
relpath = p.Filename
line = p.Line
low = p.Offset
}
if end.IsValid() {
high = info.FSet.Position(end).Offset
}
return srcPosLinkFunc(relpath, line, low, high)
}
}
func srcPosLinkFunc(s string, line, low, high int) string {
s = srcLinkFunc(s)
var buf bytes.Buffer
template.HTMLEscape(&buf, []byte(s))
// selection ranges are of form "s=low:high"
if low < high {
fmt.Fprintf(&buf, "?s=%d:%d", low, high) // no need for URL escaping
// if we have a selection, position the page
// such that the selection is a bit below the top
line -= 10
if line < 1 {
line = 1
}
}
// line id's in html-printed source are of the
// form "L%d" where %d stands for the line number
if line > 0 {
fmt.Fprintf(&buf, "#L%d", line) // no need for URL escaping
}
return buf.String()
}
func srcLinkFunc(s string) string {
s = pathpkg.Clean("/" + s)
if !strings.HasPrefix(s, "/src/") {
s = "/src" + s
}
return s
}
// queryLinkFunc returns a URL for a line in a source file with a highlighted
// query term.
// s is expected to be a path to a source file.
// query is expected to be a string that has already been appropriately escaped
// for use in a URL query.
func queryLinkFunc(s, query string, line int) string {
url := pathpkg.Clean("/"+s) + "?h=" + query
if line > 0 {
url += "#L" + strconv.Itoa(line)
}
return url
}
func docLinkFunc(s string, ident string) string {
return pathpkg.Clean("/pkg/"+s) + "/#" + ident
}
func (p *Presentation) example_htmlFunc(info *PageInfo, funcName string) string {
var buf bytes.Buffer
for _, eg := range info.Examples {
name := stripExampleSuffix(eg.Name)
if name != funcName {
continue
}
// print code
cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments}
code := p.node_htmlFunc(info, cnode, true)
out := eg.Output
wholeFile := true
// Additional formatting if this is a function body.
if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' {
wholeFile = false
// remove surrounding braces
code = code[1 : n-1]
// unindent
code = replaceLeadingIndentation(code, strings.Repeat(" ", p.TabWidth), "")
// remove output comment
if loc := exampleOutputRx.FindStringIndex(code); loc != nil {
code = strings.TrimSpace(code[:loc[0]])
}
}
// Write out the playground code in standard Go style
// (use tabs, no comment highlight, etc).
play := ""
if eg.Play != nil && p.ShowPlayground {
var buf bytes.Buffer
eg.Play.Comments = filterOutBuildAnnotations(eg.Play.Comments)
if err := format.Node(&buf, info.FSet, eg.Play); err != nil {
log.Print(err)
} else {
play = buf.String()
}
}
// Drop output, as the output comment will appear in the code.
if wholeFile && play == "" {
out = ""
}
if p.ExampleHTML == nil {
out = ""
return ""
}
err := p.ExampleHTML.Execute(&buf, struct {
Name, Doc, Code, Play, Output string
GoogleCN bool
}{eg.Name, eg.Doc, code, play, out, info.GoogleCN})
if err != nil {
log.Print(err)
}
}
return buf.String()
}
func filterOutBuildAnnotations(cg []*ast.CommentGroup) []*ast.CommentGroup {
if len(cg) == 0 {
return cg
}
for i := range cg {
if !strings.HasPrefix(cg[i].Text(), "+build ") {
// Found the first non-build tag, return from here until the end
// of the slice.
return cg[i:]
}
}
// There weren't any non-build tags, return an empty slice.
return []*ast.CommentGroup{}
}
// example_nameFunc takes an example function name and returns its display
// name. For example, "Foo_Bar_quux" becomes "Foo.Bar (Quux)".
func (p *Presentation) example_nameFunc(s string) string {
name, suffix := splitExampleName(s)
// replace _ with . for method names
name = strings.Replace(name, "_", ".", 1)
// use "Package" if no name provided
if name == "" {
name = "Package"
}
return name + suffix
}
// example_suffixFunc takes an example function name and returns its suffix in
// parenthesized form. For example, "Foo_Bar_quux" becomes " (Quux)".
func (p *Presentation) example_suffixFunc(name string) string {
_, suffix := splitExampleName(name)
return suffix
}
// implements_html returns the "> Implements" toggle for a package-level named type.
// Its contents are populated from JSON data by client-side JS at load time.
func (p *Presentation) implements_htmlFunc(info *PageInfo, typeName string) string {
if p.ImplementsHTML == nil {
return ""
}
index, ok := info.TypeInfoIndex[typeName]
if !ok {
return ""
}
var buf bytes.Buffer
err := p.ImplementsHTML.Execute(&buf, struct{ Index int }{index})
if err != nil {
log.Print(err)
}
return buf.String()
}
// methodset_html returns the "> Method set" toggle for a package-level named type.
// Its contents are populated from JSON data by client-side JS at load time.
func (p *Presentation) methodset_htmlFunc(info *PageInfo, typeName string) string {
if p.MethodSetHTML == nil {
return ""
}
index, ok := info.TypeInfoIndex[typeName]
if !ok {
return ""
}
var buf bytes.Buffer
err := p.MethodSetHTML.Execute(&buf, struct{ Index int }{index})
if err != nil {
log.Print(err)
}
return buf.String()
}
// callgraph_html returns the "> Call graph" toggle for a package-level func.
// Its contents are populated from JSON data by client-side JS at load time.
func (p *Presentation) callgraph_htmlFunc(info *PageInfo, recv, name string) string {
if p.CallGraphHTML == nil {
return ""
}
if recv != "" {
// Format must match (*ssa.Function).RelString().
name = fmt.Sprintf("(%s).%s", recv, name)
}
index, ok := info.CallGraphIndex[name]
if !ok {
return ""
}
var buf bytes.Buffer
err := p.CallGraphHTML.Execute(&buf, struct{ Index int }{index})
if err != nil {
log.Print(err)
}
return buf.String()
}
func noteTitle(note string) string {
return strings.Title(strings.ToLower(note))
}
func startsWithUppercase(s string) bool {
r, _ := utf8.DecodeRuneInString(s)
return unicode.IsUpper(r)
}
var exampleOutputRx = regexp.MustCompile(`(?i)//[[:space:]]*(unordered )?output:`)
// stripExampleSuffix strips lowercase braz in Foo_braz or Foo_Bar_braz from name
// while keeping uppercase Braz in Foo_Braz.
func stripExampleSuffix(name string) string {
if i := strings.LastIndex(name, "_"); i != -1 {
if i < len(name)-1 && !startsWithUppercase(name[i+1:]) {
name = name[:i]
}
}
return name
}
func splitExampleName(s string) (name, suffix string) {
i := strings.LastIndex(s, "_")
if 0 <= i && i < len(s)-1 && !startsWithUppercase(s[i+1:]) {
name = s[:i]
suffix = " (" + strings.Title(s[i+1:]) + ")"
return
}
name = s
return
}
// replaceLeadingIndentation replaces oldIndent at the beginning of each line
// with newIndent. This is used for formatting examples. Raw strings that
// span multiple lines are handled specially: oldIndent is not removed (since
// go/printer will not add any indentation there), but newIndent is added
// (since we may still want leading indentation).
func replaceLeadingIndentation(body, oldIndent, newIndent string) string {
// Handle indent at the beginning of the first line. After this, we handle
// indentation only after a newline.
var buf bytes.Buffer
if strings.HasPrefix(body, oldIndent) {
buf.WriteString(newIndent)
body = body[len(oldIndent):]
}
// Use a state machine to keep track of whether we're in a string or
// rune literal while we process the rest of the code.
const (
codeState = iota
runeState
interpretedStringState
rawStringState
)
searchChars := []string{
"'\"`\n", // codeState
`\'`, // runeState
`\"`, // interpretedStringState
"`\n", // rawStringState
// newlineState does not need to search
}
state := codeState
for {
i := strings.IndexAny(body, searchChars[state])
if i < 0 {
buf.WriteString(body)
break
}
c := body[i]
buf.WriteString(body[:i+1])
body = body[i+1:]
switch state {
case codeState:
switch c {
case '\'':
state = runeState
case '"':
state = interpretedStringState
case '`':
state = rawStringState
case '\n':
if strings.HasPrefix(body, oldIndent) {
buf.WriteString(newIndent)
body = body[len(oldIndent):]
}
}
case runeState:
switch c {
case '\\':
r, size := utf8.DecodeRuneInString(body)
buf.WriteRune(r)
body = body[size:]
case '\'':
state = codeState
}
case interpretedStringState:
switch c {
case '\\':
r, size := utf8.DecodeRuneInString(body)
buf.WriteRune(r)
body = body[size:]
case '"':
state = codeState
}
case rawStringState:
switch c {
case '`':
state = codeState
case '\n':
buf.WriteString(newIndent)
}
}
}
return buf.String()
}
// writeNode writes the AST node x to w.
//
// The provided fset must be non-nil. The pageInfo is optional. If
// present, the pageInfo is used to add comments to struct fields to
// say which version of Go introduced them.
func (p *Presentation) writeNode(w io.Writer, pageInfo *PageInfo, fset *token.FileSet, x interface{}) {
// convert trailing tabs into spaces using a tconv filter
// to ensure a good outcome in most browsers (there may still
// be tabs in comments and strings, but converting those into
// the right number of spaces is much harder)
//
// TODO(gri) rethink printer flags - perhaps tconv can be eliminated
// with an another printer mode (which is more efficiently
// implemented in the printer than here with another layer)
var pkgName, structName string
var apiInfo pkgAPIVersions
if gd, ok := x.(*ast.GenDecl); ok && pageInfo != nil && pageInfo.PDoc != nil &&
p.Corpus != nil &&
gd.Tok == token.TYPE && len(gd.Specs) != 0 {
pkgName = pageInfo.PDoc.ImportPath
if ts, ok := gd.Specs[0].(*ast.TypeSpec); ok {
if _, ok := ts.Type.(*ast.StructType); ok {
structName = ts.Name.Name
}
}
apiInfo = p.Corpus.pkgAPIInfo[pkgName]
}
var out = w
var buf bytes.Buffer
if structName != "" {
out = &buf
}
mode := printer.TabIndent | printer.UseSpaces
err := (&printer.Config{Mode: mode, Tabwidth: p.TabWidth}).Fprint(&tconv{p: p, output: out}, fset, x)
if err != nil {
log.Print(err)
}
// Add comments to struct fields saying which Go version introducd them.
if structName != "" {
fieldSince := apiInfo.fieldSince[structName]
typeSince := apiInfo.typeSince[structName]
// Add/rewrite comments on struct fields to note which Go version added them.
var buf2 bytes.Buffer
buf2.Grow(buf.Len() + len(" // Added in Go 1.n")*10)
bs := bufio.NewScanner(&buf)
for bs.Scan() {
line := bs.Bytes()
field := firstIdent(line)
var since string
if field != "" {
since = fieldSince[field]
if since != "" && since == typeSince {
// Don't highlight field versions if they were the
// same as the struct itself.
since = ""
}
}
if since == "" {
buf2.Write(line)
} else {
if bytes.Contains(line, slashSlash) {
line = bytes.TrimRight(line, " \t.")
buf2.Write(line)
buf2.WriteString("; added in Go ")
} else {
buf2.Write(line)
buf2.WriteString(" // Go ")
}
buf2.WriteString(since)
}
buf2.WriteByte('\n')
}
w.Write(buf2.Bytes())
}
}
var slashSlash = []byte("//")
// WriteNode writes x to w.
// TODO(bgarcia) Is this method needed? It's just a wrapper for p.writeNode.
func (p *Presentation) WriteNode(w io.Writer, fset *token.FileSet, x interface{}) {
p.writeNode(w, nil, fset, x)
}
// firstIdent returns the first identifier in x.
// This actually parses "identifiers" that begin with numbers too, but we
// never feed it such input, so it's fine.
func firstIdent(x []byte) string {
x = bytes.TrimSpace(x)
i := bytes.IndexFunc(x, func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsNumber(r) })
if i == -1 {
return string(x)
}
return string(x[:i])
}

Просмотреть файл

@ -0,0 +1,35 @@
// Copyright 2017 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.
// +build go1.7
package godoc
import (
"bytes"
"fmt"
"testing"
)
// Verify that scanIdentifier isn't quadratic.
// This doesn't actually measure and fail on its own, but it was previously
// very obvious when running by hand.
//
// TODO: if there's a reliable and non-flaky way to test this, do so.
// Maybe count user CPU time instead of wall time? But that's not easy
// to do portably in Go.
func TestStructField(t *testing.T) {
for _, n := range []int{10, 100, 1000, 10000} {
n := n
t.Run(fmt.Sprint(n), func(t *testing.T) {
var buf bytes.Buffer
fmt.Fprintf(&buf, "package foo\n\ntype T struct {\n")
for i := 0; i < n; i++ {
fmt.Fprintf(&buf, "\t// Field%d is foo.\n\tField%d int\n\n", i, i)
}
fmt.Fprintf(&buf, "}\n")
linkifySource(t, buf.Bytes())
})
}
}

Просмотреть файл

@ -0,0 +1,370 @@
// 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.
package godoc
import (
"bytes"
"go/parser"
"go/token"
"strings"
"testing"
)
func TestPkgLinkFunc(t *testing.T) {
for _, tc := range []struct {
path string
want string
}{
{"/src/fmt", "pkg/fmt"},
{"src/fmt", "pkg/fmt"},
{"/fmt", "pkg/fmt"},
{"fmt", "pkg/fmt"},
} {
if got := pkgLinkFunc(tc.path); got != tc.want {
t.Errorf("pkgLinkFunc(%v) = %v; want %v", tc.path, got, tc.want)
}
}
}
func TestSrcPosLinkFunc(t *testing.T) {
for _, tc := range []struct {
src string
line int
low int
high int
want string
}{
{"/src/fmt/print.go", 42, 30, 50, "/src/fmt/print.go?s=30:50#L32"},
{"/src/fmt/print.go", 2, 1, 5, "/src/fmt/print.go?s=1:5#L1"},
{"/src/fmt/print.go", 2, 0, 0, "/src/fmt/print.go#L2"},
{"/src/fmt/print.go", 0, 0, 0, "/src/fmt/print.go"},
{"/src/fmt/print.go", 0, 1, 5, "/src/fmt/print.go?s=1:5#L1"},
{"fmt/print.go", 0, 0, 0, "/src/fmt/print.go"},
{"fmt/print.go", 0, 1, 5, "/src/fmt/print.go?s=1:5#L1"},
} {
if got := srcPosLinkFunc(tc.src, tc.line, tc.low, tc.high); got != tc.want {
t.Errorf("srcLinkFunc(%v, %v, %v, %v) = %v; want %v", tc.src, tc.line, tc.low, tc.high, got, tc.want)
}
}
}
func TestSrcLinkFunc(t *testing.T) {
for _, tc := range []struct {
src string
want string
}{
{"/src/fmt/print.go", "/src/fmt/print.go"},
{"src/fmt/print.go", "/src/fmt/print.go"},
{"/fmt/print.go", "/src/fmt/print.go"},
{"fmt/print.go", "/src/fmt/print.go"},
} {
if got := srcLinkFunc(tc.src); got != tc.want {
t.Errorf("srcLinkFunc(%v) = %v; want %v", tc.src, got, tc.want)
}
}
}
func TestQueryLinkFunc(t *testing.T) {
for _, tc := range []struct {
src string
query string
line int
want string
}{
{"/src/fmt/print.go", "Sprintf", 33, "/src/fmt/print.go?h=Sprintf#L33"},
{"/src/fmt/print.go", "Sprintf", 0, "/src/fmt/print.go?h=Sprintf"},
{"src/fmt/print.go", "EOF", 33, "/src/fmt/print.go?h=EOF#L33"},
{"src/fmt/print.go", "a%3f+%26b", 1, "/src/fmt/print.go?h=a%3f+%26b#L1"},
} {
if got := queryLinkFunc(tc.src, tc.query, tc.line); got != tc.want {
t.Errorf("queryLinkFunc(%v, %v, %v) = %v; want %v", tc.src, tc.query, tc.line, got, tc.want)
}
}
}
func TestDocLinkFunc(t *testing.T) {
for _, tc := range []struct {
src string
ident string
want string
}{
{"fmt", "Sprintf", "/pkg/fmt/#Sprintf"},
{"fmt", "EOF", "/pkg/fmt/#EOF"},
} {
if got := docLinkFunc(tc.src, tc.ident); got != tc.want {
t.Errorf("docLinkFunc(%v, %v) = %v; want %v", tc.src, tc.ident, got, tc.want)
}
}
}
func TestSanitizeFunc(t *testing.T) {
for _, tc := range []struct {
src string
want string
}{
{},
{"foo", "foo"},
{"func f()", "func f()"},
{"func f(a int,)", "func f(a int)"},
{"func f(a int,\n)", "func f(a int)"},
{"func f(\n\ta int,\n\tb int,\n\tc int,\n)", "func f(a int, b int, c int)"},
{" ( a, b, c ) ", "(a, b, c)"},
{"( a, b, c int, foo bar , )", "(a, b, c int, foo bar)"},
{"{ a, b}", "{a, b}"},
{"[ a, b]", "[a, b]"},
} {
if got := sanitizeFunc(tc.src); got != tc.want {
t.Errorf("sanitizeFunc(%v) = %v; want %v", tc.src, got, tc.want)
}
}
}
// Test that we add <span id="StructName.FieldName"> elements
// to the HTML of struct fields.
func TestStructFieldsIDAttributes(t *testing.T) {
got := linkifySource(t, []byte(`
package foo
type T struct {
NoDoc string
// Doc has a comment.
Doc string
// Opt, if non-nil, is an option.
Opt *int
// Опция - другое поле.
Опция bool
}
`))
want := `type T struct {
<span id="T.NoDoc"></span>NoDoc <a href="/pkg/builtin/#string">string</a>
<span id="T.Doc"></span><span class="comment">// Doc has a comment.</span>
Doc <a href="/pkg/builtin/#string">string</a>
<span id="T.Opt"></span><span class="comment">// Opt, if non-nil, is an option.</span>
Opt *<a href="/pkg/builtin/#int">int</a>
<span id="T.Опция"></span><span class="comment">// Опция - другое поле.</span>
Опция <a href="/pkg/builtin/#bool">bool</a>
}`
if got != want {
t.Errorf("got: %s\n\nwant: %s\n", got, want)
}
}
// Test that we add <span id="ConstName"> elements to the HTML
// of definitions in const and var specs.
func TestValueSpecIDAttributes(t *testing.T) {
got := linkifySource(t, []byte(`
package foo
const (
NoDoc string = "NoDoc"
// Doc has a comment
Doc = "Doc"
NoVal
)`))
want := `const (
<span id="NoDoc">NoDoc</span> <a href="/pkg/builtin/#string">string</a> = &#34;NoDoc&#34;
<span class="comment">// Doc has a comment</span>
<span id="Doc">Doc</span> = &#34;Doc&#34;
<span id="NoVal">NoVal</span>
)`
if got != want {
t.Errorf("got: %s\n\nwant: %s\n", got, want)
}
}
func TestCompositeLitLinkFields(t *testing.T) {
got := linkifySource(t, []byte(`
package foo
type T struct {
X int
}
var S T = T{X: 12}`))
want := `type T struct {
<span id="T.X"></span>X <a href="/pkg/builtin/#int">int</a>
}
var <span id="S">S</span> <a href="#T">T</a> = <a href="#T">T</a>{<a href="#T.X">X</a>: 12}`
if got != want {
t.Errorf("got: %s\n\nwant: %s\n", got, want)
}
}
func TestFuncDeclNotLink(t *testing.T) {
// Function.
got := linkifySource(t, []byte(`
package http
func Get(url string) (resp *Response, err error)`))
want := `func Get(url <a href="/pkg/builtin/#string">string</a>) (resp *<a href="#Response">Response</a>, err <a href="/pkg/builtin/#error">error</a>)`
if got != want {
t.Errorf("got: %s\n\nwant: %s\n", got, want)
}
// Method.
got = linkifySource(t, []byte(`
package http
func (h Header) Get(key string) string`))
want = `func (h <a href="#Header">Header</a>) Get(key <a href="/pkg/builtin/#string">string</a>) <a href="/pkg/builtin/#string">string</a>`
if got != want {
t.Errorf("got: %s\n\nwant: %s\n", got, want)
}
}
func linkifySource(t *testing.T, src []byte) string {
p := &Presentation{
DeclLinks: true,
}
fset := token.NewFileSet()
af, err := parser.ParseFile(fset, "foo.go", src, parser.ParseComments)
if err != nil {
t.Fatal(err)
}
var buf bytes.Buffer
pi := &PageInfo{
FSet: fset,
}
sep := ""
for _, decl := range af.Decls {
buf.WriteString(sep)
sep = "\n"
buf.WriteString(p.node_htmlFunc(pi, decl, true))
}
return buf.String()
}
func TestScanIdentifier(t *testing.T) {
tests := []struct {
in, want string
}{
{"foo bar", "foo"},
{"foo/bar", "foo"},
{" foo", ""},
{"фоо", "фоо"},
{"f123", "f123"},
{"123f", ""},
}
for _, tt := range tests {
got := scanIdentifier([]byte(tt.in))
if string(got) != tt.want {
t.Errorf("scanIdentifier(%q) = %q; want %q", tt.in, got, tt.want)
}
}
}
func TestReplaceLeadingIndentation(t *testing.T) {
oldIndent := strings.Repeat(" ", 2)
newIndent := strings.Repeat(" ", 4)
tests := []struct {
src, want string
}{
{" foo\n bar\n baz", " foo\n bar\n baz"},
{" '`'\n '`'\n", " '`'\n '`'\n"},
{" '\\''\n '`'\n", " '\\''\n '`'\n"},
{" \"`\"\n \"`\"\n", " \"`\"\n \"`\"\n"},
{" `foo\n bar`", " `foo\n bar`"},
{" `foo\\`\n bar", " `foo\\`\n bar"},
{" '\\`'`foo\n bar", " '\\`'`foo\n bar"},
{
" if true {\n foo := `One\n \tTwo\nThree`\n }\n",
" if true {\n foo := `One\n \tTwo\n Three`\n }\n",
},
}
for _, tc := range tests {
if got := replaceLeadingIndentation(tc.src, oldIndent, newIndent); got != tc.want {
t.Errorf("replaceLeadingIndentation:\n%v\n---\nhave:\n%v\n---\nwant:\n%v\n",
tc.src, got, tc.want)
}
}
}
func TestSrcBreadcrumbFunc(t *testing.T) {
for _, tc := range []struct {
path string
want string
}{
{"src/", `<span class="text-muted">src/</span>`},
{"src/fmt/", `<a href="/src">src</a>/<span class="text-muted">fmt/</span>`},
{"src/fmt/print.go", `<a href="/src">src</a>/<a href="/src/fmt">fmt</a>/<span class="text-muted">print.go</span>`},
} {
if got := srcBreadcrumbFunc(tc.path); got != tc.want {
t.Errorf("srcBreadcrumbFunc(%v) = %v; want %v", tc.path, got, tc.want)
}
}
}
func TestSrcToPkgLinkFunc(t *testing.T) {
for _, tc := range []struct {
path string
want string
}{
{"src/", `<a href="/pkg">Index</a>`},
{"src/fmt/", `<a href="/pkg/fmt">fmt</a>`},
{"pkg/", `<a href="/pkg">Index</a>`},
{"pkg/LICENSE", `<a href="/pkg">Index</a>`},
} {
if got := srcToPkgLinkFunc(tc.path); got != tc.want {
t.Errorf("srcToPkgLinkFunc(%v) = %v; want %v", tc.path, got, tc.want)
}
}
}
func TestFilterOutBuildAnnotations(t *testing.T) {
// TODO: simplify this by using a multiline string once we stop
// using go vet from 1.10 on the build dashboard.
// https://golang.org/issue/26627
src := []byte("// +build !foo\n" +
"// +build !anothertag\n" +
"\n" +
"// non-tag comment\n" +
"\n" +
"package foo\n" +
"\n" +
"func bar() int {\n" +
" return 42\n" +
"}\n")
fset := token.NewFileSet()
af, err := parser.ParseFile(fset, "foo.go", src, parser.ParseComments)
if err != nil {
t.Fatal(err)
}
var found bool
for _, cg := range af.Comments {
if strings.HasPrefix(cg.Text(), "+build ") {
found = true
break
}
}
if !found {
t.Errorf("TestFilterOutBuildAnnotations is broken: missing build tag in test input")
}
found = false
for _, cg := range filterOutBuildAnnotations(af.Comments) {
if strings.HasPrefix(cg.Text(), "+build ") {
t.Errorf("filterOutBuildAnnotations failed to filter build tag")
}
if strings.Contains(cg.Text(), "non-tag comment") {
found = true
}
}
if !found {
t.Errorf("filterOutBuildAnnotations should not remove non-build tag comment")
}
}

1580
cmd/golangorg/godoc/index.go Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,323 @@
// 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.
package godoc
import (
"bytes"
"reflect"
"sort"
"strings"
"testing"
"golang.org/x/website/cmd/golangorg/godoc/vfs/mapfs"
)
func newCorpus(t *testing.T) *Corpus {
c := NewCorpus(mapfs.New(map[string]string{
"src/foo/foo.go": `// Package foo is an example.
package foo
import "bar"
const Pi = 3.1415
var Foos []Foo
// Foo is stuff.
type Foo struct{}
func New() *Foo {
return new(Foo)
}
`,
"src/bar/bar.go": `// Package bar is another example to test races.
package bar
`,
"src/other/bar/bar.go": `// Package bar is another bar package.
package bar
func X() {}
`,
"src/skip/skip.go": `// Package skip should be skipped.
package skip
func Skip() {}
`,
"src/bar/readme.txt": `Whitelisted text file.
`,
"src/bar/baz.zzz": `Text file not whitelisted.
`,
}))
c.IndexEnabled = true
c.IndexDirectory = func(dir string) bool {
return !strings.Contains(dir, "skip")
}
if err := c.Init(); err != nil {
t.Fatal(err)
}
return c
}
func TestIndex(t *testing.T) {
for _, docs := range []bool{true, false} {
for _, goCode := range []bool{true, false} {
for _, fullText := range []bool{true, false} {
c := newCorpus(t)
c.IndexDocs = docs
c.IndexGoCode = goCode
c.IndexFullText = fullText
c.UpdateIndex()
ix, _ := c.CurrentIndex()
if ix == nil {
t.Fatal("no index")
}
t.Logf("docs, goCode, fullText = %v,%v,%v", docs, goCode, fullText)
testIndex(t, c, ix)
}
}
}
}
func TestIndexWriteRead(t *testing.T) {
type key struct {
docs, goCode, fullText bool
}
type val struct {
buf *bytes.Buffer
c *Corpus
}
m := map[key]val{}
for _, docs := range []bool{true, false} {
for _, goCode := range []bool{true, false} {
for _, fullText := range []bool{true, false} {
k := key{docs, goCode, fullText}
c := newCorpus(t)
c.IndexDocs = docs
c.IndexGoCode = goCode
c.IndexFullText = fullText
c.UpdateIndex()
ix, _ := c.CurrentIndex()
if ix == nil {
t.Fatal("no index")
}
var buf bytes.Buffer
nw, err := ix.WriteTo(&buf)
if err != nil {
t.Fatalf("Index.WriteTo: %v", err)
}
m[k] = val{bytes.NewBuffer(buf.Bytes()), c}
ix2 := new(Index)
nr, err := ix2.ReadFrom(&buf)
if err != nil {
t.Fatalf("Index.ReadFrom: %v", err)
}
if nr != nw {
t.Errorf("Wrote %d bytes to index but read %d", nw, nr)
}
testIndex(t, c, ix)
}
}
}
// Test CompatibleWith
for k1, v1 := range m {
ix := new(Index)
if _, err := ix.ReadFrom(v1.buf); err != nil {
t.Fatalf("Index.ReadFrom: %v", err)
}
for k2, v2 := range m {
if got, want := ix.CompatibleWith(v2.c), k1 == k2; got != want {
t.Errorf("CompatibleWith = %v; want %v for %v, %v", got, want, k1, k2)
}
}
}
}
func testIndex(t *testing.T, c *Corpus, ix *Index) {
if _, ok := ix.words["Skip"]; ok {
t.Errorf("the word Skip was found; expected it to be skipped")
}
checkStats(t, c, ix)
checkImportCount(t, c, ix)
checkPackagePath(t, c, ix)
checkExports(t, c, ix)
checkIdents(t, c, ix)
}
// checkStats checks the Index's statistics.
// Some statistics are only set when we're indexing Go code.
func checkStats(t *testing.T, c *Corpus, ix *Index) {
want := Statistics{}
if c.IndexFullText {
want.Bytes = 314
want.Files = 4
want.Lines = 21
} else if c.IndexDocs || c.IndexGoCode {
want.Bytes = 291
want.Files = 3
want.Lines = 20
}
if c.IndexGoCode {
want.Words = 8
want.Spots = 12
}
if got := ix.Stats(); !reflect.DeepEqual(got, want) {
t.Errorf("Stats = %#v; want %#v", got, want)
}
}
// checkImportCount checks the Index's import count map.
// It is only set when we're indexing Go code.
func checkImportCount(t *testing.T, c *Corpus, ix *Index) {
want := map[string]int{}
if c.IndexGoCode {
want = map[string]int{
"bar": 1,
}
}
if got := ix.ImportCount(); !reflect.DeepEqual(got, want) {
t.Errorf("ImportCount = %v; want %v", got, want)
}
}
// checkPackagePath checks the Index's package path map.
// It is set if at least one of the indexing options is enabled.
func checkPackagePath(t *testing.T, c *Corpus, ix *Index) {
want := map[string]map[string]bool{}
if c.IndexDocs || c.IndexGoCode || c.IndexFullText {
want = map[string]map[string]bool{
"foo": {
"foo": true,
},
"bar": {
"bar": true,
"other/bar": true,
},
}
}
if got := ix.PackagePath(); !reflect.DeepEqual(got, want) {
t.Errorf("PackagePath = %v; want %v", got, want)
}
}
// checkExports checks the Index's exports map.
// It is only set when we're indexing Go code.
func checkExports(t *testing.T, c *Corpus, ix *Index) {
want := map[string]map[string]SpotKind{}
if c.IndexGoCode {
want = map[string]map[string]SpotKind{
"foo": {
"Pi": ConstDecl,
"Foos": VarDecl,
"Foo": TypeDecl,
"New": FuncDecl,
},
"other/bar": {
"X": FuncDecl,
},
}
}
if got := ix.Exports(); !reflect.DeepEqual(got, want) {
t.Errorf("Exports = %v; want %v", got, want)
}
}
// checkIdents checks the Index's indents map.
// It is only set when we're indexing documentation.
func checkIdents(t *testing.T, c *Corpus, ix *Index) {
want := map[SpotKind]map[string][]Ident{}
if c.IndexDocs {
want = map[SpotKind]map[string][]Ident{
PackageClause: {
"bar": {
{"bar", "bar", "bar", "Package bar is another example to test races."},
{"other/bar", "bar", "bar", "Package bar is another bar package."},
},
"foo": {{"foo", "foo", "foo", "Package foo is an example."}},
"other": {{"other/bar", "bar", "bar", "Package bar is another bar package."}},
},
ConstDecl: {
"Pi": {{"foo", "foo", "Pi", ""}},
},
VarDecl: {
"Foos": {{"foo", "foo", "Foos", ""}},
},
TypeDecl: {
"Foo": {{"foo", "foo", "Foo", "Foo is stuff."}},
},
FuncDecl: {
"New": {{"foo", "foo", "New", ""}},
"X": {{"other/bar", "bar", "X", ""}},
},
}
}
if got := ix.Idents(); !reflect.DeepEqual(got, want) {
t.Errorf("Idents = %v; want %v", got, want)
}
}
func TestIdentResultSort(t *testing.T) {
ic := map[string]int{
"/a/b/pkg1": 10,
"/a/b/pkg2": 2,
"/b/d/pkg3": 20,
}
for _, tc := range []struct {
ir []Ident
exp []Ident
}{
{
ir: []Ident{
{"/a/b/pkg2", "pkg2", "MyFunc2", ""},
{"/b/d/pkg3", "pkg3", "MyFunc3", ""},
{"/a/b/pkg1", "pkg1", "MyFunc1", ""},
},
exp: []Ident{
{"/b/d/pkg3", "pkg3", "MyFunc3", ""},
{"/a/b/pkg1", "pkg1", "MyFunc1", ""},
{"/a/b/pkg2", "pkg2", "MyFunc2", ""},
},
},
{
ir: []Ident{
{"/a/a/pkg1", "pkg1", "MyFunc1", ""},
{"/a/b/pkg1", "pkg1", "MyFunc1", ""},
},
exp: []Ident{
{"/a/b/pkg1", "pkg1", "MyFunc1", ""},
{"/a/a/pkg1", "pkg1", "MyFunc1", ""},
},
},
} {
if sort.Sort(byImportCount{tc.ir, ic}); !reflect.DeepEqual(tc.ir, tc.exp) {
t.Errorf("got: %v, want %v", tc.ir, tc.exp)
}
}
}
func TestIdentFilter(t *testing.T) {
ic := map[string]int{}
for _, tc := range []struct {
ir []Ident
pak string
exp []Ident
}{
{
ir: []Ident{
{"/a/b/pkg2", "pkg2", "MyFunc2", ""},
{"/b/d/pkg3", "pkg3", "MyFunc3", ""},
{"/a/b/pkg1", "pkg1", "MyFunc1", ""},
},
pak: "pkg2",
exp: []Ident{
{"/a/b/pkg2", "pkg2", "MyFunc2", ""},
},
},
} {
res := byImportCount{tc.ir, ic}.filter(tc.pak)
if !reflect.DeepEqual(res, tc.exp) {
t.Errorf("got: %v, want %v", res, tc.exp)
}
}
}

Просмотреть файл

@ -0,0 +1,195 @@
// 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"
)
// 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)
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{}
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 && doc.IsPredeclared(n.Name) {
l.path = builtinPkgPath
}
links = append(links, l)
}
}
return true
})
return
}

144
cmd/golangorg/godoc/meta.go Normal file
Просмотреть файл

@ -0,0 +1,144 @@
// Copyright 2009 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 godoc
import (
"bytes"
"encoding/json"
"log"
pathpkg "path"
"strings"
"time"
"golang.org/x/website/cmd/golangorg/godoc/vfs"
)
var (
doctype = []byte("<!DOCTYPE ")
jsonStart = []byte("<!--{")
jsonEnd = []byte("}-->")
)
// ----------------------------------------------------------------------------
// Documentation Metadata
// TODO(adg): why are some exported and some aren't? -brad
type Metadata struct {
Title string
Subtitle string
Template bool // execute as template
Path string // canonical path for this page
filePath string // filesystem path relative to goroot
}
func (m *Metadata) FilePath() string { return m.filePath }
// extractMetadata extracts the Metadata from a byte slice.
// It returns the Metadata value and the remaining data.
// If no metadata is present the original byte slice is returned.
//
func extractMetadata(b []byte) (meta Metadata, tail []byte, err error) {
tail = b
if !bytes.HasPrefix(b, jsonStart) {
return
}
end := bytes.Index(b, jsonEnd)
if end < 0 {
return
}
b = b[len(jsonStart)-1 : end+1] // drop leading <!-- and include trailing }
if err = json.Unmarshal(b, &meta); err != nil {
return
}
tail = tail[end+len(jsonEnd):]
return
}
// UpdateMetadata scans $GOROOT/doc for HTML files, reads their metadata,
// and updates the DocMetadata map.
func (c *Corpus) updateMetadata() {
metadata := make(map[string]*Metadata)
var scan func(string) // scan is recursive
scan = func(dir string) {
fis, err := c.fs.ReadDir(dir)
if err != nil {
log.Println("updateMetadata:", err)
return
}
for _, fi := range fis {
name := pathpkg.Join(dir, fi.Name())
if fi.IsDir() {
scan(name) // recurse
continue
}
if !strings.HasSuffix(name, ".html") {
continue
}
// Extract metadata from the file.
b, err := vfs.ReadFile(c.fs, name)
if err != nil {
log.Printf("updateMetadata %s: %v", name, err)
continue
}
meta, _, err := extractMetadata(b)
if err != nil {
log.Printf("updateMetadata: %s: %v", name, err)
continue
}
// Store relative filesystem path in Metadata.
meta.filePath = name
if meta.Path == "" {
// If no Path, canonical path is actual path.
meta.Path = meta.filePath
}
// Store under both paths.
metadata[meta.Path] = &meta
metadata[meta.filePath] = &meta
}
}
scan("/doc")
c.docMetadata.Set(metadata)
}
// MetadataFor returns the *Metadata for a given relative path or nil if none
// exists.
//
func (c *Corpus) MetadataFor(relpath string) *Metadata {
if m, _ := c.docMetadata.Get(); m != nil {
meta := m.(map[string]*Metadata)
// If metadata for this relpath exists, return it.
if p := meta[relpath]; p != nil {
return p
}
// Try with or without trailing slash.
if strings.HasSuffix(relpath, "/") {
relpath = relpath[:len(relpath)-1]
} else {
relpath = relpath + "/"
}
return meta[relpath]
}
return nil
}
// refreshMetadata sends a signal to update DocMetadata. If a refresh is in
// progress the metadata will be refreshed again afterward.
//
func (c *Corpus) refreshMetadata() {
select {
case c.refreshMetadataSignal <- true:
default:
}
}
// RefreshMetadataLoop runs forever, updating DocMetadata when the underlying
// file system changes. It should be launched in a goroutine.
func (c *Corpus) refreshMetadataLoop() {
for {
<-c.refreshMetadataSignal
c.updateMetadata()
time.Sleep(10 * time.Second) // at most once every 10 seconds
}
}

Просмотреть файл

@ -0,0 +1,80 @@
// Copyright 2009 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 godoc
import (
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"golang.org/x/website/cmd/golangorg/godoc/env"
)
// Page describes the contents of the top-level godoc webpage.
type Page struct {
Title string
Tabtitle string
Subtitle string
SrcPath string
Query string
Body []byte
GoogleCN bool // page is being served from golang.google.cn
TreeView bool // page needs to contain treeview related js and css
// filled in by ServePage
SearchBox bool
Playground bool
Version string
GoogleAnalytics string
}
func (p *Presentation) ServePage(w http.ResponseWriter, page Page) {
if page.Tabtitle == "" {
page.Tabtitle = page.Title
}
page.SearchBox = p.Corpus.IndexEnabled
page.Playground = p.ShowPlayground
page.Version = runtime.Version()
page.GoogleAnalytics = p.GoogleAnalytics
applyTemplateToResponseWriter(w, p.GodocHTML, page)
}
func (p *Presentation) ServeError(w http.ResponseWriter, r *http.Request, relpath string, err error) {
w.WriteHeader(http.StatusNotFound)
if perr, ok := err.(*os.PathError); ok {
rel, err := filepath.Rel(runtime.GOROOT(), perr.Path)
if err != nil {
perr.Path = "REDACTED"
} else {
perr.Path = filepath.Join("$GOROOT", rel)
}
}
p.ServePage(w, Page{
Title: "File " + relpath,
Subtitle: relpath,
Body: applyTemplate(p.ErrorHTML, "errorHTML", err),
GoogleCN: googleCN(r),
GoogleAnalytics: p.GoogleAnalytics,
})
}
func googleCN(r *http.Request) bool {
if r.FormValue("googlecn") != "" {
return true
}
if !env.IsProd() {
return false
}
if strings.HasSuffix(r.Host, ".cn") {
return true
}
switch r.Header.Get("X-AppEngine-Country") {
case "", "ZZ", "CN":
return true
}
return false
}

Просмотреть файл

@ -0,0 +1,74 @@
// Copyright 2011 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 contains support functions for parsing .go files
// accessed via godoc's file system fs.
package godoc
import (
"bytes"
"go/ast"
"go/parser"
"go/token"
pathpkg "path"
"golang.org/x/website/cmd/golangorg/godoc/vfs"
)
var linePrefix = []byte("//line ")
// This function replaces source lines starting with "//line " with a blank line.
// It does this irrespective of whether the line is truly a line comment or not;
// e.g., the line may be inside a string, or a /*-style comment; however that is
// rather unlikely (proper testing would require a full Go scan which we want to
// avoid for performance).
func replaceLinePrefixCommentsWithBlankLine(src []byte) {
for {
i := bytes.Index(src, linePrefix)
if i < 0 {
break // we're done
}
// 0 <= i && i+len(linePrefix) <= len(src)
if i == 0 || src[i-1] == '\n' {
// at beginning of line: blank out line
for i < len(src) && src[i] != '\n' {
src[i] = ' '
i++
}
} else {
// not at beginning of line: skip over prefix
i += len(linePrefix)
}
// i <= len(src)
src = src[i:]
}
}
func (c *Corpus) parseFile(fset *token.FileSet, filename string, mode parser.Mode) (*ast.File, error) {
src, err := vfs.ReadFile(c.fs, filename)
if err != nil {
return nil, err
}
// Temporary ad-hoc fix for issue 5247.
// TODO(gri) Remove this in favor of a better fix, eventually (see issue 7702).
replaceLinePrefixCommentsWithBlankLine(src)
return parser.ParseFile(fset, filename, src, mode)
}
func (c *Corpus) parseFiles(fset *token.FileSet, relpath string, abspath string, localnames []string) (map[string]*ast.File, error) {
files := make(map[string]*ast.File)
for _, f := range localnames {
absname := pathpkg.Join(abspath, f)
file, err := c.parseFile(fset, absname, parser.ParseComments)
if err != nil {
return nil, err
}
files[pathpkg.Join(relpath, f)] = file
}
return files, nil
}

169
cmd/golangorg/godoc/pres.go Normal file
Просмотреть файл

@ -0,0 +1,169 @@
// 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.
package godoc
import (
"net/http"
"regexp"
"sync"
"text/template"
"golang.org/x/website/cmd/golangorg/godoc/vfs/httpfs"
)
// SearchResultFunc functions return an HTML body for displaying search results.
type SearchResultFunc func(p *Presentation, result SearchResult) []byte
// Presentation generates output from a corpus.
type Presentation struct {
Corpus *Corpus
mux *http.ServeMux
fileServer http.Handler
cmdHandler handlerServer
pkgHandler handlerServer
CallGraphHTML,
DirlistHTML,
ErrorHTML,
ExampleHTML,
GodocHTML,
ImplementsHTML,
MethodSetHTML,
PackageHTML,
PackageRootHTML,
SearchHTML,
SearchDocHTML,
SearchCodeHTML,
SearchTxtHTML,
SearchDescXML *template.Template
// TabWidth optionally specifies the tab width.
TabWidth int
ShowTimestamps bool
ShowPlayground bool
DeclLinks bool
// SrcMode outputs source code instead of documentation in command-line mode.
SrcMode bool
// HTMLMode outputs HTML instead of plain text in command-line mode.
HTMLMode bool
// AllMode includes unexported identifiers in the output in command-line mode.
AllMode bool
// NotesRx optionally specifies a regexp to match
// notes to render in the output.
NotesRx *regexp.Regexp
// AdjustPageInfoMode optionally specifies a function to
// modify the PageInfoMode of a request. The default chosen
// value is provided.
AdjustPageInfoMode func(req *http.Request, mode PageInfoMode) PageInfoMode
// URLForSrc optionally specifies a function that takes a source file and
// returns a URL for it.
// The source file argument has the form /src/<path>/<filename>.
URLForSrc func(src string) string
// URLForSrcPos optionally specifies a function to create a URL given a
// source file, a line from the source file (1-based), and low & high offset
// positions (0-based, bytes from beginning of file). Ideally, the returned
// URL will be for the specified line of the file, while the high & low
// positions will be used to highlight a section of the file.
// The source file argument has the form /src/<path>/<filename>.
URLForSrcPos func(src string, line, low, high int) string
// URLForSrcQuery optionally specifies a function to create a URL given a
// source file, a query string, and a line from the source file (1-based).
// The source file argument has the form /src/<path>/<filename>.
// The query argument will be escaped for the purposes of embedding in a URL
// query parameter.
// Ideally, the returned URL will be for the specified line of the file with
// the query string highlighted.
URLForSrcQuery func(src, query string, line int) string
// SearchResults optionally specifies a list of functions returning an HTML
// body for displaying search results.
SearchResults []SearchResultFunc
// GoogleAnalytics optionally adds Google Analytics via the provided
// tracking ID to each page.
GoogleAnalytics string
initFuncMapOnce sync.Once
funcMap template.FuncMap
templateFuncs template.FuncMap
}
// NewPresentation returns a new Presentation from a corpus.
// It sets SearchResults to:
// [SearchResultDoc SearchResultCode SearchResultTxt].
func NewPresentation(c *Corpus) *Presentation {
if c == nil {
panic("nil Corpus")
}
p := &Presentation{
Corpus: c,
mux: http.NewServeMux(),
fileServer: http.FileServer(httpfs.New(c.fs)),
TabWidth: 4,
DeclLinks: true,
SearchResults: []SearchResultFunc{
(*Presentation).SearchResultDoc,
(*Presentation).SearchResultCode,
(*Presentation).SearchResultTxt,
},
}
p.cmdHandler = handlerServer{
p: p,
c: c,
pattern: "/cmd/",
fsRoot: "/src",
}
p.pkgHandler = handlerServer{
p: p,
c: c,
pattern: "/pkg/",
stripPrefix: "pkg/",
fsRoot: "/src",
exclude: []string{"/src/cmd"},
}
p.cmdHandler.registerWithMux(p.mux)
p.pkgHandler.registerWithMux(p.mux)
p.mux.HandleFunc("/", p.ServeFile)
p.mux.HandleFunc("/search", p.HandleSearch)
p.mux.HandleFunc("/opensearch.xml", p.serveSearchDesc)
return p
}
func (p *Presentation) FileServer() http.Handler {
return p.fileServer
}
func (p *Presentation) ServeHTTP(w http.ResponseWriter, r *http.Request) {
p.mux.ServeHTTP(w, r)
}
func (p *Presentation) PkgFSRoot() string {
return p.pkgHandler.fsRoot
}
func (p *Presentation) CmdFSRoot() string {
return p.cmdHandler.fsRoot
}
// TODO(bradfitz): move this to be a method on Corpus. Just moving code around for now,
// but this doesn't feel right.
func (p *Presentation) GetPkgPageInfo(abspath, relpath string, mode PageInfoMode) *PageInfo {
return p.pkgHandler.GetPageInfo(abspath, relpath, mode, "", "")
}
// TODO(bradfitz): move this to be a method on Corpus. Just moving code around for now,
// but this doesn't feel right.
func (p *Presentation) GetCmdPageInfo(abspath, relpath string, mode PageInfoMode) *PageInfo {
return p.cmdHandler.GetPageInfo(abspath, relpath, mode, "", "")
}

Просмотреть файл

@ -0,0 +1,170 @@
// Copyright 2015 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 proxy proxies requests to the playground's compile and share handlers.
// It is designed to run only on the instance of godoc that serves golang.org.
package proxy
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"strings"
"time"
"golang.org/x/website/cmd/golangorg/godoc/env"
)
const playgroundURL = "https://play.golang.org"
type Request struct {
Body string
}
type Response struct {
Errors string
Events []Event
}
type Event struct {
Message string
Kind string // "stdout" or "stderr"
Delay time.Duration // time to wait before printing Message
}
const expires = 7 * 24 * time.Hour // 1 week
var cacheControlHeader = fmt.Sprintf("public, max-age=%d", int(expires.Seconds()))
func RegisterHandlers(mux *http.ServeMux) {
mux.HandleFunc("/compile", compile)
mux.HandleFunc("/share", share)
}
func compile(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "I only answer to POST requests.", http.StatusMethodNotAllowed)
return
}
ctx := r.Context()
body := r.FormValue("body")
res := &Response{}
req := &Request{Body: body}
if err := makeCompileRequest(ctx, req, res); err != nil {
log.Printf("ERROR compile error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
var out interface{}
switch r.FormValue("version") {
case "2":
out = res
default: // "1"
out = struct {
CompileErrors string `json:"compile_errors"`
Output string `json:"output"`
}{res.Errors, flatten(res.Events)}
}
b, err := json.Marshal(out)
if err != nil {
log.Printf("ERROR encoding response: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
expiresTime := time.Now().Add(expires).UTC()
w.Header().Set("Expires", expiresTime.Format(time.RFC1123))
w.Header().Set("Cache-Control", cacheControlHeader)
w.Write(b)
}
// makePlaygroundRequest sends the given Request to the playground compile
// endpoint and stores the response in the given Response.
func makeCompileRequest(ctx context.Context, req *Request, res *Response) error {
reqJ, err := json.Marshal(req)
if err != nil {
return fmt.Errorf("marshalling request: %v", err)
}
hReq, _ := http.NewRequest("POST", playgroundURL+"/compile", bytes.NewReader(reqJ))
hReq.Header.Set("Content-Type", "application/json")
hReq = hReq.WithContext(ctx)
r, err := http.DefaultClient.Do(hReq)
if err != nil {
return fmt.Errorf("making request: %v", err)
}
defer r.Body.Close()
if r.StatusCode != http.StatusOK {
b, _ := ioutil.ReadAll(r.Body)
return fmt.Errorf("bad status: %v body:\n%s", r.Status, b)
}
if err := json.NewDecoder(r.Body).Decode(res); err != nil {
return fmt.Errorf("unmarshalling response: %v", err)
}
return nil
}
// flatten takes a sequence of Events and returns their contents, concatenated.
func flatten(seq []Event) string {
var buf bytes.Buffer
for _, e := range seq {
buf.WriteString(e.Message)
}
return buf.String()
}
func share(w http.ResponseWriter, r *http.Request) {
if googleCN(r) {
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
// HACK(cbro): use a simple proxy rather than httputil.ReverseProxy because of Issue #28168.
// TODO: investigate using ReverseProxy with a Director, unsetting whatever's necessary to make that work.
req, _ := http.NewRequest("POST", playgroundURL+"/share", r.Body)
req.Header.Set("Content-Type", r.Header.Get("Content-Type"))
req = req.WithContext(r.Context())
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Printf("ERROR share error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
copyHeader := func(k string) {
if v := resp.Header.Get(k); v != "" {
w.Header().Set(k, v)
}
}
copyHeader("Content-Type")
copyHeader("Content-Length")
defer resp.Body.Close()
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
}
func googleCN(r *http.Request) bool {
if r.FormValue("googlecn") != "" {
return true
}
if !env.IsProd() {
return false
}
if strings.HasSuffix(r.Host, ".cn") {
return true
}
switch r.Header.Get("X-AppEngine-Country") {
case "", "ZZ", "CN":
return true
}
return false
}

Просмотреть файл

@ -0,0 +1,138 @@
// 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.
// This file provides a compact encoding of
// a map of Mercurial hashes to Git hashes.
package redirect
import (
"encoding/binary"
"fmt"
"io"
"os"
"sort"
"strconv"
"strings"
)
// hashMap is a map of Mercurial hashes to Git hashes.
type hashMap struct {
file *os.File
entries int
}
// newHashMap takes a file handle that contains a map of Mercurial to Git
// hashes. The file should be a sequence of pairs of little-endian encoded
// uint32s, representing a hgHash and a gitHash respectively.
// The sequence must be sorted by hgHash.
// The file must remain open for as long as the returned hashMap is used.
func newHashMap(f *os.File) (*hashMap, error) {
fi, err := f.Stat()
if err != nil {
return nil, err
}
return &hashMap{file: f, entries: int(fi.Size() / 8)}, nil
}
// Lookup finds an hgHash in the map that matches the given prefix, and returns
// its corresponding gitHash. The prefix must be at least 8 characters long.
func (m *hashMap) Lookup(s string) gitHash {
if m == nil {
return 0
}
hg, err := hgHashFromString(s)
if err != nil {
return 0
}
var git gitHash
b := make([]byte, 8)
sort.Search(m.entries, func(i int) bool {
n, err := m.file.ReadAt(b, int64(i*8))
if err != nil {
panic(err)
}
if n != 8 {
panic(io.ErrUnexpectedEOF)
}
v := hgHash(binary.LittleEndian.Uint32(b[:4]))
if v == hg {
git = gitHash(binary.LittleEndian.Uint32(b[4:]))
}
return v >= hg
})
return git
}
// hgHash represents the lower (leftmost) 32 bits of a Mercurial hash.
type hgHash uint32
func (h hgHash) String() string {
return intToHash(int64(h))
}
func hgHashFromString(s string) (hgHash, error) {
if len(s) < 8 {
return 0, fmt.Errorf("string too small: len(s) = %d", len(s))
}
hash := s[:8]
i, err := strconv.ParseInt(hash, 16, 64)
if err != nil {
return 0, err
}
return hgHash(i), nil
}
// gitHash represents the leftmost 28 bits of a Git hash in its upper 28 bits,
// and it encodes hash's repository in the lower 4 bits.
type gitHash uint32
func (h gitHash) Hash() string {
return intToHash(int64(h))[:7]
}
func (h gitHash) Repo() string {
return repo(h & 0xF).String()
}
func intToHash(i int64) string {
s := strconv.FormatInt(i, 16)
if len(s) < 8 {
s = strings.Repeat("0", 8-len(s)) + s
}
return s
}
// repo represents a Go Git repository.
type repo byte
const (
repoGo repo = iota
repoBlog
repoCrypto
repoExp
repoImage
repoMobile
repoNet
repoSys
repoTalks
repoText
repoTools
)
func (r repo) String() string {
return map[repo]string{
repoGo: "go",
repoBlog: "blog",
repoCrypto: "crypto",
repoExp: "exp",
repoImage: "image",
repoMobile: "mobile",
repoNet: "net",
repoSys: "sys",
repoTalks: "talks",
repoText: "text",
repoTools: "tools",
}[r]
}

Просмотреть файл

@ -0,0 +1,254 @@
// 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.
// Package redirect provides hooks to register HTTP handlers that redirect old
// godoc paths to their new equivalents and assist in accessing the issue
// tracker, wiki, code review system, etc.
package redirect // import "golang.org/x/website/cmd/golangorg/godoc/redirect"
import (
"fmt"
"net/http"
"os"
"regexp"
"strconv"
"strings"
)
// Register registers HTTP handlers that redirect old godoc paths to their new
// equivalents and assist in accessing the issue tracker, wiki, code review
// system, etc. If mux is nil it uses http.DefaultServeMux.
func Register(mux *http.ServeMux) {
if mux == nil {
mux = http.DefaultServeMux
}
handlePathRedirects(mux, pkgRedirects, "/pkg/")
handlePathRedirects(mux, cmdRedirects, "/cmd/")
for prefix, redirect := range prefixHelpers {
p := "/" + prefix + "/"
mux.Handle(p, PrefixHandler(p, redirect))
}
for path, redirect := range redirects {
mux.Handle(path, Handler(redirect))
}
// NB: /src/pkg (sans trailing slash) is the index of packages.
mux.HandleFunc("/src/pkg/", srcPkgHandler)
mux.HandleFunc("/cl/", clHandler)
mux.HandleFunc("/change/", changeHandler)
mux.HandleFunc("/design/", designHandler)
}
func handlePathRedirects(mux *http.ServeMux, redirects map[string]string, prefix string) {
for source, target := range redirects {
h := Handler(prefix + target + "/")
p := prefix + source
mux.Handle(p, h)
mux.Handle(p+"/", h)
}
}
// Packages that were renamed between r60 and go1.
var pkgRedirects = map[string]string{
"asn1": "encoding/asn1",
"big": "math/big",
"cmath": "math/cmplx",
"csv": "encoding/csv",
"exec": "os/exec",
"exp/template/html": "html/template",
"gob": "encoding/gob",
"http": "net/http",
"http/cgi": "net/http/cgi",
"http/fcgi": "net/http/fcgi",
"http/httptest": "net/http/httptest",
"http/pprof": "net/http/pprof",
"json": "encoding/json",
"mail": "net/mail",
"rand": "math/rand",
"rpc": "net/rpc",
"rpc/jsonrpc": "net/rpc/jsonrpc",
"scanner": "text/scanner",
"smtp": "net/smtp",
"tabwriter": "text/tabwriter",
"template": "text/template",
"template/parse": "text/template/parse",
"url": "net/url",
"utf16": "unicode/utf16",
"utf8": "unicode/utf8",
"xml": "encoding/xml",
}
// Commands that were renamed between r60 and go1.
var cmdRedirects = map[string]string{
"gofix": "fix",
"goinstall": "go",
"gopack": "pack",
"gotest": "go",
"govet": "vet",
"goyacc": "yacc",
}
var redirects = map[string]string{
"/blog": "/blog/",
"/build": "http://build.golang.org",
"/change": "https://go.googlesource.com/go",
"/cl": "https://go-review.googlesource.com",
"/cmd/godoc/": "http://godoc.org/golang.org/x/website/cmd/golangorg/",
"/issue": "https://github.com/golang/go/issues",
"/issue/new": "https://github.com/golang/go/issues/new",
"/issues": "https://github.com/golang/go/issues",
"/issues/new": "https://github.com/golang/go/issues/new",
"/play": "http://play.golang.org",
"/design": "https://go.googlesource.com/proposal/+/master/design",
// In Go 1.2 the references page is part of /doc/.
"/ref": "/doc/#references",
// This next rule clobbers /ref/spec and /ref/mem.
// TODO(adg): figure out what to do here, if anything.
// "/ref/": "/doc/#references",
// Be nice to people who are looking in the wrong place.
"/doc/mem": "/ref/mem",
"/doc/spec": "/ref/spec",
"/talks": "http://talks.golang.org",
"/tour": "http://tour.golang.org",
"/wiki": "https://github.com/golang/go/wiki",
"/doc/articles/c_go_cgo.html": "/blog/c-go-cgo",
"/doc/articles/concurrency_patterns.html": "/blog/go-concurrency-patterns-timing-out-and",
"/doc/articles/defer_panic_recover.html": "/blog/defer-panic-and-recover",
"/doc/articles/error_handling.html": "/blog/error-handling-and-go",
"/doc/articles/gobs_of_data.html": "/blog/gobs-of-data",
"/doc/articles/godoc_documenting_go_code.html": "/blog/godoc-documenting-go-code",
"/doc/articles/gos_declaration_syntax.html": "/blog/gos-declaration-syntax",
"/doc/articles/image_draw.html": "/blog/go-imagedraw-package",
"/doc/articles/image_package.html": "/blog/go-image-package",
"/doc/articles/json_and_go.html": "/blog/json-and-go",
"/doc/articles/json_rpc_tale_of_interfaces.html": "/blog/json-rpc-tale-of-interfaces",
"/doc/articles/laws_of_reflection.html": "/blog/laws-of-reflection",
"/doc/articles/slices_usage_and_internals.html": "/blog/go-slices-usage-and-internals",
"/doc/go_for_cpp_programmers.html": "/wiki/GoForCPPProgrammers",
"/doc/go_tutorial.html": "http://tour.golang.org/",
}
var prefixHelpers = map[string]string{
"issue": "https://github.com/golang/go/issues/",
"issues": "https://github.com/golang/go/issues/",
"play": "http://play.golang.org/",
"talks": "http://talks.golang.org/",
"wiki": "https://github.com/golang/go/wiki/",
}
func Handler(target string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
url := target
if qs := r.URL.RawQuery; qs != "" {
url += "?" + qs
}
http.Redirect(w, r, url, http.StatusMovedPermanently)
})
}
var validId = regexp.MustCompile(`^[A-Za-z0-9-]*/?$`)
func PrefixHandler(prefix, baseURL string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if p := r.URL.Path; p == prefix {
// redirect /prefix/ to /prefix
http.Redirect(w, r, p[:len(p)-1], http.StatusFound)
return
}
id := r.URL.Path[len(prefix):]
if !validId.MatchString(id) {
http.Error(w, "Not found", http.StatusNotFound)
return
}
target := baseURL + id
http.Redirect(w, r, target, http.StatusFound)
})
}
// Redirect requests from the old "/src/pkg/foo" to the new "/src/foo".
// See http://golang.org/s/go14nopkg
func srcPkgHandler(w http.ResponseWriter, r *http.Request) {
r.URL.Path = "/src/" + r.URL.Path[len("/src/pkg/"):]
http.Redirect(w, r, r.URL.String(), http.StatusMovedPermanently)
}
func clHandler(w http.ResponseWriter, r *http.Request) {
const prefix = "/cl/"
if p := r.URL.Path; p == prefix {
// redirect /prefix/ to /prefix
http.Redirect(w, r, p[:len(p)-1], http.StatusFound)
return
}
id := r.URL.Path[len(prefix):]
// support /cl/152700045/, which is used in commit 0edafefc36.
id = strings.TrimSuffix(id, "/")
if !validId.MatchString(id) {
http.Error(w, "Not found", http.StatusNotFound)
return
}
target := ""
if n, err := strconv.Atoi(id); err == nil && isRietveldCL(n) {
// TODO: Issue 28836: if this Rietveld CL happens to
// also be a Gerrit CL, render a disambiguation HTML
// page with two links instead. We'll need to make an
// RPC (to maintner?) to figure that out. For now just
// redirect to rietveld.
target = "https://codereview.appspot.com/" + id
} else {
target = "https://go-review.googlesource.com/" + id
}
http.Redirect(w, r, target, http.StatusFound)
}
var changeMap *hashMap
// LoadChangeMap loads the specified map of Mercurial to Git revisions,
// which is used by the /change/ handler to intelligently map old hg
// revisions to their new git equivalents.
// It should be called before calling Register.
// The file should remain open as long as the process is running.
// See the implementation of this package for details.
func LoadChangeMap(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
m, err := newHashMap(f)
if err != nil {
return err
}
changeMap = m
return nil
}
func changeHandler(w http.ResponseWriter, r *http.Request) {
const prefix = "/change/"
if p := r.URL.Path; p == prefix {
// redirect /prefix/ to /prefix
http.Redirect(w, r, p[:len(p)-1], http.StatusFound)
return
}
hash := r.URL.Path[len(prefix):]
target := "https://go.googlesource.com/go/+/" + hash
if git := changeMap.Lookup(hash); git > 0 {
target = fmt.Sprintf("https://go.googlesource.com/%v/+/%v", git.Repo(), git.Hash())
}
http.Redirect(w, r, target, http.StatusFound)
}
func designHandler(w http.ResponseWriter, r *http.Request) {
const prefix = "/design/"
if p := r.URL.Path; p == prefix {
// redirect /prefix/ to /prefix
http.Redirect(w, r, p[:len(p)-1], http.StatusFound)
return
}
name := r.URL.Path[len(prefix):]
target := "https://go.googlesource.com/proposal/+/master/design/" + name + ".md"
http.Redirect(w, r, target, http.StatusFound)
}

Просмотреть файл

@ -0,0 +1,113 @@
// Copyright 2015 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 redirect
import (
"net/http"
"net/http/httptest"
"testing"
)
type redirectResult struct {
status int
path string
}
func errorResult(status int) redirectResult {
return redirectResult{status, ""}
}
func TestRedirects(t *testing.T) {
var tests = map[string]redirectResult{
"/build": {301, "http://build.golang.org"},
"/ref": {301, "/doc/#references"},
"/doc/mem": {301, "/ref/mem"},
"/doc/spec": {301, "/ref/spec"},
"/tour": {301, "http://tour.golang.org"},
"/foo": errorResult(404),
"/pkg/asn1": {301, "/pkg/encoding/asn1/"},
"/pkg/template/parse": {301, "/pkg/text/template/parse/"},
"/src/pkg/foo": {301, "/src/foo"},
"/cmd/gofix": {301, "/cmd/fix/"},
// git commits (/change)
// TODO: mercurial tags and LoadChangeMap.
"/change": {301, "https://go.googlesource.com/go"},
"/change/a": {302, "https://go.googlesource.com/go/+/a"},
"/issue": {301, "https://github.com/golang/go/issues"},
"/issue?": {301, "https://github.com/golang/go/issues"},
"/issue/1": {302, "https://github.com/golang/go/issues/1"},
"/issue/new": {301, "https://github.com/golang/go/issues/new"},
"/issue/new?a=b&c=d%20&e=f": {301, "https://github.com/golang/go/issues/new?a=b&c=d%20&e=f"},
"/issues": {301, "https://github.com/golang/go/issues"},
"/issues/1": {302, "https://github.com/golang/go/issues/1"},
"/issues/new": {301, "https://github.com/golang/go/issues/new"},
"/issues/1/2/3": errorResult(404),
"/wiki/foo": {302, "https://github.com/golang/go/wiki/foo"},
"/wiki/foo/": {302, "https://github.com/golang/go/wiki/foo/"},
"/design": {301, "https://go.googlesource.com/proposal/+/master/design"},
"/design/": {302, "/design"},
"/design/123-foo": {302, "https://go.googlesource.com/proposal/+/master/design/123-foo.md"},
"/design/text/123-foo": {302, "https://go.googlesource.com/proposal/+/master/design/text/123-foo.md"},
"/cl/1": {302, "https://go-review.googlesource.com/1"},
"/cl/1/": {302, "https://go-review.googlesource.com/1"},
"/cl/267120043": {302, "https://codereview.appspot.com/267120043"},
"/cl/267120043/": {302, "https://codereview.appspot.com/267120043"},
// Verify that we're using the Rietveld CL table:
"/cl/152046": {302, "https://codereview.appspot.com/152046"},
"/cl/152047": {302, "https://go-review.googlesource.com/152047"},
"/cl/152048": {302, "https://codereview.appspot.com/152048"},
// And verify we're using the the "bigEnoughAssumeRietveld" value:
"/cl/299999": {302, "https://go-review.googlesource.com/299999"},
"/cl/300000": {302, "https://codereview.appspot.com/300000"},
}
mux := http.NewServeMux()
Register(mux)
ts := httptest.NewServer(mux)
defer ts.Close()
for path, want := range tests {
if want.path != "" && want.path[0] == '/' {
// All redirects are absolute.
want.path = ts.URL + want.path
}
req, err := http.NewRequest("GET", ts.URL+path, nil)
if err != nil {
t.Errorf("(path: %q) unexpected error: %v", path, err)
continue
}
resp, err := http.DefaultTransport.RoundTrip(req)
if err != nil {
t.Errorf("(path: %q) unexpected error: %v", path, err)
continue
}
if resp.StatusCode != want.status {
t.Errorf("(path: %q) got status %d, want %d", path, resp.StatusCode, want.status)
}
if want.status != 301 && want.status != 302 {
// Not a redirect. Just check status.
continue
}
out, _ := resp.Location()
if got := out.String(); got != want.path {
t.Errorf("(path: %q) got %s, want %s", path, got, want.path)
}
}
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,186 @@
// Copyright 2009 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 godoc
import (
"bytes"
"fmt"
"net/http"
"regexp"
"strings"
)
type SearchResult struct {
Query string
Alert string // error or warning message
// identifier matches
Pak HitList // packages matching Query
Hit *LookupResult // identifier matches of Query
Alt *AltWords // alternative identifiers to look for
// textual matches
Found int // number of textual occurrences found
Textual []FileLines // textual matches of Query
Complete bool // true if all textual occurrences of Query are reported
Idents map[SpotKind][]Ident
}
func (c *Corpus) Lookup(query string) SearchResult {
result := &SearchResult{Query: query}
index, timestamp := c.CurrentIndex()
if index != nil {
// identifier search
if r, err := index.Lookup(query); err == nil {
result = r
} else if err != nil && !c.IndexFullText {
// ignore the error if full text search is enabled
// since the query may be a valid regular expression
result.Alert = "Error in query string: " + err.Error()
return *result
}
// full text search
if c.IndexFullText && query != "" {
rx, err := regexp.Compile(query)
if err != nil {
result.Alert = "Error in query regular expression: " + err.Error()
return *result
}
// If we get maxResults+1 results we know that there are more than
// maxResults results and thus the result may be incomplete (to be
// precise, we should remove one result from the result set, but
// nobody is going to count the results on the result page).
result.Found, result.Textual = index.LookupRegexp(rx, c.MaxResults+1)
result.Complete = result.Found <= c.MaxResults
if !result.Complete {
result.Found-- // since we looked for maxResults+1
}
}
}
// is the result accurate?
if c.IndexEnabled {
if ts := c.FSModifiedTime(); timestamp.Before(ts) {
// The index is older than the latest file system change under godoc's observation.
result.Alert = "Indexing in progress: result may be inaccurate"
}
} else {
result.Alert = "Search index disabled: no results available"
}
return *result
}
// SearchResultDoc optionally specifies a function returning an HTML body
// displaying search results matching godoc documentation.
func (p *Presentation) SearchResultDoc(result SearchResult) []byte {
return applyTemplate(p.SearchDocHTML, "searchDocHTML", result)
}
// SearchResultCode optionally specifies a function returning an HTML body
// displaying search results matching source code.
func (p *Presentation) SearchResultCode(result SearchResult) []byte {
return applyTemplate(p.SearchCodeHTML, "searchCodeHTML", result)
}
// SearchResultTxt optionally specifies a function returning an HTML body
// displaying search results of textual matches.
func (p *Presentation) SearchResultTxt(result SearchResult) []byte {
return applyTemplate(p.SearchTxtHTML, "searchTxtHTML", result)
}
// HandleSearch obtains results for the requested search and returns a page
// to display them.
func (p *Presentation) HandleSearch(w http.ResponseWriter, r *http.Request) {
query := strings.TrimSpace(r.FormValue("q"))
result := p.Corpus.Lookup(query)
var contents bytes.Buffer
for _, f := range p.SearchResults {
contents.Write(f(p, result))
}
var title string
if haveResults := contents.Len() > 0; haveResults {
title = fmt.Sprintf(`Results for query: %v`, query)
if !p.Corpus.IndexEnabled {
result.Alert = ""
}
} else {
title = fmt.Sprintf(`No results found for query %q`, query)
}
body := bytes.NewBuffer(applyTemplate(p.SearchHTML, "searchHTML", result))
body.Write(contents.Bytes())
p.ServePage(w, Page{
Title: title,
Tabtitle: query,
Query: query,
Body: body.Bytes(),
GoogleCN: googleCN(r),
})
}
func (p *Presentation) serveSearchDesc(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/opensearchdescription+xml")
data := map[string]interface{}{
"BaseURL": fmt.Sprintf("http://%s", r.Host),
}
applyTemplateToResponseWriter(w, p.SearchDescXML, &data)
}
// tocColCount returns the no. of columns
// to split the toc table to.
func tocColCount(result SearchResult) int {
tocLen := tocLen(result)
colCount := 0
// Simple heuristic based on visual aesthetic in manual testing.
switch {
case tocLen <= 10:
colCount = 1
case tocLen <= 20:
colCount = 2
case tocLen <= 80:
colCount = 3
default:
colCount = 4
}
return colCount
}
// tocLen calculates the no. of items in the toc table
// by going through various fields in the SearchResult
// that is rendered in the UI.
func tocLen(result SearchResult) int {
tocLen := 0
for _, val := range result.Idents {
if len(val) != 0 {
tocLen++
}
}
// If no identifiers, then just one item for the header text "Package <result.Query>".
// See searchcode.html for further details.
if len(result.Idents) == 0 {
tocLen++
}
if result.Hit != nil {
if len(result.Hit.Decls) > 0 {
tocLen += len(result.Hit.Decls)
// We need one extra item for the header text "Package-level declarations".
tocLen++
}
if len(result.Hit.Others) > 0 {
tocLen += len(result.Hit.Others)
// We need one extra item for the header text "Local declarations and uses".
tocLen++
}
}
// For "textual occurrences".
tocLen++
return tocLen
}

Просмотреть файл

@ -0,0 +1,836 @@
// 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.
package godoc
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"go/ast"
"go/build"
"go/doc"
"go/token"
htmlpkg "html"
htmltemplate "html/template"
"io"
"io/ioutil"
"log"
"net/http"
"os"
pathpkg "path"
"path/filepath"
"sort"
"strings"
"text/template"
"time"
"golang.org/x/website/cmd/golangorg/godoc/analysis"
"golang.org/x/website/cmd/golangorg/godoc/util"
"golang.org/x/website/cmd/golangorg/godoc/vfs"
)
// handlerServer is a migration from an old godoc http Handler type.
// This should probably merge into something else.
type handlerServer struct {
p *Presentation
c *Corpus // copy of p.Corpus
pattern string // url pattern; e.g. "/pkg/"
stripPrefix string // prefix to strip from import path; e.g. "pkg/"
fsRoot string // file system root to which the pattern is mapped; e.g. "/src"
exclude []string // file system paths to exclude; e.g. "/src/cmd"
}
func (s *handlerServer) registerWithMux(mux *http.ServeMux) {
mux.Handle(s.pattern, s)
}
// 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)
// is extracted from the AST. If there is no corresponding package in the
// directory, PageInfo.PAst and PageInfo.PDoc are nil. If there are no sub-
// directories, PageInfo.Dirs is nil. If an error occurred, PageInfo.Err is
// set to the respective error but the error is not logged.
//
func (h *handlerServer) GetPageInfo(abspath, relpath string, mode PageInfoMode, goos, goarch string) *PageInfo {
info := &PageInfo{Dirname: abspath, Mode: mode}
// Restrict to the package files that would be used when building
// the package on this system. This makes sure that if there are
// separate implementations for, say, Windows vs Unix, we don't
// jumble them all together.
// Note: If goos/goarch aren't set, the current binary's GOOS/GOARCH
// are used.
ctxt := build.Default
ctxt.IsAbsPath = pathpkg.IsAbs
ctxt.IsDir = func(path string) bool {
fi, err := h.c.fs.Stat(filepath.ToSlash(path))
return err == nil && fi.IsDir()
}
ctxt.ReadDir = func(dir string) ([]os.FileInfo, error) {
f, err := h.c.fs.ReadDir(filepath.ToSlash(dir))
filtered := make([]os.FileInfo, 0, len(f))
for _, i := range f {
if mode&NoFiltering != 0 || i.Name() != "internal" {
filtered = append(filtered, i)
}
}
return filtered, err
}
ctxt.OpenFile = func(name string) (r io.ReadCloser, err error) {
data, err := vfs.ReadFile(h.c.fs, filepath.ToSlash(name))
if err != nil {
return nil, err
}
return ioutil.NopCloser(bytes.NewReader(data)), nil
}
// Make the syscall/js package always visible by default.
// It defaults to the host's GOOS/GOARCH, and golang.org's
// linux/amd64 means the wasm syscall/js package was blank.
// And you can't run godoc on js/wasm anyway, so host defaults
// don't make sense here.
if goos == "" && goarch == "" && relpath == "syscall/js" {
goos, goarch = "js", "wasm"
}
if goos != "" {
ctxt.GOOS = goos
}
if goarch != "" {
ctxt.GOARCH = goarch
}
pkginfo, err := ctxt.ImportDir(abspath, 0)
// continue if there are no Go source files; we still want the directory info
if _, nogo := err.(*build.NoGoError); err != nil && !nogo {
info.Err = err
return info
}
// collect package files
pkgname := pkginfo.Name
pkgfiles := append(pkginfo.GoFiles, pkginfo.CgoFiles...)
if len(pkgfiles) == 0 {
// Commands written in C have no .go files in the build.
// Instead, documentation may be found in an ignored file.
// The file may be ignored via an explicit +build ignore
// constraint (recommended), or by defining the package
// documentation (historic).
pkgname = "main" // assume package main since pkginfo.Name == ""
pkgfiles = pkginfo.IgnoredGoFiles
}
// get package information, if any
if len(pkgfiles) > 0 {
// build package AST
fset := token.NewFileSet()
files, err := h.c.parseFiles(fset, relpath, abspath, pkgfiles)
if err != nil {
info.Err = err
return info
}
// ignore any errors - they are due to unresolved identifiers
pkg, _ := ast.NewPackage(fset, files, poorMansImporter, nil)
// extract package documentation
info.FSet = fset
if mode&ShowSource == 0 {
// show extracted documentation
var m doc.Mode
if mode&NoFiltering != 0 {
m |= doc.AllDecls
}
if mode&AllMethods != 0 {
m |= doc.AllMethods
}
info.PDoc = doc.New(pkg, pathpkg.Clean(relpath), m) // no trailing '/' in importpath
if mode&NoTypeAssoc != 0 {
for _, t := range info.PDoc.Types {
info.PDoc.Consts = append(info.PDoc.Consts, t.Consts...)
info.PDoc.Vars = append(info.PDoc.Vars, t.Vars...)
info.PDoc.Funcs = append(info.PDoc.Funcs, t.Funcs...)
t.Consts = nil
t.Vars = nil
t.Funcs = nil
}
// for now we cannot easily sort consts and vars since
// go/doc.Value doesn't export the order information
sort.Sort(funcsByName(info.PDoc.Funcs))
}
// collect examples
testfiles := append(pkginfo.TestGoFiles, pkginfo.XTestGoFiles...)
files, err = h.c.parseFiles(fset, relpath, abspath, testfiles)
if err != nil {
log.Println("parsing examples:", err)
}
info.Examples = collectExamples(h.c, pkg, files)
// collect any notes that we want to show
if info.PDoc.Notes != nil {
// could regexp.Compile only once per godoc, but probably not worth it
if rx := h.p.NotesRx; rx != nil {
for m, n := range info.PDoc.Notes {
if rx.MatchString(m) {
if info.Notes == nil {
info.Notes = make(map[string][]*doc.Note)
}
info.Notes[m] = n
}
}
}
}
} else {
// show source code
// TODO(gri) Consider eliminating export filtering in this mode,
// or perhaps eliminating the mode altogether.
if mode&NoFiltering == 0 {
packageExports(fset, pkg)
}
info.PAst = files
}
info.IsMain = pkgname == "main"
}
// get directory information, if any
var dir *Directory
var timestamp time.Time
if tree, ts := h.c.fsTree.Get(); tree != nil && tree.(*Directory) != nil {
// directory tree is present; lookup respective directory
// (may still fail if the file system was updated and the
// new directory tree has not yet been computed)
dir = tree.(*Directory).lookup(abspath)
timestamp = ts
}
if dir == nil {
// TODO(agnivade): handle this case better, now since there is no CLI mode.
// no directory tree present (happens in command-line mode);
// compute 2 levels for this page. The second level is to
// get the synopses of sub-directories.
// note: cannot use path filter here because in general
// it doesn't contain the FSTree path
dir = h.c.newDirectory(abspath, 2)
timestamp = time.Now()
}
info.Dirs = dir.listing(true, func(path string) bool { return h.includePath(path, mode) })
info.DirTime = timestamp
info.DirFlat = mode&FlatDir != 0
return info
}
func (h *handlerServer) includePath(path string, mode PageInfoMode) (r bool) {
// if the path is under one of the exclusion paths, don't list.
for _, e := range h.exclude {
if strings.HasPrefix(path, e) {
return false
}
}
// if the path includes 'internal', don't list unless we are in the NoFiltering mode.
if mode&NoFiltering != 0 {
return true
}
if strings.Contains(path, "internal") || strings.Contains(path, "vendor") {
for _, c := range strings.Split(filepath.Clean(path), string(os.PathSeparator)) {
if c == "internal" || c == "vendor" {
return false
}
}
}
return true
}
type funcsByName []*doc.Func
func (s funcsByName) Len() int { return len(s) }
func (s funcsByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s funcsByName) Less(i, j int) bool { return s[i].Name < s[j].Name }
func (h *handlerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if redirect(w, r) {
return
}
relpath := pathpkg.Clean(r.URL.Path[len(h.stripPrefix)+1:])
if !h.corpusInitialized() {
h.p.ServeError(w, r, relpath, errors.New("Scan is not yet complete. Please retry after a few moments"))
return
}
abspath := pathpkg.Join(h.fsRoot, relpath)
mode := h.p.GetPageInfoMode(r)
if relpath == builtinPkgPath {
mode = NoFiltering | NoTypeAssoc
}
info := h.GetPageInfo(abspath, relpath, mode, r.FormValue("GOOS"), r.FormValue("GOARCH"))
if info.Err != nil {
log.Print(info.Err)
h.p.ServeError(w, r, relpath, info.Err)
return
}
var tabtitle, title, subtitle string
switch {
case info.PAst != nil:
for _, ast := range info.PAst {
tabtitle = ast.Name.Name
break
}
case info.PDoc != nil:
tabtitle = info.PDoc.Name
default:
tabtitle = info.Dirname
title = "Directory "
if h.p.ShowTimestamps {
subtitle = "Last update: " + info.DirTime.String()
}
}
if title == "" {
if info.IsMain {
// assume that the directory name is the command name
_, tabtitle = pathpkg.Split(relpath)
title = "Command "
} else {
title = "Package "
}
}
title += tabtitle
// special cases for top-level package/command directories
switch tabtitle {
case "/src":
title = "Packages"
tabtitle = "Packages"
case "/src/cmd":
title = "Commands"
tabtitle = "Commands"
}
// Emit JSON array for type information.
pi := h.c.Analysis.PackageInfo(relpath)
hasTreeView := len(pi.CallGraph) != 0
info.CallGraphIndex = pi.CallGraphIndex
info.CallGraph = htmltemplate.JS(marshalJSON(pi.CallGraph))
info.AnalysisData = htmltemplate.JS(marshalJSON(pi.Types))
info.TypeInfoIndex = make(map[string]int)
for i, ti := range pi.Types {
info.TypeInfoIndex[ti.Name] = i
}
info.GoogleCN = googleCN(r)
var body []byte
if info.Dirname == "/src" {
body = applyTemplate(h.p.PackageRootHTML, "packageRootHTML", info)
} else {
body = applyTemplate(h.p.PackageHTML, "packageHTML", info)
}
h.p.ServePage(w, Page{
Title: title,
Tabtitle: tabtitle,
Subtitle: subtitle,
Body: body,
GoogleCN: info.GoogleCN,
TreeView: hasTreeView,
})
}
func (h *handlerServer) corpusInitialized() bool {
h.c.initMu.RLock()
defer h.c.initMu.RUnlock()
return h.c.initDone
}
type PageInfoMode uint
const (
PageInfoModeQueryString = "m" // query string where PageInfoMode is stored
NoFiltering PageInfoMode = 1 << iota // do not filter exports
AllMethods // show all embedded methods
ShowSource // show source code, do not extract documentation
NoHTML // show result in textual form, do not generate HTML
FlatDir // show directory in a flat (non-indented) manner
NoTypeAssoc // don't associate consts, vars, and factory functions with types
)
// modeNames defines names for each PageInfoMode flag.
var modeNames = map[string]PageInfoMode{
"all": NoFiltering,
"methods": AllMethods,
"src": ShowSource,
"text": NoHTML,
"flat": FlatDir,
}
// generate a query string for persisting PageInfoMode between pages.
func modeQueryString(mode PageInfoMode) string {
if modeNames := mode.names(); len(modeNames) > 0 {
return "?m=" + strings.Join(modeNames, ",")
}
return ""
}
// alphabetically sorted names of active flags for a PageInfoMode.
func (m PageInfoMode) names() []string {
var names []string
for name, mode := range modeNames {
if m&mode != 0 {
names = append(names, name)
}
}
sort.Strings(names)
return names
}
// GetPageInfoMode computes the PageInfoMode flags by analyzing the request
// URL form value "m". It is value is a comma-separated list of mode names
// as defined by modeNames (e.g.: m=src,text).
func (p *Presentation) GetPageInfoMode(r *http.Request) PageInfoMode {
var mode PageInfoMode
for _, k := range strings.Split(r.FormValue(PageInfoModeQueryString), ",") {
if m, found := modeNames[strings.TrimSpace(k)]; found {
mode |= m
}
}
if p.AdjustPageInfoMode != nil {
mode = p.AdjustPageInfoMode(r, mode)
}
return mode
}
// 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
}
// globalNames returns a set of the names declared by all package-level
// declarations. Method names are returned in the form Receiver_Method.
func globalNames(pkg *ast.Package) map[string]bool {
names := make(map[string]bool)
for _, file := range pkg.Files {
for _, decl := range file.Decls {
addNames(names, decl)
}
}
return names
}
// collectExamples collects examples for pkg from testfiles.
func collectExamples(c *Corpus, pkg *ast.Package, testfiles map[string]*ast.File) []*doc.Example {
var files []*ast.File
for _, f := range testfiles {
files = append(files, f)
}
var examples []*doc.Example
globals := globalNames(pkg)
for _, e := range doc.Examples(files...) {
name := stripExampleSuffix(e.Name)
if name == "" || globals[name] {
examples = append(examples, e)
} else if c.Verbose {
log.Printf("skipping example 'Example%s' because '%s' is not a known function or type", e.Name, e.Name)
}
}
return examples
}
// addNames adds the names declared by decl to the names set.
// Method names are added in the form ReceiverTypeName_Method.
func addNames(names map[string]bool, decl ast.Decl) {
switch d := decl.(type) {
case *ast.FuncDecl:
name := d.Name.Name
if d.Recv != nil {
var typeName string
switch r := d.Recv.List[0].Type.(type) {
case *ast.StarExpr:
typeName = r.X.(*ast.Ident).Name
case *ast.Ident:
typeName = r.Name
}
name = typeName + "_" + name
}
names[name] = true
case *ast.GenDecl:
for _, spec := range d.Specs {
switch s := spec.(type) {
case *ast.TypeSpec:
names[s.Name.Name] = true
case *ast.ValueSpec:
for _, id := range s.Names {
names[id.Name] = true
}
}
}
}
}
// packageExports is a local implementation of ast.PackageExports
// which correctly updates each package file's comment list.
// (The ast.PackageExports signature is frozen, hence the local
// implementation).
//
func packageExports(fset *token.FileSet, pkg *ast.Package) {
for _, src := range pkg.Files {
cmap := ast.NewCommentMap(fset, src, src.Comments)
ast.FileExports(src)
src.Comments = cmap.Filter(src).Comments()
}
}
func applyTemplate(t *template.Template, name string, data interface{}) []byte {
var buf bytes.Buffer
if err := t.Execute(&buf, data); err != nil {
log.Printf("%s.Execute: %s", name, err)
}
return buf.Bytes()
}
type writerCapturesErr struct {
w io.Writer
err error
}
func (w *writerCapturesErr) Write(p []byte) (int, error) {
n, err := w.w.Write(p)
if err != nil {
w.err = err
}
return n, err
}
// applyTemplateToResponseWriter uses an http.ResponseWriter as the io.Writer
// for the call to template.Execute. It uses an io.Writer wrapper to capture
// errors from the underlying http.ResponseWriter. Errors are logged only when
// they come from the template processing and not the Writer; this avoid
// polluting log files with error messages due to networking issues, such as
// client disconnects and http HEAD protocol violations.
func applyTemplateToResponseWriter(rw http.ResponseWriter, t *template.Template, data interface{}) {
w := &writerCapturesErr{w: rw}
err := t.Execute(w, data)
// There are some cases where template.Execute does not return an error when
// rw returns an error, and some where it does. So check w.err first.
if w.err == nil && err != nil {
// Log template errors.
log.Printf("%s.Execute: %s", t.Name(), err)
}
}
func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) {
canonical := pathpkg.Clean(r.URL.Path)
if !strings.HasSuffix(canonical, "/") {
canonical += "/"
}
if r.URL.Path != canonical {
url := *r.URL
url.Path = canonical
http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
redirected = true
}
return
}
func redirectFile(w http.ResponseWriter, r *http.Request) (redirected bool) {
c := pathpkg.Clean(r.URL.Path)
c = strings.TrimRight(c, "/")
if r.URL.Path != c {
url := *r.URL
url.Path = c
http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
redirected = true
}
return
}
func (p *Presentation) serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath, title string) {
src, err := vfs.ReadFile(p.Corpus.fs, abspath)
if err != nil {
log.Printf("ReadFile: %s", err)
p.ServeError(w, r, relpath, err)
return
}
if r.FormValue(PageInfoModeQueryString) == "text" {
p.ServeText(w, src)
return
}
h := r.FormValue("h")
s := RangeSelection(r.FormValue("s"))
var buf bytes.Buffer
if pathpkg.Ext(abspath) == ".go" {
// Find markup links for this file (e.g. "/src/fmt/print.go").
fi := p.Corpus.Analysis.FileInfo(abspath)
buf.WriteString("<script type='text/javascript'>document.ANALYSIS_DATA = ")
buf.Write(marshalJSON(fi.Data))
buf.WriteString(";</script>\n")
if status := p.Corpus.Analysis.Status(); status != "" {
buf.WriteString("<a href='/lib/godoc/analysis/help.html'>Static analysis features</a> ")
// TODO(adonovan): show analysis status at per-file granularity.
fmt.Fprintf(&buf, "<span style='color: grey'>[%s]</span><br/>", htmlpkg.EscapeString(status))
}
buf.WriteString("<pre>")
formatGoSource(&buf, src, fi.Links, h, s)
buf.WriteString("</pre>")
} else {
buf.WriteString("<pre>")
FormatText(&buf, src, 1, false, h, s)
buf.WriteString("</pre>")
}
fmt.Fprintf(&buf, `<p><a href="/%s?m=text">View as plain text</a></p>`, htmlpkg.EscapeString(relpath))
p.ServePage(w, Page{
Title: title,
SrcPath: relpath,
Tabtitle: relpath,
Body: buf.Bytes(),
GoogleCN: googleCN(r),
})
}
// formatGoSource HTML-escapes Go source text and writes it to w,
// decorating it with the specified analysis links.
//
func formatGoSource(buf *bytes.Buffer, text []byte, links []analysis.Link, pattern string, selection Selection) {
// Emit to a temp buffer so that we can add line anchors at the end.
saved, buf := buf, new(bytes.Buffer)
var i int
var link analysis.Link // shared state of the two funcs below
segmentIter := func() (seg Segment) {
if i < len(links) {
link = links[i]
i++
seg = Segment{link.Start(), link.End()}
}
return
}
linkWriter := func(w io.Writer, offs int, start bool) {
link.Write(w, offs, start)
}
comments := tokenSelection(text, token.COMMENT)
var highlights Selection
if pattern != "" {
highlights = regexpSelection(text, pattern)
}
FormatSelections(buf, text, linkWriter, segmentIter, selectionTag, comments, highlights, selection)
// Now copy buf to saved, adding line anchors.
// The lineSelection mechanism can't be composed with our
// linkWriter, so we have to add line spans as another pass.
n := 1
for _, line := range bytes.Split(buf.Bytes(), []byte("\n")) {
// The line numbers are inserted into the document via a CSS ::before
// pseudo-element. This prevents them from being copied when users
// highlight and copy text.
// ::before is supported in 98% of browsers: https://caniuse.com/#feat=css-gencontent
// This is also the trick Github uses to hide line numbers.
//
// The first tab for the code snippet needs to start in column 9, so
// it indents a full 8 spaces, hence the two nbsp's. Otherwise the tab
// character only indents a short amount.
//
// Due to rounding and font width Firefox might not treat 8 rendered
// characters as 8 characters wide, and subsequently may treat the tab
// character in the 9th position as moving the width from (7.5 or so) up
// to 8. See
// https://github.com/webcompat/web-bugs/issues/17530#issuecomment-402675091
// for a fuller explanation. The solution is to add a CSS class to
// explicitly declare the width to be 8 characters.
fmt.Fprintf(saved, `<span id="L%d" class="ln">%6d&nbsp;&nbsp;</span>`, n, n)
n++
saved.Write(line)
saved.WriteByte('\n')
}
}
func (p *Presentation) serveDirectory(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
if redirect(w, r) {
return
}
list, err := p.Corpus.fs.ReadDir(abspath)
if err != nil {
p.ServeError(w, r, relpath, err)
return
}
p.ServePage(w, Page{
Title: "Directory",
SrcPath: relpath,
Tabtitle: relpath,
Body: applyTemplate(p.DirlistHTML, "dirlistHTML", list),
GoogleCN: googleCN(r),
})
}
func (p *Presentation) ServeHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
// get HTML body contents
src, err := vfs.ReadFile(p.Corpus.fs, abspath)
if err != nil {
log.Printf("ReadFile: %s", err)
p.ServeError(w, r, relpath, err)
return
}
// if it begins with "<!DOCTYPE " assume it is standalone
// html that doesn't need the template wrapping.
if bytes.HasPrefix(src, doctype) {
w.Write(src)
return
}
// if it begins with a JSON blob, read in the metadata.
meta, src, err := extractMetadata(src)
if err != nil {
log.Printf("decoding metadata %s: %v", relpath, err)
}
page := Page{
Title: meta.Title,
Subtitle: meta.Subtitle,
GoogleCN: googleCN(r),
}
// evaluate as template if indicated
if meta.Template {
tmpl, err := template.New("main").Funcs(p.TemplateFuncs()).Parse(string(src))
if err != nil {
log.Printf("parsing template %s: %v", relpath, err)
p.ServeError(w, r, relpath, err)
return
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, page); err != nil {
log.Printf("executing template %s: %v", relpath, err)
p.ServeError(w, r, relpath, err)
return
}
src = buf.Bytes()
}
// if it's the language spec, add tags to EBNF productions
if strings.HasSuffix(abspath, "go_spec.html") {
var buf bytes.Buffer
Linkify(&buf, src)
src = buf.Bytes()
}
page.Body = src
p.ServePage(w, page)
}
func (p *Presentation) ServeFile(w http.ResponseWriter, r *http.Request) {
p.serveFile(w, r)
}
func (p *Presentation) serveFile(w http.ResponseWriter, r *http.Request) {
relpath := r.URL.Path
// Check to see if we need to redirect or serve another file.
if m := p.Corpus.MetadataFor(relpath); m != nil {
if m.Path != relpath {
// Redirect to canonical path.
http.Redirect(w, r, m.Path, http.StatusMovedPermanently)
return
}
// Serve from the actual filesystem path.
relpath = m.filePath
}
abspath := relpath
relpath = relpath[1:] // strip leading slash
switch pathpkg.Ext(relpath) {
case ".html":
if strings.HasSuffix(relpath, "/index.html") {
// We'll show index.html for the directory.
// Use the dir/ version as canonical instead of dir/index.html.
http.Redirect(w, r, r.URL.Path[0:len(r.URL.Path)-len("index.html")], http.StatusMovedPermanently)
return
}
p.ServeHTMLDoc(w, r, abspath, relpath)
return
case ".go":
p.serveTextFile(w, r, abspath, relpath, "Source file")
return
}
dir, err := p.Corpus.fs.Lstat(abspath)
if err != nil {
log.Print(err)
p.ServeError(w, r, relpath, err)
return
}
if dir != nil && dir.IsDir() {
if redirect(w, r) {
return
}
if index := pathpkg.Join(abspath, "index.html"); util.IsTextFile(p.Corpus.fs, index) {
p.ServeHTMLDoc(w, r, index, index)
return
}
p.serveDirectory(w, r, abspath, relpath)
return
}
if util.IsTextFile(p.Corpus.fs, abspath) {
if redirectFile(w, r) {
return
}
p.serveTextFile(w, r, abspath, relpath, "Text file")
return
}
p.fileServer.ServeHTTP(w, r)
}
func (p *Presentation) ServeText(w http.ResponseWriter, text []byte) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Write(text)
}
func marshalJSON(x interface{}) []byte {
var data []byte
var err error
const indentJSON = false // for easier debugging
if indentJSON {
data, err = json.MarshalIndent(x, "", " ")
} else {
data, err = json.Marshal(x)
}
if err != nil {
panic(fmt.Sprintf("json.Marshal failed: %s", err))
}
return data
}

Просмотреть файл

@ -0,0 +1,48 @@
// Copyright 2018 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 godoc
import (
"testing"
"golang.org/x/website/cmd/golangorg/godoc/vfs/mapfs"
)
// TestIgnoredGoFiles tests the scenario where a folder has no .go or .c files,
// but has an ignored go file.
func TestIgnoredGoFiles(t *testing.T) {
packagePath := "github.com/package"
packageComment := "main is documented in an ignored .go file"
c := NewCorpus(mapfs.New(map[string]string{
"src/" + packagePath + "/ignored.go": `// +build ignore
// ` + packageComment + `
package main`}))
srv := &handlerServer{
p: &Presentation{
Corpus: c,
},
c: c,
}
pInfo := srv.GetPageInfo("/src/"+packagePath, packagePath, NoFiltering, "linux", "amd64")
if pInfo.PDoc == nil {
t.Error("pInfo.PDoc = nil; want non-nil.")
} else {
if got, want := pInfo.PDoc.Doc, packageComment+"\n"; got != want {
t.Errorf("pInfo.PDoc.Doc = %q; want %q.", got, want)
}
if got, want := pInfo.PDoc.Name, "main"; got != want {
t.Errorf("pInfo.PDoc.Name = %q; want %q.", got, want)
}
if got, want := pInfo.PDoc.ImportPath, packagePath; got != want {
t.Errorf("pInfo.PDoc.ImportPath = %q; want %q.", got, want)
}
}
if pInfo.FSet == nil {
t.Error("pInfo.FSet = nil; want non-nil.")
}
}

Просмотреть файл

@ -0,0 +1,186 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
// +build golangorg
// Package short implements a simple URL shortener, serving an administrative
// interface at /s and shortened urls from /s/key.
// It is designed to run only on the instance of godoc that serves golang.org.
package short
// TODO(adg): collect statistics on URL visits
import (
"context"
"errors"
"fmt"
"html/template"
"io"
"log"
"net/http"
"net/url"
"regexp"
"cloud.google.com/go/datastore"
"golang.org/x/website/internal/memcache"
"google.golang.org/appengine/user"
)
const (
prefix = "/s"
kind = "Link"
baseURL = "https://golang.org" + prefix
)
// Link represents a short link.
type Link struct {
Key, Target string
}
var validKey = regexp.MustCompile(`^[a-zA-Z0-9-_.]+$`)
type server struct {
datastore *datastore.Client
memcache *memcache.CodecClient
}
func RegisterHandlers(mux *http.ServeMux, dc *datastore.Client, mc *memcache.Client) {
s := server{dc, mc.WithCodec(memcache.JSON)}
mux.HandleFunc(prefix+"/", s.linkHandler)
// TODO(cbro): move storage of the links to a text file in Gerrit.
// Disable the admin handler until that happens, since GAE Flex doesn't support
// the "google.golang.org/appengine/user" package.
// See golang.org/issue/27205#issuecomment-418673218
// mux.HandleFunc(prefix, adminHandler)
mux.HandleFunc(prefix, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusForbidden)
io.WriteString(w, "Link creation temporarily unavailable. See golang.org/issue/27205.")
})
}
// linkHandler services requests to short URLs.
// http://golang.org/s/key
// It consults memcache and datastore for the Link for key.
// It then sends a redirects or an error message.
func (h server) linkHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
key := r.URL.Path[len(prefix)+1:]
if !validKey.MatchString(key) {
http.Error(w, "not found", http.StatusNotFound)
return
}
var link Link
if err := h.memcache.Get(ctx, cacheKey(key), &link); err != nil {
k := datastore.NameKey(kind, key, nil)
err = h.datastore.Get(ctx, k, &link)
switch err {
case datastore.ErrNoSuchEntity:
http.Error(w, "not found", http.StatusNotFound)
return
default: // != nil
log.Printf("ERROR %q: %v", key, err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
case nil:
item := &memcache.Item{
Key: cacheKey(key),
Object: &link,
}
if err := h.memcache.Set(ctx, item); err != nil {
log.Printf("WARNING %q: %v", key, err)
}
}
}
http.Redirect(w, r, link.Target, http.StatusFound)
}
var adminTemplate = template.Must(template.New("admin").Parse(templateHTML))
// adminHandler serves an administrative interface.
func (h server) adminHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if !user.IsAdmin(ctx) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
var newLink *Link
var doErr error
if r.Method == "POST" {
key := r.FormValue("key")
switch r.FormValue("do") {
case "Add":
newLink = &Link{key, r.FormValue("target")}
doErr = h.putLink(ctx, newLink)
case "Delete":
k := datastore.NameKey(kind, key, nil)
doErr = h.datastore.Delete(ctx, k)
default:
http.Error(w, "unknown action", http.StatusBadRequest)
}
err := h.memcache.Delete(ctx, cacheKey(key))
if err != nil && err != memcache.ErrCacheMiss {
log.Printf("WARNING %q: %v", key, err)
}
}
var links []*Link
q := datastore.NewQuery(kind).Order("Key")
if _, err := h.datastore.GetAll(ctx, q, &links); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Printf("ERROR %v", err)
return
}
// Put the new link in the list if it's not there already.
// (Eventual consistency means that it might not show up
// immediately, which might be confusing for the user.)
if newLink != nil && doErr == nil {
found := false
for i := range links {
if links[i].Key == newLink.Key {
found = true
break
}
}
if !found {
links = append([]*Link{newLink}, links...)
}
newLink = nil
}
var data = struct {
BaseURL string
Prefix string
Links []*Link
New *Link
Error error
}{baseURL, prefix, links, newLink, doErr}
if err := adminTemplate.Execute(w, &data); err != nil {
log.Printf("ERROR adminTemplate: %v", err)
}
}
// putLink validates the provided link and puts it into the datastore.
func (h server) putLink(ctx context.Context, link *Link) error {
if !validKey.MatchString(link.Key) {
return errors.New("invalid key; must match " + validKey.String())
}
if _, err := url.Parse(link.Target); err != nil {
return fmt.Errorf("bad target: %v", err)
}
k := datastore.NameKey(kind, link.Key, nil)
_, err := h.datastore.Put(ctx, k, link)
return err
}
// cacheKey returns a short URL key as a memcache key.
func cacheKey(key string) string {
return "link-" + key
}

Просмотреть файл

@ -0,0 +1,119 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package short
const templateHTML = `
<!doctype HTML>
<html>
<head>
<title>golang.org URL shortener</title>
<style>
body {
background: white;
}
input {
border: 1px solid #ccc;
}
input[type=text] {
width: 400px;
}
input, td, th {
color: #333;
font-family: Georgia, Times New Roman, serif;
}
input, td {
font-size: 14pt;
}
th {
font-size: 16pt;
text-align: left;
padding-top: 10px;
}
.autoselect {
border: none;
}
.error {
color: #900;
}
table {
margin-left: auto;
margin-right: auto;
}
</style>
</head>
<body>
<table>
{{with .Error}}
<tr>
<th colspan="3">Error</th>
</tr>
<tr>
<td class="error" colspan="3">{{.}}</td>
</tr>
{{end}}
<tr>
<th>Key</th>
<th>Target</th>
<th></th>
</tr>
<form method="POST" action="{{.Prefix}}">
<tr>
<td><input type="text" name="key"{{with .New}} value="{{.Key}}"{{end}}></td>
<td><input type="text" name="target"{{with .New}} value="{{.Target}}"{{end}}></td>
<td><input type="submit" name="do" value="Add">
</tr>
</form>
{{with .Links}}
<tr>
<th>Short Link</th>
<th>&nbsp;</th>
<th>&nbsp;</th>
</tr>
{{range .}}
<tr>
<td><input class="autoselect" type="text" orig="{{$.BaseURL}}/{{.Key}}" value="{{$.BaseURL}}/{{.Key}}"></td>
<td><input class="autoselect" type="text" orig="{{.Target}}" value="{{.Target}}"></td>
<td>
<form method="POST" action="{{$.Prefix}}">
<input type="hidden" name="key" value="{{.Key}}">
<input type="submit" name="do" value="Delete" class="delete">
</form>
</td>
</tr>
{{end}}
{{end}}
</table>
</body>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
<script type="text/javascript">window.jQuery || document.write(unescape("%3Cscript src='/doc/jquery.js' type='text/javascript'%3E%3C/script%3E"));</script>
<script>
$(document).ready(function() {
$('.autoselect').each(function() {
$(this).click(function() {
$(this).select();
});
$(this).change(function() {
$(this).val($(this).attr('orig'));
});
});
$('.delete').click(function(e) {
var link = $(this).closest('tr').find('input').first().val();
var ok = confirm('Delete this link?\n' + link);
if (!ok) {
e.preventDefault();
return false;
}
});
});
</script>
</html>
`

Просмотреть файл

@ -0,0 +1,123 @@
// Copyright 2009 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 contains the infrastructure to create a code
// snippet for search results.
//
// Note: At the moment, this only creates HTML snippets.
package godoc
import (
"bytes"
"fmt"
"go/ast"
"go/token"
)
type Snippet struct {
Line int
Text string // HTML-escaped
}
func (p *Presentation) newSnippet(fset *token.FileSet, decl ast.Decl, id *ast.Ident) *Snippet {
// TODO instead of pretty-printing the node, should use the original source instead
var buf1 bytes.Buffer
p.writeNode(&buf1, nil, fset, decl)
// wrap text with <pre> tag
var buf2 bytes.Buffer
buf2.WriteString("<pre>")
FormatText(&buf2, buf1.Bytes(), -1, true, id.Name, nil)
buf2.WriteString("</pre>")
return &Snippet{fset.Position(id.Pos()).Line, buf2.String()}
}
func findSpec(list []ast.Spec, id *ast.Ident) ast.Spec {
for _, spec := range list {
switch s := spec.(type) {
case *ast.ImportSpec:
if s.Name == id {
return s
}
case *ast.ValueSpec:
for _, n := range s.Names {
if n == id {
return s
}
}
case *ast.TypeSpec:
if s.Name == id {
return s
}
}
}
return nil
}
func (p *Presentation) genSnippet(fset *token.FileSet, d *ast.GenDecl, id *ast.Ident) *Snippet {
s := findSpec(d.Specs, id)
if s == nil {
return nil // declaration doesn't contain id - exit gracefully
}
// only use the spec containing the id for the snippet
dd := &ast.GenDecl{
Doc: d.Doc,
TokPos: d.Pos(),
Tok: d.Tok,
Lparen: d.Lparen,
Specs: []ast.Spec{s},
Rparen: d.Rparen,
}
return p.newSnippet(fset, dd, id)
}
func (p *Presentation) funcSnippet(fset *token.FileSet, d *ast.FuncDecl, id *ast.Ident) *Snippet {
if d.Name != id {
return nil // declaration doesn't contain id - exit gracefully
}
// only use the function signature for the snippet
dd := &ast.FuncDecl{
Doc: d.Doc,
Recv: d.Recv,
Name: d.Name,
Type: d.Type,
}
return p.newSnippet(fset, dd, id)
}
// NewSnippet creates a text snippet from a declaration decl containing an
// identifier id. Parts of the declaration not containing the identifier
// may be removed for a more compact snippet.
func NewSnippet(fset *token.FileSet, decl ast.Decl, id *ast.Ident) *Snippet {
// TODO(bradfitz, adg): remove this function. But it's used by indexer, which
// doesn't have a *Presentation, and NewSnippet needs a TabWidth.
var p Presentation
p.TabWidth = 4
return p.NewSnippet(fset, decl, id)
}
// NewSnippet creates a text snippet from a declaration decl containing an
// identifier id. Parts of the declaration not containing the identifier
// may be removed for a more compact snippet.
func (p *Presentation) NewSnippet(fset *token.FileSet, decl ast.Decl, id *ast.Ident) *Snippet {
var s *Snippet
switch d := decl.(type) {
case *ast.GenDecl:
s = p.genSnippet(fset, d, id)
case *ast.FuncDecl:
s = p.funcSnippet(fset, d, id)
}
// handle failure gracefully
if s == nil {
var buf bytes.Buffer
fmt.Fprintf(&buf, `<span class="alert">could not generate a snippet for <span class="highlight">%s</span></span>`, id.Name)
s = &Snippet{fset.Position(id.Pos()).Line, buf.String()}
}
return s
}

179
cmd/golangorg/godoc/spec.go Normal file
Просмотреть файл

@ -0,0 +1,179 @@
// Copyright 2009 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 godoc
// This file contains the mechanism to "linkify" html source
// text containing EBNF sections (as found in go_spec.html).
// The result is the input source text with the EBNF sections
// modified such that identifiers are linked to the respective
// definitions.
import (
"bytes"
"fmt"
"io"
"text/scanner"
)
type ebnfParser struct {
out io.Writer // parser output
src []byte // parser input
scanner scanner.Scanner
prev int // offset of previous token
pos int // offset of current token
tok rune // one token look-ahead
lit string // token literal
}
func (p *ebnfParser) flush() {
p.out.Write(p.src[p.prev:p.pos])
p.prev = p.pos
}
func (p *ebnfParser) next() {
p.tok = p.scanner.Scan()
p.pos = p.scanner.Position.Offset
p.lit = p.scanner.TokenText()
}
func (p *ebnfParser) printf(format string, args ...interface{}) {
p.flush()
fmt.Fprintf(p.out, format, args...)
}
func (p *ebnfParser) errorExpected(msg string) {
p.printf(`<span class="highlight">error: expected %s, found %s</span>`, msg, scanner.TokenString(p.tok))
}
func (p *ebnfParser) expect(tok rune) {
if p.tok != tok {
p.errorExpected(scanner.TokenString(tok))
}
p.next() // make progress in any case
}
func (p *ebnfParser) parseIdentifier(def bool) {
if p.tok == scanner.Ident {
name := p.lit
if def {
p.printf(`<a id="%s">%s</a>`, name, name)
} else {
p.printf(`<a href="#%s" class="noline">%s</a>`, name, name)
}
p.prev += len(name) // skip identifier when printing next time
p.next()
} else {
p.expect(scanner.Ident)
}
}
func (p *ebnfParser) parseTerm() bool {
switch p.tok {
case scanner.Ident:
p.parseIdentifier(false)
case scanner.String, scanner.RawString:
p.next()
const ellipsis = '…' // U+2026, the horizontal ellipsis character
if p.tok == ellipsis {
p.next()
p.expect(scanner.String)
}
case '(':
p.next()
p.parseExpression()
p.expect(')')
case '[':
p.next()
p.parseExpression()
p.expect(']')
case '{':
p.next()
p.parseExpression()
p.expect('}')
default:
return false // no term found
}
return true
}
func (p *ebnfParser) parseSequence() {
if !p.parseTerm() {
p.errorExpected("term")
}
for p.parseTerm() {
}
}
func (p *ebnfParser) parseExpression() {
for {
p.parseSequence()
if p.tok != '|' {
break
}
p.next()
}
}
func (p *ebnfParser) parseProduction() {
p.parseIdentifier(true)
p.expect('=')
if p.tok != '.' {
p.parseExpression()
}
p.expect('.')
}
func (p *ebnfParser) parse(out io.Writer, src []byte) {
// initialize ebnfParser
p.out = out
p.src = src
p.scanner.Init(bytes.NewBuffer(src))
p.next() // initializes pos, tok, lit
// process source
for p.tok != scanner.EOF {
p.parseProduction()
}
p.flush()
}
// Markers around EBNF sections
var (
openTag = []byte(`<pre class="ebnf">`)
closeTag = []byte(`</pre>`)
)
func Linkify(out io.Writer, src []byte) {
for len(src) > 0 {
// i: beginning of EBNF text (or end of source)
i := bytes.Index(src, openTag)
if i < 0 {
i = len(src) - len(openTag)
}
i += len(openTag)
// j: end of EBNF text (or end of source)
j := bytes.Index(src[i:], closeTag) // close marker
if j < 0 {
j = len(src) - i
}
j += i
// write text before EBNF
out.Write(src[0:i])
// process EBNF
var p ebnfParser
p.parse(out, src[i:j])
// advance
src = src[j:]
}
}

Просмотреть файл

@ -0,0 +1,22 @@
// Copyright 2018 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 godoc
import (
"bytes"
"strings"
"testing"
)
func TestParseEBNFString(t *testing.T) {
var p ebnfParser
var buf bytes.Buffer
src := []byte("octal_byte_value = `\\` octal_digit octal_digit octal_digit .")
p.parse(&buf, src)
if strings.Contains(buf.String(), "error") {
t.Error(buf.String())
}
}

Просмотреть файл

@ -0,0 +1,83 @@
// 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.
package godoc
// ----------------------------------------------------------------------------
// SpotInfo
// A SpotInfo value describes a particular identifier spot in a given file;
// It encodes three values: the SpotKind (declaration or use), a line or
// snippet index "lori", and whether it's a line or index.
//
// The following encoding is used:
//
// bits 32 4 1 0
// value [lori|kind|isIndex]
//
type SpotInfo uint32
// SpotKind describes whether an identifier is declared (and what kind of
// declaration) or used.
type SpotKind uint32
const (
PackageClause SpotKind = iota
ImportDecl
ConstDecl
TypeDecl
VarDecl
FuncDecl
MethodDecl
Use
nKinds
)
var (
// These must match the SpotKind values above.
name = []string{
"Packages",
"Imports",
"Constants",
"Types",
"Variables",
"Functions",
"Methods",
"Uses",
"Unknown",
}
)
func (x SpotKind) Name() string { return name[x] }
func init() {
// sanity check: if nKinds is too large, the SpotInfo
// accessor functions may need to be updated
if nKinds > 8 {
panic("internal error: nKinds > 8")
}
}
// makeSpotInfo makes a SpotInfo.
func makeSpotInfo(kind SpotKind, lori int, isIndex bool) SpotInfo {
// encode lori: bits [4..32)
x := SpotInfo(lori) << 4
if int(x>>4) != lori {
// lori value doesn't fit - since snippet indices are
// most certainly always smaller then 1<<28, this can
// only happen for line numbers; give it no line number (= 0)
x = 0
}
// encode kind: bits [1..4)
x |= SpotInfo(kind) << 1
// encode isIndex: bit 0
if isIndex {
x |= 1
}
return x
}
func (x SpotInfo) Kind() SpotKind { return SpotKind(x >> 1 & 7) }
func (x SpotInfo) Lori() int { return int(x >> 4) }
func (x SpotInfo) IsIndex() bool { return x&1 != 0 }

Просмотреть файл

@ -0,0 +1,82 @@
// 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.
// TODO(bradfitz,adg): move to util
package godoc
import "io"
var spaces = []byte(" ") // 32 spaces seems like a good number
const (
indenting = iota
collecting
)
// A tconv is an io.Writer filter for converting leading tabs into spaces.
type tconv struct {
output io.Writer
state int // indenting or collecting
indent int // valid if state == indenting
p *Presentation
}
func (p *tconv) writeIndent() (err error) {
i := p.indent
for i >= len(spaces) {
i -= len(spaces)
if _, err = p.output.Write(spaces); err != nil {
return
}
}
// i < len(spaces)
if i > 0 {
_, err = p.output.Write(spaces[0:i])
}
return
}
func (p *tconv) Write(data []byte) (n int, err error) {
if len(data) == 0 {
return
}
pos := 0 // valid if p.state == collecting
var b byte
for n, b = range data {
switch p.state {
case indenting:
switch b {
case '\t':
p.indent += p.p.TabWidth
case '\n':
p.indent = 0
if _, err = p.output.Write(data[n : n+1]); err != nil {
return
}
case ' ':
p.indent++
default:
p.state = collecting
pos = n
if err = p.writeIndent(); err != nil {
return
}
}
case collecting:
if b == '\n' {
p.state = indenting
p.indent = 0
if _, err = p.output.Write(data[pos : n+1]); err != nil {
return
}
}
}
}
n = len(data)
if pos < n && p.state == collecting {
_, err = p.output.Write(data[pos:])
}
return
}

Просмотреть файл

@ -0,0 +1,179 @@
// Copyright 2011 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.
// Template support for writing HTML documents.
// Documents that include Template: true in their
// metadata are executed as input to text/template.
//
// This file defines functions for those templates to invoke.
// The template uses the function "code" to inject program
// source into the output by extracting code from files and
// injecting them as HTML-escaped <pre> blocks.
//
// The syntax is simple: 1, 2, or 3 space-separated arguments:
//
// Whole file:
// {{code "foo.go"}}
// One line (here the signature of main):
// {{code "foo.go" `/^func.main/`}}
// Block of text, determined by start and end (here the body of main):
// {{code "foo.go" `/^func.main/` `/^}/`
//
// Patterns can be `/regular expression/`, a decimal number, or "$"
// to signify the end of the file. In multi-line matches,
// lines that end with the four characters
// OMIT
// are omitted from the output, making it easy to provide marker
// lines in the input that will not appear in the output but are easy
// to identify by pattern.
package godoc
import (
"bytes"
"fmt"
"log"
"regexp"
"strings"
"golang.org/x/website/cmd/golangorg/godoc/vfs"
)
// Functions in this file panic on error, but the panic is recovered
// to an error by 'code'.
// contents reads and returns the content of the named file
// (from the virtual file system, so for example /doc refers to $GOROOT/doc).
func (c *Corpus) contents(name string) string {
file, err := vfs.ReadFile(c.fs, name)
if err != nil {
log.Panic(err)
}
return string(file)
}
// stringFor returns a textual representation of the arg, formatted according to its nature.
func stringFor(arg interface{}) string {
switch arg := arg.(type) {
case int:
return fmt.Sprintf("%d", arg)
case string:
if len(arg) > 2 && arg[0] == '/' && arg[len(arg)-1] == '/' {
return fmt.Sprintf("%#q", arg)
}
return fmt.Sprintf("%q", arg)
default:
log.Panicf("unrecognized argument: %v type %T", arg, arg)
}
return ""
}
func (p *Presentation) code(file string, arg ...interface{}) (s string, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("%v", r)
}
}()
text := p.Corpus.contents(file)
var command string
switch len(arg) {
case 0:
// text is already whole file.
command = fmt.Sprintf("code %q", file)
case 1:
command = fmt.Sprintf("code %q %s", file, stringFor(arg[0]))
text = p.Corpus.oneLine(file, text, arg[0])
case 2:
command = fmt.Sprintf("code %q %s %s", file, stringFor(arg[0]), stringFor(arg[1]))
text = p.Corpus.multipleLines(file, text, arg[0], arg[1])
default:
return "", fmt.Errorf("incorrect code invocation: code %q [%v, ...] (%d arguments)", file, arg[0], len(arg))
}
// Trim spaces from output.
text = strings.Trim(text, "\n")
// Replace tabs by spaces, which work better in HTML.
text = strings.Replace(text, "\t", " ", -1)
var buf bytes.Buffer
// HTML-escape text and syntax-color comments like elsewhere.
FormatText(&buf, []byte(text), -1, true, "", nil)
// Include the command as a comment.
text = fmt.Sprintf("<pre><!--{{%s}}\n-->%s</pre>", command, buf.Bytes())
return text, nil
}
// parseArg returns the integer or string value of the argument and tells which it is.
func parseArg(arg interface{}, file string, max int) (ival int, sval string, isInt bool) {
switch n := arg.(type) {
case int:
if n <= 0 || n > max {
log.Panicf("%q:%d is out of range", file, n)
}
return n, "", true
case string:
return 0, n, false
}
log.Panicf("unrecognized argument %v type %T", arg, arg)
return
}
// oneLine returns the single line generated by a two-argument code invocation.
func (c *Corpus) oneLine(file, text string, arg interface{}) string {
lines := strings.SplitAfter(c.contents(file), "\n")
line, pattern, isInt := parseArg(arg, file, len(lines))
if isInt {
return lines[line-1]
}
return lines[match(file, 0, lines, pattern)-1]
}
// multipleLines returns the text generated by a three-argument code invocation.
func (c *Corpus) multipleLines(file, text string, arg1, arg2 interface{}) string {
lines := strings.SplitAfter(c.contents(file), "\n")
line1, pattern1, isInt1 := parseArg(arg1, file, len(lines))
line2, pattern2, isInt2 := parseArg(arg2, file, len(lines))
if !isInt1 {
line1 = match(file, 0, lines, pattern1)
}
if !isInt2 {
line2 = match(file, line1, lines, pattern2)
} else if line2 < line1 {
log.Panicf("lines out of order for %q: %d %d", text, line1, line2)
}
for k := line1 - 1; k < line2; k++ {
if strings.HasSuffix(lines[k], "OMIT\n") {
lines[k] = ""
}
}
return strings.Join(lines[line1-1:line2], "")
}
// match identifies the input line that matches the pattern in a code invocation.
// If start>0, match lines starting there rather than at the beginning.
// The return value is 1-indexed.
func match(file string, start int, lines []string, pattern string) int {
// $ matches the end of the file.
if pattern == "$" {
if len(lines) == 0 {
log.Panicf("%q: empty file", file)
}
return len(lines)
}
// /regexp/ matches the line that matches the regexp.
if len(pattern) > 2 && pattern[0] == '/' && pattern[len(pattern)-1] == '/' {
re, err := regexp.Compile(pattern[1 : len(pattern)-1])
if err != nil {
log.Panic(err)
}
for i := start; i < len(lines); i++ {
if re.MatchString(lines[i]) {
return i + 1
}
}
log.Panicf("%s: no match for %#q", file, pattern)
}
log.Panicf("unrecognized pattern: %q", pattern)
return 0
}

Просмотреть файл

@ -0,0 +1,88 @@
// Copyright 2011 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 util
import "time"
// A Throttle permits throttling of a goroutine by
// calling the Throttle method repeatedly.
//
type Throttle struct {
f float64 // f = (1-r)/r for 0 < r < 1
dt time.Duration // minimum run time slice; >= 0
tr time.Duration // accumulated time running
ts time.Duration // accumulated time stopped
tt time.Time // earliest throttle time (= time Throttle returned + tm)
}
// NewThrottle creates a new Throttle with a throttle value r and
// a minimum allocated run time slice of dt:
//
// r == 0: "empty" throttle; the goroutine is always sleeping
// r == 1: full throttle; the goroutine is never sleeping
//
// A value of r == 0.6 throttles a goroutine such that it runs
// approx. 60% of the time, and sleeps approx. 40% of the time.
// Values of r < 0 or r > 1 are clamped down to values between 0 and 1.
// Values of dt < 0 are set to 0.
//
func NewThrottle(r float64, dt time.Duration) *Throttle {
var f float64
switch {
case r <= 0:
f = -1 // indicates always sleep
case r >= 1:
f = 0 // assume r == 1 (never sleep)
default:
// 0 < r < 1
f = (1 - r) / r
}
if dt < 0 {
dt = 0
}
return &Throttle{f: f, dt: dt, tt: time.Now().Add(dt)}
}
// Throttle calls time.Sleep such that over time the ratio tr/ts between
// accumulated run (tr) and sleep times (ts) approximates the value 1/(1-r)
// where r is the throttle value. Throttle returns immediately (w/o sleeping)
// if less than tm ns have passed since the last call to Throttle.
//
func (p *Throttle) Throttle() {
if p.f < 0 {
select {} // always sleep
}
t0 := time.Now()
if t0.Before(p.tt) {
return // keep running (minimum time slice not exhausted yet)
}
// accumulate running time
p.tr += t0.Sub(p.tt) + p.dt
// compute sleep time
// Over time we want:
//
// tr/ts = r/(1-r)
//
// Thus:
//
// ts = tr*f with f = (1-r)/r
//
// After some incremental run time δr added to the total run time
// tr, the incremental sleep-time δs to get to the same ratio again
// after waking up from time.Sleep is:
if δs := time.Duration(float64(p.tr)*p.f) - p.ts; δs > 0 {
time.Sleep(δs)
}
// accumulate (actual) sleep time
t1 := time.Now()
p.ts += t1.Sub(t0)
// set earliest next throttle time
p.tt = t1.Add(p.dt)
}

Просмотреть файл

@ -0,0 +1,89 @@
// 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.
// Package util contains utility types and functions for godoc.
package util // import "golang.org/x/website/cmd/golangorg/godoc/util"
import (
pathpkg "path"
"sync"
"time"
"unicode/utf8"
"golang.org/x/website/cmd/golangorg/godoc/vfs"
)
// An RWValue wraps a value and permits mutually exclusive
// access to it and records the time the value was last set.
type RWValue struct {
mutex sync.RWMutex
value interface{}
timestamp time.Time // time of last set()
}
func (v *RWValue) Set(value interface{}) {
v.mutex.Lock()
v.value = value
v.timestamp = time.Now()
v.mutex.Unlock()
}
func (v *RWValue) Get() (interface{}, time.Time) {
v.mutex.RLock()
defer v.mutex.RUnlock()
return v.value, v.timestamp
}
// IsText reports whether a significant prefix of s looks like correct UTF-8;
// that is, if it is likely that s is human-readable text.
func IsText(s []byte) bool {
const max = 1024 // at least utf8.UTFMax
if len(s) > max {
s = s[0:max]
}
for i, c := range string(s) {
if i+utf8.UTFMax > len(s) {
// last char may be incomplete - ignore
break
}
if c == 0xFFFD || c < ' ' && c != '\n' && c != '\t' && c != '\f' {
// decoding error or control character - not a text file
return false
}
}
return true
}
// textExt[x] is true if the extension x indicates a text file, and false otherwise.
var textExt = map[string]bool{
".css": false, // must be served raw
".js": false, // must be served raw
}
// IsTextFile reports whether the file has a known extension indicating
// a text file, or if a significant chunk of the specified file looks like
// correct UTF-8; that is, if it is likely that the file contains human-
// readable text.
func IsTextFile(fs vfs.Opener, filename string) bool {
// if the extension is known, use it for decision making
if isText, found := textExt[pathpkg.Ext(filename)]; found {
return isText
}
// the extension is not known; read an initial chunk
// of the file and check if it looks like text
f, err := fs.Open(filename)
if err != nil {
return false
}
defer f.Close()
var buf [1024]byte
n, err := f.Read(buf[0:])
if err != nil {
return false
}
return IsText(buf[0:n])
}

Просмотреть файл

@ -0,0 +1,224 @@
// Copyright 2018 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 caches information about which standard library types, methods,
// and functions appeared in what version of Go
package godoc
import (
"bufio"
"go/build"
"log"
"os"
"path/filepath"
"strings"
"unicode"
)
// apiVersions is a map of packages to information about those packages'
// symbols and when they were added to Go.
//
// Only things added after Go1 are tracked. Version strings are of the
// form "1.1", "1.2", etc.
type apiVersions map[string]pkgAPIVersions // keyed by Go package ("net/http")
// pkgAPIVersions contains information about which version of Go added
// certain package symbols.
//
// Only things added after Go1 are tracked. Version strings are of the
// form "1.1", "1.2", etc.
type pkgAPIVersions struct {
typeSince map[string]string // "Server" -> "1.7"
methodSince map[string]map[string]string // "*Server" ->"Shutdown"->1.8
funcSince map[string]string // "NewServer" -> "1.7"
fieldSince map[string]map[string]string // "ClientTrace" -> "Got1xxResponse" -> "1.11"
}
// sinceVersionFunc returns a string (such as "1.7") specifying which Go
// version introduced a symbol, unless it was introduced in Go1, in
// which case it returns the empty string.
//
// The kind is one of "type", "method", or "func".
//
// The receiver is only used for "methods" and specifies the receiver type,
// such as "*Server".
//
// The name is the symbol name ("Server") and the pkg is the package
// ("net/http").
func (v apiVersions) sinceVersionFunc(kind, receiver, name, pkg string) string {
pv := v[pkg]
switch kind {
case "func":
return pv.funcSince[name]
case "type":
return pv.typeSince[name]
case "method":
return pv.methodSince[receiver][name]
}
return ""
}
// versionedRow represents an API feature, a parsed line of a
// $GOROOT/api/go.*txt file.
type versionedRow struct {
pkg string // "net/http"
kind string // "type", "func", "method", "field" TODO: "const", "var"
recv string // for methods, the receiver type ("Server", "*Server")
name string // name of type, (struct) field, func, method
structName string // for struct fields, the outer struct name
}
// versionParser parses $GOROOT/api/go*.txt files and stores them in in its rows field.
type versionParser struct {
res apiVersions // initialized lazily
}
func (vp *versionParser) parseFile(name string) error {
base := filepath.Base(name)
ver := strings.TrimPrefix(strings.TrimSuffix(base, ".txt"), "go")
if ver == "1" {
return nil
}
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close()
sc := bufio.NewScanner(f)
for sc.Scan() {
row, ok := parseRow(sc.Text())
if !ok {
continue
}
if vp.res == nil {
vp.res = make(apiVersions)
}
pkgi, ok := vp.res[row.pkg]
if !ok {
pkgi = pkgAPIVersions{
typeSince: make(map[string]string),
methodSince: make(map[string]map[string]string),
funcSince: make(map[string]string),
fieldSince: make(map[string]map[string]string),
}
vp.res[row.pkg] = pkgi
}
switch row.kind {
case "func":
pkgi.funcSince[row.name] = ver
case "type":
pkgi.typeSince[row.name] = ver
case "method":
if _, ok := pkgi.methodSince[row.recv]; !ok {
pkgi.methodSince[row.recv] = make(map[string]string)
}
pkgi.methodSince[row.recv][row.name] = ver
case "field":
if _, ok := pkgi.fieldSince[row.structName]; !ok {
pkgi.fieldSince[row.structName] = make(map[string]string)
}
pkgi.fieldSince[row.structName][row.name] = ver
}
}
return sc.Err()
}
func parseRow(s string) (vr versionedRow, ok bool) {
if !strings.HasPrefix(s, "pkg ") {
// Skip comments, blank lines, etc.
return
}
rest := s[len("pkg "):]
endPkg := strings.IndexFunc(rest, func(r rune) bool { return !(unicode.IsLetter(r) || r == '/' || unicode.IsDigit(r)) })
if endPkg == -1 {
return
}
vr.pkg, rest = rest[:endPkg], rest[endPkg:]
if !strings.HasPrefix(rest, ", ") {
// If the part after the pkg name isn't ", ", then it's a OS/ARCH-dependent line of the form:
// pkg syscall (darwin-amd64), const ImplementsGetwd = false
// We skip those for now.
return
}
rest = rest[len(", "):]
switch {
case strings.HasPrefix(rest, "type "):
rest = rest[len("type "):]
sp := strings.IndexByte(rest, ' ')
if sp == -1 {
return
}
vr.name, rest = rest[:sp], rest[sp+1:]
if !strings.HasPrefix(rest, "struct, ") {
vr.kind = "type"
return vr, true
}
rest = rest[len("struct, "):]
if i := strings.IndexByte(rest, ' '); i != -1 {
vr.kind = "field"
vr.structName = vr.name
vr.name = rest[:i]
return vr, true
}
case strings.HasPrefix(rest, "func "):
vr.kind = "func"
rest = rest[len("func "):]
if i := strings.IndexByte(rest, '('); i != -1 {
vr.name = rest[:i]
return vr, true
}
case strings.HasPrefix(rest, "method "): // "method (*File) SetModTime(time.Time)"
vr.kind = "method"
rest = rest[len("method "):] // "(*File) SetModTime(time.Time)"
sp := strings.IndexByte(rest, ' ')
if sp == -1 {
return
}
vr.recv = strings.Trim(rest[:sp], "()") // "*File"
rest = rest[sp+1:] // SetMode(os.FileMode)
paren := strings.IndexByte(rest, '(')
if paren == -1 {
return
}
vr.name = rest[:paren]
return vr, true
}
return // TODO: handle more cases
}
// InitVersionInfo parses the $GOROOT/api/go*.txt API definition files to discover
// which API features were added in which Go releases.
func (c *Corpus) InitVersionInfo() {
var err error
c.pkgAPIInfo, err = parsePackageAPIInfo()
if err != nil {
// TODO: consider making this fatal, after the Go 1.11 cycle.
log.Printf("godoc: error parsing API version files: %v", err)
}
}
func parsePackageAPIInfo() (apiVersions, error) {
var apiGlob string
if os.Getenv("GOROOT") == "" {
apiGlob = filepath.Join(build.Default.GOROOT, "api", "go*.txt")
} else {
apiGlob = filepath.Join(os.Getenv("GOROOT"), "api", "go*.txt")
}
files, err := filepath.Glob(apiGlob)
if err != nil {
return nil, err
}
vp := new(versionParser)
for _, f := range files {
if err := vp.parseFile(f); err != nil {
return nil, err
}
}
return vp.res, nil
}

Просмотреть файл

@ -0,0 +1,132 @@
// Copyright 2018 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 godoc
import (
"go/build"
"testing"
)
func TestParseVersionRow(t *testing.T) {
tests := []struct {
row string
want versionedRow
}{
{
row: "# comment",
},
{
row: "",
},
{
row: "pkg archive/tar, type Writer struct",
want: versionedRow{
pkg: "archive/tar",
kind: "type",
name: "Writer",
},
},
{
row: "pkg archive/tar, type Header struct, AccessTime time.Time",
want: versionedRow{
pkg: "archive/tar",
kind: "field",
structName: "Header",
name: "AccessTime",
},
},
{
row: "pkg archive/tar, method (*Reader) Read([]uint8) (int, error)",
want: versionedRow{
pkg: "archive/tar",
kind: "method",
name: "Read",
recv: "*Reader",
},
},
{
row: "pkg archive/zip, func FileInfoHeader(os.FileInfo) (*FileHeader, error)",
want: versionedRow{
pkg: "archive/zip",
kind: "func",
name: "FileInfoHeader",
},
},
{
row: "pkg encoding/base32, method (Encoding) WithPadding(int32) *Encoding",
want: versionedRow{
pkg: "encoding/base32",
kind: "method",
name: "WithPadding",
recv: "Encoding",
},
},
}
for i, tt := range tests {
got, ok := parseRow(tt.row)
if !ok {
got = versionedRow{}
}
if got != tt.want {
t.Errorf("%d. parseRow(%q) = %+v; want %+v", i, tt.row, got, tt.want)
}
}
}
// hasTag checks whether a given release tag is contained in the current version
// of the go binary.
func hasTag(t string) bool {
for _, v := range build.Default.ReleaseTags {
if t == v {
return true
}
}
return false
}
func TestAPIVersion(t *testing.T) {
av, err := parsePackageAPIInfo()
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
kind string
pkg string
name string
receiver string
want string
}{
// Things that were added post-1.0 should appear
{"func", "archive/tar", "FileInfoHeader", "", "1.1"},
{"type", "bufio", "Scanner", "", "1.1"},
{"method", "bufio", "WriteTo", "*Reader", "1.1"},
{"func", "bytes", "LastIndexByte", "", "1.5"},
{"type", "crypto", "Decrypter", "", "1.5"},
{"method", "crypto/rsa", "Decrypt", "*PrivateKey", "1.5"},
{"method", "debug/dwarf", "GoString", "Class", "1.5"},
{"func", "os", "IsTimeout", "", "1.10"},
{"type", "strings", "Builder", "", "1.10"},
{"method", "strings", "WriteString", "*Builder", "1.10"},
// Things from package syscall should never appear
{"func", "syscall", "FchFlags", "", ""},
{"type", "syscall", "Inet4Pktinfo", "", ""},
// Things added in Go 1 should never appear
{"func", "archive/tar", "NewReader", "", ""},
{"type", "archive/tar", "Header", "", ""},
{"method", "archive/tar", "Next", "*Reader", ""},
} {
if tc.want != "" && !hasTag("go"+tc.want) {
continue
}
if got := av.sinceVersionFunc(tc.kind, tc.receiver, tc.name, tc.pkg); got != tc.want {
t.Errorf(`sinceFunc("%s", "%s", "%s", "%s") = "%s"; want "%s"`, tc.kind, tc.receiver, tc.name, tc.pkg, got, tc.want)
}
}
}

Просмотреть файл

@ -0,0 +1,89 @@
// Copyright 2016 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 vfs
import (
"fmt"
"os"
"time"
)
// NewNameSpace returns a NameSpace pre-initialized with an empty
// emulated directory mounted on the root mount point "/". This
// allows directory traversal routines to work properly even if
// a folder is not explicitly mounted at root by the user.
func NewNameSpace() NameSpace {
ns := NameSpace{}
ns.Bind("/", &emptyVFS{}, "/", BindReplace)
return ns
}
// type emptyVFS emulates a FileSystem consisting of an empty directory
type emptyVFS struct{}
// Open implements Opener. Since emptyVFS is an empty directory, all
// attempts to open a file should returns errors.
func (e *emptyVFS) Open(path string) (ReadSeekCloser, error) {
if path == "/" {
return nil, fmt.Errorf("open: / is a directory")
}
return nil, os.ErrNotExist
}
// Stat returns os.FileInfo for an empty directory if the path is
// is root "/" or error. os.FileInfo is implemented by emptyVFS
func (e *emptyVFS) Stat(path string) (os.FileInfo, error) {
if path == "/" {
return e, nil
}
return nil, os.ErrNotExist
}
func (e *emptyVFS) Lstat(path string) (os.FileInfo, error) {
return e.Stat(path)
}
// ReadDir returns an empty os.FileInfo slice for "/", else error.
func (e *emptyVFS) ReadDir(path string) ([]os.FileInfo, error) {
if path == "/" {
return []os.FileInfo{}, nil
}
return nil, os.ErrNotExist
}
func (e *emptyVFS) String() string {
return "emptyVFS(/)"
}
func (e *emptyVFS) RootType(path string) RootType {
return ""
}
// These functions below implement os.FileInfo for the single
// empty emulated directory.
func (e *emptyVFS) Name() string {
return "/"
}
func (e *emptyVFS) Size() int64 {
return 0
}
func (e *emptyVFS) Mode() os.FileMode {
return os.ModeDir | os.ModePerm
}
func (e *emptyVFS) ModTime() time.Time {
return time.Time{}
}
func (e *emptyVFS) IsDir() bool {
return true
}
func (e *emptyVFS) Sys() interface{} {
return nil
}

Просмотреть файл

@ -0,0 +1,58 @@
// Copyright 2016 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 vfs_test
import (
"testing"
"time"
"golang.org/x/website/cmd/golangorg/godoc/vfs"
"golang.org/x/website/cmd/golangorg/godoc/vfs/mapfs"
)
func TestNewNameSpace(t *testing.T) {
// We will mount this filesystem under /fs1
mount := mapfs.New(map[string]string{"fs1file": "abcdefgh"})
// Existing process. This should give error on Stat("/")
t1 := vfs.NameSpace{}
t1.Bind("/fs1", mount, "/", vfs.BindReplace)
// using NewNameSpace. This should work fine.
t2 := vfs.NewNameSpace()
t2.Bind("/fs1", mount, "/", vfs.BindReplace)
testcases := map[string][]bool{
"/": {false, true},
"/fs1": {true, true},
"/fs1/fs1file": {true, true},
}
fss := []vfs.FileSystem{t1, t2}
for j, fs := range fss {
for k, v := range testcases {
_, err := fs.Stat(k)
result := err == nil
if result != v[j] {
t.Errorf("fs: %d, testcase: %s, want: %v, got: %v, err: %s", j, k, v[j], result, err)
}
}
}
fi, err := t2.Stat("/")
if err != nil {
t.Fatal(err)
}
if fi.Name() != "/" {
t.Errorf("t2.Name() : want:%s got:%s", "/", fi.Name())
}
if !fi.ModTime().IsZero() {
t.Errorf("t2.Modime() : want:%v got:%v", time.Time{}, fi.ModTime())
}
}

Просмотреть файл

@ -0,0 +1,93 @@
// 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.
// Package gatefs provides an implementation of the FileSystem
// interface that wraps another FileSystem and limits its concurrency.
package gatefs // import "golang.org/x/website/cmd/golangorg/godoc/vfs/gatefs"
import (
"fmt"
"os"
"golang.org/x/website/cmd/golangorg/godoc/vfs"
)
// New returns a new FileSystem that delegates to fs.
// If gateCh is non-nil and buffered, it's used as a gate
// to limit concurrency on calls to fs.
func New(fs vfs.FileSystem, gateCh chan bool) vfs.FileSystem {
if cap(gateCh) == 0 {
return fs
}
return gatefs{fs, gate(gateCh)}
}
type gate chan bool
func (g gate) enter() { g <- true }
func (g gate) leave() { <-g }
type gatefs struct {
fs vfs.FileSystem
gate
}
func (fs gatefs) String() string {
return fmt.Sprintf("gated(%s, %d)", fs.fs.String(), cap(fs.gate))
}
func (fs gatefs) RootType(path string) vfs.RootType {
return fs.fs.RootType(path)
}
func (fs gatefs) Open(p string) (vfs.ReadSeekCloser, error) {
fs.enter()
defer fs.leave()
rsc, err := fs.fs.Open(p)
if err != nil {
return nil, err
}
return gatef{rsc, fs.gate}, nil
}
func (fs gatefs) Lstat(p string) (os.FileInfo, error) {
fs.enter()
defer fs.leave()
return fs.fs.Lstat(p)
}
func (fs gatefs) Stat(p string) (os.FileInfo, error) {
fs.enter()
defer fs.leave()
return fs.fs.Stat(p)
}
func (fs gatefs) ReadDir(p string) ([]os.FileInfo, error) {
fs.enter()
defer fs.leave()
return fs.fs.ReadDir(p)
}
type gatef struct {
rsc vfs.ReadSeekCloser
gate
}
func (f gatef) Read(p []byte) (n int, err error) {
f.enter()
defer f.leave()
return f.rsc.Read(p)
}
func (f gatef) Seek(offset int64, whence int) (ret int64, err error) {
f.enter()
defer f.leave()
return f.rsc.Seek(offset, whence)
}
func (f gatef) Close() error {
f.enter()
defer f.leave()
return f.rsc.Close()
}

Просмотреть файл

@ -0,0 +1,39 @@
// Copyright 2018 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 gatefs_test
import (
"os"
"runtime"
"testing"
"golang.org/x/website/cmd/golangorg/godoc/vfs"
"golang.org/x/website/cmd/golangorg/godoc/vfs/gatefs"
)
func TestRootType(t *testing.T) {
goPath := os.Getenv("GOPATH")
var expectedType vfs.RootType
if goPath == "" {
expectedType = ""
} else {
expectedType = vfs.RootTypeGoPath
}
tests := []struct {
path string
fsType vfs.RootType
}{
{runtime.GOROOT(), vfs.RootTypeGoRoot},
{goPath, expectedType},
{"/tmp/", ""},
}
for _, item := range tests {
fs := gatefs.New(vfs.OS(item.path), make(chan bool, 1))
if fs.RootType("path") != item.fsType {
t.Errorf("unexpected fsType. Expected- %v, Got- %v", item.fsType, fs.RootType("path"))
}
}
}

Просмотреть файл

@ -0,0 +1,94 @@
// 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.
// Package httpfs implements http.FileSystem using a godoc vfs.FileSystem.
package httpfs // import "golang.org/x/website/cmd/golangorg/godoc/vfs/httpfs"
import (
"fmt"
"io"
"net/http"
"os"
"golang.org/x/website/cmd/golangorg/godoc/vfs"
)
func New(fs vfs.FileSystem) http.FileSystem {
return &httpFS{fs}
}
type httpFS struct {
fs vfs.FileSystem
}
func (h *httpFS) Open(name string) (http.File, error) {
fi, err := h.fs.Stat(name)
if err != nil {
return nil, err
}
if fi.IsDir() {
return &httpDir{h.fs, name, nil}, nil
}
f, err := h.fs.Open(name)
if err != nil {
return nil, err
}
return &httpFile{h.fs, f, name}, nil
}
// httpDir implements http.File for a directory in a FileSystem.
type httpDir struct {
fs vfs.FileSystem
name string
pending []os.FileInfo
}
func (h *httpDir) Close() error { return nil }
func (h *httpDir) Stat() (os.FileInfo, error) { return h.fs.Stat(h.name) }
func (h *httpDir) Read([]byte) (int, error) {
return 0, fmt.Errorf("cannot Read from directory %s", h.name)
}
func (h *httpDir) Seek(offset int64, whence int) (int64, error) {
if offset == 0 && whence == 0 {
h.pending = nil
return 0, nil
}
return 0, fmt.Errorf("unsupported Seek in directory %s", h.name)
}
func (h *httpDir) Readdir(count int) ([]os.FileInfo, error) {
if h.pending == nil {
d, err := h.fs.ReadDir(h.name)
if err != nil {
return nil, err
}
if d == nil {
d = []os.FileInfo{} // not nil
}
h.pending = d
}
if len(h.pending) == 0 && count > 0 {
return nil, io.EOF
}
if count <= 0 || count > len(h.pending) {
count = len(h.pending)
}
d := h.pending[:count]
h.pending = h.pending[count:]
return d, nil
}
// httpFile implements http.File for a file (not directory) in a FileSystem.
type httpFile struct {
fs vfs.FileSystem
vfs.ReadSeekCloser
name string
}
func (h *httpFile) Stat() (os.FileInfo, error) { return h.fs.Stat(h.name) }
func (h *httpFile) Readdir(int) ([]os.FileInfo, error) {
return nil, fmt.Errorf("cannot Readdir from file %s", h.name)
}

Просмотреть файл

@ -0,0 +1,156 @@
// 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.
// Package mapfs file provides an implementation of the FileSystem
// interface based on the contents of a map[string]string.
package mapfs // import "golang.org/x/website/cmd/golangorg/godoc/vfs/mapfs"
import (
"io"
"os"
pathpkg "path"
"sort"
"strings"
"time"
"golang.org/x/website/cmd/golangorg/godoc/vfs"
)
// New returns a new FileSystem from the provided map.
// Map keys should be forward slash-separated pathnames
// and not contain a leading slash.
func New(m map[string]string) vfs.FileSystem {
return mapFS(m)
}
// mapFS is the map based implementation of FileSystem
type mapFS map[string]string
func (fs mapFS) String() string { return "mapfs" }
func (fs mapFS) RootType(p string) vfs.RootType {
return ""
}
func (fs mapFS) Close() error { return nil }
func filename(p string) string {
return strings.TrimPrefix(p, "/")
}
func (fs mapFS) Open(p string) (vfs.ReadSeekCloser, error) {
b, ok := fs[filename(p)]
if !ok {
return nil, os.ErrNotExist
}
return nopCloser{strings.NewReader(b)}, nil
}
func fileInfo(name, contents string) os.FileInfo {
return mapFI{name: pathpkg.Base(name), size: len(contents)}
}
func dirInfo(name string) os.FileInfo {
return mapFI{name: pathpkg.Base(name), dir: true}
}
func (fs mapFS) Lstat(p string) (os.FileInfo, error) {
b, ok := fs[filename(p)]
if ok {
return fileInfo(p, b), nil
}
ents, _ := fs.ReadDir(p)
if len(ents) > 0 {
return dirInfo(p), nil
}
return nil, os.ErrNotExist
}
func (fs mapFS) Stat(p string) (os.FileInfo, error) {
return fs.Lstat(p)
}
// slashdir returns path.Dir(p), but special-cases paths not beginning
// with a slash to be in the root.
func slashdir(p string) string {
d := pathpkg.Dir(p)
if d == "." {
return "/"
}
if strings.HasPrefix(p, "/") {
return d
}
return "/" + d
}
func (fs mapFS) ReadDir(p string) ([]os.FileInfo, error) {
p = pathpkg.Clean(p)
var ents []string
fim := make(map[string]os.FileInfo) // base -> fi
for fn, b := range fs {
dir := slashdir(fn)
isFile := true
var lastBase string
for {
if dir == p {
base := lastBase
if isFile {
base = pathpkg.Base(fn)
}
if fim[base] == nil {
var fi os.FileInfo
if isFile {
fi = fileInfo(fn, b)
} else {
fi = dirInfo(base)
}
ents = append(ents, base)
fim[base] = fi
}
}
if dir == "/" {
break
} else {
isFile = false
lastBase = pathpkg.Base(dir)
dir = pathpkg.Dir(dir)
}
}
}
if len(ents) == 0 {
return nil, os.ErrNotExist
}
sort.Strings(ents)
var list []os.FileInfo
for _, dir := range ents {
list = append(list, fim[dir])
}
return list, nil
}
// mapFI is the map-based implementation of FileInfo.
type mapFI struct {
name string
size int
dir bool
}
func (fi mapFI) IsDir() bool { return fi.dir }
func (fi mapFI) ModTime() time.Time { return time.Time{} }
func (fi mapFI) Mode() os.FileMode {
if fi.IsDir() {
return 0755 | os.ModeDir
}
return 0444
}
func (fi mapFI) Name() string { return pathpkg.Base(fi.name) }
func (fi mapFI) Size() int64 { return int64(fi.size) }
func (fi mapFI) Sys() interface{} { return nil }
type nopCloser struct {
io.ReadSeeker
}
func (nc nopCloser) Close() error { return nil }

Просмотреть файл

@ -0,0 +1,111 @@
// 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.
package mapfs
import (
"io/ioutil"
"os"
"reflect"
"testing"
)
func TestOpenRoot(t *testing.T) {
fs := New(map[string]string{
"foo/bar/three.txt": "a",
"foo/bar.txt": "b",
"top.txt": "c",
"other-top.txt": "d",
})
tests := []struct {
path string
want string
}{
{"/foo/bar/three.txt", "a"},
{"foo/bar/three.txt", "a"},
{"foo/bar.txt", "b"},
{"top.txt", "c"},
{"/top.txt", "c"},
{"other-top.txt", "d"},
{"/other-top.txt", "d"},
}
for _, tt := range tests {
rsc, err := fs.Open(tt.path)
if err != nil {
t.Errorf("Open(%q) = %v", tt.path, err)
continue
}
slurp, err := ioutil.ReadAll(rsc)
if err != nil {
t.Error(err)
}
if string(slurp) != tt.want {
t.Errorf("Read(%q) = %q; want %q", tt.path, tt.want, slurp)
}
rsc.Close()
}
_, err := fs.Open("/xxxx")
if !os.IsNotExist(err) {
t.Errorf("ReadDir /xxxx = %v; want os.IsNotExist error", err)
}
}
func TestReaddir(t *testing.T) {
fs := New(map[string]string{
"foo/bar/three.txt": "333",
"foo/bar.txt": "22",
"top.txt": "top.txt file",
"other-top.txt": "other-top.txt file",
})
tests := []struct {
dir string
want []os.FileInfo
}{
{
dir: "/",
want: []os.FileInfo{
mapFI{name: "foo", dir: true},
mapFI{name: "other-top.txt", size: len("other-top.txt file")},
mapFI{name: "top.txt", size: len("top.txt file")},
},
},
{
dir: "/foo",
want: []os.FileInfo{
mapFI{name: "bar", dir: true},
mapFI{name: "bar.txt", size: 2},
},
},
{
dir: "/foo/",
want: []os.FileInfo{
mapFI{name: "bar", dir: true},
mapFI{name: "bar.txt", size: 2},
},
},
{
dir: "/foo/bar",
want: []os.FileInfo{
mapFI{name: "three.txt", size: 3},
},
},
}
for _, tt := range tests {
fis, err := fs.ReadDir(tt.dir)
if err != nil {
t.Errorf("ReadDir(%q) = %v", tt.dir, err)
continue
}
if !reflect.DeepEqual(fis, tt.want) {
t.Errorf("ReadDir(%q) = %#v; want %#v", tt.dir, fis, tt.want)
continue
}
}
_, err := fs.ReadDir("/xxxx")
if !os.IsNotExist(err) {
t.Errorf("ReadDir /xxxx = %v; want os.IsNotExist error", err)
}
}

Просмотреть файл

@ -0,0 +1,403 @@
// Copyright 2011 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 vfs
import (
"fmt"
"io"
"os"
pathpkg "path"
"sort"
"strings"
"time"
)
// Setting debugNS = true will enable debugging prints about
// name space translations.
const debugNS = false
// A NameSpace is a file system made up of other file systems
// mounted at specific locations in the name space.
//
// The representation is a map from mount point locations
// to the list of file systems mounted at that location. A traditional
// Unix mount table would use a single file system per mount point,
// but we want to be able to mount multiple file systems on a single
// mount point and have the system behave as if the union of those
// file systems were present at the mount point.
// For example, if the OS file system has a Go installation in
// c:\Go and additional Go path trees in d:\Work1 and d:\Work2, then
// this name space creates the view we want for the godoc server:
//
// NameSpace{
// "/": {
// {old: "/", fs: OS(`c:\Go`), new: "/"},
// },
// "/src/pkg": {
// {old: "/src/pkg", fs: OS(`c:\Go`), new: "/src/pkg"},
// {old: "/src/pkg", fs: OS(`d:\Work1`), new: "/src"},
// {old: "/src/pkg", fs: OS(`d:\Work2`), new: "/src"},
// },
// }
//
// This is created by executing:
//
// ns := NameSpace{}
// ns.Bind("/", OS(`c:\Go`), "/", BindReplace)
// ns.Bind("/src/pkg", OS(`d:\Work1`), "/src", BindAfter)
// ns.Bind("/src/pkg", OS(`d:\Work2`), "/src", BindAfter)
//
// A particular mount point entry is a triple (old, fs, new), meaning that to
// operate on a path beginning with old, replace that prefix (old) with new
// and then pass that path to the FileSystem implementation fs.
//
// If you do not explicitly mount a FileSystem at the root mountpoint "/" of the
// NameSpace like above, Stat("/") will return a "not found" error which could
// break typical directory traversal routines. In such cases, use NewNameSpace()
// to get a NameSpace pre-initialized with an emulated empty directory at root.
//
// Given this name space, a ReadDir of /src/pkg/code will check each prefix
// of the path for a mount point (first /src/pkg/code, then /src/pkg, then /src,
// then /), stopping when it finds one. For the above example, /src/pkg/code
// will find the mount point at /src/pkg:
//
// {old: "/src/pkg", fs: OS(`c:\Go`), new: "/src/pkg"},
// {old: "/src/pkg", fs: OS(`d:\Work1`), new: "/src"},
// {old: "/src/pkg", fs: OS(`d:\Work2`), new: "/src"},
//
// ReadDir will when execute these three calls and merge the results:
//
// OS(`c:\Go`).ReadDir("/src/pkg/code")
// OS(`d:\Work1').ReadDir("/src/code")
// OS(`d:\Work2').ReadDir("/src/code")
//
// Note that the "/src/pkg" in "/src/pkg/code" has been replaced by
// just "/src" in the final two calls.
//
// OS is itself an implementation of a file system: it implements
// OS(`c:\Go`).ReadDir("/src/pkg/code") as ioutil.ReadDir(`c:\Go\src\pkg\code`).
//
// Because the new path is evaluated by fs (here OS(root)), another way
// to read the mount table is to mentally combine fs+new, so that this table:
//
// {old: "/src/pkg", fs: OS(`c:\Go`), new: "/src/pkg"},
// {old: "/src/pkg", fs: OS(`d:\Work1`), new: "/src"},
// {old: "/src/pkg", fs: OS(`d:\Work2`), new: "/src"},
//
// reads as:
//
// "/src/pkg" -> c:\Go\src\pkg
// "/src/pkg" -> d:\Work1\src
// "/src/pkg" -> d:\Work2\src
//
// An invariant (a redundancy) of the name space representation is that
// ns[mtpt][i].old is always equal to mtpt (in the example, ns["/src/pkg"]'s
// mount table entries always have old == "/src/pkg"). The 'old' field is
// useful to callers, because they receive just a []mountedFS and not any
// other indication of which mount point was found.
//
type NameSpace map[string][]mountedFS
// A mountedFS handles requests for path by replacing
// a prefix 'old' with 'new' and then calling the fs methods.
type mountedFS struct {
old string
fs FileSystem
new string
}
// hasPathPrefix returns true if x == y or x == y + "/" + more
func hasPathPrefix(x, y string) bool {
return x == y || strings.HasPrefix(x, y) && (strings.HasSuffix(y, "/") || strings.HasPrefix(x[len(y):], "/"))
}
// translate translates path for use in m, replacing old with new.
//
// mountedFS{"/src/pkg", fs, "/src"}.translate("/src/pkg/code") == "/src/code".
func (m mountedFS) translate(path string) string {
path = pathpkg.Clean("/" + path)
if !hasPathPrefix(path, m.old) {
panic("translate " + path + " but old=" + m.old)
}
return pathpkg.Join(m.new, path[len(m.old):])
}
func (NameSpace) String() string {
return "ns"
}
// Fprint writes a text representation of the name space to w.
func (ns NameSpace) Fprint(w io.Writer) {
fmt.Fprint(w, "name space {\n")
var all []string
for mtpt := range ns {
all = append(all, mtpt)
}
sort.Strings(all)
for _, mtpt := range all {
fmt.Fprintf(w, "\t%s:\n", mtpt)
for _, m := range ns[mtpt] {
fmt.Fprintf(w, "\t\t%s %s\n", m.fs, m.new)
}
}
fmt.Fprint(w, "}\n")
}
// clean returns a cleaned, rooted path for evaluation.
// It canonicalizes the path so that we can use string operations
// to analyze it.
func (NameSpace) clean(path string) string {
return pathpkg.Clean("/" + path)
}
type BindMode int
const (
BindReplace BindMode = iota
BindBefore
BindAfter
)
// Bind causes references to old to redirect to the path new in newfs.
// If mode is BindReplace, old redirections are discarded.
// If mode is BindBefore, this redirection takes priority over existing ones,
// but earlier ones are still consulted for paths that do not exist in newfs.
// If mode is BindAfter, this redirection happens only after existing ones
// have been tried and failed.
func (ns NameSpace) Bind(old string, newfs FileSystem, new string, mode BindMode) {
old = ns.clean(old)
new = ns.clean(new)
m := mountedFS{old, newfs, new}
var mtpt []mountedFS
switch mode {
case BindReplace:
mtpt = append(mtpt, m)
case BindAfter:
mtpt = append(mtpt, ns.resolve(old)...)
mtpt = append(mtpt, m)
case BindBefore:
mtpt = append(mtpt, m)
mtpt = append(mtpt, ns.resolve(old)...)
}
// Extend m.old, m.new in inherited mount point entries.
for i := range mtpt {
m := &mtpt[i]
if m.old != old {
if !hasPathPrefix(old, m.old) {
// This should not happen. If it does, panic so
// that we can see the call trace that led to it.
panic(fmt.Sprintf("invalid Bind: old=%q m={%q, %s, %q}", old, m.old, m.fs.String(), m.new))
}
suffix := old[len(m.old):]
m.old = pathpkg.Join(m.old, suffix)
m.new = pathpkg.Join(m.new, suffix)
}
}
ns[old] = mtpt
}
// resolve resolves a path to the list of mountedFS to use for path.
func (ns NameSpace) resolve(path string) []mountedFS {
path = ns.clean(path)
for {
if m := ns[path]; m != nil {
if debugNS {
fmt.Printf("resolve %s: %v\n", path, m)
}
return m
}
if path == "/" {
break
}
path = pathpkg.Dir(path)
}
return nil
}
// Open implements the FileSystem Open method.
func (ns NameSpace) Open(path string) (ReadSeekCloser, error) {
var err error
for _, m := range ns.resolve(path) {
if debugNS {
fmt.Printf("tx %s: %v\n", path, m.translate(path))
}
tp := m.translate(path)
r, err1 := m.fs.Open(tp)
if err1 == nil {
return r, nil
}
// IsNotExist errors in overlay FSes can mask real errors in
// the underlying FS, so ignore them if there is another error.
if err == nil || os.IsNotExist(err) {
err = err1
}
}
if err == nil {
err = &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist}
}
return nil, err
}
// stat implements the FileSystem Stat and Lstat methods.
func (ns NameSpace) stat(path string, f func(FileSystem, string) (os.FileInfo, error)) (os.FileInfo, error) {
var err error
for _, m := range ns.resolve(path) {
fi, err1 := f(m.fs, m.translate(path))
if err1 == nil {
return fi, nil
}
if err == nil {
err = err1
}
}
if err == nil {
err = &os.PathError{Op: "stat", Path: path, Err: os.ErrNotExist}
}
return nil, err
}
func (ns NameSpace) Stat(path string) (os.FileInfo, error) {
return ns.stat(path, FileSystem.Stat)
}
func (ns NameSpace) Lstat(path string) (os.FileInfo, error) {
return ns.stat(path, FileSystem.Lstat)
}
// dirInfo is a trivial implementation of os.FileInfo for a directory.
type dirInfo string
func (d dirInfo) Name() string { return string(d) }
func (d dirInfo) Size() int64 { return 0 }
func (d dirInfo) Mode() os.FileMode { return os.ModeDir | 0555 }
func (d dirInfo) ModTime() time.Time { return startTime }
func (d dirInfo) IsDir() bool { return true }
func (d dirInfo) Sys() interface{} { return nil }
var startTime = time.Now()
// ReadDir implements the FileSystem ReadDir method. It's where most of the magic is.
// (The rest is in resolve.)
//
// Logically, ReadDir must return the union of all the directories that are named
// by path. In order to avoid misinterpreting Go packages, of all the directories
// that contain Go source code, we only include the files from the first,
// but we include subdirectories from all.
//
// ReadDir must also return directory entries needed to reach mount points.
// If the name space looks like the example in the type NameSpace comment,
// but c:\Go does not have a src/pkg subdirectory, we still want to be able
// to find that subdirectory, because we've mounted d:\Work1 and d:\Work2
// there. So if we don't see "src" in the directory listing for c:\Go, we add an
// entry for it before returning.
//
func (ns NameSpace) ReadDir(path string) ([]os.FileInfo, error) {
path = ns.clean(path)
var (
haveGo = false
haveName = map[string]bool{}
all []os.FileInfo
err error
first []os.FileInfo
)
for _, m := range ns.resolve(path) {
dir, err1 := m.fs.ReadDir(m.translate(path))
if err1 != nil {
if err == nil {
err = err1
}
continue
}
if dir == nil {
dir = []os.FileInfo{}
}
if first == nil {
first = dir
}
// If we don't yet have Go files in 'all' and this directory
// has some, add all the files from this directory.
// Otherwise, only add subdirectories.
useFiles := false
if !haveGo {
for _, d := range dir {
if strings.HasSuffix(d.Name(), ".go") {
useFiles = true
haveGo = true
break
}
}
}
for _, d := range dir {
name := d.Name()
if (d.IsDir() || useFiles) && !haveName[name] {
haveName[name] = true
all = append(all, d)
}
}
}
// We didn't find any directories containing Go files.
// If some directory returned successfully, use that.
if !haveGo {
for _, d := range first {
if !haveName[d.Name()] {
haveName[d.Name()] = true
all = append(all, d)
}
}
}
// Built union. Add any missing directories needed to reach mount points.
for old := range ns {
if hasPathPrefix(old, path) && old != path {
// Find next element after path in old.
elem := old[len(path):]
elem = strings.TrimPrefix(elem, "/")
if i := strings.Index(elem, "/"); i >= 0 {
elem = elem[:i]
}
if !haveName[elem] {
haveName[elem] = true
all = append(all, dirInfo(elem))
}
}
}
if len(all) == 0 {
return nil, err
}
sort.Sort(byName(all))
return all, nil
}
// RootType returns the RootType for the given path in the namespace.
func (ns NameSpace) RootType(path string) RootType {
// We resolve the given path to a list of mountedFS and then return
// the root type for the filesystem which contains the path.
for _, m := range ns.resolve(path) {
_, err := m.fs.ReadDir(m.translate(path))
// Found a match, return the filesystem's root type
if err == nil {
return m.fs.RootType(path)
}
}
return ""
}
// byName implements sort.Interface.
type byName []os.FileInfo
func (f byName) Len() int { return len(f) }
func (f byName) Less(i, j int) bool { return f[i].Name() < f[j].Name() }
func (f byName) Swap(i, j int) { f[i], f[j] = f[j], f[i] }

Просмотреть файл

@ -0,0 +1,105 @@
// 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.
package vfs
import (
"fmt"
"go/build"
"io/ioutil"
"os"
pathpkg "path"
"path/filepath"
"runtime"
)
// We expose a new variable because otherwise we need to copy the findGOROOT logic again
// from cmd/godoc which is already copied twice from the standard library.
// GOROOT is the GOROOT path under which the godoc binary is running.
// It is needed to check whether a filesystem root is under GOROOT or not.
// This is set from cmd/godoc/main.go.
var GOROOT = runtime.GOROOT()
// OS returns an implementation of FileSystem reading from the
// tree rooted at root. Recording a root is convenient everywhere
// but necessary on Windows, because the slash-separated path
// passed to Open has no way to specify a drive letter. Using a root
// lets code refer to OS(`c:\`), OS(`d:\`) and so on.
func OS(root string) FileSystem {
var t RootType
switch {
case root == GOROOT:
t = RootTypeGoRoot
case isGoPath(root):
t = RootTypeGoPath
}
return osFS{rootPath: root, rootType: t}
}
type osFS struct {
rootPath string
rootType RootType
}
func isGoPath(path string) bool {
for _, bp := range filepath.SplitList(build.Default.GOPATH) {
for _, gp := range filepath.SplitList(path) {
if bp == gp {
return true
}
}
}
return false
}
func (root osFS) String() string { return "os(" + root.rootPath + ")" }
// RootType returns the root type for the filesystem.
//
// Note that we ignore the path argument because roottype is a property of
// this filesystem. But for other filesystems, the roottype might need to be
// dynamically deduced at call time.
func (root osFS) RootType(path string) RootType {
return root.rootType
}
func (root osFS) resolve(path string) string {
// Clean the path so that it cannot possibly begin with ../.
// If it did, the result of filepath.Join would be outside the
// tree rooted at root. We probably won't ever see a path
// with .. in it, but be safe anyway.
path = pathpkg.Clean("/" + path)
return filepath.Join(root.rootPath, path)
}
func (root osFS) Open(path string) (ReadSeekCloser, error) {
f, err := os.Open(root.resolve(path))
if err != nil {
return nil, err
}
fi, err := f.Stat()
if err != nil {
f.Close()
return nil, err
}
if fi.IsDir() {
f.Close()
return nil, fmt.Errorf("Open: %s is a directory", path)
}
return f, nil
}
func (root osFS) Lstat(path string) (os.FileInfo, error) {
return os.Lstat(root.resolve(path))
}
func (root osFS) Stat(path string) (os.FileInfo, error) {
return os.Stat(root.resolve(path))
}
func (root osFS) ReadDir(path string) ([]os.FileInfo, error) {
return ioutil.ReadDir(root.resolve(path)) // is sorted
}

Просмотреть файл

@ -0,0 +1,38 @@
// Copyright 2018 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 vfs_test
import (
"os"
"runtime"
"testing"
"golang.org/x/website/cmd/golangorg/godoc/vfs"
)
func TestRootType(t *testing.T) {
goPath := os.Getenv("GOPATH")
var expectedType vfs.RootType
if goPath == "" {
expectedType = ""
} else {
expectedType = vfs.RootTypeGoPath
}
tests := []struct {
path string
fsType vfs.RootType
}{
{runtime.GOROOT(), vfs.RootTypeGoRoot},
{goPath, expectedType},
{"/tmp/", ""},
}
for _, item := range tests {
fs := vfs.OS(item.path)
if fs.RootType("path") != item.fsType {
t.Errorf("unexpected fsType. Expected- %v, Got- %v", item.fsType, fs.RootType("path"))
}
}
}

Просмотреть файл

@ -0,0 +1,58 @@
// 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.
// Package vfs defines types for abstract file system access and provides an
// implementation accessing the file system of the underlying OS.
package vfs // import "golang.org/x/website/cmd/golangorg/godoc/vfs"
import (
"io"
"io/ioutil"
"os"
)
// RootType indicates the type of files contained within a directory.
//
// It is used to indicate whether a directory is the root
// of a GOROOT, a GOPATH, or neither.
// An empty string represents the case when a directory is neither.
type RootType string
const (
RootTypeGoRoot RootType = "GOROOT"
RootTypeGoPath RootType = "GOPATH"
)
// The FileSystem interface specifies the methods godoc is using
// to access the file system for which it serves documentation.
type FileSystem interface {
Opener
Lstat(path string) (os.FileInfo, error)
Stat(path string) (os.FileInfo, error)
ReadDir(path string) ([]os.FileInfo, error)
RootType(path string) RootType
String() string
}
// Opener is a minimal virtual filesystem that can only open regular files.
type Opener interface {
Open(name string) (ReadSeekCloser, error)
}
// A ReadSeekCloser can Read, Seek, and Close.
type ReadSeekCloser interface {
io.Reader
io.Seeker
io.Closer
}
// ReadFile reads the file named by path from fs and returns the contents.
func ReadFile(fs Opener, path string) ([]byte, error) {
rc, err := fs.Open(path)
if err != nil {
return nil, err
}
defer rc.Close()
return ioutil.ReadAll(rc)
}

Просмотреть файл

@ -0,0 +1,291 @@
// Copyright 2011 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 zipfs file provides an implementation of the FileSystem
// interface based on the contents of a .zip file.
//
// Assumptions:
//
// - The file paths stored in the zip file must use a slash ('/') as path
// separator; and they must be relative (i.e., they must not start with
// a '/' - this is usually the case if the file was created w/o special
// options).
// - The zip file system treats the file paths found in the zip internally
// like absolute paths w/o a leading '/'; i.e., the paths are considered
// relative to the root of the file system.
// - All path arguments to file system methods must be absolute paths.
package zipfs // import "golang.org/x/website/cmd/golangorg/godoc/vfs/zipfs"
import (
"archive/zip"
"fmt"
"go/build"
"io"
"os"
"path"
"path/filepath"
"sort"
"strings"
"time"
"golang.org/x/website/cmd/golangorg/godoc/vfs"
)
// zipFI is the zip-file based implementation of FileInfo
type zipFI struct {
name string // directory-local name
file *zip.File // nil for a directory
}
func (fi zipFI) Name() string {
return fi.name
}
func (fi zipFI) Size() int64 {
if f := fi.file; f != nil {
return int64(f.UncompressedSize)
}
return 0 // directory
}
func (fi zipFI) ModTime() time.Time {
if f := fi.file; f != nil {
return f.ModTime()
}
return time.Time{} // directory has no modified time entry
}
func (fi zipFI) Mode() os.FileMode {
if fi.file == nil {
// Unix directories typically are executable, hence 555.
return os.ModeDir | 0555
}
return 0444
}
func (fi zipFI) IsDir() bool {
return fi.file == nil
}
func (fi zipFI) Sys() interface{} {
return nil
}
// zipFS is the zip-file based implementation of FileSystem
type zipFS struct {
*zip.ReadCloser
list zipList
name string
}
func (fs *zipFS) String() string {
return "zip(" + fs.name + ")"
}
func (fs *zipFS) RootType(abspath string) vfs.RootType {
var t vfs.RootType
switch {
case exists(path.Join(vfs.GOROOT, abspath)):
t = vfs.RootTypeGoRoot
case isGoPath(abspath):
t = vfs.RootTypeGoPath
}
return t
}
func isGoPath(abspath string) bool {
for _, p := range filepath.SplitList(build.Default.GOPATH) {
if exists(path.Join(p, abspath)) {
return true
}
}
return false
}
func exists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func (fs *zipFS) Close() error {
fs.list = nil
return fs.ReadCloser.Close()
}
func zipPath(name string) (string, error) {
name = path.Clean(name)
if !path.IsAbs(name) {
return "", fmt.Errorf("stat: not an absolute path: %s", name)
}
return name[1:], nil // strip leading '/'
}
func isRoot(abspath string) bool {
return path.Clean(abspath) == "/"
}
func (fs *zipFS) stat(abspath string) (int, zipFI, error) {
if isRoot(abspath) {
return 0, zipFI{
name: "",
file: nil,
}, nil
}
zippath, err := zipPath(abspath)
if err != nil {
return 0, zipFI{}, err
}
i, exact := fs.list.lookup(zippath)
if i < 0 {
// zippath has leading '/' stripped - print it explicitly
return -1, zipFI{}, &os.PathError{Path: "/" + zippath, Err: os.ErrNotExist}
}
_, name := path.Split(zippath)
var file *zip.File
if exact {
file = fs.list[i] // exact match found - must be a file
}
return i, zipFI{name, file}, nil
}
func (fs *zipFS) Open(abspath string) (vfs.ReadSeekCloser, error) {
_, fi, err := fs.stat(abspath)
if err != nil {
return nil, err
}
if fi.IsDir() {
return nil, fmt.Errorf("Open: %s is a directory", abspath)
}
r, err := fi.file.Open()
if err != nil {
return nil, err
}
return &zipSeek{fi.file, r}, nil
}
type zipSeek struct {
file *zip.File
io.ReadCloser
}
func (f *zipSeek) Seek(offset int64, whence int) (int64, error) {
if whence == 0 && offset == 0 {
r, err := f.file.Open()
if err != nil {
return 0, err
}
f.Close()
f.ReadCloser = r
return 0, nil
}
return 0, fmt.Errorf("unsupported Seek in %s", f.file.Name)
}
func (fs *zipFS) Lstat(abspath string) (os.FileInfo, error) {
_, fi, err := fs.stat(abspath)
return fi, err
}
func (fs *zipFS) Stat(abspath string) (os.FileInfo, error) {
_, fi, err := fs.stat(abspath)
return fi, err
}
func (fs *zipFS) ReadDir(abspath string) ([]os.FileInfo, error) {
i, fi, err := fs.stat(abspath)
if err != nil {
return nil, err
}
if !fi.IsDir() {
return nil, fmt.Errorf("ReadDir: %s is not a directory", abspath)
}
var list []os.FileInfo
// make dirname the prefix that file names must start with to be considered
// in this directory. we must special case the root directory because, per
// the spec of this package, zip file entries MUST NOT start with /, so we
// should not append /, as we would in every other case.
var dirname string
if isRoot(abspath) {
dirname = ""
} else {
zippath, err := zipPath(abspath)
if err != nil {
return nil, err
}
dirname = zippath + "/"
}
prevname := ""
for _, e := range fs.list[i:] {
if !strings.HasPrefix(e.Name, dirname) {
break // not in the same directory anymore
}
name := e.Name[len(dirname):] // local name
file := e
if i := strings.IndexRune(name, '/'); i >= 0 {
// We infer directories from files in subdirectories.
// If we have x/y, return a directory entry for x.
name = name[0:i] // keep local directory name only
file = nil
}
// If we have x/y and x/z, don't return two directory entries for x.
// TODO(gri): It should be possible to do this more efficiently
// by determining the (fs.list) range of local directory entries
// (via two binary searches).
if name != prevname {
list = append(list, zipFI{name, file})
prevname = name
}
}
return list, nil
}
func New(rc *zip.ReadCloser, name string) vfs.FileSystem {
list := make(zipList, len(rc.File))
copy(list, rc.File) // sort a copy of rc.File
sort.Sort(list)
return &zipFS{rc, list, name}
}
type zipList []*zip.File
// zipList implements sort.Interface
func (z zipList) Len() int { return len(z) }
func (z zipList) Less(i, j int) bool { return z[i].Name < z[j].Name }
func (z zipList) Swap(i, j int) { z[i], z[j] = z[j], z[i] }
// lookup returns the smallest index of an entry with an exact match
// for name, or an inexact match starting with name/. If there is no
// such entry, the result is -1, false.
func (z zipList) lookup(name string) (index int, exact bool) {
// look for exact match first (name comes before name/ in z)
i := sort.Search(len(z), func(i int) bool {
return name <= z[i].Name
})
if i >= len(z) {
return -1, false
}
// 0 <= i < len(z)
if z[i].Name == name {
return i, true
}
// look for inexact match (must be in z[i:], if present)
z = z[i:]
name += "/"
j := sort.Search(len(z), func(i int) bool {
return name <= z[i].Name
})
if j >= len(z) {
return -1, false
}
// 0 <= j < len(z)
if strings.HasPrefix(z[j].Name, name) {
return i + j, false
}
return -1, false
}

Просмотреть файл

@ -0,0 +1,206 @@
// Copyright 2015 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 zipfs
package zipfs
import (
"archive/zip"
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"reflect"
"testing"
"golang.org/x/website/cmd/golangorg/godoc/vfs"
)
var (
// files to use to build zip used by zipfs in testing; maps path : contents
files = map[string]string{"foo": "foo", "bar/baz": "baz", "a/b/c": "c"}
// expected info for each entry in a file system described by files
tests = []struct {
Path string
IsDir bool
IsRegular bool
Name string
Contents string
Files map[string]bool
}{
{"/", true, false, "", "", map[string]bool{"foo": true, "bar": true, "a": true}},
{"//", true, false, "", "", map[string]bool{"foo": true, "bar": true, "a": true}},
{"/foo", false, true, "foo", "foo", nil},
{"/foo/", false, true, "foo", "foo", nil},
{"/foo//", false, true, "foo", "foo", nil},
{"/bar", true, false, "bar", "", map[string]bool{"baz": true}},
{"/bar/", true, false, "bar", "", map[string]bool{"baz": true}},
{"/bar/baz", false, true, "baz", "baz", nil},
{"//bar//baz", false, true, "baz", "baz", nil},
{"/a/b", true, false, "b", "", map[string]bool{"c": true}},
}
// to be initialized in setup()
fs vfs.FileSystem
statFuncs []statFunc
)
type statFunc struct {
Name string
Func func(string) (os.FileInfo, error)
}
func TestMain(t *testing.M) {
if err := setup(); err != nil {
fmt.Fprintf(os.Stderr, "Error setting up zipfs testing state: %v.\n", err)
os.Exit(1)
}
os.Exit(t.Run())
}
// setups state each of the tests uses
func setup() error {
// create zipfs
b := new(bytes.Buffer)
zw := zip.NewWriter(b)
for file, contents := range files {
w, err := zw.Create(file)
if err != nil {
return err
}
_, err = io.WriteString(w, contents)
if err != nil {
return err
}
}
zw.Close()
zr, err := zip.NewReader(bytes.NewReader(b.Bytes()), int64(b.Len()))
if err != nil {
return err
}
rc := &zip.ReadCloser{
Reader: *zr,
}
fs = New(rc, "foo")
// pull out different stat functions
statFuncs = []statFunc{
{"Stat", fs.Stat},
{"Lstat", fs.Lstat},
}
return nil
}
func TestZipFSReadDir(t *testing.T) {
for _, test := range tests {
if test.IsDir {
infos, err := fs.ReadDir(test.Path)
if err != nil {
t.Errorf("Failed to read directory %v\n", test.Path)
continue
}
got := make(map[string]bool)
for _, info := range infos {
got[info.Name()] = true
}
if want := test.Files; !reflect.DeepEqual(got, want) {
t.Errorf("ReadDir %v got %v\nwanted %v\n", test.Path, got, want)
}
}
}
}
func TestZipFSStatFuncs(t *testing.T) {
for _, test := range tests {
for _, statFunc := range statFuncs {
// test can stat
info, err := statFunc.Func(test.Path)
if err != nil {
t.Errorf("Unexpected error using %v for %v: %v\n", statFunc.Name, test.Path, err)
continue
}
// test info.Name()
if got, want := info.Name(), test.Name; got != want {
t.Errorf("Using %v for %v info.Name() got %v wanted %v\n", statFunc.Name, test.Path, got, want)
}
// test info.IsDir()
if got, want := info.IsDir(), test.IsDir; got != want {
t.Errorf("Using %v for %v info.IsDir() got %v wanted %v\n", statFunc.Name, test.Path, got, want)
}
// test info.Mode().IsDir()
if got, want := info.Mode().IsDir(), test.IsDir; got != want {
t.Errorf("Using %v for %v info.Mode().IsDir() got %v wanted %v\n", statFunc.Name, test.Path, got, want)
}
// test info.Mode().IsRegular()
if got, want := info.Mode().IsRegular(), test.IsRegular; got != want {
t.Errorf("Using %v for %v info.Mode().IsRegular() got %v wanted %v\n", statFunc.Name, test.Path, got, want)
}
// test info.Size()
if test.IsRegular {
if got, want := info.Size(), int64(len(test.Contents)); got != want {
t.Errorf("Using %v for %v inf.Size() got %v wanted %v", statFunc.Name, test.Path, got, want)
}
}
}
}
}
func TestZipFSNotExist(t *testing.T) {
_, err := fs.Open("/does-not-exist")
if err == nil {
t.Fatalf("Expected an error.\n")
}
if !os.IsNotExist(err) {
t.Errorf("Expected an error satisfying os.IsNotExist: %v\n", err)
}
}
func TestZipFSOpenSeek(t *testing.T) {
for _, test := range tests {
if test.IsRegular {
// test Open()
f, err := fs.Open(test.Path)
if err != nil {
t.Error(err)
return
}
defer f.Close()
// test Seek() multiple times
for i := 0; i < 3; i++ {
all, err := ioutil.ReadAll(f)
if err != nil {
t.Error(err)
return
}
if got, want := string(all), test.Contents; got != want {
t.Errorf("File contents for %v got %v wanted %v\n", test.Path, got, want)
}
f.Seek(0, 0)
}
}
}
}
func TestRootType(t *testing.T) {
tests := []struct {
path string
fsType vfs.RootType
}{
{"/src/net/http", vfs.RootTypeGoRoot},
{"/src/badpath", ""},
{"/", vfs.RootTypeGoRoot},
}
for _, item := range tests {
if fs.RootType(item.path) != item.fsType {
t.Errorf("unexpected fsType. Expected- %v, Got- %v", item.fsType, fs.RootType(item.path))
}
}
}

464
cmd/golangorg/godoc_test.go Normal file
Просмотреть файл

@ -0,0 +1,464 @@
// 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.
package main_test
import (
"bufio"
"bytes"
"fmt"
"go/build"
"io"
"io/ioutil"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"testing"
"time"
)
// buildGodoc builds the godoc executable.
// It returns its path, and a cleanup function.
//
// TODO(adonovan): opt: do this at most once, and do the cleanup
// exactly once. How though? There's no atexit.
func buildGodoc(t *testing.T) (bin string, cleanup func()) {
if runtime.GOARCH == "arm" {
t.Skip("skipping test on arm platforms; too slow")
}
tmp, err := ioutil.TempDir("", "godoc-regtest-")
if err != nil {
t.Fatal(err)
}
defer func() {
if cleanup == nil { // probably, go build failed.
os.RemoveAll(tmp)
}
}()
bin = filepath.Join(tmp, "godoc")
if runtime.GOOS == "windows" {
bin += ".exe"
}
cmd := exec.Command("go", "build", "-o", bin)
if err := cmd.Run(); err != nil {
t.Fatalf("Building godoc: %v", err)
}
return bin, func() { os.RemoveAll(tmp) }
}
func serverAddress(t *testing.T) string {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
ln, err = net.Listen("tcp6", "[::1]:0")
}
if err != nil {
t.Fatal(err)
}
defer ln.Close()
return ln.Addr().String()
}
func waitForServerReady(t *testing.T, addr string) {
waitForServer(t,
fmt.Sprintf("http://%v/", addr),
"The Go Programming Language",
15*time.Second,
false)
}
func waitForSearchReady(t *testing.T, addr string) {
waitForServer(t,
fmt.Sprintf("http://%v/search?q=FALLTHROUGH", addr),
"The list of tokens.",
2*time.Minute,
false)
}
func waitUntilScanComplete(t *testing.T, addr string) {
waitForServer(t,
fmt.Sprintf("http://%v/pkg", addr),
"Scan is not yet complete",
2*time.Minute,
true,
)
// setting reverse as true, which means this waits
// until the string is not returned in the response anymore
}
const pollInterval = 200 * time.Millisecond
func waitForServer(t *testing.T, url, match string, timeout time.Duration, reverse bool) {
// "health check" duplicated from x/tools/cmd/tipgodoc/tip.go
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
time.Sleep(pollInterval)
res, err := http.Get(url)
if err != nil {
continue
}
rbody, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err == nil && res.StatusCode == http.StatusOK {
if bytes.Contains(rbody, []byte(match)) && !reverse {
return
}
if !bytes.Contains(rbody, []byte(match)) && reverse {
return
}
}
}
t.Fatalf("Server failed to respond in %v", timeout)
}
// hasTag checks whether a given release tag is contained in the current version
// of the go binary.
func hasTag(t string) bool {
for _, v := range build.Default.ReleaseTags {
if t == v {
return true
}
}
return false
}
func killAndWait(cmd *exec.Cmd) {
cmd.Process.Kill()
cmd.Wait()
}
// Basic integration test for godoc HTTP interface.
func TestWeb(t *testing.T) {
testWeb(t, false)
}
// Basic integration test for godoc HTTP interface.
func TestWebIndex(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in -short mode")
}
testWeb(t, true)
}
// Basic integration test for godoc HTTP interface.
func testWeb(t *testing.T, withIndex bool) {
if runtime.GOOS == "plan9" {
t.Skip("skipping on plan9; files to start up quickly enough")
}
bin, cleanup := buildGodoc(t)
defer cleanup()
addr := serverAddress(t)
args := []string{fmt.Sprintf("-http=%s", addr)}
if withIndex {
args = append(args, "-index", "-index_interval=-1s")
}
cmd := exec.Command(bin, args...)
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
cmd.Args[0] = "godoc"
// Set GOPATH variable to non-existing path
// and GOPROXY=off to disable module fetches.
// We cannot just unset GOPATH variable because godoc would default it to ~/go.
// (We don't want the indexer looking at the local workspace during tests.)
cmd.Env = append(os.Environ(),
"GOPATH=does_not_exist",
"GOPROXY=off",
"GO111MODULE=off")
if err := cmd.Start(); err != nil {
t.Fatalf("failed to start godoc: %s", err)
}
defer killAndWait(cmd)
if withIndex {
waitForSearchReady(t, addr)
} else {
waitForServerReady(t, addr)
waitUntilScanComplete(t, addr)
}
tests := []struct {
path string
contains []string // substring
match []string // regexp
notContains []string
needIndex bool
releaseTag string // optional release tag that must be in go/build.ReleaseTags
}{
{
path: "/",
contains: []string{"Go is an open source programming language"},
},
{
path: "/pkg/fmt/",
contains: []string{"Package fmt implements formatted I/O"},
},
{
path: "/src/fmt/",
contains: []string{"scan_test.go"},
},
{
path: "/src/fmt/print.go",
contains: []string{"// Println formats using"},
},
{
path: "/pkg",
contains: []string{
"Standard library",
"Package fmt implements formatted I/O",
},
notContains: []string{
"internal/syscall",
"cmd/gc",
},
},
{
path: "/pkg/?m=all",
contains: []string{
"Standard library",
"Package fmt implements formatted I/O",
"internal/syscall/?m=all",
},
notContains: []string{
"cmd/gc",
},
},
{
path: "/search?q=ListenAndServe",
contains: []string{
"/src",
},
notContains: []string{
"/pkg/bootstrap",
},
needIndex: true,
},
{
path: "/pkg/strings/",
contains: []string{
`href="/src/strings/strings.go"`,
},
},
{
path: "/cmd/compile/internal/amd64/",
contains: []string{
`href="/src/cmd/compile/internal/amd64/ssa.go"`,
},
},
{
path: "/pkg/math/bits/",
contains: []string{
`Added in Go 1.9`,
},
},
{
path: "/pkg/net/",
contains: []string{
`// IPv6 scoped addressing zone; added in Go 1.1`,
},
},
{
path: "/pkg/net/http/httptrace/",
match: []string{
`Got1xxResponse.*// Go 1\.11`,
},
releaseTag: "go1.11",
},
// Verify we don't add version info to a struct field added the same time
// as the struct itself:
{
path: "/pkg/net/http/httptrace/",
match: []string{
`(?m)GotFirstResponseByte func\(\)\s*$`,
},
},
// Remove trailing periods before adding semicolons:
{
path: "/pkg/database/sql/",
contains: []string{
"The number of connections currently in use; added in Go 1.11",
"The number of idle connections; added in Go 1.11",
},
releaseTag: "go1.11",
},
}
for _, test := range tests {
if test.needIndex && !withIndex {
continue
}
url := fmt.Sprintf("http://%s%s", addr, test.path)
resp, err := http.Get(url)
if err != nil {
t.Errorf("GET %s failed: %s", url, err)
continue
}
body, err := ioutil.ReadAll(resp.Body)
strBody := string(body)
resp.Body.Close()
if err != nil {
t.Errorf("GET %s: failed to read body: %s (response: %v)", url, err, resp)
}
isErr := false
for _, substr := range test.contains {
if test.releaseTag != "" && !hasTag(test.releaseTag) {
continue
}
if !bytes.Contains(body, []byte(substr)) {
t.Errorf("GET %s: wanted substring %q in body", url, substr)
isErr = true
}
}
for _, re := range test.match {
if test.releaseTag != "" && !hasTag(test.releaseTag) {
continue
}
if ok, err := regexp.MatchString(re, strBody); !ok || err != nil {
if err != nil {
t.Fatalf("Bad regexp %q: %v", re, err)
}
t.Errorf("GET %s: wanted to match %s in body", url, re)
isErr = true
}
}
for _, substr := range test.notContains {
if bytes.Contains(body, []byte(substr)) {
t.Errorf("GET %s: didn't want substring %q in body", url, substr)
isErr = true
}
}
if isErr {
t.Errorf("GET %s: got:\n%s", url, body)
}
}
}
// Basic integration test for godoc -analysis=type (via HTTP interface).
func TestTypeAnalysis(t *testing.T) {
if runtime.GOOS == "plan9" {
t.Skip("skipping test on plan9 (issue #11974)") // see comment re: Plan 9 below
}
// Write a fake GOROOT/GOPATH.
tmpdir, err := ioutil.TempDir("", "godoc-analysis")
if err != nil {
t.Fatalf("ioutil.TempDir failed: %s", err)
}
defer os.RemoveAll(tmpdir)
for _, f := range []struct{ file, content string }{
{"goroot/src/lib/lib.go", `
package lib
type T struct{}
const C = 3
var V T
func (T) F() int { return C }
`},
{"gopath/src/app/main.go", `
package main
import "lib"
func main() { print(lib.V) }
`},
} {
file := filepath.Join(tmpdir, f.file)
if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil {
t.Fatalf("MkdirAll(%s) failed: %s", filepath.Dir(file), err)
}
if err := ioutil.WriteFile(file, []byte(f.content), 0644); err != nil {
t.Fatal(err)
}
}
// Start the server.
bin, cleanup := buildGodoc(t)
defer cleanup()
addr := serverAddress(t)
cmd := exec.Command(bin, fmt.Sprintf("-http=%s", addr), "-analysis=type")
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, fmt.Sprintf("GOROOT=%s", filepath.Join(tmpdir, "goroot")))
cmd.Env = append(cmd.Env, fmt.Sprintf("GOPATH=%s", filepath.Join(tmpdir, "gopath")))
cmd.Env = append(cmd.Env, "GO111MODULE=off")
cmd.Env = append(cmd.Env, "GOPROXY=off")
cmd.Stdout = os.Stderr
stderr, err := cmd.StderrPipe()
if err != nil {
t.Fatal(err)
}
cmd.Args[0] = "godoc"
if err := cmd.Start(); err != nil {
t.Fatalf("failed to start godoc: %s", err)
}
defer killAndWait(cmd)
waitForServerReady(t, addr)
// Wait for type analysis to complete.
reader := bufio.NewReader(stderr)
for {
s, err := reader.ReadString('\n') // on Plan 9 this fails
if err != nil {
t.Fatal(err)
}
fmt.Fprint(os.Stderr, s)
if strings.Contains(s, "Type analysis complete.") {
break
}
}
go io.Copy(os.Stderr, reader)
t0 := time.Now()
// Make an HTTP request and check for a regular expression match.
// The patterns are very crude checks that basic type information
// has been annotated onto the source view.
tryagain:
for _, test := range []struct{ url, pattern string }{
{"/src/lib/lib.go", "L2.*package .*Package docs for lib.*/lib"},
{"/src/lib/lib.go", "L3.*type .*type info for T.*struct"},
{"/src/lib/lib.go", "L5.*var V .*type T struct"},
{"/src/lib/lib.go", "L6.*func .*type T struct.*T.*return .*const C untyped int.*C"},
{"/src/app/main.go", "L2.*package .*Package docs for app"},
{"/src/app/main.go", "L3.*import .*Package docs for lib.*lib"},
{"/src/app/main.go", "L4.*func main.*package lib.*lib.*var lib.V lib.T.*V"},
} {
url := fmt.Sprintf("http://%s%s", addr, test.url)
resp, err := http.Get(url)
if err != nil {
t.Errorf("GET %s failed: %s", url, err)
continue
}
body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
t.Errorf("GET %s: failed to read body: %s (response: %v)", url, err, resp)
continue
}
if !bytes.Contains(body, []byte("Static analysis features")) {
// Type analysis results usually become available within
// ~4ms after godoc startup (for this input on my machine).
if elapsed := time.Since(t0); elapsed > 500*time.Millisecond {
t.Fatalf("type analysis results still unavailable after %s", elapsed)
}
time.Sleep(10 * time.Millisecond)
goto tryagain
}
match, err := regexp.Match(test.pattern, body)
if err != nil {
t.Errorf("regexp.Match(%q) failed: %s", test.pattern, err)
continue
}
if !match {
// This is a really ugly failure message.
t.Errorf("GET %s: body doesn't match %q, got:\n%s",
url, test.pattern, string(body))
}
}
}

74
cmd/golangorg/goroot.go Normal file
Просмотреть файл

@ -0,0 +1,74 @@
// Copyright 2018 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 main
import (
"os"
"path/filepath"
"runtime"
)
// Copies of functions from src/cmd/go/internal/cfg/cfg.go for
// finding the GOROOT.
// Keep them in sync until support is moved to a common place, if ever.
func findGOROOT() string {
if env := os.Getenv("GOROOT"); env != "" {
return filepath.Clean(env)
}
def := filepath.Clean(runtime.GOROOT())
if runtime.Compiler == "gccgo" {
// gccgo has no real GOROOT, and it certainly doesn't
// depend on the executable's location.
return def
}
exe, err := os.Executable()
if err == nil {
exe, err = filepath.Abs(exe)
if err == nil {
if dir := filepath.Join(exe, "../.."); isGOROOT(dir) {
// If def (runtime.GOROOT()) and dir are the same
// directory, prefer the spelling used in def.
if isSameDir(def, dir) {
return def
}
return dir
}
exe, err = filepath.EvalSymlinks(exe)
if err == nil {
if dir := filepath.Join(exe, "../.."); isGOROOT(dir) {
if isSameDir(def, dir) {
return def
}
return dir
}
}
}
}
return def
}
// isGOROOT reports whether path looks like a GOROOT.
//
// It does this by looking for the path/pkg/tool directory,
// which is necessary for useful operation of the cmd/go tool,
// and is not typically present in a GOPATH.
func isGOROOT(path string) bool {
stat, err := os.Stat(filepath.Join(path, "pkg", "tool"))
if err != nil {
return false
}
return stat.IsDir()
}
// isSameDir reports whether dir1 and dir2 are the same directory.
func isSameDir(dir1, dir2 string) bool {
if dir1 == dir2 {
return true
}
info1, err1 := os.Stat(dir1)
info2, err2 := os.Stat(dir2)
return err1 == nil && err2 == nil && os.SameFile(info1, info2)
}

151
cmd/golangorg/handlers.go Normal file
Просмотреть файл

@ -0,0 +1,151 @@
// Copyright 2010 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.
// The /doc/codewalk/ tree is synthesized from codewalk descriptions,
// files named $GOROOT/doc/codewalk/*.xml.
// For an example and a description of the format, see
// http://golang.org/doc/codewalk/codewalk or run godoc -http=:6060
// and see http://localhost:6060/doc/codewalk/codewalk .
// That page is itself a codewalk; the source code for it is
// $GOROOT/doc/codewalk/codewalk.xml.
package main
import (
"encoding/json"
"go/format"
"log"
"net/http"
"strings"
"text/template"
"golang.org/x/website/cmd/golangorg/godoc"
"golang.org/x/website/cmd/golangorg/godoc/env"
"golang.org/x/website/cmd/golangorg/godoc/redirect"
"golang.org/x/website/cmd/golangorg/godoc/vfs"
)
var (
pres *godoc.Presentation
fs = vfs.NameSpace{}
)
// hostEnforcerHandler redirects requests to "http://foo.golang.org/bar"
// to "https://golang.org/bar".
// It permits requests to the host "godoc-test.golang.org" for testing and
// golang.google.cn for Chinese users.
type hostEnforcerHandler struct {
h http.Handler
}
func (h hostEnforcerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !env.EnforceHosts() {
h.h.ServeHTTP(w, r)
return
}
if !h.isHTTPS(r) || !h.validHost(r.Host) {
r.URL.Scheme = "https"
if h.validHost(r.Host) {
r.URL.Host = r.Host
} else {
r.URL.Host = "golang.org"
}
http.Redirect(w, r, r.URL.String(), http.StatusFound)
return
}
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload")
h.h.ServeHTTP(w, r)
}
func (h hostEnforcerHandler) isHTTPS(r *http.Request) bool {
return r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https"
}
func (h hostEnforcerHandler) validHost(host string) bool {
switch strings.ToLower(host) {
case "golang.org", "golang.google.cn":
return true
}
if strings.HasSuffix(host, "-dot-golang-org.appspot.com") {
// staging/test
return true
}
return false
}
func registerHandlers(pres *godoc.Presentation) *http.ServeMux {
if pres == nil {
panic("nil Presentation")
}
mux := http.NewServeMux()
mux.HandleFunc("/doc/codewalk/", codewalk)
mux.Handle("/doc/play/", pres.FileServer())
mux.Handle("/robots.txt", pres.FileServer())
mux.Handle("/", pres)
mux.Handle("/pkg/C/", redirect.Handler("/cmd/cgo/"))
mux.HandleFunc("/fmt", fmtHandler)
redirect.Register(mux)
http.Handle("/", hostEnforcerHandler{mux})
return mux
}
func readTemplate(name string) *template.Template {
if pres == nil {
panic("no global Presentation set yet")
}
path := "lib/godoc/" + name
// use underlying file system fs to read the template file
// (cannot use template ParseFile functions directly)
data, err := vfs.ReadFile(fs, path)
if err != nil {
log.Fatal("readTemplate: ", err)
}
// be explicit with errors (for app engine use)
t, err := template.New(name).Funcs(pres.FuncMap()).Parse(string(data))
if err != nil {
log.Fatal("readTemplate: ", err)
}
return t
}
func readTemplates(p *godoc.Presentation) {
codewalkHTML = readTemplate("codewalk.html")
codewalkdirHTML = readTemplate("codewalkdir.html")
p.CallGraphHTML = readTemplate("callgraph.html")
p.DirlistHTML = readTemplate("dirlist.html")
p.ErrorHTML = readTemplate("error.html")
p.ExampleHTML = readTemplate("example.html")
p.GodocHTML = readTemplate("godoc.html")
p.ImplementsHTML = readTemplate("implements.html")
p.MethodSetHTML = readTemplate("methodset.html")
p.PackageHTML = readTemplate("package.html")
p.PackageRootHTML = readTemplate("packageroot.html")
p.SearchHTML = readTemplate("search.html")
p.SearchDocHTML = readTemplate("searchdoc.html")
p.SearchCodeHTML = readTemplate("searchcode.html")
p.SearchTxtHTML = readTemplate("searchtxt.html")
p.SearchDescXML = readTemplate("opensearch.xml")
}
type fmtResponse struct {
Body string
Error string
}
// fmtHandler takes a Go program in its "body" form value, formats it with
// standard gofmt formatting, and writes a fmtResponse as a JSON object.
func fmtHandler(w http.ResponseWriter, r *http.Request) {
resp := new(fmtResponse)
body, err := format.Source([]byte(r.FormValue("body")))
if err != nil {
resp.Error = err.Error()
} else {
resp.Body = string(body)
}
w.Header().Set("Content-type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(resp)
}

Двоичные данные
cmd/golangorg/hg-git-mapping.bin Normal file

Двоичный файл не отображается.

11
cmd/golangorg/index.go Normal file
Просмотреть файл

@ -0,0 +1,11 @@
// Copyright 2015 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 main
import "strings"
func indexDirectoryDefault(dir string) bool {
return dir != "/pkg" && !strings.HasPrefix(dir, "/pkg/")
}

358
cmd/golangorg/main.go Normal file
Просмотреть файл

@ -0,0 +1,358 @@
// Copyright 2009 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.
// godoc: Go Documentation Server
// Web server tree:
//
// http://godoc/ main landing page
// http://godoc/doc/ serve from $GOROOT/doc - spec, mem, etc.
// http://godoc/src/ serve files from $GOROOT/src; .go gets pretty-printed
// http://godoc/cmd/ serve documentation about commands
// http://godoc/pkg/ serve documentation about packages
// (idea is if you say import "compress/zlib", you go to
// http://godoc/pkg/compress/zlib)
//
// +build !golangorg
package main
import (
"archive/zip"
"bytes"
_ "expvar" // to serve /debug/vars
"flag"
"fmt"
"go/build"
"log"
"net/http"
_ "net/http/pprof" // to serve /debug/pprof/*
"net/url"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"golang.org/x/website/cmd/golangorg/godoc"
"golang.org/x/website/cmd/golangorg/godoc/analysis"
"golang.org/x/website/cmd/golangorg/godoc/vfs"
"golang.org/x/website/cmd/golangorg/godoc/vfs/gatefs"
"golang.org/x/website/cmd/golangorg/godoc/vfs/mapfs"
"golang.org/x/website/cmd/golangorg/godoc/vfs/zipfs"
"golang.org/x/website/content/static"
)
const defaultAddr = "localhost:6060" // default webserver address
var (
// file system to serve
// (with e.g.: zip -r go.zip $GOROOT -i \*.go -i \*.html -i \*.css -i \*.js -i \*.txt -i \*.c -i \*.h -i \*.s -i \*.png -i \*.jpg -i \*.sh -i favicon.ico)
zipfile = flag.String("zip", "", "zip file providing the file system to serve; disabled if empty")
// file-based index
writeIndex = flag.Bool("write_index", false, "write index to a file; the file name must be specified with -index_files")
analysisFlag = flag.String("analysis", "", `comma-separated list of analyses to perform (supported: type, pointer). See http://golang.org/lib/godoc/analysis/help.html`)
// network
httpAddr = flag.String("http", defaultAddr, "HTTP service address")
// layout control
urlFlag = flag.String("url", "", "print HTML for named URL")
verbose = flag.Bool("v", false, "verbose mode")
// file system roots
// TODO(gri) consider the invariant that goroot always end in '/'
goroot = flag.String("goroot", findGOROOT(), "Go root directory")
// layout control
showTimestamps = flag.Bool("timestamps", false, "show timestamps with directory listings")
templateDir = flag.String("templates", "", "load templates/JS/CSS from disk in this directory")
showPlayground = flag.Bool("play", false, "enable playground")
declLinks = flag.Bool("links", true, "link identifiers to their declarations")
// search index
indexEnabled = flag.Bool("index", false, "enable search index")
indexFiles = flag.String("index_files", "", "glob pattern specifying index files; if not empty, the index is read from these files in sorted order")
indexInterval = flag.Duration("index_interval", 0, "interval of indexing; 0 for default (5m), negative to only index once at startup")
maxResults = flag.Int("maxresults", 10000, "maximum number of full text search results shown")
indexThrottle = flag.Float64("index_throttle", 0.75, "index throttle value; 0.0 = no time allocated, 1.0 = full throttle")
// source code notes
notesRx = flag.String("notes", "BUG", "regular expression matching note markers to show")
)
func getFullPath(relPath string) string {
gopath := os.Getenv("GOPATH")
if gopath == "" {
gopath = build.Default.GOPATH
}
return gopath + relPath
}
// An httpResponseRecorder is an http.ResponseWriter
type httpResponseRecorder struct {
body *bytes.Buffer
header http.Header
code int
}
func (w *httpResponseRecorder) Header() http.Header { return w.header }
func (w *httpResponseRecorder) Write(b []byte) (int, error) { return len(b), nil }
func (w *httpResponseRecorder) WriteHeader(code int) { w.code = code }
func usage() {
fmt.Fprintf(os.Stderr, "usage: godoc -http="+defaultAddr+"\n")
flag.PrintDefaults()
os.Exit(2)
}
func loggingHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
log.Printf("%s\t%s", req.RemoteAddr, req.URL)
h.ServeHTTP(w, req)
})
}
func handleURLFlag() {
// Try up to 10 fetches, following redirects.
urlstr := *urlFlag
for i := 0; i < 10; i++ {
// Prepare request.
u, err := url.Parse(urlstr)
if err != nil {
log.Fatal(err)
}
req := &http.Request{
URL: u,
}
// Invoke default HTTP handler to serve request
// to our buffering httpWriter.
w := &httpResponseRecorder{code: 200, header: make(http.Header), body: new(bytes.Buffer)}
http.DefaultServeMux.ServeHTTP(w, req)
// Return data, error, or follow redirect.
switch w.code {
case 200: // ok
os.Stdout.Write(w.body.Bytes())
return
case 301, 302, 303, 307: // redirect
redirect := w.header.Get("Location")
if redirect == "" {
log.Fatalf("HTTP %d without Location header", w.code)
}
urlstr = redirect
default:
log.Fatalf("HTTP error %d", w.code)
}
}
log.Fatalf("too many redirects")
}
func initCorpus(corpus *godoc.Corpus) {
err := corpus.Init()
if err != nil {
log.Fatal(err)
}
}
func main() {
flag.Usage = usage
flag.Parse()
if certInit != nil {
certInit()
}
playEnabled = *showPlayground
// Check usage: server and no args.
if (*httpAddr != "" || *urlFlag != "") && (flag.NArg() > 0) {
fmt.Fprintln(os.Stderr, "Unexpected arguments.")
usage()
}
// Check usage: command line args or index creation mode.
if (*httpAddr != "" || *urlFlag != "") != (flag.NArg() == 0) && !*writeIndex {
fmt.Fprintln(os.Stderr, "missing args.")
usage()
}
// Set the resolved goroot.
vfs.GOROOT = *goroot
fsGate := make(chan bool, 20)
// Determine file system to use.
if *zipfile == "" {
// use file system of underlying OS
rootfs := gatefs.New(vfs.OS(*goroot), fsGate)
fs.Bind("/", rootfs, "/", vfs.BindReplace)
} else {
// use file system specified via .zip file (path separator must be '/')
rc, err := zip.OpenReader(*zipfile)
if err != nil {
log.Fatalf("%s: %s\n", *zipfile, err)
}
defer rc.Close() // be nice (e.g., -writeIndex mode)
fs.Bind("/", zipfs.New(rc, *zipfile), *goroot, vfs.BindReplace)
}
if *templateDir != "" {
fs.Bind("/lib/godoc", vfs.OS(*templateDir), "/", vfs.BindBefore)
} else {
fs.Bind("/lib/godoc", mapfs.New(static.Files), "/", vfs.BindReplace)
}
// Bind $GOPATH trees into Go root.
for _, p := range filepath.SplitList(build.Default.GOPATH) {
fs.Bind("/src", gatefs.New(vfs.OS(p), fsGate), "/src", vfs.BindAfter)
}
// adding ability to specify local doc directory
docPath := getFullPath("/src/golang.org/x/website/content/doc")
fs.Bind("/doc", gatefs.New(vfs.OS(docPath), fsGate), "/", vfs.BindBefore)
webroot := getFullPath("/src/golang.org/x/website")
fs.Bind("/robots.txt", gatefs.New(vfs.OS(webroot), fsGate), "/robots.txt", vfs.BindBefore)
fs.Bind("/favicon.ico", gatefs.New(vfs.OS(webroot), fsGate), "/favicon.ico", vfs.BindBefore)
var typeAnalysis, pointerAnalysis bool
if *analysisFlag != "" {
for _, a := range strings.Split(*analysisFlag, ",") {
switch a {
case "type":
typeAnalysis = true
case "pointer":
pointerAnalysis = true
default:
log.Fatalf("unknown analysis: %s", a)
}
}
}
corpus := godoc.NewCorpus(fs)
corpus.Verbose = *verbose
corpus.MaxResults = *maxResults
corpus.IndexEnabled = *indexEnabled
if *maxResults == 0 {
corpus.IndexFullText = false
}
corpus.IndexFiles = *indexFiles
corpus.IndexDirectory = indexDirectoryDefault
corpus.IndexThrottle = *indexThrottle
corpus.IndexInterval = *indexInterval
if *writeIndex {
corpus.IndexThrottle = 1.0
corpus.IndexEnabled = true
initCorpus(corpus)
} else {
go initCorpus(corpus)
}
// Initialize the version info before readTemplates, which saves
// the map value in a method value.
corpus.InitVersionInfo()
pres = godoc.NewPresentation(corpus)
pres.ShowTimestamps = *showTimestamps
pres.ShowPlayground = *showPlayground
pres.DeclLinks = *declLinks
if *notesRx != "" {
pres.NotesRx = regexp.MustCompile(*notesRx)
}
readTemplates(pres)
registerHandlers(pres)
if *writeIndex {
// Write search index and exit.
if *indexFiles == "" {
log.Fatal("no index file specified")
}
log.Println("initialize file systems")
*verbose = true // want to see what happens
corpus.UpdateIndex()
log.Println("writing index file", *indexFiles)
f, err := os.Create(*indexFiles)
if err != nil {
log.Fatal(err)
}
index, _ := corpus.CurrentIndex()
_, err = index.WriteTo(f)
if err != nil {
log.Fatal(err)
}
log.Println("done")
return
}
// Print content that would be served at the URL *urlFlag.
if *urlFlag != "" {
handleURLFlag()
return
}
var handler http.Handler = http.DefaultServeMux
if *verbose {
log.Printf("Go Documentation Server")
log.Printf("version = %s", runtime.Version())
log.Printf("address = %s", *httpAddr)
log.Printf("goroot = %s", *goroot)
switch {
case !*indexEnabled:
log.Print("search index disabled")
case *maxResults > 0:
log.Printf("full text index enabled (maxresults = %d)", *maxResults)
default:
log.Print("identifier search index enabled")
}
fs.Fprint(os.Stderr)
handler = loggingHandler(handler)
}
// Initialize search index.
if *indexEnabled {
go corpus.RunIndexer()
}
// Start type/pointer analysis.
if typeAnalysis || pointerAnalysis {
go analysis.Run(pointerAnalysis, &corpus.Analysis)
}
if runHTTPS != nil {
go func() {
if err := runHTTPS(handler); err != nil {
log.Fatalf("ListenAndServe TLS: %v", err)
}
}()
}
// Start http server.
if *verbose {
log.Println("starting HTTP server")
}
if wrapHTTPMux != nil {
handler = wrapHTTPMux(handler)
}
if err := http.ListenAndServe(*httpAddr, handler); err != nil {
log.Fatalf("ListenAndServe %s: %v", *httpAddr, err)
}
}
// Hooks that are set non-nil in autocert.go if the "autocert" build tag
// is used.
var (
certInit func()
runHTTPS func(http.Handler) error
wrapHTTPMux func(http.Handler) http.Handler
)

11
cmd/golangorg/play.go Normal file
Просмотреть файл

@ -0,0 +1,11 @@
// Copyright 2012 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.
// +build !golangorg
package main
// This package registers "/compile" and "/share" handlers
// that redirect to the golang.org playground.
import _ "golang.org/x/tools/playground"

Просмотреть файл

@ -0,0 +1,171 @@
// Copyright 2018 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.
// Regression tests to run against a production instance of godoc.
package main_test
import (
"bytes"
"flag"
"io"
"io/ioutil"
"net/http"
"net/url"
"regexp"
"strings"
"testing"
)
var host = flag.String("regtest.host", "", "host to run regression test against")
func init() {
flag.Parse()
*host = strings.TrimSuffix(*host, "/")
}
func TestLiveServer(t *testing.T) {
if *host == "" {
t.Skip("regtest.host flag missing.")
}
substringTests := []struct {
Message string
Path string
Substring string
Regexp string
NoAnalytics bool // expect the response to not contain GA.
PostBody string
StatusCode int // if 0, expect 2xx status code.
}{
{
Path: "/doc/faq",
Substring: "What is the purpose of the project",
},
{
Path: "/pkg/",
Substring: "Package tar",
},
{
Path: "/pkg/os/",
Substring: "func Open",
},
{
Path: "/pkg/net/http/",
Substring: `title="Added in Go 1.11"`,
Message: "version information not present - failed InitVersionInfo?",
},
{
Path: "/robots.txt",
Substring: "Disallow: /search",
Message: "robots not present - not deployed from Dockerfile?",
NoAnalytics: true,
},
{
Path: "/change/75944e2e3a63",
Substring: "bdb10cf",
Message: "no change redirect - hg to git mapping not registered?",
NoAnalytics: true,
StatusCode: 302,
},
{
Path: "/dl/",
Substring: "go1.11.windows-amd64.msi",
Message: "missing data on dl page - misconfiguration of datastore?",
},
{
Path: "/dl/?mode=json",
Substring: ".windows-amd64.msi",
NoAnalytics: true,
},
{
Message: "broken shortlinks - misconfiguration of datastore or memcache?",
Path: "/s/go2design",
Regexp: "proposal.*Found",
NoAnalytics: true,
StatusCode: 302,
},
{
Message: "incorrect search result - broken index?",
Path: "/search?q=IsDir",
Substring: "src/os/types.go",
},
{
Path: "/compile",
PostBody: "body=" + url.QueryEscape("package main; func main() { print(6*7); }"),
Regexp: `^{"compile_errors":"","output":"42"}$`,
NoAnalytics: true,
},
{
Path: "/compile",
PostBody: "body=" + url.QueryEscape("//empty"),
Substring: "expected 'package', found 'EOF'",
NoAnalytics: true,
},
{
Path: "/compile",
PostBody: "version=2&body=package+main%3Bimport+(%22fmt%22%3B%22time%22)%3Bfunc+main()%7Bfmt.Print(%22A%22)%3Btime.Sleep(time.Second)%3Bfmt.Print(%22B%22)%7D",
Regexp: `^{"Errors":"","Events":\[{"Message":"A","Kind":"stdout","Delay":0},{"Message":"B","Kind":"stdout","Delay":1000000000}\]}$`,
NoAnalytics: true,
},
{
Path: "/share",
PostBody: "package main",
Substring: "", // just check it is a 2xx.
NoAnalytics: true,
},
}
for _, tc := range substringTests {
t.Run(tc.Path, func(t *testing.T) {
method := "GET"
var reqBody io.Reader
if tc.PostBody != "" {
method = "POST"
reqBody = strings.NewReader(tc.PostBody)
}
req, err := http.NewRequest(method, *host+tc.Path, reqBody)
if err != nil {
t.Fatalf("NewRequest: %v", err)
}
if reqBody != nil {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
resp, err := http.DefaultTransport.RoundTrip(req)
if err != nil {
t.Fatalf("RoundTrip: %v", err)
}
if tc.StatusCode == 0 {
if resp.StatusCode > 299 {
t.Errorf("Non-OK status code: %v", resp.StatusCode)
}
} else if tc.StatusCode != resp.StatusCode {
t.Errorf("StatusCode; got %v, want %v", resp.StatusCode, tc.StatusCode)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("ReadAll: %v", err)
}
const googleAnalyticsID = "UA-11222381-2" // golang.org analytics ID
if !tc.NoAnalytics && !bytes.Contains(body, []byte(googleAnalyticsID)) {
t.Errorf("want response to contain analytics tracking ID")
}
if tc.Substring != "" {
tc.Regexp = regexp.QuoteMeta(tc.Substring)
}
re := regexp.MustCompile(tc.Regexp)
if !re.Match(body) {
t.Log("------ actual output -------")
t.Log(string(body))
t.Log("----------------------------")
if tc.Message != "" {
t.Log(tc.Message)
}
t.Fatalf("wanted response to match %s", tc.Regexp)
}
})
}
}

93
cmd/golangorg/x.go Normal file
Просмотреть файл

@ -0,0 +1,93 @@
// 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 contains the handlers that serve go-import redirects for Go
// sub-repositories. It specifies the mapping from import paths like
// "golang.org/x/tools" to the actual repository locations.
package main
import (
"html/template"
"log"
"net/http"
"strings"
)
const xPrefix = "/x/"
type xRepo struct {
URL, VCS string
}
var xMap = map[string]xRepo{
"codereview": {"https://code.google.com/p/go.codereview", "hg"}, // Not included at https://golang.org/pkg/#subrepo.
"arch": {"https://go.googlesource.com/arch", "git"}, // Not included at https://golang.org/pkg/#subrepo.
"benchmarks": {"https://go.googlesource.com/benchmarks", "git"},
"blog": {"https://go.googlesource.com/blog", "git"},
"build": {"https://go.googlesource.com/build", "git"},
"crypto": {"https://go.googlesource.com/crypto", "git"},
"debug": {"https://go.googlesource.com/debug", "git"},
"exp": {"https://go.googlesource.com/exp", "git"},
"image": {"https://go.googlesource.com/image", "git"},
"lint": {"https://go.googlesource.com/lint", "git"}, // Not included at https://golang.org/pkg/#subrepo.
"mobile": {"https://go.googlesource.com/mobile", "git"},
"net": {"https://go.googlesource.com/net", "git"},
"oauth2": {"https://go.googlesource.com/oauth2", "git"}, // Not included at https://golang.org/pkg/#subrepo.
"perf": {"https://go.googlesource.com/perf", "git"},
"playground": {"https://go.googlesource.com/playground", "git"}, // Not included at https://golang.org/pkg/#subrepo.
"review": {"https://go.googlesource.com/review", "git"},
"sync": {"https://go.googlesource.com/sync", "git"},
"sys": {"https://go.googlesource.com/sys", "git"},
"talks": {"https://go.googlesource.com/talks", "git"}, // Not included at https://golang.org/pkg/#subrepo.
"term": {"https://go.googlesource.com/term", "git"}, // Not included at https://golang.org/pkg/#subrepo.
"text": {"https://go.googlesource.com/text", "git"},
"time": {"https://go.googlesource.com/time", "git"},
"tools": {"https://go.googlesource.com/tools", "git"},
"tour": {"https://go.googlesource.com/tour", "git"},
"vgo": {"https://go.googlesource.com/vgo", "git"}, // Not included at https://golang.org/pkg/#subrepo.
"website": {"https://go.googlesource.com/website", "git"}, // Not included at https://golang.org/pkg/#subrepo.
}
func init() {
http.HandleFunc(xPrefix, xHandler)
}
func xHandler(w http.ResponseWriter, r *http.Request) {
head, tail := strings.TrimPrefix(r.URL.Path, xPrefix), ""
if i := strings.Index(head, "/"); i != -1 {
head, tail = head[:i], head[i:]
}
if head == "" {
http.Redirect(w, r, "https://godoc.org/-/subrepo", http.StatusTemporaryRedirect)
return
}
repo, ok := xMap[head]
if !ok {
http.NotFound(w, r)
return
}
data := struct {
Prefix, Head, Tail string
Repo xRepo
}{xPrefix, head, tail, repo}
if err := xTemplate.Execute(w, data); err != nil {
log.Println("xHandler:", err)
}
}
var xTemplate = template.Must(template.New("x").Parse(`<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="go-import" content="golang.org{{.Prefix}}{{.Head}} {{.Repo.VCS}} {{.Repo.URL}}">
<meta name="go-source" content="golang.org{{.Prefix}}{{.Head}} https://github.com/golang/{{.Head}}/ https://github.com/golang/{{.Head}}/tree/master{/dir} https://github.com/golang/{{.Head}}/blob/master{/dir}/{file}#L{line}">
<meta http-equiv="refresh" content="0; url=https://godoc.org/golang.org{{.Prefix}}{{.Head}}{{.Tail}}">
</head>
<body>
Nothing to see here; <a href="https://godoc.org/golang.org{{.Prefix}}{{.Head}}{{.Tail}}">move along</a>.
</body>
</html>
`))

1
codereview.cfg Normal file
Просмотреть файл

@ -0,0 +1 @@
issuerepo: golang/go

30
content/README.md Normal file
Просмотреть файл

@ -0,0 +1,30 @@
# content
This directory contains doc/, static/, favicon.ico, and robots.txt. The
executable lives at golang.org/x/website/cmd/golangorg.
## Development mode
In production, CSS/JS/template assets need to be compiled into the golangorg
binary. It can be tedious to recompile assets every time, but you can pass a
flag to load CSS/JS/templates from disk every time a page loads:
```
golangorg -templates=$GOPATH/src/golang.org/x/website/content/static -http=:6060
```
## Recompiling static assets
The files that live at `static/style.css`, `static/jquery.js` and so on are not
present in the final binary. They are placed into `static/static.go` by running
`go generate`. So to compile a change and test it in your browser:
1) Make changes to e.g. `static/style.css`.
2) Run `go generate golang.org/x/website/content/static` so `static/static.go` picks
up the change.
3) Run `go install golang.org/x/website/cmd/golangorg` so the compiled `golangorg` binary picks up the change.
4) Run `golangorg -http=:6060` and view your changes in the browser. You may need
to disable your browser's cache to avoid reloading a stale file.

Просмотреть файл

@ -0,0 +1,254 @@
<!--{
"title": "About the go command"
}-->
<p>The Go distribution includes a command, named
"<code><a href="/cmd/go/">go</a></code>", that
automates the downloading, building, installation, and testing of Go packages
and commands. This document talks about why we wrote a new command, what it
is, what it's not, and how to use it.</p>
<h2>Motivation</h2>
<p>You might have seen early Go talks in which Rob Pike jokes that the idea
for Go arose while waiting for a large Google server to compile. That
really was the motivation for Go: to build a language that worked well
for building the large software that Google writes and runs. It was
clear from the start that such a language must provide a way to
express dependencies between code libraries clearly, hence the package
grouping and the explicit import blocks. It was also clear from the
start that you might want arbitrary syntax for describing the code
being imported; this is why import paths are string literals.</p>
<p>An explicit goal for Go from the beginning was to be able to build Go
code using only the information found in the source itself, not
needing to write a makefile or one of the many modern replacements for
makefiles. If Go needed a configuration file to explain how to build
your program, then Go would have failed.</p>
<p>At first, there was no Go compiler, and the initial development
focused on building one and then building libraries for it. For
expedience, we postponed the automation of building Go code by using
make and writing makefiles. When compiling a single package involved
multiple invocations of the Go compiler, we even used a program to
write the makefiles for us. You can find it if you dig through the
repository history.</p>
<p>The purpose of the new go command is our return to this ideal, that Go
programs should compile without configuration or additional effort on
the part of the developer beyond writing the necessary import
statements.</p>
<h2>Configuration versus convention</h2>
<p>The way to achieve the simplicity of a configuration-free system is to
establish conventions. The system works only to the extent that those conventions
are followed. When we first launched Go, many people published packages that
had to be installed in certain places, under certain names, using certain build
tools, in order to be used. That's understandable: that's the way it works in
most other languages. Over the last few years we consistently reminded people
about the <code>goinstall</code> command
(now replaced by <a href="/cmd/go/#hdr-Download_and_install_packages_and_dependencies"><code>go get</code></a>)
and its conventions: first, that the import path is derived in a known way from
the URL of the source code; second, that the place to store the sources in
the local file system is derived in a known way from the import path; third,
that each directory in a source tree corresponds to a single package; and
fourth, that the package is built using only information in the source code.
Today, the vast majority of packages follow these conventions.
The Go ecosystem is simpler and more powerful as a result.</p>
<p>We received many requests to allow a makefile in a package directory to
provide just a little extra configuration beyond what's in the source code.
But that would have introduced new rules. Because we did not accede to such
requests, we were able to write the go command and eliminate our use of make
or any other build system.</p>
<p>It is important to understand that the go command is not a general
build tool. It cannot be configured and it does not attempt to build
anything but Go packages. These are important simplifying
assumptions: they simplify not only the implementation but also, more
important, the use of the tool itself.</p>
<h2>Go's conventions</h2>
<p>The <code>go</code> command requires that code adheres to a few key,
well-established conventions.</p>
<p>First, the import path is derived in an known way from the URL of the
source code. For Bitbucket, GitHub, Google Code, and Launchpad, the
root directory of the repository is identified by the repository's
main URL, without the <code>http://</code> prefix. Subdirectories are named by
adding to that path.
For example, the Go example programs are obtained by running</p>
<pre>
git clone https://github.com/golang/example
</pre>
<p>and thus the import path for the root directory of that repository is
"<code>github.com/golang/example</code>".
The <a href="https://godoc.org/github.com/golang/example/stringutil">stringutil</a>
package is stored in a subdirectory, so its import path is
"<code>github.com/golang/example/stringutil</code>".</p>
<p>These paths are on the long side, but in exchange we get an
automatically managed name space for import paths and the ability for
a tool like the go command to look at an unfamiliar import path and
deduce where to obtain the source code.</p>
<p>Second, the place to store sources in the local file system is derived
in a known way from the import path, specifically
<code>$GOPATH/src/&lt;import-path&gt;</code>.
If unset, <code>$GOPATH</code> defaults to a subdirectory
named <code>go</code> in the user's home directory.
If <code>$GOPATH</code> is set to a list of paths, the go command tries
<code>&lt;dir&gt;/src/&lt;import-path&gt;</code> for each of the directories in
that list.
</p>
<p>Each of those trees contains, by convention, a top-level directory named
"<code>bin</code>", for holding compiled executables, and a top-level directory
named "<code>pkg</code>", for holding compiled packages that can be imported,
and the "<code>src</code>" directory, for holding package source files.
Imposing this structure lets us keep each of these directory trees
self-contained: the compiled form and the sources are always near each
other.</p>
<p>These naming conventions also let us work in the reverse direction,
from a directory name to its import path. This mapping is important
for many of the go command's subcommands, as we'll see below.</p>
<p>Third, each directory in a source tree corresponds to a single
package. By restricting a directory to a single package, we don't have
to create hybrid import paths that specify first the directory and
then the package within that directory. Also, most file management
tools and UIs work on directories as fundamental units. Tying the
fundamental Go unit&mdash;the package&mdash;to file system structure means
that file system tools become Go package tools. Copying, moving, or
deleting a package corresponds to copying, moving, or deleting a
directory.</p>
<p>Fourth, each package is built using only the information present in
the source files. This makes it much more likely that the tool will
be able to adapt to changing build environments and conditions. For
example, if we allowed extra configuration such as compiler flags or
command line recipes, then that configuration would need to be updated
each time the build tools changed; it would also be inherently tied
to the use of a specific toolchain.</p>
<h2>Getting started with the go command</h2>
<p>Finally, a quick tour of how to use the go command.
As mentioned above, the default <code>$GOPATH</code> on Unix is <code>$HOME/go</code>.
We'll store our programs there.
To use a different location, you can set <code>$GOPATH</code>;
see <a href="/doc/code.html">How to Write Go Code</a> for details.
<p>We first add some source code. Suppose we want to use
the indexing library from the codesearch project along with a left-leaning
red-black tree. We can install both with the "<code>go get</code>"
subcommand:</p>
<pre>
$ go get github.com/google/codesearch/index
$ go get github.com/petar/GoLLRB/llrb
$
</pre>
<p>Both of these projects are now downloaded and installed into <code>$HOME/go</code>,
which contains the two directories
<code>src/github.com/google/codesearch/index/</code> and
<code>src/github.com/petar/GoLLRB/llrb/</code>, along with the compiled
packages (in <code>pkg/</code>) for those libraries and their dependencies.</p>
<p>Because we used version control systems (Mercurial and Git) to check
out the sources, the source tree also contains the other files in the
corresponding repositories, such as related packages. The "<code>go list</code>"
subcommand lists the import paths corresponding to its arguments, and
the pattern "<code>./...</code>" means start in the current directory
("<code>./</code>") and find all packages below that directory
("<code>...</code>"):</p>
<pre>
$ cd $HOME/go/src
$ go list ./...
github.com/google/codesearch/cmd/cgrep
github.com/google/codesearch/cmd/cindex
github.com/google/codesearch/cmd/csearch
github.com/google/codesearch/index
github.com/google/codesearch/regexp
github.com/google/codesearch/sparse
github.com/petar/GoLLRB/example
github.com/petar/GoLLRB/llrb
$
</pre>
<p>We can also test those packages:</p>
<pre>
$ go test ./...
? github.com/google/codesearch/cmd/cgrep [no test files]
? github.com/google/codesearch/cmd/cindex [no test files]
? github.com/google/codesearch/cmd/csearch [no test files]
ok github.com/google/codesearch/index 0.203s
ok github.com/google/codesearch/regexp 0.017s
? github.com/google/codesearch/sparse [no test files]
? github.com/petar/GoLLRB/example [no test files]
ok github.com/petar/GoLLRB/llrb 0.231s
$
</pre>
<p>If a go subcommand is invoked with no paths listed, it operates on the
current directory:</p>
<pre>
$ cd github.com/google/codesearch/regexp
$ go list
github.com/google/codesearch/regexp
$ go test -v
=== RUN TestNstateEnc
--- PASS: TestNstateEnc (0.00s)
=== RUN TestMatch
--- PASS: TestMatch (0.00s)
=== RUN TestGrep
--- PASS: TestGrep (0.00s)
PASS
ok github.com/google/codesearch/regexp 0.018s
$ go install
$
</pre>
<p>That "<code>go install</code>" subcommand installs the latest copy of the
package into the pkg directory. Because the go command can analyze the
dependency graph, "<code>go install</code>" also installs any packages that
this package imports but that are out of date, recursively.</p>
<p>Notice that "<code>go install</code>" was able to determine the name of the
import path for the package in the current directory, because of the convention
for directory naming. It would be a little more convenient if we could pick
the name of the directory where we kept source code, and we probably wouldn't
pick such a long name, but that ability would require additional configuration
and complexity in the tool. Typing an extra directory name or two is a small
price to pay for the increased simplicity and power.</p>
<h2>Limitations</h2>
<p>As mentioned above, the go command is not a general-purpose build
tool.
In particular, it does not have any facility for generating Go
source files <em>during</em> a build, although it does provide
<a href="/cmd/go/#hdr-Generate_Go_files_by_processing_source"><code>go</code>
<code>generate</code></a>,
which can automate the creation of Go files <em>before</em> the build.
For more advanced build setups, you may need to write a
makefile (or a configuration file for the build tool of your choice)
to run whatever tool creates the Go files and then check those generated source files
into your repository. This is more work for you, the package author,
but it is significantly less work for your users, who can use
"<code>go get</code>" without needing to obtain and build
any additional tools.</p>
<h2>More information</h2>
<p>For more information, read <a href="/doc/code.html">How to Write Go Code</a>
and see the <a href="/cmd/go/">go command documentation</a>.</p>

Просмотреть файл

@ -0,0 +1,8 @@
<!--{
"Title": "/doc/articles/"
}-->
<p>
See the <a href="/doc/#articles">Documents page</a> and the
<a href="/blog/index">Blog index</a> for a complete list of Go articles.
</p>

Просмотреть файл

@ -0,0 +1,389 @@
<!--{
"Title": "Data Race Detector",
"Template": true
}-->
<h2 id="Introduction">Introduction</h2>
<p>
Data races are among the most common and hardest to debug types of bugs in concurrent systems.
A data race occurs when two goroutines access the same variable concurrently and at least one of the accesses is a write.
See the <a href="/ref/mem/">The Go Memory Model</a> for details.
</p>
<p>
Here is an example of a data race that can lead to crashes and memory corruption:
</p>
<pre>
func main() {
c := make(chan bool)
m := make(map[string]string)
go func() {
m["1"] = "a" // First conflicting access.
c &lt;- true
}()
m["2"] = "b" // Second conflicting access.
&lt;-c
for k, v := range m {
fmt.Println(k, v)
}
}
</pre>
<h2 id="Usage">Usage</h2>
<p>
To help diagnose such bugs, Go includes a built-in data race detector.
To use it, add the <code>-race</code> flag to the go command:
</p>
<pre>
$ go test -race mypkg // to test the package
$ go run -race mysrc.go // to run the source file
$ go build -race mycmd // to build the command
$ go install -race mypkg // to install the package
</pre>
<h2 id="Report_Format">Report Format</h2>
<p>
When the race detector finds a data race in the program, it prints a report.
The report contains stack traces for conflicting accesses, as well as stacks where the involved goroutines were created.
Here is an example:
</p>
<pre>
WARNING: DATA RACE
Read by goroutine 185:
net.(*pollServer).AddFD()
src/net/fd_unix.go:89 +0x398
net.(*pollServer).WaitWrite()
src/net/fd_unix.go:247 +0x45
net.(*netFD).Write()
src/net/fd_unix.go:540 +0x4d4
net.(*conn).Write()
src/net/net.go:129 +0x101
net.func·060()
src/net/timeout_test.go:603 +0xaf
Previous write by goroutine 184:
net.setWriteDeadline()
src/net/sockopt_posix.go:135 +0xdf
net.setDeadline()
src/net/sockopt_posix.go:144 +0x9c
net.(*conn).SetDeadline()
src/net/net.go:161 +0xe3
net.func·061()
src/net/timeout_test.go:616 +0x3ed
Goroutine 185 (running) created at:
net.func·061()
src/net/timeout_test.go:609 +0x288
Goroutine 184 (running) created at:
net.TestProlongTimeout()
src/net/timeout_test.go:618 +0x298
testing.tRunner()
src/testing/testing.go:301 +0xe8
</pre>
<h2 id="Options">Options</h2>
<p>
The <code>GORACE</code> environment variable sets race detector options.
The format is:
</p>
<pre>
GORACE="option1=val1 option2=val2"
</pre>
<p>
The options are:
</p>
<ul>
<li>
<code>log_path</code> (default <code>stderr</code>): The race detector writes
its report to a file named <code>log_path.<em>pid</em></code>.
The special names <code>stdout</code>
and <code>stderr</code> cause reports to be written to standard output and
standard error, respectively.
</li>
<li>
<code>exitcode</code> (default <code>66</code>): The exit status to use when
exiting after a detected race.
</li>
<li>
<code>strip_path_prefix</code> (default <code>""</code>): Strip this prefix
from all reported file paths, to make reports more concise.
</li>
<li>
<code>history_size</code> (default <code>1</code>): The per-goroutine memory
access history is <code>32K * 2**history_size elements</code>.
Increasing this value can avoid a "failed to restore the stack" error in reports, at the
cost of increased memory usage.
</li>
<li>
<code>halt_on_error</code> (default <code>0</code>): Controls whether the program
exits after reporting first data race.
</li>
</ul>
<p>
Example:
</p>
<pre>
$ GORACE="log_path=/tmp/race/report strip_path_prefix=/my/go/sources/" go test -race
</pre>
<h2 id="Excluding_Tests">Excluding Tests</h2>
<p>
When you build with <code>-race</code> flag, the <code>go</code> command defines additional
<a href="/pkg/go/build/#hdr-Build_Constraints">build tag</a> <code>race</code>.
You can use the tag to exclude some code and tests when running the race detector.
Some examples:
</p>
<pre>
// +build !race
package foo
// The test contains a data race. See issue 123.
func TestFoo(t *testing.T) {
// ...
}
// The test fails under the race detector due to timeouts.
func TestBar(t *testing.T) {
// ...
}
// The test takes too long under the race detector.
func TestBaz(t *testing.T) {
// ...
}
</pre>
<h2 id="How_To_Use">How To Use</h2>
<p>
To start, run your tests using the race detector (<code>go test -race</code>).
The race detector only finds races that happen at runtime, so it can't find
races in code paths that are not executed.
If your tests have incomplete coverage,
you may find more races by running a binary built with <code>-race</code> under a realistic
workload.
</p>
<h2 id="Typical_Data_Races">Typical Data Races</h2>
<p>
Here are some typical data races. All of them can be detected with the race detector.
</p>
<h3 id="Race_on_loop_counter">Race on loop counter</h3>
<pre>
func main() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func() {
fmt.Println(i) // Not the 'i' you are looking for.
wg.Done()
}()
}
wg.Wait()
}
</pre>
<p>
The variable <code>i</code> in the function literal is the same variable used by the loop, so
the read in the goroutine races with the loop increment.
(This program typically prints 55555, not 01234.)
The program can be fixed by making a copy of the variable:
</p>
<pre>
func main() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func(j int) {
fmt.Println(j) // Good. Read local copy of the loop counter.
wg.Done()
}(i)
}
wg.Wait()
}
</pre>
<h3 id="Accidentally_shared_variable">Accidentally shared variable</h3>
<pre>
// ParallelWrite writes data to file1 and file2, returns the errors.
func ParallelWrite(data []byte) chan error {
res := make(chan error, 2)
f1, err := os.Create("file1")
if err != nil {
res &lt;- err
} else {
go func() {
// This err is shared with the main goroutine,
// so the write races with the write below.
_, err = f1.Write(data)
res &lt;- err
f1.Close()
}()
}
f2, err := os.Create("file2") // The second conflicting write to err.
if err != nil {
res &lt;- err
} else {
go func() {
_, err = f2.Write(data)
res &lt;- err
f2.Close()
}()
}
return res
}
</pre>
<p>
The fix is to introduce new variables in the goroutines (note the use of <code>:=</code>):
</p>
<pre>
...
_, err := f1.Write(data)
...
_, err := f2.Write(data)
...
</pre>
<h3 id="Unprotected_global_variable">Unprotected global variable</h3>
<p>
If the following code is called from several goroutines, it leads to races on the <code>service</code> map.
Concurrent reads and writes of the same map are not safe:
</p>
<pre>
var service map[string]net.Addr
func RegisterService(name string, addr net.Addr) {
service[name] = addr
}
func LookupService(name string) net.Addr {
return service[name]
}
</pre>
<p>
To make the code safe, protect the accesses with a mutex:
</p>
<pre>
var (
service map[string]net.Addr
serviceMu sync.Mutex
)
func RegisterService(name string, addr net.Addr) {
serviceMu.Lock()
defer serviceMu.Unlock()
service[name] = addr
}
func LookupService(name string) net.Addr {
serviceMu.Lock()
defer serviceMu.Unlock()
return service[name]
}
</pre>
<h3 id="Primitive_unprotected_variable">Primitive unprotected variable</h3>
<p>
Data races can happen on variables of primitive types as well (<code>bool</code>, <code>int</code>, <code>int64</code>, etc.),
as in this example:
</p>
<pre>
type Watchdog struct{ last int64 }
func (w *Watchdog) KeepAlive() {
w.last = time.Now().UnixNano() // First conflicting access.
}
func (w *Watchdog) Start() {
go func() {
for {
time.Sleep(time.Second)
// Second conflicting access.
if w.last < time.Now().Add(-10*time.Second).UnixNano() {
fmt.Println("No keepalives for 10 seconds. Dying.")
os.Exit(1)
}
}
}()
}
</pre>
<p>
Even such "innocent" data races can lead to hard-to-debug problems caused by
non-atomicity of the memory accesses,
interference with compiler optimizations,
or reordering issues accessing processor memory .
</p>
<p>
A typical fix for this race is to use a channel or a mutex.
To preserve the lock-free behavior, one can also use the
<a href="/pkg/sync/atomic/"><code>sync/atomic</code></a> package.
</p>
<pre>
type Watchdog struct{ last int64 }
func (w *Watchdog) KeepAlive() {
atomic.StoreInt64(&amp;w.last, time.Now().UnixNano())
}
func (w *Watchdog) Start() {
go func() {
for {
time.Sleep(time.Second)
if atomic.LoadInt64(&amp;w.last) < time.Now().Add(-10*time.Second).UnixNano() {
fmt.Println("No keepalives for 10 seconds. Dying.")
os.Exit(1)
}
}
}()
}
</pre>
<h2 id="Supported_Systems">Supported Systems</h2>
<p>
The race detector runs on <code>darwin/amd64</code>, <code>freebsd/amd64</code>,
<code>linux/amd64</code>, and <code>windows/amd64</code>.
</p>
<h2 id="Runtime_Overheads">Runtime Overhead</h2>
<p>
The cost of race detection varies by program, but for a typical program, memory
usage may increase by 5-10x and execution time by 2-20x.
</p>

Просмотреть файл

@ -0,0 +1,6 @@
<h1>Editing {{.Title}}</h1>
<form action="/save/{{.Title}}" method="POST">
<div><textarea name="body" rows="20" cols="80">{{printf "%s" .Body}}</textarea></div>
<div><input type="submit" value="Save"></div>
</form>

Просмотреть файл

@ -0,0 +1,103 @@
// Copyright 2010 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 main
import (
"errors"
"html/template"
"io/ioutil"
"log"
"net/http"
"regexp"
)
type Page struct {
Title string
Body []byte
}
func (p *Page) save() error {
filename := p.Title + ".txt"
return ioutil.WriteFile(filename, p.Body, 0600)
}
func loadPage(title string) (*Page, error) {
filename := title + ".txt"
body, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return &Page{Title: title, Body: body}, nil
}
func viewHandler(w http.ResponseWriter, r *http.Request) {
title, err := getTitle(w, r)
if err != nil {
return
}
p, err := loadPage(title)
if err != nil {
http.Redirect(w, r, "/edit/"+title, http.StatusFound)
return
}
renderTemplate(w, "view", p)
}
func editHandler(w http.ResponseWriter, r *http.Request) {
title, err := getTitle(w, r)
if err != nil {
return
}
p, err := loadPage(title)
if err != nil {
p = &Page{Title: title}
}
renderTemplate(w, "edit", p)
}
func saveHandler(w http.ResponseWriter, r *http.Request) {
title, err := getTitle(w, r)
if err != nil {
return
}
body := r.FormValue("body")
p := &Page{Title: title, Body: []byte(body)}
err = p.save()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/view/"+title, http.StatusFound)
}
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
t, err := template.ParseFiles(tmpl + ".html")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = t.Execute(w, p)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")
func getTitle(w http.ResponseWriter, r *http.Request) (string, error) {
m := validPath.FindStringSubmatch(r.URL.Path)
if m == nil {
http.NotFound(w, r)
return "", errors.New("Invalid Page Title")
}
return m[2], nil // The title is the second subexpression.
}
func main() {
http.HandleFunc("/view/", viewHandler)
http.HandleFunc("/edit/", editHandler)
http.HandleFunc("/save/", saveHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}

Просмотреть файл

@ -0,0 +1,54 @@
// Copyright 2010 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 main
import (
"html/template"
"io/ioutil"
"log"
"net/http"
)
type Page struct {
Title string
Body []byte
}
func (p *Page) save() error {
filename := p.Title + ".txt"
return ioutil.WriteFile(filename, p.Body, 0600)
}
func loadPage(title string) (*Page, error) {
filename := title + ".txt"
body, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return &Page{Title: title, Body: body}, nil
}
func editHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/edit/"):]
p, err := loadPage(title)
if err != nil {
p = &Page{Title: title}
}
t, _ := template.ParseFiles("edit.html")
t.Execute(w, p)
}
func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/view/"):]
p, _ := loadPage(title)
t, _ := template.ParseFiles("view.html")
t.Execute(w, p)
}
func main() {
http.HandleFunc("/view/", viewHandler)
http.HandleFunc("/edit/", editHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}

Просмотреть файл

@ -0,0 +1,92 @@
// Copyright 2010 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 main
import (
"html/template"
"io/ioutil"
"log"
"net/http"
"regexp"
)
type Page struct {
Title string
Body []byte
}
func (p *Page) save() error {
filename := p.Title + ".txt"
return ioutil.WriteFile(filename, p.Body, 0600)
}
func loadPage(title string) (*Page, error) {
filename := title + ".txt"
body, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return &Page{Title: title, Body: body}, nil
}
func viewHandler(w http.ResponseWriter, r *http.Request, title string) {
p, err := loadPage(title)
if err != nil {
http.Redirect(w, r, "/edit/"+title, http.StatusFound)
return
}
renderTemplate(w, "view", p)
}
func editHandler(w http.ResponseWriter, r *http.Request, title string) {
p, err := loadPage(title)
if err != nil {
p = &Page{Title: title}
}
renderTemplate(w, "edit", p)
}
func saveHandler(w http.ResponseWriter, r *http.Request, title string) {
body := r.FormValue("body")
p := &Page{Title: title, Body: []byte(body)}
err := p.save()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/view/"+title, http.StatusFound)
}
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
t, err := template.ParseFiles(tmpl + ".html")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = t.Execute(w, p)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")
func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
m := validPath.FindStringSubmatch(r.URL.Path)
if m == nil {
http.NotFound(w, r)
return
}
fn(w, r, m[2])
}
}
func main() {
http.HandleFunc("/view/", makeHandler(viewHandler))
http.HandleFunc("/edit/", makeHandler(editHandler))
http.HandleFunc("/save/", makeHandler(saveHandler))
log.Fatal(http.ListenAndServe(":8080", nil))
}

Просмотреть файл

@ -0,0 +1,66 @@
// Copyright 2010 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 main
import (
"html/template"
"io/ioutil"
"log"
"net/http"
)
type Page struct {
Title string
Body []byte
}
func (p *Page) save() error {
filename := p.Title + ".txt"
return ioutil.WriteFile(filename, p.Body, 0600)
}
func loadPage(title string) (*Page, error) {
filename := title + ".txt"
body, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return &Page{Title: title, Body: body}, nil
}
func editHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/edit/"):]
p, err := loadPage(title)
if err != nil {
p = &Page{Title: title}
}
renderTemplate(w, "edit", p)
}
func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/view/"):]
p, _ := loadPage(title)
renderTemplate(w, "view", p)
}
func saveHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/save/"):]
body := r.FormValue("body")
p := &Page{Title: title, Body: []byte(body)}
p.save()
http.Redirect(w, r, "/view/"+title, http.StatusFound)
}
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
t, _ := template.ParseFiles(tmpl + ".html")
t.Execute(w, p)
}
func main() {
http.HandleFunc("/view/", viewHandler)
http.HandleFunc("/edit/", editHandler)
http.HandleFunc("/save/", saveHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}

Просмотреть файл

@ -0,0 +1,27 @@
--- final.go 2017-08-31 13:19:00.422925489 -0700
+++ final-test.go 2017-08-31 13:23:43.381391659 -0700
@@ -8,6 +8,7 @@
"html/template"
"io/ioutil"
"log"
+ "net"
"net/http"
"regexp"
)
@@ -86,5 +87,15 @@
http.HandleFunc("/edit/", makeHandler(editHandler))
http.HandleFunc("/save/", makeHandler(saveHandler))
- log.Fatal(http.ListenAndServe(":8080", nil))
+ l, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ log.Fatal(err)
+ }
+ err = ioutil.WriteFile("final-test-port.txt", []byte(l.Addr().String()), 0644)
+ if err != nil {
+ log.Fatal(err)
+ }
+ s := &http.Server{}
+ s.Serve(l)
+ return
}

Просмотреть файл

@ -0,0 +1,90 @@
// Copyright 2010 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 main
import (
"html/template"
"io/ioutil"
"log"
"net/http"
"regexp"
)
type Page struct {
Title string
Body []byte
}
func (p *Page) save() error {
filename := p.Title + ".txt"
return ioutil.WriteFile(filename, p.Body, 0600)
}
func loadPage(title string) (*Page, error) {
filename := title + ".txt"
body, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return &Page{Title: title, Body: body}, nil
}
func viewHandler(w http.ResponseWriter, r *http.Request, title string) {
p, err := loadPage(title)
if err != nil {
http.Redirect(w, r, "/edit/"+title, http.StatusFound)
return
}
renderTemplate(w, "view", p)
}
func editHandler(w http.ResponseWriter, r *http.Request, title string) {
p, err := loadPage(title)
if err != nil {
p = &Page{Title: title}
}
renderTemplate(w, "edit", p)
}
func saveHandler(w http.ResponseWriter, r *http.Request, title string) {
body := r.FormValue("body")
p := &Page{Title: title, Body: []byte(body)}
err := p.save()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/view/"+title, http.StatusFound)
}
var templates = template.Must(template.ParseFiles("edit.html", "view.html"))
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
err := templates.ExecuteTemplate(w, tmpl+".html", p)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")
func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
m := validPath.FindStringSubmatch(r.URL.Path)
if m == nil {
http.NotFound(w, r)
return
}
fn(w, r, m[2])
}
}
func main() {
http.HandleFunc("/view/", makeHandler(viewHandler))
http.HandleFunc("/edit/", makeHandler(editHandler))
http.HandleFunc("/save/", makeHandler(saveHandler))
log.Fatal(http.ListenAndServe(":8080", nil))
}

Просмотреть файл

@ -0,0 +1,63 @@
// Copyright 2011 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 main
import (
"flag"
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"strings"
"time"
)
var (
post = flag.String("post", "", "urlencoded form data to POST")
addr = flag.Bool("addr", false, "find open address and print to stdout")
wait = flag.Duration("wait_for_port", 0, "if non-zero, the amount of time to wait for the address to become available")
)
func main() {
flag.Parse()
if *addr {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
log.Fatal(err)
}
defer l.Close()
fmt.Print(l.Addr())
return
}
url := flag.Arg(0)
if url == "" {
log.Fatal("no url supplied")
}
var r *http.Response
var err error
loopUntil := time.Now().Add(*wait)
for {
if *post != "" {
b := strings.NewReader(*post)
r, err = http.Post(url, "application/x-www-form-urlencoded", b)
} else {
r, err = http.Get(url)
}
if err == nil || *wait == 0 || time.Now().After(loopUntil) {
break
}
time.Sleep(100 * time.Millisecond)
}
if err != nil {
log.Fatal(err)
}
defer r.Body.Close()
_, err = io.Copy(os.Stdout, r.Body)
if err != nil {
log.Fatal(err)
}
}

Просмотреть файл

@ -0,0 +1,16 @@
package main
import (
"fmt"
"log"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
}
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}

Просмотреть файл

@ -0,0 +1,740 @@
<!--{
"Title": "Writing Web Applications",
"Template": true
}-->
<h2>Introduction</h2>
<p>
Covered in this tutorial:
</p>
<ul>
<li>Creating a data structure with load and save methods</li>
<li>Using the <code>net/http</code> package to build web applications
<li>Using the <code>html/template</code> package to process HTML templates</li>
<li>Using the <code>regexp</code> package to validate user input</li>
<li>Using closures</li>
</ul>
<p>
Assumed knowledge:
</p>
<ul>
<li>Programming experience</li>
<li>Understanding of basic web technologies (HTTP, HTML)</li>
<li>Some UNIX/DOS command-line knowledge</li>
</ul>
<h2>Getting Started</h2>
<p>
At present, you need to have a FreeBSD, Linux, OS X, or Windows machine to run Go.
We will use <code>$</code> to represent the command prompt.
</p>
<p>
Install Go (see the <a href="/doc/install">Installation Instructions</a>).
</p>
<p>
Make a new directory for this tutorial inside your <code>GOPATH</code> and cd to it:
</p>
<pre>
$ mkdir gowiki
$ cd gowiki
</pre>
<p>
Create a file named <code>wiki.go</code>, open it in your favorite editor, and
add the following lines:
</p>
<pre>
package main
import (
"fmt"
"io/ioutil"
)
</pre>
<p>
We import the <code>fmt</code> and <code>ioutil</code> packages from the Go
standard library. Later, as we implement additional functionality, we will
add more packages to this <code>import</code> declaration.
</p>
<h2>Data Structures</h2>
<p>
Let's start by defining the data structures. A wiki consists of a series of
interconnected pages, each of which has a title and a body (the page content).
Here, we define <code>Page</code> as a struct with two fields representing
the title and body.
</p>
{{code "doc/articles/wiki/part1.go" `/^type Page/` `/}/`}}
<p>
The type <code>[]byte</code> means "a <code>byte</code> slice".
(See <a href="/doc/articles/slices_usage_and_internals.html">Slices: usage and
internals</a> for more on slices.)
The <code>Body</code> element is a <code>[]byte</code> rather than
<code>string</code> because that is the type expected by the <code>io</code>
libraries we will use, as you'll see below.
</p>
<p>
The <code>Page</code> struct describes how page data will be stored in memory.
But what about persistent storage? We can address that by creating a
<code>save</code> method on <code>Page</code>:
</p>
{{code "doc/articles/wiki/part1.go" `/^func.*Page.*save/` `/}/`}}
<p>
This method's signature reads: "This is a method named <code>save</code> that
takes as its receiver <code>p</code>, a pointer to <code>Page</code> . It takes
no parameters, and returns a value of type <code>error</code>."
</p>
<p>
This method will save the <code>Page</code>'s <code>Body</code> to a text
file. For simplicity, we will use the <code>Title</code> as the file name.
</p>
<p>
The <code>save</code> method returns an <code>error</code> value because
that is the return type of <code>WriteFile</code> (a standard library function
that writes a byte slice to a file). The <code>save</code> method returns the
error value, to let the application handle it should anything go wrong while
writing the file. If all goes well, <code>Page.save()</code> will return
<code>nil</code> (the zero-value for pointers, interfaces, and some other
types).
</p>
<p>
The octal integer literal <code>0600</code>, passed as the third parameter to
<code>WriteFile</code>, indicates that the file should be created with
read-write permissions for the current user only. (See the Unix man page
<code>open(2)</code> for details.)
</p>
<p>
In addition to saving pages, we will want to load pages, too:
</p>
{{code "doc/articles/wiki/part1-noerror.go" `/^func loadPage/` `/^}/`}}
<p>
The function <code>loadPage</code> constructs the file name from the title
parameter, reads the file's contents into a new variable <code>body</code>, and
returns a pointer to a <code>Page</code> literal constructed with the proper
title and body values.
</p>
<p>
Functions can return multiple values. The standard library function
<code>io.ReadFile</code> returns <code>[]byte</code> and <code>error</code>.
In <code>loadPage</code>, error isn't being handled yet; the "blank identifier"
represented by the underscore (<code>_</code>) symbol is used to throw away the
error return value (in essence, assigning the value to nothing).
</p>
<p>
But what happens if <code>ReadFile</code> encounters an error? For example,
the file might not exist. We should not ignore such errors. Let's modify the
function to return <code>*Page</code> and <code>error</code>.
</p>
{{code "doc/articles/wiki/part1.go" `/^func loadPage/` `/^}/`}}
<p>
Callers of this function can now check the second parameter; if it is
<code>nil</code> then it has successfully loaded a Page. If not, it will be an
<code>error</code> that can be handled by the caller (see the
<a href="/ref/spec#Errors">language specification</a> for details).
</p>
<p>
At this point we have a simple data structure and the ability to save to and
load from a file. Let's write a <code>main</code> function to test what we've
written:
</p>
{{code "doc/articles/wiki/part1.go" `/^func main/` `/^}/`}}
<p>
After compiling and executing this code, a file named <code>TestPage.txt</code>
would be created, containing the contents of <code>p1</code>. The file would
then be read into the struct <code>p2</code>, and its <code>Body</code> element
printed to the screen.
</p>
<p>
You can compile and run the program like this:
</p>
<pre>
$ go build wiki.go
$ ./wiki
This is a sample Page.
</pre>
<p>
(If you're using Windows you must type "<code>wiki</code>" without the
"<code>./</code>" to run the program.)
</p>
<p>
<a href="part1.go">Click here to view the code we've written so far.</a>
</p>
<h2>Introducing the <code>net/http</code> package (an interlude)</h2>
<p>
Here's a full working example of a simple web server:
</p>
{{code "doc/articles/wiki/http-sample.go"}}
<p>
The <code>main</code> function begins with a call to
<code>http.HandleFunc</code>, which tells the <code>http</code> package to
handle all requests to the web root (<code>"/"</code>) with
<code>handler</code>.
</p>
<p>
It then calls <code>http.ListenAndServe</code>, specifying that it should
listen on port 8080 on any interface (<code>":8080"</code>). (Don't
worry about its second parameter, <code>nil</code>, for now.)
This function will block until the program is terminated.
</p>
<p>
<code>ListenAndServe</code> always returns an error, since it only returns when an
unexpected error occurs.
In order to log that error we wrap the function call with <code>log.Fatal</code>.
</p>
<p>
The function <code>handler</code> is of the type <code>http.HandlerFunc</code>.
It takes an <code>http.ResponseWriter</code> and an <code>http.Request</code> as
its arguments.
</p>
<p>
An <code>http.ResponseWriter</code> value assembles the HTTP server's response; by writing
to it, we send data to the HTTP client.
</p>
<p>
An <code>http.Request</code> is a data structure that represents the client
HTTP request. <code>r.URL.Path</code> is the path component
of the request URL. The trailing <code>[1:]</code> means
"create a sub-slice of <code>Path</code> from the 1st character to the end."
This drops the leading "/" from the path name.
</p>
<p>
If you run this program and access the URL:
</p>
<pre>http://localhost:8080/monkeys</pre>
<p>
the program would present a page containing:
</p>
<pre>Hi there, I love monkeys!</pre>
<h2>Using <code>net/http</code> to serve wiki pages</h2>
<p>
To use the <code>net/http</code> package, it must be imported:
</p>
<pre>
import (
"fmt"
"io/ioutil"
<b>"net/http"</b>
)
</pre>
<p>
Let's create a handler, <code>viewHandler</code> that will allow users to
view a wiki page. It will handle URLs prefixed with "/view/".
</p>
{{code "doc/articles/wiki/part2.go" `/^func viewHandler/` `/^}/`}}
<p>
Again, note the use of <code>_</code> to ignore the <code>error</code>
return value from <code>loadPage</code>. This is done here for simplicity
and generally considered bad practice. We will attend to this later.
</p>
<p>
First, this function extracts the page title from <code>r.URL.Path</code>,
the path component of the request URL.
The <code>Path</code> is re-sliced with <code>[len("/view/"):]</code> to drop
the leading <code>"/view/"</code> component of the request path.
This is because the path will invariably begin with <code>"/view/"</code>,
which is not part of the page's title.
</p>
<p>
The function then loads the page data, formats the page with a string of simple
HTML, and writes it to <code>w</code>, the <code>http.ResponseWriter</code>.
</p>
<p>
To use this handler, we rewrite our <code>main</code> function to
initialize <code>http</code> using the <code>viewHandler</code> to handle
any requests under the path <code>/view/</code>.
</p>
{{code "doc/articles/wiki/part2.go" `/^func main/` `/^}/`}}
<p>
<a href="part2.go">Click here to view the code we've written so far.</a>
</p>
<p>
Let's create some page data (as <code>test.txt</code>), compile our code, and
try serving a wiki page.
</p>
<p>
Open <code>test.txt</code> file in your editor, and save the string "Hello world" (without quotes)
in it.
</p>
<pre>
$ go build wiki.go
$ ./wiki
</pre>
<p>
(If you're using Windows you must type "<code>wiki</code>" without the
"<code>./</code>" to run the program.)
</p>
<p>
With this web server running, a visit to <code><a
href="http://localhost:8080/view/test">http://localhost:8080/view/test</a></code>
should show a page titled "test" containing the words "Hello world".
</p>
<h2>Editing Pages</h2>
<p>
A wiki is not a wiki without the ability to edit pages. Let's create two new
handlers: one named <code>editHandler</code> to display an 'edit page' form,
and the other named <code>saveHandler</code> to save the data entered via the
form.
</p>
<p>
First, we add them to <code>main()</code>:
</p>
{{code "doc/articles/wiki/final-noclosure.go" `/^func main/` `/^}/`}}
<p>
The function <code>editHandler</code> loads the page
(or, if it doesn't exist, create an empty <code>Page</code> struct),
and displays an HTML form.
</p>
{{code "doc/articles/wiki/notemplate.go" `/^func editHandler/` `/^}/`}}
<p>
This function will work fine, but all that hard-coded HTML is ugly.
Of course, there is a better way.
</p>
<h2>The <code>html/template</code> package</h2>
<p>
The <code>html/template</code> package is part of the Go standard library.
We can use <code>html/template</code> to keep the HTML in a separate file,
allowing us to change the layout of our edit page without modifying the
underlying Go code.
</p>
<p>
First, we must add <code>html/template</code> to the list of imports. We
also won't be using <code>fmt</code> anymore, so we have to remove that.
</p>
<pre>
import (
<b>"html/template"</b>
"io/ioutil"
"net/http"
)
</pre>
<p>
Let's create a template file containing the HTML form.
Open a new file named <code>edit.html</code>, and add the following lines:
</p>
{{code "doc/articles/wiki/edit.html"}}
<p>
Modify <code>editHandler</code> to use the template, instead of the hard-coded
HTML:
</p>
{{code "doc/articles/wiki/final-noerror.go" `/^func editHandler/` `/^}/`}}
<p>
The function <code>template.ParseFiles</code> will read the contents of
<code>edit.html</code> and return a <code>*template.Template</code>.
</p>
<p>
The method <code>t.Execute</code> executes the template, writing the
generated HTML to the <code>http.ResponseWriter</code>.
The <code>.Title</code> and <code>.Body</code> dotted identifiers refer to
<code>p.Title</code> and <code>p.Body</code>.
</p>
<p>
Template directives are enclosed in double curly braces.
The <code>printf "%s" .Body</code> instruction is a function call
that outputs <code>.Body</code> as a string instead of a stream of bytes,
the same as a call to <code>fmt.Printf</code>.
The <code>html/template</code> package helps guarantee that only safe and
correct-looking HTML is generated by template actions. For instance, it
automatically escapes any greater than sign (<code>&gt;</code>), replacing it
with <code>&amp;gt;</code>, to make sure user data does not corrupt the form
HTML.
</p>
<p>
Since we're working with templates now, let's create a template for our
<code>viewHandler</code> called <code>view.html</code>:
</p>
{{code "doc/articles/wiki/view.html"}}
<p>
Modify <code>viewHandler</code> accordingly:
</p>
{{code "doc/articles/wiki/final-noerror.go" `/^func viewHandler/` `/^}/`}}
<p>
Notice that we've used almost exactly the same templating code in both
handlers. Let's remove this duplication by moving the templating code
to its own function:
</p>
{{code "doc/articles/wiki/final-template.go" `/^func renderTemplate/` `/^}/`}}
<p>
And modify the handlers to use that function:
</p>
{{code "doc/articles/wiki/final-template.go" `/^func viewHandler/` `/^}/`}}
{{code "doc/articles/wiki/final-template.go" `/^func editHandler/` `/^}/`}}
<p>
If we comment out the registration of our unimplemented save handler in
<code>main</code>, we can once again build and test our program.
<a href="part3.go">Click here to view the code we've written so far.</a>
</p>
<h2>Handling non-existent pages</h2>
<p>
What if you visit <a href="http://localhost:8080/view/APageThatDoesntExist">
<code>/view/APageThatDoesntExist</code></a>? You'll see a page containing
HTML. This is because it ignores the error return value from
<code>loadPage</code> and continues to try and fill out the template
with no data. Instead, if the requested Page doesn't exist, it should
redirect the client to the edit Page so the content may be created:
</p>
{{code "doc/articles/wiki/part3-errorhandling.go" `/^func viewHandler/` `/^}/`}}
<p>
The <code>http.Redirect</code> function adds an HTTP status code of
<code>http.StatusFound</code> (302) and a <code>Location</code>
header to the HTTP response.
</p>
<h2>Saving Pages</h2>
<p>
The function <code>saveHandler</code> will handle the submission of forms
located on the edit pages. After uncommenting the related line in
<code>main</code>, let's implement the handler:
</p>
{{code "doc/articles/wiki/final-template.go" `/^func saveHandler/` `/^}/`}}
<p>
The page title (provided in the URL) and the form's only field,
<code>Body</code>, are stored in a new <code>Page</code>.
The <code>save()</code> method is then called to write the data to a file,
and the client is redirected to the <code>/view/</code> page.
</p>
<p>
The value returned by <code>FormValue</code> is of type <code>string</code>.
We must convert that value to <code>[]byte</code> before it will fit into
the <code>Page</code> struct. We use <code>[]byte(body)</code> to perform
the conversion.
</p>
<h2>Error handling</h2>
<p>
There are several places in our program where errors are being ignored. This
is bad practice, not least because when an error does occur the program will
have unintended behavior. A better solution is to handle the errors and return
an error message to the user. That way if something does go wrong, the server
will function exactly how we want and the user can be notified.
</p>
<p>
First, let's handle the errors in <code>renderTemplate</code>:
</p>
{{code "doc/articles/wiki/final-parsetemplate.go" `/^func renderTemplate/` `/^}/`}}
<p>
The <code>http.Error</code> function sends a specified HTTP response code
(in this case "Internal Server Error") and error message.
Already the decision to put this in a separate function is paying off.
</p>
<p>
Now let's fix up <code>saveHandler</code>:
</p>
{{code "doc/articles/wiki/part3-errorhandling.go" `/^func saveHandler/` `/^}/`}}
<p>
Any errors that occur during <code>p.save()</code> will be reported
to the user.
</p>
<h2>Template caching</h2>
<p>
There is an inefficiency in this code: <code>renderTemplate</code> calls
<code>ParseFiles</code> every time a page is rendered.
A better approach would be to call <code>ParseFiles</code> once at program
initialization, parsing all templates into a single <code>*Template</code>.
Then we can use the
<a href="/pkg/html/template/#Template.ExecuteTemplate"><code>ExecuteTemplate</code></a>
method to render a specific template.
</p>
<p>
First we create a global variable named <code>templates</code>, and initialize
it with <code>ParseFiles</code>.
</p>
{{code "doc/articles/wiki/final.go" `/var templates/`}}
<p>
The function <code>template.Must</code> is a convenience wrapper that panics
when passed a non-nil <code>error</code> value, and otherwise returns the
<code>*Template</code> unaltered. A panic is appropriate here; if the templates
can't be loaded the only sensible thing to do is exit the program.
</p>
<p>
The <code>ParseFiles</code> function takes any number of string arguments that
identify our template files, and parses those files into templates that are
named after the base file name. If we were to add more templates to our
program, we would add their names to the <code>ParseFiles</code> call's
arguments.
</p>
<p>
We then modify the <code>renderTemplate</code> function to call the
<code>templates.ExecuteTemplate</code> method with the name of the appropriate
template:
</p>
{{code "doc/articles/wiki/final.go" `/func renderTemplate/` `/^}/`}}
<p>
Note that the template name is the template file name, so we must
append <code>".html"</code> to the <code>tmpl</code> argument.
</p>
<h2>Validation</h2>
<p>
As you may have observed, this program has a serious security flaw: a user
can supply an arbitrary path to be read/written on the server. To mitigate
this, we can write a function to validate the title with a regular expression.
</p>
<p>
First, add <code>"regexp"</code> to the <code>import</code> list.
Then we can create a global variable to store our validation
expression:
</p>
{{code "doc/articles/wiki/final-noclosure.go" `/^var validPath/`}}
<p>
The function <code>regexp.MustCompile</code> will parse and compile the
regular expression, and return a <code>regexp.Regexp</code>.
<code>MustCompile</code> is distinct from <code>Compile</code> in that it will
panic if the expression compilation fails, while <code>Compile</code> returns
an <code>error</code> as a second parameter.
</p>
<p>
Now, let's write a function that uses the <code>validPath</code>
expression to validate path and extract the page title:
</p>
{{code "doc/articles/wiki/final-noclosure.go" `/func getTitle/` `/^}/`}}
<p>
If the title is valid, it will be returned along with a <code>nil</code>
error value. If the title is invalid, the function will write a
"404 Not Found" error to the HTTP connection, and return an error to the
handler. To create a new error, we have to import the <code>errors</code>
package.
</p>
<p>
Let's put a call to <code>getTitle</code> in each of the handlers:
</p>
{{code "doc/articles/wiki/final-noclosure.go" `/^func viewHandler/` `/^}/`}}
{{code "doc/articles/wiki/final-noclosure.go" `/^func editHandler/` `/^}/`}}
{{code "doc/articles/wiki/final-noclosure.go" `/^func saveHandler/` `/^}/`}}
<h2>Introducing Function Literals and Closures</h2>
<p>
Catching the error condition in each handler introduces a lot of repeated code.
What if we could wrap each of the handlers in a function that does this
validation and error checking? Go's
<a href="/ref/spec#Function_literals">function
literals</a> provide a powerful means of abstracting functionality
that can help us here.
</p>
<p>
First, we re-write the function definition of each of the handlers to accept
a title string:
</p>
<pre>
func viewHandler(w http.ResponseWriter, r *http.Request, title string)
func editHandler(w http.ResponseWriter, r *http.Request, title string)
func saveHandler(w http.ResponseWriter, r *http.Request, title string)
</pre>
<p>
Now let's define a wrapper function that <i>takes a function of the above
type</i>, and returns a function of type <code>http.HandlerFunc</code>
(suitable to be passed to the function <code>http.HandleFunc</code>):
</p>
<pre>
func makeHandler(fn func (http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Here we will extract the page title from the Request,
// and call the provided handler 'fn'
}
}
</pre>
<p>
The returned function is called a closure because it encloses values defined
outside of it. In this case, the variable <code>fn</code> (the single argument
to <code>makeHandler</code>) is enclosed by the closure. The variable
<code>fn</code> will be one of our save, edit, or view handlers.
</p>
<p>
Now we can take the code from <code>getTitle</code> and use it here
(with some minor modifications):
</p>
{{code "doc/articles/wiki/final.go" `/func makeHandler/` `/^}/`}}
<p>
The closure returned by <code>makeHandler</code> is a function that takes
an <code>http.ResponseWriter</code> and <code>http.Request</code> (in other
words, an <code>http.HandlerFunc</code>).
The closure extracts the <code>title</code> from the request path, and
validates it with the <code>TitleValidator</code> regexp. If the
<code>title</code> is invalid, an error will be written to the
<code>ResponseWriter</code> using the <code>http.NotFound</code> function.
If the <code>title</code> is valid, the enclosed handler function
<code>fn</code> will be called with the <code>ResponseWriter</code>,
<code>Request</code>, and <code>title</code> as arguments.
</p>
<p>
Now we can wrap the handler functions with <code>makeHandler</code> in
<code>main</code>, before they are registered with the <code>http</code>
package:
</p>
{{code "doc/articles/wiki/final.go" `/func main/` `/^}/`}}
<p>
Finally we remove the calls to <code>getTitle</code> from the handler functions,
making them much simpler:
</p>
{{code "doc/articles/wiki/final.go" `/^func viewHandler/` `/^}/`}}
{{code "doc/articles/wiki/final.go" `/^func editHandler/` `/^}/`}}
{{code "doc/articles/wiki/final.go" `/^func saveHandler/` `/^}/`}}
<h2>Try it out!</h2>
<p>
<a href="final.go">Click here to view the final code listing.</a>
</p>
<p>
Recompile the code, and run the app:
</p>
<pre>
$ go build wiki.go
$ ./wiki
</pre>
<p>
Visiting <a href="http://localhost:8080/view/ANewPage">http://localhost:8080/view/ANewPage</a>
should present you with the page edit form. You should then be able to
enter some text, click 'Save', and be redirected to the newly created page.
</p>
<h2>Other tasks</h2>
<p>
Here are some simple tasks you might want to tackle on your own:
</p>
<ul>
<li>Store templates in <code>tmpl/</code> and page data in <code>data/</code>.
<li>Add a handler to make the web root redirect to
<code>/view/FrontPage</code>.</li>
<li>Spruce up the page templates by making them valid HTML and adding some
CSS rules.</li>
<li>Implement inter-page linking by converting instances of
<code>[PageName]</code> to <br>
<code>&lt;a href="/view/PageName"&gt;PageName&lt;/a&gt;</code>.
(hint: you could use <code>regexp.ReplaceAllFunc</code> to do this)
</li>
</ul>

Просмотреть файл

@ -0,0 +1,57 @@
// Copyright 2010 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 main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
)
type Page struct {
Title string
Body []byte
}
func (p *Page) save() error {
filename := p.Title + ".txt"
return ioutil.WriteFile(filename, p.Body, 0600)
}
func loadPage(title string) (*Page, error) {
filename := title + ".txt"
body, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return &Page{Title: title, Body: body}, nil
}
func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/view/"):]
p, _ := loadPage(title)
fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.Title, p.Body)
}
func editHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/edit/"):]
p, err := loadPage(title)
if err != nil {
p = &Page{Title: title}
}
fmt.Fprintf(w, "<h1>Editing %s</h1>"+
"<form action=\"/save/%s\" method=\"POST\">"+
"<textarea name=\"body\">%s</textarea><br>"+
"<input type=\"submit\" value=\"Save\">"+
"</form>",
p.Title, p.Title, p.Body)
}
func main() {
http.HandleFunc("/view/", viewHandler)
http.HandleFunc("/edit/", editHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}

Просмотреть файл

@ -0,0 +1,33 @@
// Copyright 2010 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 main
import (
"fmt"
"io/ioutil"
)
type Page struct {
Title string
Body []byte
}
func (p *Page) save() error {
filename := p.Title + ".txt"
return ioutil.WriteFile(filename, p.Body, 0600)
}
func loadPage(title string) *Page {
filename := title + ".txt"
body, _ := ioutil.ReadFile(filename)
return &Page{Title: title, Body: body}
}
func main() {
p1 := &Page{Title: "TestPage", Body: []byte("This is a sample page.")}
p1.save()
p2 := loadPage("TestPage")
fmt.Println(string(p2.Body))
}

Просмотреть файл

@ -0,0 +1,36 @@
// Copyright 2010 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 main
import (
"fmt"
"io/ioutil"
)
type Page struct {
Title string
Body []byte
}
func (p *Page) save() error {
filename := p.Title + ".txt"
return ioutil.WriteFile(filename, p.Body, 0600)
}
func loadPage(title string) (*Page, error) {
filename := title + ".txt"
body, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return &Page{Title: title, Body: body}, nil
}
func main() {
p1 := &Page{Title: "TestPage", Body: []byte("This is a sample Page.")}
p1.save()
p2, _ := loadPage("TestPage")
fmt.Println(string(p2.Body))
}

Просмотреть файл

@ -0,0 +1,42 @@
// Copyright 2010 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 main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
)
type Page struct {
Title string
Body []byte
}
func (p *Page) save() error {
filename := p.Title + ".txt"
return ioutil.WriteFile(filename, p.Body, 0600)
}
func loadPage(title string) (*Page, error) {
filename := title + ".txt"
body, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return &Page{Title: title, Body: body}, nil
}
func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/view/"):]
p, _ := loadPage(title)
fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.Title, p.Body)
}
func main() {
http.HandleFunc("/view/", viewHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше