diff --git a/.golangci.yml b/.golangci.yml index 29c1e41..17744df 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -57,6 +57,7 @@ issues: exclude-rules: - path: _test\.go linters: + - dupl - funlen - path: cmd/godot/main\.go linters: diff --git a/README.md b/README.md index e2d2f6b..a47b163 100644 --- a/README.md +++ b/README.md @@ -13,23 +13,32 @@ end of the last sentence if needed. > Comments should begin with the name of the thing being described > and end in a period -## Install and run +## Install *NOTE: Godot is available as a part of [GolangCI Lint](https://github.com/golangci/golangci-lint) (disabled by default).* Build from source + ```sh go get -u github.com/tetafro/godot/cmd/godot ``` or download binary from [releases page](https://github.com/tetafro/godot/releases). -Run +## Run + ```sh godot ./myproject ``` +Autofix flags are also available + +```sh +godot -f ./myproject # fix issues and print the result +godot -w ./myproject # fix issues and replace the original file +``` + ## Examples Code @@ -50,5 +59,5 @@ Top level comment should end in a period: math/math.go:3:1 ``` See more examples in test files: -- [for default mode](testdata/example_default.go) -- [for using --all flag](testdata/example_checkall.go) +- [for default mode](testdata/default/check/main.go) +- [for using --all flag](testdata/checkall/check/main.go) diff --git a/cmd/godot/main.go b/cmd/godot/main.go index 36c75cf..d208da0 100644 --- a/cmd/godot/main.go +++ b/cmd/godot/main.go @@ -19,13 +19,17 @@ const usage = `Usage: godot [OPTION] [FILES] Options: -a, --all check all top-level comments (not only declarations) + -f, --fix fix issues, and print fixed version to stdout -h, --help show this message - -v, --version show version` + -v, --version show version + -w, --write fix issues, and write result to original file` type arguments struct { help bool version bool all bool + fix bool + write bool files []string } @@ -69,9 +73,22 @@ func main() { } for i := range files { - issues := godot.Run(files[i], fset, settings) - for _, iss := range issues { - fmt.Printf("%s: %s\n", iss.Message, iss.Pos) + switch { + case args.fix: + fixed, err := godot.Fix(args.files[i], files[i], fset, settings) + if err != nil { + fatalf("Failed to autofix file '%s': %v", args.files[i], err) + } + fmt.Print(string(fixed)) + case args.write: + if err := godot.Replace(args.files[i], files[i], fset, settings); err != nil { + fatalf("Failed to rewrite file '%s': %v", args.files[i], err) + } + default: + issues := godot.Run(files[i], fset, settings) + for _, iss := range issues { + fmt.Printf("%s: %s\n", iss.Message, iss.Pos) + } } } } @@ -94,6 +111,10 @@ func readArgs() (args arguments, err error) { args.version = true case "-a", "--all": args.all = true + case "-f", "--fix": + args.fix = true + case "-w", "--write": + args.write = true default: return arguments{}, fmt.Errorf("unknown flag '%s'", arg) } diff --git a/godot.go b/godot.go index f867c57..ef74ead 100644 --- a/godot.go +++ b/godot.go @@ -3,8 +3,11 @@ package godot import ( + "fmt" "go/ast" "go/token" + "io/ioutil" + "os" "regexp" "strings" ) @@ -74,6 +77,58 @@ func Run(file *ast.File, fset *token.FileSet, settings Settings) []Issue { return issues } +// Fix fixes all issues and return new version of file content. +func Fix(path string, file *ast.File, fset *token.FileSet, settings Settings) ([]byte, error) { + // Read file + content, err := ioutil.ReadFile(path) // nolint: gosec + if err != nil { + return nil, fmt.Errorf("read file: %v", err) + } + if len(content) == 0 { + return nil, nil + } + + issues := Run(file, fset, settings) + + // slice -> map + m := map[int]Issue{} + for _, iss := range issues { + m[iss.Pos.Line] = iss + } + + // Replace lines from issues + fixed := make([]byte, 0, len(content)) + for i, line := range strings.Split(string(content), "\n") { + newline := line + if iss, ok := m[i+1]; ok { + newline = iss.Replacement + } + fixed = append(fixed, []byte(newline+"\n")...) + } + fixed = fixed[:len(fixed)-1] // trim last "\n" + + return fixed, nil +} + +// Replace rewrites original file with it's fixed version. +func Replace(path string, file *ast.File, fset *token.FileSet, settings Settings) error { + info, err := os.Stat(path) + if err != nil { + return fmt.Errorf("check file: %v", err) + } + mode := info.Mode() + + fixed, err := Fix(path, file, fset, settings) + if err != nil { + return fmt.Errorf("fix issues: %v", err) + } + + if err := ioutil.WriteFile(path, fixed, mode); err != nil { + return fmt.Errorf("write file: %v", err) + } + return nil +} + func check(fset *token.FileSet, group *ast.CommentGroup) (iss Issue, ok bool) { if group == nil || len(group.List) == 0 { return Issue{}, true diff --git a/godot_test.go b/godot_test.go index 557967c..b0de06d 100644 --- a/godot_test.go +++ b/godot_test.go @@ -3,6 +3,7 @@ package godot import ( "go/parser" "go/token" + "io/ioutil" "path/filepath" "strings" "testing" @@ -287,9 +288,9 @@ func TestMakeReplacement(t *testing.T) { } } -func TestIntegration(t *testing.T) { +func TestRunIntegration(t *testing.T) { t.Run("default check", func(t *testing.T) { - var testFile = filepath.Join("testdata", "default", "example.go") + var testFile = filepath.Join("testdata", "default", "check", "main.go") expected, err := readTestFile(testFile) if err != nil { t.Fatalf("Failed to read test file %s: %v", testFile, err) @@ -316,7 +317,7 @@ func TestIntegration(t *testing.T) { }) t.Run("check all", func(t *testing.T) { - var testFile = filepath.Join("testdata", "checkall", "example.go") + var testFile = filepath.Join("testdata", "checkall", "check", "main.go") expected, err := readTestFile(testFile) if err != nil { t.Fatalf("Failed to read test file %s: %v", testFile, err) @@ -345,6 +346,76 @@ func TestIntegration(t *testing.T) { }) } +func TestFixIntegration(t *testing.T) { + t.Run("default check", func(t *testing.T) { + var testFile = filepath.Join("testdata", "default", "check", "main.go") + var expectedFile = filepath.Join("testdata", "default", "result", "main.go") + expected, err := ioutil.ReadFile(expectedFile) // nolint: gosec + if err != nil { + t.Fatalf("Failed to read test file %s: %v", expected, err) + } + + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, testFile, nil, parser.ParseComments) + if err != nil { + t.Fatalf("Failed to parse file %s: %v", testFile, err) + } + + fixed, err := Fix(testFile, file, fset, Settings{CheckAll: false}) + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + fixedLines := strings.Split(string(fixed), "\n") + expectedLines := strings.Split(string(expected), "\n") + if len(fixedLines) != len(expectedLines) { + t.Fatalf("Invalid number of result lines\n expected: %d\n got: %d", + len(expectedLines), len(fixedLines)) + } + for i := range fixedLines { + if fixedLines[i] != expectedLines[i] { + t.Fatalf("Wrong line %d in fixed file\n expected: %s\n got: %s", + i, expectedLines[i], fixedLines[i]) + } + } + }) + + t.Run("check all", func(t *testing.T) { + var testFile = filepath.Join("testdata", "checkall", "check", "main.go") + var expectedFile = filepath.Join("testdata", "checkall", "result", "main.go") + expected, err := ioutil.ReadFile(expectedFile) // nolint: gosec + if err != nil { + t.Fatalf("Failed to read test file %s: %v", expected, err) + } + + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, testFile, nil, parser.ParseComments) + if err != nil { + t.Fatalf("Failed to parse file %s: %v", testFile, err) + } + + fixed, err := Fix(testFile, file, fset, Settings{CheckAll: true}) + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + fixedLines := strings.Split(string(fixed), "\n") + expectedLines := strings.Split(string(expected), "\n") + if len(fixedLines) != len(expectedLines) { + t.Fatalf("Invalid number of result lines\n expected: %d\n got: %d", + len(expectedLines), len(fixedLines)) + } + for i := range fixedLines { + if fixedLines[i] != expectedLines[i] { + t.Fatalf("Wrong line %d in fixed file\n expected: %s\n got: %s", + i, expectedLines[i], fixedLines[i]) + } + } + }) +} + // readTestFile reads comments from file. If comment contains "PASS", // it should not be among issues. If comment contains "FAIL", it should // be among error issues. diff --git a/testdata/checkall/example.go b/testdata/checkall/check/main.go similarity index 100% rename from testdata/checkall/example.go rename to testdata/checkall/check/main.go diff --git a/testdata/checkall/result/main.go b/testdata/checkall/result/main.go new file mode 100644 index 0000000..4fb65ae --- /dev/null +++ b/testdata/checkall/result/main.go @@ -0,0 +1,92 @@ +// Package comment without a period FAIL. +package example + +/* +#include +#include + +void myprint(char* s) { + printf("%d\n", s); +} + +# PASS +*/ +import "C" +import "unsafe" + +//args: tagged comment without period PASS + +// #tag hashtag comment without period PASS + +/* +Multiline comment without a period FAIL. + +*/ + +/* +Multiline comment with a period PASS. +*/ + +/* One-line comment without a period FAIL. */ + +/* One-line comment with a period PASS. */ + +// Single-line comment without a period FAIL. + +// Single-line comment with a period PASS. + +// Declaration comment without a period FAIL. +type SimpleObject struct { + // Exported field comment - always PASS + Type string + // Unexported field comment - always PASS + secret int +} + +// Declaration comment without a period, with an indented code example: +// co := ComplexObject{} +// fmt.Println(co) // PASS +type ComplexObject struct { + // Exported field comment - always PASS + Type string + // Unexported field comment - always PASS + secret int +} + +// Declaration comment without a period, with a mixed indented code example: +// co := Message{} +// fmt.Println(co) // PASS +type Message struct { + Type string +} + +// Declaration multiline comment +// second line +// third line with a period PASS. +func Sum(a, b int) int { + // Inner comment - always PASS + a++ + b++ + + return a + b // inline comment - always PASS +} + +// Declaration multiline comment +// second line +// third line without a period FAIL. +func Mult(a, b int) int { + return a * b +} + +//export CgoExportedFunction PASS +func CgoExportedFunction(a, b int) int { + return a + b +} + +func noComment() { + cs := C.CString("Hello from stdio\n") + C.myprint(cs) + C.free(unsafe.Pointer(cs)) +} + +// Comment with a URL - http://example.com/PASS diff --git a/testdata/default/example.go b/testdata/default/check/main.go similarity index 100% rename from testdata/default/example.go rename to testdata/default/check/main.go diff --git a/testdata/default/result/main.go b/testdata/default/result/main.go new file mode 100644 index 0000000..ff1a0eb --- /dev/null +++ b/testdata/default/result/main.go @@ -0,0 +1,92 @@ +// Package comment without a period PASS +package example + +/* +#include +#include + +void myprint(char* s) { + printf("%d\n", s); +} + +# PASS +*/ +import "C" +import "unsafe" + +//args: tagged comment without period PASS + +// #tag hashtag comment without period PASS + +/* +Multiline comment without a period PASS + +*/ + +/* +Multiline comment with a period PASS. +*/ + +/* One-line comment without a period PASS */ + +/* One-line comment with a period PASS. */ + +// Single-line comment without a period PASS + +// Single-line comment with a period PASS. + +// Declaration comment without a period FAIL. +type SimpleObject struct { + // Exported field comment - always PASS + Type string + // Unexported field comment - always PASS + secret int +} + +// Declaration comment without a period, with an indented code example: +// co := ComplexObject{} +// fmt.Println(co) // PASS +type ComplexObject struct { + // Exported field comment - always PASS + Type string + // Unexported field comment - always PASS + secret int +} + +// Declaration comment without a period, with a mixed indented code example: +// co := Message{} +// fmt.Println(co) // PASS +type Message struct { + Type string +} + +// Declaration multiline comment +// second line +// third line with a period PASS. +func Sum(a, b int) int { + // Inner comment - always PASS + a++ + b++ + + return a + b // inline comment - always PASS +} + +// Declaration multiline comment +// second line +// third line without a period FAIL. +func Mult(a, b int) int { + return a * b +} + +//export CgoExportedFunction PASS +func CgoExportedFunction(a, b int) int { + return a + b +} + +func noComment() { + cs := C.CString("Hello from stdio\n") + C.myprint(cs) + C.free(unsafe.Pointer(cs)) +} + +// Comment with a URL - http://example.com/PASS