diff --git a/editorconfig/line_checkers.go b/editorconfig/line_checkers.go new file mode 100644 index 0000000..b14848c --- /dev/null +++ b/editorconfig/line_checkers.go @@ -0,0 +1,144 @@ +package editorconfig + +import ( + "regexp" + "strconv" + "strings" +) + +// tab_width, charset, end_of_line, insert_final_newline and root do not have any affect on our line +// checkers (they apply to the full files, not lines). + +var lineCheckers = map[string]LineChecker{ + "indent_style": CheckIndentStyleRule, + "indent_size": CheckIndentSizeRule, + "trim_trailing_whitespace": CheckTrimTrailingWhitespaceRule, +} + +type LineChecker func(ruleValue string, line string) *LineCheckResult + +// @todo - add fixers to each instance of LineCheckResult. +type LineCheckResult struct { + isOk bool + messageIfNotOk string +} + +var hasIndentationRegexp = regexp.MustCompile(`^[\t ]`) +var hasNoIndentationRegexp = regexp.MustCompile(`^([^\t ]|$)`) +var indentedWithMixedTabsAndSpacesRegexp = regexp.MustCompile(`^(\t+ +| +\t+)`) +var indentedWithTabsRegexp = regexp.MustCompile(`^\t+`) +var indentedWithTabsThenCommentLineRegexp = regexp.MustCompile(`^\t+ \*`) +var indentedWithSpacesRegexp = regexp.MustCompile(`^ +`) + +func HasIndentation(s string) bool { + return hasIndentationRegexp.MatchString(s) +} + +func HasNoIndentation(s string) bool { + return hasNoIndentationRegexp.MatchString(s) +} + +func IsIndentedWithMixedTabsAndSpaces(s string) bool { + return indentedWithMixedTabsAndSpacesRegexp.MatchString(s) +} + +func IsIndentedWithTabs(s string) bool { + return indentedWithTabsRegexp.MatchString(s) +} + +// This allows comments like /**\n\t *\n\t */ +func IsIndentedWithTabsThenCommentLine(s string) bool { + return indentedWithTabsThenCommentLineRegexp.MatchString(s) +} + +func IsIndentedWithSpaces(s string) bool { + return indentedWithSpacesRegexp.MatchString(s) +} + +func CheckIndentStyleRule(ruleValue string, line string) *LineCheckResult { + if HasNoIndentation(line) { + return &LineCheckResult{isOk: true} + } + + if strings.ToLower(ruleValue) == "tab" { + if IsIndentedWithSpaces(line) { + return &LineCheckResult{isOk: false, messageIfNotOk: "starts with space instead of tab"} + } else if IsIndentedWithMixedTabsAndSpaces(line) && !IsIndentedWithTabsThenCommentLine(line) { + return &LineCheckResult{isOk: false, messageIfNotOk: "indented with mix of tabs and spaces instead of just tabs"} + } else { + return &LineCheckResult{isOk: true} + } + } + + if strings.ToLower(ruleValue) == "space" { + 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"} + } else { + return &LineCheckResult{isOk: true} + } + } + + return &LineCheckResult{isOk: false, messageIfNotOk: "invalid value for indent_style: " + ruleValue} +} + +func CheckIndentSizeRule(ruleValue string, line string) *LineCheckResult { + if ruleValue == "tab" { + return &LineCheckResult{isOk: true} + } + + if HasNoIndentation(line) { + return &LineCheckResult{isOk: true} + } + + ruleValueInt, err := strconv.Atoi(ruleValue) + if err != nil { + return &LineCheckResult{isOk: false, messageIfNotOk: "value is not an integer: " + ruleValue} + } + + if ruleValueInt < 1 { + return &LineCheckResult{isOk: false, messageIfNotOk: "number of spaces must be 1 or more, is: " + ruleValue} + } + + if strings.HasPrefix(line, "\t") { + return &LineCheckResult{isOk: false, messageIfNotOk: "should be indented with spaces but is indented with tabs"} + } + + // Indented with spaces. Ensure the number of spaces is divisible by the rule value, but also + // allow an extra space followed by * to allow for comments like /**\n *\n */ (note the + // extra space before the * on the 2nd and 3rd lines). + trimmedLine := line + for strings.HasPrefix(trimmedLine, strings.Repeat(" ", ruleValueInt)) { + trimmedLine = (trimmedLine)[ruleValueInt:] + } + if strings.HasPrefix(trimmedLine, " *") { + return &LineCheckResult{isOk: true} + } + if IsIndentedWithTabs(trimmedLine) { + return &LineCheckResult{isOk: false, messageIfNotOk: "indented with mix of spaces and tabs instead of just spaces"} + } + 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} + } + + return &LineCheckResult{isOk: true} +} + +func CheckTrimTrailingWhitespaceRule(ruleValue string, line string) *LineCheckResult { + if strings.ToLower(ruleValue) == "false" { + return &LineCheckResult{isOk: true} + } + + if strings.ToLower(ruleValue) != "true" { + return &LineCheckResult{isOk: false, messageIfNotOk: "value must be true or false, but is: " + ruleValue} + } + + trimmed := strings.TrimRight(line, " \t") + if len(line) != len(trimmed) { + return &LineCheckResult{isOk: false, messageIfNotOk: "line has trailing whitespace"} + } + + return &LineCheckResult{isOk: true} +} diff --git a/editorconfig/line_checkers_test.go b/editorconfig/line_checkers_test.go new file mode 100644 index 0000000..e08c607 --- /dev/null +++ b/editorconfig/line_checkers_test.go @@ -0,0 +1,105 @@ +package editorconfig + +import ( + "testing" +) + +func ExpectPass(line string, ruleValue string, lineChecker LineChecker, t *testing.T) { + result := lineChecker(ruleValue, line) + if !result.isOk { + t.Error("Expected line to pass, but it failed: \"" + line + "\" for rule value \"" + ruleValue + "\", had error message: " + result.messageIfNotOk) + } +} + +func ExpectFail(line string, ruleValue string, lineChecker LineChecker, t *testing.T, expectedError string) { + result := lineChecker(ruleValue, line) + if result.isOk { + t.Error("Expected line to fail, but it passed: \"" + line + "\" for rule value \"" + ruleValue + "\"") + return + } + + if !result.isOk && result.messageIfNotOk != expectedError { + t.Error("Line \"" + line + "\" failed with error message \"" + result.messageIfNotOk + "\" but had expected \"" + expectedError + "\"") + return + } +} + +func TestCheckIndentStyleRule(t *testing.T) { + f := CheckIndentStyleRule + + ExpectPass("", "space", f, t) + ExpectPass("line", "space", f, 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, "starts with tab instead of space") + ExpectFail("\t line", "space", f, t, "starts with tab instead of space") + + ExpectPass("", "tab", f, t) + ExpectPass("line", "tab", f, t) + ExpectPass("\n", "tab", f, t) + ExpectPass("\tline", "tab", f, t) + ExpectPass("\t\tline", "tab", f, t) + ExpectFail(" \tline", "tab", f, t, "starts with space instead of tab") + ExpectFail("\t line", "tab", f, t, "indented with mix of tabs and spaces instead of just tabs") + ExpectFail("\t ", "tab", f, t, "indented with mix of tabs and spaces instead of just tabs") + + // Allow comments like /**\n\t *\n\t */ + ExpectPass("\t *line", "tab", f, t) + + ExpectFail(" line", "dinosaurs", f, t, "invalid value for indent_style: dinosaurs") +} + +func TestCheckIndentSizeRule(t *testing.T) { + f := CheckIndentSizeRule + + // 'indent_size=tab' can never fail this rule. + ExpectPass("line", "tab", f, t) + ExpectPass(" line", "tab", f, t) + ExpectPass(" line", "tab", f, t) + + ExpectPass("", "2", f, t) + ExpectPass("line", "2", f, t) + ExpectPass(" line", "2", f, t) + ExpectPass(" line", "2", f, t) + ExpectPass(" line", "2", f, t) + ExpectFail(" line", "2", f, t, "starts with 1 spaces which does not divide by 2") + ExpectFail(" line", "2", f, t, "starts with 3 spaces which does not divide by 2") + ExpectFail(" line", "2", f, t, "starts with 5 spaces which does not divide by 2") + ExpectFail("\tline", "2", f, t, "should be indented with spaces but is indented with tabs") + ExpectFail("\t\tline", "2", f, t, "should be indented with spaces but is indented with tabs") + ExpectFail(" \tline", "2", f, t, "indented with mix of spaces and tabs instead of just spaces") + + // Allow comments like /**\n *\n */ (note the extra space before the * on the 2nd and 3rd lines) + ExpectPass(" * line", "2", f, t) + ExpectFail(" ^ line", "2", f, t, "starts with 3 spaces which does not divide by 2") + + ExpectPass("", "3", f, t) + ExpectPass("line", "3", f, t) + ExpectPass(" line", "3", f, t) + ExpectFail(" line", "3", f, t, "starts with 5 spaces which does not divide by 3") + + ExpectFail(" anything", "0", f, t, "number of spaces must be 1 or more, is: 0") + ExpectFail(" anything", "-1", f, t, "number of spaces must be 1 or more, is: -1") + ExpectFail(" anything", "asd", f, t, "value is not an integer: asd") +} + +func TestCheckTrimTrailingWhitespaceRule(t *testing.T) { + f := CheckTrimTrailingWhitespaceRule + + // 'trim_trailing_whitespace=false' can never fail this rule. + ExpectPass("line", "false", f, t) + ExpectPass("line ", "false", f, t) + ExpectPass("line\t", "false", f, t) + ExpectPass("line \t \t \t", "false", f, t) + + ExpectPass("line", "true", f, t) + ExpectPass("line line", "true", f, t) + ExpectPass("line line", "true", f, t) + + ExpectFail("line line ", "true", f, t, "line has trailing whitespace") + ExpectFail("line line\t", "true", f, t, "line has trailing whitespace") + ExpectFail("line line ", "true", f, t, "line has trailing whitespace") + ExpectFail("line line \t", "true", f, t, "line has trailing whitespace") +}