gvisor/tools/github/reviver/reviver.go

193 lines
4.4 KiB
Go

// Copyright 2019 The gVisor Authors.
//
// 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 reviver scans the code looking for TODOs and pass them to registered
// Buggers to ensure TODOs point to active issues.
package reviver
import (
"bufio"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"sync"
)
// regexTodo matches a TODO or FIXME comment.
var regexTodo = regexp.MustCompile(`(\/\/|#)\s*(TODO|FIXME)\(([a-zA-Z0-9.\/]+)\):\s*(.+)`)
// Bugger interface is called for every TODO found in the code. If it can handle
// the TODO, it must return true. If it returns false, the next Bugger is
// called. If no Bugger handles the TODO, it's dropped on the floor.
type Bugger interface {
Activate(todo *Todo) (bool, error)
}
// Location saves the location where the TODO was found.
type Location struct {
Comment string
File string
Line uint
}
// Todo represents a unique TODO. There can be several TODOs pointing to the
// same issue in the code. They are all grouped together.
type Todo struct {
Issue string
Locations []Location
}
// Reviver scans the given paths for TODOs and calls Buggers to handle them.
type Reviver struct {
paths []string
buggers []Bugger
mu sync.Mutex
todos map[string]*Todo
errs []error
}
// New create a new Reviver.
func New(paths []string, buggers []Bugger) *Reviver {
return &Reviver{
paths: paths,
buggers: buggers,
todos: map[string]*Todo{},
}
}
// Run runs. It returns all errors found during processing, it doesn't stop
// on errors.
func (r *Reviver) Run() []error {
// Process each directory in parallel.
wg := sync.WaitGroup{}
for _, path := range r.paths {
wg.Add(1)
go func(path string) {
defer wg.Done()
r.processPath(path, &wg)
}(path)
}
wg.Wait()
r.mu.Lock()
defer r.mu.Unlock()
fmt.Printf("Processing %d TODOs (%d errors)...\n", len(r.todos), len(r.errs))
dropped := 0
for _, todo := range r.todos {
ok, err := r.processTodo(todo)
if err != nil {
r.errs = append(r.errs, err)
}
if !ok {
dropped++
}
}
fmt.Printf("Processed %d TODOs, %d were skipped (%d errors)\n", len(r.todos)-dropped, dropped, len(r.errs))
return r.errs
}
func (r *Reviver) processPath(path string, wg *sync.WaitGroup) {
fmt.Printf("Processing dir %q\n", path)
fis, err := ioutil.ReadDir(path)
if err != nil {
r.addErr(fmt.Errorf("error processing dir %q: %v", path, err))
return
}
for _, fi := range fis {
childPath := filepath.Join(path, fi.Name())
switch {
case fi.Mode().IsDir():
wg.Add(1)
go func() {
defer wg.Done()
r.processPath(childPath, wg)
}()
case fi.Mode().IsRegular():
file, err := os.Open(childPath)
if err != nil {
r.addErr(err)
continue
}
scanner := bufio.NewScanner(file)
lineno := uint(0)
for scanner.Scan() {
lineno++
line := scanner.Text()
if todo := r.processLine(line, childPath, lineno); todo != nil {
r.addTodo(todo)
}
}
}
}
}
func (r *Reviver) processLine(line, path string, lineno uint) *Todo {
matches := regexTodo.FindStringSubmatch(line)
if matches == nil {
return nil
}
if len(matches) != 5 {
panic(fmt.Sprintf("regex returned wrong matches for %q: %v", line, matches))
}
return &Todo{
Issue: matches[3],
Locations: []Location{
{
File: path,
Line: lineno,
Comment: matches[4],
},
},
}
}
func (r *Reviver) addTodo(newTodo *Todo) {
r.mu.Lock()
defer r.mu.Unlock()
if todo := r.todos[newTodo.Issue]; todo == nil {
r.todos[newTodo.Issue] = newTodo
} else {
todo.Locations = append(todo.Locations, newTodo.Locations...)
}
}
func (r *Reviver) addErr(err error) {
r.mu.Lock()
defer r.mu.Unlock()
r.errs = append(r.errs, err)
}
func (r *Reviver) processTodo(todo *Todo) (bool, error) {
for _, bugger := range r.buggers {
ok, err := bugger.Activate(todo)
if err != nil {
return false, err
}
if ok {
return true, nil
}
}
return false, nil
}