github.com/massongit/reviewdog@v0.0.0-20240331071725-4a16675475a8/filter/diff_filter.go (about)

     1  package filter
     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  // NewDiffFilter creates a new DiffFilter.
    90  func NewDiffFilter(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 {
   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 df.isSignificantLine(line), file, line
   139  }
   140  
   141  // DiffLine returns diff data from given new path and lnum. Returns nil if not
   142  // found.
   143  func (df *DiffFilter) DiffLine(path string, lnum int) *diff.Line {
   144  	npath := df.normalizePath(path)
   145  	lines, ok := df.difflines[npath]
   146  	if !ok {
   147  		return nil
   148  	}
   149  	line, ok := lines[lnum]
   150  	if !ok {
   151  		return nil
   152  	}
   153  	return line
   154  }
   155  
   156  func (df *DiffFilter) isSignificantLine(line *diff.Line) bool {
   157  	switch df.mode {
   158  	case ModeDiffContext, ModeFile, ModeNoFilter:
   159  		return true // any lines in diff are significant.
   160  	case ModeAdded, ModeDefault:
   161  		return line.Type == diff.LineAdded
   162  	}
   163  	return false
   164  }
   165  
   166  // normalizedPath is file path which is relative to **project root dir** or
   167  // to current dir if project root not found.
   168  type normalizedPath struct{ p string }
   169  
   170  func (df *DiffFilter) normalizePath(path string) normalizedPath {
   171  	return normalizedPath{p: NormalizePath(path, df.cwd, df.projectRelPath)}
   172  }
   173  
   174  func contains(path, base string) bool {
   175  	ps := splitPathList(path)
   176  	bs := splitPathList(base)
   177  	if len(ps) < len(bs) {
   178  		return false
   179  	}
   180  	for i := range bs {
   181  		if bs[i] != ps[i] {
   182  			return false
   183  		}
   184  	}
   185  	return true
   186  }
   187  
   188  // Assuming diff path should be relative path to the project root dir by
   189  // default (e.g. git diff).
   190  //
   191  // `git diff --relative` can returns relative path to current workdir, so we
   192  // ask users not to use it for reviewdog command.
   193  func (df *DiffFilter) normalizeDiffPath(filediff *diff.FileDiff) normalizedPath {
   194  	return normalizedPath{p: NormalizeDiffPath(filediff.PathNew, df.strip)}
   195  }
   196  
   197  // NormalizeDiffPath return path normalized path from given path in diff with
   198  // strip.
   199  func NormalizeDiffPath(diffpath string, strip int) string {
   200  	if diffpath == "/dev/null" {
   201  		return ""
   202  	}
   203  	path := diffpath
   204  	if strip > 0 && !filepath.IsAbs(path) {
   205  		ps := splitPathList(path)
   206  		if len(ps) > strip {
   207  			path = filepath.Join(ps[strip:]...)
   208  		}
   209  	}
   210  	return filepath.ToSlash(filepath.Clean(path))
   211  }
   212  
   213  func splitPathList(path string) []string {
   214  	return strings.Split(filepath.ToSlash(path), "/")
   215  }