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  }