github.com/reviewdog/reviewdog@v0.17.5-0.20240516205324-0cd103a83d58/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 }