github.com/alexey-mercari/reviewdog@v0.10.1-0.20200514053941-928943b10766/difffilter/filter.go (about)

     1  package difffilter
     2  
     3  import (
     4  	"fmt"
     5  	"path/filepath"
     6  	"strings"
     7  
     8  	"github.com/reviewdog/reviewdog/diff"
     9  	"github.com/reviewdog/reviewdog/service/serviceutil"
    10  )
    11  
    12  // Mode represents enumeration of available filter modes
    13  type Mode int
    14  
    15  const (
    16  	// ModeDefault represents default mode, which means users doesn't specify
    17  	// filter-mode. The behavior can be changed depending on reporters/context
    18  	// later if we want. Basically, it's same as ModeAdded because it's most safe
    19  	// and basic mode for reporters implementation.
    20  	ModeDefault Mode = iota
    21  	// ModeAdded represents filtering by added/changed diff lines.
    22  	ModeAdded
    23  	// ModeDiffContext represents filtering by diff context.
    24  	// i.e. changed lines +-N lines (e.g. N=3 for default git diff).
    25  	ModeDiffContext
    26  	// ModeFile represents filtering by changed files.
    27  	ModeFile
    28  	// ModeNoFilter doesn't filter out any results.
    29  	ModeNoFilter
    30  )
    31  
    32  // String implements the flag.Value interface
    33  func (mode *Mode) String() string {
    34  	names := [...]string{
    35  		"default",
    36  		"added",
    37  		"diff_context",
    38  		"file",
    39  		"nofilter",
    40  	}
    41  	if *mode < ModeDefault || *mode > ModeNoFilter {
    42  		return "Unknown mode"
    43  	}
    44  
    45  	return names[*mode]
    46  }
    47  
    48  // Set implements the flag.Value interface
    49  func (mode *Mode) Set(value string) error {
    50  	switch value {
    51  	case "default", "":
    52  		*mode = ModeDefault
    53  	case "added":
    54  		*mode = ModeAdded
    55  	case "diff_context":
    56  		*mode = ModeDiffContext
    57  	case "file":
    58  		*mode = ModeFile
    59  	case "nofilter":
    60  		*mode = ModeNoFilter
    61  	default:
    62  		return fmt.Errorf("invalid mode name: %s", value)
    63  	}
    64  	return nil
    65  }
    66  
    67  // DiffFilter filters lines by diff.
    68  type DiffFilter struct {
    69  	// Current working directory (workdir).
    70  	cwd string
    71  
    72  	// Relative path to the project root (e.g. git) directory from current workdir.
    73  	// It can be empty if it doesn't find any project root directory.
    74  	projectRelPath string
    75  
    76  	strip int
    77  	mode  Mode
    78  
    79  	difflines difflines
    80  	difffiles difffiles
    81  }
    82  
    83  // difflines is a hash table of normalizedPath to line number to *diff.Line.
    84  type difflines map[normalizedPath]map[int]*diff.Line
    85  
    86  // difffiles is a hash table of normalizedPath to *diff.FileDiff.
    87  type difffiles map[normalizedPath]*diff.FileDiff
    88  
    89  // New creates a new DiffFilter.
    90  func New(diff []*diff.FileDiff, strip int, cwd string, mode Mode) *DiffFilter {
    91  	df := &DiffFilter{
    92  		strip:     strip,
    93  		cwd:       cwd,
    94  		mode:      mode,
    95  		difflines: make(difflines),
    96  		difffiles: make(difffiles),
    97  	}
    98  	// If cwd is empty, projectRelPath should not have any meaningful data too.
    99  	if cwd != "" {
   100  		df.projectRelPath, _ = serviceutil.GitRelWorkdir()
   101  	}
   102  	df.addDiff(diff)
   103  	return df
   104  }
   105  
   106  func (df *DiffFilter) addDiff(filediffs []*diff.FileDiff) {
   107  	for _, filediff := range filediffs {
   108  		path := df.normalizeDiffPath(filediff)
   109  		df.difffiles[path] = filediff
   110  		lines, ok := df.difflines[path]
   111  		if !ok {
   112  			lines = make(map[int]*diff.Line)
   113  		}
   114  		for _, hunk := range filediff.Hunks {
   115  			for _, line := range hunk.Lines {
   116  				if line.LnumNew > 0 && df.isSignificantLine(line) {
   117  					lines[line.LnumNew] = line
   118  				}
   119  			}
   120  		}
   121  		df.difflines[path] = lines
   122  	}
   123  }
   124  
   125  // ShouldReport returns true, if the given path should be reported depending on
   126  // the filter Mode. It also optionally return diff file/line.
   127  func (df *DiffFilter) ShouldReport(path string, lnum int) (bool, *diff.FileDiff, *diff.Line) {
   128  	npath := df.normalizePath(path)
   129  	file := df.difffiles[npath]
   130  	lines, ok := df.difflines[npath]
   131  	if !ok {
   132  		return (df.mode == ModeNoFilter), file, nil
   133  	}
   134  	line, ok := lines[lnum]
   135  	if !ok {
   136  		return (df.mode == ModeNoFilter || df.mode == ModeFile), file, nil
   137  	}
   138  	return true, file, line
   139  }
   140  
   141  func (df *DiffFilter) isSignificantLine(line *diff.Line) bool {
   142  	switch df.mode {
   143  	case ModeDiffContext, ModeFile, ModeNoFilter:
   144  		return true // any lines in diff are significant.
   145  	case ModeAdded, ModeDefault:
   146  		return line.Type == diff.LineAdded
   147  	}
   148  	return false
   149  }
   150  
   151  // normalizedPath is file path which is relative to **project root dir** or
   152  // to current dir if project root not found.
   153  type normalizedPath struct{ p string }
   154  
   155  func (df *DiffFilter) normalizePath(path string) normalizedPath {
   156  	path = filepath.Clean(path)
   157  	// Convert absolute path to relative path only if the path is in current
   158  	// directory.
   159  	if filepath.IsAbs(path) && df.cwd != "" && contains(path, df.cwd) {
   160  		relPath, err := filepath.Rel(df.cwd, path)
   161  		if err == nil {
   162  			path = relPath
   163  		}
   164  	}
   165  	if !filepath.IsAbs(path) && df.projectRelPath != "" {
   166  		path = filepath.Join(df.projectRelPath, path)
   167  	}
   168  	return normalizedPath{p: filepath.ToSlash(path)}
   169  }
   170  
   171  func contains(path, base string) bool {
   172  	ps := splitPathList(path)
   173  	bs := splitPathList(base)
   174  	if len(ps) < len(bs) {
   175  		return false
   176  	}
   177  	for i := range bs {
   178  		if bs[i] != ps[i] {
   179  			return false
   180  		}
   181  	}
   182  	return true
   183  }
   184  
   185  // Assuming diff path should be relative path to the project root dir by
   186  // default (e.g. git diff).
   187  //
   188  // `git diff --relative` can returns relative path to current workdir, so we
   189  // ask users not to use it for reviewdog command.
   190  func (df *DiffFilter) normalizeDiffPath(filediff *diff.FileDiff) normalizedPath {
   191  	return normalizedPath{p: NormalizeDiffPath(filediff.PathNew, df.strip)}
   192  }
   193  
   194  // NormalizeDiffPath return path normalized path from given path in diff with
   195  // strip.
   196  func NormalizeDiffPath(diffpath string, strip int) string {
   197  	if diffpath == "/dev/null" {
   198  		return ""
   199  	}
   200  	path := diffpath
   201  	if strip > 0 {
   202  		ps := splitPathList(path)
   203  		if len(ps) > strip {
   204  			path = filepath.Join(ps[strip:]...)
   205  		}
   206  	}
   207  	return filepath.ToSlash(filepath.Clean(path))
   208  }
   209  
   210  func splitPathList(path string) []string {
   211  	return strings.Split(filepath.ToSlash(path), "/")
   212  }