gitlab.com/greut/eclint@v0.5.2-0.20240402114752-14681fe6e0bf/lint.go (about)

     1  package eclint
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"context"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  
    12  	"github.com/editorconfig/editorconfig-core-go/v2"
    13  	"github.com/go-logr/logr"
    14  	"golang.org/x/text/encoding"
    15  	"golang.org/x/text/encoding/unicode"
    16  	"golang.org/x/text/transform"
    17  )
    18  
    19  // DefaultTabWidth sets the width of a tab used when counting the line length.
    20  const DefaultTabWidth = 8
    21  
    22  const (
    23  	// UnsetValue is the value equivalent to an empty / unset one.
    24  	UnsetValue = "unset"
    25  	// TabValue is the value representing tab indentation (the ugly one).
    26  	TabValue = "tab"
    27  	// SpaceValue is the value representing space indentation (the good one).
    28  	SpaceValue = "space"
    29  	// Utf8 is the ubiquitous character set.
    30  	Utf8 = "utf-8"
    31  	// Latin1 is the legacy 7-bits character set.
    32  	Latin1 = "latin1"
    33  )
    34  
    35  // Lint does the hard work of validating the given file.
    36  func Lint(ctx context.Context, filename string) []error {
    37  	def, err := editorconfig.GetDefinitionForFilename(filename)
    38  	if err != nil {
    39  		return []error{fmt.Errorf("cannot open file %s. %w", filename, err)}
    40  	}
    41  
    42  	return LintWithDefinition(ctx, def, filename)
    43  }
    44  
    45  // LintWithDefinition does the hard work of validating the given file.
    46  func LintWithDefinition(ctx context.Context, d *editorconfig.Definition, filename string) []error { //nolint:funlen
    47  	log := logr.FromContextOrDiscard(ctx)
    48  
    49  	def, err := newDefinition(d)
    50  	if err != nil {
    51  		return []error{err}
    52  	}
    53  
    54  	stat, err := os.Stat(filename)
    55  	if err != nil {
    56  		return []error{fmt.Errorf("cannot stat %s. %w", filename, err)}
    57  	}
    58  
    59  	if stat.IsDir() {
    60  		log.V(2).Info("skipped directory")
    61  
    62  		return nil
    63  	}
    64  
    65  	fileSize := stat.Size()
    66  
    67  	fp, err := os.Open(filename)
    68  	if err != nil {
    69  		return []error{fmt.Errorf("cannot open %s. %w", filename, err)}
    70  	}
    71  
    72  	defer fp.Close()
    73  
    74  	r := bufio.NewReader(fp)
    75  
    76  	ok, err := probeReadable(fp, r)
    77  	if err != nil {
    78  		return []error{fmt.Errorf("cannot read %s. %w", filename, err)}
    79  	}
    80  
    81  	if !ok {
    82  		log.V(2).Info("skipped unreadable or empty file")
    83  
    84  		return nil
    85  	}
    86  
    87  	charset, isBinary, err := ProbeCharsetOrBinary(ctx, r, def.Charset)
    88  	if err != nil {
    89  		return []error{err}
    90  	}
    91  
    92  	if isBinary {
    93  		log.V(2).Info("binary file detected and skipped")
    94  
    95  		return nil
    96  	}
    97  
    98  	log.V(2).Info("charset probed", "filename", filename, "charset", charset)
    99  
   100  	var decoder *encoding.Decoder
   101  
   102  	switch charset {
   103  	case "utf-16be":
   104  		decoder = unicode.UTF16(unicode.BigEndian, unicode.ExpectBOM).NewDecoder()
   105  	case "utf-16le":
   106  		decoder = unicode.UTF16(unicode.LittleEndian, unicode.ExpectBOM).NewDecoder()
   107  	default:
   108  		decoder = unicode.UTF8.NewDecoder()
   109  	}
   110  
   111  	t := transform.NewReader(r, unicode.BOMOverride(decoder))
   112  	errs := validate(ctx, t, fileSize, charset, def)
   113  
   114  	// Enrich the errors with the filename
   115  	for i, err := range errs {
   116  		var ve ValidationError
   117  		if ok := errors.As(err, &ve); ok {
   118  			ve.Filename = filename
   119  			errs[i] = ve
   120  		} else if err != nil {
   121  			errs[i] = err
   122  		}
   123  	}
   124  
   125  	return errs
   126  }
   127  
   128  // validate is where the validations rules are applied.
   129  func validate( //nolint:cyclop,gocognit,funlen
   130  	ctx context.Context,
   131  	r io.Reader,
   132  	fileSize int64,
   133  	charset string,
   134  	def *definition,
   135  ) []error {
   136  	return ReadLines(r, fileSize, func(index int, data []byte, isEOF bool) error {
   137  		var err error
   138  
   139  		if ctx.Err() != nil {
   140  			return fmt.Errorf("read lines got interrupted: %w", ctx.Err())
   141  		}
   142  
   143  		if isEOF {
   144  			if def.InsertFinalNewline != nil {
   145  				err = checkInsertFinalNewline(data, *def.InsertFinalNewline)
   146  			}
   147  		} else {
   148  			if def.EndOfLine != "" && def.EndOfLine != UnsetValue {
   149  				err = endOfLine(def.EndOfLine, data)
   150  			}
   151  		}
   152  
   153  		if err == nil && //nolint:nestif
   154  			def.IndentStyle != "" &&
   155  			def.IndentStyle != UnsetValue &&
   156  			def.Definition.IndentSize != UnsetValue {
   157  			err = indentStyle(def.IndentStyle, def.IndentSize, data)
   158  			if err != nil && def.InsideBlockComment && def.BlockComment != nil {
   159  				// The indentation may fail within a block comment.
   160  				var ve ValidationError
   161  				if ok := errors.As(err, &ve); ok {
   162  					err = checkBlockComment(ve.Position, def.BlockComment, data)
   163  				}
   164  			}
   165  
   166  			if def.InsideBlockComment && def.BlockCommentEnd != nil {
   167  				def.InsideBlockComment = !isBlockCommentEnd(def.BlockCommentEnd, data)
   168  			}
   169  
   170  			if err == nil && !def.InsideBlockComment && def.BlockCommentStart != nil {
   171  				def.InsideBlockComment = isBlockCommentStart(def.BlockCommentStart, data)
   172  			}
   173  		}
   174  
   175  		if err == nil && def.TrimTrailingWhitespace != nil && *def.TrimTrailingWhitespace {
   176  			err = checkTrimTrailingWhitespace(data)
   177  		}
   178  
   179  		if err == nil && def.MaxLength > 0 {
   180  			// Remove any BOM from the first line.
   181  			d := data
   182  
   183  			if index == 0 && charset != "" {
   184  				for _, bom := range [][]byte{utf8Bom} {
   185  					if bytes.HasPrefix(data, bom) {
   186  						d = data[len(utf8Bom):]
   187  
   188  						break
   189  					}
   190  				}
   191  			}
   192  
   193  			err = MaxLineLength(def.MaxLength, def.TabWidth, d)
   194  		}
   195  
   196  		// Enrich the error with the line number
   197  		var ve ValidationError
   198  		if ok := errors.As(err, &ve); ok {
   199  			ve.Line = data
   200  			ve.Index = index
   201  
   202  			return ve
   203  		}
   204  
   205  		return err
   206  	})
   207  }