blob: 9d8a208bf45585703ed0eb20d29c38dbf252151a [file] [log] [blame]
/*
Copyright 2013 Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package lint provides helper methods for quickly running lint checks
// against a proposed change on Gerrit.
package lint
import (
"errors"
"fmt"
"log"
"regexp"
"strings"
"gwt.googlesource.com/buildglue.git/checkstyle"
"gwt.googlesource.com/buildglue.git/gerrit"
"gwt.googlesource.com/buildglue.git/git"
)
// Errors that can be returned by Lint.
var (
LintFailed = errors.New("commit failed lint checking")
)
// Should be kept in sync with GWT's build.xml files.
var checks = []struct {
config string
includes []string
excludes []string
}{
{
config: "eclipse/settings/code-style/gwt-checkstyle.xml",
includes: []string{
"dev/core/src/",
"dev/core/super/com/google/gwt/dev/jjs/intrinsic/",
"user/src/",
"user/super/com/google/gwt/emul/",
"user/super/com/google/gwt/junit/translatable/",
},
excludes: []string{
"dev/core/src/com/google/gwt/dev/shell/remoteui/RemoteMessageProto.java",
"dev/core/src/com/google/gwt/dev/asm/",
"dev/core/src/com/google/gwt/dev/js/rhino/",
"dev/core/src/org/eclipse/",
"dev/core/src/org/apache/",
"user/src/javax/validation/",
"user/src/org/hibernate/validator/",
},
},
{
config: "eclipse/settings/code-style/gwt-checkstyle-tests.xml",
includes: []string{
"user/test/com/google/",
"user/test/test/",
},
},
}
// Match returns true if file is a ".java" file that starts with a string
// from includes but does not start with any strings from excludes.
func match(file string, includes, excludes []string) bool {
if !strings.HasSuffix(file, ".java") {
return false
}
for _, exclude := range excludes {
if strings.HasPrefix(file, exclude) {
return false
}
}
for _, include := range includes {
if strings.HasPrefix(file, include) {
return true
}
}
return false
}
// Matches returns the sublist of files that match the provided includes
// and excludes rules.
func matches(files, includes, excludes []string) []string {
res := []string{}
for _, file := range files {
if match(file, includes, excludes) {
res = append(res, file)
}
}
return res
}
type linter struct {
rev string
comments map[string][]gerrit.Comment
failed bool
}
func newLinter(rev string) *linter {
return &linter{
rev: rev,
comments: make(map[string][]gerrit.Comment),
}
}
func (l *linter) comment(file string, line int, severity, message string) {
comment := gerrit.Comment{Line: line, Message: fmt.Sprintf("[%s] %s", severity, message)}
l.comments[file] = append(l.comments[file], comment)
if severity == "error" {
l.failed = true
}
}
func (l *linter) checkStyle(config string, files []string) error {
out, err := checkstyle.Run(config, files)
if err != nil {
log.Println("running checkstyle failed:", err)
return err
}
for _, file := range out {
if len(file.Errors) == 0 {
continue
}
blameable, err := git.Blame(l.rev, file.Name)
if err != nil {
return err
}
for _, err := range file.Errors {
if blameable[err.Line] {
l.comment(file.Name, err.Line, err.Severity, err.Message)
}
}
}
return nil
}
var fixesIssueRegexp = regexp.MustCompile("(?i)^fixes issue [0-9]{3,}[.]?$")
var issueRegexp = regexp.MustCompile("(?i)issue [0-9]{3,}")
var footerRegexp = regexp.MustCompile("^[A-Za-z0-9-]+:")
func (l *linter) checkMessage(commitmsg []string) {
warn := func(line int, message string) {
// Gerrit includes some extra header lines before the commit message.
// TODO(mdempsky): Is it always exactly six lines?
l.comment("/COMMIT_MSG", line+6, "warning", message)
}
lengthWarning := false
bugWarning := false
if len(commitmsg) >= 3 && len(commitmsg[1]) > 0 {
warn(2, "Subject line and body should be separated by a blank line.")
}
for i, line := range commitmsg {
if !lengthWarning && i != 1 && len(line) > 72 {
warn(i+1, "Commit message lines should be 72 characters or fewer.")
lengthWarning = true
}
if !bugWarning && ((i == 0 && issueRegexp.MatchString(line)) || fixesIssueRegexp.MatchString(line)) {
warn(i+1, "Please use 'Bug: issue NNNN' and place it just above the Change-Id line.")
bugWarning = true
}
}
// Note: don't check subject line
for i, inFooter := len(commitmsg)-1, true; i >= 1; i-- {
if len(commitmsg[i]) == 0 {
inFooter = false
continue
}
if !footerRegexp.MatchString(commitmsg[i]) {
if inFooter {
warn(i+1, "Footer lines should be separated from message body by a blank line.")
}
break
}
if !inFooter {
warn(i+1, "Footer lines should form a single paragraph (i.e. must not be separated by blank lines.)")
}
field := strings.SplitN(commitmsg[i], ":", 2)[0]
if field != strings.Title(field) {
warn(i+1, "Footer fields should be in Title-Case.")
break
}
}
}
func (l *linter) review() gerrit.Review {
res := gerrit.Review{}
if l.failed {
res.Message = ("Oops, this change failed the fast style check! " +
"Please fix all errors and try again.")
res.Labels = &gerrit.Labels{Verified: -1}
} else if len(l.comments) != 0 {
res.Message = ("This change passed the fast style check, " +
"but still triggered some non-error warnings. " +
"Please review and fix if appropriate.")
} else {
res.Message = ("Woo hoo, this change passed the fast style check " +
"with ZERO new warnings! You rock!")
}
if len(l.comments) != 0 {
res.Comments = l.comments
}
return res
}
// Lint fetches ref from Gerrit, inspects the commit message and runs checkstyle against
// any added or modified files, and posts a review to Gerrit with the results.
// Returns a LintFailed error if any lint errors were found.
func Lint(ref string) error {
if err := git.Run("fetch", "https://gwt.googlesource.com/gwt", ref); err != nil {
log.Println("fetch failed:", err)
return err
}
rev, err := git.Line("rev-parse", "FETCH_HEAD")
if err != nil {
return err
}
if err = git.Run("checkout", rev); err != nil {
return err
}
files, err := git.Lines("diff-tree", "-r", "--name-only", "--diff-filter=AM", rev+"^", rev)
if err != nil {
return err
}
log.Println("running lint checks")
linter := newLinter(rev)
for _, check := range checks {
if err := linter.checkStyle(check.config, matches(files, check.includes, check.excludes)); err != nil {
log.Println("checkstyle error:", err)
return err
}
}
commitmsg, err := git.Lines("show", "-s", "--pretty=format:%B", rev)
if err != nil {
return err
}
linter.checkMessage(commitmsg)
if err = gerrit.Post(ref, linter.review()); err != nil {
return err
}
if linter.failed {
return LintFailed
}
return nil
}