193 lines
4.3 KiB
Go
193 lines
4.3 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"
|
||
|
)
|
||
|
|
||
|
// This is how a TODO looks like.
|
||
|
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
|
||
|
}
|