blob: e77de4ac5f97030ea5acc0e9510e7aa5b3b0d5fe [file] [log] [blame]
// Copyright 2013 The Go Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd.
// Package lintapp implements the go-lint.appspot.com server.
package lintapp
import (
"bytes"
"context"
"encoding/gob"
"fmt"
"html/template"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"google.golang.org/appengine"
"google.golang.org/appengine/datastore"
"google.golang.org/appengine/log"
"google.golang.org/appengine/urlfetch"
"github.com/golang/gddo/gosrc"
"github.com/golang/gddo/httputil"
"github.com/golang/lint"
)
func init() {
http.Handle("/", handlerFunc(serveRoot))
http.Handle("/-/bot", handlerFunc(serveBot))
http.Handle("/-/refresh", handlerFunc(serveRefresh))
if s := os.Getenv("CONTACT_EMAIL"); s != "" {
contactEmail = s
}
}
var (
contactEmail = "golang-dev@googlegroups.com"
homeTemplate = parseTemplate("common.html", "index.html")
packageTemplate = parseTemplate("common.html", "package.html")
errorTemplate = parseTemplate("common.html", "error.html")
templateFuncs = template.FuncMap{
"timeago": timeagoFn,
"contactEmail": contactEmailFn,
}
)
func parseTemplate(fnames ...string) *template.Template {
paths := make([]string, len(fnames))
for i := range fnames {
paths[i] = filepath.Join("assets/templates", fnames[i])
}
t, err := template.New("").Funcs(templateFuncs).ParseFiles(paths...)
if err != nil {
panic(err)
}
t = t.Lookup("ROOT")
if t == nil {
panic(fmt.Sprintf("ROOT template not found in %v", fnames))
}
return t
}
func contactEmailFn() string {
return contactEmail
}
func timeagoFn(t time.Time) string {
d := time.Since(t)
switch {
case d < time.Second:
return "just now"
case d < 2*time.Second:
return "one second ago"
case d < time.Minute:
return fmt.Sprintf("%d seconds ago", d/time.Second)
case d < 2*time.Minute:
return "one minute ago"
case d < time.Hour:
return fmt.Sprintf("%d minutes ago", d/time.Minute)
case d < 2*time.Hour:
return "one hour ago"
case d < 48*time.Hour:
return fmt.Sprintf("%d hours ago", d/time.Hour)
default:
return fmt.Sprintf("%d days ago", d/(time.Hour*24))
}
}
func writeResponse(w http.ResponseWriter, status int, t *template.Template, v interface{}) error {
var buf bytes.Buffer
if err := t.Execute(&buf, v); err != nil {
return err
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Content-Length", strconv.Itoa(buf.Len()))
w.WriteHeader(status)
_, err := w.Write(buf.Bytes())
return err
}
func writeErrorResponse(w http.ResponseWriter, status int) error {
return writeResponse(w, status, errorTemplate, http.StatusText(status))
}
func httpClient(r *http.Request) *http.Client {
c := appengine.NewContext(r)
return &http.Client{
Transport: &httputil.AuthTransport{
GithubToken: os.Getenv("GITHUB_TOKEN"),
GithubClientID: os.Getenv("GITHUB_CLIENT_ID"),
GithubClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"),
Base: &urlfetch.Transport{Context: c},
UserAgent: fmt.Sprintf("%s (+http://%s/-/bot)", appengine.AppID(c), r.Host),
},
}
}
const version = 1
type storePackage struct {
Data []byte
Version int
}
type lintPackage struct {
Files []*lintFile
Path string
Updated time.Time
LineFmt string
URL string
}
type lintFile struct {
Name string
Problems []*lintProblem
URL string
}
type lintProblem struct {
Line int
Text string
LineText string
Confidence float64
Link string
}
func putPackage(c context.Context, importPath string, pkg *lintPackage) error {
var buf bytes.Buffer
if err := gob.NewEncoder(&buf).Encode(pkg); err != nil {
return err
}
_, err := datastore.Put(c,
datastore.NewKey(c, "Package", importPath, 0, nil),
&storePackage{Data: buf.Bytes(), Version: version})
return err
}
func getPackage(c context.Context, importPath string) (*lintPackage, error) {
var spkg storePackage
if err := datastore.Get(c, datastore.NewKey(c, "Package", importPath, 0, nil), &spkg); err != nil {
if err == datastore.ErrNoSuchEntity {
err = nil
}
return nil, err
}
if spkg.Version != version {
return nil, nil
}
var pkg lintPackage
if err := gob.NewDecoder(bytes.NewReader(spkg.Data)).Decode(&pkg); err != nil {
return nil, err
}
return &pkg, nil
}
func runLint(r *http.Request, importPath string) (*lintPackage, error) {
dir, err := gosrc.Get(appengine.NewContext(r), httpClient(r), importPath, "")
if err != nil {
return nil, err
}
pkg := lintPackage{
Path: importPath,
Updated: time.Now(),
LineFmt: dir.LineFmt,
URL: dir.BrowseURL,
}
linter := lint.Linter{}
for _, f := range dir.Files {
if !strings.HasSuffix(f.Name, ".go") {
continue
}
problems, err := linter.Lint(f.Name, f.Data)
if err == nil && len(problems) == 0 {
continue
}
file := lintFile{Name: f.Name, URL: f.BrowseURL}
if err != nil {
file.Problems = []*lintProblem{{Text: err.Error()}}
} else {
for _, p := range problems {
file.Problems = append(file.Problems, &lintProblem{
Line: p.Position.Line,
Text: p.Text,
LineText: p.LineText,
Confidence: p.Confidence,
Link: p.Link,
})
}
}
if len(file.Problems) > 0 {
pkg.Files = append(pkg.Files, &file)
}
}
if err := putPackage(appengine.NewContext(r), importPath, &pkg); err != nil {
return nil, err
}
return &pkg, nil
}
func filterByConfidence(r *http.Request, pkg *lintPackage) {
minConfidence, err := strconv.ParseFloat(r.FormValue("minConfidence"), 64)
if err != nil {
minConfidence = 0.8
}
for _, f := range pkg.Files {
j := 0
for i := range f.Problems {
if f.Problems[i].Confidence >= minConfidence {
f.Problems[j] = f.Problems[i]
j++
}
}
f.Problems = f.Problems[:j]
}
}
type handlerFunc func(http.ResponseWriter, *http.Request) error
func (f handlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c := appengine.NewContext(r)
err := f(w, r)
if err == nil {
return
} else if gosrc.IsNotFound(err) {
writeErrorResponse(w, 404)
} else if e, ok := err.(*gosrc.RemoteError); ok {
log.Infof(c, "Remote error %s: %v", e.Host, e)
writeResponse(w, 500, errorTemplate, fmt.Sprintf("Error accessing %s.", e.Host))
} else if err != nil {
log.Errorf(c, "Internal error %v", err)
writeErrorResponse(w, 500)
}
}
func serveRoot(w http.ResponseWriter, r *http.Request) error {
switch {
case r.Method != "GET" && r.Method != "HEAD":
return writeErrorResponse(w, 405)
case r.URL.Path == "/":
return writeResponse(w, 200, homeTemplate, nil)
default:
importPath := r.URL.Path[1:]
if !gosrc.IsValidPath(importPath) {
return gosrc.NotFoundError{Message: "bad path"}
}
c := appengine.NewContext(r)
pkg, err := getPackage(c, importPath)
if pkg == nil && err == nil {
pkg, err = runLint(r, importPath)
}
if err != nil {
return err
}
filterByConfidence(r, pkg)
return writeResponse(w, 200, packageTemplate, pkg)
}
}
func serveRefresh(w http.ResponseWriter, r *http.Request) error {
if r.Method != "POST" {
return writeErrorResponse(w, 405)
}
importPath := r.FormValue("importPath")
pkg, err := runLint(r, importPath)
if err != nil {
return err
}
http.Redirect(w, r, "/"+pkg.Path, 301)
return nil
}
func serveBot(w http.ResponseWriter, r *http.Request) error {
c := appengine.NewContext(r)
_, err := fmt.Fprintf(w, "Contact %s for help with the %s bot.", contactEmail, appengine.AppID(c))
return err
}