From 7b0b0cb6bf078c294ee38a50f8ced1a00081ec13 Mon Sep 17 00:00:00 2001 From: Yoan Blanc Date: Fri, 22 Nov 2019 09:37:19 +0000 Subject: [PATCH] lint: max_line_length supports utf-8 Signed-off-by: Yoan Blanc --- .gitlab-ci.yml | 2 +- .goreleaser.yml | 1 + cmd/eclint/main.go | 99 ++++++++++++++++++++++++++++++++++++++++++++ files.go | 6 +-- files_test.go | 8 ++-- lint.go | 7 ++-- lint_test.go | 8 ++-- main.go | 100 --------------------------------------------- option.go | 19 +++++++++ print.go | 51 +++++++++++------------ scanner.go | 2 +- validators.go | 10 +++-- validators_test.go | 6 +-- 13 files changed, 169 insertions(+), 150 deletions(-) create mode 100644 cmd/eclint/main.go delete mode 100644 main.go create mode 100644 option.go diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d0334c4..b5a371e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -17,7 +17,7 @@ go test: eclint: stage: lint script: - - go build . + - go build -o eclint cmd/eclint/main.go - ./eclint -exclude "testdata/**/*" golangci-lint: diff --git a/.goreleaser.yml b/.goreleaser.yml index 56713be..9ce975c 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -6,6 +6,7 @@ before: builds: - id: eclint binary: eclint + main: ./cmd/eclint/main.go goos: - linux - darwin diff --git a/cmd/eclint/main.go b/cmd/eclint/main.go new file mode 100644 index 0000000..90fdcf5 --- /dev/null +++ b/cmd/eclint/main.go @@ -0,0 +1,99 @@ +package main + +import ( + "flag" + "fmt" + "os" + "runtime" + "syscall" + + "golang.org/x/crypto/ssh/terminal" + + "github.com/editorconfig/editorconfig-core-go/v2" + "github.com/mattn/go-colorable" + "gitlab.com/greut/eclint" + "k8s.io/klog/v2" + "k8s.io/klog/v2/klogr" +) + +var ( + version = "dev" +) + +func main() { //nolint:funlen + flagVersion := false + log := klogr.New() + opt := eclint.Option{ + Stdout: os.Stdout, + ShowErrorQuantity: 10, + IsTerminal: terminal.IsTerminal(syscall.Stdout), + Log: log, + } + + if runtime.GOOS == "windows" { + opt.Stdout = colorable.NewColorableStdout() + } + + // Flags + klog.InitFlags(nil) + flag.BoolVar(&flagVersion, "version", false, "print the version number") + flag.BoolVar(&opt.NoColors, "no_colors", false, "enable or disable colors") + flag.BoolVar(&opt.Summary, "summary", false, "enable the summary view") + flag.BoolVar( + &opt.ShowAllErrors, + "show_all_errors", + false, + fmt.Sprintf("display all errors for each file (otherwise %d are kept)", opt.ShowErrorQuantity), + ) + flag.StringVar(&opt.Exclude, "exclude", "", "paths to exclude") + flag.Parse() + + if flagVersion { + fmt.Fprintf(opt.Stdout, "eclint %s\n", version) + return + } + + args := flag.Args() + files, err := eclint.ListFiles(log, args...) + if err != nil { + log.Error(err, "error while handling the arguments") + flag.Usage() + os.Exit(1) + return + } + + log.V(1).Info("files", "count", len(files), "exclude", opt.Exclude) + + if opt.Summary { + opt.ShowAllErrors = true + opt.ShowErrorQuantity = int(^uint(0) >> 1) + } + + c := 0 + for _, filename := range files { + // Skip excluded files + if opt.Exclude != "" { + ok, err := editorconfig.FnmatchCase(opt.Exclude, filename) + if err != nil { + log.Error(err, "exclude pattern failure", "exclude", opt.Exclude) + c++ + break + } + if ok { + continue + } + } + + errs := eclint.Lint(filename, opt.Log) + c += len(errs) + err := eclint.PrintErrors(opt, filename, errs) + if err != nil { + log.Error(err, "print errors failure", "filename", filename) + } + } + + if c > 0 { + opt.Log.V(1).Info("Some errors were found.", "count", c) + os.Exit(1) + } +} diff --git a/files.go b/files.go index ba067cf..7d31bb4 100644 --- a/files.go +++ b/files.go @@ -1,4 +1,4 @@ -package main +package eclint import ( "bytes" @@ -9,7 +9,7 @@ import ( "github.com/go-logr/logr" ) -// listFiles returns the list of files based on the input. +// ListFiles returns the list of files based on the input. // // When its empty, it relies on `git ls-files` first, which // whould fail if `git` is not present or the current working @@ -17,7 +17,7 @@ import ( // current working directory. // // When args are given, it recursively walks into them. -func listFiles(log logr.Logger, args ...string) ([]string, error) { +func ListFiles(log logr.Logger, args ...string) ([]string, error) { if len(args) == 0 { fs, err := gitLsFiles(log, ".") if err == nil { diff --git a/files_test.go b/files_test.go index 8470da5..1581dbd 100644 --- a/files_test.go +++ b/files_test.go @@ -1,4 +1,4 @@ -package main +package eclint import ( "fmt" @@ -17,7 +17,7 @@ const ( func TestListFiles(t *testing.T) { l := tlogr.TestLogger{} d := testdataSimple - fs, err := listFiles(l, d) + fs, err := ListFiles(l, d) if err != nil { t.Fatal(err) } @@ -45,7 +45,7 @@ func TestListFilesNoArgs(t *testing.T) { t.Fatal(err) } - fs, err := listFiles(l) + fs, err := ListFiles(l) if err != nil { t.Fatal(err) } @@ -78,7 +78,7 @@ func TestListFilesNoGit(t *testing.T) { t.Fatal(err) } - fs, err := listFiles(l) + fs, err := ListFiles(l) if err != nil { t.Fatal(err) } diff --git a/lint.go b/lint.go index 28034c9..0a570f8 100644 --- a/lint.go +++ b/lint.go @@ -1,4 +1,4 @@ -package main +package eclint import ( "bytes" @@ -144,7 +144,7 @@ func validate(r io.Reader, log logr.Logger, def *editorconfig.Definition) []erro } } } - err = maxLineLength(maxLength, tabWidth, d) + err = MaxLineLength(maxLength, tabWidth, d) } // Enrich the error with the line number @@ -233,7 +233,8 @@ func overrideUsingPrefix(def *editorconfig.Definition, prefix string) error { return nil } -func lint(filename string, log logr.Logger) []error { +// Lint does the hard work of validating the given file. +func Lint(filename string, log logr.Logger) []error { // XXX editorconfig should be able to treat a flux of // filenames with caching capabilities. def, err := editorconfig.GetDefinitionForFilename(filename) diff --git a/lint_test.go b/lint_test.go index d8ce93a..1aaa615 100644 --- a/lint_test.go +++ b/lint_test.go @@ -1,4 +1,4 @@ -package main +package eclint import ( "bytes" @@ -88,7 +88,7 @@ without a final newline.`), func TestLintSimple(t *testing.T) { l := tlogr.TestLogger{} - for _, err := range lint("testdata/simple/simple.txt", l) { + for _, err := range Lint("testdata/simple/simple.txt", l) { if err != nil { t.Errorf("no errors where expected, got %s", err) } @@ -98,7 +98,7 @@ func TestLintSimple(t *testing.T) { func TestLintMissing(t *testing.T) { l := tlogr.TestLogger{} - errs := lint("testdata/missing/file", l) + errs := Lint("testdata/missing/file", l) if len(errs) == 0 { t.Error("an error was expected, got none") } @@ -113,7 +113,7 @@ func TestLintMissing(t *testing.T) { func TestLintInvalid(t *testing.T) { l := tlogr.TestLogger{} - errs := lint("testdata/invalid/.editorconfig", l) + errs := Lint("testdata/invalid/.editorconfig", l) if len(errs) == 0 { t.Error("an error was expected, got none") } diff --git a/main.go b/main.go deleted file mode 100644 index f8fa381..0000000 --- a/main.go +++ /dev/null @@ -1,100 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "io" - "os" - "syscall" - - "golang.org/x/crypto/ssh/terminal" - - "github.com/editorconfig/editorconfig-core-go/v2" - "github.com/go-logr/logr" - "k8s.io/klog/v2" - "k8s.io/klog/v2/klogr" -) - -var ( - version = "dev" -) - -// option contains the environment of the program. -type option struct { - isTerminal bool - noColors bool - showAllErrors bool - summary bool - showErrorQuantity int - exclude string - log logr.Logger - stdout io.Writer -} - -func main() { //nolint:funlen - flagVersion := false - opt := option{ - stdout: os.Stdout, - showErrorQuantity: 10, - log: klogr.New(), - isTerminal: terminal.IsTerminal(syscall.Stdout), - } - - // Flags - klog.InitFlags(nil) - flag.BoolVar(&flagVersion, "version", false, "print the version number") - flag.BoolVar(&opt.noColors, "no_colors", false, "enable or disable colors") - flag.BoolVar(&opt.summary, "summary", false, "enable the summary view") - flag.BoolVar( - &opt.showAllErrors, - "show_all_errors", - false, - fmt.Sprintf("display all errors for each file (otherwise %d are kept)", opt.showErrorQuantity), - ) - flag.StringVar(&opt.exclude, "exclude", "", "paths to exclude") - flag.Parse() - - if flagVersion { - fmt.Fprintf(opt.stdout, "eclint %s\n", version) - return - } - - args := flag.Args() - files, err := listFiles(opt.log, args...) - if err != nil { - opt.log.Error(err, "error while handling the arguments") - flag.Usage() - os.Exit(1) - return - } - - opt.log.V(1).Info("files", "count", len(files), "exclude", opt.exclude) - - if opt.summary { - opt.showAllErrors = true - opt.showErrorQuantity = int(^uint(0) >> 1) - } - - c := 0 - for _, filename := range files { - // Skip excluded files - if opt.exclude != "" { - ok, err := editorconfig.FnmatchCase(opt.exclude, filename) - if err != nil { - opt.log.Error(err, "exclude pattern failure", "exclude", opt.exclude) - c++ - break - } - if ok { - continue - } - } - - c += lintAndPrint(opt, filename) - } - - if c > 0 { - opt.log.V(1).Info("Some errors were found.", "count", c) - os.Exit(1) - } -} diff --git a/option.go b/option.go new file mode 100644 index 0000000..ae9043a --- /dev/null +++ b/option.go @@ -0,0 +1,19 @@ +package eclint + +import ( + "io" + + "github.com/go-logr/logr" +) + +// Option contains the environment of the program. +type Option struct { + IsTerminal bool + NoColors bool + ShowAllErrors bool + Summary bool + ShowErrorQuantity int + Exclude string + Log logr.Logger + Stdout io.Writer +} diff --git a/print.go b/print.go index 2b25905..3079a1f 100644 --- a/print.go +++ b/print.go @@ -1,71 +1,66 @@ -package main +package eclint import ( "bytes" "fmt" - "runtime" "strconv" "github.com/logrusorgru/aurora" - "github.com/mattn/go-colorable" ) -// lintAndPrint is the rich output of the program. -func lintAndPrint(opt option, filename string) int { - c := 0 - d := 0 +// PrintErrors is the rich output of the program. +func PrintErrors(opt Option, filename string, errors []error) error { + counter := 0 - stdout := opt.stdout - if runtime.GOOS == "windows" { - stdout = colorable.NewColorableStdout() - } + log := opt.Log + stdout := opt.Stdout + + au := aurora.NewAurora(opt.IsTerminal && !opt.NoColors) - au := aurora.NewAurora(opt.isTerminal && !opt.noColors) - errs := lint(filename, opt.log) - for _, err := range errs { + for _, err := range errors { if err != nil { - if d == 0 && !opt.summary { + if counter == 0 && !opt.Summary { fmt.Fprintf(stdout, "%s:\n", au.Magenta(filename)) } if ve, ok := err.(validationError); ok { - opt.log.V(4).Info("lint error", "error", ve) - if !opt.summary { + log.V(4).Info("lint error", "error", ve) + if !opt.Summary { vi := au.Green(strconv.Itoa(ve.index)) vp := au.Green(strconv.Itoa(ve.position)) fmt.Fprintf(stdout, "%s:%s: %s\n", vi, vp, ve.error) l, err := errorAt(au, ve.line, ve.position-1) if err != nil { - opt.log.Error(err, "line formating failure", "error", ve) - continue + log.Error(err, "line formating failure", "error", ve) + return err } fmt.Fprintln(stdout, l) } } else { - opt.log.V(4).Info("lint error", "filename", filename, "error", err) + log.V(4).Info("lint error", "filename", filename, "error", err) fmt.Fprintln(stdout, err) } - if d >= opt.showErrorQuantity && len(errs) > d { + if counter >= opt.ShowErrorQuantity && len(errors) > counter { fmt.Fprintln( stdout, - fmt.Sprintf(" ... skipping at most %s errors", au.BrightRed(strconv.Itoa(len(errs)-d))), + fmt.Sprintf(" ... skipping at most %s errors", au.BrightRed(strconv.Itoa(len(errors)-counter))), ) break } - d++ - c++ + counter++ } } - if d > 0 { - if !opt.summary { + + if counter > 0 { + if !opt.Summary { fmt.Fprintln(stdout, "") } else { - fmt.Fprintf(stdout, "%s: %d errors\n", au.Magenta(filename), d) + fmt.Fprintf(stdout, "%s: %d errors\n", au.Magenta(filename), counter) } } - return c + return nil } // errorAt highlights the validationError position within the line. diff --git a/scanner.go b/scanner.go index 3357090..2e44e35 100644 --- a/scanner.go +++ b/scanner.go @@ -1,4 +1,4 @@ -package main +package eclint import ( "bufio" diff --git a/validators.go b/validators.go index 3dd0406..69c414b 100644 --- a/validators.go +++ b/validators.go @@ -1,4 +1,4 @@ -package main +package eclint import ( "bytes" @@ -217,8 +217,12 @@ func isBlockCommentEnd(end []byte, data []byte) bool { return false } -// maxLineLength checks the length of a given line -func maxLineLength(maxLength int, tabWidth int, data []byte) error { +// MaxLineLength checks the length of a given line. +// +// It assumes UTF-8 and will count as one runes. The first byte has no prefix +// 0xxxxxxx, 110xxxxx, 1110xxxx, 11110xxx, 111110xx, etc. and the following byte +// the 10xxxxxx prefix which are skipped. +func MaxLineLength(maxLength int, tabWidth int, data []byte) error { length := 0 breakingPosition := 0 for i := 0; i < len(data); i++ { diff --git a/validators_test.go b/validators_test.go index 8889069..5b00fd1 100644 --- a/validators_test.go +++ b/validators_test.go @@ -1,4 +1,4 @@ -package main +package eclint import ( "bytes" @@ -385,7 +385,7 @@ func TestMaxLineLength(t *testing.T) { tc := tc t.Run(tc.Name, func(t *testing.T) { t.Parallel() - err := maxLineLength(tc.MaxLineLength, tc.TabWidth, tc.Line) + err := MaxLineLength(tc.MaxLineLength, tc.TabWidth, tc.Line) if err != nil { t.Errorf("no errors were expected, got %s", err) } @@ -416,7 +416,7 @@ func TestMaxLineLengthFailure(t *testing.T) { tc := tc t.Run(tc.Name, func(t *testing.T) { t.Parallel() - err := maxLineLength(tc.MaxLineLength, tc.TabWidth, tc.Line) + err := MaxLineLength(tc.MaxLineLength, tc.TabWidth, tc.Line) if err == nil { t.Error("an error was expected") }