gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/tools/github/reviver/reviver.go (about)

     1  // Copyright 2019 The gVisor Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package reviver scans the code looking for TODOs and pass them to registered
    16  // Buggers to ensure TODOs point to active issues.
    17  package reviver
    18  
    19  import (
    20  	"bufio"
    21  	"fmt"
    22  	"io/ioutil"
    23  	"os"
    24  	"path/filepath"
    25  	"regexp"
    26  	"sync"
    27  )
    28  
    29  // regexTodo matches a TODO or FIXME comment.
    30  var regexTodo = regexp.MustCompile(`(\/\/|#)\s*(TODO|FIXME)\(([a-zA-Z0-9.\/]+)\):\s*(.+)`)
    31  
    32  // Bugger interface is called for every TODO found in the code. If it can handle
    33  // the TODO, it must return true. If it returns false, the next Bugger is
    34  // called. If no Bugger handles the TODO, it's dropped on the floor.
    35  type Bugger interface {
    36  	Activate(todo *Todo) (bool, error)
    37  }
    38  
    39  // Location saves the location where the TODO was found.
    40  type Location struct {
    41  	Comment string
    42  	File    string
    43  	Line    uint
    44  }
    45  
    46  // Todo represents a unique TODO. There can be several TODOs pointing to the
    47  // same issue in the code. They are all grouped together.
    48  type Todo struct {
    49  	Issue     string
    50  	Locations []Location
    51  }
    52  
    53  // Reviver scans the given paths for TODOs and calls Buggers to handle them.
    54  type Reviver struct {
    55  	paths   []string
    56  	buggers []Bugger
    57  
    58  	mu    sync.Mutex
    59  	todos map[string]*Todo
    60  	errs  []error
    61  }
    62  
    63  // New create a new Reviver.
    64  func New(paths []string, buggers []Bugger) *Reviver {
    65  	return &Reviver{
    66  		paths:   paths,
    67  		buggers: buggers,
    68  		todos:   map[string]*Todo{},
    69  	}
    70  }
    71  
    72  // Run runs. It returns all errors found during processing, it doesn't stop
    73  // on errors.
    74  func (r *Reviver) Run() []error {
    75  	// Process each directory in parallel.
    76  	wg := sync.WaitGroup{}
    77  	for _, path := range r.paths {
    78  		wg.Add(1)
    79  		go func(path string) {
    80  			defer wg.Done()
    81  			r.processPath(path, &wg)
    82  		}(path)
    83  	}
    84  
    85  	wg.Wait()
    86  
    87  	r.mu.Lock()
    88  	defer r.mu.Unlock()
    89  
    90  	fmt.Printf("Processing %d TODOs (%d errors)...\n", len(r.todos), len(r.errs))
    91  	dropped := 0
    92  	for _, todo := range r.todos {
    93  		ok, err := r.processTodo(todo)
    94  		if err != nil {
    95  			r.errs = append(r.errs, err)
    96  		}
    97  		if !ok {
    98  			dropped++
    99  		}
   100  	}
   101  	fmt.Printf("Processed %d TODOs, %d were skipped (%d errors)\n", len(r.todos)-dropped, dropped, len(r.errs))
   102  
   103  	return r.errs
   104  }
   105  
   106  func (r *Reviver) processPath(path string, wg *sync.WaitGroup) {
   107  	fmt.Printf("Processing dir %q\n", path)
   108  	fis, err := ioutil.ReadDir(path)
   109  	if err != nil {
   110  		r.addErr(fmt.Errorf("error processing dir %q: %v", path, err))
   111  		return
   112  	}
   113  
   114  	for _, fi := range fis {
   115  		childPath := filepath.Join(path, fi.Name())
   116  		switch {
   117  		case fi.Mode().IsDir():
   118  			wg.Add(1)
   119  			go func() {
   120  				defer wg.Done()
   121  				r.processPath(childPath, wg)
   122  			}()
   123  
   124  		case fi.Mode().IsRegular():
   125  			file, err := os.Open(childPath)
   126  			if err != nil {
   127  				r.addErr(err)
   128  				continue
   129  			}
   130  
   131  			scanner := bufio.NewScanner(file)
   132  			lineno := uint(0)
   133  			for scanner.Scan() {
   134  				lineno++
   135  				line := scanner.Text()
   136  				if todo := r.processLine(line, childPath, lineno); todo != nil {
   137  					r.addTodo(todo)
   138  				}
   139  			}
   140  		}
   141  	}
   142  }
   143  
   144  func (r *Reviver) processLine(line, path string, lineno uint) *Todo {
   145  	matches := regexTodo.FindStringSubmatch(line)
   146  	if matches == nil {
   147  		return nil
   148  	}
   149  	if len(matches) != 5 {
   150  		panic(fmt.Sprintf("regex returned wrong matches for %q: %v", line, matches))
   151  	}
   152  	return &Todo{
   153  		Issue: matches[3],
   154  		Locations: []Location{
   155  			{
   156  				File:    path,
   157  				Line:    lineno,
   158  				Comment: matches[4],
   159  			},
   160  		},
   161  	}
   162  }
   163  
   164  func (r *Reviver) addTodo(newTodo *Todo) {
   165  	r.mu.Lock()
   166  	defer r.mu.Unlock()
   167  
   168  	if todo := r.todos[newTodo.Issue]; todo == nil {
   169  		r.todos[newTodo.Issue] = newTodo
   170  	} else {
   171  		todo.Locations = append(todo.Locations, newTodo.Locations...)
   172  	}
   173  }
   174  
   175  func (r *Reviver) addErr(err error) {
   176  	r.mu.Lock()
   177  	defer r.mu.Unlock()
   178  	r.errs = append(r.errs, err)
   179  }
   180  
   181  func (r *Reviver) processTodo(todo *Todo) (bool, error) {
   182  	for _, bugger := range r.buggers {
   183  		ok, err := bugger.Activate(todo)
   184  		if err != nil {
   185  			return false, err
   186  		}
   187  		if ok {
   188  			return true, nil
   189  		}
   190  	}
   191  	return false, nil
   192  }