diff --git a/.github/golangci.yml b/.github/golangci.yml new file mode 100644 index 0000000..e78d09a --- /dev/null +++ b/.github/golangci.yml @@ -0,0 +1,83 @@ +run: + concurrency: 8 + timeout: 10m + issue-exit-code: 1 + tests: true + skip-dirs-use-default: true + modules-download-mode: readonly + allow-parallel-runners: false + go: "" + +output: + uniq-by-line: false + path-prefix: "" + sort-results: true + +linters: + fast: false + disable-all: true + enable: + - whitespace # Tool for detection of leading and trailing whitespace + - unconvert # Unnecessary type conversions + - tparallel # Detects inappropriate usage of t.Parallel() method in your Go test codes + - thelper # Detects golang test helpers without t.Helper() call and checks the consistency of test helpers + - stylecheck # Stylecheck is a replacement for golint + - prealloc # Finds slice declarations that could potentially be pre-allocated + - predeclared # Finds code that shadows one of Go's predeclared identifiers + - nolintlint # Ill-formed or insufficient nolint directives + - misspell # Misspelled English words in comments + - makezero # Finds slice declarations with non-zero initial length + - importas # Enforces consistent import aliases + - gosec # Security problems + - gofmt # Whether the code was gofmt-ed + - goimports # Unused imports + - goconst # Repeated strings that could be replaced by a constant + - dogsled # Checks assignments with too many blank identifiers (e.g. x, , , _, := f()) + - errname # Checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error + - errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13 + - gofumpt # Stricter gofmt + - unused # Checks Go code for unused constants, variables, functions and types + - gomodguard # Enforces an allow and block list for direct Go module dependencies + - forbidigo # Forbids some custom-set identifiers, like regexp.MatchString + +linters-settings: + gofmt: + simplify: true + goconst: + min-len: 3 + min-occurrences: 3 + gosec: + excludes: + - G204 # Subprocess launched with a potential tainted input or cmd arguments + - G306 # Expect WriteFile permissions to be 0600 or less + stylecheck: + checks: [ "all", "-ST1022", "-ST1003" ] + errorlint: + asserts: false + gocritic: + enabled-tags: + - diagnostic + - experimental + - opinionated + - performance + - style + forbidigo: + forbid: + - p: '^regexp\.(Match|MatchString)$' + msg: it will re-compile the regexp for each execution; compile the regexp with regexp.Compile and store it as a singleton + +issues: + whole-files: true + max-issues-per-linter: 0 + max-same-issues: 0 + new: false + fix: false + exclude-rules: + - path: _test\.go + linters: + - gosec # Disabled linting of weak number generators + - makezero # Disabled linting of intentional slice appends + - goconst # Disabled linting of common mnemonics and test case strings + - path: _\.gno + linters: + - errorlint # Disabled linting of error comparisons, because of lacking std lib support diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..40fc7ec --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,43 @@ +name: "Pull Request Labeler" +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + triage: + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v4 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + configuration-path: .github/labeler.yml + +--- + +A-Action: + - '**/*.yml' + +A-documentation: + - '**/*.md' + - 'internal/lints/README.md' + +A-lint: + - 'internal/lints/**/*' + +T-engine: + - 'internal/engine/**/*' + +T-fixer: + - 'internal/fixer/**/*' + +T-format: + - 'formatter/**/*' + +T-CLI: + - 'cmd/**/*' + +L-test: + - '**/*_test.go' + - 'testdata/**/*' diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml deleted file mode 100644 index 9dffd37..0000000 --- a/.github/workflows/go.yml +++ /dev/null @@ -1,28 +0,0 @@ -# This workflow will build a golang project -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go - -name: Go - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -jobs: - - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: '1.22.X' - - - name: Build - run: go build -v ./... - - - name: Test - run: go test -race -v ./... \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..6e1e558 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,21 @@ +on: + workflow_call: + workflow_dispatch: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: 1.22.x + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Lint + uses: golangci/golangci-lint-action@v6 + with: + args: + --config=./.github/golangci.yml \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..366c53c --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,20 @@ +# This workflow will build a golang project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go + +name: Go + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + + lint: + name: Go linter + uses: ./.github/workflows/lint.yml + + test: + name: Go test + uses: ./.github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..532bcc1 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,32 @@ +on: + workflow_call: + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: 1.22.x + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Go test + run: go test -shuffle=on -coverprofile coverage.out -timeout 5m ./... + + test-with-race: + runs-on: ubuntu-latest + steps: + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: 1.22.x + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Go race test + run: go test -race -shuffle=on -timeout 5m ./... diff --git a/cmd/tlin/main.go b/cmd/tlin/main.go index 3fc9c65..007a817 100644 --- a/cmd/tlin/main.go +++ b/cmd/tlin/main.go @@ -218,7 +218,7 @@ func printIssues(logger *zap.Logger, issues []tt.Issue, isJson bool, jsonOutput issuesByFile[issue.Filename] = append(issuesByFile[issue.Filename], issue) } - var sortedFiles []string + sortedFiles := make([]string, 0, len(issuesByFile)) for filename := range issuesByFile { sortedFiles = append(sortedFiles, filename) } diff --git a/cmd/tlin/main_test.go b/cmd/tlin/main_test.go index 4e3b855..d58d747 100644 --- a/cmd/tlin/main_test.go +++ b/cmd/tlin/main_test.go @@ -205,7 +205,7 @@ func TestRunAutoFix(t *testing.T) { defer os.RemoveAll(tempDir) testFile := filepath.Join(tempDir, "test.go") - err = os.WriteFile(testFile, []byte(sliceRangeIssueExample), 0644) + err = os.WriteFile(testFile, []byte(sliceRangeIssueExample), 0o644) assert.NoError(t, err) expectedIssues := []types.Issue{ @@ -240,7 +240,7 @@ func main() { assert.Contains(t, output, "Fixed issues in") // dry run test - err = os.WriteFile(testFile, []byte(sliceRangeIssueExample), 0644) + err = os.WriteFile(testFile, []byte(sliceRangeIssueExample), 0o644) assert.NoError(t, err) output = captureOutput(t, func() { @@ -299,7 +299,7 @@ func TestRunJsonOutput(t *testing.T) { fmt.Println(tempDir) testFile := filepath.Join(tempDir, "test.go") - err = os.WriteFile(testFile, []byte(sliceRangeIssueExample), 0644) + err = os.WriteFile(testFile, []byte(sliceRangeIssueExample), 0o644) assert.NoError(t, err) expectedIssues := []types.Issue{ @@ -320,18 +320,8 @@ func TestRunJsonOutput(t *testing.T) { runNormalLintProcess(ctx, logger, mockEngine, []string{testFile}, true, jsonOutput) } -func createTempFiles(t *testing.T, dir string, fileNames ...string) []string { - var paths []string - for _, fileName := range fileNames { - filePath := filepath.Join(dir, fileName) - _, err := os.Create(filePath) - assert.NoError(t, err) - paths = append(paths, filePath) - } - return paths -} - func createTempFileWithContent(t *testing.T, content string) string { + t.Helper() tempFile, err := os.CreateTemp("", "test*.go") assert.NoError(t, err) defer tempFile.Close() @@ -342,7 +332,8 @@ func createTempFileWithContent(t *testing.T, content string) string { return tempFile.Name() } -func captureOutput(_ *testing.T, f func()) string { +func captureOutput(t *testing.T, f func()) string { + t.Helper() oldStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w diff --git a/internal/analysis/cfg/builder.go b/internal/analysis/cfg/builder.go index 3304ce4..40ac402 100644 --- a/internal/analysis/cfg/builder.go +++ b/internal/analysis/cfg/builder.go @@ -179,7 +179,7 @@ func (b *builder) buildLoop(stmt ast.Stmt) { // flows as such (range same w/o init & post): // previous -> [ init -> ] for -> body -> [ post -> ] for -> next - var post ast.Stmt = stmt // post in for loop, or for stmt itself; body flows to this + post := stmt // post in for loop, or for stmt itself; body flows to this switch stmt := stmt.(type) { case *ast.ForStmt: diff --git a/internal/analysis/cfg/cfg_test.go b/internal/analysis/cfg/cfg_test.go index 1d38682..39e9b6e 100644 --- a/internal/analysis/cfg/cfg_test.go +++ b/internal/analysis/cfg/cfg_test.go @@ -828,6 +828,7 @@ type CFGWrapper struct { // w/ some other convenient fields for printing in test // cases when need be... func getWrapper(t *testing.T, str string) *CFGWrapper { + t.Helper() fset := token.NewFileSet() f, err := parser.ParseFile(fset, "", str, 0) if err != nil { @@ -891,6 +892,7 @@ func expectFromMaps(actual, exp map[ast.Stmt]struct{}) (dnf, found map[ast.Stmt] } func (c *CFGWrapper) expectDefers(t *testing.T, exp ...int) { + t.Helper() actualDefers := make(map[ast.Stmt]struct{}) for _, d := range c.cfg.Defers { actualDefers[d] = struct{}{} @@ -909,6 +911,7 @@ func (c *CFGWrapper) expectDefers(t *testing.T, exp ...int) { } func (c *CFGWrapper) expectSuccs(t *testing.T, s int, exp ...int) { + t.Helper() if _, ok := c.cfg.blocks[c.exp[s]]; !ok { t.Error("did not find parent", s) return @@ -933,6 +936,7 @@ func (c *CFGWrapper) expectSuccs(t *testing.T, s int, exp ...int) { } func (c *CFGWrapper) expectPreds(t *testing.T, s int, exp ...int) { + t.Helper() if _, ok := c.cfg.blocks[c.exp[s]]; !ok { t.Error("did not find parent", s) } @@ -969,9 +973,8 @@ func TestPrintDot(t *testing.T) { c.cfg.PrintDot(&buf, c.fset, func(s ast.Stmt) string { if _, ok := s.(*ast.AssignStmt); ok { return "!" - } else { - return "" } + return "" }) dot := buf.String() @@ -987,10 +990,18 @@ splines="ortho"; } // The order of the three lines may vary (they're from a map), so // just make sure all three lines appear somewhere - for _, re := range expected { - ok, _ := regexp.MatchString(re, dot) - if !ok { - t.Fatalf("[%s]", dot) + regexps := make([]*regexp.Regexp, len(expected)) + for i, re := range expected { + var err error + regexps[i], err = regexp.Compile(re) + if err != nil { + t.Fatalf("Failed to compile regex: %v", err) + } + } + + for i, re := range regexps { + if !re.MatchString(dot) { + t.Fatalf("Expected pattern not found: [%s] in [%s]", expected[i], dot) } } } diff --git a/internal/analysis/cfg/doc.go b/internal/analysis/cfg/doc.go index a9c2f63..d58df8b 100644 --- a/internal/analysis/cfg/doc.go +++ b/internal/analysis/cfg/doc.go @@ -1,6 +1,6 @@ // # Description // -// Pacakge cfg provides functionality to generate and analyze Control Frow Graph (CFG) for go-like grammar languages. +// Package cfg provides functionality to generate and analyze Control Frow Graph (CFG) for go-like grammar languages. // // ## Control Flow Graph (CFG) // diff --git a/internal/engine_test.go b/internal/engine_test.go index a4f9811..2a4100d 100644 --- a/internal/engine_test.go +++ b/internal/engine_test.go @@ -14,20 +14,14 @@ import ( // createTempDir creates a temporary directory and returns its path. // It also registers a cleanup function to remove the directory after the test. -func createTempDir(t testing.TB, prefix string) string { +func createTempDir(tb testing.TB, prefix string) string { + tb.Helper() tempDir, err := os.MkdirTemp("", prefix) - require.NoError(t, err) - t.Cleanup(func() { os.RemoveAll(tempDir) }) + require.NoError(tb, err) + tb.Cleanup(func() { os.RemoveAll(tempDir) }) return tempDir } -// assertFileExists checks if a file exists and has the expected content. -func assertFileExists(t *testing.T, path string, expectedContent string) { - content, err := os.ReadFile(path) - assert.NoError(t, err) - assert.Equal(t, expectedContent, string(content)) -} - func TestNewEngine(t *testing.T) { t.Parallel() @@ -70,6 +64,7 @@ func TestEngine_PrepareFile(t *testing.T) { engine := &Engine{} t.Run("Go file", func(t *testing.T) { + t.Parallel() goFile := "test.go" result, err := engine.prepareFile(goFile) assert.NoError(t, err) @@ -77,10 +72,11 @@ func TestEngine_PrepareFile(t *testing.T) { }) t.Run("Gno file", func(t *testing.T) { + t.Parallel() tempDir := createTempDir(t, "gno_test") gnoFile := filepath.Join(tempDir, "test.gno") - err := os.WriteFile(gnoFile, []byte("package main"), 0644) + err := os.WriteFile(gnoFile, []byte("package main"), 0o644) require.NoError(t, err) result, err := engine.prepareFile(gnoFile) @@ -111,7 +107,7 @@ func TestReadSourceCode(t *testing.T) { testFile := filepath.Join(tempDir, "test.go") content := "package main\n\nfunc main() {\n\tprintln(\"Hello, World!\")\n}" - err := os.WriteFile(testFile, []byte(content), 0644) + err := os.WriteFile(testFile, []byte(content), 0o644) require.NoError(t, err) sourceCode, err := ReadSourceCode(testFile) @@ -165,7 +161,8 @@ func BenchmarkCreateTempGoFile(b *testing.B) { } func BenchmarkRun(b *testing.B) { - _, currentFile, _, _ := runtime.Caller(0) + _, currentFile, _, ok := runtime.Caller(0) + require.True(b, ok) testDataDir := filepath.Join(filepath.Dir(currentFile), "../testdata") engine, err := NewEngine(testDataDir, nil) diff --git a/internal/fixer/fixer.go b/internal/fixer/fixer.go index c52599c..2d864cb 100644 --- a/internal/fixer/fixer.go +++ b/internal/fixer/fixer.go @@ -71,7 +71,7 @@ func (f *Fixer) Fix(filename string, issues []tt.Issue) error { return fmt.Errorf("failed to format file: %w", err) } - err = os.WriteFile(filename, buf.Bytes(), 0644) + err = os.WriteFile(filename, buf.Bytes(), 0o644) if err != nil { return fmt.Errorf("failed to write file: %w", err) } @@ -82,7 +82,7 @@ func (f *Fixer) Fix(filename string, issues []tt.Issue) error { return nil } -func (c *Fixer) extractIndent(line string) string { +func (f *Fixer) extractIndent(line string) string { return line[:len(line)-len(strings.TrimLeft(line, " \t"))] } diff --git a/internal/fixer/fixer_test.go b/internal/fixer/fixer_test.go index d9d934e..1dd99be 100644 --- a/internal/fixer/fixer_test.go +++ b/internal/fixer/fixer_test.go @@ -220,6 +220,7 @@ func main() { } func runTestCase(t *testing.T, input string, issues []tt.Issue, expected string, dryRun bool) { + t.Helper() _, testFile, cleanup := setupTestFile(t, input) defer cleanup() @@ -238,11 +239,12 @@ func runTestCase(t *testing.T, input string, issues []tt.Issue, expected string, } func setupTestFile(t *testing.T, content string) (string, string, func()) { + t.Helper() tmpDir, err := os.MkdirTemp("", "autofixer-test") require.NoError(t, err) testFile := filepath.Join(tmpDir, "test.go") - err = os.WriteFile(testFile, []byte(content), 0644) + err = os.WriteFile(testFile, []byte(content), 0o644) require.NoError(t, err) cleanup := func() { diff --git a/internal/lints/cyclomatic_complexity.go b/internal/lints/cyclomatic_complexity.go index 0d5bd8b..c4cddd2 100644 --- a/internal/lints/cyclomatic_complexity.go +++ b/internal/lints/cyclomatic_complexity.go @@ -30,7 +30,6 @@ func DetectHighCyclomaticComplexity(filename string, threshold int) ([]tt.Issue, for _, stat := range stats { if stat.Complexity > threshold { - funcNode, ok := funcNodes[stat.FuncName] if !ok { continue diff --git a/internal/lints/default_golangci.go b/internal/lints/default_golangci.go index 0c4716d..770baaf 100644 --- a/internal/lints/default_golangci.go +++ b/internal/lints/default_golangci.go @@ -49,7 +49,7 @@ func RunGolangciLint(filename string) ([]tt.Issue, error) { // when source code contains gno package imports (i.e. p/demo, r/demo, std). [07/25/24] json.Unmarshal(output, &golangciResult) - var issues []tt.Issue + issues := make([]tt.Issue, 0, len(golangciResult.Issues)) for _, gi := range golangciResult.Issues { issues = append(issues, tt.Issue{ Rule: gi.FromLinter, diff --git a/internal/lints/detect_cycles.go b/internal/lints/detect_cycles.go index bbc3539..bb31470 100644 --- a/internal/lints/detect_cycles.go +++ b/internal/lints/detect_cycles.go @@ -12,7 +12,7 @@ func DetectCycle(filename string, node *ast.File, fset *token.FileSet) ([]tt.Iss c := newCycle() cycles := c.detectCycles(node) - var issues []tt.Issue + issues := make([]tt.Issue, 0, len(cycles)) for _, cycle := range cycles { issue := tt.Issue{ Rule: "cycle-detection", diff --git a/internal/lints/lint_test.go b/internal/lints/lint_test.go index 7dd199f..5c9bc40 100644 --- a/internal/lints/lint_test.go +++ b/internal/lints/lint_test.go @@ -326,7 +326,8 @@ func main() { func TestDetectEmitFormat(t *testing.T) { t.Parallel() - _, current, _, _ := runtime.Caller(0) + _, current, _, ok := runtime.Caller(0) + require.True(t, ok) testDir := filepath.Join(filepath.Dir(current), "..", "..", "testdata", "emit") tests := []struct { diff --git a/internal/lints/missing_package_mod_test.go b/internal/lints/missing_package_mod_test.go index 4ad47c1..eccfb38 100644 --- a/internal/lints/missing_package_mod_test.go +++ b/internal/lints/missing_package_mod_test.go @@ -98,11 +98,11 @@ func TestDetectMissingPackageInMod(t *testing.T) { defer os.RemoveAll(tmpDir) gnoFile := filepath.Join(tmpDir, "main.gno") - err = os.WriteFile(gnoFile, []byte(tt.gnoContent), 0644) + err = os.WriteFile(gnoFile, []byte(tt.gnoContent), 0o644) require.NoError(t, err) modFile := filepath.Join(tmpDir, "gno.mod") - err = os.WriteFile(modFile, []byte(tt.modContent), 0644) + err = os.WriteFile(modFile, []byte(tt.modContent), 0o644) require.NoError(t, err) fset := token.NewFileSet() diff --git a/internal/lints/repeated_regex_compilation.go b/internal/lints/repeated_regex_compilation.go index afb39df..96cd1e9 100644 --- a/internal/lints/repeated_regex_compilation.go +++ b/internal/lints/repeated_regex_compilation.go @@ -52,7 +52,7 @@ func runAnalyzer(filename string, a *analysis.Analyzer) ([]tt.Issue, error) { return nil, err } - var issues []tt.Issue + issues := make([]tt.Issue, 0, len(diagnostics)) for _, diag := range diagnostics { issues = append(issues, tt.Issue{ Rule: a.Name, diff --git a/internal/lints/simplify_slice_expr.go b/internal/lints/simplify_slice_expr.go index 87043c5..e0e5bb6 100644 --- a/internal/lints/simplify_slice_expr.go +++ b/internal/lints/simplify_slice_expr.go @@ -1,3 +1,4 @@ +//nolint:goconst package lints import ( diff --git a/internal/symbol_table_test.go b/internal/symbol_table_test.go index 8a0b84c..6756a28 100644 --- a/internal/symbol_table_test.go +++ b/internal/symbol_table_test.go @@ -15,7 +15,9 @@ func TestSymbolTable(t *testing.T) { t.Parallel() tmpDir, err := os.MkdirTemp("", "symboltable-test") require.NoError(t, err) - defer os.RemoveAll(tmpDir) + t.Cleanup(func() { + os.RemoveAll(tmpDir) + }) // generate test files file1Content := `package test diff --git a/lint/lint_test.go b/lint/lint_test.go index 86d1081..f7f35ff 100644 --- a/lint/lint_test.go +++ b/lint/lint_test.go @@ -209,7 +209,8 @@ func TestHasDesiredExtension(t *testing.T) { } func createTempFiles(t *testing.T, dir string, fileNames ...string) []string { - var paths []string + t.Helper() + paths := make([]string, 0, len(fileNames)) for _, fileName := range fileNames { filePath := filepath.Join(dir, fileName) _, err := os.Create(filePath)