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 }