Compare commits
10 Commits
03eddbde0c
...
c256eb2881
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c256eb2881 | ||
|
|
472aaeae1b | ||
|
|
167a87e60d | ||
|
|
6a2c768176 | ||
|
|
67e51c079a | ||
|
|
948f51d2a7 | ||
|
|
bbbff6689b | ||
|
|
eb919067e6 | ||
|
|
0e16bc1255 | ||
|
|
3463ab7d0f |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
/build/
|
||||
/symfony/
|
||||
|
||||
43
CLAUDE.md
Normal file
43
CLAUDE.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a Go CLI tool for validating and fixing files according to .editorconfig rules. The tool provides four main commands: `check`, `fix`, `ls`, and `rules`.
|
||||
|
||||
## Development Commands
|
||||
|
||||
- **Build**: `go build -o editorconfig-cli .` (creates single binary)
|
||||
- **Run tests**: `go test ./editorconfig`
|
||||
- **Cross-platform build**: Use `bin/build` script (requires fixing Go path)
|
||||
- **Test the CLI**:
|
||||
- `./editorconfig-cli check [paths]` - validate files
|
||||
- `./editorconfig-cli fix [paths]` - fix files
|
||||
- `./editorconfig-cli ls [paths]` - list matched files
|
||||
- `./editorconfig-cli rules [paths]` - show rules for files
|
||||
|
||||
## Modern Setup
|
||||
|
||||
This project has been modernized with Go modules. The original `bin/build` script uses a hardcoded Go path (`/usr/local/go/bin/go`) that may not exist on modern systems. Use `go build` directly instead.
|
||||
|
||||
## Architecture
|
||||
|
||||
The main application entry point is in `main.go`, which delegates to the CLI app created in `editorconfig/cli.go`. The core functionality is organized into:
|
||||
|
||||
- **Command handlers**: `*_command.go` files implement the four main CLI commands
|
||||
- **Rule processing**: `line_checkers.go`, `line_fixers.go`, `full_file_checkers.go`, `full_file_fixers.go` contain the validation and fixing logic
|
||||
- **File discovery**: `source_file_finder.go` handles finding files to process
|
||||
- **Configuration**: `config_file_finder.go` and `config_file.go` handle .editorconfig file parsing
|
||||
- **Path matching**: `path_matcher.go` implements glob pattern matching for .editorconfig sections
|
||||
|
||||
## Key Implementation Details
|
||||
|
||||
- The project uses the `github.com/codegangsta/cli` library for command-line interface
|
||||
- Rules are applied based on .editorconfig files found in the directory hierarchy
|
||||
- The tool supports standard .editorconfig properties: `indent_style`, `indent_size`, `tab_width`, `end_of_line`, `charset`, `trim_trailing_whitespace`, `insert_final_newline`
|
||||
- File pattern matching supports most glob patterns, with some limitations documented in `path_matcher.go`
|
||||
|
||||
## Testing
|
||||
|
||||
All Go packages have corresponding `*_test.go` files. The test suite includes both unit tests and integration tests using sample files in `editorconfig/tests/`.
|
||||
12
README.md
12
README.md
@@ -37,6 +37,18 @@ needed!
|
||||
* It's super fast. The `check` command finishes small codebases in well under 1 second, and a 250k
|
||||
line codebase is checked in under 3 seconds.
|
||||
|
||||
Use in a Git pre-commit hook
|
||||
----------------------------
|
||||
|
||||
```
|
||||
editorconfig-cli check src/ tests/
|
||||
if [[ $? != '0' ]]; then
|
||||
echo 'Code is not aligned with .editorconfig'
|
||||
echo 'Review the output and commit your fixes'
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
How to contribute
|
||||
-----------------
|
||||
|
||||
|
||||
2
bin/run-fix
Executable file
2
bin/run-fix
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env bash
|
||||
go run main.go fix symfony
|
||||
@@ -1,6 +1,6 @@
|
||||
* Test on Windows.
|
||||
|
||||
* Implement fixers and a `fix` command.
|
||||
* Add a fixer for the 'indent_style' rule.
|
||||
|
||||
* Optimize speed and memory usage.
|
||||
|
||||
@@ -8,6 +8,3 @@
|
||||
|
||||
* The file pattern `{num1..num2}` is not fully implemented yet. See
|
||||
`ConvertWildcardPatternToGoRegexp` in `path_matcher.go`
|
||||
|
||||
* Add an example of how to use the `check` command in a continuous integration build or in a Git
|
||||
pre-commit hook.
|
||||
|
||||
@@ -26,6 +26,10 @@ func GetRulesToApplyToSourcePath(sourcePath string, cfs []ConfigFile) map[string
|
||||
}
|
||||
}
|
||||
|
||||
if isIgnored, _ := rules["ignore"]; isIgnored == "true" {
|
||||
return make(map[string]string)
|
||||
}
|
||||
|
||||
delete(rules, "root")
|
||||
|
||||
if indentStyleValue, _ := rules["indent_style"]; indentStyleValue == "tab" {
|
||||
|
||||
@@ -32,3 +32,16 @@ func TestGetRulesToApplyToSourcePathWhenNoRulesShouldApply(t *testing.T) {
|
||||
t.Error("No rules should be applied for the file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRulesToApplyToSourcePathWhenPathShouldBeIgnored(t *testing.T) {
|
||||
result := GetRulesToApplyToSourcePath(
|
||||
"some-file-to-ignore.ignored",
|
||||
[]ConfigFile{
|
||||
CreateConfigFileStruct("tests/.editorconfig"),
|
||||
},
|
||||
)
|
||||
|
||||
if len(result) != 0 {
|
||||
t.Error("No rules should be applied for the file")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package editorconfig
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/codegangsta/cli"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@@ -20,23 +20,22 @@ func CheckCommand(c *cli.Context) error {
|
||||
|
||||
configs := FindConfigFiles(files)
|
||||
|
||||
hasError := false
|
||||
|
||||
for _, f := range files {
|
||||
rules := GetRulesToApplyToSourcePath(f, configs)
|
||||
if len(rules) == 0 {
|
||||
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 {
|
||||
if fullFileChecker, ok := fullFileCheckers[ruleName]; ok {
|
||||
result := fullFileChecker(ruleValue, fileContent)
|
||||
if !result.isOk {
|
||||
hasError = true
|
||||
fmt.Println(f + ": " + ruleName + ": " + result.messageIfNotOk)
|
||||
}
|
||||
}
|
||||
@@ -51,6 +50,7 @@ func CheckCommand(c *cli.Context) error {
|
||||
result := lineChecker(ruleValue, line)
|
||||
if !result.isOk {
|
||||
fmt.Println(f + ": line " + strconv.Itoa(lineNo) + ": " + ruleName + ": " + result.messageIfNotOk)
|
||||
hasError = true
|
||||
// Don't show more than 1 error per line.
|
||||
break
|
||||
}
|
||||
@@ -60,5 +60,9 @@ func CheckCommand(c *cli.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
if hasError {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
78
editorconfig/fix_command.go
Normal file
78
editorconfig/fix_command.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
52
editorconfig/full_file_fixers.go
Normal file
52
editorconfig/full_file_fixers.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package editorconfig
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type FullFileFixer func(ruleValue string, fileContent string) string
|
||||
|
||||
func FixEndOfLineRule(ruleValue string, fileContent string) string {
|
||||
ruleValueLowercase := strings.ToLower(ruleValue)
|
||||
|
||||
if ruleValueLowercase == "lf" {
|
||||
fileContent = crlfRegexp.ReplaceAllString(fileContent, "\n")
|
||||
fileContent = crRegexp.ReplaceAllString(fileContent, "\n")
|
||||
return fileContent
|
||||
}
|
||||
|
||||
if ruleValueLowercase == "cr" {
|
||||
fileContent = crlfRegexp.ReplaceAllString(fileContent, "\r")
|
||||
fileContent = lfRegexp.ReplaceAllString(fileContent, "\r")
|
||||
return fileContent
|
||||
}
|
||||
|
||||
if ruleValueLowercase == "crlf" {
|
||||
fileContent = regexp.MustCompile("(\r\n|\r|\n)").ReplaceAllString(fileContent, "\r\n")
|
||||
return fileContent
|
||||
}
|
||||
|
||||
return fileContent
|
||||
}
|
||||
|
||||
/**
|
||||
* This must be called before FixEndOfLineRule so the \n added will be converted to whatever the
|
||||
* 'end_of_line' rule dictates.
|
||||
*/
|
||||
func FixInsertFinalNewLineRule(ruleValue string, fileContent string) string {
|
||||
ruleValueLowercase := strings.ToLower(ruleValue)
|
||||
|
||||
if ruleValueLowercase == "true" && !endsWithFinalNewLineRegexp.MatchString(fileContent) {
|
||||
return fileContent + "\n"
|
||||
}
|
||||
|
||||
if ruleValueLowercase == "false" {
|
||||
for endsWithFinalNewLineRegexp.MatchString(fileContent) {
|
||||
fileContent = endsWithFinalNewLineRegexp.ReplaceAllString(fileContent, "")
|
||||
}
|
||||
return fileContent
|
||||
}
|
||||
|
||||
return fileContent
|
||||
}
|
||||
56
editorconfig/full_file_fixers_test.go
Normal file
56
editorconfig/full_file_fixers_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package editorconfig
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFixEndOfLineRule(t *testing.T) {
|
||||
input := "\nline\nline 2\rline 3 \n \r\n\r"
|
||||
|
||||
toLfResult := FixEndOfLineRule("lF", input)
|
||||
if toLfResult != "\nline\nline 2\nline 3 \n \n\n" {
|
||||
t.Error("Converting to LF did not work, got: " + GetErrorWithLineBreaksVisible(toLfResult))
|
||||
}
|
||||
|
||||
toCrResult := FixEndOfLineRule("Cr", input)
|
||||
if toCrResult != "\rline\rline 2\rline 3 \r \r\r" {
|
||||
t.Error("Converting to CR did not work, got: " + GetErrorWithLineBreaksVisible(toCrResult))
|
||||
}
|
||||
|
||||
toCrlfResult := FixEndOfLineRule("CrlF", input)
|
||||
if toCrlfResult != "\r\nline\r\nline 2\r\nline 3 \r\n \r\n\r\n" {
|
||||
t.Error("Converting to CRLR did not work, got: " + GetErrorWithLineBreaksVisible(toCrlfResult))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixInsertFinalNewLineRule(t *testing.T) {
|
||||
input1 := "a\nb\nc\n"
|
||||
result1 := FixInsertFinalNewLineRule("true", input1)
|
||||
if result1 != input1 {
|
||||
t.Error("String was changed despite already having a line at the end")
|
||||
}
|
||||
|
||||
input2 := "a\rb\rc\r\r"
|
||||
result2 := FixInsertFinalNewLineRule("true", input2)
|
||||
if result2 != input2 {
|
||||
t.Error("String was changed despite already having a line at the end")
|
||||
}
|
||||
|
||||
input3 := "a\r\nb\r\nc\r\n"
|
||||
result3 := FixInsertFinalNewLineRule("true", input3)
|
||||
if result3 != input3 {
|
||||
t.Error("String was changed despite already having a line at the end")
|
||||
}
|
||||
|
||||
input4 := "a\nb"
|
||||
result4 := FixInsertFinalNewLineRule("true", input4)
|
||||
if result4 != "a\nb\n" {
|
||||
t.Error("Line was not added at the end")
|
||||
}
|
||||
|
||||
input5 := "a\r\nb\r\nc\r\n\n\n\r"
|
||||
result5 := FixInsertFinalNewLineRule("false", input5)
|
||||
if result5 != "a\r\nb\r\nc" {
|
||||
t.Error("Trailing lines were not removed")
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
63
editorconfig/line_fixers.go
Normal file
63
editorconfig/line_fixers.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package editorconfig
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type LineFixer func(ruleValue string, line string) string
|
||||
|
||||
func FixTabIndentationToSpaces(ruleValueNumberOfSpaces string, line string) string {
|
||||
numberOfSpaces, _ := strconv.Atoi(ruleValueNumberOfSpaces)
|
||||
|
||||
line = indentedWithTabsRegexp.ReplaceAllStringFunc(line, func(tabs string) string {
|
||||
return strings.Repeat(" ", len(tabs)*numberOfSpaces)
|
||||
})
|
||||
|
||||
return line
|
||||
}
|
||||
|
||||
func FixMixedIndentationToSpaces(ruleValueNumberOfSpaces string, line string) string {
|
||||
numberOfSpaces, _ := strconv.Atoi(ruleValueNumberOfSpaces)
|
||||
|
||||
for indentedWithMixedTabsAndSpacesRegexp.MatchString(line) {
|
||||
line = indentedWithMixedTabsAndSpacesRegexp.ReplaceAllStringFunc(line, func(tabsAndSpaces string) string {
|
||||
tabs := strings.Replace(tabsAndSpaces, " ", "", -1)
|
||||
spaces := strings.Replace(tabsAndSpaces, "\t", "", -1)
|
||||
|
||||
return strings.Repeat(" ", (len(tabs)*numberOfSpaces)+len(spaces))
|
||||
})
|
||||
}
|
||||
|
||||
return line
|
||||
}
|
||||
|
||||
func FixUndividableIndentationToNearestSpacesAmount(ruleValueNumberOfSpaces string, line string) string {
|
||||
numberOfSpaces, _ := strconv.Atoi(ruleValueNumberOfSpaces)
|
||||
if numberOfSpaces < 1 {
|
||||
ExitBecauseOfInternalError("Number of spaces must be integer greater than 0, is: " + ruleValueNumberOfSpaces)
|
||||
}
|
||||
|
||||
if GetNumberOfLeftSpaces(line) == 0 {
|
||||
return line
|
||||
}
|
||||
|
||||
for true {
|
||||
leftSpaces := GetNumberOfLeftSpaces(line)
|
||||
if leftSpaces%numberOfSpaces != 0 {
|
||||
line = " " + line
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return line
|
||||
}
|
||||
|
||||
func FixTrimTrailingWhitespaceRule(ruleValue string, line string) string {
|
||||
if strings.ToLower(ruleValue) != "true" {
|
||||
return line
|
||||
}
|
||||
|
||||
return endsWithTabsAndSpacesRegexp.ReplaceAllString(line, "")
|
||||
}
|
||||
90
editorconfig/line_fixers_test.go
Normal file
90
editorconfig/line_fixers_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package editorconfig
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFixTabIndentationToSpaces(t *testing.T) {
|
||||
var result string
|
||||
|
||||
result = FixTabIndentationToSpaces("4", "\t\thello world")
|
||||
if result != " hello world" {
|
||||
t.Error("Unexpected result: " + result)
|
||||
}
|
||||
|
||||
result = FixTabIndentationToSpaces("3", "\thello world")
|
||||
if result != " hello world" {
|
||||
t.Error("Unexpected result: " + result)
|
||||
}
|
||||
|
||||
result = FixTabIndentationToSpaces("2", "\t\t\thello world")
|
||||
if result != " hello world" {
|
||||
t.Error("Unexpected result: " + result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixMixedIndentationToSpaces(t *testing.T) {
|
||||
var result string
|
||||
|
||||
result = FixMixedIndentationToSpaces("2", "\t \t hello worl d")
|
||||
if result != " hello worl d" {
|
||||
t.Error("Unexpected result: " + result)
|
||||
}
|
||||
|
||||
result = FixMixedIndentationToSpaces("3", " \thello world !")
|
||||
if result != " hello world !" {
|
||||
t.Error("Unexpected result: " + result)
|
||||
}
|
||||
|
||||
result = FixMixedIndentationToSpaces("2", " \t hello world !")
|
||||
if result != " hello world !" {
|
||||
t.Error("Unexpected result: " + result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixUndividableIndentationToNearestSpacesAmount(t *testing.T) {
|
||||
var result string
|
||||
|
||||
result = FixUndividableIndentationToNearestSpacesAmount("2", "hello")
|
||||
if result != "hello" {
|
||||
t.Error("String changed but it was already fine. Changed to: " + result)
|
||||
}
|
||||
|
||||
result = FixUndividableIndentationToNearestSpacesAmount("2", " hello")
|
||||
if result != " hello" {
|
||||
t.Error("String changed but it was already fine. Changed to: " + result)
|
||||
}
|
||||
|
||||
result = FixUndividableIndentationToNearestSpacesAmount("1", " hello")
|
||||
if result != " hello" {
|
||||
t.Error("String changed but it was already fine. Changed to: " + result)
|
||||
}
|
||||
|
||||
result = FixUndividableIndentationToNearestSpacesAmount("3", " hello")
|
||||
if result != " hello" {
|
||||
t.Error("Unexpected result: " + result)
|
||||
}
|
||||
|
||||
result = FixUndividableIndentationToNearestSpacesAmount("5", " hello")
|
||||
if result != " hello" {
|
||||
t.Error("Unexpected result: " + result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixTrimTrailingWhitespaceRule(t *testing.T) {
|
||||
if FixTrimTrailingWhitespaceRule("true", "") != "" {
|
||||
t.Error()
|
||||
}
|
||||
|
||||
if FixTrimTrailingWhitespaceRule("true", " a b c") != " a b c" {
|
||||
t.Error()
|
||||
}
|
||||
|
||||
if FixTrimTrailingWhitespaceRule("true", "abc \t\t \t \t \t ") != "abc" {
|
||||
t.Error()
|
||||
}
|
||||
|
||||
if FixTrimTrailingWhitespaceRule("false", "abc \t\t \t \t \t ") != "abc \t\t \t \t \t " {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
@@ -13,3 +13,6 @@ trim_trailing_whitespace = false
|
||||
|
||||
[**.go]
|
||||
indent_style = tabs
|
||||
|
||||
[**.ignored]
|
||||
ignore = true
|
||||
|
||||
@@ -2,9 +2,11 @@ package editorconfig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var filePathSeparatorRegex = regexp.QuoteMeta(string(filepath.Separator))
|
||||
@@ -15,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(`^ +`)
|
||||
@@ -47,3 +53,23 @@ func ExitBecauseOfInternalError(err string) {
|
||||
fmt.Println(err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
func GetErrorWithLineBreaksVisible(s string) string {
|
||||
s = lfRegexp.ReplaceAllString(s, `\n`)
|
||||
s = crRegexp.ReplaceAllString(s, `\r`)
|
||||
s = crlfRegexp.ReplaceAllString(s, `\r\n`)
|
||||
return s
|
||||
}
|
||||
|
||||
func MustGetFileAsString(path string) string {
|
||||
inBytes, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
ExitBecauseOfInternalError("Could not read file: " + path)
|
||||
}
|
||||
|
||||
return string(inBytes)
|
||||
}
|
||||
|
||||
func GetNumberOfLeftSpaces(s string) int {
|
||||
return len(s) - len(strings.TrimLeft(s, " "))
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package editorconfig
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -25,3 +26,18 @@ func TestSplitIntoLines(t *testing.T) {
|
||||
t.Error("Did not split string into lines correctly, got lines: " + strings.Join(result, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustGetFileAsString(t *testing.T) {
|
||||
license := MustGetFileAsString("../LICENSE")
|
||||
if !strings.Contains(license, "MIT License") || !strings.Contains(license, "THE SOFTWARE IS PROVIDED \"AS IS\"") {
|
||||
t.Error("Could not read file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNumberOfLeftSpaces(t *testing.T) {
|
||||
for i := 0; i < 20; i++ {
|
||||
if GetNumberOfLeftSpaces(strings.Repeat(" ", i)) != i {
|
||||
t.Error("Wrong number of spaces returned when string starts with " + strconv.Itoa(i) + " spaces")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
go.mod
Normal file
11
go.mod
Normal file
@@ -0,0 +1,11 @@
|
||||
module github.com/amyboyd/editorconfig-cli
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/codegangsta/cli v1.20.0
|
||||
github.com/go-ini/ini v1.67.0
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d
|
||||
)
|
||||
|
||||
require github.com/stretchr/testify v1.10.0 // indirect
|
||||
14
go.sum
Normal file
14
go.sum
Normal file
@@ -0,0 +1,14 @@
|
||||
github.com/codegangsta/cli v1.20.0 h1:iX1FXEgwzd5+XN6wk5cVHOGQj6Q3Dcp20lUeS4lHNTw=
|
||||
github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
Reference in New Issue
Block a user