github.com/shashidharatd/test-infra@v0.0.0-20171006011030-71304e1ca560/prow/plugins/golint/golint.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package golint 18 19 import ( 20 "fmt" 21 "io/ioutil" 22 "path/filepath" 23 "regexp" 24 "strconv" 25 "strings" 26 "time" 27 28 "github.com/golang/lint" 29 "github.com/sirupsen/logrus" 30 31 "k8s.io/test-infra/prow/genfiles" 32 "k8s.io/test-infra/prow/git" 33 "k8s.io/test-infra/prow/github" 34 "k8s.io/test-infra/prow/plugins" 35 ) 36 37 const ( 38 pluginName = "golint" 39 commentTag = "<!-- golint -->" 40 maxComments = 20 41 ) 42 43 var lintRe = regexp.MustCompile(`(?mi)^/lint\s*$`) 44 45 func init() { 46 plugins.RegisterGenericCommentHandler(pluginName, handleGenericComment) 47 } 48 49 type githubClient interface { 50 GetFile(org, repo, filepath, commit string) ([]byte, error) 51 GetPullRequest(org, repo string, number int) (*github.PullRequest, error) 52 GetPullRequestChanges(org, repo string, number int) ([]github.PullRequestChange, error) 53 CreateReview(org, repo string, number int, r github.DraftReview) error 54 ListPullRequestComments(org, repo string, number int) ([]github.ReviewComment, error) 55 } 56 57 func handleGenericComment(pc plugins.PluginClient, e github.GenericCommentEvent) error { 58 return handle(pc.GitHubClient, pc.GitClient, pc.Logger, &e) 59 } 60 61 // modifiedGoFiles returns a map from filename to patch string for all go files 62 // that are modified in the PR excluding vendor/ and generated files. 63 func modifiedGoFiles(ghc githubClient, org, repo string, number int, sha string) (map[string]string, error) { 64 changes, err := ghc.GetPullRequestChanges(org, repo, number) 65 if err != nil { 66 return nil, err 67 } 68 69 gfg, err := genfiles.NewGroup(ghc, org, repo, sha) 70 if err != nil { 71 return nil, err 72 } 73 74 modifiedFiles := make(map[string]string) 75 for _, change := range changes { 76 switch { 77 case strings.HasPrefix(change.Filename, "vendor/"): 78 continue 79 case filepath.Ext(change.Filename) != ".go": 80 continue 81 case gfg.Match(change.Filename): 82 continue 83 case change.Status == github.PullRequestFileRemoved || change.Status == github.PullRequestFileRenamed: 84 continue 85 } 86 modifiedFiles[change.Filename] = change.Patch 87 } 88 return modifiedFiles, nil 89 } 90 91 // newProblems compares the list of problems with the list of past comments on 92 // the PR to decide which are new. 93 func newProblems(cs []github.ReviewComment, ps map[string]map[int]lint.Problem) map[string]map[int]lint.Problem { 94 // Make a copy, then remove the old elements. 95 res := make(map[string]map[int]lint.Problem) 96 for f, ls := range ps { 97 res[f] = make(map[int]lint.Problem) 98 for l, p := range ls { 99 res[f][l] = p 100 } 101 } 102 for _, c := range cs { 103 if c.Position == nil { 104 continue 105 } 106 if !strings.Contains(c.Body, commentTag) { 107 continue 108 } 109 delete(res[c.Path], *c.Position) 110 } 111 return res 112 } 113 114 // problemsInFiles runs golint on the files. It returns a map from the file to 115 // a map from the line in the patch to the problem. 116 func problemsInFiles(r *git.Repo, files map[string]string) (map[string]map[int]lint.Problem, error) { 117 problems := make(map[string]map[int]lint.Problem) 118 l := new(lint.Linter) 119 for f, patch := range files { 120 problems[f] = make(map[int]lint.Problem) 121 src, err := ioutil.ReadFile(filepath.Join(r.Dir, f)) 122 if err != nil { 123 return nil, err 124 } 125 ps, err := l.Lint(f, src) 126 if err != nil { 127 return nil, fmt.Errorf("linting %s: %v", f, err) 128 } 129 al, err := addedLines(patch) 130 if err != nil { 131 return nil, fmt.Errorf("computing added lines in %s: %v", f, err) 132 } 133 for _, p := range ps { 134 if pl, ok := al[p.Position.Line]; ok { 135 problems[f][pl] = p 136 } 137 } 138 } 139 return problems, nil 140 } 141 142 func handle(ghc githubClient, gc *git.Client, log *logrus.Entry, e *github.GenericCommentEvent) error { 143 // Only handle open PRs and new requests. 144 if e.IssueState != "open" || !e.IsPR || e.Action != github.GenericCommentActionCreated { 145 return nil 146 } 147 if !lintRe.MatchString(e.Body) { 148 return nil 149 } 150 151 org := e.Repo.Owner.Login 152 repo := e.Repo.Name 153 154 pr, err := ghc.GetPullRequest(org, repo, e.Number) 155 if err != nil { 156 return err 157 } 158 159 // List modified files. 160 modifiedFiles, err := modifiedGoFiles(ghc, org, repo, pr.Number, pr.Head.SHA) 161 if err != nil { 162 return err 163 } 164 if len(modifiedFiles) == 0 { 165 return nil 166 } 167 log.Infof("Will lint %d modified go files.", len(modifiedFiles)) 168 169 // Clone the repo, checkout the PR. 170 startClone := time.Now() 171 r, err := gc.Clone(e.Repo.FullName) 172 if err != nil { 173 return err 174 } 175 defer func() { 176 if err := r.Clean(); err != nil { 177 log.WithError(err).Error("Error cleaning up repo.") 178 } 179 }() 180 if err := r.CheckoutPullRequest(e.Number); err != nil { 181 return err 182 } 183 finishClone := time.Now() 184 log.WithField("duration", time.Since(startClone)).Info("Cloned and checked out PR.") 185 186 // Compute lint errors. 187 problems, err := problemsInFiles(r, modifiedFiles) 188 if err != nil { 189 return err 190 } 191 log.WithField("duration", time.Since(finishClone)).Info("Linted.") 192 193 oldComments, err := ghc.ListPullRequestComments(org, repo, e.Number) 194 if err != nil { 195 return err 196 } 197 nps := newProblems(oldComments, problems) 198 199 // Make the list of comments. 200 var comments []github.DraftReviewComment 201 for f, ls := range nps { 202 for l, p := range ls { 203 var body string 204 if p.Link == "" { 205 body = fmt.Sprintf("Golint %s: %s. %s", p.Category, p.Text, commentTag) 206 } else { 207 body = fmt.Sprintf("Golint %s: %s. [More info](%s). %s", p.Category, p.Text, p.Link, commentTag) 208 } 209 comments = append(comments, github.DraftReviewComment{ 210 Path: f, 211 Position: l, 212 Body: body, 213 }) 214 } 215 } 216 217 // Trim down the number of comments if necessary. 218 totalProblems := numProblems(problems) 219 newProblems := numProblems(nps) 220 oldProblems := totalProblems - newProblems 221 222 allowedComments := maxComments - oldProblems 223 if allowedComments < 0 { 224 allowedComments = 0 225 } 226 if len(comments) > allowedComments { 227 comments = comments[:allowedComments] 228 } 229 230 // Make the review body. 231 s := "s" 232 if totalProblems == 1 { 233 s = "" 234 } 235 response := fmt.Sprintf("%d warning%s.", totalProblems, s) 236 237 return ghc.CreateReview(org, repo, e.Number, github.DraftReview{ 238 Body: plugins.FormatResponseRaw(e.Body, e.HTMLURL, e.User.Login, response), 239 Action: github.Comment, 240 Comments: comments, 241 }) 242 } 243 244 func numProblems(ps map[string]map[int]lint.Problem) int { 245 var num int 246 for _, m := range ps { 247 num += len(m) 248 } 249 return num 250 } 251 252 // addedLines returns line numbers that were added in the patch, along with 253 // their line in the patch itself as a map from line to patch line. 254 // https://www.gnu.org/software/diffutils/manual/diffutils.html#Detailed-Unified 255 // GitHub omits the ---/+++ lines since that information is in the 256 // PullRequestChange object. 257 func addedLines(patch string) (map[int]int, error) { 258 result := make(map[int]int) 259 if patch == "" { 260 return result, nil 261 } 262 lines := strings.Split(patch, "\n") 263 for i := 0; i < len(lines); i++ { 264 _, oldLen, newLine, newLen, err := parseHunkLine(lines[i]) 265 if err != nil { 266 return nil, fmt.Errorf("couldn't parse hunk on line %d in patch %s: %v", i, patch, err) 267 } 268 oldAdd := 0 269 newAdd := 0 270 for oldAdd < oldLen || newAdd < newLen { 271 i++ 272 if i >= len(lines) { 273 return nil, fmt.Errorf("invalid patch: %s", patch) 274 } 275 switch lines[i][0] { 276 case ' ': 277 oldAdd++ 278 newAdd++ 279 case '-': 280 oldAdd++ 281 case '+': 282 result[newLine+newAdd] = i 283 newAdd++ 284 default: 285 return nil, fmt.Errorf("bad prefix on line %d in patch %s", i, patch) 286 } 287 } 288 } 289 return result, nil 290 } 291 292 // Matches the hunk line in unified diffs. These are of the form: 293 // @@ -l,s +l,s @@ section head 294 // We need to extract the four numbers, but the command and s is optional. 295 // See https://en.wikipedia.org/wiki/Diff_utility#Unified_format 296 var hunkRe = regexp.MustCompile(`^@@ -(\d+),?(\d+)? \+(\d+),?(\d+)? @@.*`) 297 298 func parseHunkLine(hunk string) (oldLine, oldLength, newLine, newLength int, err error) { 299 if !hunkRe.MatchString(hunk) { 300 err = fmt.Errorf("invalid hunk line: %s", hunk) 301 return 302 } 303 matches := hunkRe.FindStringSubmatch(hunk) 304 oldLine, err = strconv.Atoi(matches[1]) 305 if err != nil { 306 return 307 } 308 if matches[2] != "" { 309 oldLength, err = strconv.Atoi(matches[2]) 310 if err != nil { 311 return 312 } 313 } else { 314 oldLength = 1 315 } 316 newLine, err = strconv.Atoi(matches[3]) 317 if err != nil { 318 return 319 } 320 if matches[4] != "" { 321 newLength, err = strconv.Atoi(matches[4]) 322 if err != nil { 323 return 324 } 325 } else { 326 newLength = 1 327 } 328 return 329 }