gitlab.com/greut/eclint@v0.5.2-0.20240402114752-14681fe6e0bf/fix.go (about) 1 package eclint 2 3 import ( 4 "bufio" 5 "bytes" 6 "context" 7 "fmt" 8 "io" 9 "os" 10 11 "github.com/editorconfig/editorconfig-core-go/v2" 12 "github.com/go-logr/logr" 13 ) 14 15 // FixWithDefinition does the hard work of validating the given file. 16 func FixWithDefinition(ctx context.Context, d *editorconfig.Definition, filename string) error { 17 def, err := newDefinition(d) 18 if err != nil { 19 return err 20 } 21 22 stat, err := os.Stat(filename) 23 if err != nil { 24 return fmt.Errorf("cannot stat %s. %w", filename, err) 25 } 26 27 log := logr.FromContextOrDiscard(ctx) 28 29 if stat.IsDir() { 30 log.V(2).Info("skipped directory") 31 32 return nil 33 } 34 35 fileSize := stat.Size() 36 mode := stat.Mode() 37 38 r, fixed, err := fixWithFilename(ctx, def, filename, fileSize) 39 if err != nil { 40 return fmt.Errorf("cannot fix %s: %w", filename, err) 41 } 42 43 if !fixed { 44 log.V(1).Info("no fixes to apply", "filename", filename) 45 46 return nil 47 } 48 49 if r == nil { 50 return nil 51 } 52 53 // XXX keep mode as is. 54 fp, err := os.OpenFile(filename, os.O_WRONLY|os.O_TRUNC, mode) //nolint:nosnakecase 55 if err != nil { 56 return fmt.Errorf("cannot open %s using %s: %w", filename, mode, err) 57 } 58 defer fp.Close() 59 60 n, err := io.Copy(fp, r) 61 if err != nil { 62 return fmt.Errorf("error copying file: %w", err) 63 } 64 65 log.V(1).Info("bytes written", "filename", filename, "total", n) 66 67 return nil 68 } 69 70 func fixWithFilename(ctx context.Context, def *definition, filename string, fileSize int64) (io.Reader, bool, error) { 71 fp, err := os.Open(filename) 72 if err != nil { 73 return nil, false, fmt.Errorf("cannot open %s. %w", filename, err) 74 } 75 76 defer fp.Close() 77 78 r := bufio.NewReader(fp) 79 80 ok, err := probeReadable(fp, r) 81 if err != nil { 82 return nil, false, fmt.Errorf("cannot read %s. %w", filename, err) 83 } 84 85 log := logr.FromContextOrDiscard(ctx) 86 87 if !ok { 88 log.V(2).Info("skipped unreadable or empty file") 89 90 return nil, false, nil 91 } 92 93 charset, isBinary, err := ProbeCharsetOrBinary(ctx, r, def.Charset) 94 if err != nil { 95 return nil, false, err 96 } 97 98 if isBinary { 99 log.V(2).Info("binary file detected and skipped") 100 101 return nil, false, nil 102 } 103 104 log.V(2).Info("charset probed", "charset", charset) 105 106 return fix(ctx, r, fileSize, charset, def) 107 } 108 109 func computeIndentationValues(def *definition) (int, []byte, []byte, error) { 110 size := def.IndentSize 111 if def.TabWidth != 0 { 112 size = def.TabWidth 113 } 114 115 if size == 0 { 116 // Indent size default == 2 117 size = 2 118 } 119 120 // Value to be used 121 var c []byte 122 // Value that needs to be replaced 123 var x []byte 124 125 switch def.IndentStyle { 126 case SpaceValue: 127 c = bytes.Repeat([]byte{space}, size) 128 x = []byte{tab} 129 case TabValue: 130 c = []byte{tab} 131 x = bytes.Repeat([]byte{space}, size) 132 case "", UnsetValue: 133 size = 0 134 default: 135 return 0, nil, nil, fmt.Errorf( 136 "%w: %q is an invalid value of indent_style, want tab or space", 137 ErrConfiguration, 138 def.IndentStyle, 139 ) 140 } 141 142 return size, c, x, nil 143 } 144 145 func fix( //nolint:funlen 146 ctx context.Context, 147 r io.Reader, 148 fileSize int64, 149 _ string, 150 def *definition, 151 ) (io.Reader, bool, error) { 152 log := logr.FromContextOrDiscard(ctx) 153 154 buf := bytes.NewBuffer([]byte{}) 155 156 size, goodIndentation, badIndentation, err := computeIndentationValues(def) 157 if err != nil { 158 return nil, false, err 159 } 160 161 eol, err := def.EOL() 162 if err != nil { 163 return nil, false, fmt.Errorf("cannot get EOL: %w", err) 164 } 165 166 trimTrailingWhitespace := false 167 if def.TrimTrailingWhitespace != nil { 168 trimTrailingWhitespace = *def.TrimTrailingWhitespace 169 } 170 171 fixed := false 172 errs := ReadLines(r, fileSize, func(index int, data []byte, isEOF bool) error { 173 var f bool 174 if size != 0 { 175 data, f = fixTabAndSpacePrefix(data, goodIndentation, badIndentation) 176 fixed = fixed || f 177 } 178 179 if trimTrailingWhitespace { 180 data, f = fixTrailingWhitespace(data) 181 fixed = fixed || f 182 } 183 184 if def.EndOfLine != "" && !isEOF { 185 data, f = fixEndOfLine(data, eol) 186 fixed = fixed || f 187 } 188 189 _, err := buf.Write(data) 190 if err != nil { 191 return fmt.Errorf("error writing into buffer: %w", err) 192 } 193 194 log.V(2).Info("fix line", "index", index, "fixed", fixed) 195 196 return nil 197 }) 198 199 if len(errs) != 0 { 200 return nil, false, errs[0] 201 } 202 203 if def.InsertFinalNewline != nil { 204 f := fixInsertFinalNewline(buf, *def.InsertFinalNewline, eol) 205 fixed = fixed || f 206 } 207 208 return buf, fixed, nil 209 } 210 211 // fixEndOfLine replaces any non eol suffix by the given one. 212 func fixEndOfLine(data []byte, eol []byte) ([]byte, bool) { 213 fixed := false 214 215 if !bytes.HasSuffix(data, eol) { 216 fixed = true 217 data = bytes.TrimRight(data, "\r\n") 218 data = append(data, eol...) 219 } 220 221 return data, fixed 222 } 223 224 // fixTabAndSpacePrefix replaces any `x` by `c` in the given `data`. 225 func fixTabAndSpacePrefix(data []byte, c []byte, x []byte) ([]byte, bool) { 226 newData := make([]byte, 0, len(data)) 227 228 fixed := false 229 230 i := 0 231 for i < len(data) { 232 if bytes.HasPrefix(data[i:], c) { 233 i += len(c) 234 235 newData = append(newData, c...) 236 237 continue 238 } 239 240 if bytes.HasPrefix(data[i:], x) { 241 i += len(x) 242 243 newData = append(newData, c...) 244 245 fixed = true 246 247 continue 248 } 249 250 return append(newData, data[i:]...), fixed 251 } 252 253 return data, fixed 254 } 255 256 // fixTrailingWhitespace replaces any whitespace or tab from the end of the line. 257 func fixTrailingWhitespace(data []byte) ([]byte, bool) { 258 i := len(data) - 1 259 260 // u -> v is the range to clean 261 u := len(data) 262 263 v := u //nolint: ifshort 264 265 outer: 266 for i >= 0 { 267 switch data[i] { 268 case '\r', '\n': 269 i-- 270 u-- 271 v-- 272 case ' ', '\t': 273 i-- 274 u-- 275 default: 276 break outer 277 } 278 } 279 280 // If u != v then the line has been fixed. 281 fixed := u != v 282 if fixed { 283 data = append(data[:u], data[v:]...) 284 } 285 286 return data, fixed 287 } 288 289 // fixInsertFinalNewline modifies buf to fix the existence of a final newline. 290 // Line endings are assumed to already be consistent within the buffer. 291 // A nil buffer or an empty buffer is returned as is. 292 func fixInsertFinalNewline(buf *bytes.Buffer, insertFinalNewline bool, endOfLine []byte) bool { 293 fixed := false 294 295 if buf == nil || buf.Len() == 0 { 296 return fixed 297 } 298 299 if insertFinalNewline { 300 if !bytes.HasSuffix(buf.Bytes(), endOfLine) { 301 fixed = true 302 303 buf.Write(endOfLine) 304 } 305 } else { 306 for bytes.HasSuffix(buf.Bytes(), endOfLine) { 307 fixed = true 308 309 buf.Truncate(buf.Len() - len(endOfLine)) 310 } 311 } 312 313 return fixed 314 }