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 }