Compare commits

...

10 Commits

Author SHA1 Message Date
Maksim Smirnov
c256eb2881 update to modern tooling 2025-08-23 08:45:26 +02:00
Amy Boyd
472aaeae1b Document how to use in a Git pre-commit hook. 2016-12-22 14:23:43 +00:00
Amy Boyd
167a87e60d Use exit code 1 when there's a style error. 2016-12-22 14:22:12 +00:00
Amy Boyd
6a2c768176 Add support for non-standard rule 'ignore'. 2016-12-22 14:22:12 +00:00
Amy Boyd
67e51c079a Add a fixer command. 2016-12-22 14:22:12 +00:00
Amy Boyd
948f51d2a7 Add line fixer: FixTraimTrailingWhitespaceRule. 2016-12-22 13:53:37 +00:00
Amy Boyd
bbbff6689b Add line fixers: FixTabIndentationToSpaces, FixTabIndentationToSpaces, FixTabIndentationToSpaces. 2016-07-04 17:26:15 +01:00
Amy Boyd
eb919067e6 Add utils MustGetFileAsString and GetNumberOfLeftSpaces. 2016-07-04 15:15:21 +01:00
Amy Boyd
0e16bc1255 Add fixer for trailing new-line rule. 2016-07-04 14:04:58 +01:00
Amy Boyd
3463ab7d0f Add fixer for end-of-line characters. 2016-07-04 13:54:44 +01:00
22 changed files with 530 additions and 27 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
/build/
/symfony/

43
CLAUDE.md Normal file
View 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/`.

View File

@@ -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
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env bash
go run main.go fix symfony

View File

@@ -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.

View File

@@ -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" {

View File

@@ -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")
}
}

View 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
}

View File

@@ -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

View 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
}

View File

@@ -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}
}
}

View 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
}

View 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")
}
}

View File

@@ -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}

View File

@@ -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")

View 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, "")
}

View 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()
}
}

View File

@@ -13,3 +13,6 @@ trim_trailing_whitespace = false
[**.go]
indent_style = tabs
[**.ignored]
ignore = true

View File

@@ -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, " "))
}

View File

@@ -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
View 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
View 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=