From 67e51c079aa2d986e906958c07d8e77010aefd41 Mon Sep 17 00:00:00 2001 From: Amy Boyd Date: Thu, 22 Dec 2016 14:02:44 +0000 Subject: [PATCH] Add a fixer command. --- .gitignore | 1 + bin/run-fix | 2 + docs/to-do.md | 6 ++- editorconfig/check_command.go | 7 +-- editorconfig/cli.go | 6 +++ editorconfig/fix_command.go | 78 ++++++++++++++++++++++++++++++ editorconfig/full_file_checkers.go | 18 +++---- editorconfig/line_checkers.go | 26 +++++++--- editorconfig/line_checkers_test.go | 2 +- editorconfig/utils.go | 4 ++ 10 files changed, 126 insertions(+), 24 deletions(-) create mode 100755 bin/run-fix create mode 100644 editorconfig/fix_command.go diff --git a/.gitignore b/.gitignore index 84c048a..6ca29ff 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /build/ +/symfony/ diff --git a/bin/run-fix b/bin/run-fix new file mode 100755 index 0000000..c242c40 --- /dev/null +++ b/bin/run-fix @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +go run main.go fix symfony diff --git a/docs/to-do.md b/docs/to-do.md index bf57103..5e26d49 100644 --- a/docs/to-do.md +++ b/docs/to-do.md @@ -1,6 +1,8 @@ * Test on Windows. -* Implement fixers and a `fix` command. +* Add a fixer for the 'indent_style' rule. + +* Add a non-standard rule to ignore directories. * Optimize speed and memory usage. @@ -11,3 +13,5 @@ * Add an example of how to use the `check` command in a continuous integration build or in a Git pre-commit hook. + + diff --git a/editorconfig/check_command.go b/editorconfig/check_command.go index dd03848..75becae 100644 --- a/editorconfig/check_command.go +++ b/editorconfig/check_command.go @@ -3,7 +3,6 @@ package editorconfig import ( "fmt" "github.com/codegangsta/cli" - "io/ioutil" "strconv" "strings" ) @@ -26,11 +25,7 @@ func CheckCommand(c *cli.Context) error { continue } - fileContentInBytes, err := ioutil.ReadFile(f) - if err != nil { - ExitBecauseOfInternalError("Could not read file: " + f) - } - fileContent := string(fileContentInBytes) + fileContent := MustGetFileAsString(f) // Run full-file checkers. for ruleName, ruleValue := range rules { diff --git a/editorconfig/cli.go b/editorconfig/cli.go index 242982f..c893225 100644 --- a/editorconfig/cli.go +++ b/editorconfig/cli.go @@ -30,6 +30,12 @@ func CreateCliApp() *cli.App { Action: CheckCommand, ArgsUsage: "[PATH1] [PATH2...]", }, + { + Name: "fix", + Usage: "Fix invalid files", + Action: FixCommand, + ArgsUsage: "[PATH1] [PATH2...]", + }, } return app diff --git a/editorconfig/fix_command.go b/editorconfig/fix_command.go new file mode 100644 index 0000000..1fa65bd --- /dev/null +++ b/editorconfig/fix_command.go @@ -0,0 +1,78 @@ +package editorconfig + +import ( + "fmt" + "github.com/codegangsta/cli" + "os" + "strconv" + "strings" +) + +func FixCommand(c *cli.Context) error { + files, err := FindSourceFiles(c.Args()) + if err != nil { + return err + } + + if len(files) == 0 { + ExitBecauseOfInternalError("No files to check in " + strings.Join(c.Args(), ", ")) + } + + configs := FindConfigFiles(files) + + for _, f := range files { + rules := GetRulesToApplyToSourcePath(f, configs) + if len(rules) == 0 { + continue + } + + fileContent := MustGetFileAsString(f) + hasChanged := false + + // Run full-file checkers and fixers. + for ruleName, ruleValue := range rules { + if fullFileChecker, ok := fullFileCheckers[ruleName]; ok { + result := fullFileChecker(ruleValue, fileContent) + if result.isOk { + continue + } + + if result.fixer != nil { + fileContent = result.fixer(ruleValue, fileContent) + hasChanged = true + fmt.Println(f + ": " + ruleName + ": fixed") + } else { + fmt.Println(f + ": " + ruleName + ": cannot fix automatically") + } + } + } + + // Run line checkers and fixers. + lines := SplitIntoLines(fileContent) + lineNo := 1 + for _, line := range lines { + for ruleName, ruleValue := range rules { + if lineChecker, ok := lineCheckers[ruleName]; ok { + result := lineChecker(ruleValue, line) + if !result.isOk { + fmt.Println(f + ": line " + strconv.Itoa(lineNo) + ": " + ruleName + ": " + result.messageIfNotOk) + // Don't show more than 1 error per line. + break + } + } + } + lineNo++ + } + + if hasChanged { + fileHandler, err := os.Open(f) + if err != nil { + fmt.Println("Could not write to " + f) + } + fileHandler.WriteString(fileContent) + fmt.Println("Wrote to " + f) + } + } + + return nil +} diff --git a/editorconfig/full_file_checkers.go b/editorconfig/full_file_checkers.go index 6f1e35c..fe17932 100644 --- a/editorconfig/full_file_checkers.go +++ b/editorconfig/full_file_checkers.go @@ -13,10 +13,10 @@ var fullFileCheckers = map[string]FullFileChecker{ type FullFileChecker func(ruleValue string, fileContent string) *FullFileCheckResult -// @todo - add fixers to each instance of FullFileCheckResult. type FullFileCheckResult struct { isOk bool messageIfNotOk string + fixer FullFileFixer } func CheckEndOfLineRule(ruleValue string, fileContent string) *FullFileCheckResult { @@ -29,29 +29,29 @@ func CheckEndOfLineRule(ruleValue string, fileContent string) *FullFileCheckResu if ruleValueLowercase == "lf" { if crlfRegexp.MatchString(fileContent) { - return &FullFileCheckResult{isOk: false, messageIfNotOk: "should use LF for new lines but contains CRLF"} + return &FullFileCheckResult{isOk: false, messageIfNotOk: "should use LF for new lines but contains CRLF", fixer: FixEndOfLineRule} } if crRegexp.MatchString(fileContent) { - return &FullFileCheckResult{isOk: false, messageIfNotOk: "should use LF for new lines but contains CR"} + return &FullFileCheckResult{isOk: false, messageIfNotOk: "should use LF for new lines but contains CR", fixer: FixEndOfLineRule} } } if ruleValueLowercase == "cr" { if crlfRegexp.MatchString(fileContent) { - return &FullFileCheckResult{isOk: false, messageIfNotOk: "should use CR for new lines but contains CRLF"} + return &FullFileCheckResult{isOk: false, messageIfNotOk: "should use CR for new lines but contains CRLF", fixer: FixEndOfLineRule} } if lfRegexp.MatchString(fileContent) { - return &FullFileCheckResult{isOk: false, messageIfNotOk: "should use CR for new lines but contains LF"} + return &FullFileCheckResult{isOk: false, messageIfNotOk: "should use CR for new lines but contains LF", fixer: FixEndOfLineRule} } } if ruleValueLowercase == "crlf" { fileContent := crlfRegexp.ReplaceAllString(fileContent, "") if lfRegexp.MatchString(fileContent) { - return &FullFileCheckResult{isOk: false, messageIfNotOk: "should use CRLF for new lines but contains LF"} + return &FullFileCheckResult{isOk: false, messageIfNotOk: "should use CRLF for new lines but contains LF", fixer: FixEndOfLineRule} } if crRegexp.MatchString(fileContent) { - return &FullFileCheckResult{isOk: false, messageIfNotOk: "should use CRLF for new lines but contains CR"} + return &FullFileCheckResult{isOk: false, messageIfNotOk: "should use CRLF for new lines but contains CR", fixer: FixEndOfLineRule} } } @@ -74,7 +74,7 @@ func CheckInsertFinalNewLineRule(ruleValue string, fileContent string) *FullFile if endsWithFinalNewLineRegexp.MatchString(fileContent) { return &FullFileCheckResult{isOk: true} } else { - return &FullFileCheckResult{isOk: false, messageIfNotOk: "should end with an empty line but it does not"} + return &FullFileCheckResult{isOk: false, messageIfNotOk: "should end with an empty line but it does not", fixer: FixInsertFinalNewLineRule} } } @@ -82,7 +82,7 @@ func CheckInsertFinalNewLineRule(ruleValue string, fileContent string) *FullFile if !endsWithFinalNewLineRegexp.MatchString(fileContent) { return &FullFileCheckResult{isOk: true} } else { - return &FullFileCheckResult{isOk: false, messageIfNotOk: "should not end with an empty line but it does"} + return &FullFileCheckResult{isOk: false, messageIfNotOk: "should not end with an empty line but it does", fixer: FixInsertFinalNewLineRule} } } diff --git a/editorconfig/line_checkers.go b/editorconfig/line_checkers.go index b6a9c18..f30f34d 100644 --- a/editorconfig/line_checkers.go +++ b/editorconfig/line_checkers.go @@ -16,10 +16,10 @@ var lineCheckers = map[string]LineChecker{ type LineChecker func(ruleValue string, line string) *LineCheckResult -// @todo - add fixers to each instance of LineCheckResult. type LineCheckResult struct { isOk bool messageIfNotOk string + fixer LineFixer } func HasIndentation(s string) bool { @@ -66,7 +66,7 @@ func CheckIndentStyleRule(ruleValue string, line string) *LineCheckResult { if IsIndentedWithTabs(line) { return &LineCheckResult{isOk: false, messageIfNotOk: "starts with tab instead of space"} } else if IsIndentedWithMixedTabsAndSpaces(line) { - return &LineCheckResult{isOk: false, messageIfNotOk: "indented with mix of tabs and spaces instead of just tabs"} + return &LineCheckResult{isOk: false, messageIfNotOk: "indented with mix of tabs and spaces instead of just spaces"} } else { return &LineCheckResult{isOk: true} } @@ -94,7 +94,11 @@ func CheckIndentSizeRule(ruleValue string, line string) *LineCheckResult { } if strings.HasPrefix(line, "\t") { - return &LineCheckResult{isOk: false, messageIfNotOk: "should be indented with spaces but is indented with tabs"} + return &LineCheckResult{ + isOk: false, + messageIfNotOk: "should be indented with spaces but is indented with tabs", + fixer: FixTabIndentationToSpaces, + } } // Indented with spaces. Ensure the number of spaces is divisible by the rule value, but also @@ -108,11 +112,19 @@ func CheckIndentSizeRule(ruleValue string, line string) *LineCheckResult { return &LineCheckResult{isOk: true} } if IsIndentedWithTabs(trimmedLine) { - return &LineCheckResult{isOk: false, messageIfNotOk: "indented with mix of spaces and tabs instead of just spaces"} + return &LineCheckResult{ + isOk: false, + messageIfNotOk: "indented with mix of spaces and tabs instead of just spaces", + fixer: FixMixedIndentationToSpaces, + } } if HasIndentation(trimmedLine) { - leftSpaces := len(line) - len(strings.TrimLeft(line, " ")) - return &LineCheckResult{isOk: false, messageIfNotOk: "starts with " + strconv.Itoa(leftSpaces) + " spaces which does not divide by " + ruleValue} + leftSpaces := GetNumberOfLeftSpaces(line) + return &LineCheckResult{ + isOk: false, + messageIfNotOk: "starts with " + strconv.Itoa(leftSpaces) + " spaces which does not divide by " + ruleValue, + fixer: FixUndividableIndentationToNearestSpacesAmount, + } } return &LineCheckResult{isOk: true} @@ -129,7 +141,7 @@ func CheckTrimTrailingWhitespaceRule(ruleValue string, line string) *LineCheckRe trimmed := strings.TrimRight(line, " \t") if len(line) != len(trimmed) { - return &LineCheckResult{isOk: false, messageIfNotOk: "line has trailing whitespace"} + return &LineCheckResult{isOk: false, messageIfNotOk: "line has trailing whitespace", fixer: FixTrimTrailingWhitespaceRule} } return &LineCheckResult{isOk: true} diff --git a/editorconfig/line_checkers_test.go b/editorconfig/line_checkers_test.go index 1194365..815a8ae 100644 --- a/editorconfig/line_checkers_test.go +++ b/editorconfig/line_checkers_test.go @@ -32,7 +32,7 @@ func TestCheckIndentStyleRule(t *testing.T) { ExpectPass(" line", "space", f, t) ExpectPass(" ", "space", f, t) ExpectPass(" line", "space", f, t) - ExpectFail(" \tline", "space", f, t, "indented with mix of tabs and spaces instead of just tabs") + ExpectFail(" \tline", "space", f, t, "indented with mix of tabs and spaces instead of just spaces") ExpectFail("\tline", "space", f, t, "starts with tab instead of space") ExpectFail("\t line", "space", f, t, "starts with tab instead of space") diff --git a/editorconfig/utils.go b/editorconfig/utils.go index 64b5a70..5802c69 100644 --- a/editorconfig/utils.go +++ b/editorconfig/utils.go @@ -17,11 +17,15 @@ var lfRegexp = regexp.MustCompile(`\n`) var crRegexp = regexp.MustCompile(`\r`) var crlfRegexp = regexp.MustCompile(`\r\n`) +var endsWithTabsAndSpacesRegexp = regexp.MustCompile("[ \t]+$") var endsWithFinalNewLineRegexp = regexp.MustCompile(`(\n|\r|\r\n)$`) var hasIndentationRegexp = regexp.MustCompile(`^[\t ]`) var hasNoIndentationRegexp = regexp.MustCompile(`^([^\t ]|$)`) + +// @todo - this doens't match "space tab space" or "tab space tab" var indentedWithMixedTabsAndSpacesRegexp = regexp.MustCompile(`^(\t+ +| +\t+)`) + var indentedWithTabsRegexp = regexp.MustCompile(`^\t+`) var indentedWithTabsThenCommentLineRegexp = regexp.MustCompile(`^\t+ \*`) var indentedWithSpacesRegexp = regexp.MustCompile(`^ +`)