Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial / start of implementation #1

Merged
merged 17 commits into from
Mar 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates

version: 2
updates:
- package-ecosystem: "gomod"
directory: "/" # Location of package manifests
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
# Check for updates to GitHub Actions every week
interval: "weekly"
32 changes: 32 additions & 0 deletions .github/workflows/include.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Remember to change/update `description` below when copying
# this include
name: "Shared cli/server fortio workflows"
on:
push:
branches: [ main ]
tags:
# so a vX.Y.Z-test1 doesn't trigger build
- 'v[0-9]+.[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+-pre*'
pull_request:
branches: [ main ]

jobs:
call-gochecks:
uses: fortio/workflows/.github/workflows/gochecks.yml@main
call-codecov:
uses: fortio/workflows/.github/workflows/codecov.yml@main
call-codeql:
uses: fortio/workflows/.github/workflows/codeql-analysis.yml@main
permissions:
actions: read
contents: read
security-events: write
call-releaser:
uses: fortio/workflows/.github/workflows/releaser.yml@main
with:
description: "Fix line too long lll linter errors"
secrets:
GH_PAT: ${{ secrets.GH_PAT }}
DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}
DOCKER_USER: ${{ secrets.DOCKER_USER }}
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
.golangci.yml
.goreleaser.yaml
*.lll
*.bak
test.txt
lll-fixer
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
Expand Down
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
FROM scratch
COPY lll-fixer /usr/bin/lll-fixer
ENTRYPOINT ["/usr/bin/lll-fixer"]
CMD ["help"]
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

manual-test:
cp lll_fixer.go test.txt && go run . -loglevel debug test.txt ; colordiff -u lll_fixer.go test.txt


.PHONY: manual-test
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,41 @@
# lll-fixer
Fix lll (line length limit) lines too long linter errors in go files

## Installation

From source
```
go install github.com/ldemailly/lll-fixer@latest
```

Or see the numerous binaries in https://github.com/ldemailly/lll-fixer/releases

Or docker `fortio/lll-fixer:latest`

Or brew `brew install fortio/tap/lll-fixer`

(I manage the fortio org and usually put everything there but this one is a bit unrelated so for now it is hosted here in `ldemailly` yet uses fortio's org for docker and brew)

## Example

Test on itself:
```
$ go run . lll_fixer.go
```

```diff
diff --git a/lll_fixer.go b/lll_fixer.go
index c5edf97..30452c3 100644
--- a/lll_fixer.go
+++ b/lll_fixer.go
@@ -43,7 +43,8 @@ func main() {
}
}

-// process modifies the file filename to split long comments at maxlen. making this line longer than 80 characters to test.
+// process modifies the file filename to split long comments at maxlen. making
+// this line longer than 80 characters to test.
func process(fset *token.FileSet, filename string, maxlen int) string {
log.Infof("Processing file %q", filename)
// Parse the Go file
```
44 changes: 44 additions & 0 deletions bug_repro/bug.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package main

import (
"fmt"
"go/format"
"go/parser"
"go/token"
"os"
)

/*
* multi line comment.
*/
func main() {
code := `package main

/*
* multi line comment.
*/
func main() {
}`
fmt.Println("---input---")
fmt.Println(code)
fmt.Println("---processing---")
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, "bug.go", code, parser.ParseComments)
if err != nil {
panic(err)
}
for _, cg := range node.Comments {
for _, c := range cg.List {
fmt.Printf("Found comment %q\n", c.Text)
if len(c.Text) > 11 {
fmt.Printf("Splitting comment %q\n", c.Text)
c.Text = c.Text[:11] + "\n *" + c.Text[11:]
fmt.Printf("into -> %q\n", c.Text)
}
}
}
fmt.Println("---result---")
if err := format.Node(os.Stdout, fset, node); err != nil {
panic(err)
}
}
13 changes: 13 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module github.com/ldemailly/lll-fixer

go 1.21

require (
fortio.org/cli v1.5.1
fortio.org/log v1.12.0
)

require (
fortio.org/struct2env v0.4.0 // indirect
fortio.org/version v1.0.3 // indirect
)
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
fortio.org/cli v1.5.1 h1:lqPvkxRVSajsVwLfblaN62BPAICPp05Oab+yjRvI3DU=
fortio.org/cli v1.5.1/go.mod h1:Tp7AypudP1mJomTUN/J/vlOTlZDWTMsok09MMyA99ow=
fortio.org/log v1.12.0 h1:5Yg4pL9Pp0jcWeJYixm2xikMCldVaSDMgDFDmQJZfho=
fortio.org/log v1.12.0/go.mod h1:1tMBG/Elr6YqjmJCWiejJp2FPvXg7/9UAN0Rfpkyt1o=
fortio.org/struct2env v0.4.0 h1:k5alSOTf3YHiB3MuacjDHQ3YhVWvNZ95ZP/a6MqvyLo=
fortio.org/struct2env v0.4.0/go.mod h1:lENUe70UwA1zDUCX+8AsO663QCFqYaprk5lnPhjD410=
fortio.org/version v1.0.3 h1:5gJ3plj6isAOMq52cI5ifo4cC+QHmJF76Wevc5Cp4x0=
fortio.org/version v1.0.3/go.mod h1:2JQp9Ax+tm6QKiGuzR5nJY63kFeANcgrZ0osoQFDVm0=
147 changes: 147 additions & 0 deletions lll_fixer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package main

import (
"flag"
"go/ast"
"go/format"
"go/parser"
"go/token"
"os"
"os/exec"
"strings"

"fortio.org/cli"
"fortio.org/log"
)

func main() {
maxlen := flag.Int("len", 79, "max line length")
funmpt := flag.Bool("fumpt", false, "run gofumpt on the modified file")
cli.MinArgs = 1
cli.MaxArgs = -1
cli.ArgsHelp = "filenames..."
if false {
// just to test literal string split
cli.ArgsHelp = "this is a very a long string literal to test the split of long strings inside code"
}
cli.Main()
fset := token.NewFileSet()
for _, filename := range flag.Args() {
newname := process(fset, filename, *maxlen)
// swap .lll to .go and .go to .bak
backup := filename + ".bak"
if err := os.Rename(filename, backup); err != nil {
log.Fatalf("Error renaming file %q to %q: %v", filename, backup, err)
}
log.Infof("Renamed file %q to %q", filename, backup)
if err := os.Rename(newname, filename); err != nil {
log.Fatalf("Error renaming file %q to %q: %v", newname, filename, err)
}
log.Infof("Renamed file %q to %q", newname, filename)
// Run gofumpt on the modified file
if *funmpt {
cmd := exec.Command("gofumpt", "-w", filename)
if err := cmd.Run(); err != nil {
log.Errf("Error running gofumpt: %v", err)
return
}
log.Infof("Ran gofumpt on the now modified file %q", filename)

Check warning on line 48 in lll_fixer.go

View check run for this annotation

Codecov / codecov/patch

lll_fixer.go#L17-L48

Added lines #L17 - L48 were not covered by tests
}
}
}

func lineLead(s string) string {
i := strings.LastIndex(s, "\n")
return s[i+1 : i+4] // will break with -len too low
}

/*
* Multi line comment with one line longer than 80 characters to test the split of multi line comments.
*/
func splitAtWord(s string, maxlen int) string {
if len(s) <= maxlen {
return s
}
// find the last space before maxlen
i := strings.LastIndex(s[:maxlen], " ")
nospace := (i == -1)
if nospace {
// no space found, split at maxlen
log.Warnf("No word/space found in first %d characters for %q", maxlen, s)
i = maxlen
}
start := s[:i]
lead := lineLead(start)
var mid string
switch {
case strings.HasPrefix(lead, "/* "):
mid = "\n * "
case strings.HasPrefix(lead, " * "):
mid = "\n * "
case strings.HasPrefix(lead, "// "):
mid = "\n// "
case strings.HasPrefix(lead, "\""):
mid = "\" +\n\t\"" // for string literals splitting
if !nospace {
mid += " "
}
default:
log.Warnf("Unexpected lead %q", lead)
mid = "\n "
}
log.Debugf("Start lead is %q", lead)
return strings.TrimSpace(s[:i]) + mid + strings.TrimLeft(s[i:], " ")
}

// TODO process other nodes (and also maybe split leftmost position in line vs length of comment/literal
// which could be far to the right)

func processNode(n ast.Node, maxlen int) bool {
if false {
log.Debugf("Found node: %+v to shrink to %d", n, maxlen)
}

Check warning on line 102 in lll_fixer.go

View check run for this annotation

Codecov / codecov/patch

lll_fixer.go#L99-L102

Added lines #L99 - L102 were not covered by tests
// process string literals
if lit, ok := n.(*ast.BasicLit); ok && lit.Kind == token.STRING {
lit.Value = splitAtWord(lit.Value, maxlen)
}

Check warning on line 106 in lll_fixer.go

View check run for this annotation

Codecov / codecov/patch

lll_fixer.go#L104-L106

Added lines #L104 - L106 were not covered by tests
// more nodes...
return true

Check warning on line 108 in lll_fixer.go

View check run for this annotation

Codecov / codecov/patch

lll_fixer.go#L108

Added line #L108 was not covered by tests
}

// process modifies the file filename to split long comments at maxlen. making this line longer than 80 characters to test.
func process(fset *token.FileSet, filename string, maxlen int) string {
log.Infof("Processing file %q", filename)
// Parse the Go file and this is an indented comment to test the split past column 80.
node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
if err != nil {
log.Fatalf("Error parsing %q: %v", filename, err)
return "error.lll" // unreachable
}
for _, cg := range node.Comments {
for _, c := range cg.List {
log.Debugf("Found comment %q", c.Text)
if len(c.Text) > maxlen {
log.LogVf("Splitting comment %q", c.Text)
c.Text = splitAtWord(c.Text, maxlen)
log.LogVf("into -> %q", c.Text)
}

Check warning on line 127 in lll_fixer.go

View check run for this annotation

Codecov / codecov/patch

lll_fixer.go#L112-L127

Added lines #L112 - L127 were not covered by tests
}
}
// Traverse and modify the AST
ast.Inspect(node, func(n ast.Node) bool {
return processNode(n, maxlen)
})

Check warning on line 133 in lll_fixer.go

View check run for this annotation

Codecov / codecov/patch

lll_fixer.go#L131-L133

Added lines #L131 - L133 were not covered by tests

// Generate the modified code
newname := filename + ".lll"
f, err := os.Create(newname)
if err != nil {
log.Errf("Error creating modified file %q: %v", newname, err)
}
defer f.Close()
if err := format.Node(f, fset, node); err != nil {
log.Errf("Error outputting modified file: %v", err)
}
log.Infof("Modified file written to %q", newname)
return newname

Check warning on line 146 in lll_fixer.go

View check run for this annotation

Codecov / codecov/patch

lll_fixer.go#L136-L146

Added lines #L136 - L146 were not covered by tests
}
50 changes: 50 additions & 0 deletions lll_fixer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package main

import (
"testing"
)

func TestLineLead(t *testing.T) {
// tests input vs actual struct:
tests := []struct {
input string
expected string
}{
{"Hello", "Hel"},
{"\nHello", "Hel"},
{"abc\nxyz\nTest", "Tes"},
}
// loop through the tests
for _, test := range tests {
actual := lineLead(test.input)
// compare the actual vs expected
if actual != test.expected {
t.Errorf("Test failed! Expected: %q, Actual: %q", test.expected, actual)
}
}
}

func TestSplitAtWord(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"Hello", "Hello"},
{"123456789ABC", "123456789A\n BC"},
{"// abc 1234567890", "// abc\n// 1234567890"},
{"/* abc 1234567890 */", "/* abc\n * 1234567890 */"},
{"/*\n * abc 1234567890\n*/", "/*\n * abc\n * 1234567890\n*/"},
{`"abc 1234567890"`, `"abc" +
" 1234567890"`},
{`"123456789ABC"`, `"123456789" +
"ABC"`},
}
// loop through the tests
for _, test := range tests {
actual := splitAtWord(test.input, 10)
// compare the actual vs expected
if actual != test.expected {
t.Errorf("Test failed! Expected: %q, Actual: %q", test.expected, actual)
}
}
}
Loading