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/
|
/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
|
* 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.
|
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
|
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.
|
* Test on Windows.
|
||||||
|
|
||||||
* Implement fixers and a `fix` command.
|
* Add a fixer for the 'indent_style' rule.
|
||||||
|
|
||||||
* Optimize speed and memory usage.
|
* Optimize speed and memory usage.
|
||||||
|
|
||||||
@@ -8,6 +8,3 @@
|
|||||||
|
|
||||||
* The file pattern `{num1..num2}` is not fully implemented yet. See
|
* The file pattern `{num1..num2}` is not fully implemented yet. See
|
||||||
`ConvertWildcardPatternToGoRegexp` in `path_matcher.go`
|
`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")
|
delete(rules, "root")
|
||||||
|
|
||||||
if indentStyleValue, _ := rules["indent_style"]; indentStyleValue == "tab" {
|
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")
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/codegangsta/cli"
|
"github.com/codegangsta/cli"
|
||||||
"io/ioutil"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -20,23 +20,22 @@ func CheckCommand(c *cli.Context) error {
|
|||||||
|
|
||||||
configs := FindConfigFiles(files)
|
configs := FindConfigFiles(files)
|
||||||
|
|
||||||
|
hasError := false
|
||||||
|
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
rules := GetRulesToApplyToSourcePath(f, configs)
|
rules := GetRulesToApplyToSourcePath(f, configs)
|
||||||
if len(rules) == 0 {
|
if len(rules) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
fileContentInBytes, err := ioutil.ReadFile(f)
|
fileContent := MustGetFileAsString(f)
|
||||||
if err != nil {
|
|
||||||
ExitBecauseOfInternalError("Could not read file: " + f)
|
|
||||||
}
|
|
||||||
fileContent := string(fileContentInBytes)
|
|
||||||
|
|
||||||
// Run full-file checkers.
|
// Run full-file checkers.
|
||||||
for ruleName, ruleValue := range rules {
|
for ruleName, ruleValue := range rules {
|
||||||
if fullFileChecker, ok := fullFileCheckers[ruleName]; ok {
|
if fullFileChecker, ok := fullFileCheckers[ruleName]; ok {
|
||||||
result := fullFileChecker(ruleValue, fileContent)
|
result := fullFileChecker(ruleValue, fileContent)
|
||||||
if !result.isOk {
|
if !result.isOk {
|
||||||
|
hasError = true
|
||||||
fmt.Println(f + ": " + ruleName + ": " + result.messageIfNotOk)
|
fmt.Println(f + ": " + ruleName + ": " + result.messageIfNotOk)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,6 +50,7 @@ func CheckCommand(c *cli.Context) error {
|
|||||||
result := lineChecker(ruleValue, line)
|
result := lineChecker(ruleValue, line)
|
||||||
if !result.isOk {
|
if !result.isOk {
|
||||||
fmt.Println(f + ": line " + strconv.Itoa(lineNo) + ": " + ruleName + ": " + result.messageIfNotOk)
|
fmt.Println(f + ": line " + strconv.Itoa(lineNo) + ": " + ruleName + ": " + result.messageIfNotOk)
|
||||||
|
hasError = true
|
||||||
// Don't show more than 1 error per line.
|
// Don't show more than 1 error per line.
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -60,5 +60,9 @@ func CheckCommand(c *cli.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hasError {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ func CreateCliApp() *cli.App {
|
|||||||
Action: CheckCommand,
|
Action: CheckCommand,
|
||||||
ArgsUsage: "[PATH1] [PATH2...]",
|
ArgsUsage: "[PATH1] [PATH2...]",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "fix",
|
||||||
|
Usage: "Fix invalid files",
|
||||||
|
Action: FixCommand,
|
||||||
|
ArgsUsage: "[PATH1] [PATH2...]",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return app
|
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
|
type FullFileChecker func(ruleValue string, fileContent string) *FullFileCheckResult
|
||||||
|
|
||||||
// @todo - add fixers to each instance of FullFileCheckResult.
|
|
||||||
type FullFileCheckResult struct {
|
type FullFileCheckResult struct {
|
||||||
isOk bool
|
isOk bool
|
||||||
messageIfNotOk string
|
messageIfNotOk string
|
||||||
|
fixer FullFileFixer
|
||||||
}
|
}
|
||||||
|
|
||||||
func CheckEndOfLineRule(ruleValue string, fileContent string) *FullFileCheckResult {
|
func CheckEndOfLineRule(ruleValue string, fileContent string) *FullFileCheckResult {
|
||||||
@@ -29,29 +29,29 @@ func CheckEndOfLineRule(ruleValue string, fileContent string) *FullFileCheckResu
|
|||||||
|
|
||||||
if ruleValueLowercase == "lf" {
|
if ruleValueLowercase == "lf" {
|
||||||
if crlfRegexp.MatchString(fileContent) {
|
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) {
|
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 ruleValueLowercase == "cr" {
|
||||||
if crlfRegexp.MatchString(fileContent) {
|
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) {
|
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" {
|
if ruleValueLowercase == "crlf" {
|
||||||
fileContent := crlfRegexp.ReplaceAllString(fileContent, "")
|
fileContent := crlfRegexp.ReplaceAllString(fileContent, "")
|
||||||
if lfRegexp.MatchString(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) {
|
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) {
|
if endsWithFinalNewLineRegexp.MatchString(fileContent) {
|
||||||
return &FullFileCheckResult{isOk: true}
|
return &FullFileCheckResult{isOk: true}
|
||||||
} else {
|
} 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) {
|
if !endsWithFinalNewLineRegexp.MatchString(fileContent) {
|
||||||
return &FullFileCheckResult{isOk: true}
|
return &FullFileCheckResult{isOk: true}
|
||||||
} else {
|
} 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
|
type LineChecker func(ruleValue string, line string) *LineCheckResult
|
||||||
|
|
||||||
// @todo - add fixers to each instance of LineCheckResult.
|
|
||||||
type LineCheckResult struct {
|
type LineCheckResult struct {
|
||||||
isOk bool
|
isOk bool
|
||||||
messageIfNotOk string
|
messageIfNotOk string
|
||||||
|
fixer LineFixer
|
||||||
}
|
}
|
||||||
|
|
||||||
func HasIndentation(s string) bool {
|
func HasIndentation(s string) bool {
|
||||||
@@ -66,7 +66,7 @@ func CheckIndentStyleRule(ruleValue string, line string) *LineCheckResult {
|
|||||||
if IsIndentedWithTabs(line) {
|
if IsIndentedWithTabs(line) {
|
||||||
return &LineCheckResult{isOk: false, messageIfNotOk: "starts with tab instead of space"}
|
return &LineCheckResult{isOk: false, messageIfNotOk: "starts with tab instead of space"}
|
||||||
} else if IsIndentedWithMixedTabsAndSpaces(line) {
|
} 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 {
|
} else {
|
||||||
return &LineCheckResult{isOk: true}
|
return &LineCheckResult{isOk: true}
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,11 @@ func CheckIndentSizeRule(ruleValue string, line string) *LineCheckResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(line, "\t") {
|
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
|
// 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}
|
return &LineCheckResult{isOk: true}
|
||||||
}
|
}
|
||||||
if IsIndentedWithTabs(trimmedLine) {
|
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) {
|
if HasIndentation(trimmedLine) {
|
||||||
leftSpaces := len(line) - len(strings.TrimLeft(line, " "))
|
leftSpaces := GetNumberOfLeftSpaces(line)
|
||||||
return &LineCheckResult{isOk: false, messageIfNotOk: "starts with " + strconv.Itoa(leftSpaces) + " spaces which does not divide by " + ruleValue}
|
return &LineCheckResult{
|
||||||
|
isOk: false,
|
||||||
|
messageIfNotOk: "starts with " + strconv.Itoa(leftSpaces) + " spaces which does not divide by " + ruleValue,
|
||||||
|
fixer: FixUndividableIndentationToNearestSpacesAmount,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &LineCheckResult{isOk: true}
|
return &LineCheckResult{isOk: true}
|
||||||
@@ -129,7 +141,7 @@ func CheckTrimTrailingWhitespaceRule(ruleValue string, line string) *LineCheckRe
|
|||||||
|
|
||||||
trimmed := strings.TrimRight(line, " \t")
|
trimmed := strings.TrimRight(line, " \t")
|
||||||
if len(line) != len(trimmed) {
|
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}
|
return &LineCheckResult{isOk: true}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func TestCheckIndentStyleRule(t *testing.T) {
|
|||||||
ExpectPass(" line", "space", f, t)
|
ExpectPass(" line", "space", f, t)
|
||||||
ExpectPass(" ", "space", f, t)
|
ExpectPass(" ", "space", f, t)
|
||||||
ExpectPass(" line", "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("\tline", "space", f, t, "starts with tab instead of space")
|
||||||
ExpectFail("\t line", "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]
|
[**.go]
|
||||||
indent_style = tabs
|
indent_style = tabs
|
||||||
|
|
||||||
|
[**.ignored]
|
||||||
|
ignore = true
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ package editorconfig
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var filePathSeparatorRegex = regexp.QuoteMeta(string(filepath.Separator))
|
var filePathSeparatorRegex = regexp.QuoteMeta(string(filepath.Separator))
|
||||||
@@ -15,11 +17,15 @@ var lfRegexp = regexp.MustCompile(`\n`)
|
|||||||
var crRegexp = regexp.MustCompile(`\r`)
|
var crRegexp = regexp.MustCompile(`\r`)
|
||||||
var crlfRegexp = regexp.MustCompile(`\r\n`)
|
var crlfRegexp = regexp.MustCompile(`\r\n`)
|
||||||
|
|
||||||
|
var endsWithTabsAndSpacesRegexp = regexp.MustCompile("[ \t]+$")
|
||||||
var endsWithFinalNewLineRegexp = regexp.MustCompile(`(\n|\r|\r\n)$`)
|
var endsWithFinalNewLineRegexp = regexp.MustCompile(`(\n|\r|\r\n)$`)
|
||||||
|
|
||||||
var hasIndentationRegexp = regexp.MustCompile(`^[\t ]`)
|
var hasIndentationRegexp = regexp.MustCompile(`^[\t ]`)
|
||||||
var hasNoIndentationRegexp = 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 indentedWithMixedTabsAndSpacesRegexp = regexp.MustCompile(`^(\t+ +| +\t+)`)
|
||||||
|
|
||||||
var indentedWithTabsRegexp = regexp.MustCompile(`^\t+`)
|
var indentedWithTabsRegexp = regexp.MustCompile(`^\t+`)
|
||||||
var indentedWithTabsThenCommentLineRegexp = regexp.MustCompile(`^\t+ \*`)
|
var indentedWithTabsThenCommentLineRegexp = regexp.MustCompile(`^\t+ \*`)
|
||||||
var indentedWithSpacesRegexp = regexp.MustCompile(`^ +`)
|
var indentedWithSpacesRegexp = regexp.MustCompile(`^ +`)
|
||||||
@@ -47,3 +53,23 @@ func ExitBecauseOfInternalError(err string) {
|
|||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(2)
|
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 (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -25,3 +26,18 @@ func TestSplitIntoLines(t *testing.T) {
|
|||||||
t.Error("Did not split string into lines correctly, got lines: " + strings.Join(result, ", "))
|
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