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