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

     1  package eclint
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  
     8  	"github.com/editorconfig/editorconfig-core-go/v2"
     9  )
    10  
    11  const (
    12  	cr    = '\r'
    13  	lf    = '\n'
    14  	tab   = '\t'
    15  	space = ' '
    16  )
    17  
    18  var (
    19  	utf8Bom    = []byte{0xef, 0xbb, 0xbf} //nolint:gochecknoglobals
    20  	utf16leBom = []byte{0xff, 0xfe}       //nolint:gochecknoglobals
    21  	utf16beBom = []byte{0xfe, 0xff}       //nolint:gochecknoglobals
    22  	utf32leBom = []byte{0xff, 0xfe, 0, 0} //nolint:gochecknoglobals
    23  	utf32beBom = []byte{0, 0, 0xfe, 0xff} //nolint:gochecknoglobals
    24  )
    25  
    26  // ErrConfiguration represents an error in the editorconfig value.
    27  var ErrConfiguration = errors.New("configuration error")
    28  
    29  // ValidationError is a rich type containing information about the error.
    30  type ValidationError struct {
    31  	Message  string
    32  	Filename string
    33  	Line     []byte
    34  	Index    int
    35  	Position int
    36  }
    37  
    38  func (e ValidationError) String() string {
    39  	return e.Error()
    40  }
    41  
    42  // Error builds the error string.
    43  func (e ValidationError) Error() string {
    44  	return fmt.Sprintf("%s:%d:%d: %s", e.Filename, e.Index+1, e.Position+1, e.Message)
    45  }
    46  
    47  // endOfLines checks the line ending.
    48  func endOfLine(eol string, data []byte) error {
    49  	switch eol {
    50  	case editorconfig.EndOfLineLf:
    51  		if !bytes.HasSuffix(data, []byte{lf}) || bytes.HasSuffix(data, []byte{cr, lf}) {
    52  			return ValidationError{
    53  				Message:  "line does not end with lf (`\\n`)",
    54  				Position: len(data),
    55  			}
    56  		}
    57  	case editorconfig.EndOfLineCrLf:
    58  		if !bytes.HasSuffix(data, []byte{cr, lf}) && !bytes.HasSuffix(data, []byte{0x00, cr, 0x00, lf}) {
    59  			return ValidationError{
    60  				Message:  "line does not end with crlf (`\\r\\n`)",
    61  				Position: len(data),
    62  			}
    63  		}
    64  	case editorconfig.EndOfLineCr:
    65  		if !bytes.HasSuffix(data, []byte{cr}) {
    66  			return ValidationError{
    67  				Message:  "line does not end with cr (`\\r`)",
    68  				Position: len(data),
    69  			}
    70  		}
    71  	default:
    72  		return fmt.Errorf("%w: %q is an invalid value for eol, want cr, crlf, or lf", ErrConfiguration, eol)
    73  	}
    74  
    75  	return nil
    76  }
    77  
    78  // indentStyle checks that the line beginnings are either space or tabs.
    79  func indentStyle(style string, size int, data []byte) error {
    80  	var c byte
    81  
    82  	var x byte
    83  
    84  	switch style {
    85  	case SpaceValue:
    86  		c = space
    87  		x = tab
    88  	case TabValue:
    89  		c = tab
    90  		x = space
    91  		size = 1
    92  	case UnsetValue:
    93  		return nil
    94  	default:
    95  		return fmt.Errorf("%w: %q is an invalid value of indent_style, want tab or space", ErrConfiguration, style)
    96  	}
    97  
    98  	if size == 0 {
    99  		return nil
   100  	}
   101  
   102  	if size < 0 {
   103  		return fmt.Errorf("%w: %d is an invalid value of indent_size, want a number or unset", ErrConfiguration, size)
   104  	}
   105  
   106  	for i := 0; i < len(data); i++ {
   107  		if data[i] == c {
   108  			continue
   109  		}
   110  
   111  		if data[i] == x {
   112  			return ValidationError{
   113  				Message:  fmt.Sprintf("indentation style mismatch expected %q (%s) got %q", c, style, x),
   114  				Position: i,
   115  			}
   116  		}
   117  
   118  		if data[i] == cr || data[i] == lf || (size > 0 && i%size == 0) {
   119  			break
   120  		}
   121  
   122  		return ValidationError{
   123  			Message:  fmt.Sprintf("indentation size doesn't match expected %d, got %d", size, i),
   124  			Position: i,
   125  		}
   126  	}
   127  
   128  	return nil
   129  }
   130  
   131  // checkInsertFinalNewline checks whenever the final line contains a newline or not.
   132  func checkInsertFinalNewline(data []byte, insertFinalNewline bool) error {
   133  	if len(data) == 0 {
   134  		return nil
   135  	}
   136  
   137  	lastChar := data[len(data)-1]
   138  	if lastChar != cr && lastChar != lf {
   139  		if insertFinalNewline {
   140  			return ValidationError{
   141  				Message:  "the final newline is missing",
   142  				Position: len(data),
   143  			}
   144  		}
   145  	} else {
   146  		if !insertFinalNewline {
   147  			return ValidationError{
   148  				Message:  "an extraneous final newline was found",
   149  				Position: len(data),
   150  			}
   151  		}
   152  	}
   153  
   154  	return nil
   155  }
   156  
   157  // checkTrimTrailingWhitespace lints any spaces before the final newline.
   158  func checkTrimTrailingWhitespace(data []byte) error {
   159  	for i := len(data) - 1; i >= 0; i-- {
   160  		if data[i] == cr || data[i] == lf {
   161  			continue
   162  		}
   163  
   164  		if data[i] == space || data[i] == tab {
   165  			return ValidationError{
   166  				Message:  "line has some trailing whitespaces",
   167  				Position: i,
   168  			}
   169  		}
   170  
   171  		break
   172  	}
   173  
   174  	return nil
   175  }
   176  
   177  // isBlockCommentStart tells you when a block comment started on this line.
   178  func isBlockCommentStart(start []byte, data []byte) bool {
   179  	for i := 0; i < len(data); i++ {
   180  		if data[i] == space || data[i] == tab {
   181  			continue
   182  		}
   183  
   184  		return bytes.HasPrefix(data[i:], start)
   185  	}
   186  
   187  	return false
   188  }
   189  
   190  // checkBlockComment checks the line is a valid block comment.
   191  func checkBlockComment(i int, prefix []byte, data []byte) error {
   192  	for ; i < len(data); i++ {
   193  		if data[i] == space || data[i] == tab {
   194  			continue
   195  		}
   196  
   197  		if !bytes.HasPrefix(data[i:], prefix) {
   198  			return ValidationError{
   199  				Message:  fmt.Sprintf("block_comment prefix %q was expected inside a block comment", string(prefix)),
   200  				Position: i,
   201  			}
   202  		}
   203  
   204  		break
   205  	}
   206  
   207  	return nil
   208  }
   209  
   210  // isBlockCommentEnd tells you when a block comment end on this line.
   211  func isBlockCommentEnd(end []byte, data []byte) bool {
   212  	for i := len(data) - 1; i > 0; i-- {
   213  		if data[i] == cr || data[i] == lf {
   214  			continue
   215  		}
   216  
   217  		return bytes.HasSuffix(data[:i+1], end)
   218  	}
   219  
   220  	return false
   221  }
   222  
   223  // MaxLineLength checks the length of a given line.
   224  //
   225  // It assumes UTF-8 and will count as one runes. The first byte has no prefix
   226  // 0xxxxxxx, 110xxxxx, 1110xxxx, 11110xxx, 111110xx, etc. and the following byte
   227  // the 10xxxxxx prefix which are skipped.
   228  func MaxLineLength(maxLength int, tabWidth int, data []byte) error {
   229  	length := 0
   230  	breakingPosition := 0
   231  
   232  	for i := 0; i < len(data); i++ {
   233  		if data[i] == cr || data[i] == lf {
   234  			break
   235  		}
   236  
   237  		switch {
   238  		case data[i] == tab:
   239  			length += tabWidth
   240  		case (data[i] >> 6) == 0b10:
   241  			// skip 0x10xxxxxx that are UTF-8 continuation markers
   242  		default:
   243  			length++
   244  		}
   245  
   246  		if length > maxLength && breakingPosition == 0 {
   247  			breakingPosition = i
   248  		}
   249  	}
   250  
   251  	if length > maxLength {
   252  		return ValidationError{
   253  			Message:  fmt.Sprintf("line is too long (%d > %d)", length, maxLength),
   254  			Position: breakingPosition,
   255  		}
   256  	}
   257  
   258  	return nil
   259  }